codedeck 0.5.3 → 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 (50) hide show
  1. package/README.md +2 -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/review.js +122 -0
  5. package/dist/cli/commands/review.js.map +1 -0
  6. package/dist/cli/commands/usage-backfill.js +149 -0
  7. package/dist/cli/commands/usage-backfill.js.map +1 -0
  8. package/dist/cli/commands/usage.js +49 -4
  9. package/dist/cli/commands/usage.js.map +1 -1
  10. package/dist/cli/index.js +3 -3
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/core/claude-transcript.js +139 -0
  13. package/dist/core/claude-transcript.js.map +1 -0
  14. package/dist/core/pricing.js +14 -5
  15. package/dist/core/pricing.js.map +1 -1
  16. package/dist/core/run-usage.js +53 -10
  17. package/dist/core/run-usage.js.map +1 -1
  18. package/dist/core/usage-source.js +17 -0
  19. package/dist/core/usage-source.js.map +1 -0
  20. package/dist/daemon/daemon.js +216 -8
  21. package/dist/daemon/daemon.js.map +1 -1
  22. package/dist/daemon/ipc.js +4 -3
  23. package/dist/daemon/ipc.js.map +1 -1
  24. package/dist/drivers/opencode/driver.js +24 -2
  25. package/dist/drivers/opencode/driver.js.map +1 -1
  26. package/dist/drivers/session-driver.js +7 -1
  27. package/dist/drivers/session-driver.js.map +1 -1
  28. package/dist/open/launchers/claude.js +78 -2
  29. package/dist/open/launchers/claude.js.map +1 -1
  30. package/dist/open/link-watcher.js +44 -0
  31. package/dist/open/link-watcher.js.map +1 -0
  32. package/dist/open/runtime.js +15 -12
  33. package/dist/open/runtime.js.map +1 -1
  34. package/dist/plugin/hooks/pane-toggle.ts +13 -0
  35. package/dist/plugin/hooks/register.tsx +20 -29
  36. package/dist/plugin/hooks/session-id.sh +1 -1
  37. package/dist/plugin/mods/agents/pane.ts +67 -30
  38. package/dist/plugin/mods/agents/types.ts +5 -1
  39. package/dist/plugin/statusline.sh +40 -3
  40. package/dist/store/database.js +44 -0
  41. package/dist/store/database.js.map +1 -1
  42. package/dist/store/native-links.js +80 -0
  43. package/dist/store/native-links.js.map +1 -0
  44. package/dist/store/sessions.js +113 -41
  45. package/dist/store/sessions.js.map +1 -1
  46. package/dist/store/usage-ledger.js +153 -0
  47. package/dist/store/usage-ledger.js.map +1 -0
  48. package/dist/web/review-page.js +1 -2
  49. package/dist/web/review-page.js.map +1 -1
  50. package/package.json +1 -1
@@ -7,6 +7,7 @@ import type { Register } from "claude-code";
7
7
  import { formatPane, paneButtonLabel, selectPane } from "../mods/agents/pane.js";
8
8
  import { parseRows } from "../mods/agents/parse.js";
9
9
  import type { PaneSnapshot } from "../mods/agents/types.js";
10
+ import { togglePane } from "./pane-toggle.js";
10
11
 
11
12
  // Stable pane id: 1 to 64 letters, digits, "_" or "-". open carries it into
12
13
  // e.requestId on the render event, which is how this module tells its own pane
@@ -42,6 +43,12 @@ type Engine$ = {
42
43
  };
43
44
  };
44
45
 
46
+ const toggleAgentsPane = async ($: Engine$, isOpen: boolean): Promise<boolean> =>
47
+ togglePane(isOpen, {
48
+ open: () => $.ui.open({ id: PANE_ID, side: "right" }),
49
+ close: () => $.ui.close({ id: PANE_ID }),
50
+ });
51
+
45
52
  // Passing $ into a helper is allowed, verified. What the engine refuses is
46
53
  // pulling a namespace off it: `const P = $.process` fails to load the module.
47
54
  // So refresh takes $ as a parameter and the state register owns travels beside
@@ -118,20 +125,15 @@ export const register: Register = (on) => {
118
125
  description: "toggle the codedeck agents pane",
119
126
  immediate: true,
120
127
  });
121
- // Only open when there is actually a run behind the pane. A session that
122
- // loads this plugin by manual --plugin-dir or a global install has no
123
- // CODEDECK_RUN_ID, and the dock there is permanently empty while eating
124
- // most of the screen for the whole session. /band stays the escape hatch
125
- // for anyone who wants the pane anyway.
128
+ // Only show the button when there is actually a run behind the pane. A
129
+ // session that loads this plugin by manual --plugin-dir or a global
130
+ // install has no CODEDECK_RUN_ID, and its dock stays empty. /band remains
131
+ // available for anyone who wants to open it anyway.
126
132
  const runId = await $.env.get("CODEDECK_RUN_ID");
127
133
  state.hasRun = Boolean(runId);
128
134
  if (runId) {
129
- // No width is requested: the engine sizes the dock itself and reports
130
- // the usable content back as e.props.bodyColumns on every render.
131
- await $.ui.open({ id: PANE_ID, side: "right" });
132
- paneOpen = true;
133
- // Populate before the first turn, or the pane stays blank through the
134
- // whole opening exchange, which is exactly when the run is dispatched.
135
+ // Prime the button label without taking screen space when the session
136
+ // starts. The panel opens only on an explicit button press or /band.
135
137
  void refresh($, state);
136
138
  }
137
139
  return next(e);
@@ -143,14 +145,9 @@ export const register: Register = (on) => {
143
145
  // is the one call here not yet verified in a PTY session, paneOpen stays
144
146
  // true and the next /band retries the close, instead of the flag and the
145
147
  // pane desyncing for the rest of the session.
146
- if (paneOpen) {
147
- await $.ui.close({ id: PANE_ID });
148
- paneOpen = false;
149
- } else {
150
- await $.ui.open({ id: PANE_ID, side: "right" });
151
- paneOpen = true;
152
- }
148
+ paneOpen = await toggleAgentsPane($, paneOpen);
153
149
  await $.ui.invalidate("ui.render");
150
+ if (paneOpen) void refresh($, state);
154
151
  return { text: paneOpen ? "agents pane open" : "agents pane closed" };
155
152
  });
156
153
 
@@ -175,14 +172,9 @@ export const register: Register = (on) => {
175
172
  // A $ captured from a past render does work, verified, but it outlives the
176
173
  // event it came from and nothing promises how long.
177
174
  if ((e as { element?: string }).element === BUTTON_KEY) {
178
- await $.ui.open({ id: PANE_ID, side: "right" });
179
- // Opening an already open pane is a no-op, verified live, so the button
180
- // never has to know the truth. Re-syncing the flag is the point: the
181
- // pane's own close box closes it without firing a single hook, and after
182
- // that one /band would toggle the wrong way.
183
- paneOpen = true;
175
+ paneOpen = await toggleAgentsPane($, paneOpen);
184
176
  await $.ui.invalidate("ui.render");
185
- void refresh($, state);
177
+ if (paneOpen) void refresh($, state);
186
178
  }
187
179
  return await next(e);
188
180
  });
@@ -195,10 +187,9 @@ export const register: Register = (on) => {
195
187
  // wrong shape".
196
188
  on("ui.render", async ($, e, next) => {
197
189
  if (e.surface !== "terminal") return await next(e);
198
- // The way back in. The pane can be closed from its own close box, which
199
- // fires no hook, so without a visible affordance the only recovery is
200
- // knowing that /band exists. One button above the prompt, only when there
201
- // is a run behind it.
190
+ // The pane can be closed from its own close box, which fires no hook.
191
+ // Keep one button above the prompt as the visible toggle for the state
192
+ // transitions this module owns.
202
193
  if (e.component === "AbovePrompt") {
203
194
  if (!state.hasRun) return await next(e);
204
195
  const { Box, Button } = await $.ui.resolve(e, "Box", "Button");
@@ -20,4 +20,4 @@ target=${CODEDECK_SESSION_FILE:-}
20
20
  id=$(grep -oE '"session_id":"[0-9a-fA-F-]+"' | head -1 | cut -d'"' -f4)
21
21
  [[ -n "$id" ]] || exit 0
22
22
 
23
- printf '%s' "$id" > "$target"
23
+ printf '%s\n' "$id" >> "$target"
@@ -1,6 +1,6 @@
1
1
  // Canvas layer of the orchestrator agents pane.
2
2
  //
3
- // The `codedeck web` canvas, drawn as ASCII boxes for a docked terminal pane.
3
+ // The run canvas, drawn as ASCII boxes for a docked terminal pane.
4
4
  // Two pure functions behind one frozen interface: selectPane narrows the
5
5
  // daemon's full session list to one run, formatPane turns that snapshot into
6
6
  // the lines of the drawing and fits them to the height the pane was given.
@@ -47,24 +47,22 @@ 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",
60
55
  stopped: "Paused",
61
56
  failed: "Failed",
62
57
  });
58
+ // Brand marks from the JetBrains Nerd Font and Omarchy icon font. The pane is
59
+ // a terminal surface, so these keep the identity of each harness without
60
+ // spending the width of its full name.
63
61
  const HARNESS_GLYPH = vocab({
64
- claude: "▲",
65
- opencode: "■",
66
- codex: "◆",
67
- omp: "⬟",
62
+ claude: "\uEC82",
63
+ opencode: "\uE902",
64
+ codex: "\uEC81",
65
+ omp: "\uE903",
68
66
  });
69
67
  const HARNESS_LABEL = vocab({
70
68
  claude: "Claude",
@@ -108,6 +106,8 @@ function toPaneRow(row: SessionRow): PaneRow {
108
106
  id: row.id,
109
107
  status: row.status || EMPTY_CELL,
110
108
  agent: row.agent || EMPTY_CELL,
109
+ model: text(row.model) || undefined,
110
+ effort: text(row.effort) || undefined,
111
111
  name: row.name || EMPTY_CELL,
112
112
  updatedAt: iso,
113
113
  };
@@ -130,7 +130,11 @@ export function selectPane(rows: SessionRow[], runId: string, budget?: number):
130
130
  );
131
131
  const orchestratorRow = matching.find((row) => row.origin === "open");
132
132
  const orchestrator = orchestratorRow
133
- ? { 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
+ }
134
138
  : undefined;
135
139
  const workers = matching.filter((row) => row.origin !== "open");
136
140
  const ordered = [...workers].sort(compareRows);
@@ -176,20 +180,34 @@ function harnessLabel(agent: unknown): string {
176
180
  return HARNESS_LABEL[key] ?? cell(agent);
177
181
  }
178
182
 
179
- function dot(status: unknown): string {
180
- return STATUS_DOT[text(status)] ?? "○";
181
- }
182
-
183
183
  function statusWord(status: unknown): string {
184
184
  return STATUS_WORD[text(status)] ?? cell(status);
185
185
  }
186
186
 
187
+ function footerLegend(columns: number): string {
188
+ const fullHarness =
189
+ " " +
190
+ glyph("claude") +
191
+ " Claude " +
192
+ glyph("opencode") +
193
+ " OpenCode " +
194
+ glyph("codex") +
195
+ " Codex " +
196
+ glyph("omp") +
197
+ " OMP";
198
+ if (fullHarness.length <= columns - 2) return fullHarness;
199
+
200
+ const iconHarness =
201
+ " " + glyph("claude") + " " + glyph("opencode") + " " + glyph("codex") + " " + glyph("omp");
202
+ return iconHarness;
203
+ }
204
+
187
205
  function age(iso: unknown, now: number): string {
188
206
  if (typeof iso !== "string") return "";
189
207
  const then = Date.parse(iso);
190
208
  if (Number.isNaN(then)) return "";
191
209
  const minutes = Math.floor((now - then) / 60000);
192
- if (minutes < 1) return "agora";
210
+ if (minutes < 1) return "now";
193
211
  if (minutes < 60) return `${minutes}m`;
194
212
  return `${Math.floor(minutes / 60)}h`;
195
213
  }
@@ -198,6 +216,16 @@ function count(value: unknown): number {
198
216
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
199
217
  }
200
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
+
201
229
  // How old a row looks for eviction: an unparseable timestamp is the oldest
202
230
  // possible age, matching how compareRows parks such rows at the end of the
203
231
  // display. The orchestrator pins itself to +Infinity so it is the very last
@@ -230,7 +258,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
230
258
  );
231
259
  const orchestrator =
232
260
  source.orchestrator && typeof source.orchestrator === "object"
233
- ? (source.orchestrator as Partial<{ agent: string }>)
261
+ ? (source.orchestrator as Partial<{ agent: string; model: string; effort: string }>)
234
262
  : undefined;
235
263
  const hidden = count(source.hidden);
236
264
  const total = count(source.total);
@@ -296,8 +324,11 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
296
324
  if (orchestrator !== undefined) {
297
325
  blocks.push(
298
326
  cardBlock(Number.POSITIVE_INFINITY, "", [
299
- ` ${glyph(orchestrator.agent)} ● Orchestrator`,
327
+ ` ${glyph(orchestrator.agent)} Orchestrator`,
300
328
  ` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
329
+ ...(text(orchestrator.model).trim() || text(orchestrator.effort).trim()
330
+ ? [detailLine(orchestrator.model, orchestrator.effort, "", "", INNER)]
331
+ : []),
301
332
  ]),
302
333
  );
303
334
  }
@@ -311,9 +342,9 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
311
342
  const word = statusWord(status);
312
343
  blocks.push(
313
344
  cardBlock(rowAge(row.updatedAt), cell(row.id), [
314
- ` ${glyph(row.agent)} ${dot(row.status)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
345
+ ` ${glyph(row.agent)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
315
346
  ` ${cell(row.name)}`,
316
- when === "" ? ` ${word}` : ` ${word} · ${when}`,
347
+ detailLine(row.model, row.effort, word, when, INNER),
317
348
  ]),
318
349
  );
319
350
  }
@@ -347,8 +378,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
347
378
  const stemAbove = (list: Block[], i: number): boolean =>
348
379
  i > 0 && (list[i].kind === "card" || list[i - 1].kind === "card");
349
380
 
350
- // Rule 3: these ten lines are the frame. Whenever anything is drawn at all
351
- // 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.
352
382
  const head = [
353
383
  frameTop(`Run canvas ${cell(source.runId)}`),
354
384
  frameRow(` ${total} sessions · ${working} working`),
@@ -356,11 +386,10 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
356
386
  frameSep(),
357
387
  frameRow(""),
358
388
  ];
389
+ const harnessLegend = footerLegend(W);
359
390
  const tail = [
360
- frameRow(""),
361
391
  frameSep(),
362
- frameRow(" ● working ◉ waiting ○ finished"),
363
- frameRow(" ▲ Claude ■ OpenCode ◆ Codex ⬟ OMP"),
392
+ frameRow(harnessLegend),
364
393
  frameBot(),
365
394
  ];
366
395
  const fixed = head.length + tail.length;
@@ -407,6 +436,10 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined
407
436
  if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
408
437
  else out.push(hiddenRow);
409
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
+ }
410
443
  out.push(...tail);
411
444
  return out;
412
445
  }
@@ -443,10 +476,14 @@ export function paneButtonLabel(snapshot: PaneSnapshot | undefined): string {
443
476
  if (!snapshot || typeof snapshot !== "object") return "agents";
444
477
  try {
445
478
  const rows = Array.isArray(snapshot.rows) ? snapshot.rows : [];
446
- const working = rows.filter((row) => LIVE.has(row?.status ?? "")).length;
447
- if (working > 0) return `${working} working`;
448
- const waiting = rows.filter((row) => WAIT.has(row?.status ?? "")).length;
449
- return waiting > 0 ? `${waiting} waiting for you` : "no active agents";
479
+ const workingRows = rows.filter((row) => LIVE.has(row?.status ?? ""));
480
+ if (workingRows.length > 0) {
481
+ return `${workingRows.length} working ${workingRows.map((row) => glyph(row?.agent)).join("")}`;
482
+ }
483
+ const waitingRows = rows.filter((row) => WAIT.has(row?.status ?? ""));
484
+ return waitingRows.length > 0
485
+ ? `${waitingRows.length} waiting for you ${waitingRows.map((row) => glyph(row?.agent)).join("")}`
486
+ : "no active agents";
450
487
  } catch {
451
488
  return "agents";
452
489
  }
@@ -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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"}