comfyui-mcp 0.50.0 → 0.50.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/comfyui/fetch.js +61 -8
- package/dist/comfyui/fetch.js.map +1 -1
- package/dist/orchestrator/agent-backend.js.map +1 -1
- package/dist/orchestrator/call-tool-admission.js +18 -3
- package/dist/orchestrator/call-tool-admission.js.map +1 -1
- package/dist/orchestrator/claude-backend.js +262 -14
- package/dist/orchestrator/claude-backend.js.map +1 -1
- package/dist/orchestrator/index.js +84 -26
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +8 -1
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +215 -11
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/run-completion-journal.js +129 -34
- package/dist/orchestrator/run-completion-journal.js.map +1 -1
- package/dist/orchestrator/turn-registry.js +227 -0
- package/dist/orchestrator/turn-registry.js.map +1 -0
- package/dist/services/download-cache.js +6 -20
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/image-management.js +31 -10
- package/dist/services/image-management.js.map +1 -1
- package/dist/services/instance-witness.js +99 -0
- package/dist/services/instance-witness.js.map +1 -0
- package/dist/services/live-widget-overlay.js +139 -0
- package/dist/services/live-widget-overlay.js.map +1 -0
- package/dist/services/lmstudio-lifecycle.js +2 -1
- package/dist/services/lmstudio-lifecycle.js.map +1 -1
- package/dist/services/local-vram.js +44 -0
- package/dist/services/local-vram.js.map +1 -0
- package/dist/services/model-resolver.js +63 -7
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +79 -5
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/panel-installer.js +36 -9
- package/dist/services/panel-installer.js.map +1 -1
- package/dist/services/panel-recovery.js +32 -3
- package/dist/services/panel-recovery.js.map +1 -1
- package/dist/services/panel-sync.js +50 -0
- package/dist/services/panel-sync.js.map +1 -1
- package/dist/services/panel-workspace.js +7 -3
- package/dist/services/panel-workspace.js.map +1 -1
- package/dist/services/process-control.js +165 -51
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/queue-monitor.js +26 -0
- package/dist/services/queue-monitor.js.map +1 -1
- package/dist/services/queue-status-broadcast.test.js +2 -0
- package/dist/services/queue-status-broadcast.test.js.map +1 -1
- package/dist/services/self-update.js +290 -38
- package/dist/services/self-update.js.map +1 -1
- package/dist/services/session-scope.js +17 -0
- package/dist/services/session-scope.js.map +1 -1
- package/dist/services/ui-bridge.js +16 -2
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/update-comfyui.js +160 -2
- package/dist/services/update-comfyui.js.map +1 -1
- package/dist/services/workflow-converter.js +10 -1
- package/dist/services/workflow-converter.js.map +1 -1
- package/dist/tools/image-management.js +41 -6
- package/dist/tools/image-management.js.map +1 -1
- package/dist/tools/install-comfyui.js +1 -1
- package/dist/tools/install-comfyui.js.map +1 -1
- package/dist/tools/model-management.js +58 -7
- package/dist/tools/model-management.js.map +1 -1
- package/dist/tools/skills-access.js +8 -3
- package/dist/tools/skills-access.js.map +1 -1
- package/dist/tools/vocabulary.js +88 -0
- package/dist/tools/vocabulary.js.map +1 -1
- package/dist/utils/errors.js +45 -1
- package/dist/utils/errors.js.map +1 -1
- package/package.json +1 -1
- package/scripts/check-tool-vocabulary.mts +14 -1
- package/scripts/export-vocabulary.mts +33 -5
- package/scripts/gen-tool-docs.ts +10 -1
package/dist/comfyui/fetch.js
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import { getComfyUIAuthHeaders } from "../config.js";
|
|
2
|
+
import { describeFetchFailure, isBareFetchFailure } from "../utils/errors.js";
|
|
3
|
+
/** The request target, for the diagnostic. Never throws on an odd input. */
|
|
4
|
+
function targetOf(input) {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof input === "string")
|
|
7
|
+
return input;
|
|
8
|
+
if (input instanceof URL)
|
|
9
|
+
return input.href;
|
|
10
|
+
return input.url ?? String(input);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return String(input);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Turn a network-layer throw into a diagnostic that says WHAT was attempted.
|
|
18
|
+
*
|
|
19
|
+
* `TypeError: fetch failed` was reaching tool results verbatim: #954 saw it from
|
|
20
|
+
* the workflow-templates listing (now `list_packs` action:"list_templates") and
|
|
21
|
+
* could not tell which host was tried, and #952 saw it from the readonly tools
|
|
22
|
+
* while the panel bridge was working fine — reading it, reasonably, as "the tool
|
|
23
|
+
* is broken" when the headless target was simply a different (unreachable)
|
|
24
|
+
* address than the one the panel is bound to.
|
|
25
|
+
*
|
|
26
|
+
* The two ARE separate targets by design: the panel talks to whichever ComfyUI
|
|
27
|
+
* the browser is on, while these calls go to the configured COMFYUI_URL. That is
|
|
28
|
+
* not a bug, but it is invisible unless the failure names the address.
|
|
29
|
+
*/
|
|
30
|
+
function describeComfyFetchFailure(err, target) {
|
|
31
|
+
const { message, code } = describeFetchFailure(err);
|
|
32
|
+
const wrapped = new Error(`${message} — while requesting ${target}. ` +
|
|
33
|
+
`That is the headless ComfyUI target (COMFYUI_URL); a CONNECTED sidebar panel does not imply this address is reachable, ` +
|
|
34
|
+
`because the panel talks to whichever ComfyUI its browser tab is on. ` +
|
|
35
|
+
`Check the target with install_comfyui (action:"environment"), and confirm the server is up with get_system_stats (action:"health").`, { cause: err });
|
|
36
|
+
if (code)
|
|
37
|
+
wrapped.code = code;
|
|
38
|
+
return wrapped;
|
|
39
|
+
}
|
|
2
40
|
/**
|
|
3
41
|
* `fetch` wrapper for ComfyUI HTTP requests that injects the configured generic
|
|
4
42
|
* auth header(s) (COMFYUI_AUTH_* — for self-hosted ComfyUI behind a reverse
|
|
@@ -9,15 +47,30 @@ import { getComfyUIAuthHeaders } from "../config.js";
|
|
|
9
47
|
* server. Non-ComfyUI requests (HuggingFace, Civitai, Comfy Cloud) keep using
|
|
10
48
|
* plain `fetch` — Comfy Cloud has its own X-API-Key path in cloud-client.ts.
|
|
11
49
|
*/
|
|
12
|
-
export function comfyuiFetch(input, init = {}) {
|
|
50
|
+
export async function comfyuiFetch(input, init = {}) {
|
|
13
51
|
const auth = getComfyUIAuthHeaders();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
52
|
+
let request;
|
|
53
|
+
if (Object.keys(auth).length === 0) {
|
|
54
|
+
request = fetch(input, init);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const headers = new Headers(init.headers);
|
|
58
|
+
for (const [name, value] of Object.entries(auth)) {
|
|
59
|
+
if (!headers.has(name))
|
|
60
|
+
headers.set(name, value);
|
|
61
|
+
}
|
|
62
|
+
request = fetch(input, { ...init, headers });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return await request;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// ONLY the opaque undici failure is rewritten. An AbortError, a timeout with
|
|
69
|
+
// its own message, or anything else already says what happened, and
|
|
70
|
+
// replacing that text would be a downgrade.
|
|
71
|
+
if (!isBareFetchFailure(err))
|
|
72
|
+
throw err;
|
|
73
|
+
throw describeComfyFetchFailure(err, targetOf(input));
|
|
20
74
|
}
|
|
21
|
-
return fetch(input, { ...init, headers });
|
|
22
75
|
}
|
|
23
76
|
//# sourceMappingURL=fetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/comfyui/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/comfyui/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE9E,4EAA4E;AAC5E,SAAS,QAAQ,CAAC,KAA6B;IAC7C,IAAI,CAAC;QACH,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5C,IAAI,KAAK,YAAY,GAAG;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC;QAC5C,OAAQ,KAAiB,CAAC,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,yBAAyB,CAAC,GAAY,EAAE,MAAc;IAC7D,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,IAAI,KAAK,CACvB,GAAG,OAAO,uBAAuB,MAAM,IAAI;QACzC,yHAAyH;QACzH,sEAAsE;QACtE,qIAAqI,EACvI,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACF,IAAI,IAAI;QAAG,OAA6B,CAAC,IAAI,GAAG,IAAI,CAAC;IACrD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAA6B,EAC7B,OAAoB,EAAE;IAEtB,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAC;IACrC,IAAI,OAA0B,CAAC;IAC/B,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,6EAA6E;QAC7E,oEAAoE;QACpE,4CAA4C;QAC5C,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC;QACxC,MAAM,yBAAyB,CAAC,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACxD,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-backend.js","sourceRoot":"","sources":["../../src/orchestrator/agent-backend.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,mFAAmF;AACnF,oEAAoE;AACpE,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAC/E,+EAA+E;AAC/E,mFAAmF;
|
|
1
|
+
{"version":3,"file":"agent-backend.js","sourceRoot":"","sources":["../../src/orchestrator/agent-backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* decision is a pure, unit-testable function — the dispatcher is a one-line
|
|
5
5
|
* call site.
|
|
6
6
|
*/
|
|
7
|
-
import { retiredToolMessage } from "../tools/vocabulary.js";
|
|
7
|
+
import { retiredToolMessage, TOOL_NAMES } from "../tools/vocabulary.js";
|
|
8
8
|
/** The direct tool channel: a mobile client can invoke these READ/DOWNLOAD backend
|
|
9
9
|
* tools without an agent turn (structured nav data + rig downloads). The
|
|
10
10
|
* bridge is already token-gated; this whitelist keeps call_tool to non-destructive
|
|
@@ -434,8 +434,23 @@ export function callToolAdmission(tool, args) {
|
|
|
434
434
|
const retired = retiredToolMessage(tool);
|
|
435
435
|
if (retired !== undefined)
|
|
436
436
|
return retired;
|
|
437
|
-
if (!CALL_TOOL_WHITELIST.has(tool))
|
|
438
|
-
|
|
437
|
+
if (!CALL_TOOL_WHITELIST.has(tool)) {
|
|
438
|
+
// Distinguish "does not exist" from "exists, but not on THIS channel" (#908).
|
|
439
|
+
// The bare "is not permitted" was a dead end: the panel's RunPod Deploy/Start
|
|
440
|
+
// buttons returned it verbatim for ~2 weeks after #278 correctly removed those
|
|
441
|
+
// two tools from this whitelist (both start BILLING, and a confirmation-less
|
|
442
|
+
// mirrored tab must not spend money). The exclusion was right; the message
|
|
443
|
+
// told nobody what had happened or what to do instead, so it read as "the
|
|
444
|
+
// panel is broken" rather than "this channel deliberately does not carry it".
|
|
445
|
+
const exists = TOOL_NAMES.includes(tool);
|
|
446
|
+
return exists
|
|
447
|
+
? `tool "${tool}" exists but is not available on the direct call_tool channel ` +
|
|
448
|
+
`(the canvas-less path used by mobile, mirrored tabs and panel buttons). This is a ` +
|
|
449
|
+
`deliberate exclusion, not a missing tool — some tools are withheld here because they ` +
|
|
450
|
+
`spend money or mutate state that this channel cannot confirm with the user. Ask the ` +
|
|
451
|
+
`agent to run it instead: the agent session carries the full tool surface and can confirm.`
|
|
452
|
+
: `tool "${tool}" is not permitted`;
|
|
453
|
+
}
|
|
439
454
|
const actions = CALL_TOOL_ACTION_WHITELIST.get(tool);
|
|
440
455
|
if (actions !== undefined) {
|
|
441
456
|
const action = typeof args.action === "string" ? args.action : undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call-tool-admission.js","sourceRoot":"","sources":["../../src/orchestrator/call-tool-admission.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"call-tool-admission.js","sourceRoot":"","sources":["../../src/orchestrator/call-tool-admission.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
13
|
import { errorText } from "./error-text.js";
|
|
14
14
|
import { buildAgentSpawnEnv } from "../services/panel-secrets.js";
|
|
15
|
+
import { loadTurnRegistry, saveTurnRegistry, tombstoneTurn } from "./turn-registry.js";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
15
17
|
import { CLAUDE_CAPABILITIES, } from "./agent-backend.js";
|
|
16
18
|
// ---- reasoning effort mapping ----
|
|
17
19
|
// Effort is now a provider-neutral union (it must survive a provider switch — see
|
|
@@ -204,6 +206,11 @@ export async function fetchSupportedCommands(model) {
|
|
|
204
206
|
* silently realigning it: the pop that crosses the evicted gap (and any
|
|
205
207
|
* traceless result) fails closed as unverifiable, never ok:true (#745 r3). */
|
|
206
208
|
const MAX_UNRESOLVED_TURNS = 16;
|
|
209
|
+
/** Bound on REMEMBERED dead-session turn ids (#886). The set exists to
|
|
210
|
+
* recognize a late result from a restarted session — it is not a
|
|
211
|
+
* classification input, so a cap here only bounds how far back recognition
|
|
212
|
+
* reaches; beyond it a stray falls back to the plain traceless report. */
|
|
213
|
+
const MAX_REMEMBERED_DEAD_TURNS = 64;
|
|
207
214
|
/**
|
|
208
215
|
* The Claude Agent SDK adapter. One instance per PanelAgent; it holds the live
|
|
209
216
|
* `Query` for the current session and re-creates it on each `run()`.
|
|
@@ -260,6 +267,51 @@ export class ClaudeBackend {
|
|
|
260
267
|
* unconsumed candidate it fails closed as traceless (#728 r6). */
|
|
261
268
|
consumedInterrupted = new Set();
|
|
262
269
|
turns = [];
|
|
270
|
+
// #886 — "submitted" must survive the session: when a run() restart drops the
|
|
271
|
+
// live traces above, their ids move here (and a RESUMED session rehydrates
|
|
272
|
+
// them from the durable registry written by the previous process). A
|
|
273
|
+
// traceless result while this set is non-empty is then RECOGNIZED for what
|
|
274
|
+
// it most likely is — a turn that was in flight when its session died,
|
|
275
|
+
// completing late — and reported as "completed but unverified": never a
|
|
276
|
+
// fabricated success (fail-closed #745 stands), but also no longer the false
|
|
277
|
+
// "could not be matched to any submitted turn", which claimed the turn never
|
|
278
|
+
// existed when only its record was lost. Insertion-ordered; recognition
|
|
279
|
+
// consumes the oldest entry (results land in submission order).
|
|
280
|
+
//
|
|
281
|
+
// Entries are owner-TAGGED (`<instanceId>:<turnId>`): turnSeq restarts per
|
|
282
|
+
// process, so two instances can each leave their own turn "1" outstanding —
|
|
283
|
+
// untagged ids would collapse those into one record and the second late
|
|
284
|
+
// result would be falsely reported as matching no submission (codex gate).
|
|
285
|
+
instanceId = randomUUID();
|
|
286
|
+
rememberedDeadTurns = new Set();
|
|
287
|
+
/** The remembered set's bound dropped a record at some point — persisted
|
|
288
|
+
* into the registry file (`incomplete`) so a RESPAWN also knows the record
|
|
289
|
+
* is not provably complete, not just this process (codex gate r2). */
|
|
290
|
+
rememberedIncomplete = false;
|
|
291
|
+
/** Session id the durable registry file belongs to (learned at init). Null
|
|
292
|
+
* until then — a submission pulled before init is processed can't be filed
|
|
293
|
+
* under a session nobody can resume yet. */
|
|
294
|
+
registrySessionId = null;
|
|
295
|
+
/** A registry read/write failed — the record of outstanding turns is not
|
|
296
|
+
* provably complete, so log it and never lean on the registry's ABSENCE as
|
|
297
|
+
* evidence in that state. */
|
|
298
|
+
registryDegraded = false;
|
|
299
|
+
// #885 — a stall is not a user rejection. The Agent SDK has no mid-turn steer
|
|
300
|
+
// (Codex's turn/steer equivalent): a user message pushed into the stream is
|
|
301
|
+
// queued by the CLI until the current turn ENDS — exactly what a frozen turn
|
|
302
|
+
// cannot do — so the notice cannot ride the stalled turn itself. What must
|
|
303
|
+
// not happen is the transcript keeping ONLY the interrupt's generic
|
|
304
|
+
// user-rejection payload: on the next turn the model reads it and tells the
|
|
305
|
+
// user "you cancelled" — a lie about the user's own actions. So the notice
|
|
306
|
+
// is held here and prepended (clearly delimited as harness text, not the
|
|
307
|
+
// user's words) to the NEXT user turn, where it lands in the transcript
|
|
308
|
+
// right beside that payload. recoverStalledTurn therefore returns false:
|
|
309
|
+
// PanelAgent keeps its proven bounded interrupt/restart recovery for the
|
|
310
|
+
// frozen turn, and the correction rides the turn after.
|
|
311
|
+
pendingStallNotice = null;
|
|
312
|
+
/** turnSeq when the notice was stored — a genuine `success` landing for that
|
|
313
|
+
* turn means the "stall" recovered by itself and the notice is stale. */
|
|
314
|
+
pendingStallNoticeTurn = 0;
|
|
263
315
|
constructor(deps) {
|
|
264
316
|
this.deps = deps;
|
|
265
317
|
}
|
|
@@ -300,7 +352,20 @@ export class ClaudeBackend {
|
|
|
300
352
|
* image refs to inline base64 blocks so the agent SEES them in this turn (no
|
|
301
353
|
* get_image view/fetch round-trip). */
|
|
302
354
|
async shapeTurn(turn) {
|
|
303
|
-
|
|
355
|
+
let text = turn.text;
|
|
356
|
+
// #885 — deliver the held stall correction AHEAD of the user's words, so
|
|
357
|
+
// the model reads the true origin of the interrupt right next to the
|
|
358
|
+
// generic rejection payload it would otherwise take at face value. The
|
|
359
|
+
// notice names the PREVIOUS turn explicitly: on this new turn, a bare
|
|
360
|
+
// "this turn stalled" would mispoint.
|
|
361
|
+
if (this.pendingStallNotice) {
|
|
362
|
+
text =
|
|
363
|
+
`${this.pendingStallNotice}\n\n` +
|
|
364
|
+
`[harness note: the notice above is about the PREVIOUS turn — the one the harness cleared. ` +
|
|
365
|
+
`It is not part of the user's message and the user did not write it. The user's message follows.]\n\n` +
|
|
366
|
+
text;
|
|
367
|
+
this.pendingStallNotice = null;
|
|
368
|
+
}
|
|
304
369
|
const images = turn.images ?? [];
|
|
305
370
|
let content = text;
|
|
306
371
|
if (images.length) {
|
|
@@ -382,23 +447,51 @@ export class ClaudeBackend {
|
|
|
382
447
|
const query = await loadQuery();
|
|
383
448
|
const self = this;
|
|
384
449
|
// A (re)started session can never produce results for turns submitted to a
|
|
385
|
-
// PREVIOUS session
|
|
386
|
-
// past EVERY turn stamped so far (turnSeq), whether or not traces
|
|
387
|
-
// in a FULLY DRAINED (e.g. poisoned) session the FIFO is already
|
|
388
|
-
// the sequence must still move past the old ids or the new
|
|
389
|
-
// result re-crosses the old gap and falsely re-poisons
|
|
390
|
-
//
|
|
391
|
-
// instead. This genuine restart boundary is also the ONLY reset of the
|
|
450
|
+
// PREVIOUS session through the live FIFO — clear it and advance the result
|
|
451
|
+
// sequence past EVERY turn stamped so far (turnSeq), whether or not traces
|
|
452
|
+
// remain: in a FULLY DRAINED (e.g. poisoned) session the FIFO is already
|
|
453
|
+
// empty, but the sequence must still move past the old ids or the new
|
|
454
|
+
// session's first result re-crosses the old gap and falsely re-poisons
|
|
455
|
+
// (#745 r5). This genuine restart boundary is also the ONLY reset of the
|
|
392
456
|
// sticky classification poison (#745 r4). Done here, at run() entry (BEFORE
|
|
393
457
|
// the new query exists), rather than on system/init: the SDK's prompt drain
|
|
394
458
|
// legitimately pulls the first batch before init is processed, and an
|
|
395
459
|
// init-time clear would unmark that genuinely in-flight turn (#745).
|
|
460
|
+
//
|
|
461
|
+
// But "the trace is gone" must not become "no turn was ever submitted"
|
|
462
|
+
// (#886): the dead session's unresolved ids are REMEMBERED (and, when this
|
|
463
|
+
// run RESUMES a session, rehydrated from the durable registry a previous
|
|
464
|
+
// process wrote), so a stray late result is recognized as a submitted
|
|
465
|
+
// turn's late landing and reported "completed but unverified" instead of
|
|
466
|
+
// "matched to no submitted turn". Recognition never upgrades a result to
|
|
467
|
+
// ok:true — fail-closed stands.
|
|
468
|
+
for (const dead of this.turns)
|
|
469
|
+
this.rememberDeadTurn(`${this.instanceId}:${dead.id}`);
|
|
396
470
|
this.turns.length = 0;
|
|
397
471
|
this.lastResultTurnId = this.turnSeq;
|
|
398
472
|
this.classificationPoisoned = false;
|
|
399
473
|
this.parkedInterrupted.clear(); // dead session's parks die with it
|
|
400
474
|
this.consumedInterrupted.clear(); // …and its landing history
|
|
401
475
|
this.markerBase = this.lastResultTurnId; // turn markers are run-relative (#728)
|
|
476
|
+
const resumeTarget = opts.resume ?? opts.sessionId ?? null;
|
|
477
|
+
// Registry writes before init (the prompt drain can submit ahead of it)
|
|
478
|
+
// file under the resume target — the session they will actually run in.
|
|
479
|
+
// A fresh start files nothing until init names the new session.
|
|
480
|
+
this.registrySessionId = resumeTarget;
|
|
481
|
+
if (resumeTarget) {
|
|
482
|
+
const loaded = loadTurnRegistry(resumeTarget);
|
|
483
|
+
// Hydrate whatever the session's files prove is outstanding — including
|
|
484
|
+
// this instance's OWN file (tags dedupe against the in-memory carry, and
|
|
485
|
+
// tombstones keep already-reported strays from resurrecting). A degraded
|
|
486
|
+
// or self-declared-incomplete record must not later ground the confident
|
|
487
|
+
// "matched to no submitted turn".
|
|
488
|
+
for (const tag of loaded.tags)
|
|
489
|
+
this.rememberDeadTurn(tag);
|
|
490
|
+
if (loaded.kind === "degraded" || loaded.incomplete) {
|
|
491
|
+
this.registryDegraded = true;
|
|
492
|
+
logger.warn(`[claude-backend] turn registry for the resumed session is ${loaded.kind === "degraded" ? `unreadable (${loaded.reason})` : "incomplete"} — outstanding-turn recognition is degraded (#886)`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
402
495
|
// Wrap the neutral channel: shape each turn into the SDK's native form right
|
|
403
496
|
// before it's read, so PanelAgent never deals in SDKUserMessage.
|
|
404
497
|
async function* prompt() {
|
|
@@ -416,6 +509,7 @@ export class ClaudeBackend {
|
|
|
416
509
|
const evicted = self.turns.shift();
|
|
417
510
|
logger.warn(`[claude-backend] over ${MAX_UNRESOLVED_TURNS} unresolved turns — evicted turn ${evicted?.id}'s trace; its result will fail closed as unverifiable (#740)`);
|
|
418
511
|
}
|
|
512
|
+
self.saveRegistry(); // the submission record must survive a restart (#886)
|
|
419
513
|
yield msg;
|
|
420
514
|
}
|
|
421
515
|
}
|
|
@@ -470,6 +564,22 @@ export class ClaudeBackend {
|
|
|
470
564
|
active.interrupted = true;
|
|
471
565
|
await this.q?.interrupt();
|
|
472
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* #885 — record the harness-stall notice so the NEXT user turn carries it
|
|
569
|
+
* into the transcript beside the interrupt's generic user-rejection payload
|
|
570
|
+
* (see the pendingStallNotice field comment for why the notice cannot ride
|
|
571
|
+
* the stalled turn itself on this provider). Returns false: no mid-turn
|
|
572
|
+
* steer exists here, so PanelAgent keeps its bounded interrupt/restart
|
|
573
|
+
* recovery for the frozen turn — the notice's job is only to stop the agent
|
|
574
|
+
* from telling the user THEY cancelled, and to tell it a retry is safe.
|
|
575
|
+
*/
|
|
576
|
+
async recoverStalledTurn(notice) {
|
|
577
|
+
if (notice.trim()) {
|
|
578
|
+
this.pendingStallNotice = notice;
|
|
579
|
+
this.pendingStallNoticeTurn = this.turnSeq;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
473
583
|
/** Permanently dispose of the live SDK query. The Agent SDK has no explicit
|
|
474
584
|
* "dispose" beyond interrupt(), which both stops the in-flight turn and lets
|
|
475
585
|
* the underlying transport wind down once the prompt generator is no longer
|
|
@@ -492,6 +602,71 @@ export class ClaudeBackend {
|
|
|
492
602
|
async listModels() {
|
|
493
603
|
return [];
|
|
494
604
|
}
|
|
605
|
+
/** Remember a submitted turn whose result can no longer arrive through the
|
|
606
|
+
* live FIFO (its session restarted). Recognition-only (#886): membership
|
|
607
|
+
* changes what an unmatched result can honestly be CALLED, never how it is
|
|
608
|
+
* classified. Bounded — oldest drops first, but a drop means the record is
|
|
609
|
+
* no longer provably complete, so it degrades recognition (the hedged
|
|
610
|
+
* wording) rather than silently reverting to the confident negative. */
|
|
611
|
+
rememberDeadTurn(tag) {
|
|
612
|
+
if (this.rememberedDeadTurns.has(tag))
|
|
613
|
+
return;
|
|
614
|
+
this.rememberedDeadTurns.add(tag);
|
|
615
|
+
if (this.rememberedDeadTurns.size > MAX_REMEMBERED_DEAD_TURNS) {
|
|
616
|
+
const oldest = this.rememberedDeadTurns.keys().next().value;
|
|
617
|
+
if (oldest !== undefined)
|
|
618
|
+
this.rememberedDeadTurns.delete(oldest);
|
|
619
|
+
this.registryDegraded = true;
|
|
620
|
+
this.rememberedIncomplete = true; // persisted — a respawn must hedge too
|
|
621
|
+
logger.warn(`[claude-backend] over ${MAX_REMEMBERED_DEAD_TURNS} remembered dead turns — dropped the oldest record; outstanding-turn recognition is degraded (#886)`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** Consume the oldest remembered dead-turn tag, if any — a recognized late
|
|
625
|
+
* landing spends the record (in memory, in this instance's file, and in the
|
|
626
|
+
* session's tombstones) so a second stray can never re-claim it. */
|
|
627
|
+
takeOldestRememberedDeadTurn() {
|
|
628
|
+
const oldest = this.rememberedDeadTurns.keys().next().value;
|
|
629
|
+
if (oldest === undefined)
|
|
630
|
+
return null;
|
|
631
|
+
this.rememberedDeadTurns.delete(oldest);
|
|
632
|
+
const sid = this.registrySessionId;
|
|
633
|
+
if (sid) {
|
|
634
|
+
try {
|
|
635
|
+
tombstoneTurn(sid, oldest);
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
// A lost tombstone can only REVIVE an already-reported record (a
|
|
639
|
+
// repeated hedged disclosure) — never a fabricated success. Log, move on.
|
|
640
|
+
logger.warn(`[claude-backend] turn tombstone write failed: ${msgOf(err)} (#886)`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
this.saveRegistry();
|
|
644
|
+
return oldest;
|
|
645
|
+
}
|
|
646
|
+
/** Persist which submitted turns are still outstanding (#886). A failure
|
|
647
|
+
* degrades recognition (flagged + logged) but must never break the turn
|
|
648
|
+
* itself — the registry is a recognition aid, not load-bearing for the
|
|
649
|
+
* turn in flight.
|
|
650
|
+
*
|
|
651
|
+
* Ordering: saves happen AFTER the trace push/pop they record. A crash in
|
|
652
|
+
* the pop→save window leaves a STALE record (a turn whose result was seen
|
|
653
|
+
* still listed outstanding) — over-recognition, which produces the hedged
|
|
654
|
+
* "completed but unverified" disclosure. The reverse order could LOSE a
|
|
655
|
+
* record, and a lost record produces the confident "matched to no
|
|
656
|
+
* submitted turn" — a false assertion — so over-recognition is the
|
|
657
|
+
* deliberate failure side. */
|
|
658
|
+
saveRegistry() {
|
|
659
|
+
const sid = this.registrySessionId;
|
|
660
|
+
if (!sid)
|
|
661
|
+
return;
|
|
662
|
+
try {
|
|
663
|
+
saveTurnRegistry(sid, this.instanceId, this.turns.map((t) => t.id), [...this.rememberedDeadTurns], this.rememberedIncomplete);
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
this.registryDegraded = true;
|
|
667
|
+
logger.warn(`[claude-backend] turn registry write failed: ${msgOf(err)} — outstanding-turn recognition is degraded (#886)`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
495
670
|
/** Normalize one SDK message into zero or more canonical AgentEvents. */
|
|
496
671
|
*route(message) {
|
|
497
672
|
switch (message.type) {
|
|
@@ -499,9 +674,15 @@ export class ClaudeBackend {
|
|
|
499
674
|
if (message.subtype === "init") {
|
|
500
675
|
// New session (or a restarted one). No turn state is reset here: all
|
|
501
676
|
// of it is per-turn (the `turns` FIFO), and dead traces from a
|
|
502
|
-
// previous session
|
|
503
|
-
//
|
|
504
|
-
// before this init was processed
|
|
677
|
+
// previous session were already carried into `rememberedDeadTurns`
|
|
678
|
+
// at run() entry — an init-time clear could unmark a turn the prompt
|
|
679
|
+
// drain already handed to the SDK before this init was processed
|
|
680
|
+
// (#745 review).
|
|
681
|
+
// The durable turn registry is keyed by THIS session id from now on
|
|
682
|
+
// (#886) — including a pre-init submission the prompt drain already
|
|
683
|
+
// pulled, which couldn't be filed before the session id was known.
|
|
684
|
+
this.registrySessionId = message.session_id;
|
|
685
|
+
this.saveRegistry();
|
|
505
686
|
yield {
|
|
506
687
|
type: "session",
|
|
507
688
|
sessionId: message.session_id,
|
|
@@ -696,13 +877,63 @@ export class ClaudeBackend {
|
|
|
696
877
|
idx = this.turns.findIndex((t) => t.interrupted && this.parkedInterrupted.has(t.id) && !this.consumedInterrupted.has(t.id));
|
|
697
878
|
}
|
|
698
879
|
const trace = idx >= 0 ? (this.turns.splice(idx, 1)[0] ?? null) : null;
|
|
880
|
+
if (trace) {
|
|
881
|
+
// The pop retires the turn's outstanding record (#886) — in this
|
|
882
|
+
// instance's own file AND in the session's tombstones: a concurrent
|
|
883
|
+
// resumer may have copied this turn's id into its own file as a dead
|
|
884
|
+
// record while it was still live, and only a tombstone invalidates
|
|
885
|
+
// that replica (rewriting this instance's file does not). A lost
|
|
886
|
+
// tombstone can only revive the copy as a hedged over-recognition —
|
|
887
|
+
// never a fabricated success — so a failure logs and moves on.
|
|
888
|
+
const sid = this.registrySessionId;
|
|
889
|
+
if (sid) {
|
|
890
|
+
try {
|
|
891
|
+
tombstoneTurn(sid, `${this.instanceId}:${trace.id}`);
|
|
892
|
+
}
|
|
893
|
+
catch (err) {
|
|
894
|
+
logger.warn(`[claude-backend] turn tombstone write failed: ${msgOf(err)} (#886)`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
this.saveRegistry();
|
|
898
|
+
}
|
|
699
899
|
let unverifiable = null;
|
|
900
|
+
// #886: an unmatched result with a REMEMBERED outstanding submission is
|
|
901
|
+
// a different report than one with none. Both fail closed — only the
|
|
902
|
+
// honesty of the message differs.
|
|
903
|
+
let unverifiedCompletion = false;
|
|
700
904
|
if (this.classificationPoisoned) {
|
|
701
905
|
unverifiable = "Turn bookkeeping was poisoned by an earlier overflow — no result in this session can be verified";
|
|
702
906
|
}
|
|
703
907
|
else if (!trace) {
|
|
704
|
-
|
|
705
|
-
|
|
908
|
+
const deadId = this.takeOldestRememberedDeadTurn();
|
|
909
|
+
if (deadId !== null) {
|
|
910
|
+
// A turn WAS submitted (before a session restart dropped its trace)
|
|
911
|
+
// and never reported an outcome — this result is most plausibly its
|
|
912
|
+
// late landing. Say exactly that: not a success (unverifiable — the
|
|
913
|
+
// match is inference, not proof), and not a failure either — the
|
|
914
|
+
// turn may have finished real work, and "turn failed, try again"
|
|
915
|
+
// would invite a destructive duplicate retry.
|
|
916
|
+
unverifiedCompletion = true;
|
|
917
|
+
unverifiable =
|
|
918
|
+
`A result (reported as "${resultSubtype}") arrived that matches no live turn, but a turn ` +
|
|
919
|
+
`submitted before the session restarted never reported an outcome — this is most likely that ` +
|
|
920
|
+
`turn completing late. It may have finished real work (files written, changes made), yet I ` +
|
|
921
|
+
`cannot verify which turn it was, so I am NOT counting it as a success. Check what that turn ` +
|
|
922
|
+
`was doing before asking for it again, so the work is not done twice.`;
|
|
923
|
+
logger.warn(`[claude-backend] traceless result (${resultSubtype}) recognized as a likely late landing for a pre-restart submitted turn — completed but unverified, failing closed (#886)`);
|
|
924
|
+
}
|
|
925
|
+
else if (this.registryDegraded) {
|
|
926
|
+
// The recognition record itself is not provably complete — do not
|
|
927
|
+
// assert the negative as confidently as a healthy registry allows.
|
|
928
|
+
unverifiable =
|
|
929
|
+
"The result could not be matched to any submitted turn, and the durable turn record " +
|
|
930
|
+
"could not be fully read or written, so an outstanding earlier submission can't be ruled out";
|
|
931
|
+
logger.warn(`[claude-backend] ${unverifiable} — unverifiable, failing closed (#740/#886)`);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
unverifiable = "The result could not be matched to any submitted turn";
|
|
935
|
+
logger.warn(`[claude-backend] ${unverifiable} — unverifiable, failing closed (#740)`);
|
|
936
|
+
}
|
|
706
937
|
}
|
|
707
938
|
else {
|
|
708
939
|
let crossesGap = false;
|
|
@@ -755,7 +986,13 @@ export class ClaudeBackend {
|
|
|
755
986
|
ok = false;
|
|
756
987
|
yield {
|
|
757
988
|
type: "error",
|
|
758
|
-
|
|
989
|
+
// The recognized late landing's message is self-contained (it says
|
|
990
|
+
// "not counted as a success" itself) and flagged so the panel
|
|
991
|
+
// doesn't frame it as a failure (#886).
|
|
992
|
+
message: unverifiedCompletion
|
|
993
|
+
? unverifiable
|
|
994
|
+
: `${unverifiable} — reporting the turn as unverifiable rather than a success.`,
|
|
995
|
+
...(unverifiedCompletion ? { unverifiedCompletion: true } : {}),
|
|
759
996
|
...stamp,
|
|
760
997
|
};
|
|
761
998
|
}
|
|
@@ -772,6 +1009,17 @@ export class ClaudeBackend {
|
|
|
772
1009
|
...stamp,
|
|
773
1010
|
};
|
|
774
1011
|
}
|
|
1012
|
+
// #885: a SELF landing for the stalled turn (success OR a genuine
|
|
1013
|
+
// error — i.e. not the interrupt's doing) means the turn ended on its
|
|
1014
|
+
// own and no rejection payload ever landed in the transcript, so the
|
|
1015
|
+
// held correction would be stale; drop it. An INTERRUPT landing
|
|
1016
|
+
// (trace.interrupted) is exactly the case the notice exists for.
|
|
1017
|
+
if (this.pendingStallNotice !== null &&
|
|
1018
|
+
trace !== null &&
|
|
1019
|
+
!trace.interrupted &&
|
|
1020
|
+
trace.id >= this.pendingStallNoticeTurn) {
|
|
1021
|
+
this.pendingStallNotice = null;
|
|
1022
|
+
}
|
|
775
1023
|
yield {
|
|
776
1024
|
type: "result",
|
|
777
1025
|
ok,
|