@koda-sl/baker-bridge 0.110.0-dev.6a86a7961 → 0.110.0-dev.ec7b0e42b
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 +21 -19
- 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 -68
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +227 -170
- 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/tag-fields.d.ts.map +1 -1
- package/dist/hono/tag-fields.js +2 -0
- package/dist/hono/tag-fields.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 -46
- package/dist/hono/main-sync.d.ts.map +0 -1
- package/dist/hono/main-sync.js +0 -422
- 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 -288
- 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,
|
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
1236
|
-
return { prompt:
|
|
1296
|
+
if (rebuilt.leadingCommand) {
|
|
1297
|
+
return { prompt: rebuilt.prompt, attemptedRehydration: false, resumeOptions: {} };
|
|
1237
1298
|
}
|
|
1238
1299
|
return {
|
|
1239
|
-
prompt: buildRehydratedPrompt(
|
|
1240
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|