@koda-sl/baker-bridge 0.108.0-dev.f7c15fa76 → 0.109.0-dev.88120240
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.
- package/README.md +22 -21
- package/dist/hono/agent-session.d.ts +1 -0
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +13 -2
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +37 -0
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent-tasks.d.ts.map +1 -1
- package/dist/hono/agent-tasks.js +7 -3
- package/dist/hono/agent-tasks.js.map +1 -1
- package/dist/hono/agent-tasks.test.js +15 -0
- package/dist/hono/agent-tasks.test.js.map +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +12 -9
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/attachments.d.ts +6 -1
- package/dist/hono/attachments.d.ts.map +1 -1
- package/dist/hono/attachments.js +9 -4
- package/dist/hono/attachments.js.map +1 -1
- package/dist/hono/attachments.test.js +5 -0
- package/dist/hono/attachments.test.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +31 -12
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +130 -62
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +227 -146
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +0 -1
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +2 -5
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/types.d.ts +7 -6
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/hono/workspace-settle.d.ts +20 -0
- package/dist/hono/workspace-settle.d.ts.map +1 -0
- package/dist/hono/workspace-settle.js +46 -0
- package/dist/hono/workspace-settle.js.map +1 -0
- package/dist/review/digest.d.ts +7 -1
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +80 -16
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +83 -0
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +2 -1
- package/dist/hono/main-sync.d.ts +0 -41
- package/dist/hono/main-sync.d.ts.map +0 -1
- package/dist/hono/main-sync.js +0 -390
- package/dist/hono/main-sync.js.map +0 -1
- package/dist/hono/main-sync.test.d.ts +0 -2
- package/dist/hono/main-sync.test.d.ts.map +0 -1
- package/dist/hono/main-sync.test.js +0 -266
- package/dist/hono/main-sync.test.js.map +0 -1
package/dist/hono/bridge-job.js
CHANGED
|
@@ -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
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
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
|
|
502
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -984,34 +1055,17 @@ export class BridgeJobExecutor {
|
|
|
984
1055
|
// skill) must stay the leading token, so they skip speaker attribution and
|
|
985
1056
|
// reference context — otherwise the prepended lines push the command off the
|
|
986
1057
|
// front and it lands as ordinary message text the model just echoes.
|
|
987
|
-
const
|
|
988
|
-
const verbatim = bridgeCommand ??
|
|
1058
|
+
const verbatim = verbatimSlashCommand(job.prompt) ??
|
|
989
1059
|
leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
|
|
990
1060
|
let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
|
|
991
|
-
|
|
992
|
-
// itself). A skill reads and edits the workspace, so it is brought level
|
|
993
|
-
// first — but a note beside the command would push it off the front, so
|
|
994
|
-
// the thread hears about it on its next turn.
|
|
995
|
-
const notes = bridgeCommand === null
|
|
996
|
-
? ((await this.mainSync?.beforeTurn(job.threadId, job.publishedElsewhere ?? [], {
|
|
997
|
-
withNote: verbatim === null,
|
|
998
|
-
})) ?? {})
|
|
999
|
-
: {};
|
|
1000
|
-
const viewing = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
|
|
1001
|
-
const reminder = joinReminders(notes.resumed, viewing);
|
|
1002
|
-
const recoveryReminder = joinReminders(notes.recovered, viewing);
|
|
1061
|
+
const reminder = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
|
|
1003
1062
|
if (job.attachments && job.attachments.length > 0) {
|
|
1004
|
-
requestPrompt += await this.prepareAttachments(job.attachments);
|
|
1063
|
+
requestPrompt += await this.prepareAttachments(job.attachments, "message");
|
|
1005
1064
|
}
|
|
1006
1065
|
if (!controls.isActive()) {
|
|
1007
1066
|
return null;
|
|
1008
1067
|
}
|
|
1009
|
-
return {
|
|
1010
|
-
prompt: requestPrompt,
|
|
1011
|
-
...(reminder ? { reminder } : {}),
|
|
1012
|
-
...(recoveryReminder ? { recoveryReminder } : {}),
|
|
1013
|
-
leadingCommand: verbatim !== null,
|
|
1014
|
-
};
|
|
1068
|
+
return { prompt: requestPrompt, ...(reminder ? { reminder } : {}), leadingCommand: verbatim !== null };
|
|
1015
1069
|
}
|
|
1016
1070
|
/**
|
|
1017
1071
|
* The reviewer's prompt: the brief Convex wrote, with the evidence block this
|
|
@@ -1210,6 +1264,18 @@ export class BridgeJobExecutor {
|
|
|
1210
1264
|
});
|
|
1211
1265
|
}
|
|
1212
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
|
+
}
|
|
1213
1279
|
async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
|
|
1214
1280
|
if (!attempt.usedBoundary ||
|
|
1215
1281
|
state.sawUserVisibleSdkMessage ||
|
|
@@ -1218,6 +1284,7 @@ export class BridgeJobExecutor {
|
|
|
1218
1284
|
return null;
|
|
1219
1285
|
}
|
|
1220
1286
|
await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason: "sdk_error" });
|
|
1287
|
+
const rebuilt = await this.withContinuedAttachments(job, request);
|
|
1221
1288
|
void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydrated_invalid_resume", {
|
|
1222
1289
|
historyEntryCount: job.history?.entries.length ?? 0,
|
|
1223
1290
|
omittedEntryCount: job.history?.omittedEntryCount ?? 0,
|
|
@@ -1226,12 +1293,12 @@ export class BridgeJobExecutor {
|
|
|
1226
1293
|
});
|
|
1227
1294
|
// A leading command stays bare here too: a history preamble in front of it
|
|
1228
1295
|
// would turn it into plain text (see `prepareSessionAttempt`).
|
|
1229
|
-
if (
|
|
1230
|
-
return { prompt:
|
|
1296
|
+
if (rebuilt.leadingCommand) {
|
|
1297
|
+
return { prompt: rebuilt.prompt, attemptedRehydration: false, resumeOptions: {} };
|
|
1231
1298
|
}
|
|
1232
1299
|
return {
|
|
1233
|
-
prompt: buildRehydratedPrompt(
|
|
1234
|
-
...(
|
|
1300
|
+
prompt: buildRehydratedPrompt(rebuilt.prompt, job.history),
|
|
1301
|
+
...(rebuilt.reminder ? { reminder: rebuilt.reminder } : {}),
|
|
1235
1302
|
attemptedRehydration: true,
|
|
1236
1303
|
resumeOptions: {},
|
|
1237
1304
|
};
|
|
@@ -1286,11 +1353,6 @@ export class BridgeJobExecutor {
|
|
|
1286
1353
|
const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
|
|
1287
1354
|
if (malformed)
|
|
1288
1355
|
return Promise.resolve({ behavior: "deny", message: malformed });
|
|
1289
|
-
const clash = toolName === "Write" && typeof input?.file_path === "string"
|
|
1290
|
-
? (this.mainSync?.refuseWrite(job.threadId, input.file_path) ?? null)
|
|
1291
|
-
: null;
|
|
1292
|
-
if (clash)
|
|
1293
|
-
return Promise.resolve({ behavior: "deny", message: clash });
|
|
1294
1356
|
// Cast what is owed FIRST, whatever comes next — including another create. Checked
|
|
1295
1357
|
// the other way round, a second create (the agent retrying after a rejected flag,
|
|
1296
1358
|
// which is what happens in practice) would overwrite the handle and the first
|
|
@@ -1516,7 +1578,19 @@ export class BridgeJobExecutor {
|
|
|
1516
1578
|
// in-progress session.
|
|
1517
1579
|
async drainQuery(job, controls, state, query, closeQuery) {
|
|
1518
1580
|
const stallDetector = this.createStallDetector(job, controls, closeQuery);
|
|
1519
|
-
|
|
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
|
+
});
|
|
1520
1594
|
let costUsd = 0;
|
|
1521
1595
|
// Errors thrown while HANDLING a message (control-channel breaker, error
|
|
1522
1596
|
// results) are real verdicts and must surface as-is; only errors from the
|
|
@@ -1526,6 +1600,7 @@ export class BridgeJobExecutor {
|
|
|
1526
1600
|
for await (const msg of query) {
|
|
1527
1601
|
stallDetector.observeMessage(msg);
|
|
1528
1602
|
turnEnd.observeMessage(msg);
|
|
1603
|
+
controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
|
|
1529
1604
|
try {
|
|
1530
1605
|
costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
|
|
1531
1606
|
}
|
|
@@ -1533,7 +1608,7 @@ export class BridgeJobExecutor {
|
|
|
1533
1608
|
consumeError = true;
|
|
1534
1609
|
throw error;
|
|
1535
1610
|
}
|
|
1536
|
-
turnEnd.maybeEndAfter(msg);
|
|
1611
|
+
turnEnd.maybeEndAfter(msg, state.refusedWithoutFallback === true);
|
|
1537
1612
|
if (!controls.isActive()) {
|
|
1538
1613
|
closeQuery();
|
|
1539
1614
|
break;
|
|
@@ -1548,6 +1623,8 @@ export class BridgeJobExecutor {
|
|
|
1548
1623
|
throw error;
|
|
1549
1624
|
}
|
|
1550
1625
|
finally {
|
|
1626
|
+
drained = true;
|
|
1627
|
+
controls.setWorkInFlight?.(false);
|
|
1551
1628
|
stallDetector.dispose();
|
|
1552
1629
|
turnEnd.dispose();
|
|
1553
1630
|
await this.openPendingCastingAtTurnEnd(job, controls);
|
|
@@ -1555,7 +1632,11 @@ export class BridgeJobExecutor {
|
|
|
1555
1632
|
if (stallDetector.stalled() && controls.isActive()) {
|
|
1556
1633
|
throw new StreamStallError();
|
|
1557
1634
|
}
|
|
1558
|
-
|
|
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);
|
|
1559
1640
|
}
|
|
1560
1641
|
/**
|
|
1561
1642
|
* Open a casting the turn recorded but never reached.
|
|
@@ -1648,17 +1729,7 @@ export class BridgeJobExecutor {
|
|
|
1648
1729
|
}
|
|
1649
1730
|
trackMainRequestUsage(state, msg);
|
|
1650
1731
|
if (msg.type === "result") {
|
|
1651
|
-
const
|
|
1652
|
-
state.observedCostUsd = own.costUsd;
|
|
1653
|
-
// A query's result `modelUsage` is cumulative (like `total_cost_usd`, which
|
|
1654
|
-
// we overwrite above), so keep the latest snapshot rather than summing —
|
|
1655
|
-
// it is folded into the turn totals when the query settles (see
|
|
1656
|
-
// `runQueryOnce`). Guard against a spuriously empty later result wiping a
|
|
1657
|
-
// real earlier one.
|
|
1658
|
-
const snapshot = queryModelUsage(own.modelUsage);
|
|
1659
|
-
if (snapshot.size > 0) {
|
|
1660
|
-
state.queryModelUsage = snapshot;
|
|
1661
|
-
}
|
|
1732
|
+
const ownCostUsd = recordCumulativeUsage(state, { totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage });
|
|
1662
1733
|
await this.publishContextUsage(job, state, msg.modelUsage);
|
|
1663
1734
|
// What a Session review returns. The SDK enforces the JSON schema, so this
|
|
1664
1735
|
// either parses against the findings contract or the review failed to
|
|
@@ -1675,7 +1746,7 @@ export class BridgeJobExecutor {
|
|
|
1675
1746
|
await this.clearIfDirty(job, controls, state, "result_error");
|
|
1676
1747
|
throw resultError;
|
|
1677
1748
|
}
|
|
1678
|
-
return
|
|
1749
|
+
return ownCostUsd;
|
|
1679
1750
|
}
|
|
1680
1751
|
if (isUserVisibleSdkMessage(msg)) {
|
|
1681
1752
|
state.sawUserVisibleSdkMessage = true;
|
|
@@ -2288,7 +2359,4 @@ function userTurnsBefore(job) {
|
|
|
2288
2359
|
.trim();
|
|
2289
2360
|
return said || undefined;
|
|
2290
2361
|
}
|
|
2291
|
-
function joinReminders(...blocks) {
|
|
2292
|
-
return blocks.filter((block) => block !== undefined).join("\n\n") || undefined;
|
|
2293
|
-
}
|
|
2294
2362
|
//# sourceMappingURL=bridge-job.js.map
|