@koda-sl/baker-bridge 0.71.0 → 0.72.0-dev.423b46fd1
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 +57 -19
- package/dist/hono/bridge-job.d.ts +15 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +117 -40
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +176 -35
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/context-usage.d.ts +18 -0
- package/dist/hono/context-usage.d.ts.map +1 -1
- package/dist/hono/context-usage.js +51 -0
- package/dist/hono/context-usage.js.map +1 -1
- package/dist/hono/context-usage.test.js +23 -1
- package/dist/hono/context-usage.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/convex.js.map +1 -1
- package/dist/hono/relay.d.ts +1 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +14 -14
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +43 -20
- 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
|
@@ -298,8 +298,19 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
298
298
|
- **Context window usage** is relayed after every turn to `POST
|
|
299
299
|
/api/chat/context-usage` as `{ tokens, window }`. Occupied `tokens` come from
|
|
300
300
|
the **last main-conversation request's own usage** (`input_tokens +
|
|
301
|
-
cache_read_input_tokens + cache_creation_input_tokens`
|
|
302
|
-
|
|
301
|
+
cache_read_input_tokens + cache_creation_input_tokens`) — the real fill of the
|
|
302
|
+
window at that request. It is read from **two** places, because neither is
|
|
303
|
+
reliable alone: the assembled assistant message, and the raw
|
|
304
|
+
`message_delta` stream event (`streamEventContextUsage`). Anthropic reports a
|
|
305
|
+
request's input counts on `message_start`, which is where the runtime builds
|
|
306
|
+
the message's `usage` from — but the OpenRouter passthrough sends
|
|
307
|
+
`message_start` empty and puts the real numbers on `message_delta`, so the
|
|
308
|
+
assistant message arrives all zeros. Reading only the message meant the meter
|
|
309
|
+
never had a single reading in production: 3,910 assistant messages in one day,
|
|
310
|
+
every one reporting zero input tokens, so no chat ever stored a
|
|
311
|
+
`contextUsage` and the meter never rendered. A zero from either source is
|
|
312
|
+
ignored rather than written, so the meter holds its last real value instead of
|
|
313
|
+
resetting. The bridge does *not*
|
|
303
314
|
use the turn `result`'s per-model token totals: those aggregate every API
|
|
304
315
|
round-trip in the turn (one per tool call, plus any same-model subagent work),
|
|
305
316
|
so summing them inflates the reading by roughly the number of tool calls and
|
|
@@ -308,7 +319,11 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
308
319
|
messages count). The `window` is the model's **real OpenRouter context
|
|
309
320
|
length**, resolved from the model id (`contextWindowForModel` in
|
|
310
321
|
`model-capabilities.ts`): opus 5, opus 4.6, sonnet 4.6 and sonnet 5 are 1M, haiku 4.5
|
|
311
|
-
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
|
|
312
327
|
the OpenRouter passthrough (`ANTHROPIC_BASE_URL=openrouter.ai`) while the CLI
|
|
313
328
|
resolves the window from its own bundled catalog, which is keyed to the
|
|
314
329
|
first-party API. There they disagree: the catalog lists opus 4.6 and sonnet 4.6
|
|
@@ -372,22 +387,44 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
372
387
|
would push the command off the front and it would land as ordinary message
|
|
373
388
|
text. `/compact` resumes the live session and asks the SDK to summarize older
|
|
374
389
|
turns.
|
|
375
|
-
- **Publish
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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.
|
|
386
424
|
- **The refusal itself is never shown.** The CLI renders a refused request as an
|
|
387
425
|
assistant message *before* the query throws, and relayed text is never
|
|
388
|
-
retracted — so
|
|
389
|
-
|
|
390
|
-
owed its compaction retry, that message is dropped outright
|
|
426
|
+
retracted — so recovery used to leave a raw `Prompt is too long` in the chat,
|
|
427
|
+
above the publish that then succeeded. That message is dropped outright
|
|
391
428
|
(`isContextOverflowRefusal`: a single text block, ≤200 chars, leading with the
|
|
392
429
|
provider's error *and* matching `isContextOverflowError` — so `API Error: 400 …
|
|
393
430
|
prompt is too long` is dropped, `API Error: 500` and an answer that merely
|
|
@@ -397,8 +434,9 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
|
|
|
397
434
|
changes the leftover cannot come back as the turn's end-of-turn post. Dropping
|
|
398
435
|
it before the session observer also stops it from counting as
|
|
399
436
|
work done, which would otherwise make the retry resume with "continue where you
|
|
400
|
-
left off" instead of delivering the `/publish` the turn still owes.
|
|
401
|
-
|
|
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.
|
|
402
440
|
- **Compaction progress + confirmation.** A compaction summarizes the session to
|
|
403
441
|
free context. A manual `/compact` turn emits no assistant text, so it would
|
|
404
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" | "rehydration_failed", metadata: Record<string, unknown>): Promise<void>;
|
|
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> & {
|
|
@@ -256,6 +256,20 @@ export declare class BridgeJobExecutor {
|
|
|
256
256
|
private drainQuery;
|
|
257
257
|
private createStallDetector;
|
|
258
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;
|
|
259
273
|
private handleMessage;
|
|
260
274
|
private handleSystemMessage;
|
|
261
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
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { applyInFlightTask, applyTaskLifecycleMessage, backgroundTaskSnapshot } from "./agent-tasks.js";
|
|
5
5
|
import { CONNECTION_INPUT_TOOL_NAME, normalizeConnectionInputToolInput, } from "./connection-input.js";
|
|
6
|
-
import { messageContextTokens, pickContextWindow, turnContextUsage, } from "./context-usage.js";
|
|
6
|
+
import { messageContextTokens, pickContextWindow, streamEventContextUsage, turnContextUsage, } from "./context-usage.js";
|
|
7
7
|
import { FLOW_INPUT_TOOL_NAME, normalizeFlowInputToolInput, sanitizeFlowRequests, } from "./flow-input.js";
|
|
8
8
|
import { composioScopesToInvalidate, failedCustomServers, reportMcpInvalid, } from "./mcp-servers.js";
|
|
9
9
|
import { autoCompactWindowForModel, betasForModel, contextWindowForModel } from "./model-capabilities.js";
|
|
@@ -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
|
};
|
|
@@ -152,6 +152,18 @@ function isUserVisibleSdkMessage(msg) {
|
|
|
152
152
|
const delta = event.delta;
|
|
153
153
|
return delta?.type === "text_delta" && typeof delta.text === "string" && delta.text.length > 0;
|
|
154
154
|
}
|
|
155
|
+
// What the user reads when a turn ends on a size refusal the restart could not
|
|
156
|
+
// save. "Prompt is too long" is the provider talking to us, not the product
|
|
157
|
+
// talking to a marketer: it names nothing they did and nothing they can do. The
|
|
158
|
+
// failed turn clears the session, so sending again genuinely does start from the
|
|
159
|
+
// shorter rehydrated history — which is the action this asks for.
|
|
160
|
+
const CONTEXT_OVERFLOW_MESSAGE = "This chat has grown too long to continue in one step. Send your message again — it will pick up from a shorter version of this conversation.";
|
|
161
|
+
function userFacingTurnError(message) {
|
|
162
|
+
if (isContextOverflowError(message)) {
|
|
163
|
+
return CONTEXT_OVERFLOW_MESSAGE;
|
|
164
|
+
}
|
|
165
|
+
return enrichImageError(message);
|
|
166
|
+
}
|
|
155
167
|
function enrichImageError(message) {
|
|
156
168
|
if (!message.toLowerCase().includes("could not process image")) {
|
|
157
169
|
return message;
|
|
@@ -586,17 +598,39 @@ function createTurnEndController(job, query, closeQuery, timings) {
|
|
|
586
598
|
}
|
|
587
599
|
// Track the occupancy of the last main-conversation request. The meter reads a
|
|
588
600
|
// single request's usage, not the turn's aggregate totals — and only from the
|
|
589
|
-
// main conversation, so subagent work never pollutes it.
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
601
|
+
// main conversation, so subagent work never pollutes it.
|
|
602
|
+
//
|
|
603
|
+
// Two sources, because neither is reliable alone. The assembled assistant
|
|
604
|
+
// message is the natural one, but through the OpenRouter passthrough every
|
|
605
|
+
// provider route we run reports it as all zeros: the runtime builds that usage
|
|
606
|
+
// from `message_start`, which the passthrough sends empty, and puts the real
|
|
607
|
+
// numbers on `message_delta` instead. Production showed the cost of trusting
|
|
608
|
+
// only the message — 3,910 assistant messages in a day, every one reporting zero
|
|
609
|
+
// input tokens, so the meter had no reading to show and never appeared at all.
|
|
610
|
+
//
|
|
611
|
+
// Zero is never a real occupancy, so a zero from either source keeps the
|
|
612
|
+
// previous reading rather than resetting the meter.
|
|
593
613
|
function trackMainRequestUsage(state, msg) {
|
|
614
|
+
if (msg.type === "stream_event") {
|
|
615
|
+
if (msg.parent_tool_use_id) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const event = msg.event;
|
|
619
|
+
recordMainRequestUsage(state, streamEventContextUsage(event), event?.message?.model);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
594
622
|
if (msg.type !== "assistant" || msg.parent_tool_use_id || msg.subagent_type) {
|
|
595
623
|
return;
|
|
596
624
|
}
|
|
597
|
-
const
|
|
625
|
+
const message = msg.message;
|
|
626
|
+
recordMainRequestUsage(state, message.usage, message.model);
|
|
627
|
+
}
|
|
628
|
+
function recordMainRequestUsage(state, usage, model) {
|
|
598
629
|
if (usage && messageContextTokens(usage) > 0) {
|
|
599
630
|
state.lastMainRequestUsage = usage;
|
|
631
|
+
if (typeof model === "string" && model.length > 0) {
|
|
632
|
+
state.lastMainRequestModel = model;
|
|
633
|
+
}
|
|
600
634
|
}
|
|
601
635
|
}
|
|
602
636
|
function classifyDirtyReason(error, controls) {
|
|
@@ -648,6 +682,7 @@ export class BridgeJobExecutor {
|
|
|
648
682
|
const loaded = await this.loadTrustedBoundary(job, controls);
|
|
649
683
|
attempt = prepareSessionAttempt({ requestPrompt, history: job.history, loaded });
|
|
650
684
|
state.usedBoundary = attempt.usedBoundary;
|
|
685
|
+
state.detachedSession = attempt.detachedSession === true;
|
|
651
686
|
result.attemptedRehydration = attempt.attemptedRehydration;
|
|
652
687
|
this.recordAttemptContextSource(job, attempt);
|
|
653
688
|
result = await this.runAttempt(job, controls, state, attempt);
|
|
@@ -710,12 +745,19 @@ export class BridgeJobExecutor {
|
|
|
710
745
|
break;
|
|
711
746
|
}
|
|
712
747
|
catch (error) {
|
|
713
|
-
// The turn
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
|
|
718
|
-
|
|
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}`);
|
|
719
761
|
current = compactionRetryAttempt(current, state.inProgressSession);
|
|
720
762
|
continue;
|
|
721
763
|
}
|
|
@@ -742,7 +784,13 @@ export class BridgeJobExecutor {
|
|
|
742
784
|
await this.clearIfDirty(job, controls, state, "abort");
|
|
743
785
|
return { kind: "cancelled", costUsd, model: state.model, modelUsage };
|
|
744
786
|
}
|
|
745
|
-
|
|
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() });
|
|
746
794
|
return { kind: "completed", costUsd, model: state.model, modelUsage, cleanBoundary };
|
|
747
795
|
}
|
|
748
796
|
errorOutcome(job, controls, state, result, error) {
|
|
@@ -764,13 +812,19 @@ export class BridgeJobExecutor {
|
|
|
764
812
|
costUsd: result.costUsd,
|
|
765
813
|
model,
|
|
766
814
|
modelUsage,
|
|
767
|
-
errors: [
|
|
815
|
+
errors: [userFacingTurnError(errorMessage(error))],
|
|
768
816
|
};
|
|
769
817
|
}
|
|
770
818
|
resultWithObservedCost(result, state) {
|
|
771
819
|
return { ...result, costUsd: Math.max(result.costUsd, state.observedCostUsd) };
|
|
772
820
|
}
|
|
773
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
|
+
}
|
|
774
828
|
if (attempt.contextSource === "local_resume") {
|
|
775
829
|
void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "local_resume", {
|
|
776
830
|
historyEntryCount: job.history?.entries.length ?? 0,
|
|
@@ -861,7 +915,6 @@ export class BridgeJobExecutor {
|
|
|
861
915
|
// it (they are cumulative); it is folded into the turn totals in `finally`,
|
|
862
916
|
// so a stall that lands after a clean result still contributes its snapshot.
|
|
863
917
|
state.queryModelUsage = new Map();
|
|
864
|
-
state.compactionRetryOwed = attempt.autoCompact === false;
|
|
865
918
|
const mcpServers = await this.resolveMcpServers(job);
|
|
866
919
|
const skills = await readSessionSkills(bridgeRuntimeFiles.projectRoot());
|
|
867
920
|
const autoCompactWindow = autoCompactWindowForModel(model);
|
|
@@ -1108,14 +1161,15 @@ export class BridgeJobExecutor {
|
|
|
1108
1161
|
return costUsd;
|
|
1109
1162
|
}
|
|
1110
1163
|
controls.markEvent?.();
|
|
1111
|
-
// Drop the size refusal outright
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1164
|
+
// Drop the size refusal outright. A recovered turn must not leave a raw
|
|
1165
|
+
// provider error sitting above the work that then succeeded, and a turn that
|
|
1166
|
+
// fails anyway says so once, in product language, through the error outcome —
|
|
1167
|
+
// never twice, and never in the provider's words. Dropping it before
|
|
1168
|
+
// `observeInProgressSession` also keeps it from setting `resumeSessionAt` —
|
|
1169
|
+
// the refusal is not work the agent did, and counting it as such would send
|
|
1170
|
+
// the retry a "continue where you left off" note instead of the `/publish`
|
|
1171
|
+
// the turn still owes.
|
|
1172
|
+
if (isContextOverflowRefusal(msg)) {
|
|
1119
1173
|
// Today the refusal is synthetic — the request is rejected pre-flight, so
|
|
1120
1174
|
// no deltas precede it and `turnText` is empty. Clearing it anyway is what
|
|
1121
1175
|
// makes the drop hold if that ever changes: streamed text survives the
|
|
@@ -1151,22 +1205,7 @@ export class BridgeJobExecutor {
|
|
|
1151
1205
|
if (snapshot.size > 0) {
|
|
1152
1206
|
state.queryModelUsage = snapshot;
|
|
1153
1207
|
}
|
|
1154
|
-
|
|
1155
|
-
// main-conversation request's own usage — the real occupancy of the window
|
|
1156
|
-
// at that request. We deliberately do NOT use the result's per-model token
|
|
1157
|
-
// totals: those aggregate every API round-trip in the turn (one per tool
|
|
1158
|
-
// call, plus any same-model subagent work), so a handful of tool calls
|
|
1159
|
-
// inflates them past the window and pins the meter at 100% on an almost-
|
|
1160
|
-
// empty chat. The window is the model's real OpenRouter context length
|
|
1161
|
-
// (opus/sonnet 4.6 = 1M); we resolve it from the model id because the
|
|
1162
|
-
// runtime under-reports it through the OpenRouter passthrough, falling back
|
|
1163
|
-
// to the reported window for unknown models. A relay failure must never
|
|
1164
|
-
// fail the turn.
|
|
1165
|
-
const window = contextWindowForModel(state.model) ?? pickContextWindow(msg.modelUsage);
|
|
1166
|
-
const contextUsage = turnContextUsage(state.lastMainRequestUsage, window);
|
|
1167
|
-
if (contextUsage) {
|
|
1168
|
-
await this.relay.contextUsage({ threadId: job.threadId, turnId: job.turnId }, contextUsage);
|
|
1169
|
-
}
|
|
1208
|
+
await this.publishContextUsage(job, state, msg.modelUsage);
|
|
1170
1209
|
const resultError = sdkResultError(msg);
|
|
1171
1210
|
if (resultError) {
|
|
1172
1211
|
await this.clearIfDirty(job, controls, state, "result_error");
|
|
@@ -1180,6 +1219,38 @@ export class BridgeJobExecutor {
|
|
|
1180
1219
|
await this.handleMessage(job, state, msg);
|
|
1181
1220
|
return costUsd;
|
|
1182
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
|
+
}
|
|
1183
1254
|
async handleMessage(job, state, msg) {
|
|
1184
1255
|
const target = { threadId: job.threadId, turnId: job.turnId };
|
|
1185
1256
|
if (msg.type === "system") {
|
|
@@ -1270,6 +1341,12 @@ export class BridgeJobExecutor {
|
|
|
1270
1341
|
await this.relay.assistantEvent({ threadId: job.threadId, turnId: job.turnId }, syntheticAssistant, state.model);
|
|
1271
1342
|
}
|
|
1272
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
|
+
}
|
|
1273
1350
|
if (!state.usedBoundary && !state.inProgressSession.sawSessionId) {
|
|
1274
1351
|
return;
|
|
1275
1352
|
}
|