Compare commits

...
Author SHA1 Message Date
teahubxyzandClaude Fable 5.1 458ac0484e changelog: Base: branches, a launched worker finishes, oldest card first
ci / test (pull_request) Successful in 34s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-18 04:24:34 +00:00
teahubxyzandClaude Fable 5.1 de3857aa93 tests: a card's Base: branch (14b); the cap gates launches, a run under way finishes, open PRs get reviewed, oldest first (57)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-18 04:24:14 +00:00
teahubxyzandClaude Fable 5.1 a5249ce8db worker: build where the card says (Base: <branch>), finish what it starts (TIME_CAP_SLACK), oldest card first
A `Base: <branch>` line in a card makes the seat worker cut its branch from that base (named time/card-N-<base>), see
that branch's files and open the PR against it: every venture card lives on gh-pages and until now the worker always
branched from main, which is why #60, #63, #95 ran out of turns and #109 targeted the wrong branch.

The daily spend cap gates launches; a run that began within budget may finish, up to TIME_CAP_SLACK x the cap (1.5),
and a PR already open gets its review on a capped day too (#87's run was stopped at step 27 with the spend lost). The
monthly cap stays hard. The tick takes the oldest ready card first, as promote_next does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-18 04:23:25 +00:00
time 183e09332d ledger: 2 calls, $0.00 2026-10-18 04:16:22 +00:00
time 97e863b9fb ledger: 31 calls, $3.10 2026-10-18 04:14:42 +00:00
time 999abbf590 ledger: 95 calls, $5.01
time/worker-run cat > /tmp/p.py <<'EOF' import re p = ".github/time/time.py" s = open(p).read() old = ''' gh(
2026-10-18 02:10:37 +00:00
teahubxyzandClaude Fable 5.1 4f8eda2483 labs/aicharts: fast mode (Clef first, LLM checks) as demo default (#42)
ci / test (push) Successful in 34s
time/worker-run curl -sS -m 15 https://docs.whop.com/.well-known/skills/index.json | head -c 1500; echo; curl -sS -m
labs/aicharts: fast mode (Clef first, LLM checks) as demo default (#42)

Opened 2026-10-14 by a Claude Code session under the owner's login, which is why it read as the operator's PR for three days. Lab-only and reversible: Clef-flash draws first, the LLM checks in parallel, the chart is redrawn only on disagreement; API callers keep the LLM default. The lab's tests pass on the branch (run 652, "aicharts: all good"); its deploy step cannot run from prod, which holds no Cloudflare secrets, so nothing is deployed by this merge.

Squash-merged by the CEO session on the owner's explicit instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-18 00:12:54 +00:00
teahubxyzandClaude Fable 5.1 b539fff1b2 heartbeat: per-seat tokens for haiku, sonnet, opus and fable
ci / test (push) Successful in 35s
One Gitea user per seat (model line); the reviewer's verdict becomes a real review by that seat's own user once #76 lands. TIME_SEAT_CLAUDE stays until then as the umbrella.

On the owner's explicit instruction (CEO session).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 22:54:56 +00:00
time 0d893d7fc5 ledger: 40 calls, $2.52 2026-10-17 20:24:01 +00:00
teahubxyzandClaude Fable 5.1 5c8254cc50 TIME_PROTECTED governs every protected path except the workflow directories (#81)
ci / test (push) Successful in 34s
TIME_PROTECTED governs every protected path except the workflow directories (#81)

touches_protected() hard-coded all of .github/ and .gitea/ on top of TIME_PROTECTED, so the engine under .github/time/ could never land without a human. Only the workflow directories stay hard-coded; DEFAULT_PROTECTED is unchanged. With this repo's TIME_PROTECTED narrowed, card PRs under .github/time/ with a trusted Approve and green checks merge via the time bot.

CI green on 3b757fa. Squash-merged by the CEO session on the owner's explicit instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 20:20:12 +00:00
teahubxyzandClaude Fable 5.1 b53016007a heartbeat: 55-minute job, and the claude seat's token for reviews
ci / test (push) Successful in 34s
The job timeout moves from 20 to 55 minutes: workers can overlap inside one run (#75), and at 20 every in-flight worker died together. TIME_SEAT_CLAUDE reaches the heartbeat so the reviewer can post as the `claude` user once #76 lands; until then it is unused.

Both on the owner's explicit instruction (CEO session).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 20:16:32 +00:00
timeandClaude Fable 5.1 449f89fc38 Reviewer rule wording; no duplicate "Closes #N" in PR bodies (#74)
ci / test (push) Successful in 35s
Reviewer rule wording; no duplicate "Closes #N" in PR bodies (#74)

Reviewer Approve (haiku) on the diff, CI green on 16b369dd. Engine-only change for card #58; squash-merged by the CEO session on the owner's explicit instruction.

Closes #58

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-17 20:13:25 +00:00
time 5e2d211609 ledger: 10 calls, $0.13 2026-10-17 18:28:11 +00:00
time fb1df02f64 ledger: 16 calls, $1.65
time/worker-run cd .github/time; python3 - <<'E' s=open('time.py').read() s=s.replace(''' body = f"{a['su
2026-10-17 18:23:44 +00:00
8 changed files with 366 additions and 28 deletions

No files matched your search

+28 -2
View File
@@ -169,7 +169,8 @@ export function attach(chart, { data, schema, endpoint, dataset }) {
const Base = globalThis.HTMLElement ?? class {};
// <ai-chart endpoint="https://..." dataset="orders" brain="jev"></ai-chart> brain: llm | jev | clef (server default if unset)
// <ai-chart endpoint="https://..." dataset="orders" brain="fast"></ai-chart>
// brain: fast (decision model first, LLM checks) | llm | jev | clef (server default if unset)
// Needs Chart.js on the page (window.Chart). Fires "spec" after each answer and "save" when Save is pressed; set
// element.onSave = async ({ spec, prompt, dataset }) => ... to store charts yourself (the demo stores them in D1).
export class AiChart extends Base {
@@ -202,7 +203,9 @@ export class AiChart extends Base {
try {
await this.load();
const brain = this.getAttribute("brain");
const { spec, ...info } = await fetchSpec(this.endpoint, { dataset: this.dataset, prompt, prior: this._spec, ...(brain ? { brain } : {}) });
const body = { dataset: this.dataset, prompt, prior: this._spec };
if (brain === "fast") return await this.askFast(body);
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, ...(brain ? { brain } : {}) });
await this.render(spec, prompt);
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt, info } }));
} catch (e) {
@@ -210,6 +213,29 @@ export class AiChart extends Base {
}
}
// brain="fast": a decision model draws first (under a second), an LLM answers in the background, and the chart is
// redrawn only if the LLM's chart differs. Whichever comes back alone still draws.
async askFast(body) {
const ask = ++this._asks || (this._asks = 1);
const current = () => ask === this._asks;
const same = (a, b) => JSON.stringify({ ...a, title: 0 }) === JSON.stringify({ ...b, title: 0 });
const llm = fetchSpec(this.endpoint, { ...body, brain: "llm" }).catch((e) => ({ error: e }));
let drawn = null;
try {
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, brain: "clef", floor: 0 });
if (!current()) return;
await this.render(spec, body.prompt);
drawn = spec;
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: "fast" } } }));
} catch { /* the LLM will answer */ }
const out = await llm;
if (!current() || out.error) { if (!drawn && out.error) throw out.error; return; }
const { spec, ...info } = out;
const changed = !drawn || !same(drawn, spec);
if (changed) await this.render(spec, body.prompt);
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: drawn ? (changed ? "corrected" : "confirmed") : "llm" } } }));
}
async render(spec, prompt) {
await this.load();
const v = validateSpec(spec, this._schema);
+6 -4
View File
@@ -52,16 +52,18 @@ export function questions(info) {
x: choice("Which column goes on the x axis (the categories or time the chart is broken down by)?", axis),
measure: choice("Which quantity does the chart measure?", measure),
agg: choice("How should that quantity be combined within each group?", AGG_MENU),
series: choice("Should the chart split into one coloured series per value of another column? Pick none unless the request compares groups against each other (\"web vs store\", \"by region over time\").",
{ none: "One series only.", ...Object.fromEntries(text.map((c) => [c.name, `one series per ${c.name}`])) }),
series: choice("Does the request compare two or more values of a column side by side (\"web vs store\", \"split by channel\", \"each region over time\")? If so, pick that column; it becomes one coloured line or bar per value. Otherwise none.",
{ none: "No comparison asked for: a single series.",
...Object.fromEntries(text.map((c) => [c.name, `Compare ${c.name}s (${(info.aggregates[c.name]?.values || []).join(" vs ")}): one series per ${c.name}.`])) }),
order: choice("How should the groups be ordered?", ORDER_MENU),
top: choice("Does the request limit how many groups to show?", TOP_MENU),
};
for (const c of text) {
const vals = info.aggregates[c.name]?.values;
if (vals?.length && vals.length <= 30)
q[`only.${c.name}`] = choice(`Does the request restrict the data to one ${c.name}? Pick any unless it names one.`,
{ any: `No restriction on ${c.name}.`, ...Object.fromEntries(vals.map((v) => [v, `Only ${c.name} = ${v}.`])) });
q[`only.${c.name}`] = choice(`Does the request name exactly one ${c.name} to look at (for example "${vals[0]} sales" or "in the ${vals[0]}")? Pick that value; pick any if it names none, or names several to compare.`,
{ any: `The request names no single ${c.name}.`,
...Object.fromEntries(vals.map((v) => [v, `The request mentions "${v}": keep only rows where ${c.name} is ${v}.`])) });
}
return q;
}
+4 -3
View File
@@ -287,8 +287,8 @@ h2{font-size:18px;margin:28px 0 8px}.row{display:flex;flex-wrap:wrap;gap:8px;ali
</style></head><body><main>
<h1>AICharts</h1>
<p class="lede">Ask for a chart in plain words. The model writes a small spec that is checked before anything draws, and never sees the rows: only column names, five sample rows and value ranges. Data: a sample tea shop, 24 months of orders.</p>
<div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="">default</option><option value="jev">Jev</option><option value="clef">Clef</option><option value="llm">LLM</option></select></label></div>
<ai-chart dataset="orders"></ai-chart>
<div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="fast" selected>Fast (Clef, LLM checks)</option><option value="clef">Clef only</option><option value="jev">Jev</option><option value="llm">LLM only</option></select></label></div>
<ai-chart dataset="orders" brain="fast"></ai-chart>
<div class="meta" id="meta"></div>
<details><summary>Spec</summary><pre id="spec">Ask something first.</pre></details>
<h2>Saved charts</h2><ul id="saved"><li>None yet.</li></ul>
@@ -310,7 +310,8 @@ brainSel.onchange = () => brainSel.value ? el.setAttribute("brain", brainSel.val
el.addEventListener("spec", (e) => {
const { spec, info = {} } = e.detail;
document.getElementById("spec").textContent = JSON.stringify({ ...info, spec }, null, 2);
const bits = [info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""),
const stage = { fast: "first draw", confirmed: "LLM agrees", corrected: "corrected by the LLM" }[info.stage];
const bits = [stage, info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""),
info.ms != null && info.ms + " ms", info.confidence != null && "confidence " + info.confidence].filter(Boolean);
meta.replaceChildren(document.createTextNode(bits.join(" · ")));
for (const alt of info.alternatives || []) {
+61 -1
View File
@@ -20,7 +20,8 @@ def fake_git_commit(branch, files, message, *, base=None, append=None):
"""Pushes land in whichever fake world tm.http is bound to, as the contents API's writes used to."""
w = getattr(tm.http, "__self__", None)
if w is None: return "sha"
if not files and not append: w.branches = getattr(w, "branches", []) + [branch]
if not files and not append:
w.branches = getattr(w, "branches", []) + [branch]; w.bases = {**getattr(w, "bases", {}), branch: base}
for fp, data in (files or {}).items():
if hasattr(w, "writes"): w.writes.append((fp, "s" if fp in w.files else None))
if data is None: w.files.pop(fp, None)
@@ -62,6 +63,7 @@ class World:
tc("write_file", {"path": ".github/workflows/evil.yml", "content": "nope"}),
tc("open_pr", {"title": "Set x to 2", "summary": "Changed app.py; ran nothing."})]
assert body["tools"] and body["messages"][0]["role"] == "system"
self.venice_system = body["messages"][0]["content"]
if self.turn <= len(script):
return {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [script[self.turn - 1]]}}]}
return {"choices": [{"message": {"role": "assistant", "content": "done"}}]}
@@ -222,6 +224,7 @@ prh = {**pr, "head": {"sha": "abc"}}
chk = {7: {"pr": {**prh, "state": "open"}, "statuses": [{"status": "pending", "context": "ci"}, {"status": "success", "context": "lint"}]}}
run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh], None, chk), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
assert "# Checks\n- ci: pending\n- lint: success\n\n# Diff" in seen["user"], seen["user"]
assert "means that context reported success" in seen["system"] and "tests or builds passed" not in seen["system"]
assert "evidence you can rely on" in seen["system"] and "pending check is not yet evidence" in seen["system"]
run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh]), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
assert "# Checks\n(checks unavailable)\n\n# Diff" in seen["user"], seen["user"]
@@ -280,6 +283,14 @@ assert not merged(w) and "objected" in w.posted[0][1]
# protected paths -> human merges
w = run(pr_world(files=[".github/workflows/ci.yml"]), {**K, "TIME_AUTOMERGE": "on"})
assert not merged(w) and "protected paths" in w.posted[0][1]
# the default protects all of .github/, the engine included; TIME_PROTECTED can narrow that to the workflow dirs, which stay
# protected whatever it says
w = run(pr_world(files=[".github/time/time.py", "CHANGELOG.md"]), {**K, "TIME_AUTOMERGE": "on"}); assert not merged(w)
NARROW = ".github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret"
w = run(pr_world(files=[".github/time/time.py", "CHANGELOG.md"]), {**K, "TIME_AUTOMERGE": "on", "TIME_PROTECTED": NARROW}); assert merged(w)
w = run(pr_world(files=[".gitea/workflows/ci.yml"]), {**K, "TIME_AUTOMERGE": "on", "TIME_PROTECTED": "deploy"})
assert not merged(w) and "protected paths" in w.posted[0][1]
os.environ.pop("TIME_PROTECTED", None)
# failing or missing checks block the merge even when approved
w = run(pr_world(checks=[{"status": "failure", "context": "ci"}]), {**K, "TIME_AUTOMERGE": "on"})
assert not merged(w) and "checks are failing" in w.posted[0][1]
@@ -303,6 +314,14 @@ assert w.branches == ["time/card-1"] and w.files["app.py"] == "x = 2\n" and w.wr
assert ".github/workflows/evil.yml" not in w.files
assert len(w.opened) == 1 and w.opened[0]["head"] == "time/card-1" and "Closes #1" in w.opened[0]["body"]
assert "opened PR #42" in w.posted[-1][1] and "kimi-k2-7-code" in w.posted[-1][1]
# 14b a card with a `Base: <branch>` line is built on a branch cut from that base, named for it, and its PR targets it
w = run(World([card(1, "Base: gh-pages\n\n" + D)], {1: [beat("would-dispatch")]}), {**K, "TIME_WORKER": "venice:kimi code", "VENICE_API_KEY": "vk"})
assert w.branches == ["time/card-1-gh-pages"] and w.bases == {"time/card-1-gh-pages": "gh-pages"}, (w.branches, getattr(w, "bases", None))
assert w.opened[0]["head"] == "time/card-1-gh-pages" and w.opened[0]["base"] == "gh-pages", w.opened
assert "cut from `gh-pages`" in w.venice_system and "targets `gh-pages`" in w.venice_system
assert "on branch `time/card-1-gh-pages`" in w.posted[0][1], w.posted[0]
assert tm.card_base({"body": "Base: `release/1.x`\nDONE: x"}) == "release/1.x" and tm.card_base({"body": "rebase: no\nDONE: x"}) is None
assert tm.card_branch({"number": 5, "body": D}) == "time/card-5" and w.bases.get("time/card-1") is None
# then the normal path links the PR and reviews it
w2 = run(World([card(1, D)], {1: [beat("dispatched", launched=True, worker="venice", pr=42)]},
[{"number": 42, "title": "Set x to 2", "body": "Closes #1", "html_url": "u"}]), K)
@@ -395,6 +414,12 @@ assert h.call("open_pr", {"title": "T", "summary": "round two"}) == "updated PR
assert w.patched[0][0] == 9 and "round two" in w.patched[0][1]["body"] and "Closes #1" in w.patched[0][1]["body"]
h2 = tm.Hands(2, "time/card-2"); h2.label = "kimi"
assert h2.call("open_pr", {"title": "T", "summary": "s"}) == "opened PR #42" and len(w.opened) == 1, "a closed PR is not reopened"
# no duplicate "Closes #N": added when the summary lacks it, left alone when it has it (#1 is not satisfied by #12)
for summ, n in (("s", 1), ("s\n\nCloses #1", 1), ("s\n\ncloses #1.", 1), ("s\n\nCloses #12", 2)):
w = World([], prs=[{"number": 9, "state": "open", "head": {"ref": "time/card-1"}}]); tm.http = w.http
h = tm.Hands(1, "time/card-1"); h.label = "kimi"; h.call("open_pr", {"title": "T", "summary": summ})
body = w.patched[0][1]["body"]
assert body.lower().count("closes #1") == n and body.count("Closes #1\n") + body.count("Closes #1.") <= 1, (summ, body)
# the Cursor worker answers an objection too: on the PR's own branch, no new PR, the review text in the prompt
w = objected_world()
w = run(w, {**K, "TIME_WORKER": "cursor"})
@@ -482,6 +507,41 @@ w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}, ledger=o
assert w.vars == {"TIME_MODE": ("PUT", "dry-run")} and "dry-run" in next(i for i in w.issues.values() if "andon" in i["labels"])["body"]
print("ALL OK 51")
# 57 the daily cap gates launches, not the run under way: a worker launched within budget finishes up to TIME_CAP_SLACK x
# the cap (default 1.5), and a PR that is open still gets its review; a new launch and the monthly cap see no slack
near = [{"at": iso(), "model": "claude-opus-5-5", "usd": 4.99}] # each scripted step books $0.022, so step 2 crosses $5
sent.clear()
w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}, ledger=near), {"TIME_WORKER": "claude:sonnet"})
assert len(w.opened) == 1 and len(sent) == 5 and "opened PR #42" in w.posted[-1][1], "the run finished under the slack"
sent.clear()
w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}, ledger=near), {"TIME_WORKER": "claude:sonnet", "TIME_CAP_SLACK": "1"})
assert w.opened == [] and len(sent) == 1 and "$5.00 cap" in w.posted[-1][1] and '"beat":"capped"' in w.posted[-1][1].replace(" ", ""), "no slack: the old stop"
os.environ.pop("TIME_CAP_SLACK"); sent.clear()
w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}, ledger=[{"at": iso(), "model": "claude-opus-5-5", "usd": 5.5}]), {"TIME_WORKER": "claude:sonnet"})
assert sent == [] and w.opened == [] and "a run under way may go to" not in w.posted[-1][1], "a new launch never gets the slack"
sent.clear()
w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}, ledger=[{"at": iso(), "model": "claude-opus-5-5", "usd": 99.99}]), {"TIME_WORKER": "claude:sonnet", "TIME_DAILY_USD": "500"})
assert len(sent) == 1 and w.opened == [] and "this month's spend" in w.posted[-1][1], "the monthly cap is hard inside a run too"
# the review of an open PR runs on a capped day (within the slack), and says so if even the slack is spent
tm.think = real_think # the real one: the budget check lives in chat(), under it
tm.pr_diff = lambda n, limit=0: "diff --git a b"
def review_world(usd):
wr = ClaudeWorld([card(1, D)], {1: [beat("dispatched", launched=True)]}, [pr], ledger=[{"at": iso(), "model": "claude-opus-5-5", "usd": usd}])
wr.venice = lambda body: {"choices": [{"message": {"content": "**Approve** fine"}}]}
return wr
w = claude_env(review_world(6), {"TIME_REVIEW": "venice:kimi", "VENICE_API_KEY": "vk"})
assert w.posted[0][0] == 7 and w.posted[0][1].startswith("**Approve**") and "`kimi` reviewed" in w.posted[1][1], w.posted
w = claude_env(review_world(8), {"TIME_REVIEW": "venice:kimi", "VENICE_API_KEY": "vk"})
assert w.posted[0][0] == 1 and "may go to $7.50" in w.posted[0][1] and '"beat":"capped"' in w.posted[0][1].replace(" ", ""), w.posted
os.environ.pop("VENICE_API_KEY", None); os.environ.pop("TIME_CAP_SLACK", None)
# the tick takes the oldest ready card first, whatever order the API lists them in
wo = ClaudeWorld([card(9, D), card(4, D)], {9: [beat("would-dispatch")], 4: [beat("would-dispatch")]})
_lab = wo.labelled
wo.labelled = lambda name: sorted(_lab(name), key=lambda i: -i["number"])
w = claude_env(wo, {"TIME_WORKER": "claude:sonnet", "TIME_DAILY_CAP": "1"})
assert [p[0] for p in w.posted][:1] == [4] and len(w.opened) == 1, w.posted
print("ALL OK 57")
# TIME_WORKER=claude:sonnet: same hands, Claude's endpoint, the seat's label in the PR footer and the beats
sent.clear()
w = claude_env(ClaudeWorld([card(1, D)], {1: [beat("would-dispatch")]}), {"TIME_WORKER": "claude:sonnet"})
+63 -17
View File
@@ -20,6 +20,7 @@ import datetime as dt
import importlib.util
import atexit
import base64
import contextlib
import json
import os
import re
@@ -308,12 +309,30 @@ def budget() -> tuple[float, float, float, float]:
spend(now.strftime("%Y-%m")), float(os.environ.get("TIME_MONTHLY_USD") or 100))
_slack = 1.0 # what budget_check allows over the daily cap: 1.0 for a new launch, TIME_CAP_SLACK inside finishing()
@contextlib.contextmanager
def finishing():
"""Inside this block a model call may run past the daily cap, up to TIME_CAP_SLACK x it (default 1.5): a worker that
was launched within budget finishes its card, and a PR that is already open gets its review, instead of dying
mid-run with the spend so far lost (a card stopped at step 27 of 40 has cost its whole run and produced nothing).
A new launch never gets the slack, and the monthly cap is hard everywhere."""
global _slack
old, _slack = _slack, max(1.0, float(os.environ.get("TIME_CAP_SLACK") or 1.5))
try:
yield
finally:
_slack = old
def budget_check() -> None:
today, day_cap, month, month_cap = budget()
if month >= month_cap:
raise Budget(f"this month's spend is ${month:.2f} of the ${month_cap:.2f} cap; Time is back in dry-run until a human raises it")
if today >= day_cap:
raise Budget(f"today's spend is ${today:.2f} of the ${day_cap:.2f} cap; carrying on tomorrow")
if today >= day_cap * _slack:
allowance = f" (a run under way may go to ${day_cap * _slack:.2f})" if _slack > 1 else ""
raise Budget(f"today's spend is ${today:.2f} of the ${day_cap:.2f} cap{allowance}; carrying on tomorrow")
def andon(reason: str) -> None:
@@ -542,7 +561,9 @@ def verdict(pr: dict, since: str) -> str | None:
def touches_protected(pr_number: int) -> list[str]:
paths = [p.strip() for p in (os.environ.get("TIME_PROTECTED") or DEFAULT_PROTECTED).split(",") if p.strip()]
paths = list(dict.fromkeys([".github/", ".gitea/", *paths])) # workflows stay a human's merge whatever TIME_PROTECTED says
# workflows stay a human's merge whatever TIME_PROTECTED says; the rest is TIME_PROTECTED's call: the default keeps all
# of .github/, and a repo that trusts its reviewer can narrow it so the engine under .github/time/ lands like other code
paths = list(dict.fromkeys([".github/workflows/", ".gitea/workflows/", *paths]))
files, page = [], 1
while True: # every file, not the first 100; a rename counts under both names
batch = gh("GET", f"/repos/{REPO}/pulls/{pr_number}/files?limit=100&page={page}")
@@ -674,7 +695,7 @@ WORKER_RULES = """You are the worker for card #{n} in {repo}: "{title}".
{body}
You have hands: list_files, read_file, write_file (one file per call, full content), run (shell on a fresh checkout of
your branch), open_pr. Work on the branch `{branch}`. Only two things reach it: write_file, and files a run lists in
your branch), open_pr. Work on the branch `{branch}`.{base_note} Only two things reach it: write_file, and files a run lists in
`keep` (for generated files, e.g. a sample built by a script). Anything else a run does is thrown away. Keep the change small and focused.
Read before you write. Always `run` the project's tests or checks before opening the PR (that run is the PR's check). When it is done and checked, call open_pr once
with a short summary; the PR must close the card, and Time adds that line for you. Do not touch .github/workflows or .gitea/workflows.
@@ -799,11 +820,28 @@ def git_commit(branch: str, files: dict[str, bytes | None], message: str, *, bas
shutil.rmtree(d, ignore_errors=True)
BASE_LINE = re.compile(r"^\s*Base:\s*`?([A-Za-z0-9_./+-]+)`?\s*$", re.M | re.I)
def card_base(card: dict) -> str | None:
"""The branch a card's work is cut from and its PR targets, from a `Base: <branch>` line in the body (a venture that
lives on `gh-pages`, say). None means the default branch, as before."""
m = BASE_LINE.search(card.get("body") or "")
return m.group(1) if m else None
def card_branch(card: dict) -> str:
"""`time/card-N`, or `time/card-N-<base>` for a card on another branch: a branch cut from the default branch before
the card said otherwise is never resumed by mistake, and the name says where the work lives."""
base = card_base(card)
return f"time/card-{card['number']}" + (f"-{base}" if base else "")
class Hands:
def __init__(self, n: int, branch: str):
self.n, self.branch, self.pr, self.steps = n, branch, None, 0
def __init__(self, n: int, branch: str, base: str | None = None):
self.n, self.branch, self.base, self.pr, self.steps = n, branch, base, None, 0
self.label = "a seat" # venice_work names the seat for the PR footer
git_commit(branch, {}, f"card #{n}: branch") # already there: resume on it (nothing to commit, push is a no-op)
git_commit(branch, {}, f"card #{n}: branch", base=base) # already there: resume on it (nothing to commit, push is a no-op)
self.checkout = None
def _checkout(self) -> str:
@@ -875,12 +913,13 @@ class Hands:
note = ("\n" + "; ".join(kept)) if kept else ""
return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}{note}"
if name == "open_pr":
body = f"{a['summary']}\n\nCloses #{self.n}\n\n<sub>worker: {self.label} via Time</sub>"
closes = "" if re.search(rf"\bcloses #{self.n}(?!\d)", a["summary"], re.I) else f"\n\nCloses #{self.n}" # once only
body = f"{a['summary']}{closes}\n\n<sub>worker: {self.label} via Time</sub>"
if (old := open_pr_for(self.branch)): # a revision: the PR is open already, so say what changed there
gh("PATCH", f"/repos/{REPO}/pulls/{old['number']}", {"title": a["title"][:200], "body": body}, as_time=True)
self.pr = old["number"]
return f"updated PR #{self.pr}"
pr = gh("POST", f"/repos/{REPO}/pulls", {"title": a["title"][:200], "head": self.branch, "base": default_branch(),
pr = gh("POST", f"/repos/{REPO}/pulls", {"title": a["title"][:200], "head": self.branch, "base": self.base or default_branch(),
"body": body}, as_time=True)
self.pr = pr["number"]
return f"opened PR #{self.pr}"
@@ -907,10 +946,13 @@ def venice_work(card: dict, done: str, spec: str, hands_cls=None, review: str |
seat, model, effort = worker_seat_model(spec)
fallback = (os.environ.get("TIME_WORKER_FALLBACK") or ("grok build" if provider_of(model) == "venice" else "off")).strip()
fb_model = worker_seat_model(fallback)[1] if fallback.lower() != "off" else None
hands = (hands_cls or Hands)(card["number"], f"time/card-{card['number']}")
base = card_base(card)
hands = (hands_cls or Hands)(card["number"], card_branch(card), **({"base": base} if base else {}))
hands.label = SEATS[seat]["label"]
base_note = (f" It is cut from `{base}` and your pull request targets `{base}`, not the default branch: that is where "
"this card's files live.") if base else ""
system = WORKER_RULES.format(n=card["number"], repo=REPO, title=card["title"],
body=(card.get("body") or "")[:20000], branch=hands.branch, done=done)
body=(card.get("body") or "")[:20000], branch=hands.branch, done=done, base_note=base_note)
if review is not None: # answering a review: same branch, and the reviewer's words go in marked as theirs
system += REVISE_RULES.format(branch=hands.branch, review=review or "(no review comments were found; check the PR)")
msgs = [{"role": "system", "content": system},
@@ -922,13 +964,15 @@ def venice_work(card: dict, done: str, spec: str, hands_cls=None, review: str |
if effort:
body["reasoning_effort"] = effort
try:
msg = chat(body, timeout=300)["choices"][0]["message"]
with finishing(): # launched within budget: this run may finish past the daily cap (TIME_CAP_SLACK)
msg = chat(body, timeout=300)["choices"][0]["message"]
except RuntimeError as e:
if isinstance(e, Budget) or not fb_model or model == fb_model or not re.search(r"-> (429|5\d\d)\b", str(e)):
raise
print(f"{model} stays overloaded; the fallback {fb_model} takes over")
model = body["model"] = fb_model
msg = chat(body, timeout=300)["choices"][0]["message"]
with finishing():
msg = chat(body, timeout=300)["choices"][0]["message"]
msgs.append({k: v for k, v in msg.items() if k in ("role", "content", "tool_calls")})
calls = msg.get("tool_calls") or []
if not calls:
@@ -1026,8 +1070,9 @@ def review(card: dict, pr: dict, done: str) -> dict:
+ REVIEW_PROMPT.format(pr=pr["number"], repo=REPO, n=card["number"], title=card["title"], done=done)
.replace("Leave one PR comment that", "Your reply is posted as the PR comment. It")
+ "\nThe checks listed under # Checks are the head commit's real statuses: evidence you can rely on "
"(a success there means those tests or builds passed). A pending check is not yet evidence either way.")
text = think(seat, model, effort, system, user)
"(a success there means that context reported success). A pending check is not yet evidence either way.")
with finishing(): # the PR is open and paid for: its review runs past the daily cap too, within the slack
text = think(seat, model, effort, system, user)
gh("POST", f"/repos/{REPO}/issues/{pr['number']}/comments",
{"body": f"{text}\n\n<sub>review by {seat} · {model or SEATS[seat]['default']} · via Time</sub>"})
return {"review": "venice", "seat": seat}
@@ -1057,7 +1102,7 @@ def tick() -> None:
f"Cursor cloud agent{f' ({worker_model})' if worker_model else ''}"
now = dt.datetime.now(dt.timezone.utc)
today = now.date().isoformat()
cards = issues("state=open&labels=ready")
cards = sorted(issues("state=open&labels=ready"), key=lambda c: c["number"]) # oldest first, like promote_next
prs = gh("GET", f"/repos/{REPO}/pulls?state=all&sort=recentupdate&limit=50")
launches_today = 0
plan = []
@@ -1108,7 +1153,7 @@ def tick() -> None:
if seat_worker(worker):
prov = provider_of(worker_seat_model(worker)[1])
comment(n, f"⏱️ Time: {worker_name} is " + (f"answering the review on PR #{rev['number']}" if rev else "working on this")
+ f" now, on branch `time/card-{n}`.", {"beat": "dispatched", "launched": True, "worker": prov})
+ f" now, on branch `{card_branch(card)}`.", {"beat": "dispatched", "launched": True, "worker": prov})
launches_today += 1
if rev:
try:
@@ -1193,6 +1238,7 @@ def cmd_doctor() -> int:
row("TIME_REVIEW", True, os.environ.get("TIME_REVIEW") or "off (default)")
today, day_cap, month, month_cap = budget()
row("TIME_DAILY_USD", True, os.environ.get("TIME_DAILY_USD") or "5 (default)")
row("TIME_CAP_SLACK", True, os.environ.get("TIME_CAP_SLACK") or "1.5 (default)")
row("TIME_MONTHLY_USD", True, os.environ.get("TIME_MONTHLY_USD") or "100 (default)")
row("spend", today < day_cap and month < month_cap, f"${today:.2f} today, ${month:.2f} this month ({ledger_path(dt.datetime.now(dt.timezone.utc).strftime('%Y-%m'))})")
row("(optional) ANTHROPIC_API_KEY (Claude seats)", bool(os.environ.get("ANTHROPIC_API_KEY")),
+6 -1
View File
@@ -24,7 +24,7 @@ jobs:
(startsWith(github.event.comment.body, '**Approve') || startsWith(github.event.comment.body, 'Approve') ||
startsWith(github.event.comment.body, '**Object') || startsWith(github.event.comment.body, 'Object'))))
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 55 # workers can overlap inside one job now (#75); under the hourly cadence, and the concurrency group queues the next run
steps:
- uses: actions/checkout@v5
with:
@@ -37,6 +37,11 @@ jobs:
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # user-level secret on teahubxyz (docs/decisions.md on the spine)
TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # repo secret: the `time` bot opens and merges PRs (Gitea denies that to the Actions identity)
TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }} # umbrella seat, retired once the per-seat tokens below are in use (#76)
TIME_SEAT_HAIKU: ${{ secrets.TIME_SEAT_HAIKU }} # one Gitea user per seat (model line): a seat's verdict is a real review by its own user (#76)
TIME_SEAT_SONNET: ${{ secrets.TIME_SEAT_SONNET }}
TIME_SEAT_OPUS: ${{ secrets.TIME_SEAT_OPUS }}
TIME_SEAT_FABLE: ${{ secrets.TIME_SEAT_FABLE }}
TIME_TRUSTED: ${{ vars.TIME_TRUSTED }} # org owners etc.; collaborators are trusted without being listed
TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets
TIME_MODE: ${{ vars.TIME_MODE }}
+4
View File
@@ -18,6 +18,10 @@
- The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it.
### Added
- Cards can live on another branch. A `Base: <branch>` line in a card (`Base: gh-pages` for a venture) makes the seat worker cut its branch from that base, name it `time/card-N-<base>`, see that branch's files, and open the PR against it; without the line nothing changes. Until now every worker branched from the default branch whatever the card said, which is why the cards on `gh-pages` ran out of turns or opened PRs against `main`.
- A launched worker finishes what it starts. The daily spend cap gates launches; a run that began within budget may go on to `TIME_CAP_SLACK` x the cap (default 1.5), and a PR that is already open gets its review on a capped day too. A run stopped mid-way had cost its whole spend and produced nothing. The monthly cap stays hard everywhere; `TIME_CAP_SLACK=1` restores the old stop.
- The tick takes the oldest `ready` card first, the order `promote_next` already uses, instead of the newest the API lists first.
- `TIME_PROTECTED` now governs every protected path except the workflow directories (`.github/workflows/`, `.gitea/workflows/`), which stay a human's merge whatever it says. The default still protects all of `.github/`; a repo that trusts its reviewer can set `TIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret` so the engine under `.github/time/` merges itself on Approve + green like any other code.
- Objections go back to the worker. `/time retry` on a card whose PR is open hands the worker the reviewer's latest comments (marked as the reviewer's, strangers' left out) on the same branch; `open_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, so the next beat reviews again and an objection older than the push no longer counts.
- A batch's children start in the tick that spawned them (GitHub lists a card made a moment ago only later, so they used to wait a tick).
- Build briefs keep claims to what the code does: no promised features or schedules, and the trending repos are the wave a venture rides, never something it "builds on". Fix rounds take a failing claim off the page rather than building a feature in a hurry. In the first live batch, every fix round was a claim the code didn't back.
+194
View File
@@ -167,3 +167,197 @@
{"at":"2026-10-16T00:11:37Z","model":"claude-sonnet-5-5","in":51544,"out":3967,"usd":0.142758}
{"at":"2026-10-16T13:13:33Z","model":"claude-opus-5-5","in":27046,"out":777,"usd":0.123724}
{"at":"2026-10-17T13:13:44Z","model":"claude-opus-5-5","in":26444,"out":667,"usd":0.119116}
{"at":"2026-10-17T18:18:07Z","model":"claude-sonnet-5-5","in":1751,"out":26,"usd":0.003762}
{"at":"2026-10-17T18:18:09Z","model":"claude-sonnet-5-5","in":2279,"out":284,"usd":0.007398}
{"at":"2026-10-17T18:18:13Z","model":"claude-sonnet-5-5","in":28494,"out":393,"usd":0.060918}
{"at":"2026-10-17T18:18:16Z","model":"claude-sonnet-5-5","in":31262,"out":208,"usd":0.064604}
{"at":"2026-10-17T18:18:42Z","model":"claude-sonnet-5-5","in":34213,"out":3118,"usd":0.099606}
{"at":"2026-10-17T18:19:44Z","model":"claude-sonnet-5-5","in":35944,"out":7606,"usd":0.147948}
{"at":"2026-10-17T18:20:25Z","model":"claude-sonnet-5-5","in":37180,"out":4821,"usd":0.12257}
{"at":"2026-10-17T18:20:51Z","model":"claude-sonnet-5-5","in":38270,"out":3106,"usd":0.1076}
{"at":"2026-10-17T18:22:14Z","model":"claude-sonnet-5-5","in":41295,"out":11757,"usd":0.20016}
{"at":"2026-10-17T18:22:29Z","model":"claude-sonnet-5-5","in":45974,"out":1680,"usd":0.108748}
{"at":"2026-10-17T18:23:09Z","model":"claude-sonnet-5-5","in":46382,"out":6616,"usd":0.158924}
{"at":"2026-10-17T18:23:11Z","model":"claude-sonnet-5-5","in":51455,"out":72,"usd":0.10363}
{"at":"2026-10-17T18:23:14Z","model":"claude-sonnet-5-5","in":51814,"out":351,"usd":0.107138}
{"at":"2026-10-17T18:23:24Z","model":"claude-sonnet-5-5","in":52210,"out":1496,"usd":0.11938}
{"at":"2026-10-17T18:23:31Z","model":"claude-sonnet-5-5","in":53661,"out":655,"usd":0.113872}
{"at":"2026-10-17T18:23:43Z","model":"claude-sonnet-5-5","in":54212,"out":1549,"usd":0.123914}
{"at":"2026-10-17T18:27:28Z","model":"claude-haiku-5-5","in":14087,"out":3022,"usd":0.00292}
{"at":"2026-10-17T18:27:32Z","model":"claude-sonnet-5-5","in":1363,"out":26,"usd":0.002986}
{"at":"2026-10-17T18:27:34Z","model":"claude-sonnet-5-5","in":1891,"out":100,"usd":0.004782}
{"at":"2026-10-17T18:27:36Z","model":"claude-sonnet-5-5","in":3064,"out":115,"usd":0.007278}
{"at":"2026-10-17T18:27:38Z","model":"claude-sonnet-5-5","in":5804,"out":98,"usd":0.012588}
{"at":"2026-10-17T18:27:44Z","model":"claude-sonnet-5-5","in":7456,"out":857,"usd":0.023482}
{"at":"2026-10-17T18:27:49Z","model":"claude-sonnet-5-5","in":8267,"out":820,"usd":0.024734}
{"at":"2026-10-17T18:27:55Z","model":"claude-sonnet-5-5","in":9230,"out":918,"usd":0.02764}
{"at":"2026-10-17T18:27:59Z","model":"claude-sonnet-5-5","in":10126,"out":226,"usd":0.022512}
{"at":"2026-10-17T18:28:11Z","model":"claude-haiku-5-5","in":2565,"out":1767,"usd":0.00114}
{"at":"2026-10-17T20:20:54Z","model":"claude-sonnet-5-5","in":2158,"out":26,"usd":0.004576}
{"at":"2026-10-17T20:20:56Z","model":"claude-sonnet-5-5","in":2637,"out":163,"usd":0.006904}
{"at":"2026-10-17T20:20:58Z","model":"claude-sonnet-5-5","in":3153,"out":187,"usd":0.008176}
{"at":"2026-10-17T20:21:01Z","model":"claude-sonnet-5-5","in":5843,"out":191,"usd":0.013596}
{"at":"2026-10-17T20:21:06Z","model":"claude-sonnet-5-5","in":6251,"out":341,"usd":0.015912}
{"at":"2026-10-17T20:21:09Z","model":"claude-sonnet-5-5","in":6655,"out":381,"usd":0.01712}
{"at":"2026-10-17T20:21:16Z","model":"claude-sonnet-5-5","in":7036,"out":659,"usd":0.020662}
{"at":"2026-10-17T20:21:20Z","model":"claude-sonnet-5-5","in":7483,"out":397,"usd":0.018936}
{"at":"2026-10-17T20:21:37Z","model":"claude-sonnet-5-5","in":7960,"out":1855,"usd":0.03447}
{"at":"2026-10-17T20:21:49Z","model":"claude-sonnet-5-5","in":10584,"out":1157,"usd":0.032738}
{"at":"2026-10-17T20:21:57Z","model":"claude-sonnet-5-5","in":11307,"out":837,"usd":0.030984}
{"at":"2026-10-17T20:22:04Z","model":"claude-sonnet-5-5","in":16615,"out":513,"usd":0.03836}
{"at":"2026-10-17T20:22:17Z","model":"claude-sonnet-5-5","in":19010,"out":942,"usd":0.04744}
{"at":"2026-10-17T20:22:23Z","model":"claude-sonnet-5-5","in":19216,"out":477,"usd":0.043202}
{"at":"2026-10-17T20:22:33Z","model":"claude-sonnet-5-5","in":19515,"out":970,"usd":0.04873}
{"at":"2026-10-17T20:22:45Z","model":"claude-sonnet-5-5","in":19943,"out":1162,"usd":0.051506}
{"at":"2026-10-17T20:22:47Z","model":"claude-sonnet-5-5","in":22260,"out":116,"usd":0.04568}
{"at":"2026-10-17T20:22:50Z","model":"claude-sonnet-5-5","in":22837,"out":187,"usd":0.047544}
{"at":"2026-10-17T20:22:54Z","model":"claude-sonnet-5-5","in":23284,"out":220,"usd":0.048768}
{"at":"2026-10-17T20:22:56Z","model":"claude-sonnet-5-5","in":24921,"out":165,"usd":0.051492}
{"at":"2026-10-17T20:22:58Z","model":"claude-sonnet-5-5","in":25358,"out":70,"usd":0.051416}
{"at":"2026-10-17T20:23:00Z","model":"claude-sonnet-5-5","in":27711,"out":210,"usd":0.057522}
{"at":"2026-10-17T20:23:04Z","model":"claude-sonnet-5-5","in":31018,"out":347,"usd":0.065506}
{"at":"2026-10-17T20:23:08Z","model":"claude-sonnet-5-5","in":33756,"out":253,"usd":0.070042}
{"at":"2026-10-17T20:23:10Z","model":"claude-sonnet-5-5","in":35236,"out":161,"usd":0.072082}
{"at":"2026-10-17T20:23:16Z","model":"claude-sonnet-5-5","in":37623,"out":401,"usd":0.079256}
{"at":"2026-10-17T20:23:22Z","model":"claude-sonnet-5-5","in":40686,"out":229,"usd":0.083662}
{"at":"2026-10-17T20:23:23Z","model":"claude-sonnet-5-5","in":42769,"out":101,"usd":0.086548}
{"at":"2026-10-17T20:23:26Z","model":"claude-sonnet-5-5","in":45100,"out":186,"usd":0.09206}
{"at":"2026-10-17T20:23:28Z","model":"claude-sonnet-5-5","in":47113,"out":261,"usd":0.096836}
{"at":"2026-10-17T20:23:32Z","model":"claude-sonnet-5-5","in":49280,"out":332,"usd":0.10188}
{"at":"2026-10-17T20:23:34Z","model":"claude-sonnet-5-5","in":51809,"out":190,"usd":0.105518}
{"at":"2026-10-17T20:23:37Z","model":"claude-sonnet-5-5","in":53813,"out":206,"usd":0.109686}
{"at":"2026-10-17T20:23:38Z","model":"claude-sonnet-5-5","in":54057,"out":85,"usd":0.108964}
{"at":"2026-10-17T20:23:40Z","model":"claude-sonnet-5-5","in":54152,"out":114,"usd":0.109444}
{"at":"2026-10-17T20:23:43Z","model":"claude-sonnet-5-5","in":54408,"out":310,"usd":0.111916}
{"at":"2026-10-17T20:23:47Z","model":"claude-sonnet-5-5","in":55850,"out":193,"usd":0.11363}
{"at":"2026-10-17T20:23:49Z","model":"claude-sonnet-5-5","in":58100,"out":213,"usd":0.11833}
{"at":"2026-10-17T20:23:53Z","model":"claude-sonnet-5-5","in":60500,"out":471,"usd":0.12571}
{"at":"2026-10-17T20:23:59Z","model":"claude-sonnet-5-5","in":62407,"out":618,"usd":0.130994}
{"at":"2026-10-18T01:56:39Z","model":"kimi-k3","in":551,"out":4000,"usd":null}
{"at":"2026-10-18T01:57:03Z","model":"kimi-k3","in":501,"out":3119,"usd":null}
{"at":"2026-10-18T01:57:15Z","model":"claude-sonnet-5-5","in":1727,"out":55,"usd":0.004004}
{"at":"2026-10-18T01:57:17Z","model":"claude-sonnet-5-5","in":1791,"out":26,"usd":0.003842}
{"at":"2026-10-18T01:57:19Z","model":"claude-sonnet-5-5","in":2319,"out":145,"usd":0.006088}
{"at":"2026-10-18T01:57:21Z","model":"claude-sonnet-5-5","in":2905,"out":140,"usd":0.00721}
{"at":"2026-10-18T01:57:23Z","model":"claude-sonnet-5-5","in":3879,"out":155,"usd":0.009308}
{"at":"2026-10-18T01:57:26Z","model":"claude-sonnet-5-5","in":5731,"out":145,"usd":0.012912}
{"at":"2026-10-18T01:57:28Z","model":"claude-sonnet-5-5","in":5893,"out":159,"usd":0.013376}
{"at":"2026-10-18T01:57:34Z","model":"claude-sonnet-5-5","in":6477,"out":485,"usd":0.017804}
{"at":"2026-10-18T01:57:40Z","model":"claude-sonnet-5-5","in":9000,"out":449,"usd":0.02249}
{"at":"2026-10-18T01:57:46Z","model":"claude-sonnet-5-5","in":14302,"out":392,"usd":0.032524}
{"at":"2026-10-18T01:57:52Z","model":"claude-sonnet-5-5","in":18183,"out":277,"usd":0.039136}
{"at":"2026-10-18T01:57:54Z","model":"claude-sonnet-5-5","in":20570,"out":117,"usd":0.04231}
{"at":"2026-10-18T01:57:57Z","model":"claude-sonnet-5-5","in":22918,"out":155,"usd":0.047386}
{"at":"2026-10-18T01:58:02Z","model":"claude-sonnet-5-5","in":25289,"out":246,"usd":0.053038}
{"at":"2026-10-18T01:58:05Z","model":"claude-sonnet-5-5","in":29676,"out":137,"usd":0.060722}
{"at":"2026-10-18T01:58:07Z","model":"claude-sonnet-5-5","in":30008,"out":137,"usd":0.061386}
{"at":"2026-10-18T01:58:10Z","model":"claude-sonnet-5-5","in":31487,"out":159,"usd":0.064564}
{"at":"2026-10-18T01:58:14Z","model":"claude-sonnet-5-5","in":31874,"out":214,"usd":0.065888}
{"at":"2026-10-18T01:58:18Z","model":"claude-sonnet-5-5","in":34808,"out":173,"usd":0.071346}
{"at":"2026-10-18T01:58:21Z","model":"claude-sonnet-5-5","in":35490,"out":295,"usd":0.07393}
{"at":"2026-10-18T01:58:26Z","model":"claude-sonnet-5-5","in":36311,"out":203,"usd":0.074652}
{"at":"2026-10-18T01:58:32Z","model":"claude-sonnet-5-5","in":38059,"out":410,"usd":0.080218}
{"at":"2026-10-18T01:58:43Z","model":"claude-sonnet-5-5","in":40152,"out":1218,"usd":0.092484}
{"at":"2026-10-18T01:58:52Z","model":"claude-sonnet-5-5","in":43041,"out":833,"usd":0.094412}
{"at":"2026-10-18T01:58:56Z","model":"claude-sonnet-5-5","in":43754,"out":158,"usd":0.089088}
{"at":"2026-10-18T01:58:58Z","model":"claude-sonnet-5-5","in":44894,"out":223,"usd":0.092018}
{"at":"2026-10-18T01:59:03Z","model":"claude-sonnet-5-5","in":46474,"out":360,"usd":0.096548}
{"at":"2026-10-18T01:59:05Z","model":"claude-sonnet-5-5","in":47416,"out":89,"usd":0.095722}
{"at":"2026-10-18T01:59:09Z","model":"claude-sonnet-5-5","in":48704,"out":446,"usd":0.101868}
{"at":"2026-10-18T01:59:13Z","model":"claude-sonnet-5-5","in":49422,"out":265,"usd":0.101494}
{"at":"2026-10-18T01:59:30Z","model":"claude-sonnet-5-5","in":50006,"out":2066,"usd":0.120672}
{"at":"2026-10-18T01:59:59Z","model":"claude-sonnet-5-5","in":52558,"out":3267,"usd":0.137786}
{"at":"2026-10-18T02:00:12Z","model":"claude-sonnet-5-5","in":53089,"out":1369,"usd":0.119868}
{"at":"2026-10-18T02:01:24Z","model":"claude-sonnet-5-5","in":53909,"out":4905,"usd":0.156868}
{"at":"2026-10-18T02:01:56Z","model":"claude-sonnet-5-5","in":54081,"out":3464,"usd":0.142802}
{"at":"2026-10-18T02:02:39Z","model":"claude-sonnet-5-5","in":56406,"out":5010,"usd":0.162912}
{"at":"2026-10-18T02:02:49Z","model":"claude-sonnet-5-5","in":57895,"out":902,"usd":0.12481}
{"at":"2026-10-18T02:02:54Z","model":"claude-sonnet-5-5","in":58155,"out":454,"usd":0.12085}
{"at":"2026-10-18T02:02:59Z","model":"claude-sonnet-5-5","in":58339,"out":412,"usd":0.120798}
{"at":"2026-10-18T02:03:21Z","model":"claude-sonnet-5-5","in":58576,"out":2342,"usd":0.140572}
{"at":"2026-10-18T02:03:23Z","model":"claude-sonnet-5-5","in":1763,"out":55,"usd":0.004076}
{"at":"2026-10-18T02:03:24Z","model":"claude-sonnet-5-5","in":1827,"out":26,"usd":0.003914}
{"at":"2026-10-18T02:03:26Z","model":"claude-sonnet-5-5","in":2355,"out":212,"usd":0.00683}
{"at":"2026-10-18T02:03:28Z","model":"claude-sonnet-5-5","in":3439,"out":160,"usd":0.008478}
{"at":"2026-10-18T02:03:31Z","model":"claude-sonnet-5-5","in":3718,"out":108,"usd":0.008516}
{"at":"2026-10-18T02:03:33Z","model":"claude-sonnet-5-5","in":3957,"out":155,"usd":0.009464}
{"at":"2026-10-18T02:03:37Z","model":"claude-sonnet-5-5","in":5502,"out":273,"usd":0.013734}
{"at":"2026-10-18T02:03:42Z","model":"claude-sonnet-5-5","in":6651,"out":407,"usd":0.017372}
{"at":"2026-10-18T02:03:49Z","model":"claude-sonnet-5-5","in":9178,"out":806,"usd":0.026416}
{"at":"2026-10-18T02:03:55Z","model":"claude-sonnet-5-5","in":10903,"out":497,"usd":0.026776}
{"at":"2026-10-18T02:04:04Z","model":"claude-sonnet-5-5","in":12818,"out":971,"usd":0.035346}
{"at":"2026-10-18T02:04:11Z","model":"claude-sonnet-5-5","in":14529,"out":639,"usd":0.035448}
{"at":"2026-10-18T02:04:24Z","model":"claude-sonnet-5-5","in":14719,"out":1393,"usd":0.043368}
{"at":"2026-10-18T02:04:45Z","model":"claude-sonnet-5-5","in":14922,"out":2760,"usd":0.057444}
{"at":"2026-10-18T02:04:58Z","model":"claude-sonnet-5-5","in":16390,"out":1316,"usd":0.04594}
{"at":"2026-10-18T02:05:01Z","model":"claude-sonnet-5-5","in":1854,"out":109,"usd":0.004798}
{"at":"2026-10-18T02:05:04Z","model":"claude-sonnet-5-5","in":4304,"out":76,"usd":0.009368}
{"at":"2026-10-18T02:05:06Z","model":"claude-sonnet-5-5","in":6549,"out":83,"usd":0.013928}
{"at":"2026-10-18T02:05:08Z","model":"claude-sonnet-5-5","in":8739,"out":87,"usd":0.018348}
{"at":"2026-10-18T02:05:12Z","model":"claude-sonnet-5-5","in":11087,"out":145,"usd":0.023624}
{"at":"2026-10-18T02:05:15Z","model":"claude-sonnet-5-5","in":13825,"out":81,"usd":0.02846}
{"at":"2026-10-18T02:05:18Z","model":"claude-sonnet-5-5","in":16833,"out":191,"usd":0.035576}
{"at":"2026-10-18T02:05:22Z","model":"claude-sonnet-5-5","in":44658,"out":230,"usd":0.091616}
{"at":"2026-10-18T02:06:02Z","model":"claude-sonnet-5-5","in":45947,"out":5089,"usd":0.142784}
{"at":"2026-10-18T02:06:09Z","model":"claude-sonnet-5-5","in":48809,"out":662,"usd":0.104238}
{"at":"2026-10-18T02:06:24Z","model":"claude-sonnet-5-5","in":49311,"out":2669,"usd":0.125312}
{"at":"2026-10-18T02:06:33Z","model":"claude-sonnet-5-5","in":53209,"out":918,"usd":0.115598}
{"at":"2026-10-18T02:06:36Z","model":"claude-sonnet-5-5","in":1767,"out":109,"usd":0.004624}
{"at":"2026-10-18T02:06:38Z","model":"claude-sonnet-5-5","in":2032,"out":90,"usd":0.004964}
{"at":"2026-10-18T02:06:39Z","model":"claude-sonnet-5-5","in":3049,"out":157,"usd":0.007668}
{"at":"2026-10-18T02:06:41Z","model":"claude-sonnet-5-5","in":3606,"out":135,"usd":0.008562}
{"at":"2026-10-18T02:06:44Z","model":"claude-sonnet-5-5","in":4993,"out":98,"usd":0.010966}
{"at":"2026-10-18T02:06:46Z","model":"claude-sonnet-5-5","in":5985,"out":192,"usd":0.01389}
{"at":"2026-10-18T02:06:52Z","model":"claude-sonnet-5-5","in":8112,"out":272,"usd":0.018944}
{"at":"2026-10-18T02:07:05Z","model":"claude-sonnet-5-5","in":8707,"out":243,"usd":0.019844}
{"at":"2026-10-18T02:07:09Z","model":"claude-sonnet-5-5","in":11005,"out":320,"usd":0.02521}
{"at":"2026-10-18T02:07:11Z","model":"claude-sonnet-5-5","in":11254,"out":243,"usd":0.024938}
{"at":"2026-10-18T02:07:16Z","model":"claude-sonnet-5-5","in":11861,"out":319,"usd":0.026912}
{"at":"2026-10-18T02:07:19Z","model":"claude-sonnet-5-5","in":12251,"out":342,"usd":0.027922}
{"at":"2026-10-18T02:07:22Z","model":"claude-sonnet-5-5","in":14924,"out":285,"usd":0.032698}
{"at":"2026-10-18T02:07:28Z","model":"claude-sonnet-5-5","in":17307,"out":321,"usd":0.037824}
{"at":"2026-10-18T02:07:35Z","model":"claude-sonnet-5-5","in":20420,"out":540,"usd":0.04624}
{"at":"2026-10-18T02:07:46Z","model":"claude-sonnet-5-5","in":20700,"out":1659,"usd":0.05799}
{"at":"2026-10-18T02:07:54Z","model":"claude-sonnet-5-5","in":21516,"out":534,"usd":0.048372}
{"at":"2026-10-18T02:08:01Z","model":"claude-sonnet-5-5","in":21835,"out":578,"usd":0.04945}
{"at":"2026-10-18T02:08:10Z","model":"claude-sonnet-5-5","in":22012,"out":977,"usd":0.053794}
{"at":"2026-10-18T02:08:29Z","model":"claude-sonnet-5-5","in":22239,"out":2156,"usd":0.066038}
{"at":"2026-10-18T02:08:51Z","model":"claude-sonnet-5-5","in":23312,"out":3127,"usd":0.077894}
{"at":"2026-10-18T02:08:59Z","model":"claude-sonnet-5-5","in":25395,"out":540,"usd":0.05619}
{"at":"2026-10-18T02:09:09Z","model":"claude-sonnet-5-5","in":25790,"out":1327,"usd":0.06485}
{"at":"2026-10-18T02:09:18Z","model":"claude-sonnet-5-5","in":27350,"out":479,"usd":0.05949}
{"at":"2026-10-18T02:10:25Z","model":"claude-sonnet-5-5","in":27839,"out":1544,"usd":0.071118}
{"at":"2026-10-18T02:10:28Z","model":"claude-sonnet-5-5","in":29104,"out":312,"usd":0.061328}
{"at":"2026-10-18T04:06:03Z","model":"claude-haiku-5-5","in":5317,"out":2107,"usd":0.001585}
{"at":"2026-10-18T04:06:06Z","model":"claude-sonnet-5-5","in":2134,"out":26,"usd":0.004528}
{"at":"2026-10-18T04:06:09Z","model":"claude-sonnet-5-5","in":2662,"out":376,"usd":0.009084}
{"at":"2026-10-18T04:06:12Z","model":"claude-sonnet-5-5","in":29849,"out":442,"usd":0.064118}
{"at":"2026-10-18T04:06:17Z","model":"claude-sonnet-5-5","in":38782,"out":401,"usd":0.081574}
{"at":"2026-10-18T04:06:19Z","model":"claude-sonnet-5-5","in":44310,"out":211,"usd":0.09073}
{"at":"2026-10-18T04:06:22Z","model":"claude-sonnet-5-5","in":46904,"out":256,"usd":0.096368}
{"at":"2026-10-18T04:06:24Z","model":"claude-sonnet-5-5","in":48169,"out":279,"usd":0.099128}
{"at":"2026-10-18T04:06:29Z","model":"claude-sonnet-5-5","in":50276,"out":483,"usd":0.105382}
{"at":"2026-10-18T04:06:34Z","model":"claude-sonnet-5-5","in":52765,"out":612,"usd":0.11165}
{"at":"2026-10-18T04:06:39Z","model":"claude-sonnet-5-5","in":55365,"out":517,"usd":0.1159}
{"at":"2026-10-18T04:07:30Z","model":"claude-sonnet-5-5","in":57769,"out":6235,"usd":0.177888}
{"at":"2026-10-18T04:07:38Z","model":"claude-sonnet-5-5","in":60362,"out":821,"usd":0.128934}
{"at":"2026-10-18T04:08:31Z","model":"claude-sonnet-5-5","in":61644,"out":6597,"usd":0.189258}
{"at":"2026-10-18T04:09:16Z","model":"claude-sonnet-5-5","in":62547,"out":5473,"usd":0.179824}
{"at":"2026-10-18T04:11:13Z","model":"claude-sonnet-5-5","in":62660,"out":16000,"usd":0.28532}
{"at":"2026-10-18T04:13:02Z","model":"claude-sonnet-5-5","in":62694,"out":15930,"usd":0.284688}
{"at":"2026-10-18T04:13:30Z","model":"claude-sonnet-5-5","in":67478,"out":4905,"usd":0.184006}
{"at":"2026-10-18T04:13:32Z","model":"claude-sonnet-5-5","in":72483,"out":183,"usd":0.146796}
{"at":"2026-10-18T04:13:47Z","model":"claude-sonnet-5-5","in":73099,"out":2129,"usd":0.167488}
{"at":"2026-10-18T04:13:51Z","model":"claude-sonnet-5-5","in":75235,"out":325,"usd":0.15372}
{"at":"2026-10-18T04:14:02Z","model":"claude-sonnet-5-5","in":75591,"out":1385,"usd":0.165032}
{"at":"2026-10-18T04:14:06Z","model":"claude-sonnet-5-5","in":2473,"out":170,"usd":0.006646}
{"at":"2026-10-18T04:14:08Z","model":"claude-sonnet-5-5","in":5358,"out":173,"usd":0.012446}
{"at":"2026-10-18T04:14:10Z","model":"claude-sonnet-5-5","in":6785,"out":179,"usd":0.01536}
{"at":"2026-10-18T04:14:12Z","model":"claude-sonnet-5-5","in":9531,"out":152,"usd":0.020582}
{"at":"2026-10-18T04:14:14Z","model":"claude-sonnet-5-5","in":12333,"out":194,"usd":0.026606}
{"at":"2026-10-18T04:14:29Z","model":"claude-sonnet-5-5","in":14662,"out":2551,"usd":0.054834}
{"at":"2026-10-18T04:14:31Z","model":"claude-sonnet-5-5","in":17568,"out":121,"usd":0.036346}
{"at":"2026-10-18T04:14:34Z","model":"claude-sonnet-5-5","in":18361,"out":324,"usd":0.039962}
{"at":"2026-10-18T04:14:41Z","model":"claude-sonnet-5-5","in":18986,"out":760,"usd":0.045572}
{"at":"2026-10-18T04:16:11Z","model":"claude-haiku-5-5","in":13815,"out":3027,"usd":0.002895}
{"at":"2026-10-18T04:16:22Z","model":"claude-haiku-5-5","in":5746,"out":1940,"usd":0.001545}