@koda-sl/baker-bridge 0.56.0-dev.7f6a084be → 0.56.1
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 +37 -12
- package/dist/hono/agent-session.d.ts +6 -0
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +48 -1
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +60 -2
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent.test.js +1 -0
- package/dist/hono/agent.test.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +10 -0
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +209 -110
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +253 -27
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/context-usage.d.ts +8 -1
- package/dist/hono/context-usage.d.ts.map +1 -1
- package/dist/hono/context-usage.js +35 -10
- package/dist/hono/context-usage.js.map +1 -1
- package/dist/hono/context-usage.test.js +38 -34
- package/dist/hono/context-usage.test.js.map +1 -1
- package/dist/hono/model-router.d.ts +4 -0
- package/dist/hono/model-router.d.ts.map +1 -1
- package/dist/hono/model-router.js +22 -1
- package/dist/hono/model-router.js.map +1 -1
- package/dist/hono/model-router.test.js +33 -1
- package/dist/hono/model-router.test.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +8 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +1 -0
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/session-continuity.js +23 -0
- package/dist/hono/session-continuity.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-job.d.ts","sourceRoot":"","sources":["../../src/hono/bridge-job.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAoB,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC1G,OAAO,EAAE,KAAK,SAAS,EAA6B,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,KAAK,YAAY,
|
|
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
|
@@ -2,13 +2,14 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { applyTaskLifecycleMessage } from "./agent-tasks.js";
|
|
5
|
-
import {
|
|
5
|
+
import { pickContextWindow, turnContextUsage } from "./context-usage.js";
|
|
6
6
|
import { FLOW_INPUT_TOOL_NAME, normalizeFlowInputToolInput, sanitizeFlowRequests, } from "./flow-input.js";
|
|
7
7
|
import { composioScopesToInvalidate, reportMcpInvalid, } from "./mcp-servers.js";
|
|
8
|
+
import { contextWindowForModel } from "./model-router.js";
|
|
8
9
|
import { bakerPlugins } from "./plugins.js";
|
|
9
10
|
import { appendCurrentReferenceContext } from "./prompt-context.js";
|
|
10
11
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
11
|
-
import { applyCurrentSenderAttribution, buildCleanBoundary, buildRehydratedPrompt, emptyInProgressSession, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, } from "./session-continuity.js";
|
|
12
|
+
import { applyCurrentSenderAttribution, buildCleanBoundary, buildRehydratedPrompt, emptyInProgressSession, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
|
|
12
13
|
import { verbatimSlashCommand } from "./slash-commands.js";
|
|
13
14
|
import { createBakerUiServer, normalizeTagInputToolInput, sanitizeTagChanges, TAG_INPUT_TOOL_NAME, } from "./tag-input.js";
|
|
14
15
|
export function parsePlanDecision(value) {
|
|
@@ -54,6 +55,28 @@ export function remapAnswersToQuestionText(questions, answers) {
|
|
|
54
55
|
return Object.keys(byText).length > 0 ? byText : answers;
|
|
55
56
|
}
|
|
56
57
|
const DEFAULT_ASK_USER_QUESTION_TIMEOUT_MS = 15 * 60 * 1000;
|
|
58
|
+
// A model request that streams nothing for this long is a hung upstream
|
|
59
|
+
// connection (observed in production: an OpenRouter request that never yields
|
|
60
|
+
// and never errors). Only armed while the CLI should be talking to the model —
|
|
61
|
+
// right after a tool_result goes back or between stream deltas — never during
|
|
62
|
+
// tool execution, compaction, or a blocking dashboard prompt, where silence is
|
|
63
|
+
// legitimate. Kept well above worst-case time-to-first-token on huge contexts.
|
|
64
|
+
const DEFAULT_STALL_TIMEOUT_MS = 3 * 60 * 1000;
|
|
65
|
+
const DEFAULT_STALL_CHECK_INTERVAL_MS = 15_000;
|
|
66
|
+
// Retries resume the CLI session in place, so each one is cheap; past this the
|
|
67
|
+
// upstream is genuinely down and the turn should fail legibly.
|
|
68
|
+
const MAX_STALL_RETRIES = 2;
|
|
69
|
+
// Agent-facing deny message when a blocking dashboard prompt (question, plan
|
|
70
|
+
// review, tag/flow form) goes unanswered past the wait window. The turn must
|
|
71
|
+
// end cleanly — never error — so the user can pick up with a normal message.
|
|
72
|
+
const PROMPT_UNANSWERED_MESSAGE = "The user has not answered this yet. This is not an error. Stop waiting: end your turn with a brief note " +
|
|
73
|
+
"about what you need from them, so they can reply whenever they're ready. Do not call this tool again this turn.";
|
|
74
|
+
export class StreamStallError extends Error {
|
|
75
|
+
constructor() {
|
|
76
|
+
super("The connection to the model stalled and could not be recovered. Send a message to continue.");
|
|
77
|
+
this.name = "StreamStallError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
const CLAUDE_PLAN_MAX_BYTES = 512 * 1024;
|
|
58
81
|
// Rejecting a plan denies ExitPlanMode. We must NOT interrupt the query: an
|
|
59
82
|
// interrupt tears the turn down mid-tool-call, so Claude Code ends with an
|
|
@@ -277,6 +300,8 @@ export class BridgeJobExecutor {
|
|
|
277
300
|
prepareAttachments;
|
|
278
301
|
mcpServersProvider;
|
|
279
302
|
askUserQuestionTimeoutMs;
|
|
303
|
+
stallTimeoutMs;
|
|
304
|
+
stallCheckIntervalMs;
|
|
280
305
|
now;
|
|
281
306
|
constructor(dependencies) {
|
|
282
307
|
this.sdk = dependencies.sdk;
|
|
@@ -286,6 +311,8 @@ export class BridgeJobExecutor {
|
|
|
286
311
|
this.prepareAttachments = dependencies.prepareAttachments ?? (async () => "");
|
|
287
312
|
this.mcpServersProvider = dependencies.mcpServersProvider;
|
|
288
313
|
this.askUserQuestionTimeoutMs = dependencies.askUserQuestionTimeoutMs ?? DEFAULT_ASK_USER_QUESTION_TIMEOUT_MS;
|
|
314
|
+
this.stallTimeoutMs = dependencies.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
315
|
+
this.stallCheckIntervalMs = dependencies.stallCheckIntervalMs ?? DEFAULT_STALL_CHECK_INTERVAL_MS;
|
|
289
316
|
this.now = dependencies.now ?? Date.now;
|
|
290
317
|
}
|
|
291
318
|
async execute(job, controls) {
|
|
@@ -356,7 +383,25 @@ export class BridgeJobExecutor {
|
|
|
356
383
|
return { costUsd: 0, attemptedRehydration: attempt.attemptedRehydration };
|
|
357
384
|
}
|
|
358
385
|
state.model = selected.model;
|
|
359
|
-
|
|
386
|
+
// A stalled stream closes the wedged query and resumes the CLI session in
|
|
387
|
+
// place — the turn survives a hung upstream request instead of dying at the
|
|
388
|
+
// 30-minute session watchdog with all mid-turn work stranded.
|
|
389
|
+
let costUsd = 0;
|
|
390
|
+
let current = attempt;
|
|
391
|
+
for (let stallRetries = 0;;) {
|
|
392
|
+
try {
|
|
393
|
+
costUsd += await this.runQueryOnce(job, controls, state, current, selected);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (!(error instanceof StreamStallError) || stallRetries >= MAX_STALL_RETRIES || !controls.isActive()) {
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
stallRetries += 1;
|
|
401
|
+
console.error(`[BridgeJob] model stream stalled — resuming session (retry ${stallRetries}/${MAX_STALL_RETRIES}) for thread ${job.threadId}`);
|
|
402
|
+
current = stallContinuationAttempt(current, state.inProgressSession);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
360
405
|
if (controls.isActive()) {
|
|
361
406
|
void this.relay.routerSample({ threadId: job.threadId, turnId: job.turnId }, selected.model, selected.routerMessages);
|
|
362
407
|
}
|
|
@@ -516,8 +561,30 @@ export class BridgeJobExecutor {
|
|
|
516
561
|
query.close?.();
|
|
517
562
|
};
|
|
518
563
|
controls.abortSignal.addEventListener("abort", closeQuery, { once: true });
|
|
564
|
+
try {
|
|
565
|
+
costUsd = await this.drainQuery(job, controls, state, query, closeQuery);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
controls.abortSignal.removeEventListener("abort", closeQuery);
|
|
569
|
+
}
|
|
570
|
+
if (!controls.isActive()) {
|
|
571
|
+
closeQuery();
|
|
572
|
+
}
|
|
573
|
+
if (!state.inProgressSession.sawCleanResult && controls.isActive()) {
|
|
574
|
+
await this.clearIfDirty(job, controls, state, "missing_result");
|
|
575
|
+
}
|
|
576
|
+
return costUsd;
|
|
577
|
+
}
|
|
578
|
+
// Consumes the query's message stream under stall protection. Throws
|
|
579
|
+
// StreamStallError when the stream went silent past the stall window while
|
|
580
|
+
// the model should have been streaming — the caller retries by resuming the
|
|
581
|
+
// in-progress session.
|
|
582
|
+
async drainQuery(job, controls, state, query, closeQuery) {
|
|
583
|
+
const stallDetector = this.createStallDetector(job, controls, closeQuery);
|
|
584
|
+
let costUsd = 0;
|
|
519
585
|
try {
|
|
520
586
|
for await (const msg of query) {
|
|
587
|
+
stallDetector.observeMessage(msg);
|
|
521
588
|
costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
|
|
522
589
|
if (!controls.isActive()) {
|
|
523
590
|
closeQuery();
|
|
@@ -527,19 +594,64 @@ export class BridgeJobExecutor {
|
|
|
527
594
|
}
|
|
528
595
|
catch (error) {
|
|
529
596
|
closeQuery();
|
|
597
|
+
if (stallDetector.stalled() && controls.isActive()) {
|
|
598
|
+
throw new StreamStallError();
|
|
599
|
+
}
|
|
530
600
|
throw error;
|
|
531
601
|
}
|
|
532
602
|
finally {
|
|
533
|
-
|
|
603
|
+
stallDetector.dispose();
|
|
534
604
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
if (!state.inProgressSession.sawCleanResult && controls.isActive()) {
|
|
539
|
-
await this.clearIfDirty(job, controls, state, "missing_result");
|
|
605
|
+
if (stallDetector.stalled() && controls.isActive()) {
|
|
606
|
+
throw new StreamStallError();
|
|
540
607
|
}
|
|
541
608
|
return costUsd;
|
|
542
609
|
}
|
|
610
|
+
// Stall detection: armed only while the CLI should be talking to the model —
|
|
611
|
+
// right after a tool_result goes back (the next API request), between stream
|
|
612
|
+
// deltas, and at query start (CLI boot + first request complete well inside
|
|
613
|
+
// the stall window, and a retry that hangs before its first message must not
|
|
614
|
+
// escape detection). Silence is legitimate during tool execution, compaction,
|
|
615
|
+
// and blocking dashboard prompts, so those phases stay disarmed (the session
|
|
616
|
+
// watchdog still backstops them). On stall the wedged query is closed so the
|
|
617
|
+
// caller can retry by resuming the session.
|
|
618
|
+
createStallDetector(job, controls, closeQuery) {
|
|
619
|
+
let stalled = false;
|
|
620
|
+
let awaitingModel = true;
|
|
621
|
+
let lastMessageAt = this.now();
|
|
622
|
+
const hasPendingPrompt = () => controls.pendingQuestions.size > 0 ||
|
|
623
|
+
controls.pendingPlans.size > 0 ||
|
|
624
|
+
controls.pendingTagInputs.size > 0 ||
|
|
625
|
+
controls.pendingFlowInputs.size > 0;
|
|
626
|
+
const timer = setInterval(() => {
|
|
627
|
+
// Keep the clock fresh while a blocking dashboard prompt is outstanding.
|
|
628
|
+
// The user can take far longer than the stall window to answer (a prompt
|
|
629
|
+
// waits up to 15 min); without this, the first tick after they answer
|
|
630
|
+
// would see a stale `lastMessageAt` and misfire a stall on a healthy turn.
|
|
631
|
+
if (hasPendingPrompt()) {
|
|
632
|
+
lastMessageAt = this.now();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!awaitingModel || stalled) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (this.now() - lastMessageAt <= this.stallTimeoutMs) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
stalled = true;
|
|
642
|
+
console.error(`[BridgeJob] no stream data for ${this.stallTimeoutMs}ms while awaiting the model — closing query for retry (thread ${job.threadId})`);
|
|
643
|
+
closeQuery();
|
|
644
|
+
}, this.stallCheckIntervalMs);
|
|
645
|
+
timer.unref?.();
|
|
646
|
+
return {
|
|
647
|
+
observeMessage: (msg) => {
|
|
648
|
+
lastMessageAt = this.now();
|
|
649
|
+
awaitingModel = msg.type === "user" || msg.type === "stream_event";
|
|
650
|
+
},
|
|
651
|
+
stalled: () => stalled,
|
|
652
|
+
dispose: () => clearInterval(timer),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
543
655
|
async consumeSdkMessage(job, controls, state, msg, costUsd) {
|
|
544
656
|
if (!controls.isActive()) {
|
|
545
657
|
return costUsd;
|
|
@@ -558,13 +670,30 @@ export class BridgeJobExecutor {
|
|
|
558
670
|
throw controlChannelClosedError();
|
|
559
671
|
}
|
|
560
672
|
}
|
|
673
|
+
// Track the occupancy of the last main-conversation request. We read the
|
|
674
|
+
// meter from a single request's usage, not the turn's aggregate totals —
|
|
675
|
+
// and only from the main conversation, so subagent work never pollutes it.
|
|
676
|
+
if (msg.type === "assistant" && !msg.parent_tool_use_id && !msg.subagent_type) {
|
|
677
|
+
const usage = msg.message.usage;
|
|
678
|
+
if (usage) {
|
|
679
|
+
state.lastMainRequestUsage = usage;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
561
682
|
if (msg.type === "result") {
|
|
562
683
|
state.observedCostUsd = msg.total_cost_usd;
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
|
|
684
|
+
// Surface how full the conversation's context is. Tokens come from the last
|
|
685
|
+
// main-conversation request's own usage — the real occupancy of the window
|
|
686
|
+
// at that request. We deliberately do NOT use the result's per-model token
|
|
687
|
+
// totals: those aggregate every API round-trip in the turn (one per tool
|
|
688
|
+
// call, plus any same-model subagent work), so a handful of tool calls
|
|
689
|
+
// inflates them past the window and pins the meter at 100% on an almost-
|
|
690
|
+
// empty chat. The window is the model's real OpenRouter context length
|
|
691
|
+
// (opus/sonnet 4.6 = 1M); we resolve it from the model id because the
|
|
692
|
+
// runtime under-reports it through the OpenRouter passthrough, falling back
|
|
693
|
+
// to the reported window for unknown models. A relay failure must never
|
|
694
|
+
// fail the turn.
|
|
695
|
+
const window = contextWindowForModel(state.model) ?? pickContextWindow(msg.modelUsage);
|
|
696
|
+
const contextUsage = turnContextUsage(state.lastMainRequestUsage, window);
|
|
568
697
|
if (contextUsage) {
|
|
569
698
|
await this.relay.contextUsage({ threadId: job.threadId, turnId: job.turnId }, contextUsage);
|
|
570
699
|
}
|
|
@@ -667,25 +796,26 @@ export class BridgeJobExecutor {
|
|
|
667
796
|
}
|
|
668
797
|
await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason });
|
|
669
798
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
799
|
+
// Parks the turn on a blocking dashboard prompt. An unanswered prompt must
|
|
800
|
+
// NOT error the turn: the wait window ending resolves to `unanswered` so the
|
|
801
|
+
// caller can deny the tool gracefully — the agent ends its turn cleanly and
|
|
802
|
+
// the user replies with a normal message whenever they return.
|
|
803
|
+
awaitPromptDecision(input) {
|
|
804
|
+
const { label, signal, controls, pending, toolUseId } = input;
|
|
805
|
+
return new Promise((resolve, reject) => {
|
|
677
806
|
if (signal?.aborted || controls.abortSignal.aborted) {
|
|
678
|
-
reject(new Error(
|
|
807
|
+
reject(new Error(`${label} aborted before awaiting decision`));
|
|
679
808
|
return;
|
|
680
809
|
}
|
|
681
810
|
const timeout = setTimeout(() => {
|
|
682
|
-
|
|
683
|
-
|
|
811
|
+
pending.delete(toolUseId);
|
|
812
|
+
console.error(`[${label}] no answer after ${this.askUserQuestionTimeoutMs}ms — ending the wait gracefully`);
|
|
813
|
+
resolve({ kind: "unanswered" });
|
|
684
814
|
}, this.askUserQuestionTimeoutMs);
|
|
685
|
-
|
|
686
|
-
resolve: (
|
|
815
|
+
pending.set(toolUseId, {
|
|
816
|
+
resolve: (value) => {
|
|
687
817
|
clearTimeout(timeout);
|
|
688
|
-
resolve(
|
|
818
|
+
resolve({ kind: "answered", value });
|
|
689
819
|
},
|
|
690
820
|
reject: (reason) => {
|
|
691
821
|
clearTimeout(timeout);
|
|
@@ -694,17 +824,34 @@ export class BridgeJobExecutor {
|
|
|
694
824
|
});
|
|
695
825
|
const abort = () => {
|
|
696
826
|
clearTimeout(timeout);
|
|
697
|
-
|
|
698
|
-
reject(new Error(
|
|
827
|
+
pending.delete(toolUseId);
|
|
828
|
+
reject(new Error(`${label} aborted`));
|
|
699
829
|
};
|
|
700
830
|
signal?.addEventListener("abort", abort, { once: true });
|
|
701
831
|
controls.abortSignal.addEventListener("abort", abort, { once: true });
|
|
702
832
|
});
|
|
833
|
+
}
|
|
834
|
+
async handleAskUserQuestion(job, controls, input, toolUseId, signal) {
|
|
835
|
+
const target = { threadId: job.threadId, turnId: job.turnId };
|
|
836
|
+
const posted = await this.relay.inputRequest(target, { toolUseId, questions: input.questions });
|
|
837
|
+
if (!posted) {
|
|
838
|
+
console.error(`[AskUserQuestion] failed to post input-request for ${toolUseId} — frontend may not show question UI`);
|
|
839
|
+
}
|
|
840
|
+
const decision = await this.awaitPromptDecision({
|
|
841
|
+
label: "AskUserQuestion",
|
|
842
|
+
signal,
|
|
843
|
+
controls,
|
|
844
|
+
pending: controls.pendingQuestions,
|
|
845
|
+
toolUseId,
|
|
846
|
+
});
|
|
703
847
|
void this.relay.inputResolved(target);
|
|
848
|
+
if (decision.kind === "unanswered") {
|
|
849
|
+
return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
|
|
850
|
+
}
|
|
704
851
|
// The SDK matches AskUserQuestion answers to questions by TEXT, so hand it
|
|
705
852
|
// the documented `{ [questionText]: answer }` shape, not the dashboard's
|
|
706
853
|
// positional `q{i}` keys — otherwise the tool reports "dismissed".
|
|
707
|
-
const answersByQuestionText = remapAnswersToQuestionText(input.questions,
|
|
854
|
+
const answersByQuestionText = remapAnswersToQuestionText(input.questions, decision.value);
|
|
708
855
|
return { behavior: "allow", updatedInput: { questions: input.questions, answers: answersByQuestionText } };
|
|
709
856
|
}
|
|
710
857
|
async handleTagInputRequest(job, controls, input, toolUseId, signal) {
|
|
@@ -733,37 +880,20 @@ export class BridgeJobExecutor {
|
|
|
733
880
|
message: "The tag approval form could not be delivered to the dashboard — retry request_tag_input in a moment.",
|
|
734
881
|
};
|
|
735
882
|
}
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
controls.pendingTagInputs.delete(toolUseId);
|
|
743
|
-
reject(new Error(`request_tag_input timed out after ${this.askUserQuestionTimeoutMs}ms`));
|
|
744
|
-
}, this.askUserQuestionTimeoutMs);
|
|
745
|
-
controls.pendingTagInputs.set(toolUseId, {
|
|
746
|
-
resolve: (result) => {
|
|
747
|
-
clearTimeout(timeout);
|
|
748
|
-
resolve(result);
|
|
749
|
-
},
|
|
750
|
-
reject: (reason) => {
|
|
751
|
-
clearTimeout(timeout);
|
|
752
|
-
reject(reason);
|
|
753
|
-
},
|
|
754
|
-
});
|
|
755
|
-
const abort = () => {
|
|
756
|
-
clearTimeout(timeout);
|
|
757
|
-
controls.pendingTagInputs.delete(toolUseId);
|
|
758
|
-
reject(new Error("request_tag_input aborted"));
|
|
759
|
-
};
|
|
760
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
761
|
-
controls.abortSignal.addEventListener("abort", abort, { once: true });
|
|
883
|
+
const decision = await this.awaitPromptDecision({
|
|
884
|
+
label: "request_tag_input",
|
|
885
|
+
signal,
|
|
886
|
+
controls,
|
|
887
|
+
pending: controls.pendingTagInputs,
|
|
888
|
+
toolUseId,
|
|
762
889
|
});
|
|
763
890
|
void this.relay.tagInputResolved(target);
|
|
891
|
+
if (decision.kind === "unanswered") {
|
|
892
|
+
return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
|
|
893
|
+
}
|
|
764
894
|
// The MCP tool handler echoes `resolution` back as the tool result — the
|
|
765
895
|
// redacted per-tab results are all the model ever sees.
|
|
766
|
-
return { behavior: "allow", updatedInput: { changes: sanitizedChanges, resolution } };
|
|
896
|
+
return { behavior: "allow", updatedInput: { changes: sanitizedChanges, resolution: decision.value } };
|
|
767
897
|
}
|
|
768
898
|
async handleFlowInputRequest(job, controls, input, toolUseId, signal) {
|
|
769
899
|
const target = { threadId: job.threadId, turnId: job.turnId };
|
|
@@ -787,38 +917,21 @@ export class BridgeJobExecutor {
|
|
|
787
917
|
message: "The form-configuration request could not be delivered to the dashboard — retry request_flow_input in a moment.",
|
|
788
918
|
};
|
|
789
919
|
}
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
controls.pendingFlowInputs.delete(toolUseId);
|
|
797
|
-
reject(new Error(`request_flow_input timed out after ${this.askUserQuestionTimeoutMs}ms`));
|
|
798
|
-
}, this.askUserQuestionTimeoutMs);
|
|
799
|
-
controls.pendingFlowInputs.set(toolUseId, {
|
|
800
|
-
resolve: (result) => {
|
|
801
|
-
clearTimeout(timeout);
|
|
802
|
-
resolve(result);
|
|
803
|
-
},
|
|
804
|
-
reject: (reason) => {
|
|
805
|
-
clearTimeout(timeout);
|
|
806
|
-
reject(reason);
|
|
807
|
-
},
|
|
808
|
-
});
|
|
809
|
-
const abort = () => {
|
|
810
|
-
clearTimeout(timeout);
|
|
811
|
-
controls.pendingFlowInputs.delete(toolUseId);
|
|
812
|
-
reject(new Error("request_flow_input aborted"));
|
|
813
|
-
};
|
|
814
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
815
|
-
controls.abortSignal.addEventListener("abort", abort, { once: true });
|
|
920
|
+
const decision = await this.awaitPromptDecision({
|
|
921
|
+
label: "request_flow_input",
|
|
922
|
+
signal,
|
|
923
|
+
controls,
|
|
924
|
+
pending: controls.pendingFlowInputs,
|
|
925
|
+
toolUseId,
|
|
816
926
|
});
|
|
817
927
|
void this.relay.flowInputResolved(target);
|
|
928
|
+
if (decision.kind === "unanswered") {
|
|
929
|
+
return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
|
|
930
|
+
}
|
|
818
931
|
// The MCP tool handler echoes `resolution` — the redacted per-tab results are
|
|
819
932
|
// all the model ever sees; secret values are written into the flow file by the
|
|
820
933
|
// bridge's /flow-input-decision handler and never reach here.
|
|
821
|
-
return { behavior: "allow", updatedInput: { requests: sanitizedRequests, resolution } };
|
|
934
|
+
return { behavior: "allow", updatedInput: { requests: sanitizedRequests, resolution: decision.value } };
|
|
822
935
|
}
|
|
823
936
|
async handlePlanReview(job, controls, input, toolUseId, signal) {
|
|
824
937
|
const target = { threadId: job.threadId, turnId: job.turnId };
|
|
@@ -828,34 +941,20 @@ export class BridgeJobExecutor {
|
|
|
828
941
|
if (!posted) {
|
|
829
942
|
console.error(`[ExitPlanMode] failed to post plan-review for ${toolUseId} — frontend may not show plan UI`);
|
|
830
943
|
}
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
controls.pendingPlans.delete(toolUseId);
|
|
838
|
-
reject(new Error(`ExitPlanMode timed out after ${this.askUserQuestionTimeoutMs}ms`));
|
|
839
|
-
}, this.askUserQuestionTimeoutMs);
|
|
840
|
-
controls.pendingPlans.set(toolUseId, {
|
|
841
|
-
resolve: (decision) => {
|
|
842
|
-
clearTimeout(timeout);
|
|
843
|
-
resolve(decision);
|
|
844
|
-
},
|
|
845
|
-
reject: (reason) => {
|
|
846
|
-
clearTimeout(timeout);
|
|
847
|
-
reject(reason);
|
|
848
|
-
},
|
|
849
|
-
});
|
|
850
|
-
const abort = () => {
|
|
851
|
-
clearTimeout(timeout);
|
|
852
|
-
controls.pendingPlans.delete(toolUseId);
|
|
853
|
-
reject(new Error("ExitPlanMode aborted"));
|
|
854
|
-
};
|
|
855
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
856
|
-
controls.abortSignal.addEventListener("abort", abort, { once: true });
|
|
944
|
+
const reviewed = await this.awaitPromptDecision({
|
|
945
|
+
label: "ExitPlanMode",
|
|
946
|
+
signal,
|
|
947
|
+
controls,
|
|
948
|
+
pending: controls.pendingPlans,
|
|
949
|
+
toolUseId,
|
|
857
950
|
});
|
|
858
951
|
void this.relay.planResolved(target);
|
|
952
|
+
if (reviewed.kind === "unanswered") {
|
|
953
|
+
// Deny WITHOUT interrupt (same invariant as plan rejection) so the turn
|
|
954
|
+
// ends cleanly; the user can approve with a normal message later.
|
|
955
|
+
return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
|
|
956
|
+
}
|
|
957
|
+
const decision = reviewed.value;
|
|
859
958
|
if (decision.kind === "approve") {
|
|
860
959
|
return { behavior: "allow", updatedInput: input };
|
|
861
960
|
}
|