teamshare-bridge 0.27.5 → 0.28.2
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/dist/bridge/daemon.js +57 -2
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/wake-handler.d.ts +16 -0
- package/dist/bridge/wake-handler.js +67 -37
- package/dist/bridge/wake-handler.js.map +1 -1
- package/dist/bridge/wake-outbox.d.ts +28 -0
- package/dist/bridge/wake-outbox.js +45 -1
- package/dist/bridge/wake-outbox.js.map +1 -1
- package/dist/bridge/wake-routing.js +13 -3
- package/dist/bridge/wake-routing.js.map +1 -1
- package/dist/cli/commands/build.js +5 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/chat.js +8 -0
- package/dist/cli/commands/chat.js.map +1 -1
- package/dist/cli/commands/run.js +9 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/lib/api-content.d.ts +11 -0
- package/dist/lib/api-content.js.map +1 -1
- package/dist/lib/api-types.d.ts +2 -0
- package/dist/lib/browser/browse-harness.js +31 -6
- package/dist/lib/browser/browse-harness.js.map +1 -1
- package/dist/lib/chat/board-reply.d.ts +5 -0
- package/dist/lib/chat/board-reply.js +12 -0
- package/dist/lib/chat/board-reply.js.map +1 -1
- package/dist/lib/chat/chat-reply.d.ts +54 -0
- package/dist/lib/chat/chat-reply.js +141 -9
- package/dist/lib/chat/chat-reply.js.map +1 -1
- package/dist/lib/chat/doc-reply.d.ts +5 -0
- package/dist/lib/chat/doc-reply.js +63 -18
- package/dist/lib/chat/doc-reply.js.map +1 -1
- package/dist/lib/git/git-branch.d.ts +37 -0
- package/dist/lib/git/git-branch.js +56 -6
- package/dist/lib/git/git-branch.js.map +1 -1
- package/dist/lib/git/git-finalize.d.ts +5 -0
- package/dist/lib/git/git-finalize.js +62 -3
- package/dist/lib/git/git-finalize.js.map +1 -1
- package/dist/lib/git/git-prepare.d.ts +5 -0
- package/dist/lib/git/git-prepare.js +12 -2
- package/dist/lib/git/git-prepare.js.map +1 -1
- package/dist/lib/harness/harness-adapter.d.ts +21 -0
- package/dist/lib/harness/harness-adapter.js.map +1 -1
- package/dist/lib/harness/prompt-file.d.ts +56 -0
- package/dist/lib/harness/prompt-file.js +120 -13
- package/dist/lib/harness/prompt-file.js.map +1 -1
- package/dist/lib/harness/session-store.d.ts +88 -0
- package/dist/lib/harness/session-store.js +141 -2
- package/dist/lib/harness/session-store.js.map +1 -1
- package/dist/lib/harness/spawn.d.ts +15 -0
- package/dist/lib/harness/spawn.js +424 -68
- package/dist/lib/harness/spawn.js.map +1 -1
- package/dist/lib/harness/turn-watch.d.ts +149 -0
- package/dist/lib/harness/turn-watch.js +265 -0
- package/dist/lib/harness/turn-watch.js.map +1 -0
- package/dist/lib/orchestrator/handlers/git.js +4 -3
- package/dist/lib/orchestrator/handlers/git.js.map +1 -1
- package/dist/lib/orchestrator/worktree.d.ts +2 -0
- package/dist/lib/orchestrator/worktree.js +1 -0
- package/dist/lib/orchestrator/worktree.js.map +1 -1
- package/dist/lib/workspace/mirror-lease.d.ts +10 -0
- package/dist/lib/workspace/mirror-lease.js +1 -0
- package/dist/lib/workspace/mirror-lease.js.map +1 -1
- package/dist/lib/workspace/mirror.d.ts +16 -0
- package/dist/lib/workspace/mirror.js +124 -22
- package/dist/lib/workspace/mirror.js.map +1 -1
- package/dist/lib/workspace/state-root.d.ts +8 -0
- package/dist/lib/workspace/state-root.js +11 -0
- package/dist/lib/workspace/state-root.js.map +1 -1
- package/dist/lib/workspace/stop-notice.d.ts +13 -1
- package/dist/lib/workspace/stop-notice.js +17 -2
- package/dist/lib/workspace/stop-notice.js.map +1 -1
- package/dist/lib/workspace/stop-save.d.ts +3 -0
- package/dist/lib/workspace/stop-save.js +39 -5
- package/dist/lib/workspace/stop-save.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isContextOverflowText = isContextOverflowText;
|
|
3
4
|
exports.killTree = killTree;
|
|
4
5
|
exports.isIsolatedTree = isIsolatedTree;
|
|
5
6
|
exports.formatShellContinuation = formatShellContinuation;
|
|
@@ -29,6 +30,7 @@ const line_assembler_1 = require("./line-assembler");
|
|
|
29
30
|
const opencode_outcome_1 = require("./opencode-outcome");
|
|
30
31
|
const opencode_control_1 = require("./opencode-control");
|
|
31
32
|
const activity_1 = require("./activity");
|
|
33
|
+
const turn_watch_1 = require("./turn-watch");
|
|
32
34
|
const agy_pty_1 = require("./agy-pty");
|
|
33
35
|
const prompt_file_1 = require("./prompt-file");
|
|
34
36
|
const diagnostics_1 = require("../diagnostics");
|
|
@@ -37,12 +39,6 @@ const relay_key_1 = require("./relay-key");
|
|
|
37
39
|
const resume_1 = require("./resume");
|
|
38
40
|
const opencode_session_1 = require("../llm/opencode-session");
|
|
39
41
|
const session_store_1 = require("./session-store");
|
|
40
|
-
/**
|
|
41
|
-
* Maximum prompt length before we switch to temp-file delivery.
|
|
42
|
-
* Windows argv limit is 32,767 chars; we leave headroom for binary path,
|
|
43
|
-
* MCP config, and other flags.
|
|
44
|
-
*/
|
|
45
|
-
const PROMPT_FILE_THRESHOLD = 28_000;
|
|
46
42
|
/** Real task ids are UUIDs; synthetic keys (chat-<projectId>) are not reported. */
|
|
47
43
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
48
44
|
let warnedNoPty = false;
|
|
@@ -85,6 +81,37 @@ function createRawCapture(settings) {
|
|
|
85
81
|
function msg(err) {
|
|
86
82
|
return err instanceof Error ? err.message : String(err);
|
|
87
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Does this harness output line report "the conversation is too big for the
|
|
86
|
+
* model's context window"? (2026-12)
|
|
87
|
+
*
|
|
88
|
+
* Harnesses fail this way in words, not codes, and every vendor phrases it
|
|
89
|
+
* differently: OpenAI says "maximum context length", Anthropic "prompt is too
|
|
90
|
+
* long", Gemini "input token count ... exceeds", local models print
|
|
91
|
+
* "context window exceeded". We only need to recognise it well enough to
|
|
92
|
+
* ROTATE instead of failing the reply - a false positive costs one fresh
|
|
93
|
+
* session, a false negative loses the human's message.
|
|
94
|
+
*
|
|
95
|
+
* Deliberately narrow: bare "token" or "context" alone would match ordinary
|
|
96
|
+
* chatter, so each pattern pairs the idea with a limit/exceed verb.
|
|
97
|
+
*/
|
|
98
|
+
function isContextOverflowText(text) {
|
|
99
|
+
const t = text.toLowerCase();
|
|
100
|
+
return (/maximum context length/.test(t) ||
|
|
101
|
+
/context[ _-]?(?:length|window)[^.\n]{0,40}(?:exceed|too (?:long|large|big)|limit)/.test(t) ||
|
|
102
|
+
/(?:exceed|over)[^.\n]{0,30}(?:context|token) (?:limit|window|length)/.test(t) ||
|
|
103
|
+
/prompt is too long/.test(t) ||
|
|
104
|
+
/input token count[^.\n]{0,40}exceed/.test(t) ||
|
|
105
|
+
/too many (?:input )?tokens/.test(t) ||
|
|
106
|
+
/(?:reduce|shorten)[^.\n]{0,40}(?:prompt|conversation|messages)/.test(t) ||
|
|
107
|
+
/context_length_exceeded/.test(t));
|
|
108
|
+
}
|
|
109
|
+
/** The budget a conversational session runs under (project override or default). */
|
|
110
|
+
function contextBudget(limit) {
|
|
111
|
+
return typeof limit === 'number' && limit > 0
|
|
112
|
+
? limit
|
|
113
|
+
: session_store_1.DEFAULT_CHAT_CONTEXT_TOKENS;
|
|
114
|
+
}
|
|
88
115
|
/**
|
|
89
116
|
* Does this task still have a question a human has not answered?
|
|
90
117
|
*
|
|
@@ -261,12 +288,54 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
261
288
|
let resumeReq = settings.resume;
|
|
262
289
|
let preassignedId;
|
|
263
290
|
let priorAttempts = 0;
|
|
291
|
+
// Context accounting (2026-12): this record is what makes rotation possible.
|
|
292
|
+
let sessionRecord = store
|
|
293
|
+
? (0, session_store_1.peekHarnessSession)(store.agentId, store.taskId, store.mode)
|
|
294
|
+
: null;
|
|
295
|
+
/** Characters already in the conversation we are about to resume (2026-12). */
|
|
296
|
+
let priorContextChars = sessionRecord?.contextChars ?? 0;
|
|
297
|
+
const budget = contextBudget(settings.contextLimitTokens);
|
|
264
298
|
if (!resumeReq && store && spec && !settings.fresh) {
|
|
265
299
|
const rec = (0, session_store_1.loadHarnessSession)(store.agentId, store.taskId, store.mode, resolved, cwd);
|
|
266
300
|
if (rec) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
301
|
+
// ── CONTEXT BUDGET: rotate instead of overflowing ────────────────
|
|
302
|
+
// A resumed conversation keeps everything it has said. When the estimate
|
|
303
|
+
// reaches the project's budget we FORGET the session and start a new one
|
|
304
|
+
// - the alternative is the model eventually hitting its own window in
|
|
305
|
+
// the middle of a reply, which the human sees as a broken agent.
|
|
306
|
+
if ((0, session_store_1.sessionContextExceeded)(rec, budget)) {
|
|
307
|
+
const estimated = (0, session_store_1.estimateSessionTokens)(rec.contextChars);
|
|
308
|
+
(0, session_store_1.clearHarnessSession)(store.agentId, store.taskId, store.mode);
|
|
309
|
+
log(`[resume] context budget reached (${estimated.toLocaleString()} of ` +
|
|
310
|
+
`${budget.toLocaleString()} estimated tokens) - starting a NEW ` +
|
|
311
|
+
`${adapter.name} session so the conversation continues cleanly`);
|
|
312
|
+
try {
|
|
313
|
+
settings.onSessionRotated?.({
|
|
314
|
+
reason: 'context-budget',
|
|
315
|
+
estimatedTokens: estimated,
|
|
316
|
+
limitTokens: budget,
|
|
317
|
+
previousSessionId: rec.id,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
/* a notice callback must never break the run */
|
|
322
|
+
}
|
|
323
|
+
// Keep the rotation count on the NEW record (written after the run).
|
|
324
|
+
priorContextChars = 0;
|
|
325
|
+
sessionRecord = {
|
|
326
|
+
...(sessionRecord ?? rec),
|
|
327
|
+
rotations: (rec.rotations ?? 0) + 1,
|
|
328
|
+
lastRotatedReason: 'context-budget',
|
|
329
|
+
contextChars: 0,
|
|
330
|
+
turns: 0,
|
|
331
|
+
consecutiveFailures: 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
priorAttempts = rec.attempts ?? 0;
|
|
336
|
+
resumeReq = { id: rec.id, ...(settings.fork ? { fork: true } : {}) };
|
|
337
|
+
log(`[resume] continuing ${adapter.name} session ${shortId(rec.id)}`);
|
|
338
|
+
}
|
|
270
339
|
}
|
|
271
340
|
else if (spec.preassign) {
|
|
272
341
|
preassignedId = (0, resume_1.newHarnessSessionId)();
|
|
@@ -302,45 +371,56 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
302
371
|
log(`[harness] ${adapter.binary} not found - install it first.`, 'stderr');
|
|
303
372
|
return Promise.resolve(1);
|
|
304
373
|
}
|
|
305
|
-
// ── Long prompt →
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
374
|
+
// ── Long prompt → file delivery (decided PER INVOCATION) ───────────────
|
|
375
|
+
//
|
|
376
|
+
// The decision is taken inside buildArgs, from the prompt ACTUALLY being
|
|
377
|
+
// sent and from the whole assembled command line. It used to be taken once,
|
|
378
|
+
// above, and then skipped whenever `promptOverride` was passed - and runOnce
|
|
379
|
+
// ALWAYS passes it (`buildArgs(req, effectivePrompt)`), so the file was
|
|
380
|
+
// written, logged, and never used: the 62 KB brief still went on argv and
|
|
381
|
+
// `spawn` died with `ENAMETOOLONG` before the harness started.
|
|
382
|
+
const promptFiles = [];
|
|
383
|
+
/**
|
|
384
|
+
* Write one prompt file. Never throws: a full/read-only disk must produce a
|
|
385
|
+
* log line, not an exception on a path that is already a last resort.
|
|
386
|
+
*/
|
|
387
|
+
const openPromptFile = (text) => {
|
|
388
|
+
try {
|
|
389
|
+
const file = (0, prompt_file_1.writePromptFile)(text);
|
|
390
|
+
promptFiles.push(file);
|
|
391
|
+
return file;
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
log(`[harness] could not write the prompt file (${msg(err)}) - ` +
|
|
395
|
+
'falling back to argv, which may exceed the OS command-line limit', 'stderr');
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const buildArgs = (req, promptForRun) => {
|
|
313
400
|
const opts = req
|
|
314
401
|
? { ...settings, resume: req }
|
|
315
402
|
: { ...settings, resume: undefined };
|
|
316
|
-
const effectivePrompt =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
const a = adapter.headlessArgs(effectivePrompt, opts, true);
|
|
327
|
-
const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
|
|
328
|
-
'Do not respond until you have read the file. ' +
|
|
329
|
-
'The file contains your complete instructions including project context, tools, and the user message.';
|
|
330
|
-
if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(resolved)) {
|
|
331
|
-
a.unshift('-p', shortInstruction);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
a.push(shortInstruction);
|
|
335
|
-
}
|
|
336
|
-
return a;
|
|
403
|
+
const effectivePrompt = promptForRun ?? prompt;
|
|
404
|
+
const inline = adapter.headlessArgs(effectivePrompt, opts);
|
|
405
|
+
const commandLineChars = (0, prompt_file_1.argvChars)([
|
|
406
|
+
bin.file,
|
|
407
|
+
...(bin.args ?? []),
|
|
408
|
+
...inline,
|
|
409
|
+
]);
|
|
410
|
+
if (!(0, prompt_file_1.shouldUsePromptFile)(adapter, effectivePrompt.length, commandLineChars)) {
|
|
411
|
+
return inline;
|
|
337
412
|
}
|
|
338
|
-
|
|
413
|
+
const file = openPromptFile(effectivePrompt);
|
|
414
|
+
if (!file)
|
|
415
|
+
return inline;
|
|
416
|
+
log(`[harness] prompt too long for argv (${effectivePrompt.length} chars, ` +
|
|
417
|
+
`~${commandLineChars} char command line) — wrote it to ${file} and ` +
|
|
418
|
+
'passed the path instead', 'stderr');
|
|
419
|
+
return (0, prompt_file_1.promptFileArgs)(adapter, adapter.headlessArgs(effectivePrompt, opts, true), file);
|
|
339
420
|
};
|
|
340
421
|
const cleanupPrompt = () => {
|
|
341
|
-
|
|
342
|
-
(0, prompt_file_1.cleanupPromptFile)(
|
|
343
|
-
promptFile = null;
|
|
422
|
+
for (const file of promptFiles.splice(0))
|
|
423
|
+
(0, prompt_file_1.cleanupPromptFile)(file);
|
|
344
424
|
};
|
|
345
425
|
// Shell/subagent lifecycle (2026-10): OpenCode V2 executes `shell` and
|
|
346
426
|
// subagent work on the SHARED SERVICE. Both can outlive the CLI client and
|
|
@@ -375,9 +455,24 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
375
455
|
// project for it. Without this the run's MCP identity config landed in
|
|
376
456
|
// the HUMAN's project, and their interactive session silently acted as
|
|
377
457
|
// the agent (one project = one MCP client).
|
|
458
|
+
//
|
|
459
|
+
// STABLE PER CONVERSATION (2026-12). The scope id used to be the bridge
|
|
460
|
+
// session id, which is new on every wake - so an opencode conversation was
|
|
461
|
+
// re-registered under a different project directory each run, and a resumed
|
|
462
|
+
// harness session came from "another project". When the caller has a
|
|
463
|
+
// conversation scope, the directory is now derived from it, so the same
|
|
464
|
+
// chat/doc/board/plan conversation always resumes in the same directory.
|
|
465
|
+
// The agent id is part of the id: two agents answering in one channel are
|
|
466
|
+
// two conversations and must not share a config directory.
|
|
378
467
|
let scope = null;
|
|
379
468
|
if ((0, run_scope_1.needsRunScope)(settings.cwd)) {
|
|
380
|
-
|
|
469
|
+
const conversationId = store
|
|
470
|
+
? `${store.agentId.slice(0, 8)}-${store.taskId}-${store.mode}`
|
|
471
|
+
: undefined;
|
|
472
|
+
scope = (0, run_scope_1.createRunScope)(settings.cwd, conversationId ??
|
|
473
|
+
settings.sessionId ??
|
|
474
|
+
settings.opencodeSessionId ??
|
|
475
|
+
`run-${Date.now()}`);
|
|
381
476
|
if (scope)
|
|
382
477
|
(0, run_scope_1.excludeRunScopeFromGit)(scope.projectRoot);
|
|
383
478
|
}
|
|
@@ -544,7 +639,13 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
544
639
|
// The relay token was minted before the guard could be checked; it must
|
|
545
640
|
// not linger for a run that never started.
|
|
546
641
|
void revokeRelayKey(runKey);
|
|
547
|
-
return {
|
|
642
|
+
return {
|
|
643
|
+
code: 1,
|
|
644
|
+
resumeFailure: false,
|
|
645
|
+
outputChars: 0,
|
|
646
|
+
contextOverflow: false,
|
|
647
|
+
promptChars: (effectivePrompt ?? prompt).length,
|
|
648
|
+
};
|
|
548
649
|
}
|
|
549
650
|
}
|
|
550
651
|
const args = buildArgs(req, effectivePrompt);
|
|
@@ -580,6 +681,26 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
580
681
|
* kill fires only when ALL of them have been quiet for the budget.
|
|
581
682
|
*/
|
|
582
683
|
const activity = new activity_1.ActivityClock();
|
|
684
|
+
/**
|
|
685
|
+
* TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
|
|
686
|
+
*
|
|
687
|
+
* The activity clock answers "is anything happening ANYWHERE"; this one
|
|
688
|
+
* answers "is a model turn OPEN with no output", which is the exact shape
|
|
689
|
+
* of a hung provider call (a `step_start` and then nothing for 19 minutes
|
|
690
|
+
* was the Aga Autos chat incident). Ladder: warn -> interrupt -> kill.
|
|
691
|
+
* The kill reuses exit 124 so the conversation is kept for a retry.
|
|
692
|
+
*/
|
|
693
|
+
const turn = new turn_watch_1.TurnWatch();
|
|
694
|
+
const turnBudget = (0, turn_watch_1.turnBudgets)(settings.sessionMode ?? store?.mode, settings.timeoutMin ?? 60);
|
|
695
|
+
const turnWatchOff = /^(0|off|false|no)$/i.test(process.env.TEAMSHARE_TURN_WATCH ?? '');
|
|
696
|
+
/** One warning / interrupt per step id, so a recovered run is not re-hit. */
|
|
697
|
+
let turnWarnedStep = null;
|
|
698
|
+
let turnInterruptedStep = null;
|
|
699
|
+
let turnInterruptedAt = null;
|
|
700
|
+
/** Hard guard inputs - real work in flight is never a stall. */
|
|
701
|
+
let shellRunningNow = false;
|
|
702
|
+
let relayInFlight = 0;
|
|
703
|
+
let lastSubagentAt = 0;
|
|
583
704
|
const STALL_WARN_MS = Math.max(1, Number(process.env.TEAMSHARE_STALL_WARN_MIN) || 5) * 60_000;
|
|
584
705
|
const HEARTBEAT_MS = 5 * 60_000;
|
|
585
706
|
/**
|
|
@@ -716,6 +837,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
716
837
|
// A NEW subagent message is real work even though the parent
|
|
717
838
|
// CLI printed nothing for it (the line below is OUR log).
|
|
718
839
|
activity.note('subagent', `ses ${shortId(child.id)}`);
|
|
840
|
+
// The turn watchdog's "active subagent" guard: a child that
|
|
841
|
+
// is producing output must never be killed as a stalled turn.
|
|
842
|
+
lastSubagentAt = Date.now();
|
|
719
843
|
startIdleTimer();
|
|
720
844
|
if (Date.now() - lastHeartbeatAt >= HEARTBEAT_MS) {
|
|
721
845
|
lastHeartbeatAt = Date.now();
|
|
@@ -746,15 +870,33 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
746
870
|
subagentTimer = null;
|
|
747
871
|
}
|
|
748
872
|
};
|
|
873
|
+
// Context accounting for THIS invocation (2026-12).
|
|
874
|
+
let outputChars = 0;
|
|
875
|
+
let contextOverflow = false;
|
|
749
876
|
// Line sinks reassemble across pipe chunks: Windows reads cap at 64KB,
|
|
750
877
|
// and a large NDJSON event split mid-line used to fail JSON.parse and
|
|
751
878
|
// print RAW into the console, flooding/breaking the stream (gotchas.md).
|
|
752
879
|
const handleLine = (line, level) => {
|
|
753
880
|
settings.onRawLine?.(line);
|
|
881
|
+
// Context accounting (2026-12): every emitted character counts toward
|
|
882
|
+
// this conversation's budget, and the harness's own "too long" error is
|
|
883
|
+
// how we learn the model's window is smaller than the project setting.
|
|
884
|
+
outputChars += line.length + 1;
|
|
885
|
+
if (!contextOverflow && isContextOverflowText(line)) {
|
|
886
|
+
contextOverflow = true;
|
|
887
|
+
}
|
|
754
888
|
// A transcript is a log, not a vault: a key the model echoes must not
|
|
755
889
|
// reach the console or the diagnostic sidecar (2026-10-25).
|
|
756
890
|
const safeLine = (0, diagnostics_1.scrubSecrets)(line);
|
|
757
891
|
rawCapture(safeLine);
|
|
892
|
+
// TURN WATCHDOG (2026-11): feed the open-step clock. A `step_start`
|
|
893
|
+
// that never reaches `step_finish` is a hung model turn, not work -
|
|
894
|
+
// see `turn-watch.ts` for the incident this exists for.
|
|
895
|
+
if (!turnWatchOff && resolved === 'opencode') {
|
|
896
|
+
const turnEvent = (0, turn_watch_1.extractTurnEvent)(line);
|
|
897
|
+
if (turnEvent)
|
|
898
|
+
turn.note(turnEvent);
|
|
899
|
+
}
|
|
758
900
|
// SCOPE GUARD (2026-10-25): see `tripScopeGuard`.
|
|
759
901
|
tripScopeGuard(safeLine);
|
|
760
902
|
// Track background shells the service owns (they outlive this CLI).
|
|
@@ -813,7 +955,14 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
813
955
|
if (killTimer)
|
|
814
956
|
clearTimeout(killTimer);
|
|
815
957
|
cleanMcp();
|
|
816
|
-
resolve({
|
|
958
|
+
resolve({
|
|
959
|
+
code: scopeAborted ? 1 : code,
|
|
960
|
+
resumeFailure,
|
|
961
|
+
capturedId,
|
|
962
|
+
outputChars,
|
|
963
|
+
contextOverflow,
|
|
964
|
+
promptChars: (effectivePrompt ?? prompt).length,
|
|
965
|
+
});
|
|
817
966
|
};
|
|
818
967
|
const startIdleTimer = () => {
|
|
819
968
|
if (killTimer)
|
|
@@ -887,6 +1036,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
887
1036
|
if (settled)
|
|
888
1037
|
return;
|
|
889
1038
|
const now = Date.now();
|
|
1039
|
+
// Turn-watchdog guards are re-derived every tick: a shell that
|
|
1040
|
+
// exited must stop shielding a stalled turn, and vice versa.
|
|
1041
|
+
shellRunningNow = false;
|
|
890
1042
|
// 1) Relayed MCP/tool activity. Give up after 3 misses (no daemon,
|
|
891
1043
|
// or a run wired straight to the API) instead of polling forever.
|
|
892
1044
|
if (runKey && relayProbeMisses < 3) {
|
|
@@ -896,6 +1048,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
896
1048
|
}
|
|
897
1049
|
else {
|
|
898
1050
|
relayProbeMisses = 0;
|
|
1051
|
+
// A call that has not answered yet is work in flight, so the
|
|
1052
|
+
// turn watchdog must not treat the silence as a hung model.
|
|
1053
|
+
relayInFlight = signal.inFlight;
|
|
899
1054
|
if (signal.lastActivityAt > lastRelayActivityAt) {
|
|
900
1055
|
lastRelayActivityAt = signal.lastActivityAt;
|
|
901
1056
|
activity.note('mcp', signal.lastTool ?? 'tool call');
|
|
@@ -941,6 +1096,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
941
1096
|
continue;
|
|
942
1097
|
}
|
|
943
1098
|
activity.note('shell', shell.command ?? shell.id);
|
|
1099
|
+
// HARD GUARD: a running command is real work. The turn
|
|
1100
|
+
// watchdog must never kill a run the agent is waiting on.
|
|
1101
|
+
shellRunningNow = true;
|
|
944
1102
|
startIdleTimer();
|
|
945
1103
|
heartbeat(`background shell ${shell.command ?? shell.id} is still running`);
|
|
946
1104
|
break;
|
|
@@ -963,6 +1121,110 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
963
1121
|
? 'no output at all'
|
|
964
1122
|
: `no stdout for ${Math.max(1, stdoutMin)} min`} - last activity: ${(0, activity_1.activityActor)(latest)}. Not dead unless the ${Math.max(1, Math.round(timeoutMs / 60_000))} min idle timeout is reached.`, 'stdout');
|
|
965
1123
|
}
|
|
1124
|
+
// 4) TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
|
|
1125
|
+
//
|
|
1126
|
+
// The activity clock above is deliberately patient: it cannot
|
|
1127
|
+
// tell a hung provider call from a long tool call, so it waits the
|
|
1128
|
+
// full idle budget. A model turn that OPENED and never produced a
|
|
1129
|
+
// token is not ambiguous, though - it is the Aga Autos incident
|
|
1130
|
+
// (a `step_start`, then 19 silent minutes). Ladder: warn ->
|
|
1131
|
+
// interrupt -> kill, and the kill reuses exit 124 so the
|
|
1132
|
+
// conversation is kept for a retry instead of starting over.
|
|
1133
|
+
if (!turnWatchOff && !settled) {
|
|
1134
|
+
const turnSnap = turn.snapshot();
|
|
1135
|
+
// A DIFFERENT step means the session recovered (or moved on):
|
|
1136
|
+
// clear the interrupt latch so the next stall is judged fresh.
|
|
1137
|
+
if (turnInterruptedStep !== null &&
|
|
1138
|
+
turnSnap.stepId !== turnInterruptedStep) {
|
|
1139
|
+
turnInterruptedStep = null;
|
|
1140
|
+
turnInterruptedAt = null;
|
|
1141
|
+
}
|
|
1142
|
+
const verdict = (0, turn_watch_1.decideTurnAction)({
|
|
1143
|
+
snapshot: turnSnap,
|
|
1144
|
+
now,
|
|
1145
|
+
budgets: turnBudget,
|
|
1146
|
+
inFlightMcp: relayInFlight,
|
|
1147
|
+
shellRunning: shellRunningNow,
|
|
1148
|
+
subagentActive: lastSubagentAt > 0 &&
|
|
1149
|
+
now - lastSubagentAt < turnBudget.subagentGraceMs,
|
|
1150
|
+
interruptedAt: turnInterruptedAt,
|
|
1151
|
+
});
|
|
1152
|
+
const stepKey = turnSnap.stepId ?? `step-${turnSnap.stepOpenedAt ?? 0}`;
|
|
1153
|
+
if (verdict.action === 'warn' && turnWarnedStep !== stepKey) {
|
|
1154
|
+
turnWarnedStep = stepKey;
|
|
1155
|
+
log(`[harness] ${adapter.name} model turn open for ` +
|
|
1156
|
+
`${Math.max(1, Math.round(verdict.openForMs / 60_000))} min, ` +
|
|
1157
|
+
`silent for ${Math.max(1, Math.round(verdict.silentMs / 60_000))} min ` +
|
|
1158
|
+
`(${verdict.reason}) - interrupting at ` +
|
|
1159
|
+
`${Math.round(turnBudget.interruptMs / 60_000)} min, killing at ` +
|
|
1160
|
+
`${Math.round(turnBudget.killMs / 60_000)} min unless it recovers`, 'stderr');
|
|
1161
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1162
|
+
kind: 'harness_failure',
|
|
1163
|
+
severity: 'warning',
|
|
1164
|
+
message: `model turn stalled (${verdict.reason})`,
|
|
1165
|
+
errorCode: 'HARNESS_TURN_STALL',
|
|
1166
|
+
agentId: store?.agentId,
|
|
1167
|
+
context: {
|
|
1168
|
+
harness: resolved,
|
|
1169
|
+
mode: settings.sessionMode ?? null,
|
|
1170
|
+
runKey: runKey ?? null,
|
|
1171
|
+
openForMs: verdict.openForMs,
|
|
1172
|
+
silentMs: verdict.silentMs,
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (verdict.action === 'interrupt' &&
|
|
1177
|
+
turnInterruptedStep !== stepKey) {
|
|
1178
|
+
turnInterruptedStep = stepKey;
|
|
1179
|
+
turnInterruptedAt = now;
|
|
1180
|
+
log(`[harness] ${adapter.name} model turn stalled ` +
|
|
1181
|
+
`(${verdict.reason}) - interrupting the service session so it ` +
|
|
1182
|
+
'can continue; no work is lost', 'stderr');
|
|
1183
|
+
if (capturedId)
|
|
1184
|
+
await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
|
|
1185
|
+
}
|
|
1186
|
+
if (verdict.action === 'kill') {
|
|
1187
|
+
// Same contract as the idle kill: OUR decision, not a broken
|
|
1188
|
+
// conversation, so the session is kept for a retry.
|
|
1189
|
+
idleKilled = true;
|
|
1190
|
+
forcedExitCode = 124;
|
|
1191
|
+
log(`[harness] ${adapter.name} model turn stalled ` +
|
|
1192
|
+
`(${verdict.reason}) - killing the run; the conversation is ` +
|
|
1193
|
+
'kept so the next attempt continues it', 'stderr');
|
|
1194
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1195
|
+
kind: 'harness_failure',
|
|
1196
|
+
severity: 'error',
|
|
1197
|
+
message: `model turn stalled and was killed (${verdict.reason})`,
|
|
1198
|
+
errorCode: 'HARNESS_TURN_STALLED',
|
|
1199
|
+
agentId: store?.agentId,
|
|
1200
|
+
context: {
|
|
1201
|
+
harness: resolved,
|
|
1202
|
+
mode: settings.sessionMode ?? null,
|
|
1203
|
+
runKey: runKey ?? null,
|
|
1204
|
+
openForMs: verdict.openForMs,
|
|
1205
|
+
silentMs: verdict.silentMs,
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
// Interrupt FIRST: on OpenCode V2 the shared service owns the
|
|
1209
|
+
// turn, so killing the CLI alone leaves a silent session.
|
|
1210
|
+
if (capturedId)
|
|
1211
|
+
await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
|
|
1212
|
+
try {
|
|
1213
|
+
killTree(child.pid);
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
/* ignore */
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
child.kill();
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
/* ignore */
|
|
1223
|
+
}
|
|
1224
|
+
done(124);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
966
1228
|
})();
|
|
967
1229
|
}, 30_000);
|
|
968
1230
|
activityTimer.unref?.();
|
|
@@ -1018,11 +1280,33 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1018
1280
|
done(forcedExitCode ?? code ?? 1);
|
|
1019
1281
|
});
|
|
1020
1282
|
}).catch((err) => {
|
|
1021
|
-
// `spawn()` can throw SYNCHRONOUSLY (bad argv, ENOENT
|
|
1022
|
-
// event), which means the `close` handler above never
|
|
1023
|
-
// config here too - it holds the agent API key - and
|
|
1024
|
-
// reach the caller.
|
|
1283
|
+
// `spawn()` can throw SYNCHRONOUSLY (bad argv, ENAMETOOLONG, ENOENT
|
|
1284
|
+
// before the `error` event), which means the `close` handler above never
|
|
1285
|
+
// runs. Clean the MCP config here too - it holds the agent API key - and
|
|
1286
|
+
// let the rejection reach the caller.
|
|
1025
1287
|
cleanMcp();
|
|
1288
|
+
// A raw `spawn ENAMETOOLONG` reaches the task as if the binary were
|
|
1289
|
+
// broken. Name the real cause and the fix instead.
|
|
1290
|
+
if (err?.code === 'ENAMETOOLONG') {
|
|
1291
|
+
const detail = `the ${adapter.name} command line was too long to start ` +
|
|
1292
|
+
`(prompt ${prompt.length} chars, OS limit ~32k). Shorten the task ` +
|
|
1293
|
+
'brief / project context, or use a harness that supports prompt files.';
|
|
1294
|
+
log(`[harness] ${detail}`, 'stderr');
|
|
1295
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1296
|
+
kind: 'harness_failure',
|
|
1297
|
+
severity: 'error',
|
|
1298
|
+
message: detail,
|
|
1299
|
+
errorCode: 'HARNESS_ENAMETOOLONG',
|
|
1300
|
+
agentId: settings.sessionStore?.agentId,
|
|
1301
|
+
context: {
|
|
1302
|
+
harness: adapter.name,
|
|
1303
|
+
promptChars: prompt.length,
|
|
1304
|
+
mode: settings.sessionMode ?? null,
|
|
1305
|
+
taskId: settings.sessionStore?.taskId ?? null,
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
throw new Error(`spawn ENAMETOOLONG: ${detail}`);
|
|
1309
|
+
}
|
|
1026
1310
|
throw err;
|
|
1027
1311
|
});
|
|
1028
1312
|
};
|
|
@@ -1037,9 +1321,35 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1037
1321
|
// a pre-assigned fresh run that fails just clears below.
|
|
1038
1322
|
const wasContinuation = Boolean(resumeReq?.id || resumeReq?.latest);
|
|
1039
1323
|
if (wasContinuation && (result.resumeFailure || result.code !== 0)) {
|
|
1040
|
-
|
|
1324
|
+
// ── CONTEXT OVERFLOW IS A ROTATION, NOT A FAILURE (2026-12) ────────
|
|
1325
|
+
//
|
|
1326
|
+
// The project budget was below the model's real window (or the estimate
|
|
1327
|
+
// drifted low), so the model itself refused the prompt. Forgetting the
|
|
1328
|
+
// session and retrying once fresh is exactly what a human wants: the
|
|
1329
|
+
// reply still happens, and the next turn starts a clean conversation.
|
|
1330
|
+
if (result.contextOverflow) {
|
|
1331
|
+
const estimated = (0, session_store_1.estimateSessionTokens)(sessionRecord?.contextChars ?? 0);
|
|
1332
|
+
log(`[resume] the model rejected this conversation as too long - ` +
|
|
1333
|
+
`starting a NEW session for this turn`, 'stderr');
|
|
1334
|
+
try {
|
|
1335
|
+
settings.onSessionRotated?.({
|
|
1336
|
+
reason: 'context-overflow',
|
|
1337
|
+
estimatedTokens: estimated,
|
|
1338
|
+
limitTokens: budget,
|
|
1339
|
+
previousSessionId: resumeReq?.id,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
/* a notice callback must never break the run */
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
log('[resume] continuation failed - starting a fresh session', 'stderr');
|
|
1348
|
+
}
|
|
1041
1349
|
if (store)
|
|
1042
1350
|
(0, session_store_1.clearHarnessSession)(store.agentId, store.taskId, store.mode);
|
|
1351
|
+
// The conversation restarted: its context estimate restarts with it.
|
|
1352
|
+
priorContextChars = 0;
|
|
1043
1353
|
preassignedId = spec?.preassign ? (0, resume_1.newHarnessSessionId)() : undefined;
|
|
1044
1354
|
const freshReq = preassignedId
|
|
1045
1355
|
? { preassignId: preassignedId }
|
|
@@ -1237,11 +1547,38 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1237
1547
|
// "the session is poisoned". Discarding it made the retry repeat hours
|
|
1238
1548
|
// of work from zero. Real harness failures still forget the session.
|
|
1239
1549
|
const keepForIdleResume = failedRun && idleKilled && Boolean(finalReq?.id ?? result.capturedId);
|
|
1240
|
-
|
|
1550
|
+
// ── CONVERSATIONAL MODES KEEP THEIR CONVERSATION (2026-12) ──────────
|
|
1551
|
+
//
|
|
1552
|
+
// A chat / doc / board reply is ONE TURN of a long conversation. Clearing
|
|
1553
|
+
// the session on every non-zero exit means a single transient provider
|
|
1554
|
+
// error silently restarts the thread from zero - the human sees an agent
|
|
1555
|
+
// that "forgot everything". The agent-loop rule (a failed attempt's
|
|
1556
|
+
// session is consumed) exists because a retry must not resume a poisoned
|
|
1557
|
+
// planning session; a conversation is different, so it gets one retry and
|
|
1558
|
+
// is replaced only after the SECOND consecutive failure.
|
|
1559
|
+
//
|
|
1560
|
+
// `failedRun` is REQUIRED here (2026-11): without it a SUCCESSFUL turn
|
|
1561
|
+
// took this branch too, and logged "run failed (attempt 0) - KEEPING the
|
|
1562
|
+
// conversation" on every single reply. Live case: a chat mention answered
|
|
1563
|
+
// the question, posted the message through MCP, exited 0 - and the
|
|
1564
|
+
// console still said the run failed.
|
|
1565
|
+
const conversational = session_store_1.CONVERSATIONAL_MODES.has(store.mode);
|
|
1566
|
+
const failureCount = failedRun
|
|
1567
|
+
? Math.max(1, (0, session_store_1.noteHarnessSessionFailure)(store.agentId, store.taskId, store.mode, `${adapter.name} exited with code ${finalCode}`))
|
|
1568
|
+
: 0;
|
|
1569
|
+
const keepForConversation = failedRun && conversational && failureCount < 2;
|
|
1570
|
+
if (failedRun &&
|
|
1571
|
+
!questionStillOpen &&
|
|
1572
|
+
!keepForIdleResume &&
|
|
1573
|
+
!keepForConversation) {
|
|
1241
1574
|
(0, session_store_1.clearHarnessSession)(store.agentId, store.taskId, store.mode);
|
|
1242
1575
|
log(`[resume] run failed - forgetting session (next attempt starts fresh)`);
|
|
1243
1576
|
}
|
|
1244
1577
|
else if (spec.capture !== 'none') {
|
|
1578
|
+
if (keepForConversation) {
|
|
1579
|
+
log(`[resume] run failed (attempt ${failureCount} of 2) - keeping the ` +
|
|
1580
|
+
`conversation so the next message continues instead of starting over`, 'stderr');
|
|
1581
|
+
}
|
|
1245
1582
|
if (questionStillOpen) {
|
|
1246
1583
|
log(`[resume] run failed but a question is still open - keeping the ` +
|
|
1247
1584
|
`session so the human's answer can resume it`);
|
|
@@ -1270,6 +1607,18 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1270
1607
|
createdAt: now,
|
|
1271
1608
|
lastUsedAt: now,
|
|
1272
1609
|
attempts: priorAttempts + 1,
|
|
1610
|
+
// Context accounting (2026-12): what this turn ADDED to the
|
|
1611
|
+
// conversation. `priorContextChars` is 0 whenever the session was
|
|
1612
|
+
// rotated/replaced, so a new conversation starts counting clean.
|
|
1613
|
+
contextChars: priorContextChars + result.promptChars + result.outputChars,
|
|
1614
|
+
turns: (sessionRecord?.turns ?? 0) + 1,
|
|
1615
|
+
consecutiveFailures: 0,
|
|
1616
|
+
...(sessionRecord?.rotations
|
|
1617
|
+
? { rotations: sessionRecord.rotations }
|
|
1618
|
+
: {}),
|
|
1619
|
+
...(sessionRecord?.lastRotatedReason
|
|
1620
|
+
? { lastRotatedReason: sessionRecord.lastRotatedReason }
|
|
1621
|
+
: {}),
|
|
1273
1622
|
});
|
|
1274
1623
|
log(`[resume] recorded ${adapter.name} session ${shortId(id)} for future runs`);
|
|
1275
1624
|
// ONE lightweight report per run (never per line) so the cloud can
|
|
@@ -1326,9 +1675,11 @@ async function readRelayActivity(runKey) {
|
|
|
1326
1675
|
const at = Number(body.lastActivityAt);
|
|
1327
1676
|
if (!Number.isFinite(at) || at <= 0)
|
|
1328
1677
|
return null;
|
|
1678
|
+
const inFlight = Number(body.inFlight);
|
|
1329
1679
|
return {
|
|
1330
1680
|
lastActivityAt: at,
|
|
1331
1681
|
lastTool: typeof body.lastTool === 'string' ? body.lastTool : null,
|
|
1682
|
+
inFlight: Number.isFinite(inFlight) && inFlight > 0 ? inFlight : 0,
|
|
1332
1683
|
};
|
|
1333
1684
|
}
|
|
1334
1685
|
catch {
|
|
@@ -1392,28 +1743,33 @@ function spawnThroughPty(binary, prompt, log, settings, name) {
|
|
|
1392
1743
|
const adapterLike = (0, harness_adapter_1.getAdapter)(name);
|
|
1393
1744
|
let promptFile = null;
|
|
1394
1745
|
let args;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1746
|
+
const inlineArgs = adapterLike.headlessArgs(prompt, settings);
|
|
1747
|
+
const commandLineChars = (0, prompt_file_1.argvChars)([
|
|
1748
|
+
bin.file,
|
|
1749
|
+
...(bin.args ?? []),
|
|
1750
|
+
...inlineArgs,
|
|
1751
|
+
]);
|
|
1752
|
+
if ((0, prompt_file_1.shouldUsePromptFile)(adapterLike, prompt.length, commandLineChars)) {
|
|
1753
|
+
try {
|
|
1754
|
+
promptFile = (0, prompt_file_1.writePromptFile)(prompt);
|
|
1755
|
+
}
|
|
1756
|
+
catch (err) {
|
|
1757
|
+
promptFile = null;
|
|
1758
|
+
log(`[harness] could not write the prompt file (${msg(err)}) - ` +
|
|
1759
|
+
'falling back to argv, which may exceed the OS command-line limit', 'stderr');
|
|
1760
|
+
}
|
|
1761
|
+
if (promptFile) {
|
|
1762
|
+
log(`[harness] prompt too long for argv (${prompt.length} chars, ` +
|
|
1763
|
+
`~${commandLineChars} char command line) — wrote it to ` +
|
|
1764
|
+
`${promptFile} and passed the path instead`, 'stderr');
|
|
1765
|
+
args = (0, prompt_file_1.promptFileArgs)(adapterLike, adapterLike.headlessArgs(prompt, settings, true), promptFile);
|
|
1401
1766
|
}
|
|
1402
1767
|
else {
|
|
1403
|
-
args =
|
|
1404
|
-
const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
|
|
1405
|
-
'Do not respond until you have read the file. ' +
|
|
1406
|
-
'The file contains your complete instructions including project context, tools, and the user message.';
|
|
1407
|
-
if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(name)) {
|
|
1408
|
-
args.unshift('-p', shortInstruction);
|
|
1409
|
-
}
|
|
1410
|
-
else {
|
|
1411
|
-
args.push(shortInstruction);
|
|
1412
|
-
}
|
|
1768
|
+
args = inlineArgs;
|
|
1413
1769
|
}
|
|
1414
1770
|
}
|
|
1415
1771
|
else {
|
|
1416
|
-
args =
|
|
1772
|
+
args = inlineArgs;
|
|
1417
1773
|
}
|
|
1418
1774
|
let mcpOverride = null;
|
|
1419
1775
|
if (adapterLike.supportsMcp &&
|