comfyui-mcp 0.52.0 → 0.52.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.
Files changed (84) hide show
  1. package/README.md +0 -7
  2. package/dist/boot.js +41 -0
  3. package/dist/boot.js.map +1 -1
  4. package/dist/comfyui/client.js +6 -27
  5. package/dist/comfyui/client.js.map +1 -1
  6. package/dist/comfyui/cloud-client.js +9 -1
  7. package/dist/comfyui/cloud-client.js.map +1 -1
  8. package/dist/comfyui/upload-target.js +38 -0
  9. package/dist/comfyui/upload-target.js.map +1 -0
  10. package/dist/orchestrator/agent-backend.js.map +1 -1
  11. package/dist/orchestrator/blind-native-gate.js +138 -0
  12. package/dist/orchestrator/blind-native-gate.js.map +1 -0
  13. package/dist/orchestrator/claude-backend.js +172 -50
  14. package/dist/orchestrator/claude-backend.js.map +1 -1
  15. package/dist/orchestrator/codex-backend.js +67 -11
  16. package/dist/orchestrator/codex-backend.js.map +1 -1
  17. package/dist/orchestrator/grok-backend.js +24 -14
  18. package/dist/orchestrator/grok-backend.js.map +1 -1
  19. package/dist/orchestrator/index.js +23 -1
  20. package/dist/orchestrator/index.js.map +1 -1
  21. package/dist/orchestrator/mcp-session-health.js +88 -7
  22. package/dist/orchestrator/mcp-session-health.js.map +1 -1
  23. package/dist/orchestrator/panel-agent.js +159 -6
  24. package/dist/orchestrator/panel-agent.js.map +1 -1
  25. package/dist/orchestrator/panel-tools.js +62 -14
  26. package/dist/orchestrator/panel-tools.js.map +1 -1
  27. package/dist/orchestrator/preview-budget.js +41 -0
  28. package/dist/orchestrator/preview-budget.js.map +1 -0
  29. package/dist/orchestrator/session-store.js +43 -0
  30. package/dist/orchestrator/session-store.js.map +1 -1
  31. package/dist/orchestrator/turn-origins.js +68 -26
  32. package/dist/orchestrator/turn-origins.js.map +1 -1
  33. package/dist/services/download-cache.js +17 -4
  34. package/dist/services/download-cache.js.map +1 -1
  35. package/dist/services/download-jobs.js +136 -1
  36. package/dist/services/download-jobs.js.map +1 -1
  37. package/dist/services/extra-paths.js +57 -4
  38. package/dist/services/extra-paths.js.map +1 -1
  39. package/dist/services/image-management.js +9 -2
  40. package/dist/services/image-management.js.map +1 -1
  41. package/dist/services/listener-ownership.js +17 -5
  42. package/dist/services/listener-ownership.js.map +1 -1
  43. package/dist/services/model-resolver.js +106 -45
  44. package/dist/services/model-resolver.js.map +1 -1
  45. package/dist/services/node-management.js +85 -10
  46. package/dist/services/node-management.js.map +1 -1
  47. package/dist/services/process-control.js +110 -8
  48. package/dist/services/process-control.js.map +1 -1
  49. package/dist/services/queue-monitor.js +54 -7
  50. package/dist/services/queue-monitor.js.map +1 -1
  51. package/dist/services/queue-monitor.stall.test.js +86 -1
  52. package/dist/services/queue-monitor.stall.test.js.map +1 -1
  53. package/dist/services/registry-client.js +31 -18
  54. package/dist/services/registry-client.js.map +1 -1
  55. package/dist/services/ui-bridge.js +80 -0
  56. package/dist/services/ui-bridge.js.map +1 -1
  57. package/dist/services/workflow-settings-extractor.js +1 -1
  58. package/dist/services/workflow-settings-extractor.js.map +1 -1
  59. package/dist/tools/image-management.js +20 -6
  60. package/dist/tools/image-management.js.map +1 -1
  61. package/dist/tools/model-management.js +21 -2
  62. package/dist/tools/model-management.js.map +1 -1
  63. package/locales/ar/main.json +1 -0
  64. package/locales/en/main.json +1 -0
  65. package/locales/es/main.json +1 -0
  66. package/locales/fa/main.json +1 -0
  67. package/locales/fr/main.json +1 -0
  68. package/locales/ja/main.json +1 -0
  69. package/locales/ko/main.json +1 -0
  70. package/locales/pt-BR/main.json +1 -0
  71. package/locales/ru/main.json +1 -0
  72. package/locales/tr/main.json +1 -0
  73. package/locales/zh/main.json +1 -0
  74. package/locales/zh-TW/main.json +1 -0
  75. package/package.json +3 -5
  76. package/plugin/skills/model-registry/SKILL.md +1 -1
  77. package/scripts/check-docs-links.mjs +23 -0
  78. package/scripts/check-docs-mdx.mjs +71 -0
  79. package/scripts/smoke-install.mjs +10 -9
  80. package/dist/services/download-root-correspondence.js +0 -64
  81. package/dist/services/download-root-correspondence.js.map +0 -1
  82. package/model-settings.json +0 -509
  83. package/model-settings.user.jsonc.example +0 -97
  84. package/scripts/postinstall.mjs +0 -29
@@ -20,10 +20,12 @@
20
20
  // panel server (Claude lane) and the stdio `comfyui` child. `route()` already reads
21
21
  // that message for the session id and throws the rest away.
22
22
  //
23
- // This module is only the comparison. It says what was observed and nothing more:
24
- // it does not diagnose WHY a server is missing (still unknown for #1524, and the
25
- // two candidate causes need different fixes), and it does not promise a restart
26
- // would help.
23
+ // This module is the comparison and the wording. The comparison says what was
24
+ // observed and nothing more: it does not diagnose WHY a server is missing
25
+ // (still open for #1524, and the two candidate causes need different fixes).
26
+ // The wording carries the recovery story the same way: a reconnect attempt is
27
+ // reported as attempted, its outcome as whatever the re-read status poll said —
28
+ // never as a promise that a restart or reconnect would help.
27
29
  /**
28
30
  * Compare the MCP servers we handed the session against the ones it reports.
29
31
  *
@@ -84,6 +86,21 @@ export function inspectMcpServers(configured, reported) {
84
86
  * sessions their tools are gone.
85
87
  */
86
88
  const FAILED_STATUSES = new Set(["failed", "needs-auth", "disabled", "error"]);
89
+ /**
90
+ * Whether an automatic reconnect is worth attempting for a degraded status.
91
+ *
92
+ * `failed` / `error` / absent-from-the-report are DROP shapes — re-running the
93
+ * connection can bring the server back. `needs-auth` waits on the user
94
+ * completing an auth flow and `disabled` is a deliberate off switch: an
95
+ * automatic retry of the first claims effort that cannot land, and of the
96
+ * second overrides intent — so neither is retried, only reported.
97
+ */
98
+ export function reconnectableMcpStatus(status) {
99
+ if (status === null)
100
+ return true;
101
+ const s = status.trim().toLowerCase();
102
+ return s === "failed" || s === "error";
103
+ }
87
104
  /**
88
105
  * The line the user sees when a session starts without a server it was given.
89
106
  *
@@ -102,9 +119,73 @@ export function degradedMcpNotice(degraded) {
102
119
  const plural = degraded.length > 1 ? "servers" : "server";
103
120
  const theirTools = degraded.length > 1 ? "Their tools are" : "Its tools are";
104
121
  return (`This session started without ${degraded.length} of its MCP ${plural}: ${named}. ` +
105
- `${theirTools} not available to the agent for the rest of this session, ` +
122
+ `${theirTools} not available to the agent until the server connects, ` +
106
123
  `so anything that needs them will fail or be worked around silently. ` +
107
- `Starting a new session (Disconnect → Connect) is what re-runs the connection; ` +
108
- `whether it succeeds depends on why this one did not, which this message does not know.`);
124
+ `A reconnect is attempted at the next turn boundary for any of them that can be retried, ` +
125
+ `and the outcome is reported; starting a new session (Disconnect → Connect) ` +
126
+ `re-runs the connection from scratch. ` +
127
+ `Whether either succeeds depends on why this one did not, which this message does not know.`);
128
+ }
129
+ /** Format a degraded-server list the way every notice in this module does. */
130
+ function namedDegraded(degraded) {
131
+ return degraded
132
+ .map((d) => (d.status === null ? `\`${d.name}\` (not reported)` : `\`${d.name}\` (${d.status})`))
133
+ .join(", ");
134
+ }
135
+ /**
136
+ * The line the user sees when a server is down and STAYS down past the
137
+ * recovery this session can offer (#1524). `why` names exactly which situation
138
+ * the session is in — the one thing the reader must not have to guess:
139
+ *
140
+ * - `reconnect-failed`: an automatic reconnect was tried and the re-read still
141
+ * reports the server down;
142
+ * - `unverified`: an automatic reconnect was tried but the status re-read that
143
+ * would judge it could not be read — so the outcome is not claimed either
144
+ * way ("did not come back" would be a claim nobody checked);
145
+ * - `not-retriable`: the status (`needs-auth`, `disabled`) is not one an
146
+ * automatic reconnect can clear — nothing was attempted;
147
+ * - `unsupported`: the harness exposes no reconnect to ask for — nothing
148
+ * could be attempted.
149
+ *
150
+ * Like `degradedMcpNotice`, it stops at the observation: it does not diagnose
151
+ * why the server is down and does not promise a new session fixes it.
152
+ */
153
+ export function unrecoveredMcpNotice(degraded, why) {
154
+ if (degraded.length === 0)
155
+ return "";
156
+ const plural = degraded.length > 1;
157
+ const reason = why === "reconnect-failed"
158
+ ? `the automatic reconnect did not bring ${plural ? "them" : "it"} back`
159
+ : why === "unverified"
160
+ ? "an automatic reconnect was attempted, but its outcome could not be read back"
161
+ : why === "not-retriable"
162
+ ? "an automatic reconnect cannot clear this status, so none was attempted"
163
+ : "this harness offers no reconnect to ask for";
164
+ return (`MCP ${plural ? "servers" : "server"} ${namedDegraded(degraded)} ${plural ? "are" : "is"} ` +
165
+ `unavailable to the agent — ${reason}. ` +
166
+ `Anything that needs ${plural ? "their" : "its"} tools will fail or be worked around ` +
167
+ `silently for the rest of this session. ` +
168
+ `Starting a new session (Disconnect → Connect) re-runs the connection; ` +
169
+ `whether it succeeds depends on why the server is down, which this message does not know.`);
170
+ }
171
+ /**
172
+ * The line the user sees when a down server reads CONNECTED again (#1524).
173
+ * `byReconnect` distinguishes the two honest shapes: the session's own
174
+ * reconnect attempt landed, or the harness's supervision brought the server
175
+ * back on its own (the reporter watched `comfyui` do exactly this while
176
+ * `panel` stayed down). The claim is only ever what a status poll reported —
177
+ * never that the tools were exercised.
178
+ */
179
+ export function recoveredMcpNotice(names, byReconnect) {
180
+ if (names.length === 0)
181
+ return "";
182
+ const named = names.map((n) => `\`${n}\``).join(", ");
183
+ const plural = names.length > 1;
184
+ const how = byReconnect
185
+ ? `the automatic reconnect brought ${plural ? "them" : "it"} back`
186
+ : `${plural ? "they are" : "it is"} connected again`;
187
+ return (`MCP ${plural ? "servers" : "server"} ${named}: ${how} — ` +
188
+ `${plural ? "their" : "its"} tools are available to the agent. ` +
189
+ `Anything the agent reported as unreachable while the server was down may be worth another try.`);
109
190
  }
110
191
  //# sourceMappingURL=mcp-session-health.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-session-health.js","sourceRoot":"","sources":["../../src/orchestrator/mcp-session-health.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-session-health.js","sourceRoot":"","sources":["../../src/orchestrator/mcp-session-health.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { COMPLETION_DISAGREEMENT_NOTE } from "./download-done-guard.js";
20
20
  import { downloadsAtRiskOfRespawn } from "../services/download-jobs.js";
21
21
  import { orphanedByDeferredRespawnNote } from "../services/panel-secrets.js";
22
22
  import { errorText, promptText } from "./error-text.js";
23
+ import { MAX_SESSION_PREVIEW_ATTACHMENTS, MAX_SESSION_PREVIEW_BYTES, previewByteBudgetLabel, } from "./preview-budget.js";
23
24
  import { dedupeAudioRefs, noAudioPartText } from "./audio-attachment.js";
24
25
  import { runErrorNotice } from "./cli-remedy.js";
25
26
  import { runCompletionDirective } from "./todo-state.js";
@@ -305,6 +306,23 @@ export class PanelAgent {
305
306
  pendingRewind = null;
306
307
  /** Captured from the session's init message; enables resume across restarts. */
307
308
  sessionId = null;
309
+ /** #1516 — cumulative AUTOMATIC previews committed to turns of the CURRENT
310
+ * provider conversation (charged at the drain, the point of fact). Reset
311
+ * whenever the session id changes: a fresh conversation starts at zero. */
312
+ previewImagesCharged = 0;
313
+ /** #1516 — the byte half of the ledger ADOPTED from the durable store (see
314
+ * initialPreviewLedger). Live bytes come from the backend; the total is
315
+ * previewBytesBase + backend.automaticPreviewBytes(). */
316
+ previewBytesBase = 0;
317
+ /** #1516 — automatic previews composed but NOT delivered, accounted where the
318
+ * withholding was decided (session budget at inject, either budget at the
319
+ * drain trim). Diagnostics; each was named in an agent-visible note. */
320
+ previewWithheldSession = 0;
321
+ /** #1516 — the session id the charged ledger belongs to; reconciled lazily. */
322
+ previewLedgerSid = null;
323
+ /** #1516 — durable ledger awaiting adoption once the live session id proves
324
+ * to be the conversation it was recorded against (never applied blindly). */
325
+ pendingPreviewLedger = null;
308
326
  /** Id of the assistant message currently streaming (from message_start), so
309
327
  * stream deltas and the final committed `say` share one bubble id. */
310
328
  streamMsgId = null;
@@ -334,6 +352,7 @@ export class PanelAgent {
334
352
  this.deps = deps;
335
353
  this.model = deps.model;
336
354
  this.effort = deps.effort;
355
+ this.pendingPreviewLedger = deps.initialPreviewLedger ?? null;
337
356
  // Default to the Claude adapter; injectable so a future toggle can swap it.
338
357
  this.backend =
339
358
  backend ??
@@ -343,6 +362,7 @@ export class PanelAgent {
343
362
  systemAppend: deps.systemAppend,
344
363
  panelServer: deps.panelServer,
345
364
  pluginPath: deps.pluginPath,
365
+ isBlind: deps.isBlind,
346
366
  });
347
367
  }
348
368
  short() {
@@ -478,6 +498,78 @@ export class PanelAgent {
478
498
  queuedPreviewImageCount() {
479
499
  return this.queue.reduce((n, it) => n + (it.completionOnly ? (it.images?.length ?? 0) : 0), 0);
480
500
  }
501
+ /** #1516 — keep the cumulative preview ledger attached to the conversation it
502
+ * measures. The session id only becomes known at the init event and can
503
+ * change under us (fresh fork, resume-miss restart, provider switch), so
504
+ * this runs lazily at every budget decision: a new id resets the ledger to
505
+ * zero, and the durable pending ledger is adopted ONLY when the live id is
506
+ * exactly the one it was recorded against — a ledger applied to the wrong
507
+ * conversation would either starve a fresh one or un-bound an old one. */
508
+ reconcilePreviewLedger() {
509
+ if (this.sessionId === this.previewLedgerSid)
510
+ return;
511
+ this.previewLedgerSid = this.sessionId;
512
+ this.previewImagesCharged = 0;
513
+ this.previewBytesBase = 0;
514
+ this.previewWithheldSession = 0;
515
+ if (this.sessionId && this.pendingPreviewLedger) {
516
+ if (this.pendingPreviewLedger.sid === this.sessionId) {
517
+ this.previewImagesCharged = this.pendingPreviewLedger.images;
518
+ this.previewBytesBase = this.pendingPreviewLedger.bytes;
519
+ }
520
+ this.pendingPreviewLedger = null;
521
+ }
522
+ }
523
+ /** #1516 — cumulative delivered automatic-preview BYTES for the current
524
+ * conversation: the durable base (pre-restart) plus what this process's
525
+ * backend actually fetched. Backends that do not report bytes contribute
526
+ * zero and are bounded by the image count alone. */
527
+ sessionPreviewBytes() {
528
+ return this.previewBytesBase + (this.backend.automaticPreviewBytes?.() ?? 0);
529
+ }
530
+ /** #1516 — how many automatic previews this conversation may still receive:
531
+ * the tighter of the remaining COUNT and the remaining BYTES. Bytes
532
+ * delivered so far is what is knowable here — the in-flight turn's fetches
533
+ * have not happened yet — so the byte gate can overshoot by at most one
534
+ * turn's worth; the backend enforces the same budget at fetch time, which
535
+ * is the bound of record (see codex-backend's image loop). */
536
+ sessionPreviewBudgetLeft() {
537
+ this.reconcilePreviewLedger();
538
+ if (this.sessionPreviewBytes() >= MAX_SESSION_PREVIEW_BYTES)
539
+ return 0;
540
+ return Math.max(0, MAX_SESSION_PREVIEW_ATTACHMENTS - this.previewImagesCharged);
541
+ }
542
+ /** #1516 — the cumulative automatic-preview ledger, for diagnostics: what was
543
+ * delivered to the current provider conversation, what was withheld at the
544
+ * session budget, and the budget itself. */
545
+ previewBudgetStatus() {
546
+ this.reconcilePreviewLedger();
547
+ return {
548
+ images: this.previewImagesCharged,
549
+ bytes: this.sessionPreviewBytes(),
550
+ withheld: this.previewWithheldSession,
551
+ budgetImages: MAX_SESSION_PREVIEW_ATTACHMENTS,
552
+ budgetBytes: MAX_SESSION_PREVIEW_BYTES,
553
+ };
554
+ }
555
+ /** #1516 — persist the ledger through the manager so an orchestrator restart
556
+ * does not reset a budget the still-living provider conversation keeps
557
+ * accumulating against. Bytes are read NOW (the in-flight turn's fetches
558
+ * lag), so the stored value is a lower bound — disclosed, and refreshed on
559
+ * every later change. */
560
+ reportPreviewLedger() {
561
+ if (!this.deps.onPreviewLedger)
562
+ return;
563
+ try {
564
+ this.deps.onPreviewLedger(this.tabId, {
565
+ images: this.previewImagesCharged,
566
+ bytes: this.sessionPreviewBytes(),
567
+ });
568
+ }
569
+ catch (err) {
570
+ logger.debug(`[panel-agent ${this.short()}] preview-ledger report failed: ${msgOf(err)}`);
571
+ }
572
+ }
481
573
  /** Drop a still-queued message (the user cancelled/edited it before the agent
482
574
  * got to it). Returns true if it was found and removed; false if it was
483
575
  * already dequeued (the turn started — too late to cancel). */
@@ -536,10 +628,29 @@ export class PanelAgent {
536
628
  // this one, which is why the drain corrects the count rather than trusting
537
629
  // it (see MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS).
538
630
  const previewBudgetLeft = Math.max(0, MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS - this.queuedPreviewImageCount());
631
+ // #1516 — ...AND what is left of the CONVERSATION's cumulative budget.
632
+ // The per-turn bound above stops one turn from going unbounded; this one
633
+ // stops N turns from accumulating 8N inline images into a Codex rollout
634
+ // that compaction then recopies without a media-aware bound. Queued
635
+ // previews are charged against the session budget HERE too (they drain
636
+ // into this conversation), so a burst queueing behind a busy turn cannot
637
+ // over-commit it. `sessionBound` remembers WHICH budget spoke, so the
638
+ // notice below names the bound that actually fired.
639
+ const sessionBudgetLeft = Math.max(0, this.sessionPreviewBudgetLeft() - this.queuedPreviewImageCount());
640
+ const sessionBound = sessionBudgetLeft < previewBudgetLeft;
641
+ const effectiveBudget = sessionBound ? sessionBudgetLeft : previewBudgetLeft;
539
642
  const attachedImgs = correlationIsUntrusted
540
643
  ? []
541
- : attachableImgs.slice(0, previewBudgetLeft);
644
+ : attachableImgs.slice(0, effectiveBudget);
542
645
  const omittedImgs = attachableImgs.length - attachedImgs.length;
646
+ // #1516 — account for what the SESSION budget withheld (diagnostics +
647
+ // durable ledger). Only the session-bound case counts here: a per-turn
648
+ // trim is the pre-existing turn budget doing its job, and an untrusted
649
+ // correlation never had pixels to withhold.
650
+ if (sessionBound && !correlationIsUntrusted && omittedImgs > 0) {
651
+ this.previewWithheldSession += omittedImgs;
652
+ this.reportPreviewLedger();
653
+ }
543
654
  // The unnamed remainder. Every sentence below counts outputs with
544
655
  // `imgs.length` but attaches and names from `attachableImgs`, so on a MIXED
545
656
  // event — one output with a filename, one without — the two disagree and
@@ -614,9 +725,13 @@ export class PanelAgent {
614
725
  attachableImgs.length === 0
615
726
  ? `The panel reported no usable filename for ${imgs.length === 1 ? "it" : "them"}, so ${imgs.length === 1 ? "it is" : "they are"} NOT attached to this agent turn and cannot be fetched by name — check get_history (action:"list") if you need to see what this run produced. `
616
727
  : attachedImgs.length === 0
617
- ? `NONE of these outputs are attached to this agent turn: earlier completion(s) in this same turn already spent its ${MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS}-image preview budget. All ${imgs.length} are shown to the user in the panel — fetch one with get_image action:"get" if you need to look: ${withheldRefs}. `
728
+ ? sessionBound
729
+ ? `NONE of these outputs are attached to this agent turn: this conversation's cumulative automatic-preview budget (${MAX_SESSION_PREVIEW_ATTACHMENTS} images / ~${previewByteBudgetLabel()}) is spent — the bound that keeps a long session from accumulating unbounded inline media the provider retains and recopies at compaction. All ${imgs.length} are shown to the user in the panel — fetch one with get_image action:"get" if you need to look: ${withheldRefs}. `
730
+ : `NONE of these outputs are attached to this agent turn: earlier completion(s) in this same turn already spent its ${MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS}-image preview budget. All ${imgs.length} are shown to the user in the panel — fetch one with get_image action:"get" if you need to look: ${withheldRefs}. `
618
731
  : omittedImgs > 0
619
- ? `The first ${attachedImgs.length} image(s) are attached below; all ${imgs.length} outputs are already shown to the user in the panel. ${omittedImgs} further preview(s) were omitted to keep this TURN's image context bounded — fetch one with get_image action:"get" if you need it: ${withheldRefs}. `
732
+ ? sessionBound
733
+ ? `The first ${attachedImgs.length} image(s) are attached below; all ${imgs.length} outputs are already shown to the user in the panel. ${omittedImgs} further preview(s) were omitted because this conversation's cumulative automatic-preview budget (${MAX_SESSION_PREVIEW_ATTACHMENTS} images / ~${previewByteBudgetLabel()}) is nearly spent — fetch one with get_image action:"get" if you need it: ${withheldRefs}. `
734
+ : `The first ${attachedImgs.length} image(s) are attached below; all ${imgs.length} outputs are already shown to the user in the panel. ${omittedImgs} further preview(s) were omitted to keep this TURN's image context bounded — fetch one with get_image action:"get" if you need it: ${withheldRefs}. `
620
735
  : `The image(s) are attached below and already shown to the user in the panel. `
621
736
  : `You cannot view images on this provider, but they are already shown to the user in the panel. `
622
737
  : ``) +
@@ -647,7 +762,10 @@ export class PanelAgent {
647
762
  // up to the bound above. Past it (or for an UNDETERMINED origin) the text
648
763
  // says so and points at get_image, so a fetch is needed but never a guess.
649
764
  if (this.backend.capabilities.vision) {
650
- images = attachedImgs.map((i) => ({ ...i, type: i.type ?? "output" }));
765
+ // `automatic` is what the backend's cumulative byte ledger keys on —
766
+ // a user's explicit attachment never carries it and never spends the
767
+ // session preview budget (#1516).
768
+ images = attachedImgs.map((i) => ({ ...i, type: i.type ?? "output", automatic: true }));
651
769
  }
652
770
  }
653
771
  else if (ev.kind === "ask_answer") {
@@ -1155,17 +1273,34 @@ export class PanelAgent {
1155
1273
  // corrects the notices instead of assuming they agree with the outcome.
1156
1274
  // A user's own attachment is never touched — only `completionOnly` items,
1157
1275
  // which are the injected panel events.
1158
- let previewBudget = MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS;
1276
+ // The drain is also where the CUMULATIVE session budget (#1516) meets the
1277
+ // fact of the turn: the ceiling is the tighter of the per-turn 8 and what
1278
+ // the conversation has left. injectEvent already charged queued previews
1279
+ // against its estimate, so an in-flight batch (invisible there) is the
1280
+ // divergence this line absorbs — same estimate/fact split as the per-turn
1281
+ // budget above.
1282
+ let previewBudget = Math.min(MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS, this.sessionPreviewBudgetLeft());
1159
1283
  const trimmedPreviews = [];
1284
+ let deliveredPreviews = 0;
1160
1285
  let images = batch.flatMap((it) => {
1161
1286
  const refs = it.images ?? [];
1162
1287
  if (!it.completionOnly)
1163
1288
  return refs;
1164
1289
  const take = refs.slice(0, Math.max(0, previewBudget));
1165
1290
  previewBudget -= take.length;
1291
+ deliveredPreviews += take.length;
1166
1292
  trimmedPreviews.push(...refs.slice(take.length));
1167
1293
  return take;
1168
1294
  });
1295
+ // Charge the conversation for what THIS turn actually carries, and persist
1296
+ // the ledger so a restart does not reset the budget of a conversation that
1297
+ // lives on. A replayed (interrupted-then-requeued) batch charges AGAIN —
1298
+ // deliberately: the replay really does write those pixels into the
1299
+ // provider thread a second time, which is the accumulation being bounded.
1300
+ if (deliveredPreviews > 0) {
1301
+ this.previewImagesCharged += deliveredPreviews;
1302
+ this.reportPreviewLedger();
1303
+ }
1169
1304
  // A trim here means a notice ABOVE is now wrong: it was composed when its
1170
1305
  // images were going to ride this turn, and they are not. Measured sequence
1171
1306
  // — 8 previews in flight, a second completion queued behind them (its own
@@ -1182,9 +1317,13 @@ export class PanelAgent {
1182
1317
  if (trimmedPreviews.length && this.backend.capabilities.vision) {
1183
1318
  text +=
1184
1319
  `\n\n[panel note: ${trimmedPreviews.length} automatic preview image(s) were NOT attached to this turn. ` +
1185
- `Several run completions were merged into one turn and they SHARE a single ${MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS}-image budget, ` +
1320
+ `Several run completions were merged into one turn and they SHARE one bounded automatic-preview budget (at most ${MAX_RUN_COMPLETION_IMAGE_ATTACHMENTS} per turn, and a cumulative per-conversation ceiling of ${MAX_SESSION_PREVIEW_ATTACHMENTS} images / ~${previewByteBudgetLabel()}), ` +
1186
1321
  `so a notice above may say its images are "attached below" when they are not — where they disagree, THIS note is the accurate one. ` +
1187
1322
  `They are outputs on the ComfyUI side that this turn does not carry — fetch any of them with get_image action:"get" — ${describeImageRefs(trimmedPreviews)}.]`;
1323
+ // These previews are withheld from the conversation for good (the note
1324
+ // redirects to get_image), so the diagnostics counter accounts them.
1325
+ this.previewWithheldSession += trimmedPreviews.length;
1326
+ this.reportPreviewLedger();
1188
1327
  logger.info(`[panel-agent ${this.short()}] trimmed ${trimmedPreviews.length} automatic preview image(s) at the drain ` +
1189
1328
  `(completions merged into one turn); the turn says so and names them (#1516)`);
1190
1329
  }
@@ -1985,6 +2124,19 @@ export class PanelAgentManager {
1985
2124
  this.opts.sessionStore?.set(id, sid, this.opts.identityForKey?.(id));
1986
2125
  this.opts.onSession?.(id, sid, model);
1987
2126
  },
2127
+ // #1516 — persist the cumulative preview ledger beside the session id it
2128
+ // belongs to. setPreviewLedger REFUSES a session mismatch, so a ledger is
2129
+ // only ever stored against the exact conversation it measures — a stale
2130
+ // report for a since-replaced session is dropped, not misfiled.
2131
+ onPreviewLedger: (id, ledger) => {
2132
+ const sid = this.agents.get(id)?.sessionId;
2133
+ if (sid)
2134
+ this.opts.sessionStore?.setPreviewLedger(id, sid, ledger);
2135
+ },
2136
+ // #1516 — hand the freshly-spawned agent the durable ledger of the
2137
+ // conversation it is expected to resume; the agent adopts it only when
2138
+ // the live session id proves to be that exact conversation.
2139
+ initialPreviewLedger: this.opts.sessionStore?.previewLedger(tabId),
1988
2140
  onTurnAnchor: this.opts.onTurnAnchor,
1989
2141
  // Wrap onTurn so the manager learns when a turn ends — the safe point to
1990
2142
  // apply a deferred, session-restarting effort change.
@@ -2004,6 +2156,7 @@ export class PanelAgentManager {
2004
2156
  onEventUndelivered: this.opts.onEventUndelivered,
2005
2157
  panelServer: this.opts.makePanelServer?.(tabId),
2006
2158
  pluginPath: this.opts.pluginPath,
2159
+ isBlind: this.opts.isBlind,
2007
2160
  }, backend);
2008
2161
  }
2009
2162
  /** Update the system-prompt append used for NEWLY-spawned agents. Lets the