@koda-sl/baker-bridge 0.152.0-dev.1a19b1876 → 0.153.0-dev.ded7a9497
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 +33 -7
- 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 +0 -7
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +1 -7
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +5 -8
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +9 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +151 -20
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +233 -2
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/convex.d.ts +2 -1
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/convex.js +4 -0
- package/dist/hono/convex.js.map +1 -1
- package/dist/hono/main-sync.d.ts.map +1 -1
- package/dist/hono/main-sync.js +18 -5
- package/dist/hono/main-sync.js.map +1 -1
- package/dist/hono/main-sync.test.js +12 -0
- package/dist/hono/main-sync.test.js.map +1 -1
- package/dist/hono/relay.d.ts +8 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js +3 -0
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/served-model.d.ts +23 -0
- package/dist/hono/served-model.d.ts.map +1 -0
- package/dist/hono/served-model.js +82 -0
- package/dist/hono/served-model.js.map +1 -0
- package/dist/hono/served-model.test.d.ts +2 -0
- package/dist/hono/served-model.test.d.ts.map +1 -0
- package/dist/hono/served-model.test.js +29 -0
- package/dist/hono/served-model.test.js.map +1 -0
- package/dist/hono/session-continuity.d.ts +30 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +201 -5
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/session-continuity.js +99 -8
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/session-store.d.ts +2 -0
- package/dist/hono/session-store.d.ts.map +1 -1
- package/dist/hono/session-store.js +6 -0
- package/dist/hono/session-store.js.map +1 -1
- package/dist/hono/session-usage-baseline.d.ts +1 -0
- package/dist/hono/session-usage-baseline.d.ts.map +1 -1
- package/dist/hono/session-usage-baseline.js +17 -4
- package/dist/hono/session-usage-baseline.js.map +1 -1
- package/dist/hono/turn-events.d.ts +1 -1
- package/dist/hono/turn-events.d.ts.map +1 -1
- package/dist/hono/turn-events.js.map +1 -1
- package/dist/review/digest.d.ts +35 -0
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +171 -27
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +130 -1
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +1 -1
package/dist/hono/bridge-job.js
CHANGED
|
@@ -26,7 +26,8 @@ import { BEDROCK_SERVABLE_CAPABILITIES, buildAgentExtraBody, EXTRA_BODY_ENV, MOD
|
|
|
26
26
|
import { credentialReminder, currentReferenceReminder, kickoffBriefReminder } from "./prompt-context.js";
|
|
27
27
|
import { publishLanguageReminder, publishRunReminder } from "./publish-handoff.js";
|
|
28
28
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
29
|
-
import {
|
|
29
|
+
import { alignServedModels } from "./served-model.js";
|
|
30
|
+
import { applyCurrentSenderAttribution, buildCancelBoundary, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, interruptedResumeAttempt, isCliResumeRefusal, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
|
|
30
31
|
import { readSessionSkills } from "./session-skills.js";
|
|
31
32
|
import { ownQueryUsage, readUsageBaseline } from "./session-usage-baseline.js";
|
|
32
33
|
import { BridgeSlashCommands, leadingSkillCommand, verbatimSlashCommand } from "./slash-commands.js";
|
|
@@ -424,10 +425,20 @@ const CONTEXT_OVERFLOW_MESSAGE = "This chat has grown too long to continue in on
|
|
|
424
425
|
// so the turn ends on this instead. The refused exchange is not saved to the
|
|
425
426
|
// session, so a rephrased message starts from the conversation before it.
|
|
426
427
|
const MODEL_REFUSAL_MESSAGE = "I couldn't help with that request. Try rephrasing what you need.";
|
|
427
|
-
|
|
428
|
+
// What the user reads when the model provider failed the request itself — out
|
|
429
|
+
// of credit, rate-limited, down. The CLI's own text ("API Error: 402 Insufficient
|
|
430
|
+
// credits. Add more using https://openrouter.ai/…") is the provider talking to
|
|
431
|
+
// Baker: a vendor, a status code and a billing page the marketer cannot open.
|
|
432
|
+
// The raw text stays in the bridge log and the turn events.
|
|
433
|
+
const PROVIDER_FAILURE_MESSAGE = "Baker's AI service had a problem on our side, so this step stopped. Nothing you did caused it — send your message again to pick up where it stopped.";
|
|
434
|
+
function userFacingTurnError(error) {
|
|
435
|
+
const message = errorMessage(error);
|
|
428
436
|
if (isContextOverflowError(message)) {
|
|
429
437
|
return CONTEXT_OVERFLOW_MESSAGE;
|
|
430
438
|
}
|
|
439
|
+
if (error instanceof SdkResultError && error.providerFailure) {
|
|
440
|
+
return PROVIDER_FAILURE_MESSAGE;
|
|
441
|
+
}
|
|
431
442
|
return enrichImageError(message);
|
|
432
443
|
}
|
|
433
444
|
function enrichImageError(message) {
|
|
@@ -468,6 +479,9 @@ function subagentModelEnv() {
|
|
|
468
479
|
const override = agentModelOverride();
|
|
469
480
|
return override ? { CLAUDE_CODE_SUBAGENT_MODEL: override, CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "1" } : {};
|
|
470
481
|
}
|
|
482
|
+
function claudeConfigDir() {
|
|
483
|
+
return process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude");
|
|
484
|
+
}
|
|
471
485
|
function claudePlanDir() {
|
|
472
486
|
return resolve(process.env.HOME || homedir(), ".claude", "plans");
|
|
473
487
|
}
|
|
@@ -565,14 +579,35 @@ function sdkResultErrorMessage(msg) {
|
|
|
565
579
|
function turnResultError(state, msg) {
|
|
566
580
|
return state.refusedWithoutFallback ? undefined : sdkResultError(msg);
|
|
567
581
|
}
|
|
582
|
+
class SdkResultError extends Error {
|
|
583
|
+
providerFailure;
|
|
584
|
+
name = "SdkResultError";
|
|
585
|
+
constructor(message,
|
|
586
|
+
/** The model provider refused or failed the request, rather than the turn's own work failing. */
|
|
587
|
+
providerFailure) {
|
|
588
|
+
super(`Claude Code returned an error result: ${message}`);
|
|
589
|
+
this.providerFailure = providerFailure;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// What the request's own content caused (an unreadable image, a malformed or
|
|
593
|
+
// oversized request): sending the same message again would fail the same way, so
|
|
594
|
+
// it keeps its own remediation instead of the provider-failure message.
|
|
595
|
+
const REQUEST_CONTENT_STATUSES = [400, 413, 422];
|
|
596
|
+
function isProviderFailureResult(msg) {
|
|
597
|
+
if (msg.subtype !== "success") {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const status = msg.api_error_status ?? Number(/^API Error: (\d{3})\b/.exec(msg.result)?.[1]);
|
|
601
|
+
if (REQUEST_CONTENT_STATUSES.includes(status)) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
return typeof msg.api_error_status === "number" || msg.result.startsWith("API Error");
|
|
605
|
+
}
|
|
568
606
|
function sdkResultError(msg) {
|
|
569
607
|
if (msg.type !== "result" || !msg.is_error) {
|
|
570
608
|
return undefined;
|
|
571
609
|
}
|
|
572
|
-
|
|
573
|
-
const error = new Error(`Claude Code returned an error result: ${message}`);
|
|
574
|
-
error.name = "SdkResultError";
|
|
575
|
-
return error;
|
|
610
|
+
return new SdkResultError(sdkResultErrorMessage(msg), isProviderFailureResult(msg));
|
|
576
611
|
}
|
|
577
612
|
// When the bridge closes the sandbox CLI's stdin (its permission control
|
|
578
613
|
// channel) — on abort/replace/timeout — while the CLI is still alive (a long
|
|
@@ -637,6 +672,22 @@ export function isControlChannelDeny(msg) {
|
|
|
637
672
|
}
|
|
638
673
|
return false;
|
|
639
674
|
}
|
|
675
|
+
// A tool result's prefix sent for the history replay, which keeps only its head.
|
|
676
|
+
const TOOL_OUTCOME_TEXT_CHARS = 2000;
|
|
677
|
+
/** How each tool call answered in a `user` message came out, bounded to what history replays. */
|
|
678
|
+
export function toolOutcomesInMessage(msg) {
|
|
679
|
+
if (msg.type !== "user")
|
|
680
|
+
return [];
|
|
681
|
+
return messageContentBlocks(msg).flatMap((block) => block.type === "tool_result" && typeof block.tool_use_id === "string"
|
|
682
|
+
? [
|
|
683
|
+
{
|
|
684
|
+
toolUseId: block.tool_use_id,
|
|
685
|
+
isError: block.is_error === true,
|
|
686
|
+
text: toolResultText(block.content).slice(0, TOOL_OUTCOME_TEXT_CHARS),
|
|
687
|
+
},
|
|
688
|
+
]
|
|
689
|
+
: []);
|
|
690
|
+
}
|
|
640
691
|
function controlChannelClosedError() {
|
|
641
692
|
const error = new Error("The sandbox tool control-channel closed mid-turn — work outlived the turn it was started in " +
|
|
642
693
|
"(a backgrounded command or a subagent waking the model back up), so further tool calls were " +
|
|
@@ -686,6 +737,15 @@ export function isContextOverflowRefusal(msg) {
|
|
|
686
737
|
}
|
|
687
738
|
return OVERFLOW_REFUSAL_LEADS.some((lead) => text.startsWith(lead));
|
|
688
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* True when an assistant message is the CLI's own report of a failed model
|
|
742
|
+
* request ("API Error: 402 Insufficient credits…", "API Error: Server error
|
|
743
|
+
* mid-response…"), not anything the model wrote. The CLI writes it for a
|
|
744
|
+
* terminal user: a `<synthetic>` model and the request's `error` class.
|
|
745
|
+
*/
|
|
746
|
+
function isApiErrorMessage(msg) {
|
|
747
|
+
return msg.type === "assistant" && msg.error !== undefined && msg.message.model === "<synthetic>";
|
|
748
|
+
}
|
|
689
749
|
/** Content blocks of an assistant/user SDK message; [] for other shapes. */
|
|
690
750
|
function messageContentBlocks(msg) {
|
|
691
751
|
const content = msg.message?.content;
|
|
@@ -1084,8 +1144,7 @@ export class BridgeJobExecutor {
|
|
|
1084
1144
|
this.turnEndExitGuardMs = dependencies.turnEndExitGuardMs ?? DEFAULT_TURN_END_EXIT_GUARD_MS;
|
|
1085
1145
|
this.now = dependencies.now ?? Date.now;
|
|
1086
1146
|
this.readUsageBaseline =
|
|
1087
|
-
dependencies.readUsageBaseline ??
|
|
1088
|
-
((sessionId) => readUsageBaseline(sessionId, process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude")));
|
|
1147
|
+
dependencies.readUsageBaseline ?? ((sessionId) => readUsageBaseline(sessionId, claudeConfigDir()));
|
|
1089
1148
|
this.stalledCliExitMs = dependencies.stalledCliExitMs ?? STALLED_CLI_EXIT_MS;
|
|
1090
1149
|
this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
|
|
1091
1150
|
this.catchUpWithMain = dependencies.catchUpWithMain;
|
|
@@ -1122,12 +1181,14 @@ export class BridgeJobExecutor {
|
|
|
1122
1181
|
replayAttempt = prepareSessionAttempt({
|
|
1123
1182
|
requestPrompt: request.prompt,
|
|
1124
1183
|
reminder: request.reminder,
|
|
1184
|
+
delivered: request.mainSync?.delivered,
|
|
1125
1185
|
history: job.history,
|
|
1126
1186
|
loaded,
|
|
1127
1187
|
leadingCommand: request.leadingCommand,
|
|
1128
1188
|
});
|
|
1129
|
-
attempt = (await this.interruptedTurnAttempt(job, replayAttempt)) ?? replayAttempt;
|
|
1189
|
+
attempt = (await this.interruptedTurnAttempt(job, replayAttempt, request.mainSync)) ?? replayAttempt;
|
|
1130
1190
|
state.usedBoundary = attempt.usedBoundary;
|
|
1191
|
+
state.requestPrompt = request.prompt;
|
|
1131
1192
|
state.detachedSession = attempt.detachedSession === true;
|
|
1132
1193
|
result.attemptedRehydration = attempt.attemptedRehydration;
|
|
1133
1194
|
this.recordAttemptContextSource(job, attempt);
|
|
@@ -1138,6 +1199,7 @@ export class BridgeJobExecutor {
|
|
|
1138
1199
|
const repaired = await this.fallbackAttempt(job, controls, { request, attempt, replayAttempt }, state, error);
|
|
1139
1200
|
if (repaired && controls.isActive()) {
|
|
1140
1201
|
const repairState = createBridgeJobState();
|
|
1202
|
+
repairState.detachedSession = repaired.detachedSession === true;
|
|
1141
1203
|
let repairResult = { costUsd: 0, attemptedRehydration: repaired.attemptedRehydration };
|
|
1142
1204
|
try {
|
|
1143
1205
|
repairResult = await this.runAttempt(job, controls, repairState, repaired);
|
|
@@ -1163,9 +1225,9 @@ export class BridgeJobExecutor {
|
|
|
1163
1225
|
return await this.invalidResumeFallbackAttempt(job, controls, tried.request, tried.attempt, state, error);
|
|
1164
1226
|
}
|
|
1165
1227
|
/** The turn's own session, when Convex sent again a turn this bridge was running when it went down. */
|
|
1166
|
-
async interruptedTurnAttempt(job, replay) {
|
|
1228
|
+
async interruptedTurnAttempt(job, replay, mainSync) {
|
|
1167
1229
|
const inFlight = await this.inFlightTurns.load(job.threadId).catch(() => undefined);
|
|
1168
|
-
const resumed = interruptedResumeAttempt(replay, { jobTurnId: job.turnId, inFlight });
|
|
1230
|
+
const resumed = interruptedResumeAttempt(replay, { jobTurnId: job.turnId, inFlight, mainSync });
|
|
1169
1231
|
if (inFlight && !resumed) {
|
|
1170
1232
|
await this.inFlightTurns.clear(job.threadId, inFlight.turnId).catch(() => undefined);
|
|
1171
1233
|
}
|
|
@@ -1178,7 +1240,7 @@ export class BridgeJobExecutor {
|
|
|
1178
1240
|
async interruptedResumeFallbackAttempt(job, replay, state, error) {
|
|
1179
1241
|
if (!replay ||
|
|
1180
1242
|
state.sawUserVisibleSdkMessage ||
|
|
1181
|
-
(error instanceof Error && error.name === "SdkResultError") ||
|
|
1243
|
+
(error instanceof Error && error.name === "SdkResultError" && !isCliResumeRefusal(error)) ||
|
|
1182
1244
|
!isResumeSessionError(error)) {
|
|
1183
1245
|
return null;
|
|
1184
1246
|
}
|
|
@@ -1216,17 +1278,21 @@ export class BridgeJobExecutor {
|
|
|
1216
1278
|
leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
|
|
1217
1279
|
let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
|
|
1218
1280
|
// A command must stay the leading token, so it carries no note: the news waits for the next turn.
|
|
1219
|
-
const
|
|
1281
|
+
const syncNote = verbatim === null ? mainSyncReminder(mainSync) : undefined;
|
|
1282
|
+
const reminder = turnReminder(job, verbatim, syncNote);
|
|
1220
1283
|
if (job.attachments && job.attachments.length > 0) {
|
|
1221
1284
|
requestPrompt += await this.prepareAttachments(job.attachments, "message");
|
|
1222
1285
|
}
|
|
1223
1286
|
if (!controls.isActive()) {
|
|
1224
1287
|
return null;
|
|
1225
1288
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1289
|
+
return {
|
|
1290
|
+
prompt: requestPrompt,
|
|
1291
|
+
...(reminder ? { reminder } : {}),
|
|
1292
|
+
leadingCommand: verbatim !== null,
|
|
1293
|
+
// Told only when an attempt carrying the note is sent (`runQueryOnce`).
|
|
1294
|
+
...(mainSync && syncNote ? { mainSync: { reminder: syncNote, delivered: mainSync.told } } : {}),
|
|
1295
|
+
};
|
|
1230
1296
|
}
|
|
1231
1297
|
/**
|
|
1232
1298
|
* The reviewer's prompt: the brief Convex wrote, with the evidence block this
|
|
@@ -1383,18 +1449,29 @@ export class BridgeJobExecutor {
|
|
|
1383
1449
|
if (!controls.isActive()) {
|
|
1384
1450
|
return { kind: "cancelled", costUsd: result.costUsd, model, modelUsage };
|
|
1385
1451
|
}
|
|
1452
|
+
const shown = userFacingTurnError(error);
|
|
1453
|
+
if (shown !== errorMessage(error)) {
|
|
1454
|
+
console.error(`[BridgeJob] turn failed for thread ${job.threadId}: ${errorMessage(error)}`);
|
|
1455
|
+
}
|
|
1386
1456
|
return {
|
|
1387
1457
|
kind: "errored",
|
|
1388
1458
|
costUsd: result.costUsd,
|
|
1389
1459
|
model,
|
|
1390
1460
|
modelUsage,
|
|
1391
|
-
errors: [
|
|
1461
|
+
errors: [shown],
|
|
1392
1462
|
};
|
|
1393
1463
|
}
|
|
1394
1464
|
usageBaselineFor(attempt) {
|
|
1395
1465
|
const resumed = attempt.resumeOptions.resume;
|
|
1396
1466
|
return resumed ? this.readUsageBaseline(resumed) : Promise.resolve(undefined);
|
|
1397
1467
|
}
|
|
1468
|
+
alignResumedTranscript(attempt) {
|
|
1469
|
+
const resumed = attempt.resumeOptions.resume;
|
|
1470
|
+
if (!resumed) {
|
|
1471
|
+
return Promise.resolve();
|
|
1472
|
+
}
|
|
1473
|
+
return alignServedModels(resumed, claudeConfigDir());
|
|
1474
|
+
}
|
|
1398
1475
|
async awaitReplacedCliExit(next) {
|
|
1399
1476
|
if (next.resumeOptions.resume && this.stalledCliExitMs > 0) {
|
|
1400
1477
|
await new Promise((resolve) => setTimeout(resolve, this.stalledCliExitMs));
|
|
@@ -1460,7 +1537,7 @@ export class BridgeJobExecutor {
|
|
|
1460
1537
|
async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
|
|
1461
1538
|
if (!attempt.usedBoundary ||
|
|
1462
1539
|
state.sawUserVisibleSdkMessage ||
|
|
1463
|
-
(error instanceof Error && error.name === "SdkResultError") ||
|
|
1540
|
+
(error instanceof Error && error.name === "SdkResultError" && !isCliResumeRefusal(error)) ||
|
|
1464
1541
|
!isResumeSessionError(error)) {
|
|
1465
1542
|
return null;
|
|
1466
1543
|
}
|
|
@@ -1480,6 +1557,7 @@ export class BridgeJobExecutor {
|
|
|
1480
1557
|
return {
|
|
1481
1558
|
prompt: buildRehydratedPrompt(rebuilt.prompt, job.history),
|
|
1482
1559
|
...(rebuilt.reminder ? { reminder: rebuilt.reminder } : {}),
|
|
1560
|
+
...(rebuilt.reminder && rebuilt.mainSync ? { delivered: rebuilt.mainSync.delivered } : {}),
|
|
1483
1561
|
attemptedRehydration: true,
|
|
1484
1562
|
resumeOptions: {},
|
|
1485
1563
|
};
|
|
@@ -1651,6 +1729,7 @@ export class BridgeJobExecutor {
|
|
|
1651
1729
|
// so a stall that lands after a clean result still contributes its snapshot.
|
|
1652
1730
|
state.queryModelUsage = new Map();
|
|
1653
1731
|
state.queryUsageBaseline = await this.usageBaselineFor(attempt);
|
|
1732
|
+
await this.alignResumedTranscript(attempt);
|
|
1654
1733
|
const mcpServers = await this.resolveMcpServers(job);
|
|
1655
1734
|
const skills = await readSessionSkills(bridgeRuntimeFiles.projectRoot());
|
|
1656
1735
|
const autoCompactWindow = autoCompactWindowForModel(model);
|
|
@@ -1772,6 +1851,7 @@ export class BridgeJobExecutor {
|
|
|
1772
1851
|
},
|
|
1773
1852
|
...attempt.resumeOptions,
|
|
1774
1853
|
};
|
|
1854
|
+
await markDelivered(attempt, controls);
|
|
1775
1855
|
const query = this.sdk.query({ prompt: attempt.prompt, reminder: attempt.reminder, options });
|
|
1776
1856
|
controls.reloadMcpServers = () => this.reloadMcpServers(job, query);
|
|
1777
1857
|
let queryClosed = false;
|
|
@@ -2077,11 +2157,17 @@ export class BridgeJobExecutor {
|
|
|
2077
2157
|
state.currentTurnTools = [];
|
|
2078
2158
|
state.postedAssistantEvent = true;
|
|
2079
2159
|
await this.relay.assistantEvent(target, msg, state.model);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const outcomes = toolOutcomesInMessage(msg);
|
|
2163
|
+
if (outcomes.length > 0) {
|
|
2164
|
+
await this.relay.toolOutcomes(target, outcomes);
|
|
2080
2165
|
}
|
|
2081
2166
|
}
|
|
2082
2167
|
/**
|
|
2083
2168
|
* A message consumed here and never relayed or counted as the agent's work:
|
|
2084
|
-
* a provider refusal for size,
|
|
2169
|
+
* a provider refusal for size, the CLI's report of a failed model request, or
|
|
2170
|
+
* part of a model refusal.
|
|
2085
2171
|
*/
|
|
2086
2172
|
droppedBeforeRelay(job, state, msg) {
|
|
2087
2173
|
// Drop the size refusal outright. A recovered turn must not leave a raw
|
|
@@ -2103,6 +2189,22 @@ export class BridgeJobExecutor {
|
|
|
2103
2189
|
state.turnText = "";
|
|
2104
2190
|
return true;
|
|
2105
2191
|
}
|
|
2192
|
+
if (isApiErrorMessage(msg)) {
|
|
2193
|
+
// The provider's words, never the product's: a turn that recovers shows
|
|
2194
|
+
// nothing, and one that fails says so once through the error outcome.
|
|
2195
|
+
const text = messageContentBlocks(msg)
|
|
2196
|
+
.map((block) => (typeof block.text === "string" ? block.text : ""))
|
|
2197
|
+
.join(" ");
|
|
2198
|
+
console.error(`[BridgeJob] model request failed for thread ${job.threadId}: ${text}`);
|
|
2199
|
+
this.recordTurnEvent({
|
|
2200
|
+
threadId: job.threadId,
|
|
2201
|
+
turnId: job.turnId,
|
|
2202
|
+
event: "api_error",
|
|
2203
|
+
detail: { error: msg.error, text },
|
|
2204
|
+
});
|
|
2205
|
+
state.turnText = "";
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2106
2208
|
return this.observeModelRefusal(job, state, msg);
|
|
2107
2209
|
}
|
|
2108
2210
|
/**
|
|
@@ -2216,6 +2318,10 @@ export class BridgeJobExecutor {
|
|
|
2216
2318
|
if (state.detachedSession) {
|
|
2217
2319
|
return;
|
|
2218
2320
|
}
|
|
2321
|
+
if (reason === "abort") {
|
|
2322
|
+
await this.keepCancelledTurn(job, controls, state);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2219
2325
|
if (!state.usedBoundary && !state.inProgressSession.sawSessionId) {
|
|
2220
2326
|
return;
|
|
2221
2327
|
}
|
|
@@ -2227,6 +2333,25 @@ export class BridgeJobExecutor {
|
|
|
2227
2333
|
});
|
|
2228
2334
|
await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason });
|
|
2229
2335
|
}
|
|
2336
|
+
// A cancel (Stop, a replacing message, an answered card) is not a broken
|
|
2337
|
+
// session: the next turn resumes what this one completed (`buildCancelBoundary`).
|
|
2338
|
+
async keepCancelledTurn(job, controls, state) {
|
|
2339
|
+
const boundary = buildCancelBoundary({
|
|
2340
|
+
inProgress: state.inProgressSession,
|
|
2341
|
+
resumed: state.usedBoundary,
|
|
2342
|
+
request: state.requestPrompt,
|
|
2343
|
+
nowMs: this.now(),
|
|
2344
|
+
});
|
|
2345
|
+
this.recordTurnEvent({
|
|
2346
|
+
threadId: job.threadId,
|
|
2347
|
+
turnId: job.turnId,
|
|
2348
|
+
event: "cancel_resume_point",
|
|
2349
|
+
detail: { resumeFrom: state.inProgressSession.cancelPoint ? "cancelled_turn" : "previous_turn" },
|
|
2350
|
+
});
|
|
2351
|
+
if (boundary) {
|
|
2352
|
+
await this.sessionStore.save(job.threadId, boundary, { runToken: controls.runToken });
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2230
2355
|
// Parks the turn on a blocking dashboard prompt. An unanswered prompt must
|
|
2231
2356
|
// NOT error the turn: the wait window ending resolves to `unanswered` so the
|
|
2232
2357
|
// caller can deny the tool gracefully — the agent ends its turn cleanly and
|
|
@@ -2641,6 +2766,12 @@ function registerOrAutomateCasting(job, controls, toolName, command) {
|
|
|
2641
2766
|
controls.avatarAwaitingCasting = created;
|
|
2642
2767
|
return null;
|
|
2643
2768
|
}
|
|
2769
|
+
/** The last step before the prompt goes out: a turn stopped before here never carried its note. */
|
|
2770
|
+
async function markDelivered(attempt, controls) {
|
|
2771
|
+
if (attempt.delivered && controls.isActive()) {
|
|
2772
|
+
await attempt.delivered();
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2644
2775
|
/** Baker's notes for the turn, sent as their own block beside the prompt (see `SessionAttempt.reminder`). */
|
|
2645
2776
|
function turnReminder(job, verbatim, mainSync) {
|
|
2646
2777
|
const reminders = [
|