codedeck 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/open.js +94 -2
  3. package/dist/cli/commands/open.js.map +1 -1
  4. package/dist/cli/commands/usage-backfill.js +149 -0
  5. package/dist/cli/commands/usage-backfill.js.map +1 -0
  6. package/dist/cli/commands/usage.js +49 -4
  7. package/dist/cli/commands/usage.js.map +1 -1
  8. package/dist/core/claude-transcript.js +139 -0
  9. package/dist/core/claude-transcript.js.map +1 -0
  10. package/dist/core/pricing.js +14 -5
  11. package/dist/core/pricing.js.map +1 -1
  12. package/dist/core/run-usage.js +53 -10
  13. package/dist/core/run-usage.js.map +1 -1
  14. package/dist/core/usage-source.js +17 -0
  15. package/dist/core/usage-source.js.map +1 -0
  16. package/dist/daemon/daemon.js +216 -8
  17. package/dist/daemon/daemon.js.map +1 -1
  18. package/dist/daemon/ipc.js +4 -3
  19. package/dist/daemon/ipc.js.map +1 -1
  20. package/dist/drivers/session-driver.js +7 -1
  21. package/dist/drivers/session-driver.js.map +1 -1
  22. package/dist/open/launchers/claude.js +78 -2
  23. package/dist/open/launchers/claude.js.map +1 -1
  24. package/dist/open/link-watcher.js +44 -0
  25. package/dist/open/link-watcher.js.map +1 -0
  26. package/dist/open/runtime.js +15 -12
  27. package/dist/open/runtime.js.map +1 -1
  28. package/dist/plugin/hooks/register.tsx +7 -11
  29. package/dist/plugin/hooks/session-id.sh +1 -1
  30. package/dist/plugin/mods/agents/pane.ts +35 -29
  31. package/dist/plugin/mods/agents/types.ts +5 -1
  32. package/dist/plugin/statusline.sh +40 -3
  33. package/dist/store/database.js +44 -0
  34. package/dist/store/database.js.map +1 -1
  35. package/dist/store/native-links.js +80 -0
  36. package/dist/store/native-links.js.map +1 -0
  37. package/dist/store/sessions.js +113 -41
  38. package/dist/store/sessions.js.map +1 -1
  39. package/dist/store/usage-ledger.js +153 -0
  40. package/dist/store/usage-ledger.js.map +1 -0
  41. package/package.json +1 -1
@@ -47,13 +47,8 @@ function vocab(entries: Record<string, string>): Record<string, string> {
47
47
  return Object.assign(Object.create(null) as Record<string, string>, entries);
48
48
  }
49
49
 
50
- const STATUS_DOT = vocab({
51
- working: "●",
52
- starting: "●",
53
- needs_input: "◉",
54
- });
55
50
  const STATUS_WORD = vocab({
56
- working: "Working now",
51
+ working: "Working",
57
52
  starting: "Starting",
58
53
  needs_input: "Waiting for you",
59
54
  completed: "Completed",
@@ -111,6 +106,8 @@ function toPaneRow(row: SessionRow): PaneRow {
111
106
  id: row.id,
112
107
  status: row.status || EMPTY_CELL,
113
108
  agent: row.agent || EMPTY_CELL,
109
+ model: text(row.model) || undefined,
110
+ effort: text(row.effort) || undefined,
114
111
  name: row.name || EMPTY_CELL,
115
112
  updatedAt: iso,
116
113
  };
@@ -133,7 +130,11 @@ export function selectPane(rows: SessionRow[], runId: string, budget?: number):
133
130
  );
134
131
  const orchestratorRow = matching.find((row) => row.origin === "open");
135
132
  const orchestrator = orchestratorRow
136
- ? { agent: text(orchestratorRow.agent) || EMPTY_CELL }
133
+ ? {
134
+ agent: text(orchestratorRow.agent) || EMPTY_CELL,
135
+ model: text(orchestratorRow.model) || undefined,
136
+ effort: text(orchestratorRow.effort) || undefined,
137
+ }
137
138
  : undefined;
138
139
  const workers = matching.filter((row) => row.origin !== "open");
139
140
  const ordered = [...workers].sort(compareRows);
@@ -179,16 +180,11 @@ function harnessLabel(agent: unknown): string {
179
180
  return HARNESS_LABEL[key] ?? cell(agent);
180
181
  }
181
182
 
182
- function dot(status: unknown): string {
183
- return STATUS_DOT[text(status)] ?? "○";
184
- }
185
-
186
183
  function statusWord(status: unknown): string {
187
184
  return STATUS_WORD[text(status)] ?? cell(status);
188
185
  }
189
186
 
190
- function footerLegend(columns: number): [string, string] {
191
- const fullStatus = " ● working ◉ waiting ○ finished";
187
+ function footerLegend(columns: number): string {
192
188
  const fullHarness =
193
189
  " " +
194
190
  glyph("claude") +
@@ -199,15 +195,11 @@ function footerLegend(columns: number): [string, string] {
199
195
  " Codex " +
200
196
  glyph("omp") +
201
197
  " OMP";
202
- if (fullStatus.length <= columns - 2 && fullHarness.length <= columns - 2) {
203
- return [fullStatus, fullHarness];
204
- }
198
+ if (fullHarness.length <= columns - 2) return fullHarness;
205
199
 
206
- const compactStatus = " ● wk ◉ wait ○ done";
207
200
  const iconHarness =
208
201
  " " + glyph("claude") + " " + glyph("opencode") + " " + glyph("codex") + " " + glyph("omp");
209
- if (compactStatus.length + 4 <= columns - 2) return [compactStatus, iconHarness];
210
- return [" ● ◉ ○", iconHarness];
202
+ return iconHarness;
211
203
  }
212
204
 
213
205
  function age(iso: unknown, now: number): string {
@@ -215,7 +207,7 @@ function age(iso: unknown, now: number): string {
215
207
  const then = Date.parse(iso);
216
208
  if (Number.isNaN(then)) return "";
217
209
  const minutes = Math.floor((now - then) / 60000);
218
- if (minutes < 1) return "agora";
210
+ if (minutes < 1) return "now";
219
211
  if (minutes < 60) return `${minutes}m`;
220
212
  return `${Math.floor(minutes / 60)}h`;
221
213
  }
@@ -224,6 +216,16 @@ function count(value: unknown): number {
224
216
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
225
217
  }
226
218
 
219
+ function detailLine(model: unknown, effort: unknown, status: string, when: string, width: number): string {
220
+ const state = status ? `${status}${when ? ` · ${when}` : ""}` : "";
221
+ const suffix = [text(effort).trim() ? `${cell(effort)} effort` : "", state]
222
+ .filter(Boolean)
223
+ .join(" · ");
224
+ const modelWidth = width - 3 - suffix.length - (suffix ? 3 : 0);
225
+ const modelText = text(model).trim() && modelWidth > 0 ? fit(cell(model), modelWidth).trimEnd() : "";
226
+ return ` ${[modelText, suffix].filter(Boolean).join(" · ")}`;
227
+ }
228
+
227
229
  // How old a row looks for eviction: an unparseable timestamp is the oldest
228
230
  // possible age, matching how compareRows parks such rows at the end of the
229
231
  // display. The orchestrator pins itself to +Infinity so it is the very last
@@ -256,7 +258,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
256
258
  );
257
259
  const orchestrator =
258
260
  source.orchestrator && typeof source.orchestrator === "object"
259
- ? (source.orchestrator as Partial<{ agent: string }>)
261
+ ? (source.orchestrator as Partial<{ agent: string; model: string; effort: string }>)
260
262
  : undefined;
261
263
  const hidden = count(source.hidden);
262
264
  const total = count(source.total);
@@ -322,8 +324,11 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
322
324
  if (orchestrator !== undefined) {
323
325
  blocks.push(
324
326
  cardBlock(Number.POSITIVE_INFINITY, "", [
325
- ` ${glyph(orchestrator.agent)} ● Orchestrator`,
327
+ ` ${glyph(orchestrator.agent)} Orchestrator`,
326
328
  ` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
329
+ ...(text(orchestrator.model).trim() || text(orchestrator.effort).trim()
330
+ ? [detailLine(orchestrator.model, orchestrator.effort, "", "", INNER)]
331
+ : []),
327
332
  ]),
328
333
  );
329
334
  }
@@ -337,9 +342,9 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
337
342
  const word = statusWord(status);
338
343
  blocks.push(
339
344
  cardBlock(rowAge(row.updatedAt), cell(row.id), [
340
- ` ${glyph(row.agent)} ${dot(row.status)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
345
+ ` ${glyph(row.agent)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
341
346
  ` ${cell(row.name)}`,
342
- when === "" ? ` ${word}` : ` ${word} · ${when}`,
347
+ detailLine(row.model, row.effort, word, when, INNER),
343
348
  ]),
344
349
  );
345
350
  }
@@ -373,8 +378,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
373
378
  const stemAbove = (list: Block[], i: number): boolean =>
374
379
  i > 0 && (list[i].kind === "card" || list[i - 1].kind === "card");
375
380
 
376
- // Rule 3: these ten lines are the frame. Whenever anything is drawn at all
377
- // they are drawn, and they are never the lines that the fit cuts.
381
+ // These frame lines are always drawn when the pane has content.
378
382
  const head = [
379
383
  frameTop(`Run canvas ${cell(source.runId)}`),
380
384
  frameRow(` ${total} sessions · ${working} working`),
@@ -382,11 +386,9 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
382
386
  frameSep(),
383
387
  frameRow(""),
384
388
  ];
385
- const [statusLegend, harnessLegend] = footerLegend(W);
389
+ const harnessLegend = footerLegend(W);
386
390
  const tail = [
387
- frameRow(""),
388
391
  frameSep(),
389
- frameRow(statusLegend),
390
392
  frameRow(harnessLegend),
391
393
  frameBot(),
392
394
  ];
@@ -434,6 +436,10 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
434
436
  if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
435
437
  else out.push(hiddenRow);
436
438
  }
439
+ if (limit !== undefined) {
440
+ const spare = limit - out.length - tail.length;
441
+ for (let i = 0; i < spare; i += 1) out.push(frameRow(""));
442
+ }
437
443
  out.push(...tail);
438
444
  return out;
439
445
  }
@@ -13,6 +13,8 @@ export interface SessionRow {
13
13
  origin?: string | null;
14
14
  name?: string;
15
15
  agent?: string;
16
+ model?: string;
17
+ effort?: string;
16
18
  status?: string;
17
19
  updatedAt?: string;
18
20
  }
@@ -26,6 +28,8 @@ export interface PaneRow {
26
28
  id: string;
27
29
  status: string;
28
30
  agent: string;
31
+ model?: string;
32
+ effort?: string;
29
33
  name: string;
30
34
  updatedAt?: string;
31
35
  }
@@ -34,7 +38,7 @@ export interface PaneRow {
34
38
  export interface PaneSnapshot {
35
39
  runId: string;
36
40
  /** The orchestrator's own row, the only one whose origin is "open". */
37
- orchestrator: { agent: string } | undefined;
41
+ orchestrator: { agent: string; model?: string; effort?: string } | undefined;
38
42
  rows: PaneRow[];
39
43
  /** Rows that matched the run but fell outside the budget. */
40
44
  hidden: number;
@@ -181,8 +181,17 @@ const getRunUsage = () => {
181
181
  const runId = text(process.env.CODEDECK_RUN_ID);
182
182
  if (!runId) return undefined;
183
183
 
184
+ const args = ["usage", runId, "--json"];
185
+ const sessionId = payload.session_id;
186
+ const observeCost = nonNegativeNumber(payload.cost?.total_cost_usd);
187
+ if (
188
+ typeof sessionId === "string" &&
189
+ /^[0-9a-fA-F-]{8,}$/.test(sessionId) &&
190
+ observeCost !== undefined
191
+ ) args.push("--observe", `${sessionId}=${observeCost}`);
192
+
184
193
  try {
185
- const output = execFileSync("codedeck", ["usage", runId, "--json"], {
194
+ const output = execFileSync("codedeck", args, {
186
195
  encoding: "utf8",
187
196
  stdio: ["ignore", "pipe", "ignore"],
188
197
  timeout: 1000,
@@ -215,8 +224,30 @@ const getRunUsage = () => {
215
224
  }
216
225
  };
217
226
 
227
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
228
+
229
+ const getOrchestratorUsage = (usage) => {
230
+ const value = usage.orchestrator;
231
+ if (
232
+ !isObject(value) ||
233
+ nonNegativeNumber(value.costUsd) === undefined ||
234
+ typeof value.costComplete !== "boolean" ||
235
+ nonNegativeNumber(value.inputTokens) === undefined ||
236
+ nonNegativeNumber(value.outputTokens) === undefined ||
237
+ nonNegativeNumber(value.cachedTokens) === undefined ||
238
+ !Array.isArray(value.sources)
239
+ ) return undefined;
240
+ if (!value.sources.every((source) =>
241
+ isObject(source) &&
242
+ typeof source.nativeId === "string" &&
243
+ nonNegativeNumber(source.costUsd) !== undefined
244
+ )) return undefined;
245
+ return value;
246
+ };
247
+
218
248
  const local = localCost();
219
249
  const runUsage = getRunUsage();
250
+ const orchestratorUsage = runUsage ? getOrchestratorUsage(runUsage) : undefined;
220
251
  const workerTokens = runUsage
221
252
  ? runUsage.inputTokens + runUsage.outputTokens + runUsage.cachedTokens
222
253
  : undefined;
@@ -227,8 +258,14 @@ const tokenField = () => {
227
258
 
228
259
  const runField = () => {
229
260
  if (!runUsage) return undefined;
230
- const total = (local ?? 0) + runUsage.costUsd;
231
- const incomplete = !runUsage.costComplete;
261
+ const sourceCost = orchestratorUsage
262
+ ? orchestratorUsage.sources.reduce(
263
+ (sum, source) => source.nativeId === payload.session_id ? sum : sum + source.costUsd,
264
+ 0,
265
+ )
266
+ : 0;
267
+ const total = (local ?? 0) + runUsage.costUsd + sourceCost;
268
+ const incomplete = !runUsage.costComplete || Boolean(orchestratorUsage && !orchestratorUsage.costComplete);
232
269
  if (!incomplete && total < COST_DISPLAY_THRESHOLD) return undefined;
233
270
  return paint(MUTED, "run ") + costAmount(total, incomplete);
234
271
  };
@@ -89,9 +89,53 @@ export class Database {
89
89
  active INTEGER NOT NULL DEFAULT 1
90
90
  );
91
91
 
92
+ CREATE TABLE IF NOT EXISTS usage_sources (
93
+ source_key TEXT PRIMARY KEY,
94
+ cost REAL,
95
+ input_tokens INTEGER,
96
+ output_tokens INTEGER,
97
+ cached_tokens INTEGER,
98
+ updated_at TEXT NOT NULL
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS usage_attributions (
102
+ session_id TEXT NOT NULL,
103
+ source_key TEXT NOT NULL,
104
+ cost REAL,
105
+ input_tokens INTEGER NOT NULL DEFAULT 0,
106
+ output_tokens INTEGER NOT NULL DEFAULT 0,
107
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
108
+ PRIMARY KEY (session_id, source_key),
109
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
110
+ FOREIGN KEY (source_key) REFERENCES usage_sources(source_key) ON DELETE CASCADE
111
+ );
112
+
113
+ CREATE TABLE IF NOT EXISTS session_native_links (
114
+ session_id TEXT NOT NULL,
115
+ native_id TEXT NOT NULL,
116
+ linked_at TEXT NOT NULL,
117
+ reconciled_at TEXT,
118
+ state TEXT,
119
+ PRIMARY KEY (session_id, native_id),
120
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS usage_legacy (
124
+ native_id TEXT PRIMARY KEY,
125
+ ended_at TEXT NOT NULL,
126
+ cwd TEXT,
127
+ repository TEXT,
128
+ model TEXT,
129
+ cost REAL,
130
+ input_tokens INTEGER NOT NULL DEFAULT 0,
131
+ output_tokens INTEGER NOT NULL DEFAULT 0,
132
+ cached_tokens INTEGER NOT NULL DEFAULT 0
133
+ );
134
+
92
135
  CREATE INDEX IF NOT EXISTS idx_events_session_seq ON events(session_id, sequence);
93
136
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
94
137
  CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at DESC);
138
+ CREATE INDEX IF NOT EXISTS idx_usage_attributions_source_key ON usage_attributions(source_key);
95
139
  CREATE UNIQUE INDEX IF NOT EXISTS idx_claims_active_session_path
96
140
  ON claims(session_id, path_glob) WHERE active = 1;
97
141
  `);
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import { SessionStore } from "./sessions.js";
2
+ function toNativeLink(row) {
3
+ return {
4
+ sessionId: row.session_id,
5
+ nativeId: row.native_id,
6
+ linkedAt: row.linked_at,
7
+ reconciledAt: row.reconciled_at,
8
+ state: row.state,
9
+ };
10
+ }
11
+ export class NativeLinkStore {
12
+ db;
13
+ constructor(db) {
14
+ this.db = db;
15
+ }
16
+ link(sessionId, nativeId) {
17
+ const previous = this.db.prepare(`
18
+ SELECT native_id FROM session_native_links
19
+ WHERE session_id = ? AND native_id <> ? AND reconciled_at IS NULL
20
+ ORDER BY linked_at, native_id
21
+ `).all(sessionId, nativeId).map((row) => row.native_id);
22
+ const result = this.db.prepare(`
23
+ INSERT INTO session_native_links (session_id, native_id, linked_at)
24
+ VALUES (?, ?, ?)
25
+ ON CONFLICT(session_id, native_id) DO NOTHING
26
+ `).run(sessionId, nativeId, new Date().toISOString());
27
+ return { created: Number(result.changes) > 0, previous };
28
+ }
29
+ unreconciled(sessionId) {
30
+ const rows = this.db.prepare(`
31
+ SELECT session_id, native_id, linked_at, reconciled_at, state
32
+ FROM session_native_links
33
+ WHERE session_id = ? AND reconciled_at IS NULL
34
+ ORDER BY linked_at, native_id
35
+ `).all(sessionId);
36
+ return rows.map(toNativeLink);
37
+ }
38
+ linksFor(sessionIds) {
39
+ if (sessionIds.length === 0)
40
+ return [];
41
+ const placeholders = sessionIds.map(() => "?").join(", ");
42
+ const rows = this.db.prepare(`
43
+ SELECT session_id, native_id, linked_at, reconciled_at, state
44
+ FROM session_native_links
45
+ WHERE session_id IN (${placeholders})
46
+ ORDER BY session_id, linked_at, native_id
47
+ `).all(...sessionIds);
48
+ return rows.map(toNativeLink);
49
+ }
50
+ markReconciled(sessionId, nativeId, state) {
51
+ this.db.prepare(`
52
+ UPDATE session_native_links SET reconciled_at = ?, state = ?
53
+ WHERE session_id = ? AND native_id = ?
54
+ `).run(new Date().toISOString(), state, sessionId, nativeId);
55
+ }
56
+ staleOpenRows(isDead) {
57
+ const candidates = this.db.prepare(`
58
+ SELECT DISTINCT sessions.id, sessions.status
59
+ FROM sessions
60
+ INNER JOIN session_native_links
61
+ ON session_native_links.session_id = sessions.id
62
+ WHERE sessions.origin = 'open'
63
+ AND session_native_links.reconciled_at IS NULL
64
+ AND sessions.status IN ('interrupted', 'working')
65
+ ORDER BY sessions.created_at, sessions.id
66
+ `).all();
67
+ const sessions = new SessionStore(this.db);
68
+ const stale = [];
69
+ for (const candidate of candidates) {
70
+ const session = sessions.get(candidate.id);
71
+ if (!session)
72
+ continue;
73
+ if (candidate.status === "interrupted" || (candidate.status === "working" && isDead(session))) {
74
+ stale.push(session);
75
+ }
76
+ }
77
+ return stale;
78
+ }
79
+ }
80
+ //# sourceMappingURL=native-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-links.js","sourceRoot":"","sources":["../../src/store/native-links.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { isActiveStatus } from "../core/session.js";
2
- import { computeSessionCost } from "../core/pricing.js";
2
+ import { cachedInInputFor, computeSessionCost } from "../core/pricing.js";
3
3
  function rowToSession(row) {
4
4
  // `failure` is stored as JSON text; tolerate a corrupt blob rather than
5
5
  // losing the whole session row over it.
@@ -133,21 +133,29 @@ export class SessionStore {
133
133
  worktree: patch.worktree,
134
134
  branch: patch.branch,
135
135
  base_commit: patch.baseCommit,
136
- pid: patch.pid,
137
- completed_at: patch.completedAt ? patch.completedAt.toISOString() : undefined,
136
+ pid: patch.pid === undefined ? undefined : patch.pid ?? null,
137
+ completed_at: patch.completedAt === undefined
138
+ ? undefined
139
+ : patch.completedAt === null
140
+ ? null
141
+ : patch.completedAt.toISOString(),
138
142
  usage_input_tokens: patch.usage?.inputTokens,
139
143
  usage_output_tokens: patch.usage?.outputTokens,
140
144
  usage_cached_tokens: patch.usage?.cachedTokens,
141
145
  usage_cost: patch.usage?.cost,
142
- last_event: patch.lastEvent,
146
+ last_event: patch.lastEvent === undefined ? undefined : patch.lastEvent ?? null,
143
147
  effort: patch.effort,
144
148
  fast: patch.fast === undefined ? undefined : patch.fast ? 1 : 0,
145
149
  sandbox: patch.sandbox,
146
150
  dangerously_bypass_approvals_and_sandbox: patch.dangerouslyBypassApprovalsAndSandbox === undefined ? undefined : patch.dangerouslyBypassApprovalsAndSandbox ? 1 : 0,
147
- pid_start_time: patch.pidStartTime,
151
+ pid_start_time: patch.pidStartTime === undefined ? undefined : patch.pidStartTime ?? null,
148
152
  log_offset: patch.logOffset,
149
153
  stderr_offset: patch.stderrOffset,
150
- failure: patch.failure === undefined ? undefined : JSON.stringify(patch.failure),
154
+ failure: patch.failure === undefined
155
+ ? undefined
156
+ : patch.failure === null
157
+ ? null
158
+ : JSON.stringify(patch.failure),
151
159
  origin: patch.origin,
152
160
  pending_message: patch.pendingMessage === undefined ? undefined : (patch.pendingMessage ?? null),
153
161
  pending_at: patch.pendingAt === undefined ? undefined : (patch.pendingAt ?? null),
@@ -182,6 +190,10 @@ export class SessionStore {
182
190
  const row = this.db.prepare(`SELECT * FROM sessions WHERE native_session_id = ? LIMIT 1`).get(nativeId);
183
191
  return row ? rowToSession(row) : null;
184
192
  }
193
+ listOpenByNativeId(nativeId) {
194
+ const rows = this.db.prepare(`SELECT * FROM sessions WHERE origin = 'open' AND native_session_id = ? ORDER BY updated_at DESC, id ASC`).all(nativeId);
195
+ return rows.map(rowToSession);
196
+ }
185
197
  queryUsage(params = {}) {
186
198
  const { since, until } = resolveUsageDateRange(params);
187
199
  const rows = this.db.prepare(`
@@ -189,11 +201,28 @@ export class SessionStore {
189
201
  id, run_id, name, agent, model, status,
190
202
  repository, cwd, worktree,
191
203
  created_at, updated_at, completed_at,
192
- usage_input_tokens, usage_output_tokens, usage_cached_tokens, usage_cost
204
+ usage_input_tokens, usage_output_tokens, usage_cached_tokens, usage_cost, origin
193
205
  FROM sessions
194
206
  WHERE created_at >= ? AND created_at <= ?
195
207
  ORDER BY created_at ASC
196
208
  `).all(since, until);
209
+ const hasLegacyTable = this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'usage_legacy'`).get() !== undefined;
210
+ const legacyRows = hasLegacyTable
211
+ ? this.db.prepare(`
212
+ SELECT native_id, ended_at, cwd, repository, model, cost,
213
+ input_tokens, output_tokens, cached_tokens
214
+ FROM usage_legacy
215
+ WHERE ended_at >= ? AND ended_at <= ?
216
+ ORDER BY ended_at ASC
217
+ `).all(since, until)
218
+ : [];
219
+ const unknownOpenCostSessionIds = new Set(this.db.prepare(`
220
+ SELECT DISTINCT session_native_links.session_id
221
+ FROM session_native_links
222
+ INNER JOIN sessions ON sessions.id = session_native_links.session_id
223
+ WHERE sessions.origin = 'open'
224
+ AND session_native_links.state IN ('missing', 'no-price')
225
+ `).all().map((row) => row.session_id));
197
226
  const totals = {
198
227
  sessionCount: 0,
199
228
  activeSessionCount: 0,
@@ -212,10 +241,39 @@ export class SessionStore {
212
241
  const byModelMap = new Map();
213
242
  const byAgentMap = new Map();
214
243
  const byRunMap = new Map();
244
+ const byOriginMap = new Map();
215
245
  const repoFilter = params.repository?.toLowerCase();
216
246
  const modelFilter = params.model?.toLowerCase();
217
247
  const agentFilter = params.agent;
218
248
  const runIdFilter = params.runId;
249
+ const accumulate = (map, key, inputTokens, outputTokens, cachedTokens, totalTokens, cost, label) => {
250
+ let bucket = map.get(key);
251
+ if (!bucket) {
252
+ bucket = {
253
+ key,
254
+ label: label ?? key,
255
+ sessionCount: 0,
256
+ inputTokens: 0,
257
+ outputTokens: 0,
258
+ cachedTokens: 0,
259
+ totalTokens: 0,
260
+ costUsd: 0,
261
+ costComplete: true,
262
+ };
263
+ map.set(key, bucket);
264
+ }
265
+ bucket.sessionCount++;
266
+ bucket.inputTokens += inputTokens;
267
+ bucket.outputTokens += outputTokens;
268
+ bucket.cachedTokens += cachedTokens;
269
+ bucket.totalTokens += totalTokens;
270
+ if (cost === null) {
271
+ bucket.costComplete = false;
272
+ }
273
+ else {
274
+ bucket.costUsd += cost;
275
+ }
276
+ };
219
277
  for (const row of rows) {
220
278
  if (runIdFilter && row.run_id !== runIdFilter)
221
279
  continue;
@@ -232,11 +290,13 @@ export class SessionStore {
232
290
  const outputTokens = row.usage_output_tokens ?? 0;
233
291
  const cachedTokens = row.usage_cached_tokens ?? 0;
234
292
  const totalTokens = inputTokens + outputTokens + cachedTokens;
235
- const cost = computeSessionCost({
293
+ const calculatedCost = computeSessionCost({
236
294
  model: row.model,
295
+ cachedInInput: cachedInInputFor(row.agent),
237
296
  reportedCost: row.usage_cost,
238
297
  usage: { inputTokens, outputTokens, cachedTokens },
239
298
  });
299
+ const cost = unknownOpenCostSessionIds.has(row.id) ? null : calculatedCost;
240
300
  totals.sessionCount++;
241
301
  if (isActiveStatus(row.status))
242
302
  totals.activeSessionCount++;
@@ -255,49 +315,59 @@ export class SessionStore {
255
315
  else {
256
316
  totals.costUsd += cost;
257
317
  }
258
- const accumulate = (map, key, label) => {
259
- let bucket = map.get(key);
260
- if (!bucket) {
261
- bucket = {
262
- key,
263
- label: label ?? key,
264
- sessionCount: 0,
265
- inputTokens: 0,
266
- outputTokens: 0,
267
- cachedTokens: 0,
268
- totalTokens: 0,
269
- costUsd: 0,
270
- costComplete: true,
271
- };
272
- map.set(key, bucket);
273
- }
274
- bucket.sessionCount++;
275
- bucket.inputTokens += inputTokens;
276
- bucket.outputTokens += outputTokens;
277
- bucket.cachedTokens += cachedTokens;
278
- bucket.totalTokens += totalTokens;
279
- if (cost === null) {
280
- bucket.costComplete = false;
281
- }
282
- else {
283
- bucket.costUsd += cost;
284
- }
285
- };
286
318
  // Day (local date string YYYY-MM-DD)
287
319
  const d = new Date(row.created_at);
288
320
  const dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
289
- accumulate(byDayMap, dayKey);
321
+ accumulate(byDayMap, dayKey, inputTokens, outputTokens, cachedTokens, totalTokens, cost);
290
322
  // Repo (normalized project name across worktrees)
291
323
  const repoKey = normalizeProjectName(row);
292
- accumulate(byRepoMap, repoKey);
324
+ accumulate(byRepoMap, repoKey, inputTokens, outputTokens, cachedTokens, totalTokens, cost);
293
325
  // Model
294
- accumulate(byModelMap, row.model || "unknown");
326
+ accumulate(byModelMap, row.model || "unknown", inputTokens, outputTokens, cachedTokens, totalTokens, cost);
295
327
  // Agent
296
- accumulate(byAgentMap, row.agent);
328
+ accumulate(byAgentMap, row.agent, inputTokens, outputTokens, cachedTokens, totalTokens, cost);
297
329
  // Run
298
330
  if (row.run_id) {
299
- accumulate(byRunMap, row.run_id, row.name ? `${row.name} (${row.run_id.slice(0, 8)})` : row.run_id.slice(0, 8));
331
+ accumulate(byRunMap, row.run_id, inputTokens, outputTokens, cachedTokens, totalTokens, cost, row.name ? `${row.name} (${row.run_id.slice(0, 8)})` : row.run_id.slice(0, 8));
300
332
  }
333
+ accumulate(byOriginMap, row.origin === "open" ? "orchestrator" : "worker", inputTokens, outputTokens, cachedTokens, totalTokens, cost);
334
+ }
335
+ for (const row of legacyRows) {
336
+ if (runIdFilter)
337
+ continue;
338
+ if (agentFilter && agentFilter !== "claude")
339
+ continue;
340
+ if (modelFilter && (!row.model || !row.model.toLowerCase().includes(modelFilter)))
341
+ continue;
342
+ if (repoFilter) {
343
+ const repoStr = [row.repository, row.cwd].filter(Boolean).join(" ").toLowerCase();
344
+ if (!repoStr.includes(repoFilter))
345
+ continue;
346
+ }
347
+ const inputTokens = row.input_tokens;
348
+ const outputTokens = row.output_tokens;
349
+ const cachedTokens = row.cached_tokens;
350
+ const totalTokens = inputTokens + outputTokens + cachedTokens;
351
+ const cost = row.cost;
352
+ totals.sessionCount++;
353
+ totals.inputTokens += inputTokens;
354
+ totals.outputTokens += outputTokens;
355
+ totals.cachedTokens += cachedTokens;
356
+ totals.totalTokens += totalTokens;
357
+ if (cost === null) {
358
+ totals.costComplete = false;
359
+ totals.sessionsWithoutCost++;
360
+ }
361
+ else {
362
+ totals.costUsd += cost;
363
+ }
364
+ const d = new Date(row.ended_at);
365
+ const dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
366
+ accumulate(byDayMap, dayKey, inputTokens, outputTokens, cachedTokens, totalTokens, cost);
367
+ accumulate(byRepoMap, normalizeProjectName({ repository: row.repository, cwd: row.cwd }), inputTokens, outputTokens, cachedTokens, totalTokens, cost);
368
+ accumulate(byModelMap, row.model || "unknown", inputTokens, outputTokens, cachedTokens, totalTokens, cost);
369
+ accumulate(byAgentMap, "claude", inputTokens, outputTokens, cachedTokens, totalTokens, cost);
370
+ accumulate(byOriginMap, "orchestrator", inputTokens, outputTokens, cachedTokens, totalTokens, cost);
301
371
  }
302
372
  const sortDescending = (a, b) => {
303
373
  if (b.costUsd !== a.costUsd)
@@ -309,6 +379,7 @@ export class SessionStore {
309
379
  const byModel = [...byModelMap.values()].sort(sortDescending);
310
380
  const byAgent = [...byAgentMap.values()].sort(sortDescending);
311
381
  const byRun = [...byRunMap.values()].sort(sortDescending);
382
+ const byOrigin = [...byOriginMap.values()].sort(sortDescending);
312
383
  return {
313
384
  range: {
314
385
  period: params.period,
@@ -321,6 +392,7 @@ export class SessionStore {
321
392
  byModel,
322
393
  byAgent,
323
394
  byRun,
395
+ byOrigin,
324
396
  };
325
397
  }
326
398
  }