@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451

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 (165) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +70 -22
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +192 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +29 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -19,6 +19,8 @@ export interface ProviderCliDraftContext {
19
19
  }
20
20
  export interface ProviderCliRunResult {
21
21
  readonly summary: string;
22
+ readonly resultCommitSha?: string | null;
23
+ readonly publishedCommitSha?: string | null;
22
24
  readonly spec?: RunnerDispatchSpec | null;
23
25
  readonly artifact?: RunnerDispatchArtifact | null;
24
26
  readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
@@ -4,6 +4,8 @@ export declare function messageFromError(error: unknown): string;
4
4
  export declare function clip(value: string, maxLength: number): string;
5
5
  export interface ProviderOutputState {
6
6
  readonly sessionLabel: string | null;
7
+ /** Stable provider conversation identifier used for an in-place repair turn. */
8
+ readonly providerThreadId: string | null;
7
9
  finalMessage: string | null;
8
10
  /** True only while the provider has reported a tool/command start without a matching completion. */
9
11
  readonly hasActiveToolCall: boolean;
@@ -13,6 +15,14 @@ export interface ProviderOutputState {
13
15
  }
14
16
  export declare function createProviderOutputState(provider: string): ProviderOutputState;
15
17
  export declare function parseJsonLine(line: string): Record<string, unknown> | null;
18
+ /**
19
+ * Claude Code exposes MCP tools as `mcp__<server>__<tool>` and replaces characters outside
20
+ * `[A-Za-z0-9_-]` with `_`, so `forge.complete_qa` arrives as `mcp__forge__forge_complete_qa`.
21
+ */
22
+ export declare function parseClaudeMcpToolName(name: string): {
23
+ readonly server: string;
24
+ readonly tool: string;
25
+ } | null;
16
26
  export declare function formatCodexMcpToolCallEvents(type: string, event: Record<string, unknown>): readonly RunnerDispatchEvent[];
17
27
  export declare function objectField(source: Record<string, unknown>, key: string): Record<string, unknown>;
18
28
  export declare function stringField(source: Record<string, unknown>, key: string): string | null;
@@ -16,9 +16,14 @@ export function clip(value, maxLength) {
16
16
  return `${value.slice(0, maxLength - ellipsis.length)}${ellipsis}`;
17
17
  }
18
18
  export function createProviderOutputState(provider) {
19
- return provider.trim().toLowerCase() === "cursor"
20
- ? new CursorStreamJsonState()
21
- : new CodexJsonState();
19
+ const normalized = provider.trim().toLowerCase();
20
+ if (normalized === "cursor") {
21
+ return new CursorStreamJsonState();
22
+ }
23
+ if (normalized === "claude") {
24
+ return new ClaudeStreamJsonState();
25
+ }
26
+ return new CodexJsonState();
22
27
  }
23
28
  class CursorStreamJsonState {
24
29
  sessionId = null;
@@ -29,6 +34,9 @@ class CursorStreamJsonState {
29
34
  get sessionLabel() {
30
35
  return this.sessionId ? `session ${this.sessionId}` : null;
31
36
  }
37
+ get providerThreadId() {
38
+ return this.sessionId;
39
+ }
32
40
  get usage() {
33
41
  return this.accumulatedUsage;
34
42
  }
@@ -49,7 +57,8 @@ class CursorStreamJsonState {
49
57
  role: "runner",
50
58
  kind: "checkpoint",
51
59
  content: `Cursor session started: ${this.sessionId}.`,
52
- eventType: "provider_thread_started"
60
+ eventType: "provider_thread_started",
61
+ data: JSON.stringify({ threadId: this.sessionId })
53
62
  }];
54
63
  }
55
64
  }
@@ -116,13 +125,19 @@ class CursorStreamJsonState {
116
125
  if (Object.keys(usage).length === 0 && durationMs === 0) {
117
126
  return;
118
127
  }
128
+ const uncachedInputTokens = readUsageNumber(usage, "inputTokens");
129
+ const cacheReadTokens = readUsageNumber(usage, "cacheReadTokens");
130
+ const cacheCreationTokens = readUsageNumber(usage, "cacheWriteTokens");
119
131
  this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
120
- inputTokens: readUsageNumber(usage, "inputTokens"),
132
+ inputTokens: uncachedInputTokens + cacheReadTokens + cacheCreationTokens,
133
+ uncachedInputTokens,
121
134
  outputTokens: readUsageNumber(usage, "outputTokens"),
122
- cacheReadTokens: readUsageNumber(usage, "cacheReadTokens"),
123
- cacheCreationTokens: readUsageNumber(usage, "cacheWriteTokens"),
135
+ cacheReadTokens,
136
+ cacheCreationTokens,
124
137
  providerDurationMs: durationMs > 0 ? durationMs : null,
125
- source: "provider_reported"
138
+ source: "provider_reported",
139
+ inputTokenSemantics: "excludes_cache_tokens",
140
+ rawUsage: [{ provider: "cursor", usage }]
126
141
  });
127
142
  }
128
143
  }
@@ -225,6 +240,292 @@ export function parseJsonLine(line) {
225
240
  ? parsed
226
241
  : null;
227
242
  }
243
+ const claudeFileEditTools = new Set(["edit", "multiedit", "write", "notebookedit"]);
244
+ /**
245
+ * Parses `claude -p --output-format stream-json --verbose` output. Each line is one of:
246
+ * `system/init`, `assistant` (text + tool_use blocks), `user` (tool_result blocks), or the
247
+ * terminal `result` event that carries the aggregated usage for the whole run.
248
+ */
249
+ class ClaudeStreamJsonState {
250
+ sessionId = null;
251
+ finalMessage = null;
252
+ emittedSession = false;
253
+ accumulatedUsage = null;
254
+ activeToolUses = new Map();
255
+ get sessionLabel() {
256
+ return this.sessionId ? `session ${this.sessionId}` : null;
257
+ }
258
+ get providerThreadId() {
259
+ return this.sessionId;
260
+ }
261
+ get usage() {
262
+ return this.accumulatedUsage;
263
+ }
264
+ get hasActiveToolCall() {
265
+ return this.activeToolUses.size > 0;
266
+ }
267
+ consume(line) {
268
+ const event = parseJsonLine(line);
269
+ if (!event) {
270
+ return [];
271
+ }
272
+ const type = stringField(event, "type");
273
+ if (type === "system" && stringField(event, "subtype") === "init") {
274
+ return this.consumeInit(event);
275
+ }
276
+ if (type === "assistant") {
277
+ return this.consumeAssistant(event);
278
+ }
279
+ if (type === "user") {
280
+ return this.consumeToolResults(event);
281
+ }
282
+ if (type === "result") {
283
+ return this.consumeResult(event, line);
284
+ }
285
+ if (type === "error") {
286
+ return [providerCliError(line)];
287
+ }
288
+ return [];
289
+ }
290
+ consumeInit(event) {
291
+ const events = [];
292
+ this.sessionId = stringField(event, "session_id") ?? this.sessionId;
293
+ if (this.sessionId && !this.emittedSession) {
294
+ this.emittedSession = true;
295
+ events.push({
296
+ role: "runner",
297
+ kind: "checkpoint",
298
+ content: `Claude session started: ${this.sessionId}.`,
299
+ eventType: "provider_thread_started"
300
+ });
301
+ }
302
+ const servers = Array.isArray(event.mcp_servers) ? event.mcp_servers : [];
303
+ for (const entry of servers) {
304
+ if (!entry || typeof entry !== "object") {
305
+ continue;
306
+ }
307
+ const server = entry;
308
+ const name = stringField(server, "name");
309
+ const status = stringField(server, "status");
310
+ if (name && status && status !== "connected") {
311
+ events.push({
312
+ role: "runner",
313
+ kind: "checkpoint",
314
+ content: `Claude MCP server ${name} is ${status}.`,
315
+ eventType: "provider_mcp_server_status",
316
+ data: JSON.stringify({ server_name: name, status })
317
+ });
318
+ }
319
+ }
320
+ return events;
321
+ }
322
+ consumeAssistant(event) {
323
+ const events = [];
324
+ const text = assistantMessageText(event);
325
+ if (text) {
326
+ this.finalMessage = text;
327
+ events.push({
328
+ role: "assistant",
329
+ kind: "message",
330
+ content: clip(text, 4000),
331
+ eventType: "provider_message"
332
+ });
333
+ }
334
+ for (const block of messageContentBlocks(event)) {
335
+ if (stringField(block, "type") !== "tool_use") {
336
+ continue;
337
+ }
338
+ const id = stringField(block, "id");
339
+ const name = stringField(block, "name");
340
+ if (!id || !name) {
341
+ continue;
342
+ }
343
+ const toolUse = { name, input: objectField(block, "input") };
344
+ this.activeToolUses.set(id, toolUse);
345
+ events.push(...formatClaudeToolUseStarted(toolUse));
346
+ }
347
+ return events;
348
+ }
349
+ consumeToolResults(event) {
350
+ const events = [];
351
+ for (const block of messageContentBlocks(event)) {
352
+ if (stringField(block, "type") !== "tool_result") {
353
+ continue;
354
+ }
355
+ const id = stringField(block, "tool_use_id");
356
+ const toolUse = id ? this.activeToolUses.get(id) : undefined;
357
+ if (!id || !toolUse) {
358
+ continue;
359
+ }
360
+ this.activeToolUses.delete(id);
361
+ events.push(...formatClaudeToolUseCompleted(toolUse, block.is_error === true, toolResultText(block)));
362
+ }
363
+ return events;
364
+ }
365
+ consumeResult(event, line) {
366
+ this.activeToolUses.clear();
367
+ this.accumulateUsage(event);
368
+ const result = stringField(event, "result");
369
+ const subtype = stringField(event, "subtype") ?? "";
370
+ const failed = event.is_error === true || subtype.startsWith("error");
371
+ if (!failed) {
372
+ if (result) {
373
+ this.finalMessage = result;
374
+ }
375
+ return [{
376
+ role: "runner",
377
+ kind: "checkpoint",
378
+ content: "Claude turn completed.",
379
+ eventType: "provider_turn_completed"
380
+ }];
381
+ }
382
+ return [providerCliError(line)];
383
+ }
384
+ accumulateUsage(event) {
385
+ const usage = objectField(event, "usage");
386
+ const durationMs = readUsageNumber(event, "duration_ms") || readUsageNumber(event, "duration_api_ms");
387
+ if (Object.keys(usage).length === 0 && durationMs === 0) {
388
+ return;
389
+ }
390
+ // Anthropic's input_tokens excludes cache reads and writes; normalize to a cache-inclusive total.
391
+ const uncachedInputTokens = readUsageNumber(usage, "input_tokens");
392
+ const cacheReadTokens = readUsageNumber(usage, "cache_read_input_tokens");
393
+ const cacheCreationTokens = readUsageNumber(usage, "cache_creation_input_tokens");
394
+ this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
395
+ inputTokens: uncachedInputTokens + cacheReadTokens + cacheCreationTokens,
396
+ uncachedInputTokens,
397
+ outputTokens: readUsageNumber(usage, "output_tokens"),
398
+ cacheReadTokens,
399
+ cacheCreationTokens,
400
+ providerDurationMs: durationMs > 0 ? durationMs : null,
401
+ source: "provider_reported",
402
+ inputTokenSemantics: "excludes_cache_tokens",
403
+ rawUsage: [{ provider: "claude", usage }]
404
+ });
405
+ }
406
+ }
407
+ function providerCliError(line) {
408
+ return {
409
+ role: "runner",
410
+ kind: "error",
411
+ content: clip(line.trim(), 1000),
412
+ eventType: "provider_cli_error"
413
+ };
414
+ }
415
+ function messageContentBlocks(event) {
416
+ const content = objectField(event, "message").content;
417
+ if (!Array.isArray(content)) {
418
+ return [];
419
+ }
420
+ return content.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry));
421
+ }
422
+ function toolResultText(block) {
423
+ const content = block.content;
424
+ if (typeof content === "string") {
425
+ return content.trim() || null;
426
+ }
427
+ if (!Array.isArray(content)) {
428
+ return null;
429
+ }
430
+ const text = content
431
+ .map((entry) => entry && typeof entry === "object" ? stringField(entry, "text") : null)
432
+ .filter((part) => Boolean(part))
433
+ .join("\n")
434
+ .trim();
435
+ return text || null;
436
+ }
437
+ /**
438
+ * Claude Code exposes MCP tools as `mcp__<server>__<tool>` and replaces characters outside
439
+ * `[A-Za-z0-9_-]` with `_`, so `forge.complete_qa` arrives as `mcp__forge__forge_complete_qa`.
440
+ */
441
+ export function parseClaudeMcpToolName(name) {
442
+ const match = /^mcp__(.+?)__(.+)$/u.exec(name);
443
+ if (!match) {
444
+ return null;
445
+ }
446
+ const server = match[1] ?? "";
447
+ const rawTool = match[2] ?? "";
448
+ const tool = server === "forge" && rawTool.startsWith("forge_") ? `forge.${rawTool.slice("forge_".length)}` : rawTool;
449
+ return server && tool ? { server, tool } : null;
450
+ }
451
+ function claudeMcpEvents(toolUse, phase, errorText) {
452
+ const mcp = parseClaudeMcpToolName(toolUse.name);
453
+ if (!mcp) {
454
+ return null;
455
+ }
456
+ const item = {
457
+ type: "mcp_tool_call",
458
+ server: mcp.server,
459
+ tool: mcp.tool,
460
+ status: phase
461
+ };
462
+ if (phase === "failed") {
463
+ item.error = errorText ? clip(errorText, 600) : "MCP tool call failed.";
464
+ }
465
+ return formatCodexMcpToolCallEvents(phase === "started" ? "item.started" : "item.completed", { item });
466
+ }
467
+ function formatClaudeToolUseStarted(toolUse) {
468
+ const mcp = claudeMcpEvents(toolUse, "started", null);
469
+ if (mcp) {
470
+ return mcp;
471
+ }
472
+ const normalized = toolUse.name.toLowerCase();
473
+ const filePath = stringField(toolUse.input, "file_path") ?? stringField(toolUse.input, "notebook_path") ?? stringField(toolUse.input, "path");
474
+ let content;
475
+ if (normalized === "bash") {
476
+ const command = stringField(toolUse.input, "command");
477
+ content = command ? `Shell command started: ${clip(command, 500)}` : "Shell command started.";
478
+ }
479
+ else if (normalized === "read" && filePath) {
480
+ content = `Reading ${filePath}`;
481
+ }
482
+ else if (claudeFileEditTools.has(normalized) && filePath) {
483
+ content = `Writing ${filePath}`;
484
+ }
485
+ else {
486
+ content = `Tool call started: ${toolUse.name}`;
487
+ }
488
+ return [{
489
+ role: "worker",
490
+ kind: "checkpoint",
491
+ content,
492
+ eventType: normalized === "bash" ? "provider_command" : "provider_tool_call"
493
+ }];
494
+ }
495
+ function formatClaudeToolUseCompleted(toolUse, isError, resultText) {
496
+ const mcp = claudeMcpEvents(toolUse, isError ? "failed" : "completed", resultText);
497
+ if (mcp) {
498
+ return mcp;
499
+ }
500
+ const normalized = toolUse.name.toLowerCase();
501
+ const filePath = stringField(toolUse.input, "file_path") ?? stringField(toolUse.input, "notebook_path") ?? stringField(toolUse.input, "path");
502
+ if (claudeFileEditTools.has(normalized) && filePath && !isError) {
503
+ return [{
504
+ role: "worker",
505
+ kind: "file_edit",
506
+ content: `Wrote ${filePath}`,
507
+ eventType: "provider_file_edit"
508
+ }];
509
+ }
510
+ const status = isError ? "failed" : "completed";
511
+ let content;
512
+ if (normalized === "bash") {
513
+ const command = stringField(toolUse.input, "command");
514
+ content = command ? `Shell command ${status}: ${clip(command, 500)}` : `Shell command ${status}.`;
515
+ }
516
+ else if (normalized === "read" && filePath) {
517
+ content = isError ? `Read failed: ${filePath}` : `Read ${filePath}`;
518
+ }
519
+ else {
520
+ content = `Tool call ${status}: ${toolUse.name}`;
521
+ }
522
+ return [{
523
+ role: "worker",
524
+ kind: "checkpoint",
525
+ content,
526
+ eventType: normalized === "bash" ? "provider_command" : "provider_tool_call"
527
+ }];
528
+ }
228
529
  class CodexJsonState {
229
530
  threadId = null;
230
531
  finalMessage = null;
@@ -234,6 +535,9 @@ class CodexJsonState {
234
535
  get sessionLabel() {
235
536
  return this.threadId ? `thread ${this.threadId}` : null;
236
537
  }
538
+ get providerThreadId() {
539
+ return this.threadId;
540
+ }
237
541
  get usage() {
238
542
  return this.accumulatedUsage;
239
543
  }
@@ -255,7 +559,8 @@ class CodexJsonState {
255
559
  role: "runner",
256
560
  kind: "checkpoint",
257
561
  content: `Codex thread started: ${this.threadId}.`,
258
- eventType: "provider_thread_started"
562
+ eventType: "provider_thread_started",
563
+ data: JSON.stringify({ threadId: this.threadId })
259
564
  }];
260
565
  }
261
566
  }
@@ -332,12 +637,17 @@ class CodexJsonState {
332
637
  if (Object.keys(usage).length === 0) {
333
638
  return;
334
639
  }
640
+ const inputTokens = readUsageNumber(usage, "input_tokens");
641
+ const cacheReadTokens = readUsageNumber(usage, "cached_input_tokens");
335
642
  this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
336
- inputTokens: readUsageNumber(usage, "input_tokens"),
643
+ inputTokens,
644
+ uncachedInputTokens: Math.max(0, inputTokens - cacheReadTokens),
337
645
  outputTokens: readUsageNumber(usage, "output_tokens"),
338
- cacheReadTokens: readUsageNumber(usage, "cached_input_tokens"),
646
+ cacheReadTokens,
339
647
  reasoningOutputTokens: readUsageNumber(usage, "reasoning_output_tokens"),
340
- source: "provider_reported"
648
+ source: "provider_reported",
649
+ inputTokenSemantics: "includes_cached_input",
650
+ rawUsage: [{ provider: "codex", usage }]
341
651
  });
342
652
  }
343
653
  }
@@ -425,11 +735,45 @@ function mcpErrorSummary(item) {
425
735
  if (direct) {
426
736
  return clip(direct, 600);
427
737
  }
428
- const error = objectField(item, "error");
429
- return stringField(error, "message")
430
- ?? stringField(error, "summary")
431
- ?? stringField(objectField(item, "result"), "error")
432
- ?? null;
738
+ const messages = uniqueErrorMessages([
739
+ item.error,
740
+ item.result,
741
+ item.content,
742
+ ]);
743
+ return messages.length > 0 ? clip(messages.join(" Caused by: "), 600) : null;
744
+ }
745
+ function uniqueErrorMessages(values) {
746
+ const messages = [];
747
+ const seenMessages = new Set();
748
+ const seenObjects = new Set();
749
+ const visit = (value, depth) => {
750
+ if (depth > 8 || value === null || value === undefined)
751
+ return;
752
+ if (typeof value === "string") {
753
+ const message = value.trim();
754
+ if (message.length > 0 && !seenMessages.has(message)) {
755
+ seenMessages.add(message);
756
+ messages.push(message);
757
+ }
758
+ return;
759
+ }
760
+ if (typeof value !== "object" || seenObjects.has(value))
761
+ return;
762
+ seenObjects.add(value);
763
+ if (Array.isArray(value)) {
764
+ for (const entry of value)
765
+ visit(entry, depth + 1);
766
+ return;
767
+ }
768
+ const record = value;
769
+ for (const key of ["message", "summary", "error", "cause", "data", "details", "result", "content"]) {
770
+ if (key in record)
771
+ visit(record[key], depth + 1);
772
+ }
773
+ };
774
+ for (const value of values)
775
+ visit(value, 0);
776
+ return messages;
433
777
  }
434
778
  function safeProviderMcpData(item, serverName, toolName, status) {
435
779
  const elapsedMs = numericField(item, "elapsed_ms") ?? numericField(item, "duration_ms");
@@ -1,17 +1,24 @@
1
1
  import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
2
2
  export type UsageSource = "provider_reported" | "cli_reported" | "estimated" | "unknown";
3
+ export type InputTokenSemantics = "includes_cached_input" | "excludes_cache_tokens" | "unknown";
4
+ export declare const PROVIDER_USAGE_SCHEMA_VERSION = 1;
3
5
  /**
4
6
  * Normalized, provider-agnostic token usage accumulated for a dispatch. Cost is never computed
5
7
  * here; the API recomputes cost from these tokens x model rates (ADR-0006).
6
8
  */
7
9
  export interface ProviderUsage {
10
+ /** Normalized total input. Cached input is a subset, never an additive peer. */
8
11
  readonly inputTokens: number;
12
+ readonly uncachedInputTokens: number | null;
9
13
  readonly outputTokens: number;
10
14
  readonly cacheReadTokens: number;
11
15
  readonly cacheCreationTokens: number;
12
16
  readonly reasoningOutputTokens: number;
13
17
  readonly providerDurationMs: number | null;
14
18
  readonly source: UsageSource;
19
+ readonly inputTokenSemantics: InputTokenSemantics;
20
+ /** Exact provider usage objects retained for audit/re-normalization. */
21
+ readonly rawUsage: readonly Record<string, unknown>[];
15
22
  }
16
23
  export declare function emptyUsage(source?: UsageSource): ProviderUsage;
17
24
  export declare function addUsage(base: ProviderUsage, next: Partial<ProviderUsage>): ProviderUsage;
@@ -1,27 +1,38 @@
1
+ export const PROVIDER_USAGE_SCHEMA_VERSION = 1;
1
2
  export function emptyUsage(source = "unknown") {
2
3
  return {
3
4
  inputTokens: 0,
5
+ uncachedInputTokens: 0,
4
6
  outputTokens: 0,
5
7
  cacheReadTokens: 0,
6
8
  cacheCreationTokens: 0,
7
9
  reasoningOutputTokens: 0,
8
10
  providerDurationMs: null,
9
- source
11
+ source,
12
+ inputTokenSemantics: "unknown",
13
+ rawUsage: []
10
14
  };
11
15
  }
12
16
  export function addUsage(base, next) {
17
+ const nextUncached = next.uncachedInputTokens;
18
+ const uncachedInputTokens = base.uncachedInputTokens === null || nextUncached === null
19
+ ? null
20
+ : base.uncachedInputTokens + nonNegative(nextUncached);
13
21
  return {
14
22
  inputTokens: base.inputTokens + nonNegative(next.inputTokens),
23
+ uncachedInputTokens,
15
24
  outputTokens: base.outputTokens + nonNegative(next.outputTokens),
16
25
  cacheReadTokens: base.cacheReadTokens + nonNegative(next.cacheReadTokens),
17
26
  cacheCreationTokens: base.cacheCreationTokens + nonNegative(next.cacheCreationTokens),
18
27
  reasoningOutputTokens: base.reasoningOutputTokens + nonNegative(next.reasoningOutputTokens),
19
28
  providerDurationMs: next.providerDurationMs ?? base.providerDurationMs,
20
- source: next.source ?? base.source
29
+ source: next.source ?? base.source,
30
+ inputTokenSemantics: mergeSemantics(base.inputTokenSemantics, next.inputTokenSemantics),
31
+ rawUsage: [...base.rawUsage, ...(next.rawUsage ?? [])]
21
32
  };
22
33
  }
23
34
  export function totalTokens(usage) {
24
- return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;
35
+ return usage.inputTokens + usage.outputTokens;
25
36
  }
26
37
  export function hasUsage(usage) {
27
38
  return usage !== null && totalTokens(usage) > 0;
@@ -37,20 +48,26 @@ export function buildProviderUsageEvent(dispatch, usage) {
37
48
  lane: dispatch.execution.lane,
38
49
  phase: dispatch.cycleRun.phase,
39
50
  attempt: dispatch.cycleRun.attempt,
51
+ usage_schema_version: PROVIDER_USAGE_SCHEMA_VERSION,
52
+ input_token_semantics: usage.inputTokenSemantics,
40
53
  input_tokens: usage.inputTokens,
54
+ uncached_input_tokens: usage.uncachedInputTokens,
41
55
  output_tokens: usage.outputTokens,
42
56
  cache_read_tokens: usage.cacheReadTokens,
43
57
  cache_creation_tokens: usage.cacheCreationTokens,
44
58
  reasoning_output_tokens: usage.reasoningOutputTokens,
45
59
  metered: dispatch.execution.authKind === "api_key",
46
60
  usage_source: usage.source,
47
- provider_duration_ms: usage.providerDurationMs
61
+ provider_duration_ms: usage.providerDurationMs,
62
+ normalized_total_tokens: totalTokens(usage),
63
+ normalized_total_formula: "total_input + output; cached_input is a subset of total_input",
64
+ raw_usage: usage.rawUsage
48
65
  };
49
66
  const total = totalTokens(usage);
50
67
  return {
51
68
  role: "runner",
52
69
  kind: "checkpoint",
53
- content: `Provider usage: ${total} tokens (${usage.inputTokens} in / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
70
+ content: `Provider usage: ${total} tokens (${usage.inputTokens} total in, including ${usage.cacheReadTokens} cached / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
54
71
  eventType: "provider_usage",
55
72
  data: JSON.stringify(data)
56
73
  };
@@ -62,3 +79,9 @@ export function readUsageNumber(source, key) {
62
79
  function nonNegative(value) {
63
80
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
64
81
  }
82
+ function mergeSemantics(base, next) {
83
+ if (!next || next === "unknown") {
84
+ return base;
85
+ }
86
+ return base === "unknown" || base === next ? next : "unknown";
87
+ }