@koda-sl/baker-bridge 0.109.0-dev.fa751dbf1 → 0.109.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 (52) hide show
  1. package/README.md +19 -19
  2. package/dist/hono/agent-session.d.ts +1 -0
  3. package/dist/hono/agent-session.d.ts.map +1 -1
  4. package/dist/hono/agent-session.js +13 -2
  5. package/dist/hono/agent-session.js.map +1 -1
  6. package/dist/hono/agent-session.test.js +37 -0
  7. package/dist/hono/agent-session.test.js.map +1 -1
  8. package/dist/hono/agent-tasks.d.ts.map +1 -1
  9. package/dist/hono/agent-tasks.js +7 -3
  10. package/dist/hono/agent-tasks.js.map +1 -1
  11. package/dist/hono/agent-tasks.test.js +15 -0
  12. package/dist/hono/agent-tasks.test.js.map +1 -1
  13. package/dist/hono/agent.d.ts.map +1 -1
  14. package/dist/hono/agent.js +12 -9
  15. package/dist/hono/agent.js.map +1 -1
  16. package/dist/hono/attachments.d.ts +6 -1
  17. package/dist/hono/attachments.d.ts.map +1 -1
  18. package/dist/hono/attachments.js +9 -4
  19. package/dist/hono/attachments.js.map +1 -1
  20. package/dist/hono/attachments.test.js +5 -0
  21. package/dist/hono/attachments.test.js.map +1 -1
  22. package/dist/hono/bridge-job.d.ts +31 -12
  23. package/dist/hono/bridge-job.d.ts.map +1 -1
  24. package/dist/hono/bridge-job.js +130 -68
  25. package/dist/hono/bridge-job.js.map +1 -1
  26. package/dist/hono/bridge-job.test.js +227 -170
  27. package/dist/hono/bridge-job.test.js.map +1 -1
  28. package/dist/hono/session-continuity.d.ts +0 -1
  29. package/dist/hono/session-continuity.d.ts.map +1 -1
  30. package/dist/hono/session-continuity.js +2 -5
  31. package/dist/hono/session-continuity.js.map +1 -1
  32. package/dist/hono/types.d.ts +7 -6
  33. package/dist/hono/types.d.ts.map +1 -1
  34. package/dist/hono/workspace-settle.d.ts +20 -0
  35. package/dist/hono/workspace-settle.d.ts.map +1 -0
  36. package/dist/hono/workspace-settle.js +46 -0
  37. package/dist/hono/workspace-settle.js.map +1 -0
  38. package/dist/review/digest.d.ts +7 -1
  39. package/dist/review/digest.d.ts.map +1 -1
  40. package/dist/review/digest.js +80 -16
  41. package/dist/review/digest.js.map +1 -1
  42. package/dist/review/digest.test.js +83 -0
  43. package/dist/review/digest.test.js.map +1 -1
  44. package/package.json +2 -1
  45. package/dist/hono/main-sync.d.ts +0 -46
  46. package/dist/hono/main-sync.d.ts.map +0 -1
  47. package/dist/hono/main-sync.js +0 -409
  48. package/dist/hono/main-sync.js.map +0 -1
  49. package/dist/hono/main-sync.test.d.ts +0 -2
  50. package/dist/hono/main-sync.test.d.ts.map +0 -1
  51. package/dist/hono/main-sync.test.js +0 -276
  52. package/dist/hono/main-sync.test.js.map +0 -1
@@ -29,6 +29,7 @@ import { createBakerUiServer, normalizeTagInputToolInput, sanitizeTagChanges, TA
29
29
  import { blockingToolDelegated, toolRunAsCommand } from "./tool-in-shell.js";
30
30
  import { recordTurnEvent } from "./turn-events.js";
31
31
  import { addQueryModelUsage, queryModelUsage, turnModelUsageList } from "./turn-model-usage.js";
32
+ import { settleWorkspaceAfterTasks } from "./workspace-settle.js";
32
33
  export function parsePlanDecision(value) {
33
34
  if (!value || typeof value !== "object") {
34
35
  return null;
@@ -151,10 +152,11 @@ const BASE_DISALLOWED_TOOLS = [
151
152
  // not asked to fund a research fleet. Ordinary fan-out is unaffected:
152
153
  // `Task` stays, so the scaffold's subagents still run.
153
154
  "Workflow",
154
- // Cross-session messaging. A Runtime holds exactly one session, so
155
- // there is never a peer to reach; every call is a dead end that costs a
156
- // round trip to discover.
157
- "SendMessage",
155
+ // NOT listed: `SendMessage`. It is how the model follows up with a
156
+ // subagent it already ran, and the CLI tells it to on every Agent result.
157
+ // The continued subagent runs in the background, but it announces itself
158
+ // with `task_started` / `background_tasks_changed`, so the turn stays open
159
+ // until it finishes (`createTurnEndController`).
158
160
  // A Session review may not change anything. Removing the definitions is
159
161
  // the second of three layers — the key it carries is read-scoped and its
160
162
  // Bash calls go through `reviewBashPolicy` — because each fails
@@ -498,9 +500,9 @@ function sdkResultError(msg) {
498
500
  // the whole turn; the deny THRESHOLD below still guards a one-off end-of-turn race.
499
501
  // The trailing colon matters: the CLI always emits the envelope followed by the
500
502
  // error that caused it ("…failed: Error: Stream closed", "…failed: AbortError: …").
501
- // Prose that merely NAMES the failure quotes it without one, which is how the
502
- // scaffold's guard-background-commands hook describes what it prevents — and its
503
- // block is an is_error tool_result, so a colon-less match counted a healthy
503
+ // Prose that merely NAMES the failure quotes it without one, which is how a
504
+ // hook explaining a block quotes it (the scaffold's former background-subagent
505
+ // block did) — and a hook block is an is_error tool_result, so a colon-less match counted a healthy
504
506
  // blocked spawn as a deny and could trip the breaker on a healthy turn.
505
507
  const CONTROL_CHANNEL_DENY_MARKER = "Tool permission request failed:";
506
508
  // Hook failures are reported through the same is_error tool_result channel but
@@ -663,9 +665,27 @@ export function createStallDetector(input) {
663
665
  awaitingModel = (msg.type === "user" || msg.type === "stream_event") && pendingToolUses.size === 0;
664
666
  },
665
667
  stalled: () => stalled,
668
+ hasPendingToolUses: () => pendingToolUses.size > 0,
666
669
  dispose: () => clearInterval(timer),
667
670
  };
668
671
  }
672
+ // A query's cost and `modelUsage` are cumulative — on every result message and
673
+ // on the CLI's usage reply alike — so the latest reading replaces the last one
674
+ // rather than adding to it. It is folded into the turn totals when the query
675
+ // settles (see `runQueryOnce`). A spuriously empty later reading never wipes a
676
+ // real earlier one.
677
+ function recordCumulativeUsage(state, reading) {
678
+ const own = ownQueryUsage(reading, state.queryUsageBaseline);
679
+ state.observedCostUsd = own.costUsd;
680
+ const snapshot = queryModelUsage(own.modelUsage);
681
+ if (snapshot.size > 0) {
682
+ state.queryModelUsage = snapshot;
683
+ }
684
+ return own.costUsd;
685
+ }
686
+ // How long ending a turn waits on the CLI's usage reply before going ahead
687
+ // without it. Billing a turn late is recoverable; hanging one is not.
688
+ const USAGE_READING_TIMEOUT_MS = 5_000;
669
689
  // Decides when the turn is really over and the CLI may be told to exit.
670
690
  //
671
691
  // The prompt rides a held-open input stream (see `createHeldOpenQuery` in
@@ -687,7 +707,10 @@ export function createStallDetector(input) {
687
707
  // events must not, or the end would never re-arm. After input ends, an exit
688
708
  // guard closes the query if the CLI neither speaks nor exits, so a wedged
689
709
  // process cannot hang the turn until the session watchdog.
690
- function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
710
+ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent,
711
+ // Runs once, before input ends on a turn that ran tasks: a task can settle
712
+ // without waking the model, so the last result may not include what it spent.
713
+ beforeEndInput) {
691
714
  const inFlightTasks = new Set();
692
715
  // The background_tasks_changed LEVEL signal, tracked separately from the
693
716
  // edge-paired set above: a foreground command the CLI auto-backgrounds emits
@@ -700,15 +723,52 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
700
723
  let cleanResultSeen = false;
701
724
  let lastWasTaskLifecycle = false;
702
725
  let inputEnded = false;
726
+ // Between deciding to end and ending, while the usage reading is in flight.
727
+ // Anything that would have cancelled a pending end — model activity, a task
728
+ // starting — is recorded here and abandons the end instead.
729
+ let ending;
730
+ // The query stopped being drained (a Stop, a crash): a reading answering
731
+ // after that has no turn left to end.
732
+ let disposed = false;
703
733
  let settleTimer;
704
734
  let exitGuard;
705
735
  const endInput = () => {
706
736
  settleTimer = undefined;
707
737
  // Fire-time guard: a task that started during the settle window keeps the
708
738
  // channel open; a later settle re-arms the end.
709
- if (inputEnded || hasInFlightWork()) {
739
+ if (inputEnded || ending || hasInFlightWork()) {
710
740
  return;
711
741
  }
742
+ if (sawAnyTask && beforeEndInput) {
743
+ const attempt = { interrupted: false };
744
+ ending = attempt;
745
+ let timeout;
746
+ const timedOut = new Promise((resolveTimeout) => {
747
+ timeout = setTimeout(resolveTimeout, USAGE_READING_TIMEOUT_MS);
748
+ timeout.unref?.();
749
+ });
750
+ void Promise.race([beforeEndInput().catch(() => undefined), timedOut]).finally(() => {
751
+ clearTimeout(timeout);
752
+ ending = undefined;
753
+ if (disposed) {
754
+ return;
755
+ }
756
+ // Re-check at the moment input would end, as the fire-time guard above
757
+ // does: a wake or a task that landed during the reading keeps the
758
+ // channel open, and the woken turn's own result or settle ends it.
759
+ if (attempt.interrupted || hasInFlightWork()) {
760
+ if (cleanResultSeen && !hasInFlightWork()) {
761
+ scheduleEnd();
762
+ }
763
+ return;
764
+ }
765
+ finishEndInput();
766
+ });
767
+ return;
768
+ }
769
+ finishEndInput();
770
+ };
771
+ const finishEndInput = () => {
712
772
  inputEnded = true;
713
773
  query.endInput?.();
714
774
  onTurnEvent({
@@ -778,15 +838,21 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
778
838
  clearTimeout(settleTimer);
779
839
  settleTimer = undefined;
780
840
  }
841
+ if (ending && (isModelActivity || hasInFlightWork())) {
842
+ ending.interrupted = true;
843
+ }
781
844
  if (exitGuard) {
782
845
  exitGuard.refresh();
783
846
  }
784
847
  },
785
- maybeEndAfter: (msg) => {
848
+ maybeEndAfter: (msg, refused) => {
786
849
  if (inputEnded) {
787
850
  return;
788
851
  }
789
- if (msg.type === "result" && !msg.is_error) {
852
+ // A refusal's error result is not thrown (the refusal already decided how
853
+ // the turn ends), so it has to end input like a clean one or the CLI idles
854
+ // on stdin until the session watchdog.
855
+ if (msg.type === "result" && (!msg.is_error || refused)) {
790
856
  cleanResultSeen = true;
791
857
  if (hasInFlightWork()) {
792
858
  console.error(`[BridgeJob] result received with ${inFlightTasks.size} task(s) and ${backgroundTaskCount} background command(s) in flight — holding the turn open (thread ${job.threadId})`);
@@ -803,7 +869,10 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
803
869
  scheduleEnd();
804
870
  }
805
871
  },
872
+ hasInFlightWork,
873
+ ranTasks: () => sawAnyTask,
806
874
  dispose: () => {
875
+ disposed = true;
807
876
  if (settleTimer) {
808
877
  clearTimeout(settleTimer);
809
878
  settleTimer = undefined;
@@ -895,7 +964,7 @@ export class BridgeJobExecutor {
895
964
  readUsageBaseline;
896
965
  stalledCliExitMs;
897
966
  readFlowTree;
898
- mainSync;
967
+ settleWorkspace;
899
968
  constructor(dependencies) {
900
969
  this.sdk = dependencies.sdk;
901
970
  this.relay = dependencies.relay;
@@ -914,9 +983,9 @@ export class BridgeJobExecutor {
914
983
  dependencies.readUsageBaseline ??
915
984
  ((sessionId) => readUsageBaseline(sessionId, process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude")));
916
985
  this.stalledCliExitMs = dependencies.stalledCliExitMs ?? STALLED_CLI_EXIT_MS;
986
+ this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
917
987
  this.readFlowTree =
918
988
  dependencies.readFlowTree ?? ((flowSlug) => readRawFlowTree(bridgeRuntimeFiles.flowsDir(), flowSlug));
919
- this.mainSync = dependencies.mainSync;
920
989
  }
921
990
  async execute(job, controls) {
922
991
  const state = createBridgeJobState();
@@ -929,10 +998,12 @@ export class BridgeJobExecutor {
929
998
  return { kind: "cancelled", costUsd: 0, model: state.model };
930
999
  }
931
1000
  const loaded = await this.loadTrustedBoundary(job, controls);
1001
+ if (loaded?.kind !== "clean") {
1002
+ request = await this.withContinuedAttachments(job, request);
1003
+ }
932
1004
  attempt = prepareSessionAttempt({
933
1005
  requestPrompt: request.prompt,
934
1006
  reminder: request.reminder,
935
- recoveryReminder: request.recoveryReminder,
936
1007
  history: job.history,
937
1008
  loaded,
938
1009
  leadingCommand: request.leadingCommand,
@@ -963,12 +1034,6 @@ export class BridgeJobExecutor {
963
1034
  await this.clearIfDirty(job, controls, state, classifyDirtyReason(error, controls));
964
1035
  return this.errorOutcome(job, controls, state, this.resultWithObservedCost(result, state), error);
965
1036
  }
966
- finally {
967
- // A review only reads the finished Session's workspace.
968
- if (!job.review) {
969
- await this.mainSync?.afterTurn();
970
- }
971
- }
972
1037
  }
973
1038
  async loadTrustedBoundary(job, controls) {
974
1039
  const loaded = await this.sessionStore.load(job.threadId);
@@ -990,34 +1055,17 @@ export class BridgeJobExecutor {
990
1055
  // skill) must stay the leading token, so they skip speaker attribution and
991
1056
  // reference context — otherwise the prepended lines push the command off the
992
1057
  // front and it lands as ordinary message text the model just echoes.
993
- const bridgeCommand = verbatimSlashCommand(job.prompt);
994
- const verbatim = bridgeCommand ??
1058
+ const verbatim = verbatimSlashCommand(job.prompt) ??
995
1059
  leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
996
1060
  let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
997
- // Publish and compact keep the workspace as it is (Publish rebases onto main
998
- // itself). A skill reads and edits the workspace, so it is brought level
999
- // first — but a note beside the command would push it off the front, so
1000
- // the thread hears about it on its next turn.
1001
- const notes = bridgeCommand === null
1002
- ? ((await this.mainSync?.beforeTurn(job.threadId, job.publishedElsewhere ?? [], {
1003
- withNote: verbatim === null,
1004
- })) ?? {})
1005
- : {};
1006
- const viewing = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
1007
- const reminder = joinReminders(notes.resumed, viewing);
1008
- const recoveryReminder = joinReminders(notes.recovered, viewing);
1061
+ const reminder = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
1009
1062
  if (job.attachments && job.attachments.length > 0) {
1010
- requestPrompt += await this.prepareAttachments(job.attachments);
1063
+ requestPrompt += await this.prepareAttachments(job.attachments, "message");
1011
1064
  }
1012
1065
  if (!controls.isActive()) {
1013
1066
  return null;
1014
1067
  }
1015
- return {
1016
- prompt: requestPrompt,
1017
- ...(reminder ? { reminder } : {}),
1018
- ...(recoveryReminder ? { recoveryReminder } : {}),
1019
- leadingCommand: verbatim !== null,
1020
- };
1068
+ return { prompt: requestPrompt, ...(reminder ? { reminder } : {}), leadingCommand: verbatim !== null };
1021
1069
  }
1022
1070
  /**
1023
1071
  * The reviewer's prompt: the brief Convex wrote, with the evidence block this
@@ -1216,6 +1264,18 @@ export class BridgeJobExecutor {
1216
1264
  });
1217
1265
  }
1218
1266
  }
1267
+ /**
1268
+ * The files of the request a late reply continues, for a turn that does not
1269
+ * resume the Session that received them with the request. Its history names
1270
+ * them but cannot open them.
1271
+ */
1272
+ async withContinuedAttachments(job, request) {
1273
+ if (!job.continuedAttachments?.length || request.leadingCommand) {
1274
+ return request;
1275
+ }
1276
+ const suffix = await this.prepareAttachments(job.continuedAttachments, "continued_request");
1277
+ return { ...request, prompt: request.prompt + suffix };
1278
+ }
1219
1279
  async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
1220
1280
  if (!attempt.usedBoundary ||
1221
1281
  state.sawUserVisibleSdkMessage ||
@@ -1224,6 +1284,7 @@ export class BridgeJobExecutor {
1224
1284
  return null;
1225
1285
  }
1226
1286
  await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason: "sdk_error" });
1287
+ const rebuilt = await this.withContinuedAttachments(job, request);
1227
1288
  void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydrated_invalid_resume", {
1228
1289
  historyEntryCount: job.history?.entries.length ?? 0,
1229
1290
  omittedEntryCount: job.history?.omittedEntryCount ?? 0,
@@ -1232,12 +1293,12 @@ export class BridgeJobExecutor {
1232
1293
  });
1233
1294
  // A leading command stays bare here too: a history preamble in front of it
1234
1295
  // would turn it into plain text (see `prepareSessionAttempt`).
1235
- if (request.leadingCommand) {
1236
- return { prompt: request.prompt, attemptedRehydration: false, resumeOptions: {} };
1296
+ if (rebuilt.leadingCommand) {
1297
+ return { prompt: rebuilt.prompt, attemptedRehydration: false, resumeOptions: {} };
1237
1298
  }
1238
1299
  return {
1239
- prompt: buildRehydratedPrompt(request.prompt, job.history),
1240
- ...(request.recoveryReminder ? { reminder: request.recoveryReminder } : {}),
1300
+ prompt: buildRehydratedPrompt(rebuilt.prompt, job.history),
1301
+ ...(rebuilt.reminder ? { reminder: rebuilt.reminder } : {}),
1241
1302
  attemptedRehydration: true,
1242
1303
  resumeOptions: {},
1243
1304
  };
@@ -1292,11 +1353,6 @@ export class BridgeJobExecutor {
1292
1353
  const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
1293
1354
  if (malformed)
1294
1355
  return Promise.resolve({ behavior: "deny", message: malformed });
1295
- const clash = toolName === "Write" && typeof input?.file_path === "string"
1296
- ? (this.mainSync?.refuseWrite(job.threadId, input.file_path) ?? null)
1297
- : null;
1298
- if (clash)
1299
- return Promise.resolve({ behavior: "deny", message: clash });
1300
1356
  // Cast what is owed FIRST, whatever comes next — including another create. Checked
1301
1357
  // the other way round, a second create (the agent retrying after a rejected flag,
1302
1358
  // which is what happens in practice) would overwrite the handle and the first
@@ -1522,7 +1578,19 @@ export class BridgeJobExecutor {
1522
1578
  // in-progress session.
1523
1579
  async drainQuery(job, controls, state, query, closeQuery) {
1524
1580
  const stallDetector = this.createStallDetector(job, controls, closeQuery);
1525
- const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent);
1581
+ // What the query had spent when its turn ended, read from the CLI — set
1582
+ // only when a task ran, and higher than the last result's figure when a
1583
+ // task kept working after it.
1584
+ let settledCostUsd = 0;
1585
+ // A reading that answers after the query drained (past its timeout) belongs
1586
+ // to a query already folded into the turn — and a retry reuses `state`.
1587
+ let drained = false;
1588
+ const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent, async () => {
1589
+ const reading = await query.usage?.();
1590
+ if (reading && !drained) {
1591
+ settledCostUsd = recordCumulativeUsage(state, reading);
1592
+ }
1593
+ });
1526
1594
  let costUsd = 0;
1527
1595
  // Errors thrown while HANDLING a message (control-channel breaker, error
1528
1596
  // results) are real verdicts and must surface as-is; only errors from the
@@ -1532,6 +1600,7 @@ export class BridgeJobExecutor {
1532
1600
  for await (const msg of query) {
1533
1601
  stallDetector.observeMessage(msg);
1534
1602
  turnEnd.observeMessage(msg);
1603
+ controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
1535
1604
  try {
1536
1605
  costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
1537
1606
  }
@@ -1539,7 +1608,7 @@ export class BridgeJobExecutor {
1539
1608
  consumeError = true;
1540
1609
  throw error;
1541
1610
  }
1542
- turnEnd.maybeEndAfter(msg);
1611
+ turnEnd.maybeEndAfter(msg, state.refusedWithoutFallback === true);
1543
1612
  if (!controls.isActive()) {
1544
1613
  closeQuery();
1545
1614
  break;
@@ -1554,6 +1623,8 @@ export class BridgeJobExecutor {
1554
1623
  throw error;
1555
1624
  }
1556
1625
  finally {
1626
+ drained = true;
1627
+ controls.setWorkInFlight?.(false);
1557
1628
  stallDetector.dispose();
1558
1629
  turnEnd.dispose();
1559
1630
  await this.openPendingCastingAtTurnEnd(job, controls);
@@ -1561,7 +1632,11 @@ export class BridgeJobExecutor {
1561
1632
  if (stallDetector.stalled() && controls.isActive()) {
1562
1633
  throw new StreamStallError();
1563
1634
  }
1564
- return costUsd;
1635
+ // Not for a Session review: it runs with every hook off and may not change anything.
1636
+ if (turnEnd.ranTasks() && controls.isActive() && !job.review) {
1637
+ await this.settleWorkspace();
1638
+ }
1639
+ return Math.max(costUsd, settledCostUsd);
1565
1640
  }
1566
1641
  /**
1567
1642
  * Open a casting the turn recorded but never reached.
@@ -1654,17 +1729,7 @@ export class BridgeJobExecutor {
1654
1729
  }
1655
1730
  trackMainRequestUsage(state, msg);
1656
1731
  if (msg.type === "result") {
1657
- const own = ownQueryUsage({ totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage }, state.queryUsageBaseline);
1658
- state.observedCostUsd = own.costUsd;
1659
- // A query's result `modelUsage` is cumulative (like `total_cost_usd`, which
1660
- // we overwrite above), so keep the latest snapshot rather than summing —
1661
- // it is folded into the turn totals when the query settles (see
1662
- // `runQueryOnce`). Guard against a spuriously empty later result wiping a
1663
- // real earlier one.
1664
- const snapshot = queryModelUsage(own.modelUsage);
1665
- if (snapshot.size > 0) {
1666
- state.queryModelUsage = snapshot;
1667
- }
1732
+ const ownCostUsd = recordCumulativeUsage(state, { totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage });
1668
1733
  await this.publishContextUsage(job, state, msg.modelUsage);
1669
1734
  // What a Session review returns. The SDK enforces the JSON schema, so this
1670
1735
  // either parses against the findings contract or the review failed to
@@ -1681,7 +1746,7 @@ export class BridgeJobExecutor {
1681
1746
  await this.clearIfDirty(job, controls, state, "result_error");
1682
1747
  throw resultError;
1683
1748
  }
1684
- return own.costUsd;
1749
+ return ownCostUsd;
1685
1750
  }
1686
1751
  if (isUserVisibleSdkMessage(msg)) {
1687
1752
  state.sawUserVisibleSdkMessage = true;
@@ -2294,7 +2359,4 @@ function userTurnsBefore(job) {
2294
2359
  .trim();
2295
2360
  return said || undefined;
2296
2361
  }
2297
- function joinReminders(...blocks) {
2298
- return blocks.filter((block) => block !== undefined).join("\n\n") || undefined;
2299
- }
2300
2362
  //# sourceMappingURL=bridge-job.js.map