@koda-sl/baker-bridge 0.147.0-dev.ad499429a → 0.148.0-dev.5b9952021
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 -2
- 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 +1 -7
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +7 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +111 -12
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +153 -2
- package/dist/hono/bridge-job.test.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 +81 -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 +24 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +200 -5
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/session-continuity.js +94 -7
- 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/package.json +1 -1
package/dist/hono/bridge-job.js
CHANGED
|
@@ -25,7 +25,8 @@ import { BEDROCK_SERVABLE_CAPABILITIES, buildAgentExtraBody, EXTRA_BODY_ENV, MOD
|
|
|
25
25
|
import { credentialReminder, currentReferenceReminder, kickoffBriefReminder } from "./prompt-context.js";
|
|
26
26
|
import { publishLanguageReminder, publishRunReminder } from "./publish-handoff.js";
|
|
27
27
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
28
|
-
import {
|
|
28
|
+
import { alignServedModels } from "./served-model.js";
|
|
29
|
+
import { applyCurrentSenderAttribution, buildCancelBoundary, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, interruptedResumeAttempt, isCliResumeRefusal, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
|
|
29
30
|
import { readSessionSkills } from "./session-skills.js";
|
|
30
31
|
import { ownQueryUsage, readUsageBaseline } from "./session-usage-baseline.js";
|
|
31
32
|
import { BridgeSlashCommands, leadingSkillCommand, verbatimSlashCommand } from "./slash-commands.js";
|
|
@@ -423,10 +424,20 @@ const CONTEXT_OVERFLOW_MESSAGE = "This chat has grown too long to continue in on
|
|
|
423
424
|
// so the turn ends on this instead. The refused exchange is not saved to the
|
|
424
425
|
// session, so a rephrased message starts from the conversation before it.
|
|
425
426
|
const MODEL_REFUSAL_MESSAGE = "I couldn't help with that request. Try rephrasing what you need.";
|
|
426
|
-
|
|
427
|
+
// What the user reads when the model provider failed the request itself — out
|
|
428
|
+
// of credit, rate-limited, down. The CLI's own text ("API Error: 402 Insufficient
|
|
429
|
+
// credits. Add more using https://openrouter.ai/…") is the provider talking to
|
|
430
|
+
// Baker: a vendor, a status code and a billing page the marketer cannot open.
|
|
431
|
+
// The raw text stays in the bridge log and the turn events.
|
|
432
|
+
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.";
|
|
433
|
+
function userFacingTurnError(error) {
|
|
434
|
+
const message = errorMessage(error);
|
|
427
435
|
if (isContextOverflowError(message)) {
|
|
428
436
|
return CONTEXT_OVERFLOW_MESSAGE;
|
|
429
437
|
}
|
|
438
|
+
if (error instanceof SdkResultError && error.providerFailure) {
|
|
439
|
+
return PROVIDER_FAILURE_MESSAGE;
|
|
440
|
+
}
|
|
430
441
|
return enrichImageError(message);
|
|
431
442
|
}
|
|
432
443
|
function enrichImageError(message) {
|
|
@@ -467,6 +478,9 @@ function subagentModelEnv() {
|
|
|
467
478
|
const override = agentModelOverride();
|
|
468
479
|
return override ? { CLAUDE_CODE_SUBAGENT_MODEL: override, CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "1" } : {};
|
|
469
480
|
}
|
|
481
|
+
function claudeConfigDir() {
|
|
482
|
+
return process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude");
|
|
483
|
+
}
|
|
470
484
|
function claudePlanDir() {
|
|
471
485
|
return resolve(process.env.HOME || homedir(), ".claude", "plans");
|
|
472
486
|
}
|
|
@@ -564,14 +578,35 @@ function sdkResultErrorMessage(msg) {
|
|
|
564
578
|
function turnResultError(state, msg) {
|
|
565
579
|
return state.refusedWithoutFallback ? undefined : sdkResultError(msg);
|
|
566
580
|
}
|
|
581
|
+
class SdkResultError extends Error {
|
|
582
|
+
providerFailure;
|
|
583
|
+
name = "SdkResultError";
|
|
584
|
+
constructor(message,
|
|
585
|
+
/** The model provider refused or failed the request, rather than the turn's own work failing. */
|
|
586
|
+
providerFailure) {
|
|
587
|
+
super(`Claude Code returned an error result: ${message}`);
|
|
588
|
+
this.providerFailure = providerFailure;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// What the request's own content caused (an unreadable image, a malformed or
|
|
592
|
+
// oversized request): sending the same message again would fail the same way, so
|
|
593
|
+
// it keeps its own remediation instead of the provider-failure message.
|
|
594
|
+
const REQUEST_CONTENT_STATUSES = [400, 413, 422];
|
|
595
|
+
function isProviderFailureResult(msg) {
|
|
596
|
+
if (msg.subtype !== "success") {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const status = msg.api_error_status ?? Number(/^API Error: (\d{3})\b/.exec(msg.result)?.[1]);
|
|
600
|
+
if (REQUEST_CONTENT_STATUSES.includes(status)) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
return typeof msg.api_error_status === "number" || msg.result.startsWith("API Error");
|
|
604
|
+
}
|
|
567
605
|
function sdkResultError(msg) {
|
|
568
606
|
if (msg.type !== "result" || !msg.is_error) {
|
|
569
607
|
return undefined;
|
|
570
608
|
}
|
|
571
|
-
|
|
572
|
-
const error = new Error(`Claude Code returned an error result: ${message}`);
|
|
573
|
-
error.name = "SdkResultError";
|
|
574
|
-
return error;
|
|
609
|
+
return new SdkResultError(sdkResultErrorMessage(msg), isProviderFailureResult(msg));
|
|
575
610
|
}
|
|
576
611
|
// When the bridge closes the sandbox CLI's stdin (its permission control
|
|
577
612
|
// channel) — on abort/replace/timeout — while the CLI is still alive (a long
|
|
@@ -685,6 +720,15 @@ export function isContextOverflowRefusal(msg) {
|
|
|
685
720
|
}
|
|
686
721
|
return OVERFLOW_REFUSAL_LEADS.some((lead) => text.startsWith(lead));
|
|
687
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* True when an assistant message is the CLI's own report of a failed model
|
|
725
|
+
* request ("API Error: 402 Insufficient credits…", "API Error: Server error
|
|
726
|
+
* mid-response…"), not anything the model wrote. The CLI writes it for a
|
|
727
|
+
* terminal user: a `<synthetic>` model and the request's `error` class.
|
|
728
|
+
*/
|
|
729
|
+
function isApiErrorMessage(msg) {
|
|
730
|
+
return msg.type === "assistant" && msg.error !== undefined && msg.message.model === "<synthetic>";
|
|
731
|
+
}
|
|
688
732
|
/** Content blocks of an assistant/user SDK message; [] for other shapes. */
|
|
689
733
|
function messageContentBlocks(msg) {
|
|
690
734
|
const content = msg.message?.content;
|
|
@@ -1060,6 +1104,7 @@ export class BridgeJobExecutor {
|
|
|
1060
1104
|
turnEndExitGuardMs;
|
|
1061
1105
|
now;
|
|
1062
1106
|
readUsageBaseline;
|
|
1107
|
+
alignServedModels;
|
|
1063
1108
|
stalledCliExitMs;
|
|
1064
1109
|
readFlowTree;
|
|
1065
1110
|
settleWorkspace;
|
|
@@ -1081,8 +1126,8 @@ export class BridgeJobExecutor {
|
|
|
1081
1126
|
this.turnEndExitGuardMs = dependencies.turnEndExitGuardMs ?? DEFAULT_TURN_END_EXIT_GUARD_MS;
|
|
1082
1127
|
this.now = dependencies.now ?? Date.now;
|
|
1083
1128
|
this.readUsageBaseline =
|
|
1084
|
-
dependencies.readUsageBaseline ??
|
|
1085
|
-
|
|
1129
|
+
dependencies.readUsageBaseline ?? ((sessionId) => readUsageBaseline(sessionId, claudeConfigDir()));
|
|
1130
|
+
this.alignServedModels = dependencies.alignServedModels;
|
|
1086
1131
|
this.stalledCliExitMs = dependencies.stalledCliExitMs ?? STALLED_CLI_EXIT_MS;
|
|
1087
1132
|
this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
|
|
1088
1133
|
this.readFlowTree =
|
|
@@ -1123,6 +1168,7 @@ export class BridgeJobExecutor {
|
|
|
1123
1168
|
});
|
|
1124
1169
|
attempt = (await this.interruptedTurnAttempt(job, replayAttempt)) ?? replayAttempt;
|
|
1125
1170
|
state.usedBoundary = attempt.usedBoundary;
|
|
1171
|
+
state.requestPrompt = request.prompt;
|
|
1126
1172
|
state.detachedSession = attempt.detachedSession === true;
|
|
1127
1173
|
result.attemptedRehydration = attempt.attemptedRehydration;
|
|
1128
1174
|
this.recordAttemptContextSource(job, attempt);
|
|
@@ -1133,6 +1179,7 @@ export class BridgeJobExecutor {
|
|
|
1133
1179
|
const repaired = await this.fallbackAttempt(job, controls, { request, attempt, replayAttempt }, state, error);
|
|
1134
1180
|
if (repaired && controls.isActive()) {
|
|
1135
1181
|
const repairState = createBridgeJobState();
|
|
1182
|
+
repairState.detachedSession = repaired.detachedSession === true;
|
|
1136
1183
|
let repairResult = { costUsd: 0, attemptedRehydration: repaired.attemptedRehydration };
|
|
1137
1184
|
try {
|
|
1138
1185
|
repairResult = await this.runAttempt(job, controls, repairState, repaired);
|
|
@@ -1173,7 +1220,7 @@ export class BridgeJobExecutor {
|
|
|
1173
1220
|
async interruptedResumeFallbackAttempt(job, replay, state, error) {
|
|
1174
1221
|
if (!replay ||
|
|
1175
1222
|
state.sawUserVisibleSdkMessage ||
|
|
1176
|
-
(error instanceof Error && error.name === "SdkResultError") ||
|
|
1223
|
+
(error instanceof Error && error.name === "SdkResultError" && !isCliResumeRefusal(error)) ||
|
|
1177
1224
|
!isResumeSessionError(error)) {
|
|
1178
1225
|
return null;
|
|
1179
1226
|
}
|
|
@@ -1368,18 +1415,29 @@ export class BridgeJobExecutor {
|
|
|
1368
1415
|
if (!controls.isActive()) {
|
|
1369
1416
|
return { kind: "cancelled", costUsd: result.costUsd, model, modelUsage };
|
|
1370
1417
|
}
|
|
1418
|
+
const shown = userFacingTurnError(error);
|
|
1419
|
+
if (shown !== errorMessage(error)) {
|
|
1420
|
+
console.error(`[BridgeJob] turn failed for thread ${job.threadId}: ${errorMessage(error)}`);
|
|
1421
|
+
}
|
|
1371
1422
|
return {
|
|
1372
1423
|
kind: "errored",
|
|
1373
1424
|
costUsd: result.costUsd,
|
|
1374
1425
|
model,
|
|
1375
1426
|
modelUsage,
|
|
1376
|
-
errors: [
|
|
1427
|
+
errors: [shown],
|
|
1377
1428
|
};
|
|
1378
1429
|
}
|
|
1379
1430
|
usageBaselineFor(attempt) {
|
|
1380
1431
|
const resumed = attempt.resumeOptions.resume;
|
|
1381
1432
|
return resumed ? this.readUsageBaseline(resumed) : Promise.resolve(undefined);
|
|
1382
1433
|
}
|
|
1434
|
+
alignResumedTranscript(attempt) {
|
|
1435
|
+
const resumed = attempt.resumeOptions.resume;
|
|
1436
|
+
if (!resumed) {
|
|
1437
|
+
return Promise.resolve();
|
|
1438
|
+
}
|
|
1439
|
+
return this.alignServedModels ? this.alignServedModels(resumed) : alignServedModels(resumed, claudeConfigDir());
|
|
1440
|
+
}
|
|
1383
1441
|
async awaitReplacedCliExit(next) {
|
|
1384
1442
|
if (next.resumeOptions.resume && this.stalledCliExitMs > 0) {
|
|
1385
1443
|
await new Promise((resolve) => setTimeout(resolve, this.stalledCliExitMs));
|
|
@@ -1445,7 +1503,7 @@ export class BridgeJobExecutor {
|
|
|
1445
1503
|
async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
|
|
1446
1504
|
if (!attempt.usedBoundary ||
|
|
1447
1505
|
state.sawUserVisibleSdkMessage ||
|
|
1448
|
-
(error instanceof Error && error.name === "SdkResultError") ||
|
|
1506
|
+
(error instanceof Error && error.name === "SdkResultError" && !isCliResumeRefusal(error)) ||
|
|
1449
1507
|
!isResumeSessionError(error)) {
|
|
1450
1508
|
return null;
|
|
1451
1509
|
}
|
|
@@ -1636,6 +1694,7 @@ export class BridgeJobExecutor {
|
|
|
1636
1694
|
// so a stall that lands after a clean result still contributes its snapshot.
|
|
1637
1695
|
state.queryModelUsage = new Map();
|
|
1638
1696
|
state.queryUsageBaseline = await this.usageBaselineFor(attempt);
|
|
1697
|
+
await this.alignResumedTranscript(attempt);
|
|
1639
1698
|
const mcpServers = await this.resolveMcpServers(job);
|
|
1640
1699
|
const skills = await readSessionSkills(bridgeRuntimeFiles.projectRoot());
|
|
1641
1700
|
const autoCompactWindow = autoCompactWindowForModel(model);
|
|
@@ -2066,7 +2125,8 @@ export class BridgeJobExecutor {
|
|
|
2066
2125
|
}
|
|
2067
2126
|
/**
|
|
2068
2127
|
* A message consumed here and never relayed or counted as the agent's work:
|
|
2069
|
-
* a provider refusal for size,
|
|
2128
|
+
* a provider refusal for size, the CLI's report of a failed model request, or
|
|
2129
|
+
* part of a model refusal.
|
|
2070
2130
|
*/
|
|
2071
2131
|
droppedBeforeRelay(job, state, msg) {
|
|
2072
2132
|
// Drop the size refusal outright. A recovered turn must not leave a raw
|
|
@@ -2088,6 +2148,22 @@ export class BridgeJobExecutor {
|
|
|
2088
2148
|
state.turnText = "";
|
|
2089
2149
|
return true;
|
|
2090
2150
|
}
|
|
2151
|
+
if (isApiErrorMessage(msg)) {
|
|
2152
|
+
// The provider's words, never the product's: a turn that recovers shows
|
|
2153
|
+
// nothing, and one that fails says so once through the error outcome.
|
|
2154
|
+
const text = messageContentBlocks(msg)
|
|
2155
|
+
.map((block) => (typeof block.text === "string" ? block.text : ""))
|
|
2156
|
+
.join(" ");
|
|
2157
|
+
console.error(`[BridgeJob] model request failed for thread ${job.threadId}: ${text}`);
|
|
2158
|
+
this.recordTurnEvent({
|
|
2159
|
+
threadId: job.threadId,
|
|
2160
|
+
turnId: job.turnId,
|
|
2161
|
+
event: "api_error",
|
|
2162
|
+
detail: { error: msg.error, text },
|
|
2163
|
+
});
|
|
2164
|
+
state.turnText = "";
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2091
2167
|
return this.observeModelRefusal(job, state, msg);
|
|
2092
2168
|
}
|
|
2093
2169
|
/**
|
|
@@ -2201,6 +2277,10 @@ export class BridgeJobExecutor {
|
|
|
2201
2277
|
if (state.detachedSession) {
|
|
2202
2278
|
return;
|
|
2203
2279
|
}
|
|
2280
|
+
if (reason === "abort") {
|
|
2281
|
+
await this.keepCancelledTurn(job, controls, state);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2204
2284
|
if (!state.usedBoundary && !state.inProgressSession.sawSessionId) {
|
|
2205
2285
|
return;
|
|
2206
2286
|
}
|
|
@@ -2212,6 +2292,25 @@ export class BridgeJobExecutor {
|
|
|
2212
2292
|
});
|
|
2213
2293
|
await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason });
|
|
2214
2294
|
}
|
|
2295
|
+
// A cancel (Stop, a replacing message, an answered card) is not a broken
|
|
2296
|
+
// session: the next turn resumes what this one completed (`buildCancelBoundary`).
|
|
2297
|
+
async keepCancelledTurn(job, controls, state) {
|
|
2298
|
+
const boundary = buildCancelBoundary({
|
|
2299
|
+
inProgress: state.inProgressSession,
|
|
2300
|
+
resumed: state.usedBoundary,
|
|
2301
|
+
request: state.requestPrompt,
|
|
2302
|
+
nowMs: this.now(),
|
|
2303
|
+
});
|
|
2304
|
+
this.recordTurnEvent({
|
|
2305
|
+
threadId: job.threadId,
|
|
2306
|
+
turnId: job.turnId,
|
|
2307
|
+
event: "cancel_resume_point",
|
|
2308
|
+
detail: { resumeFrom: state.inProgressSession.cancelPoint ? "cancelled_turn" : "previous_turn" },
|
|
2309
|
+
});
|
|
2310
|
+
if (boundary) {
|
|
2311
|
+
await this.sessionStore.save(job.threadId, boundary, { runToken: controls.runToken });
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2215
2314
|
// Parks the turn on a blocking dashboard prompt. An unanswered prompt must
|
|
2216
2315
|
// NOT error the turn: the wait window ending resolves to `unanswered` so the
|
|
2217
2316
|
// caller can deny the tool gracefully — the agent ends its turn cleanly and
|