@koda-sl/baker-bridge 0.109.0 → 0.110.0-dev.6a86a7961

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 +0 -1
  3. package/dist/hono/agent-session.d.ts.map +1 -1
  4. package/dist/hono/agent-session.js +2 -13
  5. package/dist/hono/agent-session.js.map +1 -1
  6. package/dist/hono/agent-session.test.js +0 -37
  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 +3 -7
  10. package/dist/hono/agent-tasks.js.map +1 -1
  11. package/dist/hono/agent-tasks.test.js +0 -15
  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 +9 -12
  15. package/dist/hono/agent.js.map +1 -1
  16. package/dist/hono/attachments.d.ts +1 -6
  17. package/dist/hono/attachments.d.ts.map +1 -1
  18. package/dist/hono/attachments.js +4 -9
  19. package/dist/hono/attachments.js.map +1 -1
  20. package/dist/hono/attachments.test.js +0 -5
  21. package/dist/hono/attachments.test.js.map +1 -1
  22. package/dist/hono/bridge-job.d.ts +12 -31
  23. package/dist/hono/bridge-job.d.ts.map +1 -1
  24. package/dist/hono/bridge-job.js +68 -130
  25. package/dist/hono/bridge-job.js.map +1 -1
  26. package/dist/hono/bridge-job.test.js +170 -227
  27. package/dist/hono/bridge-job.test.js.map +1 -1
  28. package/dist/hono/main-sync.d.ts +46 -0
  29. package/dist/hono/main-sync.d.ts.map +1 -0
  30. package/dist/hono/main-sync.js +422 -0
  31. package/dist/hono/main-sync.js.map +1 -0
  32. package/dist/hono/main-sync.test.d.ts +2 -0
  33. package/dist/hono/main-sync.test.d.ts.map +1 -0
  34. package/dist/hono/main-sync.test.js +288 -0
  35. package/dist/hono/main-sync.test.js.map +1 -0
  36. package/dist/hono/session-continuity.d.ts +1 -0
  37. package/dist/hono/session-continuity.d.ts.map +1 -1
  38. package/dist/hono/session-continuity.js +5 -2
  39. package/dist/hono/session-continuity.js.map +1 -1
  40. package/dist/hono/types.d.ts +6 -7
  41. package/dist/hono/types.d.ts.map +1 -1
  42. package/dist/review/digest.d.ts +1 -7
  43. package/dist/review/digest.d.ts.map +1 -1
  44. package/dist/review/digest.js +16 -80
  45. package/dist/review/digest.js.map +1 -1
  46. package/dist/review/digest.test.js +0 -83
  47. package/dist/review/digest.test.js.map +1 -1
  48. package/package.json +1 -2
  49. package/dist/hono/workspace-settle.d.ts +0 -20
  50. package/dist/hono/workspace-settle.d.ts.map +0 -1
  51. package/dist/hono/workspace-settle.js +0 -46
  52. package/dist/hono/workspace-settle.js.map +0 -1
@@ -29,7 +29,6 @@ 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";
33
32
  export function parsePlanDecision(value) {
34
33
  if (!value || typeof value !== "object") {
35
34
  return null;
@@ -152,11 +151,10 @@ const BASE_DISALLOWED_TOOLS = [
152
151
  // not asked to fund a research fleet. Ordinary fan-out is unaffected:
153
152
  // `Task` stays, so the scaffold's subagents still run.
154
153
  "Workflow",
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`).
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",
160
158
  // A Session review may not change anything. Removing the definitions is
161
159
  // the second of three layers — the key it carries is read-scoped and its
162
160
  // Bash calls go through `reviewBashPolicy` — because each fails
@@ -500,9 +498,9 @@ function sdkResultError(msg) {
500
498
  // the whole turn; the deny THRESHOLD below still guards a one-off end-of-turn race.
501
499
  // The trailing colon matters: the CLI always emits the envelope followed by the
502
500
  // error that caused it ("…failed: Error: Stream closed", "…failed: AbortError: …").
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
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
506
504
  // blocked spawn as a deny and could trip the breaker on a healthy turn.
507
505
  const CONTROL_CHANNEL_DENY_MARKER = "Tool permission request failed:";
508
506
  // Hook failures are reported through the same is_error tool_result channel but
@@ -665,27 +663,9 @@ export function createStallDetector(input) {
665
663
  awaitingModel = (msg.type === "user" || msg.type === "stream_event") && pendingToolUses.size === 0;
666
664
  },
667
665
  stalled: () => stalled,
668
- hasPendingToolUses: () => pendingToolUses.size > 0,
669
666
  dispose: () => clearInterval(timer),
670
667
  };
671
668
  }
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;
689
669
  // Decides when the turn is really over and the CLI may be told to exit.
690
670
  //
691
671
  // The prompt rides a held-open input stream (see `createHeldOpenQuery` in
@@ -707,10 +687,7 @@ const USAGE_READING_TIMEOUT_MS = 5_000;
707
687
  // events must not, or the end would never re-arm. After input ends, an exit
708
688
  // guard closes the query if the CLI neither speaks nor exits, so a wedged
709
689
  // process cannot hang the turn until the session watchdog.
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) {
690
+ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
714
691
  const inFlightTasks = new Set();
715
692
  // The background_tasks_changed LEVEL signal, tracked separately from the
716
693
  // edge-paired set above: a foreground command the CLI auto-backgrounds emits
@@ -723,52 +700,15 @@ beforeEndInput) {
723
700
  let cleanResultSeen = false;
724
701
  let lastWasTaskLifecycle = false;
725
702
  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;
733
703
  let settleTimer;
734
704
  let exitGuard;
735
705
  const endInput = () => {
736
706
  settleTimer = undefined;
737
707
  // Fire-time guard: a task that started during the settle window keeps the
738
708
  // channel open; a later settle re-arms the end.
739
- if (inputEnded || ending || hasInFlightWork()) {
709
+ if (inputEnded || hasInFlightWork()) {
740
710
  return;
741
711
  }
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 = () => {
772
712
  inputEnded = true;
773
713
  query.endInput?.();
774
714
  onTurnEvent({
@@ -838,21 +778,15 @@ beforeEndInput) {
838
778
  clearTimeout(settleTimer);
839
779
  settleTimer = undefined;
840
780
  }
841
- if (ending && (isModelActivity || hasInFlightWork())) {
842
- ending.interrupted = true;
843
- }
844
781
  if (exitGuard) {
845
782
  exitGuard.refresh();
846
783
  }
847
784
  },
848
- maybeEndAfter: (msg, refused) => {
785
+ maybeEndAfter: (msg) => {
849
786
  if (inputEnded) {
850
787
  return;
851
788
  }
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)) {
789
+ if (msg.type === "result" && !msg.is_error) {
856
790
  cleanResultSeen = true;
857
791
  if (hasInFlightWork()) {
858
792
  console.error(`[BridgeJob] result received with ${inFlightTasks.size} task(s) and ${backgroundTaskCount} background command(s) in flight — holding the turn open (thread ${job.threadId})`);
@@ -869,10 +803,7 @@ beforeEndInput) {
869
803
  scheduleEnd();
870
804
  }
871
805
  },
872
- hasInFlightWork,
873
- ranTasks: () => sawAnyTask,
874
806
  dispose: () => {
875
- disposed = true;
876
807
  if (settleTimer) {
877
808
  clearTimeout(settleTimer);
878
809
  settleTimer = undefined;
@@ -964,7 +895,7 @@ export class BridgeJobExecutor {
964
895
  readUsageBaseline;
965
896
  stalledCliExitMs;
966
897
  readFlowTree;
967
- settleWorkspace;
898
+ mainSync;
968
899
  constructor(dependencies) {
969
900
  this.sdk = dependencies.sdk;
970
901
  this.relay = dependencies.relay;
@@ -983,9 +914,9 @@ export class BridgeJobExecutor {
983
914
  dependencies.readUsageBaseline ??
984
915
  ((sessionId) => readUsageBaseline(sessionId, process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude")));
985
916
  this.stalledCliExitMs = dependencies.stalledCliExitMs ?? STALLED_CLI_EXIT_MS;
986
- this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
987
917
  this.readFlowTree =
988
918
  dependencies.readFlowTree ?? ((flowSlug) => readRawFlowTree(bridgeRuntimeFiles.flowsDir(), flowSlug));
919
+ this.mainSync = dependencies.mainSync;
989
920
  }
990
921
  async execute(job, controls) {
991
922
  const state = createBridgeJobState();
@@ -998,12 +929,10 @@ export class BridgeJobExecutor {
998
929
  return { kind: "cancelled", costUsd: 0, model: state.model };
999
930
  }
1000
931
  const loaded = await this.loadTrustedBoundary(job, controls);
1001
- if (loaded?.kind !== "clean") {
1002
- request = await this.withContinuedAttachments(job, request);
1003
- }
1004
932
  attempt = prepareSessionAttempt({
1005
933
  requestPrompt: request.prompt,
1006
934
  reminder: request.reminder,
935
+ recoveryReminder: request.recoveryReminder,
1007
936
  history: job.history,
1008
937
  loaded,
1009
938
  leadingCommand: request.leadingCommand,
@@ -1034,6 +963,12 @@ export class BridgeJobExecutor {
1034
963
  await this.clearIfDirty(job, controls, state, classifyDirtyReason(error, controls));
1035
964
  return this.errorOutcome(job, controls, state, this.resultWithObservedCost(result, state), error);
1036
965
  }
966
+ finally {
967
+ // A review only reads the finished Session's workspace.
968
+ if (!job.review) {
969
+ await this.mainSync?.afterTurn();
970
+ }
971
+ }
1037
972
  }
1038
973
  async loadTrustedBoundary(job, controls) {
1039
974
  const loaded = await this.sessionStore.load(job.threadId);
@@ -1055,17 +990,34 @@ export class BridgeJobExecutor {
1055
990
  // skill) must stay the leading token, so they skip speaker attribution and
1056
991
  // reference context — otherwise the prepended lines push the command off the
1057
992
  // front and it lands as ordinary message text the model just echoes.
1058
- const verbatim = verbatimSlashCommand(job.prompt) ??
993
+ const bridgeCommand = verbatimSlashCommand(job.prompt);
994
+ const verbatim = bridgeCommand ??
1059
995
  leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
1060
996
  let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
1061
- const reminder = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
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);
1062
1009
  if (job.attachments && job.attachments.length > 0) {
1063
- requestPrompt += await this.prepareAttachments(job.attachments, "message");
1010
+ requestPrompt += await this.prepareAttachments(job.attachments);
1064
1011
  }
1065
1012
  if (!controls.isActive()) {
1066
1013
  return null;
1067
1014
  }
1068
- return { prompt: requestPrompt, ...(reminder ? { reminder } : {}), leadingCommand: verbatim !== null };
1015
+ return {
1016
+ prompt: requestPrompt,
1017
+ ...(reminder ? { reminder } : {}),
1018
+ ...(recoveryReminder ? { recoveryReminder } : {}),
1019
+ leadingCommand: verbatim !== null,
1020
+ };
1069
1021
  }
1070
1022
  /**
1071
1023
  * The reviewer's prompt: the brief Convex wrote, with the evidence block this
@@ -1264,18 +1216,6 @@ export class BridgeJobExecutor {
1264
1216
  });
1265
1217
  }
1266
1218
  }
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
- }
1279
1219
  async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
1280
1220
  if (!attempt.usedBoundary ||
1281
1221
  state.sawUserVisibleSdkMessage ||
@@ -1284,7 +1224,6 @@ export class BridgeJobExecutor {
1284
1224
  return null;
1285
1225
  }
1286
1226
  await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason: "sdk_error" });
1287
- const rebuilt = await this.withContinuedAttachments(job, request);
1288
1227
  void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydrated_invalid_resume", {
1289
1228
  historyEntryCount: job.history?.entries.length ?? 0,
1290
1229
  omittedEntryCount: job.history?.omittedEntryCount ?? 0,
@@ -1293,12 +1232,12 @@ export class BridgeJobExecutor {
1293
1232
  });
1294
1233
  // A leading command stays bare here too: a history preamble in front of it
1295
1234
  // would turn it into plain text (see `prepareSessionAttempt`).
1296
- if (rebuilt.leadingCommand) {
1297
- return { prompt: rebuilt.prompt, attemptedRehydration: false, resumeOptions: {} };
1235
+ if (request.leadingCommand) {
1236
+ return { prompt: request.prompt, attemptedRehydration: false, resumeOptions: {} };
1298
1237
  }
1299
1238
  return {
1300
- prompt: buildRehydratedPrompt(rebuilt.prompt, job.history),
1301
- ...(rebuilt.reminder ? { reminder: rebuilt.reminder } : {}),
1239
+ prompt: buildRehydratedPrompt(request.prompt, job.history),
1240
+ ...(request.recoveryReminder ? { reminder: request.recoveryReminder } : {}),
1302
1241
  attemptedRehydration: true,
1303
1242
  resumeOptions: {},
1304
1243
  };
@@ -1353,6 +1292,11 @@ export class BridgeJobExecutor {
1353
1292
  const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
1354
1293
  if (malformed)
1355
1294
  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 });
1356
1300
  // Cast what is owed FIRST, whatever comes next — including another create. Checked
1357
1301
  // the other way round, a second create (the agent retrying after a rejected flag,
1358
1302
  // which is what happens in practice) would overwrite the handle and the first
@@ -1578,19 +1522,7 @@ export class BridgeJobExecutor {
1578
1522
  // in-progress session.
1579
1523
  async drainQuery(job, controls, state, query, closeQuery) {
1580
1524
  const stallDetector = this.createStallDetector(job, controls, closeQuery);
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
- });
1525
+ const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent);
1594
1526
  let costUsd = 0;
1595
1527
  // Errors thrown while HANDLING a message (control-channel breaker, error
1596
1528
  // results) are real verdicts and must surface as-is; only errors from the
@@ -1600,7 +1532,6 @@ export class BridgeJobExecutor {
1600
1532
  for await (const msg of query) {
1601
1533
  stallDetector.observeMessage(msg);
1602
1534
  turnEnd.observeMessage(msg);
1603
- controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
1604
1535
  try {
1605
1536
  costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
1606
1537
  }
@@ -1608,7 +1539,7 @@ export class BridgeJobExecutor {
1608
1539
  consumeError = true;
1609
1540
  throw error;
1610
1541
  }
1611
- turnEnd.maybeEndAfter(msg, state.refusedWithoutFallback === true);
1542
+ turnEnd.maybeEndAfter(msg);
1612
1543
  if (!controls.isActive()) {
1613
1544
  closeQuery();
1614
1545
  break;
@@ -1623,8 +1554,6 @@ export class BridgeJobExecutor {
1623
1554
  throw error;
1624
1555
  }
1625
1556
  finally {
1626
- drained = true;
1627
- controls.setWorkInFlight?.(false);
1628
1557
  stallDetector.dispose();
1629
1558
  turnEnd.dispose();
1630
1559
  await this.openPendingCastingAtTurnEnd(job, controls);
@@ -1632,11 +1561,7 @@ export class BridgeJobExecutor {
1632
1561
  if (stallDetector.stalled() && controls.isActive()) {
1633
1562
  throw new StreamStallError();
1634
1563
  }
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);
1564
+ return costUsd;
1640
1565
  }
1641
1566
  /**
1642
1567
  * Open a casting the turn recorded but never reached.
@@ -1729,7 +1654,17 @@ export class BridgeJobExecutor {
1729
1654
  }
1730
1655
  trackMainRequestUsage(state, msg);
1731
1656
  if (msg.type === "result") {
1732
- const ownCostUsd = recordCumulativeUsage(state, { totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage });
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
+ }
1733
1668
  await this.publishContextUsage(job, state, msg.modelUsage);
1734
1669
  // What a Session review returns. The SDK enforces the JSON schema, so this
1735
1670
  // either parses against the findings contract or the review failed to
@@ -1746,7 +1681,7 @@ export class BridgeJobExecutor {
1746
1681
  await this.clearIfDirty(job, controls, state, "result_error");
1747
1682
  throw resultError;
1748
1683
  }
1749
- return ownCostUsd;
1684
+ return own.costUsd;
1750
1685
  }
1751
1686
  if (isUserVisibleSdkMessage(msg)) {
1752
1687
  state.sawUserVisibleSdkMessage = true;
@@ -2359,4 +2294,7 @@ function userTurnsBefore(job) {
2359
2294
  .trim();
2360
2295
  return said || undefined;
2361
2296
  }
2297
+ function joinReminders(...blocks) {
2298
+ return blocks.filter((block) => block !== undefined).join("\n\n") || undefined;
2299
+ }
2362
2300
  //# sourceMappingURL=bridge-job.js.map