teamshare-bridge 0.28.0 → 0.28.3
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 +44 -5
- package/dist/bridge/daemon.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/cli/commands/research.js +3 -3
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/lib/browser/browse-harness.js +31 -6
- package/dist/lib/browser/browse-harness.js.map +1 -1
- package/dist/lib/chat/chat-reply.js +16 -0
- package/dist/lib/chat/chat-reply.js.map +1 -1
- package/dist/lib/chat/doc-reply.js +31 -16
- package/dist/lib/chat/doc-reply.js.map +1 -1
- package/dist/lib/harness/harness-adapter.js +27 -1
- 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/relay-key.d.ts +14 -8
- package/dist/lib/harness/relay-key.js +19 -13
- package/dist/lib/harness/relay-key.js.map +1 -1
- package/dist/lib/harness/run-scope.d.ts +16 -0
- package/dist/lib/harness/run-scope.js +154 -19
- package/dist/lib/harness/run-scope.js.map +1 -1
- package/dist/lib/harness/session-store.d.ts +10 -0
- package/dist/lib/harness/session-store.js +17 -0
- package/dist/lib/harness/session-store.js.map +1 -1
- package/dist/lib/harness/spawn.js +266 -69
- 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/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/package.json +1 -1
|
@@ -30,6 +30,7 @@ const line_assembler_1 = require("./line-assembler");
|
|
|
30
30
|
const opencode_outcome_1 = require("./opencode-outcome");
|
|
31
31
|
const opencode_control_1 = require("./opencode-control");
|
|
32
32
|
const activity_1 = require("./activity");
|
|
33
|
+
const turn_watch_1 = require("./turn-watch");
|
|
33
34
|
const agy_pty_1 = require("./agy-pty");
|
|
34
35
|
const prompt_file_1 = require("./prompt-file");
|
|
35
36
|
const diagnostics_1 = require("../diagnostics");
|
|
@@ -38,12 +39,6 @@ const relay_key_1 = require("./relay-key");
|
|
|
38
39
|
const resume_1 = require("./resume");
|
|
39
40
|
const opencode_session_1 = require("../llm/opencode-session");
|
|
40
41
|
const session_store_1 = require("./session-store");
|
|
41
|
-
/**
|
|
42
|
-
* Maximum prompt length before we switch to temp-file delivery.
|
|
43
|
-
* Windows argv limit is 32,767 chars; we leave headroom for binary path,
|
|
44
|
-
* MCP config, and other flags.
|
|
45
|
-
*/
|
|
46
|
-
const PROMPT_FILE_THRESHOLD = 28_000;
|
|
47
42
|
/** Real task ids are UUIDs; synthetic keys (chat-<projectId>) are not reported. */
|
|
48
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;
|
|
49
44
|
let warnedNoPty = false;
|
|
@@ -376,45 +371,56 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
376
371
|
log(`[harness] ${adapter.binary} not found - install it first.`, 'stderr');
|
|
377
372
|
return Promise.resolve(1);
|
|
378
373
|
}
|
|
379
|
-
// ── Long prompt →
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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) => {
|
|
387
400
|
const opts = req
|
|
388
401
|
? { ...settings, resume: req }
|
|
389
402
|
: { ...settings, resume: undefined };
|
|
390
|
-
const effectivePrompt =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
const a = adapter.headlessArgs(effectivePrompt, opts, true);
|
|
401
|
-
const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
|
|
402
|
-
'Do not respond until you have read the file. ' +
|
|
403
|
-
'The file contains your complete instructions including project context, tools, and the user message.';
|
|
404
|
-
if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(resolved)) {
|
|
405
|
-
a.unshift('-p', shortInstruction);
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
a.push(shortInstruction);
|
|
409
|
-
}
|
|
410
|
-
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;
|
|
411
412
|
}
|
|
412
|
-
|
|
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);
|
|
413
420
|
};
|
|
414
421
|
const cleanupPrompt = () => {
|
|
415
|
-
|
|
416
|
-
(0, prompt_file_1.cleanupPromptFile)(
|
|
417
|
-
promptFile = null;
|
|
422
|
+
for (const file of promptFiles.splice(0))
|
|
423
|
+
(0, prompt_file_1.cleanupPromptFile)(file);
|
|
418
424
|
};
|
|
419
425
|
// Shell/subagent lifecycle (2026-10): OpenCode V2 executes `shell` and
|
|
420
426
|
// subagent work on the SHARED SERVICE. Both can outlive the CLI client and
|
|
@@ -469,6 +475,17 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
469
475
|
`run-${Date.now()}`);
|
|
470
476
|
if (scope)
|
|
471
477
|
(0, run_scope_1.excludeRunScopeFromGit)(scope.projectRoot);
|
|
478
|
+
else {
|
|
479
|
+
// Degraded, and it must never be silent: without a scope the run's
|
|
480
|
+
// identity config lands in the SHARED folder, where a concurrent run
|
|
481
|
+
// and the human's own session overwrite it (see
|
|
482
|
+
// docs/agent-guides/mcp-identity.md). The run still starts - refusing
|
|
483
|
+
// would block every run on a read-only folder - but the operator can
|
|
484
|
+
// see why an identity went wrong.
|
|
485
|
+
log('[scope] could not create a private run scope - this run shares the ' +
|
|
486
|
+
"folder's own opencode config (identity may be taken from the " +
|
|
487
|
+
'human). Fix: check permissions on <folder>/.teamshare-runs.', 'stderr');
|
|
488
|
+
}
|
|
472
489
|
}
|
|
473
490
|
const runCwd = scope?.dir ?? settings.cwd;
|
|
474
491
|
lastRunCwd = runCwd ?? lastRunCwd;
|
|
@@ -516,7 +533,13 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
516
533
|
// because the OpenCode service keeps the MCP connection it opened with it.
|
|
517
534
|
// Derived from task + mode so a resume, a retry or a question pause reuses
|
|
518
535
|
// the SAME token instead of 401ing the cached connection.
|
|
519
|
-
|
|
536
|
+
//
|
|
537
|
+
// The AGENT is part of the key (2026-11): a chat run's "task" is the
|
|
538
|
+
// CONVERSATION (`chat-<projectId>-<channelId>`), which two agents answering
|
|
539
|
+
// in one channel share. Without it, agent B's spawn dropped agent A's live
|
|
540
|
+
// token (a different key must never inherit a session's token) and the two
|
|
541
|
+
// revoked each other in a loop - the "only when multiple agents run" bug.
|
|
542
|
+
const runKey = (0, relay_key_1.relayKeyFor)(settings.sessionStore?.taskId ?? null, settings.sessionStore?.mode ?? settings.sessionMode ?? null, settings.sessionId, settings.sessionStore?.agentId);
|
|
520
543
|
if (adapter.supportsMcp && settings.agentKey && adapter.writeMcpConfig) {
|
|
521
544
|
const relay = await resolveMcpRelay(settings.agentKey, settings.sessionStore?.agentId, runKey);
|
|
522
545
|
mcpOverride = adapter.writeMcpConfig(settings.agentKey, settings.apiBaseUrl ?? process.env.TEAMSHARE_API_URL ?? api_1.DEFAULT_API_URL, settings.sessionMode, settings, relay?.url, relay?.token);
|
|
@@ -675,6 +698,26 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
675
698
|
* kill fires only when ALL of them have been quiet for the budget.
|
|
676
699
|
*/
|
|
677
700
|
const activity = new activity_1.ActivityClock();
|
|
701
|
+
/**
|
|
702
|
+
* TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
|
|
703
|
+
*
|
|
704
|
+
* The activity clock answers "is anything happening ANYWHERE"; this one
|
|
705
|
+
* answers "is a model turn OPEN with no output", which is the exact shape
|
|
706
|
+
* of a hung provider call (a `step_start` and then nothing for 19 minutes
|
|
707
|
+
* was the Aga Autos chat incident). Ladder: warn -> interrupt -> kill.
|
|
708
|
+
* The kill reuses exit 124 so the conversation is kept for a retry.
|
|
709
|
+
*/
|
|
710
|
+
const turn = new turn_watch_1.TurnWatch();
|
|
711
|
+
const turnBudget = (0, turn_watch_1.turnBudgets)(settings.sessionMode ?? store?.mode, settings.timeoutMin ?? 60);
|
|
712
|
+
const turnWatchOff = /^(0|off|false|no)$/i.test(process.env.TEAMSHARE_TURN_WATCH ?? '');
|
|
713
|
+
/** One warning / interrupt per step id, so a recovered run is not re-hit. */
|
|
714
|
+
let turnWarnedStep = null;
|
|
715
|
+
let turnInterruptedStep = null;
|
|
716
|
+
let turnInterruptedAt = null;
|
|
717
|
+
/** Hard guard inputs - real work in flight is never a stall. */
|
|
718
|
+
let shellRunningNow = false;
|
|
719
|
+
let relayInFlight = 0;
|
|
720
|
+
let lastSubagentAt = 0;
|
|
678
721
|
const STALL_WARN_MS = Math.max(1, Number(process.env.TEAMSHARE_STALL_WARN_MIN) || 5) * 60_000;
|
|
679
722
|
const HEARTBEAT_MS = 5 * 60_000;
|
|
680
723
|
/**
|
|
@@ -811,6 +854,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
811
854
|
// A NEW subagent message is real work even though the parent
|
|
812
855
|
// CLI printed nothing for it (the line below is OUR log).
|
|
813
856
|
activity.note('subagent', `ses ${shortId(child.id)}`);
|
|
857
|
+
// The turn watchdog's "active subagent" guard: a child that
|
|
858
|
+
// is producing output must never be killed as a stalled turn.
|
|
859
|
+
lastSubagentAt = Date.now();
|
|
814
860
|
startIdleTimer();
|
|
815
861
|
if (Date.now() - lastHeartbeatAt >= HEARTBEAT_MS) {
|
|
816
862
|
lastHeartbeatAt = Date.now();
|
|
@@ -860,6 +906,14 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
860
906
|
// reach the console or the diagnostic sidecar (2026-10-25).
|
|
861
907
|
const safeLine = (0, diagnostics_1.scrubSecrets)(line);
|
|
862
908
|
rawCapture(safeLine);
|
|
909
|
+
// TURN WATCHDOG (2026-11): feed the open-step clock. A `step_start`
|
|
910
|
+
// that never reaches `step_finish` is a hung model turn, not work -
|
|
911
|
+
// see `turn-watch.ts` for the incident this exists for.
|
|
912
|
+
if (!turnWatchOff && resolved === 'opencode') {
|
|
913
|
+
const turnEvent = (0, turn_watch_1.extractTurnEvent)(line);
|
|
914
|
+
if (turnEvent)
|
|
915
|
+
turn.note(turnEvent);
|
|
916
|
+
}
|
|
863
917
|
// SCOPE GUARD (2026-10-25): see `tripScopeGuard`.
|
|
864
918
|
tripScopeGuard(safeLine);
|
|
865
919
|
// Track background shells the service owns (they outlive this CLI).
|
|
@@ -999,6 +1053,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
999
1053
|
if (settled)
|
|
1000
1054
|
return;
|
|
1001
1055
|
const now = Date.now();
|
|
1056
|
+
// Turn-watchdog guards are re-derived every tick: a shell that
|
|
1057
|
+
// exited must stop shielding a stalled turn, and vice versa.
|
|
1058
|
+
shellRunningNow = false;
|
|
1002
1059
|
// 1) Relayed MCP/tool activity. Give up after 3 misses (no daemon,
|
|
1003
1060
|
// or a run wired straight to the API) instead of polling forever.
|
|
1004
1061
|
if (runKey && relayProbeMisses < 3) {
|
|
@@ -1008,6 +1065,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1008
1065
|
}
|
|
1009
1066
|
else {
|
|
1010
1067
|
relayProbeMisses = 0;
|
|
1068
|
+
// A call that has not answered yet is work in flight, so the
|
|
1069
|
+
// turn watchdog must not treat the silence as a hung model.
|
|
1070
|
+
relayInFlight = signal.inFlight;
|
|
1011
1071
|
if (signal.lastActivityAt > lastRelayActivityAt) {
|
|
1012
1072
|
lastRelayActivityAt = signal.lastActivityAt;
|
|
1013
1073
|
activity.note('mcp', signal.lastTool ?? 'tool call');
|
|
@@ -1053,6 +1113,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1053
1113
|
continue;
|
|
1054
1114
|
}
|
|
1055
1115
|
activity.note('shell', shell.command ?? shell.id);
|
|
1116
|
+
// HARD GUARD: a running command is real work. The turn
|
|
1117
|
+
// watchdog must never kill a run the agent is waiting on.
|
|
1118
|
+
shellRunningNow = true;
|
|
1056
1119
|
startIdleTimer();
|
|
1057
1120
|
heartbeat(`background shell ${shell.command ?? shell.id} is still running`);
|
|
1058
1121
|
break;
|
|
@@ -1075,6 +1138,110 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1075
1138
|
? 'no output at all'
|
|
1076
1139
|
: `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');
|
|
1077
1140
|
}
|
|
1141
|
+
// 4) TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
|
|
1142
|
+
//
|
|
1143
|
+
// The activity clock above is deliberately patient: it cannot
|
|
1144
|
+
// tell a hung provider call from a long tool call, so it waits the
|
|
1145
|
+
// full idle budget. A model turn that OPENED and never produced a
|
|
1146
|
+
// token is not ambiguous, though - it is the Aga Autos incident
|
|
1147
|
+
// (a `step_start`, then 19 silent minutes). Ladder: warn ->
|
|
1148
|
+
// interrupt -> kill, and the kill reuses exit 124 so the
|
|
1149
|
+
// conversation is kept for a retry instead of starting over.
|
|
1150
|
+
if (!turnWatchOff && !settled) {
|
|
1151
|
+
const turnSnap = turn.snapshot();
|
|
1152
|
+
// A DIFFERENT step means the session recovered (or moved on):
|
|
1153
|
+
// clear the interrupt latch so the next stall is judged fresh.
|
|
1154
|
+
if (turnInterruptedStep !== null &&
|
|
1155
|
+
turnSnap.stepId !== turnInterruptedStep) {
|
|
1156
|
+
turnInterruptedStep = null;
|
|
1157
|
+
turnInterruptedAt = null;
|
|
1158
|
+
}
|
|
1159
|
+
const verdict = (0, turn_watch_1.decideTurnAction)({
|
|
1160
|
+
snapshot: turnSnap,
|
|
1161
|
+
now,
|
|
1162
|
+
budgets: turnBudget,
|
|
1163
|
+
inFlightMcp: relayInFlight,
|
|
1164
|
+
shellRunning: shellRunningNow,
|
|
1165
|
+
subagentActive: lastSubagentAt > 0 &&
|
|
1166
|
+
now - lastSubagentAt < turnBudget.subagentGraceMs,
|
|
1167
|
+
interruptedAt: turnInterruptedAt,
|
|
1168
|
+
});
|
|
1169
|
+
const stepKey = turnSnap.stepId ?? `step-${turnSnap.stepOpenedAt ?? 0}`;
|
|
1170
|
+
if (verdict.action === 'warn' && turnWarnedStep !== stepKey) {
|
|
1171
|
+
turnWarnedStep = stepKey;
|
|
1172
|
+
log(`[harness] ${adapter.name} model turn open for ` +
|
|
1173
|
+
`${Math.max(1, Math.round(verdict.openForMs / 60_000))} min, ` +
|
|
1174
|
+
`silent for ${Math.max(1, Math.round(verdict.silentMs / 60_000))} min ` +
|
|
1175
|
+
`(${verdict.reason}) - interrupting at ` +
|
|
1176
|
+
`${Math.round(turnBudget.interruptMs / 60_000)} min, killing at ` +
|
|
1177
|
+
`${Math.round(turnBudget.killMs / 60_000)} min unless it recovers`, 'stderr');
|
|
1178
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1179
|
+
kind: 'harness_failure',
|
|
1180
|
+
severity: 'warning',
|
|
1181
|
+
message: `model turn stalled (${verdict.reason})`,
|
|
1182
|
+
errorCode: 'HARNESS_TURN_STALL',
|
|
1183
|
+
agentId: store?.agentId,
|
|
1184
|
+
context: {
|
|
1185
|
+
harness: resolved,
|
|
1186
|
+
mode: settings.sessionMode ?? null,
|
|
1187
|
+
runKey: runKey ?? null,
|
|
1188
|
+
openForMs: verdict.openForMs,
|
|
1189
|
+
silentMs: verdict.silentMs,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (verdict.action === 'interrupt' &&
|
|
1194
|
+
turnInterruptedStep !== stepKey) {
|
|
1195
|
+
turnInterruptedStep = stepKey;
|
|
1196
|
+
turnInterruptedAt = now;
|
|
1197
|
+
log(`[harness] ${adapter.name} model turn stalled ` +
|
|
1198
|
+
`(${verdict.reason}) - interrupting the service session so it ` +
|
|
1199
|
+
'can continue; no work is lost', 'stderr');
|
|
1200
|
+
if (capturedId)
|
|
1201
|
+
await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
|
|
1202
|
+
}
|
|
1203
|
+
if (verdict.action === 'kill') {
|
|
1204
|
+
// Same contract as the idle kill: OUR decision, not a broken
|
|
1205
|
+
// conversation, so the session is kept for a retry.
|
|
1206
|
+
idleKilled = true;
|
|
1207
|
+
forcedExitCode = 124;
|
|
1208
|
+
log(`[harness] ${adapter.name} model turn stalled ` +
|
|
1209
|
+
`(${verdict.reason}) - killing the run; the conversation is ` +
|
|
1210
|
+
'kept so the next attempt continues it', 'stderr');
|
|
1211
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1212
|
+
kind: 'harness_failure',
|
|
1213
|
+
severity: 'error',
|
|
1214
|
+
message: `model turn stalled and was killed (${verdict.reason})`,
|
|
1215
|
+
errorCode: 'HARNESS_TURN_STALLED',
|
|
1216
|
+
agentId: store?.agentId,
|
|
1217
|
+
context: {
|
|
1218
|
+
harness: resolved,
|
|
1219
|
+
mode: settings.sessionMode ?? null,
|
|
1220
|
+
runKey: runKey ?? null,
|
|
1221
|
+
openForMs: verdict.openForMs,
|
|
1222
|
+
silentMs: verdict.silentMs,
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
// Interrupt FIRST: on OpenCode V2 the shared service owns the
|
|
1226
|
+
// turn, so killing the CLI alone leaves a silent session.
|
|
1227
|
+
if (capturedId)
|
|
1228
|
+
await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
|
|
1229
|
+
try {
|
|
1230
|
+
killTree(child.pid);
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
/* ignore */
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
child.kill();
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
/* ignore */
|
|
1240
|
+
}
|
|
1241
|
+
done(124);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1078
1245
|
})();
|
|
1079
1246
|
}, 30_000);
|
|
1080
1247
|
activityTimer.unref?.();
|
|
@@ -1130,11 +1297,33 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1130
1297
|
done(forcedExitCode ?? code ?? 1);
|
|
1131
1298
|
});
|
|
1132
1299
|
}).catch((err) => {
|
|
1133
|
-
// `spawn()` can throw SYNCHRONOUSLY (bad argv, ENOENT
|
|
1134
|
-
// event), which means the `close` handler above never
|
|
1135
|
-
// config here too - it holds the agent API key - and
|
|
1136
|
-
// reach the caller.
|
|
1300
|
+
// `spawn()` can throw SYNCHRONOUSLY (bad argv, ENAMETOOLONG, ENOENT
|
|
1301
|
+
// before the `error` event), which means the `close` handler above never
|
|
1302
|
+
// runs. Clean the MCP config here too - it holds the agent API key - and
|
|
1303
|
+
// let the rejection reach the caller.
|
|
1137
1304
|
cleanMcp();
|
|
1305
|
+
// A raw `spawn ENAMETOOLONG` reaches the task as if the binary were
|
|
1306
|
+
// broken. Name the real cause and the fix instead.
|
|
1307
|
+
if (err?.code === 'ENAMETOOLONG') {
|
|
1308
|
+
const detail = `the ${adapter.name} command line was too long to start ` +
|
|
1309
|
+
`(prompt ${prompt.length} chars, OS limit ~32k). Shorten the task ` +
|
|
1310
|
+
'brief / project context, or use a harness that supports prompt files.';
|
|
1311
|
+
log(`[harness] ${detail}`, 'stderr');
|
|
1312
|
+
(0, diagnostics_1.reportDiagnosticSafe)({
|
|
1313
|
+
kind: 'harness_failure',
|
|
1314
|
+
severity: 'error',
|
|
1315
|
+
message: detail,
|
|
1316
|
+
errorCode: 'HARNESS_ENAMETOOLONG',
|
|
1317
|
+
agentId: settings.sessionStore?.agentId,
|
|
1318
|
+
context: {
|
|
1319
|
+
harness: adapter.name,
|
|
1320
|
+
promptChars: prompt.length,
|
|
1321
|
+
mode: settings.sessionMode ?? null,
|
|
1322
|
+
taskId: settings.sessionStore?.taskId ?? null,
|
|
1323
|
+
},
|
|
1324
|
+
});
|
|
1325
|
+
throw new Error(`spawn ENAMETOOLONG: ${detail}`);
|
|
1326
|
+
}
|
|
1138
1327
|
throw err;
|
|
1139
1328
|
});
|
|
1140
1329
|
};
|
|
@@ -1384,11 +1573,17 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1384
1573
|
// session is consumed) exists because a retry must not resume a poisoned
|
|
1385
1574
|
// planning session; a conversation is different, so it gets one retry and
|
|
1386
1575
|
// is replaced only after the SECOND consecutive failure.
|
|
1576
|
+
//
|
|
1577
|
+
// `failedRun` is REQUIRED here (2026-11): without it a SUCCESSFUL turn
|
|
1578
|
+
// took this branch too, and logged "run failed (attempt 0) - KEEPING the
|
|
1579
|
+
// conversation" on every single reply. Live case: a chat mention answered
|
|
1580
|
+
// the question, posted the message through MCP, exited 0 - and the
|
|
1581
|
+
// console still said the run failed.
|
|
1387
1582
|
const conversational = session_store_1.CONVERSATIONAL_MODES.has(store.mode);
|
|
1388
1583
|
const failureCount = failedRun
|
|
1389
1584
|
? Math.max(1, (0, session_store_1.noteHarnessSessionFailure)(store.agentId, store.taskId, store.mode, `${adapter.name} exited with code ${finalCode}`))
|
|
1390
1585
|
: 0;
|
|
1391
|
-
const keepForConversation = conversational && failureCount < 2;
|
|
1586
|
+
const keepForConversation = failedRun && conversational && failureCount < 2;
|
|
1392
1587
|
if (failedRun &&
|
|
1393
1588
|
!questionStillOpen &&
|
|
1394
1589
|
!keepForIdleResume &&
|
|
@@ -1398,13 +1593,8 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
1398
1593
|
}
|
|
1399
1594
|
else if (spec.capture !== 'none') {
|
|
1400
1595
|
if (keepForConversation) {
|
|
1401
|
-
log(`[resume] run failed (attempt ${failureCount}) -
|
|
1402
|
-
`conversation so the next message continues instead of starting over
|
|
1403
|
-
if (failureCount >= 2) {
|
|
1404
|
-
// Unreachable by construction (`keepForConversation` is false at
|
|
1405
|
-
// 2), kept as a guard if the threshold ever changes.
|
|
1406
|
-
log('[resume] session failed twice - replacing it', 'stderr');
|
|
1407
|
-
}
|
|
1596
|
+
log(`[resume] run failed (attempt ${failureCount} of 2) - keeping the ` +
|
|
1597
|
+
`conversation so the next message continues instead of starting over`, 'stderr');
|
|
1408
1598
|
}
|
|
1409
1599
|
if (questionStillOpen) {
|
|
1410
1600
|
log(`[resume] run failed but a question is still open - keeping the ` +
|
|
@@ -1502,9 +1692,11 @@ async function readRelayActivity(runKey) {
|
|
|
1502
1692
|
const at = Number(body.lastActivityAt);
|
|
1503
1693
|
if (!Number.isFinite(at) || at <= 0)
|
|
1504
1694
|
return null;
|
|
1695
|
+
const inFlight = Number(body.inFlight);
|
|
1505
1696
|
return {
|
|
1506
1697
|
lastActivityAt: at,
|
|
1507
1698
|
lastTool: typeof body.lastTool === 'string' ? body.lastTool : null,
|
|
1699
|
+
inFlight: Number.isFinite(inFlight) && inFlight > 0 ? inFlight : 0,
|
|
1508
1700
|
};
|
|
1509
1701
|
}
|
|
1510
1702
|
catch {
|
|
@@ -1568,28 +1760,33 @@ function spawnThroughPty(binary, prompt, log, settings, name) {
|
|
|
1568
1760
|
const adapterLike = (0, harness_adapter_1.getAdapter)(name);
|
|
1569
1761
|
let promptFile = null;
|
|
1570
1762
|
let args;
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1763
|
+
const inlineArgs = adapterLike.headlessArgs(prompt, settings);
|
|
1764
|
+
const commandLineChars = (0, prompt_file_1.argvChars)([
|
|
1765
|
+
bin.file,
|
|
1766
|
+
...(bin.args ?? []),
|
|
1767
|
+
...inlineArgs,
|
|
1768
|
+
]);
|
|
1769
|
+
if ((0, prompt_file_1.shouldUsePromptFile)(adapterLike, prompt.length, commandLineChars)) {
|
|
1770
|
+
try {
|
|
1771
|
+
promptFile = (0, prompt_file_1.writePromptFile)(prompt);
|
|
1772
|
+
}
|
|
1773
|
+
catch (err) {
|
|
1774
|
+
promptFile = null;
|
|
1775
|
+
log(`[harness] could not write the prompt file (${msg(err)}) - ` +
|
|
1776
|
+
'falling back to argv, which may exceed the OS command-line limit', 'stderr');
|
|
1777
|
+
}
|
|
1778
|
+
if (promptFile) {
|
|
1779
|
+
log(`[harness] prompt too long for argv (${prompt.length} chars, ` +
|
|
1780
|
+
`~${commandLineChars} char command line) — wrote it to ` +
|
|
1781
|
+
`${promptFile} and passed the path instead`, 'stderr');
|
|
1782
|
+
args = (0, prompt_file_1.promptFileArgs)(adapterLike, adapterLike.headlessArgs(prompt, settings, true), promptFile);
|
|
1577
1783
|
}
|
|
1578
1784
|
else {
|
|
1579
|
-
args =
|
|
1580
|
-
const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
|
|
1581
|
-
'Do not respond until you have read the file. ' +
|
|
1582
|
-
'The file contains your complete instructions including project context, tools, and the user message.';
|
|
1583
|
-
if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(name)) {
|
|
1584
|
-
args.unshift('-p', shortInstruction);
|
|
1585
|
-
}
|
|
1586
|
-
else {
|
|
1587
|
-
args.push(shortInstruction);
|
|
1588
|
-
}
|
|
1785
|
+
args = inlineArgs;
|
|
1589
1786
|
}
|
|
1590
1787
|
}
|
|
1591
1788
|
else {
|
|
1592
|
-
args =
|
|
1789
|
+
args = inlineArgs;
|
|
1593
1790
|
}
|
|
1594
1791
|
let mcpOverride = null;
|
|
1595
1792
|
if (adapterLike.supportsMcp &&
|