@koda-sl/baker-bridge 0.71.2-dev.5e66c2900 → 0.72.0-dev.3fc14023b
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 +42 -33
- package/dist/hono/bridge-job.d.ts +15 -32
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +78 -84
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +67 -78
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/convex.d.ts +1 -1
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/relay.d.ts +1 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/session-continuity.d.ts +14 -15
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +43 -21
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/session-continuity.test.js +22 -25
- package/dist/hono/session-continuity.test.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,7 +319,11 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
319
319
|
messages count). The `window` is the model's **real OpenRouter context
|
|
320
320
|
length**, resolved from the model id (`contextWindowForModel` in
|
|
321
321
|
`model-capabilities.ts`): opus 5, opus 4.6, sonnet 4.6 and sonnet 5 are 1M, haiku 4.5
|
|
322
|
-
is 200k
|
|
322
|
+
is 200k — and resolved from **the model that actually served the request**,
|
|
323
|
+
which is not always the session's. A skill can pin its own model for the turn
|
|
324
|
+
(publish pins Haiku 4.5), and measuring such a turn against the session's 1M
|
|
325
|
+
reads a third full when it is in fact over — the mismatch that hid the publish
|
|
326
|
+
failure. We resolve it ourselves because the runtime reaches these models through
|
|
323
327
|
the OpenRouter passthrough (`ANTHROPIC_BASE_URL=openrouter.ai`) while the CLI
|
|
324
328
|
resolves the window from its own bundled catalog, which is keyed to the
|
|
325
329
|
first-party API. There they disagree: the catalog lists opus 4.6 and sonnet 4.6
|
|
@@ -383,35 +387,40 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
383
387
|
would push the command off the front and it would land as ordinary message
|
|
384
388
|
text. `/compact` resumes the live session and asks the SDK to summarize older
|
|
385
389
|
turns.
|
|
386
|
-
- **Publish
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
- **
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
390
|
+
- **Publish runs detached from the chat session.** `/publish` neither resumes the
|
|
391
|
+
chat's session nor replaces it on the way out (`detachedSession` on the
|
|
392
|
+
attempt): it starts empty, every time, whatever the conversation is doing.
|
|
393
|
+
- **Why.** The publish skill pins its own model — Haiku 4.5, a genuine 200k
|
|
394
|
+
window — while the chat runs on a 1M one. Resuming the chat's session
|
|
395
|
+
therefore handed a conversation sized for 1M to a model with a fifth of the
|
|
396
|
+
room. Measured on a failure in production: the last chat request was 356,707
|
|
397
|
+
tokens (36% of its own window, 78% *over* the publish model's), and the
|
|
398
|
+
provider refused the publish pre-flight while the chat's own meter showed
|
|
399
|
+
plenty of space. Summarizing could not rescue it either — the summary request
|
|
400
|
+
has to send the same conversation. Detached publishes measured ~51k against
|
|
401
|
+
the same 200k window.
|
|
402
|
+
- **It costs nothing**, because publish does not read the conversation: the
|
|
403
|
+
skill says so outright ("Do not reference or act on prior conversation
|
|
404
|
+
context"), and it works from the state of the workspace — scripts, git, and
|
|
405
|
+
the files verify complains about.
|
|
406
|
+
- **The chat's session is left intact**, on success *and* on failure. Handing
|
|
407
|
+
publish's throwaway session back as the thread's boundary would make the next
|
|
408
|
+
message resume a transcript holding one `/publish` and nothing else, and
|
|
409
|
+
clearing the boundary on a failed publish would charge the user their
|
|
410
|
+
conversation for a publish that did not happen.
|
|
411
|
+
- **Publish still never summarizes as a precaution.** Auto-compact fires on a
|
|
412
|
+
*threshold*, not on a real overflow, so a long chat once paid for a summary
|
|
413
|
+
before anything happened — the user watched "Summarizing this chat to free up
|
|
414
|
+
space…" instead of a publish starting. A `/publish` turn runs with
|
|
415
|
+
`settings.autoCompactEnabled: false`, because it still runs a build and a
|
|
416
|
+
verify inside its own session and that output alone can cross the threshold.
|
|
417
|
+
If the turn's *own* work then overflows (`isContextOverflowError` — Anthropic's
|
|
418
|
+
`prompt is too long`, OpenRouter's `maximum context length`), it retries once
|
|
419
|
+
with compaction allowed, resuming the in-flight session (forked) so the steps
|
|
420
|
+
already completed are not repeated. Gated on work having been produced
|
|
421
|
+
(`resumeSessionAt`), which is the only state where a summary has anything to
|
|
422
|
+
shrink — before that, the turn stops rather than re-sending a request the
|
|
423
|
+
provider just refused.
|
|
415
424
|
- **The refusal itself is never shown.** The CLI renders a refused request as an
|
|
416
425
|
assistant message *before* the query throws, and relayed text is never
|
|
417
426
|
retracted — so recovery used to leave a raw `Prompt is too long` in the chat,
|
|
@@ -425,9 +434,9 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
425
434
|
changes the leftover cannot come back as the turn's end-of-turn post. Dropping
|
|
426
435
|
it before the session observer also stops it from counting as
|
|
427
436
|
work done, which would otherwise make the retry resume with "continue where you
|
|
428
|
-
left off" instead of delivering the `/publish` the turn still owes. A turn
|
|
429
|
-
|
|
430
|
-
|
|
437
|
+
left off" instead of delivering the `/publish` the turn still owes. A turn that
|
|
438
|
+
cannot recover reports the failure once, in product language — the provider's
|
|
439
|
+
wording never reaches the chat.
|
|
431
440
|
- **Compaction progress + confirmation.** A compaction summarizes the session to
|
|
432
441
|
free context. A manual `/compact` turn emits no assistant text, so it would
|
|
433
442
|
otherwise complete with nothing visible; an automatic compaction happens
|
|
@@ -133,7 +133,7 @@ export type BridgeJobRelay = {
|
|
|
133
133
|
heartbeat(target: CallbackTarget): Promise<void>;
|
|
134
134
|
compaction(target: CallbackTarget, state: "compacting" | "done"): Promise<void>;
|
|
135
135
|
contextUsage(target: CallbackTarget, usage: ContextUsage): Promise<void>;
|
|
136
|
-
contextSource(target: CallbackTarget, source: "local_resume" | "rehydrated_missing_session" | "rehydrated_invalid_resume" | "rehydrated_repaired_tool_state" | "
|
|
136
|
+
contextSource(target: CallbackTarget, source: "local_resume" | "rehydrated_missing_session" | "rehydrated_invalid_resume" | "rehydrated_repaired_tool_state" | "detached_publish" | "rehydration_failed", metadata: Record<string, unknown>): Promise<void>;
|
|
137
137
|
complete(target: CallbackTarget, outcome: BridgeJobOutcome): Promise<void>;
|
|
138
138
|
};
|
|
139
139
|
export type AgentSdkQuery = AsyncIterable<SDKMessage> & {
|
|
@@ -239,37 +239,6 @@ export declare class BridgeJobExecutor {
|
|
|
239
239
|
private loadTrustedBoundary;
|
|
240
240
|
private prepareRequestPrompt;
|
|
241
241
|
private runAttempt;
|
|
242
|
-
/**
|
|
243
|
-
* The next attempt for a request the provider refused for size, or null when
|
|
244
|
-
* the turn has no move left.
|
|
245
|
-
*
|
|
246
|
-
* Restarting on a fresh session is the FIRST choice, not the last. Once the
|
|
247
|
-
* conversation itself is past the window, summarizing it cannot rescue the
|
|
248
|
-
* turn: the summary request has to send that same conversation and is refused
|
|
249
|
-
* the same way. Production bore this out — the compaction attempt died 0.9s
|
|
250
|
-
* after it started, leaving a spinner that never resolved and a raw provider
|
|
251
|
-
* error above a failed publish, while the user's own fix (send `/publish`
|
|
252
|
-
* again, which lands on a fresh session because the failed turn cleared the
|
|
253
|
-
* dirty one) worked every time. This does that fix automatically.
|
|
254
|
-
*
|
|
255
|
-
* Abandoning the session is free *for publish specifically*: the refusal lands
|
|
256
|
-
* pre-flight, before the agent has done anything, and `/publish` re-enters a
|
|
257
|
-
* fresh session as the bare command it already is — it publishes from the state
|
|
258
|
-
* of the workspace, not from the conversation, so nothing it needs was in the
|
|
259
|
-
* session that got dropped. An ordinary message is answered *from* that
|
|
260
|
-
* context, so restarting it would quietly swap a live session for a short
|
|
261
|
-
* rehydrated history and give a worse answer with no explanation. That turn is
|
|
262
|
-
* left to fail and say so. It loses nothing by failing: a failed turn clears
|
|
263
|
-
* the dirty session anyway (`clearIfDirty`), so resending lands on exactly the
|
|
264
|
-
* rehydrated context the restart would have used — the user just gets told.
|
|
265
|
-
*
|
|
266
|
-
* Compaction is kept for the one case where a restart WOULD cost something —
|
|
267
|
-
* the agent already produced work this turn, so a fresh session would make it
|
|
268
|
-
* redo the steps it just took. `resumeSessionAt` is that test: it is set only
|
|
269
|
-
* by an assistant message, whereas `sessionId` arrives with the CLI's `init`,
|
|
270
|
-
* before the first model request.
|
|
271
|
-
*/
|
|
272
|
-
private contextOverflowRecovery;
|
|
273
242
|
private finishSuccessfulAttempt;
|
|
274
243
|
private errorOutcome;
|
|
275
244
|
private resultWithObservedCost;
|
|
@@ -287,6 +256,20 @@ export declare class BridgeJobExecutor {
|
|
|
287
256
|
private drainQuery;
|
|
288
257
|
private createStallDetector;
|
|
289
258
|
private consumeSdkMessage;
|
|
259
|
+
/**
|
|
260
|
+
* Surface how full the conversation's context is, at turn end.
|
|
261
|
+
*
|
|
262
|
+
* Tokens come from the last main-conversation request's own usage — the real
|
|
263
|
+
* occupancy of the window at that request. We deliberately do NOT use the
|
|
264
|
+
* result's per-model token totals: those aggregate every API round-trip in the
|
|
265
|
+
* turn (one per tool call, plus any same-model subagent work), so a handful of
|
|
266
|
+
* tool calls inflates them past the window and pins the meter at 100% on an
|
|
267
|
+
* almost-empty chat. The window is the model's real OpenRouter context length
|
|
268
|
+
* (opus/sonnet 4.6 = 1M); we resolve it from the model id because the runtime
|
|
269
|
+
* under-reports it through the OpenRouter passthrough, falling back to the
|
|
270
|
+
* reported window for unknown models. A relay failure must never fail the turn.
|
|
271
|
+
*/
|
|
272
|
+
private publishContextUsage;
|
|
290
273
|
private handleMessage;
|
|
291
274
|
private handleSystemMessage;
|
|
292
275
|
private handleContentBlockStart;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-job.d.ts","sourceRoot":"","sources":["../../src/hono/bridge-job.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bridge-job.d.ts","sourceRoot":"","sources":["../../src/hono/bridge-job.ts"],"names":[],"mappings":"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"}
|
package/dist/hono/bridge-job.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appendCurrentReferenceContext } from "./prompt-context.js";
|
|
|
12
12
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
13
13
|
import { applyCurrentSenderAttribution, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
|
|
14
14
|
import { readSessionSkills } from "./session-skills.js";
|
|
15
|
-
import {
|
|
15
|
+
import { verbatimSlashCommand } from "./slash-commands.js";
|
|
16
16
|
import { createBakerUiServer, normalizeTagInputToolInput, sanitizeTagChanges, TAG_INPUT_TOOL_NAME, } from "./tag-input.js";
|
|
17
17
|
import { addQueryModelUsage, queryModelUsage, turnModelUsageList } from "./turn-model-usage.js";
|
|
18
18
|
export function parsePlanDecision(value) {
|
|
@@ -126,7 +126,7 @@ function createBridgeJobState() {
|
|
|
126
126
|
inProgressSession: emptyInProgressSession(),
|
|
127
127
|
observedCostUsd: 0,
|
|
128
128
|
controlChannelDenies: 0,
|
|
129
|
-
|
|
129
|
+
detachedSession: false,
|
|
130
130
|
modelUsageTotals: new Map(),
|
|
131
131
|
queryModelUsage: new Map(),
|
|
132
132
|
};
|
|
@@ -615,17 +615,22 @@ function trackMainRequestUsage(state, msg) {
|
|
|
615
615
|
if (msg.parent_tool_use_id) {
|
|
616
616
|
return;
|
|
617
617
|
}
|
|
618
|
-
|
|
618
|
+
const event = msg.event;
|
|
619
|
+
recordMainRequestUsage(state, streamEventContextUsage(event), event?.message?.model);
|
|
619
620
|
return;
|
|
620
621
|
}
|
|
621
622
|
if (msg.type !== "assistant" || msg.parent_tool_use_id || msg.subagent_type) {
|
|
622
623
|
return;
|
|
623
624
|
}
|
|
624
|
-
|
|
625
|
+
const message = msg.message;
|
|
626
|
+
recordMainRequestUsage(state, message.usage, message.model);
|
|
625
627
|
}
|
|
626
|
-
function recordMainRequestUsage(state, usage) {
|
|
628
|
+
function recordMainRequestUsage(state, usage, model) {
|
|
627
629
|
if (usage && messageContextTokens(usage) > 0) {
|
|
628
630
|
state.lastMainRequestUsage = usage;
|
|
631
|
+
if (typeof model === "string" && model.length > 0) {
|
|
632
|
+
state.lastMainRequestModel = model;
|
|
633
|
+
}
|
|
629
634
|
}
|
|
630
635
|
}
|
|
631
636
|
function classifyDirtyReason(error, controls) {
|
|
@@ -677,6 +682,7 @@ export class BridgeJobExecutor {
|
|
|
677
682
|
const loaded = await this.loadTrustedBoundary(job, controls);
|
|
678
683
|
attempt = prepareSessionAttempt({ requestPrompt, history: job.history, loaded });
|
|
679
684
|
state.usedBoundary = attempt.usedBoundary;
|
|
685
|
+
state.detachedSession = attempt.detachedSession === true;
|
|
680
686
|
result.attemptedRehydration = attempt.attemptedRehydration;
|
|
681
687
|
this.recordAttemptContextSource(job, attempt);
|
|
682
688
|
result = await this.runAttempt(job, controls, state, attempt);
|
|
@@ -739,12 +745,21 @@ export class BridgeJobExecutor {
|
|
|
739
745
|
break;
|
|
740
746
|
}
|
|
741
747
|
catch (error) {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
+
// The publish turn's OWN work no longer fits. Not the conversation —
|
|
749
|
+
// publish never carries it (`prepareSessionAttempt` runs it detached) —
|
|
750
|
+
// but a build plus a verify inside one session, which is the only thing
|
|
751
|
+
// left that a summary can actually shrink. Gated on work having been
|
|
752
|
+
// produced, because that is what makes the session worth keeping;
|
|
753
|
+
// summarizing an empty one could not free a single token. Once per turn:
|
|
754
|
+
// the retry runs with compaction on, so a second overflow is a real
|
|
755
|
+
// failure rather than something to loop on.
|
|
756
|
+
if (isContextOverflowError(error) &&
|
|
757
|
+
controls.isActive() &&
|
|
758
|
+
current.autoCompact === false &&
|
|
759
|
+
state.inProgressSession.resumeSessionAt) {
|
|
760
|
+
console.error(`[BridgeJob] publish outgrew the context window mid-turn — compacting for ${job.threadId}`);
|
|
761
|
+
current = compactionRetryAttempt(current, state.inProgressSession);
|
|
762
|
+
continue;
|
|
748
763
|
}
|
|
749
764
|
if (!(error instanceof StreamStallError) || stallRetries >= MAX_STALL_RETRIES || !controls.isActive()) {
|
|
750
765
|
throw error;
|
|
@@ -756,62 +771,6 @@ export class BridgeJobExecutor {
|
|
|
756
771
|
}
|
|
757
772
|
return { costUsd, attemptedRehydration: attempt.attemptedRehydration };
|
|
758
773
|
}
|
|
759
|
-
/**
|
|
760
|
-
* The next attempt for a request the provider refused for size, or null when
|
|
761
|
-
* the turn has no move left.
|
|
762
|
-
*
|
|
763
|
-
* Restarting on a fresh session is the FIRST choice, not the last. Once the
|
|
764
|
-
* conversation itself is past the window, summarizing it cannot rescue the
|
|
765
|
-
* turn: the summary request has to send that same conversation and is refused
|
|
766
|
-
* the same way. Production bore this out — the compaction attempt died 0.9s
|
|
767
|
-
* after it started, leaving a spinner that never resolved and a raw provider
|
|
768
|
-
* error above a failed publish, while the user's own fix (send `/publish`
|
|
769
|
-
* again, which lands on a fresh session because the failed turn cleared the
|
|
770
|
-
* dirty one) worked every time. This does that fix automatically.
|
|
771
|
-
*
|
|
772
|
-
* Abandoning the session is free *for publish specifically*: the refusal lands
|
|
773
|
-
* pre-flight, before the agent has done anything, and `/publish` re-enters a
|
|
774
|
-
* fresh session as the bare command it already is — it publishes from the state
|
|
775
|
-
* of the workspace, not from the conversation, so nothing it needs was in the
|
|
776
|
-
* session that got dropped. An ordinary message is answered *from* that
|
|
777
|
-
* context, so restarting it would quietly swap a live session for a short
|
|
778
|
-
* rehydrated history and give a worse answer with no explanation. That turn is
|
|
779
|
-
* left to fail and say so. It loses nothing by failing: a failed turn clears
|
|
780
|
-
* the dirty session anyway (`clearIfDirty`), so resending lands on exactly the
|
|
781
|
-
* rehydrated context the restart would have used — the user just gets told.
|
|
782
|
-
*
|
|
783
|
-
* Compaction is kept for the one case where a restart WOULD cost something —
|
|
784
|
-
* the agent already produced work this turn, so a fresh session would make it
|
|
785
|
-
* redo the steps it just took. `resumeSessionAt` is that test: it is set only
|
|
786
|
-
* by an assistant message, whereas `sessionId` arrives with the CLI's `init`,
|
|
787
|
-
* before the first model request.
|
|
788
|
-
*/
|
|
789
|
-
async contextOverflowRecovery(job, controls, state, original, current) {
|
|
790
|
-
const producedWork = state.inProgressSession.resumeSessionAt !== undefined;
|
|
791
|
-
if (isPublishCommandPrompt(job.prompt) &&
|
|
792
|
-
!producedWork &&
|
|
793
|
-
original.usedBoundary &&
|
|
794
|
-
!state.restartedOnFreshSession) {
|
|
795
|
-
state.restartedOnFreshSession = true;
|
|
796
|
-
console.error(`[BridgeJob] conversation no longer fits the context window — restarting thread ${job.threadId}`);
|
|
797
|
-
await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason: "sdk_error" });
|
|
798
|
-
void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydrated_context_overflow", {
|
|
799
|
-
historyEntryCount: job.history?.entries.length ?? 0,
|
|
800
|
-
omittedEntryCount: job.history?.omittedEntryCount ?? 0,
|
|
801
|
-
historyTruncated: job.history?.truncated ?? false,
|
|
802
|
-
reason: "context_overflow",
|
|
803
|
-
});
|
|
804
|
-
return prepareSessionAttempt({ requestPrompt: original.prompt, history: job.history });
|
|
805
|
-
}
|
|
806
|
-
// Work to preserve (or a restart already spent): summarizing is the only
|
|
807
|
-
// move left. Once per turn — the retry runs with compaction on, so a second
|
|
808
|
-
// overflow is a real failure rather than something to loop on.
|
|
809
|
-
if (current.autoCompact === false) {
|
|
810
|
-
console.error(`[BridgeJob] publish no longer fits the context window — compacting for thread ${job.threadId}`);
|
|
811
|
-
return compactionRetryAttempt(current, state.inProgressSession);
|
|
812
|
-
}
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
774
|
async finishSuccessfulAttempt(job, controls, state, costUsd) {
|
|
816
775
|
const modelUsage = turnModelUsageList(state.modelUsageTotals);
|
|
817
776
|
if (!controls.isActive()) {
|
|
@@ -825,7 +784,13 @@ export class BridgeJobExecutor {
|
|
|
825
784
|
await this.clearIfDirty(job, controls, state, "abort");
|
|
826
785
|
return { kind: "cancelled", costUsd, model: state.model, modelUsage };
|
|
827
786
|
}
|
|
828
|
-
|
|
787
|
+
// A detached turn (publish) ran in a session of its own. Handing that session
|
|
788
|
+
// back as the thread's boundary would make the next chat message resume a
|
|
789
|
+
// transcript containing one `/publish` and nothing else — the conversation,
|
|
790
|
+
// still perfectly good, replaced by the turn that was told not to read it.
|
|
791
|
+
const cleanBoundary = state.detachedSession
|
|
792
|
+
? undefined
|
|
793
|
+
: buildCleanBoundary({ inProgress: state.inProgressSession, nowMs: this.now() });
|
|
829
794
|
return { kind: "completed", costUsd, model: state.model, modelUsage, cleanBoundary };
|
|
830
795
|
}
|
|
831
796
|
errorOutcome(job, controls, state, result, error) {
|
|
@@ -854,6 +819,12 @@ export class BridgeJobExecutor {
|
|
|
854
819
|
return { ...result, costUsd: Math.max(result.costUsd, state.observedCostUsd) };
|
|
855
820
|
}
|
|
856
821
|
recordAttemptContextSource(job, attempt) {
|
|
822
|
+
if (attempt.contextSource === "detached_publish") {
|
|
823
|
+
void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "detached_publish", {
|
|
824
|
+
historyEntryCount: job.history?.entries.length ?? 0,
|
|
825
|
+
});
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
857
828
|
if (attempt.contextSource === "local_resume") {
|
|
858
829
|
void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "local_resume", {
|
|
859
830
|
historyEntryCount: job.history?.entries.length ?? 0,
|
|
@@ -1234,22 +1205,7 @@ export class BridgeJobExecutor {
|
|
|
1234
1205
|
if (snapshot.size > 0) {
|
|
1235
1206
|
state.queryModelUsage = snapshot;
|
|
1236
1207
|
}
|
|
1237
|
-
|
|
1238
|
-
// main-conversation request's own usage — the real occupancy of the window
|
|
1239
|
-
// at that request. We deliberately do NOT use the result's per-model token
|
|
1240
|
-
// totals: those aggregate every API round-trip in the turn (one per tool
|
|
1241
|
-
// call, plus any same-model subagent work), so a handful of tool calls
|
|
1242
|
-
// inflates them past the window and pins the meter at 100% on an almost-
|
|
1243
|
-
// empty chat. The window is the model's real OpenRouter context length
|
|
1244
|
-
// (opus/sonnet 4.6 = 1M); we resolve it from the model id because the
|
|
1245
|
-
// runtime under-reports it through the OpenRouter passthrough, falling back
|
|
1246
|
-
// to the reported window for unknown models. A relay failure must never
|
|
1247
|
-
// fail the turn.
|
|
1248
|
-
const window = contextWindowForModel(state.model) ?? pickContextWindow(msg.modelUsage);
|
|
1249
|
-
const contextUsage = turnContextUsage(state.lastMainRequestUsage, window);
|
|
1250
|
-
if (contextUsage) {
|
|
1251
|
-
await this.relay.contextUsage({ threadId: job.threadId, turnId: job.turnId }, contextUsage);
|
|
1252
|
-
}
|
|
1208
|
+
await this.publishContextUsage(job, state, msg.modelUsage);
|
|
1253
1209
|
const resultError = sdkResultError(msg);
|
|
1254
1210
|
if (resultError) {
|
|
1255
1211
|
await this.clearIfDirty(job, controls, state, "result_error");
|
|
@@ -1263,6 +1219,38 @@ export class BridgeJobExecutor {
|
|
|
1263
1219
|
await this.handleMessage(job, state, msg);
|
|
1264
1220
|
return costUsd;
|
|
1265
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Surface how full the conversation's context is, at turn end.
|
|
1224
|
+
*
|
|
1225
|
+
* Tokens come from the last main-conversation request's own usage — the real
|
|
1226
|
+
* occupancy of the window at that request. We deliberately do NOT use the
|
|
1227
|
+
* result's per-model token totals: those aggregate every API round-trip in the
|
|
1228
|
+
* turn (one per tool call, plus any same-model subagent work), so a handful of
|
|
1229
|
+
* tool calls inflates them past the window and pins the meter at 100% on an
|
|
1230
|
+
* almost-empty chat. The window is the model's real OpenRouter context length
|
|
1231
|
+
* (opus/sonnet 4.6 = 1M); we resolve it from the model id because the runtime
|
|
1232
|
+
* under-reports it through the OpenRouter passthrough, falling back to the
|
|
1233
|
+
* reported window for unknown models. A relay failure must never fail the turn.
|
|
1234
|
+
*/
|
|
1235
|
+
async publishContextUsage(job, state, modelUsage) {
|
|
1236
|
+
const window = contextWindowForModel(state.lastMainRequestModel) ??
|
|
1237
|
+
contextWindowForModel(state.model) ??
|
|
1238
|
+
pickContextWindow(modelUsage);
|
|
1239
|
+
const contextUsage = turnContextUsage(state.lastMainRequestUsage, window);
|
|
1240
|
+
if (contextUsage) {
|
|
1241
|
+
await this.relay.contextUsage({ threadId: job.threadId, turnId: job.turnId }, contextUsage);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (state.postedAssistantEvent) {
|
|
1245
|
+
// A turn that produced assistant output but no occupancy reading means every
|
|
1246
|
+
// source we know of came back empty. That is how the meter stayed dead
|
|
1247
|
+
// without anyone noticing: silence looked exactly like a quiet chat. The
|
|
1248
|
+
// cause is a shape mismatch upstream, not something a test can catch, so it
|
|
1249
|
+
// has to be visible from the outside — grep the sandbox logs for this line
|
|
1250
|
+
// before concluding the meter "just doesn't show".
|
|
1251
|
+
console.error(`[BridgeJob] no context occupancy reported this turn for thread ${job.threadId}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1266
1254
|
async handleMessage(job, state, msg) {
|
|
1267
1255
|
const target = { threadId: job.threadId, turnId: job.turnId };
|
|
1268
1256
|
if (msg.type === "system") {
|
|
@@ -1353,6 +1341,12 @@ export class BridgeJobExecutor {
|
|
|
1353
1341
|
await this.relay.assistantEvent({ threadId: job.threadId, turnId: job.turnId }, syntheticAssistant, state.model);
|
|
1354
1342
|
}
|
|
1355
1343
|
async clearIfDirty(job, controls, state, reason) {
|
|
1344
|
+
// A detached turn never touched the stored session, so nothing it does can
|
|
1345
|
+
// have dirtied it. Clearing here would take the conversation away as the
|
|
1346
|
+
// price of a failed publish — the session id this turn saw is its own.
|
|
1347
|
+
if (state.detachedSession) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1356
1350
|
if (!state.usedBoundary && !state.inProgressSession.sawSessionId) {
|
|
1357
1351
|
return;
|
|
1358
1352
|
}
|