@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.
- package/README.md +19 -19
- package/dist/hono/agent-session.d.ts +0 -1
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +2 -13
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +0 -37
- 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 +3 -7
- package/dist/hono/agent-tasks.js.map +1 -1
- package/dist/hono/agent-tasks.test.js +0 -15
- package/dist/hono/agent-tasks.test.js.map +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +9 -12
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/attachments.d.ts +1 -6
- package/dist/hono/attachments.d.ts.map +1 -1
- package/dist/hono/attachments.js +4 -9
- package/dist/hono/attachments.js.map +1 -1
- package/dist/hono/attachments.test.js +0 -5
- package/dist/hono/attachments.test.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +12 -31
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +68 -130
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +170 -227
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/main-sync.d.ts +46 -0
- package/dist/hono/main-sync.d.ts.map +1 -0
- package/dist/hono/main-sync.js +422 -0
- package/dist/hono/main-sync.js.map +1 -0
- package/dist/hono/main-sync.test.d.ts +2 -0
- package/dist/hono/main-sync.test.d.ts.map +1 -0
- package/dist/hono/main-sync.test.js +288 -0
- package/dist/hono/main-sync.test.js.map +1 -0
- package/dist/hono/session-continuity.d.ts +1 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +5 -2
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/types.d.ts +6 -7
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/review/digest.d.ts +1 -7
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +16 -80
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +0 -83
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +1 -2
- package/dist/hono/workspace-settle.d.ts +0 -20
- package/dist/hono/workspace-settle.d.ts.map +0 -1
- package/dist/hono/workspace-settle.js +0 -46
- package/dist/hono/workspace-settle.js.map +0 -1
package/dist/hono/bridge-job.js
CHANGED
|
@@ -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
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
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
|
|
504
|
-
//
|
|
505
|
-
// block
|
|
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 ||
|
|
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
|
|
785
|
+
maybeEndAfter: (msg) => {
|
|
849
786
|
if (inputEnded) {
|
|
850
787
|
return;
|
|
851
788
|
}
|
|
852
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1010
|
+
requestPrompt += await this.prepareAttachments(job.attachments);
|
|
1064
1011
|
}
|
|
1065
1012
|
if (!controls.isActive()) {
|
|
1066
1013
|
return null;
|
|
1067
1014
|
}
|
|
1068
|
-
return {
|
|
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 (
|
|
1297
|
-
return { prompt:
|
|
1235
|
+
if (request.leadingCommand) {
|
|
1236
|
+
return { prompt: request.prompt, attemptedRehydration: false, resumeOptions: {} };
|
|
1298
1237
|
}
|
|
1299
1238
|
return {
|
|
1300
|
-
prompt: buildRehydratedPrompt(
|
|
1301
|
-
...(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|