@adhdev/daemon-core 0.9.82-rc.552 → 0.9.82-rc.554

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 (39) hide show
  1. package/dist/cli-adapter-types.d.ts +29 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +80 -1
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +55 -0
  4. package/dist/cli-adapters/terminal-screen.d.ts +5 -0
  5. package/dist/commands/stream-commands.d.ts +31 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +789 -15
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +785 -12
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/mesh/mesh-ledger.d.ts +1 -1
  12. package/dist/mesh/mesh-refine-gates.d.ts +86 -0
  13. package/dist/providers/cli-provider-instance.d.ts +97 -0
  14. package/dist/providers/provider-instance.d.ts +9 -0
  15. package/dist/providers/spec/adapter.d.ts +7 -0
  16. package/dist/providers/spec/cli-adapter.d.ts +64 -0
  17. package/dist/providers/spec/fsm-driver.d.ts +11 -0
  18. package/dist/repo-mesh-types.d.ts +23 -0
  19. package/package.json +3 -3
  20. package/src/cli-adapter-types.ts +29 -0
  21. package/src/cli-adapters/provider-cli-adapter.ts +135 -0
  22. package/src/cli-adapters/provider-cli-shared.ts +172 -0
  23. package/src/cli-adapters/terminal-screen.ts +5 -0
  24. package/src/commands/handler.ts +4 -0
  25. package/src/commands/router-refine.ts +40 -1
  26. package/src/commands/router.ts +15 -0
  27. package/src/commands/stream-commands.ts +125 -0
  28. package/src/index.ts +1 -0
  29. package/src/mesh/coordinator-prompt.ts +2 -0
  30. package/src/mesh/mesh-events-utils.ts +15 -0
  31. package/src/mesh/mesh-ledger.ts +6 -0
  32. package/src/mesh/mesh-refine-gates.ts +256 -0
  33. package/src/providers/cli-provider-instance.ts +277 -6
  34. package/src/providers/provider-instance-manager.ts +11 -0
  35. package/src/providers/provider-instance.ts +10 -0
  36. package/src/providers/spec/adapter.ts +7 -0
  37. package/src/providers/spec/cli-adapter.ts +122 -0
  38. package/src/providers/spec/fsm-driver.ts +5 -0
  39. package/src/repo-mesh-types.ts +35 -0
package/dist/index.js CHANGED
@@ -124,6 +124,15 @@ function resolveDelegatedWorkerAutoApprove(meshPolicy, nodePolicy) {
124
124
  }
125
125
  return true;
126
126
  }
127
+ function resolveAllowSendKeysDestructive(meshPolicy, nodePolicy) {
128
+ if (typeof nodePolicy?.allowSendKeysDestructive === "boolean") {
129
+ return nodePolicy.allowSendKeysDestructive;
130
+ }
131
+ if (typeof meshPolicy?.allowSendKeysDestructive === "boolean") {
132
+ return meshPolicy.allowSendKeysDestructive;
133
+ }
134
+ return false;
135
+ }
127
136
  function resolveProviderMaxParallel(slots, providerType) {
128
137
  const wanted = typeof providerType === "string" ? providerType.trim().toLowerCase() : "";
129
138
  if (!wanted) return void 0;
@@ -419,10 +428,10 @@ function readInjected(value) {
419
428
  }
420
429
  function getDaemonBuildInfo() {
421
430
  if (cached) return cached;
422
- const commit = readInjected(true ? "178a437592e2f9544675a54768b48281804bdddb" : void 0) ?? "unknown";
423
- const commitShort = readInjected(true ? "178a4375" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
424
- const version = readInjected(true ? "0.9.82-rc.552" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
425
- const builtAt = readInjected(true ? "2026-07-17T04:25:41.391Z" : void 0);
431
+ const commit = readInjected(true ? "23529650799df69f9a14f16f6c2b34db153f1b27" : void 0) ?? "unknown";
432
+ const commitShort = readInjected(true ? "23529650" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
433
+ const version = readInjected(true ? "0.9.82-rc.554" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
434
+ const builtAt = readInjected(true ? "2026-07-17T07:57:39.144Z" : void 0);
426
435
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
427
436
  return cached;
428
437
  }
@@ -2988,6 +2997,8 @@ var init_dist = __esm({
2988
2997
  "mesh_send_task",
2989
2998
  "mesh_read_chat",
2990
2999
  "mesh_read_debug",
3000
+ "mesh_read_terminal",
3001
+ "mesh_send_keys",
2991
3002
  "mesh_launch_session",
2992
3003
  "mesh_git_status",
2993
3004
  "mesh_read_node_logs",
@@ -4134,6 +4145,8 @@ var init_coordinator_prompt = __esm({
4134
4145
  | \`mesh_launch_session\` | Start a new agent session on a node |
4135
4146
  | \`mesh_read_chat\` | Read recent chat messages from a delegated agent session |
4136
4147
  | \`mesh_read_debug\` | Collect a daemon-side chat/parser debug bundle for a session |
4148
+ | \`mesh_read_terminal\` | Read a worker session's CURRENT raw terminal screen (the live rendered PTY viewport \u2014 prompt/modal/spinner/unparsed output), not the parsed chat. Byte-bounded (32KiB default, 64KiB max; bottom of screen kept). Use to see exactly what a worker is showing when mesh_read_chat is not enough (e.g. after a stall alert). Screen text may contain secrets \u2014 treat as sensitive |
4149
+ | \`mesh_send_keys\` | Inject a STRUCTURED key sequence into a worker's live PTY (text + named keys ENTER/ESC/CTRL_C/UP/DOWN/LEFT/RIGHT/TAB/BACKSPACE). For interactions mesh_send_task can't express \u2014 answer a non-approval prompt, navigate a picker, submit a typed line, or interrupt (CTRL_C). Use mesh_approve for approval modals (send_keys is refused on one). Destructive keys (CTRL_C/ESC) need confirm_destructive=true AND mesh policy allowSendKeysDestructive. Refused on a pending submit/echo race. Audited (key enums only) |
4137
4150
  | \`mesh_task_history\` | Read the task ledger \u2014 dispatches, completions, failures. Use to understand what has been done before deciding next steps |
4138
4151
  | \`mesh_ledger_query\` | Read-only ledger query along the kind/time/node axes (complement to task-axis mesh_task_history): filter by kind, since, node, tail \u2014 answer "what happened on node X / what failed since T" without scanning transcripts |
4139
4152
  | \`mesh_reconcile_ledger\` | Reconcile daemon-local ledgers over P2P \u2014 import missing entries from remote nodes into the coordinator local ledger |
@@ -8792,6 +8805,13 @@ Do NOT retry on this node. Consider reassigning to a different node or asking th
8792
8805
  return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
8793
8806
  }
8794
8807
  if (args.event === "monitor:no_progress") {
8808
+ if (args.metadataEvent.meshWorkerStall === true) {
8809
+ const observedStatus = readNonEmptyString2(args.metadataEvent.observedStatus);
8810
+ const stalledMs = typeof args.metadataEvent.stalledMs === "number" ? args.metadataEvent.stalledMs : void 0;
8811
+ const stalledSuffix = stalledMs !== void 0 ? ` for ${Math.round(stalledMs / 1e3)}s` : "";
8812
+ const statusSuffix = observedStatus ? ` (observed status: ${observedStatus})` : "";
8813
+ return `[System] ${args.nodeLabel}: PTY output unchanged${stalledSuffix}${statusSuffix}${metadata}. This is an informational stall \u2014 the worker's screen has been static regardless of its reported status; it may be genuinely idle, waiting, or wedged, so this is NOT a failure or auto-restart. Judge whether to inspect it: wait for pendingCoordinatorEvents/a completion event, or make one bounded mesh_read_chat check if you need to see its current screen, then wait again.`;
8814
+ }
8795
8815
  return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
8796
8816
  }
8797
8817
  if (args.event === "worktree_bootstrap_complete") {
@@ -12584,6 +12604,82 @@ function sanitizeTerminalText(str) {
12584
12604
  const accumulator = new TerminalTranscriptAccumulator();
12585
12605
  return stripTerminalNoise(stripAnsi(accumulator.append(str)));
12586
12606
  }
12607
+ function encodeMeshSendKeys(items) {
12608
+ if (!Array.isArray(items) || items.length === 0) {
12609
+ throw new Error("send_keys: sequence must be a non-empty array");
12610
+ }
12611
+ if (items.length > MESH_SEND_KEYS_MAX_ITEMS) {
12612
+ throw new Error(`send_keys: sequence exceeds ${MESH_SEND_KEYS_MAX_ITEMS} items`);
12613
+ }
12614
+ const parts = [];
12615
+ const keys = [];
12616
+ let hasDestructive = false;
12617
+ let submits = false;
12618
+ let textBytes = 0;
12619
+ for (const item of items) {
12620
+ if (item && typeof item.text === "string") {
12621
+ const text = item.text;
12622
+ textBytes += Buffer.byteLength(text, "utf8");
12623
+ if (textBytes > MESH_SEND_KEYS_MAX_TEXT_BYTES) {
12624
+ throw new Error(`send_keys: total literal text exceeds ${MESH_SEND_KEYS_MAX_TEXT_BYTES} bytes`);
12625
+ }
12626
+ parts.push(text);
12627
+ submits = false;
12628
+ continue;
12629
+ }
12630
+ const keyName = item && typeof item.key === "string" ? item.key : "";
12631
+ if (!(keyName in MESH_SEND_KEY_ENCODING)) {
12632
+ throw new Error(`send_keys: unknown key '${keyName}'`);
12633
+ }
12634
+ const key2 = keyName;
12635
+ parts.push(MESH_SEND_KEY_ENCODING[key2]);
12636
+ keys.push(key2);
12637
+ if (MESH_DESTRUCTIVE_KEYS.has(key2)) hasDestructive = true;
12638
+ submits = key2 === "ENTER";
12639
+ }
12640
+ return { sequence: parts.join(""), keys, hasDestructive, submits };
12641
+ }
12642
+ function truncateToByteTailByLine(text, maxBytes) {
12643
+ const input = String(text ?? "");
12644
+ const originalBytes = Buffer.byteLength(input, "utf8");
12645
+ if (originalBytes <= maxBytes) {
12646
+ return { text: input, truncated: false, originalBytes, returnedBytes: originalBytes };
12647
+ }
12648
+ const lines = input.split("\n");
12649
+ const kept = [];
12650
+ let bytes = 0;
12651
+ for (let i = lines.length - 1; i >= 0; i--) {
12652
+ const line = lines[i];
12653
+ const lineBytes = Buffer.byteLength(line, "utf8");
12654
+ const joinCost = kept.length > 0 ? 1 : 0;
12655
+ if (bytes + lineBytes + joinCost > maxBytes) break;
12656
+ bytes += lineBytes + joinCost;
12657
+ kept.unshift(line);
12658
+ }
12659
+ if (kept.length > 0) {
12660
+ const out = kept.join("\n");
12661
+ return {
12662
+ text: out,
12663
+ truncated: true,
12664
+ originalBytes,
12665
+ returnedBytes: Buffer.byteLength(out, "utf8")
12666
+ };
12667
+ }
12668
+ const lastLine = lines[lines.length - 1] ?? "";
12669
+ const buf = Buffer.from(lastLine, "utf8");
12670
+ let slice = buf.subarray(Math.max(0, buf.length - maxBytes));
12671
+ let decoded = slice.toString("utf8");
12672
+ while (decoded.length > 0 && decoded.charCodeAt(0) === 65533 && slice.length > 0) {
12673
+ slice = slice.subarray(1);
12674
+ decoded = slice.toString("utf8");
12675
+ }
12676
+ return {
12677
+ text: decoded,
12678
+ truncated: true,
12679
+ originalBytes,
12680
+ returnedBytes: Buffer.byteLength(decoded, "utf8")
12681
+ };
12682
+ }
12587
12683
  function listCliScriptNames(scripts) {
12588
12684
  if (!scripts) return [];
12589
12685
  return Object.entries(scripts).filter(([, fn]) => typeof fn === "function").map(([name]) => name);
@@ -12785,7 +12881,7 @@ function normalizeCliProviderForRuntime(raw) {
12785
12881
  }
12786
12882
  };
12787
12883
  }
12788
- var os5, path11, TerminalTranscriptAccumulator, buildCliSpawnEnv;
12884
+ var os5, path11, TerminalTranscriptAccumulator, MESH_SEND_KEY_ENCODING, MESH_DESTRUCTIVE_KEYS, MESH_SEND_KEYS_MAX_ITEMS, MESH_SEND_KEYS_MAX_TEXT_BYTES, buildCliSpawnEnv;
12789
12885
  var init_provider_cli_shared = __esm({
12790
12886
  "src/cli-adapters/provider-cli-shared.ts"() {
12791
12887
  "use strict";
@@ -12948,6 +13044,20 @@ var init_provider_cli_shared = __esm({
12948
13044
  this.ensureRow();
12949
13045
  }
12950
13046
  };
13047
+ MESH_SEND_KEY_ENCODING = {
13048
+ ENTER: "\r",
13049
+ ESC: "\x1B",
13050
+ CTRL_C: "",
13051
+ UP: "\x1B[A",
13052
+ DOWN: "\x1B[B",
13053
+ RIGHT: "\x1B[C",
13054
+ LEFT: "\x1B[D",
13055
+ TAB: " ",
13056
+ BACKSPACE: "\x7F"
13057
+ };
13058
+ MESH_DESTRUCTIVE_KEYS = /* @__PURE__ */ new Set(["CTRL_C", "ESC"]);
13059
+ MESH_SEND_KEYS_MAX_ITEMS = 64;
13060
+ MESH_SEND_KEYS_MAX_TEXT_BYTES = 4096;
12951
13061
  buildCliSpawnEnv = import_session_host_core2.sanitizeSpawnEnv;
12952
13062
  }
12953
13063
  });
@@ -18072,6 +18182,10 @@ var init_terminal_screen = __esm({
18072
18182
  getCursorPosition() {
18073
18183
  return this.terminal.getCursorPosition();
18074
18184
  }
18185
+ /** Current viewport dimensions (cols × rows). */
18186
+ getSize() {
18187
+ return { cols: this.cols, rows: this.rows };
18188
+ }
18075
18189
  dispose() {
18076
18190
  this.terminal.dispose();
18077
18191
  }
@@ -26827,11 +26941,12 @@ function appendBoundedText(current, chunk, maxChars) {
26827
26941
  if (current.length <= keepFromCurrent) return current + chunk;
26828
26942
  return current.slice(-keepFromCurrent) + chunk;
26829
26943
  }
26830
- var os16, import_session_host_core6, FORCE_SUBMIT_SETTLE_MS, ProviderCliAdapter;
26944
+ var os16, import_crypto10, import_session_host_core6, FORCE_SUBMIT_SETTLE_MS, ProviderCliAdapter;
26831
26945
  var init_provider_cli_adapter = __esm({
26832
26946
  "src/cli-adapters/provider-cli-adapter.ts"() {
26833
26947
  "use strict";
26834
26948
  os16 = __toESM(require("os"));
26949
+ import_crypto10 = require("crypto");
26835
26950
  init_logger();
26836
26951
  init_debug_config();
26837
26952
  init_terminal_screen();
@@ -27033,6 +27148,11 @@ var init_provider_cli_adapter = __esm({
27033
27148
  static MAX_ACCUMULATED_BUFFER = 262144;
27034
27149
  parsedStatusCache = null;
27035
27150
  static SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
27151
+ // MESH-READ-TERMINAL (feature 2): byte caps for getTerminalScreenSnapshot.
27152
+ // Byte, not char — a multi-byte-glyph screen can exceed an MCP payload cap
27153
+ // while the char count still looks safe. 32KiB default, 64KiB absolute hard cap.
27154
+ static TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
27155
+ static TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
27036
27156
  // (FALSEIDLE Path-C) Consecutive gate-eligible getStatus polls a mesh/autonomous
27037
27157
  // session must show before the poll-static-idle confirm fires. 2 = one extra
27038
27158
  // status tick of hysteresis: enough to reject a single momentary-silence
@@ -28611,6 +28731,103 @@ ${lastSnapshot}`;
28611
28731
  isAlive() {
28612
28732
  return this.ptyProcess !== null;
28613
28733
  }
28734
+ /**
28735
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Narrow, least-privilege
28736
+ * read of the CURRENT rendered viewport for mesh_read_terminal. Deliberately
28737
+ * NARROWER than getSnapshot()/getDebugSnapshot():
28738
+ * - It returns ONLY the terminal's current rendered viewport (what a human
28739
+ * would see on screen right now), the cursor position and the viewport
28740
+ * size. NO debug buffers, NO parser/FSM state, NO scrollback/history.
28741
+ * - It does NOT call getParseScreenText() (which may graft an older snapshot
28742
+ * onto the current frame for parse accuracy) — the caller asked for the
28743
+ * live viewport, not a parse-optimized composite.
28744
+ * - The payload is bounded in BYTES (UTF-8) with bottom-tail preservation so
28745
+ * a screen of multi-byte glyphs can never exceed the MCP payload cap. See
28746
+ * truncateToByteTailByLine.
28747
+ *
28748
+ * SECURITY NOTE: the raw viewport can contain tokens / command args / env
28749
+ * values / user data. Callers MUST gate this on mesh ownership and MUST NOT
28750
+ * log the returned text. Opt-in redaction is intentionally out of scope for
28751
+ * this feature and left as a future enhancement.
28752
+ *
28753
+ * `maxBytes` is clamped to [1KiB, ABSOLUTE_MAX] (default 32KiB, hard cap 64KiB).
28754
+ */
28755
+ getTerminalScreenSnapshot(maxBytes = _ProviderCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES) {
28756
+ const cap = Math.min(
28757
+ _ProviderCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
28758
+ Math.max(1024, Math.floor(maxBytes) || _ProviderCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES)
28759
+ );
28760
+ const rawViewport = this.terminalScreen.getText() || "";
28761
+ const size = this.terminalScreen.getSize();
28762
+ const cursor = this.terminalScreen.getCursorPosition();
28763
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
28764
+ const hash = (0, import_crypto10.createHash)("sha256").update(rawViewport, "utf8").digest("hex").slice(0, 16);
28765
+ return {
28766
+ text: truncation.text,
28767
+ cursor,
28768
+ cols: size.cols,
28769
+ rows: size.rows,
28770
+ truncated: truncation.truncated,
28771
+ originalBytes: truncation.originalBytes,
28772
+ returnedBytes: truncation.returnedBytes,
28773
+ hash
28774
+ };
28775
+ }
28776
+ /**
28777
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key sequence
28778
+ * into the PTY for the mesh_send_keys tool. Reuses the same serialized write
28779
+ * path as sends (writeToPty via writeRaw semantics) so the injection FIFO-
28780
+ * orders behind any in-flight write.
28781
+ *
28782
+ * Two guards run INSIDE this method, immediately before the write, so they see
28783
+ * a consistent snapshot of the adapter's submit/echo/queue state (no async gap
28784
+ * between check and write — the encode is synchronous and the write is chained
28785
+ * atomically after it):
28786
+ *
28787
+ * 1. submit-race recheck (echo-gate): even though writeToPty is FIFO, an
28788
+ * already-SCHEDULED echo-gated Enter (engine.submitPendingUntil in the
28789
+ * future), an armed stuck-submit retry (submitRetryTimer), or an in-flight
28790
+ * pending-outbound flush can submit at a DIFFERENT tick than our write —
28791
+ * so an injected literal could be submitted by a pending Enter, or our
28792
+ * ENTER could submit a half-typed pending body. When any of those is live
28793
+ * we REFUSE the injection rather than interleave.
28794
+ *
28795
+ * 2. modal fail-closed: if the session is parked on an actionable approval
28796
+ * modal, refuse a NON-destructive injection (ENTER/text/arrows) and direct
28797
+ * the caller to mesh_approve — so a modal choice can't be confirmed via
28798
+ * send_keys to bypass the approval policy. (A destructive ESC/CTRL_C, which
28799
+ * dismisses rather than confirms, is allowed to proceed past this gate; it
28800
+ * is separately gated by confirm_destructive + policy at the tool layer.)
28801
+ *
28802
+ * The caller (tool layer) owns the destructive-key double gate and the audit
28803
+ * ledger. This method NEVER logs the literal text (only key enums / byte len).
28804
+ */
28805
+ async injectKeys(items, opts = {}) {
28806
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
28807
+ const encoded = encodeMeshSendKeys(items);
28808
+ const now = Date.now();
28809
+ const submitPending = this.engine.submitPendingUntil > now;
28810
+ const submitRetryArmed = this.submitRetryTimer !== null;
28811
+ const outboundBusy = this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length > 0;
28812
+ if (submitPending || submitRetryArmed || outboundBusy) {
28813
+ LOG.warn("CLI", `[${this.cliType}] send_keys refused (submit_race): submitPending=${submitPending} submitRetry=${submitRetryArmed} outboundBusy=${outboundBusy} keys=${encoded.keys.join(",")}`);
28814
+ return { ok: false, refused: "submit_race", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
28815
+ }
28816
+ const modalActive = this.engine.hasActionableApproval();
28817
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
28818
+ LOG.warn("CLI", `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(",")} \u2014 use mesh_approve`);
28819
+ return { ok: false, refused: "actionable_modal", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
28820
+ }
28821
+ await this.writeToPty(encoded.sequence);
28822
+ LOG.info("CLI", `[${this.cliType}] send_keys injected keys=${encoded.keys.join(",") || "(text-only)"} bytes=${Buffer.byteLength(encoded.sequence, "utf8")} destructive=${encoded.hasDestructive}`);
28823
+ return {
28824
+ ok: true,
28825
+ keys: encoded.keys,
28826
+ hasDestructive: encoded.hasDestructive,
28827
+ submits: encoded.submits,
28828
+ bytes: Buffer.byteLength(encoded.sequence, "utf8")
28829
+ };
28830
+ }
28614
28831
  flushOutboundQueue() {
28615
28832
  this.schedulePendingOutboundFlush();
28616
28833
  }
@@ -29436,6 +29653,7 @@ __export(index_exports, {
29436
29653
  resetConfig: () => resetConfig,
29437
29654
  resetDebugRuntimeConfig: () => resetDebugRuntimeConfig,
29438
29655
  resetState: () => resetState,
29656
+ resolveAllowSendKeysDestructive: () => resolveAllowSendKeysDestructive,
29439
29657
  resolveAutoConvergeCodeChange: () => resolveAutoConvergeCodeChange,
29440
29658
  resolveChatMessageKind: () => resolveChatMessageKind,
29441
29659
  resolveConvergeRequiredTags: () => resolveConvergeRequiredTags,
@@ -39838,6 +40056,63 @@ function handlePtyResize(_h, args) {
39838
40056
  if (!cols || !rows) return { success: false, error: "cols and rows required" };
39839
40057
  return { success: false, error: "PTY resize temporarily disabled", code: "PTY_RESIZE_DISABLED" };
39840
40058
  }
40059
+ function handleReadTerminal(h, args) {
40060
+ const targetSessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : "";
40061
+ const sessionId = targetSessionId || h.currentSession?.sessionId || "";
40062
+ if (!sessionId) return { success: false, error: "targetSessionId required" };
40063
+ const session = h.ctx.sessionRegistry?.get(sessionId);
40064
+ const instanceKey = session?.adapterKey || session?.instanceKey || sessionId;
40065
+ const instance = h.ctx.instanceManager?.getInstance(instanceKey);
40066
+ if (!instance) return { success: false, error: `Session not found: ${sessionId.split("_")[0]}` };
40067
+ if (instance.category !== "cli" || typeof instance.getTerminalScreenSnapshot !== "function") {
40068
+ return { success: false, error: "read_terminal is only supported for CLI (PTY) sessions" };
40069
+ }
40070
+ const requestedMaxBytes = typeof args?.maxBytes === "number" && Number.isFinite(args.maxBytes) ? args.maxBytes : void 0;
40071
+ const snapshot = instance.getTerminalScreenSnapshot(requestedMaxBytes);
40072
+ if (!snapshot) {
40073
+ return { success: false, error: "read_terminal is only available for coordinator-spawned mesh worker sessions" };
40074
+ }
40075
+ LOG.info("Command", `[readTerminal] session=${sessionId.split("_")[0]} bytes=${snapshot.returnedBytes}/${snapshot.originalBytes} truncated=${snapshot.truncated} cols=${snapshot.cols} rows=${snapshot.rows}`);
40076
+ return { success: true, ...snapshot };
40077
+ }
40078
+ async function handleSendKeys(h, args) {
40079
+ const targetSessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : "";
40080
+ const sessionId = targetSessionId || h.currentSession?.sessionId || "";
40081
+ if (!sessionId) return { success: false, error: "targetSessionId required" };
40082
+ const items = Array.isArray(args?.sequence) ? args.sequence : null;
40083
+ if (!items || items.length === 0) {
40084
+ return { success: false, error: "sequence (non-empty array of {text}|{key}) required" };
40085
+ }
40086
+ const session = h.ctx.sessionRegistry?.get(sessionId);
40087
+ const instanceKey = session?.adapterKey || session?.instanceKey || sessionId;
40088
+ const instance = h.ctx.instanceManager?.getInstance(instanceKey);
40089
+ if (!instance) return { success: false, error: `Session not found: ${sessionId.split("_")[0]}` };
40090
+ if (instance.category !== "cli" || typeof instance.injectKeys !== "function") {
40091
+ return { success: false, error: "send_keys is only supported for CLI (PTY) sessions" };
40092
+ }
40093
+ const DESTRUCTIVE = /* @__PURE__ */ new Set(["CTRL_C", "ESC"]);
40094
+ const hasDestructiveRequested = items.some((it) => it && typeof it.key === "string" && DESTRUCTIVE.has(it.key));
40095
+ if (hasDestructiveRequested && args?.confirm_destructive !== true) {
40096
+ return {
40097
+ success: false,
40098
+ error: "destructive key (CTRL_C/ESC) requires confirm_destructive=true",
40099
+ refused: "destructive_unconfirmed"
40100
+ };
40101
+ }
40102
+ try {
40103
+ const result = await instance.injectKeys(items, {
40104
+ allowModalOverride: args?.allow_modal_override === true
40105
+ });
40106
+ if (!result.ok) {
40107
+ LOG.info("Command", `[sendKeys] session=${sessionId.split("_")[0]} refused=${result.refused} keys=${result.keys.join(",")} destructive=${result.hasDestructive}`);
40108
+ return { success: false, error: `send_keys refused: ${result.refused}`, refused: result.refused, keys: result.keys, hasDestructive: result.hasDestructive };
40109
+ }
40110
+ LOG.info("Command", `[sendKeys] session=${sessionId.split("_")[0]} injected keys=${result.keys.join(",") || "(text-only)"} bytes=${result.bytes} destructive=${result.hasDestructive} submits=${result.submits}`);
40111
+ return { success: true, keys: result.keys, hasDestructive: result.hasDestructive, submits: result.submits, bytes: result.bytes };
40112
+ } catch (e) {
40113
+ return { success: false, error: `send_keys: ${e?.message || String(e)}` };
40114
+ }
40115
+ }
39841
40116
  function handleGetProviderSettings(h, args) {
39842
40117
  const loader = h.ctx.providerLoader;
39843
40118
  const { providerType } = args || {};
@@ -40695,6 +40970,12 @@ var DaemonCommandHandler = class {
40695
40970
  return handlePtyInput(this, args);
40696
40971
  case "pty_resize":
40697
40972
  return handlePtyResize(this, args);
40973
+ // ─── MESH-READ-TERMINAL (feature 2): raw viewport read ──────────
40974
+ case "read_terminal":
40975
+ return handleReadTerminal(this, args);
40976
+ // ─── MESH-SEND-KEYS (feature 3): structured key injection ────────
40977
+ case "send_keys":
40978
+ return handleSendKeys(this, args);
40698
40979
  // ─── Provider Settings (stream-commands.ts) ──────────
40699
40980
  case "get_provider_settings":
40700
40981
  return handleGetProviderSettings(this, args);
@@ -43441,6 +43722,12 @@ var TerminalAdapter = class {
43441
43722
  const pos = this.screen.getCursorPosition();
43442
43723
  return { row: pos.row, col: pos.col };
43443
43724
  }
43725
+ /** Current terminal geometry (columns × rows). Tracked here rather than
43726
+ * read off the screen buffer so a resize is reflected immediately, before
43727
+ * the next repaint. Consumed by the mesh_read_terminal viewport read. */
43728
+ getScreenSize() {
43729
+ return { cols: this.cols, rows: this.rows };
43730
+ }
43444
43731
  send_keys(text) {
43445
43732
  this.recordEvent("input", capPreview(escapeControl(text)), text.length);
43446
43733
  this.pty?.write(text);
@@ -43779,6 +44066,9 @@ var FsmDriver = class {
43779
44066
  getScreen() {
43780
44067
  return this.adapter.snapshot();
43781
44068
  }
44069
+ getScreenSize() {
44070
+ return this.adapter.getScreenSize();
44071
+ }
43782
44072
  /** Scrollback-inclusive screen as line array — used only for modal/button
43783
44073
  * content extraction so a tall prompt's off-screen anchors stay matchable.
43784
44074
  * Falls back to the viewport snapshot if scrollback read is unavailable. */
@@ -45817,6 +46107,8 @@ function readTailJsonlLines(filePath, maxBytes) {
45817
46107
 
45818
46108
  // src/providers/spec/cli-adapter.ts
45819
46109
  var fs20 = __toESM(require("fs"));
46110
+ var import_node_crypto4 = require("crypto");
46111
+ init_provider_cli_shared();
45820
46112
  init_logger();
45821
46113
  function stripAnsi3(text) {
45822
46114
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
@@ -45995,6 +46287,105 @@ var SpecCliAdapter = class _SpecCliAdapter {
45995
46287
  isReady() {
45996
46288
  return this.spawned && !this.exited;
45997
46289
  }
46290
+ // Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
46291
+ // The spec path drives the child through the transport/driver rather than a
46292
+ // directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
46293
+ // lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
46294
+ // session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
46295
+ isAlive() {
46296
+ return this.spawned && !this.exited;
46297
+ }
46298
+ // MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
46299
+ // ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
46300
+ // not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
46301
+ // the char count still looks safe.
46302
+ static TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
46303
+ static TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
46304
+ /**
46305
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
46306
+ * of the CURRENT rendered viewport for mesh_read_terminal on the spec path
46307
+ * (claude-cli / antigravity / codex-cli — the native-source providers that
46308
+ * route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
46309
+ * - returns ONLY the driver's current viewport snapshot, the cursor
46310
+ * position and the terminal geometry — NO scrollback, NO parser/FSM
46311
+ * state, NO debug buffers;
46312
+ * - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
46313
+ * screen of multi-byte glyphs can never exceed the MCP payload cap;
46314
+ * - `hash` is over the FULL untruncated viewport so a caller can detect a
46315
+ * screen change across polls even when the returned text was truncated.
46316
+ *
46317
+ * SECURITY: the raw viewport can carry tokens / command args / env / user
46318
+ * data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
46319
+ */
46320
+ getTerminalScreenSnapshot(maxBytes = _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES) {
46321
+ const cap = Math.min(
46322
+ _SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
46323
+ Math.max(1024, Math.floor(maxBytes) || _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES)
46324
+ );
46325
+ let rawViewport = "";
46326
+ try {
46327
+ rawViewport = this.driver.snapshot() || "";
46328
+ } catch {
46329
+ rawViewport = "";
46330
+ }
46331
+ let cursor = { row: 0, col: 0 };
46332
+ try {
46333
+ cursor = this.driver.getCursorPosition();
46334
+ } catch {
46335
+ }
46336
+ let size = { cols: 0, rows: 0 };
46337
+ try {
46338
+ size = this.driver.getScreenSize?.() ?? size;
46339
+ } catch {
46340
+ }
46341
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
46342
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(rawViewport, "utf8").digest("hex").slice(0, 16);
46343
+ return {
46344
+ text: truncation.text,
46345
+ cursor: { col: cursor.col, row: cursor.row },
46346
+ cols: size.cols,
46347
+ rows: size.rows,
46348
+ truncated: truncation.truncated,
46349
+ originalBytes: truncation.originalBytes,
46350
+ returnedBytes: truncation.returnedBytes,
46351
+ hash
46352
+ };
46353
+ }
46354
+ /**
46355
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
46356
+ * sequence into the spec-driven PTY for mesh_send_keys. Mirrors
46357
+ * ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
46358
+ * whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
46359
+ * contiguous string, so a submit key can never be separated from the text
46360
+ * it submits).
46361
+ *
46362
+ * The spec path drives the child through the FsmDriver, not a directly-held
46363
+ * ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
46364
+ * against here (the driver serializes its own writes), so the only guard is
46365
+ * the modal fail-closed: a NON-destructive injection into an actionable
46366
+ * approval modal is refused (use mesh_approve) unless explicitly overridden.
46367
+ * A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
46368
+ * past this gate (the tool layer owns the destructive double-gate + audit).
46369
+ * This method NEVER logs the literal text — only key enums / byte length.
46370
+ */
46371
+ async injectKeys(items, opts = {}) {
46372
+ if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
46373
+ const encoded = encodeMeshSendKeys(items);
46374
+ const modalActive = this.latestState?.status === "approval";
46375
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
46376
+ LOG.warn("SpecAdapter", `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(",")} \u2014 use mesh_approve`);
46377
+ return { ok: false, refused: "actionable_modal", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
46378
+ }
46379
+ this.driver.dispatch({ kind: "pty_write", data: encoded.sequence });
46380
+ LOG.info("SpecAdapter", `[${this.cliType}] send_keys injected keys=${encoded.keys.join(",") || "(text-only)"} bytes=${Buffer.byteLength(encoded.sequence, "utf8")} destructive=${encoded.hasDestructive}`);
46381
+ return {
46382
+ ok: true,
46383
+ keys: encoded.keys,
46384
+ hasDestructive: encoded.hasDestructive,
46385
+ submits: encoded.submits,
46386
+ bytes: Buffer.byteLength(encoded.sequence, "utf8")
46387
+ };
46388
+ }
45998
46389
  setOnStatusChange(cb) {
45999
46390
  this.statusCallback = cb;
46000
46391
  }
@@ -47307,6 +47698,15 @@ var CliProviderInstance = class _CliProviderInstance {
47307
47698
  * with no approval never carries recency and is never held (no regression).
47308
47699
  */
47309
47700
  static APPROVAL_RESUME_GRACE_MS = 18e3;
47701
+ // MESH-STALL-WATCH (feature 1: STALL detection): how long a coordinator-spawned
47702
+ // mesh worker's raw PTY output (lastOutputAt) may stay unchanged before the
47703
+ // status-agnostic stall watchdog fires ONE informational monitor:no_progress
47704
+ // event. Unlike the StatusMonitor no-progress watchdog (which only runs while a
47705
+ // turn is generating), this observes pure screen stasis regardless of the
47706
+ // reported status — a worker parked idle, wedged mid-turn, or spawned with no
47707
+ // output at all. 180s matches DEFAULT_MONITOR_CONFIG.noProgressThresholdSec so
47708
+ // the two watchdogs agree on the same "long interval" bound.
47709
+ static MESH_WORKER_STALL_THRESHOLD_MS = 18e4;
47310
47710
  adapter;
47311
47711
  context = null;
47312
47712
  events = [];
@@ -47320,6 +47720,16 @@ var CliProviderInstance = class _CliProviderInstance {
47320
47720
  // first sets it; the other becomes a no-op.
47321
47721
  agentReadyEmitted = false;
47322
47722
  generatingStartedAt = 0;
47723
+ // MESH-STALL-WATCH (feature 1): the lastOutputAt value the stall episode is
47724
+ // currently armed against. A stall episode is "the raw PTY output has not
47725
+ // advanced past this anchor". When the adapter has never emitted output
47726
+ // (lastOutputAt === 0) the anchor is the spawn time (this.startedAt) so a
47727
+ // worker that produced NOTHING is still caught. On any new output the anchor
47728
+ // re-arms to the fresh lastOutputAt and meshStallEmittedForAnchor resets, so a
47729
+ // single continuous stall fires AT MOST ONCE and a later stall re-arms cleanly.
47730
+ // -1 = not yet initialised for this session.
47731
+ meshStallAnchorAt = -1;
47732
+ meshStallEmittedForAnchor = false;
47323
47733
  // FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
47324
47734
  // phase (→generating or →waiting_approval). The completedDebouncePending snapshots
47325
47735
  // this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
@@ -48243,6 +48653,25 @@ var CliProviderInstance = class _CliProviderInstance {
48243
48653
  }
48244
48654
  return "";
48245
48655
  }
48656
+ /**
48657
+ * NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
48658
+ * cached for the current turn (lastCompletionSummary), if any. The evidence
48659
+ * probe (completionFinalAssistantEvidence) is a POINT-SAMPLE: on a native-source
48660
+ * provider (antigravity) the parsed screen and the native transcript can both
48661
+ * momentarily yield no in-turn final assistant at the exact instant the
48662
+ * completion gate fires — source='unavailable', missingEvidence=true — even
48663
+ * though a prior poll already read the real answer off native-history and cached
48664
+ * it here (the same value mesh_read_chat.summary shows). Consulting the cache at
48665
+ * emit time lets that already-secured summary count as evidence, so the completion
48666
+ * notification carries the answer instead of completion_diagnostic=missing_final_assistant
48667
+ * with an empty summary. Returns '' when the cache is empty or was reset by the
48668
+ * next turn (see lastCompletionSummary = null on onTurnStarted).
48669
+ */
48670
+ cachedCompletionSummaryContent() {
48671
+ const cached3 = this.lastCompletionSummary;
48672
+ const content = typeof cached3?.content === "string" ? cached3.content.trim() : "";
48673
+ return content;
48674
+ }
48246
48675
  completionFinalAssistantEvidence(parsedMessages, turnStartedAt) {
48247
48676
  const turnClosed = !this.hasAdapterPendingResponse();
48248
48677
  if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
@@ -48306,12 +48735,19 @@ var CliProviderInstance = class _CliProviderInstance {
48306
48735
  const lastVisibleRole = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : null;
48307
48736
  const lastVisibleKind = typeof lastVisible?.kind === "string" ? lastVisible.kind : null;
48308
48737
  const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
48738
+ const cachedSummary = evidence.present ? "" : this.cachedCompletionSummaryContent();
48739
+ const creditedFromCache = !evidence.present && cachedSummary.length > 0;
48740
+ const finalAssistantPresent = evidence.present || creditedFromCache;
48741
+ const finalAssistantEvidenceSource = evidence.present ? evidence.source : creditedFromCache ? "cached-summary" : evidence.source;
48742
+ const clearMissingBlock = creditedFromCache && args.blockReason === "missing_final_assistant";
48743
+ const effectiveBlockReason = clearMissingBlock ? void 0 : args.blockReason;
48309
48744
  return {
48310
48745
  providerType: this.type,
48311
48746
  sessionId: this.instanceId,
48312
48747
  providerSessionId: this.providerSessionId || null,
48313
48748
  workspace: this.workingDir,
48314
- blockReason: args.blockReason,
48749
+ ...effectiveBlockReason ? { blockReason: effectiveBlockReason } : {},
48750
+ ...clearMissingBlock ? { originalBlockReason: args.blockReason } : {},
48315
48751
  emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
48316
48752
  waitedMs: args.waitedMs,
48317
48753
  maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
@@ -48319,8 +48755,9 @@ var CliProviderInstance = class _CliProviderInstance {
48319
48755
  latestVisibleStatus: args.latestVisibleStatus,
48320
48756
  parsedStatus: typeof parsed?.status === "string" ? parsed.status : parseError ? "parse_error" : "unknown",
48321
48757
  parseError: parseError || void 0,
48322
- finalAssistantPresent: evidence.present,
48323
- finalAssistantEvidenceSource: evidence.source,
48758
+ finalAssistantPresent,
48759
+ finalAssistantFromCachedSummary: !evidence.present && cachedSummary.length > 0,
48760
+ finalAssistantEvidenceSource,
48324
48761
  visibleMessageCount: visibleMessages.length,
48325
48762
  lastVisibleRole,
48326
48763
  lastVisibleKind,
@@ -48498,6 +48935,128 @@ var CliProviderInstance = class _CliProviderInstance {
48498
48935
  isMeshWorkerSession() {
48499
48936
  return !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.meshNodeId || this.settings.launchedByCoordinator);
48500
48937
  }
48938
+ /**
48939
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Public read of the
48940
+ * CURRENT rendered PTY viewport for the mesh_read_terminal tool, delegating to
48941
+ * the adapter's narrow getTerminalScreenSnapshot() (viewport + cursor + size
48942
+ * only; no debug buffers / parser state / history; byte-bounded, bottom-tail
48943
+ * preserved).
48944
+ *
48945
+ * Gated on isMeshWorkerSession(): this raw viewport can expose tokens /
48946
+ * command args / env / user data, so only a coordinator-spawned worker session
48947
+ * is readable. The MCP layer ALSO cross-checks mesh/session/node ownership
48948
+ * (isMeshOwnedDelegateSession) — isMeshWorkerSession alone is a broad
48949
+ * "delegated" gate, so the two together block cross-mesh access. Returns null
48950
+ * for a non-mesh session so the daemon command surfaces a clean refusal.
48951
+ */
48952
+ getTerminalScreenSnapshot(maxBytes) {
48953
+ if (!this.isMeshWorkerSession()) return null;
48954
+ if (typeof this.adapter.getTerminalScreenSnapshot !== "function") return null;
48955
+ return this.adapter.getTerminalScreenSnapshot(maxBytes);
48956
+ }
48957
+ /**
48958
+ * MESH-SEND-KEYS (feature 3: key injection). Public entry for the
48959
+ * mesh_send_keys tool, delegating to the adapter's injectKeys() (structured
48960
+ * key encoding + atomic write + submit-race recheck + modal fail-closed).
48961
+ *
48962
+ * Gated on isMeshWorkerSession(): PTY input into a worker is a
48963
+ * coordinator-only capability. The MCP layer ALSO cross-checks mesh/session/
48964
+ * node ownership (isMeshOwnedDelegateSession) and owns the destructive-key
48965
+ * double gate (confirm_destructive + policy) and the audit ledger. Returns a
48966
+ * refusal object for a non-mesh session so the daemon command surfaces a clean
48967
+ * error (never silently writes to a non-worker PTY).
48968
+ */
48969
+ async injectKeys(items, opts = {}) {
48970
+ if (!this.isMeshWorkerSession()) {
48971
+ return { ok: false, refused: "not_mesh_worker", keys: [], hasDestructive: false };
48972
+ }
48973
+ if (typeof this.adapter.injectKeys !== "function") {
48974
+ return { ok: false, refused: "unsupported", keys: [], hasDestructive: false };
48975
+ }
48976
+ return this.adapter.injectKeys(items, opts);
48977
+ }
48978
+ /**
48979
+ * MESH-STALL-WATCH (feature 1: STALL detection). Status-agnostic stall
48980
+ * watchdog for coordinator-spawned mesh worker sessions. Driven by the
48981
+ * ProviderInstanceManager's existing 5s onTick loop (NO new timer) — see
48982
+ * ProviderInstanceManager.startTicking. Reuses the adapter's raw-PTY-output
48983
+ * clock (lastOutputAt, bumped on every output chunk) as the sole signal: if a
48984
+ * live worker's screen has been byte-for-byte unchanged for
48985
+ * MESH_WORKER_STALL_THRESHOLD_MS (180s), fire ONE informational
48986
+ * monitor:no_progress event down the existing task_stalled ledger +
48987
+ * pendingCoordinatorEvent path.
48988
+ *
48989
+ * Deliberately status-agnostic: it does NOT read getStatus()'s reported status
48990
+ * (which would couple it to the generating-only StatusMonitor and the
48991
+ * idle-timeout FSM). A normally-idle worker CAN trip this after 3 quiet
48992
+ * minutes; that is accepted and surfaced as an informational stall (NOT a
48993
+ * failure/auto-restart) so the coordinator judges. getStatus/getState are NOT
48994
+ * called here, so status heartbeats never move the stall anchor.
48995
+ *
48996
+ * Anchoring: the episode arms against the current lastOutputAt; a worker that
48997
+ * has emitted nothing yet (lastOutputAt === 0) anchors on this.startedAt (spawn
48998
+ * time) so a silent spawn is still caught. Any new output re-arms the anchor
48999
+ * and clears the emitted flag, so one continuous stall emits at most once and a
49000
+ * later stall re-arms cleanly.
49001
+ */
49002
+ checkMeshWorkerStall(now = Date.now()) {
49003
+ if (!this.isMeshWorkerSession()) {
49004
+ this.meshStallAnchorAt = -1;
49005
+ this.meshStallEmittedForAnchor = false;
49006
+ return;
49007
+ }
49008
+ if (typeof this.adapter.isAlive === "function" && !this.adapter.isAlive()) {
49009
+ this.meshStallAnchorAt = -1;
49010
+ this.meshStallEmittedForAnchor = false;
49011
+ return;
49012
+ }
49013
+ let lastOutputAt;
49014
+ try {
49015
+ const status = this.adapter.getStatus({ allowParse: false });
49016
+ lastOutputAt = typeof status?.lastOutputAt === "number" && Number.isFinite(status.lastOutputAt) ? status.lastOutputAt : 0;
49017
+ } catch {
49018
+ return;
49019
+ }
49020
+ const anchor = lastOutputAt > 0 ? lastOutputAt : this.startedAt;
49021
+ if (this.meshStallAnchorAt === -1) {
49022
+ this.meshStallAnchorAt = anchor;
49023
+ this.meshStallEmittedForAnchor = false;
49024
+ return;
49025
+ }
49026
+ if (anchor > this.meshStallAnchorAt) {
49027
+ this.meshStallAnchorAt = anchor;
49028
+ this.meshStallEmittedForAnchor = false;
49029
+ return;
49030
+ }
49031
+ if (this.meshStallEmittedForAnchor) return;
49032
+ const stalledMs = now - this.meshStallAnchorAt;
49033
+ if (stalledMs < _CliProviderInstance.MESH_WORKER_STALL_THRESHOLD_MS) return;
49034
+ this.meshStallEmittedForAnchor = true;
49035
+ let observedStatus = "unknown";
49036
+ try {
49037
+ const s2 = this.adapter.getStatus({ allowParse: false })?.status;
49038
+ if (typeof s2 === "string" && s2) observedStatus = s2;
49039
+ } catch {
49040
+ }
49041
+ if (this.isMeshWorkerSession()) {
49042
+ traceMeshEventStage("fired", this.meshTraceCtx("monitor:no_progress"), "mesh_worker_stall_watchdog");
49043
+ }
49044
+ const stalledSec = Math.round(stalledMs / 1e3);
49045
+ this.pushEvent({
49046
+ event: "monitor:no_progress",
49047
+ agentKey: `${this.type}:cli`,
49048
+ elapsedSec: stalledSec,
49049
+ timestamp: now,
49050
+ // MESH-STALL-WATCH marker: buildMeshSystemMessage generalizes the
49051
+ // coordinator message (generating-specific → "PTY output unchanged")
49052
+ // when this is set, since this watchdog fires status-agnostically.
49053
+ meshWorkerStall: true,
49054
+ lastOutputAt: this.meshStallAnchorAt,
49055
+ stalledMs,
49056
+ observedStatus,
49057
+ taskId: this.completingTurnTaskId()
49058
+ });
49059
+ }
48501
49060
  /**
48502
49061
  * AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
48503
49062
  * persist before the in-progress settle gate is torn down. For a delegated
@@ -48803,7 +49362,13 @@ var CliProviderInstance = class _CliProviderInstance {
48803
49362
  // delegated session's inbox preview blank — or, for a LOCAL worktree session,
48804
49363
  // stuck on the dispatched user task. If the parser DID surface assistant text,
48805
49364
  // prefer it; only fall back to '' when no assistant summary can be derived.
48806
- finalSummary: blockReason.startsWith("parsed_status:") ? this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) ?? "" : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
49365
+ // NOTIF Defect-B: completionFinalSummary is a point-sample of native-history/
49366
+ // screen at THIS instant; on a native-source provider (antigravity) it can be
49367
+ // empty at the forced-emit instant even though a prior poll already cached the
49368
+ // real answer (lastCompletionSummary). Fall back to the cache so the notification
49369
+ // carries the summary that mesh_read_chat.summary already shows — consistent with
49370
+ // completionDiagnostic.finalAssistantPresent being credited from the same cache.
49371
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) || this.cachedCompletionSummaryContent() || (blockReason.startsWith("parsed_status:") ? "" : void 0),
48807
49372
  completionDiagnostic
48808
49373
  });
48809
49374
  this.completedDebouncePending = null;
@@ -49459,6 +50024,10 @@ ${buttons.join("\n")}`;
49459
50024
  this.completedDebouncePending = null;
49460
50025
  continue;
49461
50026
  }
50027
+ if (me.type === "monitor:no_progress" && this.isMeshWorkerSession()) {
50028
+ traceMeshEventDrop("mesh_worker_stall_watchdog_owns_no_progress", this.meshTraceCtx("monitor:no_progress"));
50029
+ continue;
50030
+ }
49462
50031
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
49463
50032
  }
49464
50033
  }
@@ -60495,6 +61064,150 @@ ${e?.stderr || ""}`
60495
61064
  };
60496
61065
  }
60497
61066
  }
61067
+ async function classifyPatchEquivalenceFailure(repoRoot, baseHead, branchHead, summary, options = {}) {
61068
+ const targetBaseRef = options.targetBaseRef || baseHead;
61069
+ const autoPublish = options.autoPublishSubmoduleMainCommits;
61070
+ const evidence = {
61071
+ baseHead,
61072
+ branchHead,
61073
+ mergeBase: summary.mergeBase,
61074
+ expectedPatchId: summary.expectedPatchId,
61075
+ actualPatchId: summary.actualPatchId,
61076
+ patchIdEqual: !!summary.expectedPatchId && summary.expectedPatchId === summary.actualPatchId,
61077
+ ...autoPublish !== void 0 ? { autoPublishSubmoduleMainCommits: autoPublish } : {}
61078
+ };
61079
+ try {
61080
+ const git = (args) => (0, import_node_child_process6.execFileSync)(GIT2, args, {
61081
+ cwd: repoRoot,
61082
+ encoding: "utf8",
61083
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
61084
+ windowsHide: true
61085
+ });
61086
+ const gitOk = (args) => {
61087
+ try {
61088
+ git(args);
61089
+ return true;
61090
+ } catch {
61091
+ return false;
61092
+ }
61093
+ };
61094
+ let ahead = 0;
61095
+ let behind = 0;
61096
+ try {
61097
+ const out = git(["rev-list", "--left-right", "--count", `${targetBaseRef}...${branchHead}`]).trim();
61098
+ const [left, right] = out.split(/\s+/).map((n) => Number.parseInt(n, 10));
61099
+ behind = Number.isFinite(left) ? left : 0;
61100
+ ahead = Number.isFinite(right) ? right : 0;
61101
+ } catch {
61102
+ }
61103
+ evidence.ahead = ahead;
61104
+ evidence.behind = behind;
61105
+ const baseIsAncestor = gitOk(["merge-base", "--is-ancestor", targetBaseRef, branchHead]);
61106
+ evidence.baseDiverged = !baseIsAncestor;
61107
+ let diffStat = "";
61108
+ try {
61109
+ if (summary.mergedTree) {
61110
+ diffStat = git(["diff", "--stat", baseHead, summary.mergedTree]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
61111
+ } else {
61112
+ diffStat = git(["diff", "--stat", baseHead, branchHead]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
61113
+ }
61114
+ } catch {
61115
+ }
61116
+ if (diffStat) evidence.diffStat = diffStat;
61117
+ const submoduleGitlinks = [];
61118
+ try {
61119
+ const nameStatus = git(["diff", "--name-only", "--diff-filter=d", baseHead, branchHead]).trim();
61120
+ const changedPaths = nameStatus ? nameStatus.split("\n").map((p) => p.trim()).filter(Boolean) : [];
61121
+ for (const p of changedPaths) {
61122
+ let baseCommit;
61123
+ let branchCommit;
61124
+ try {
61125
+ const baseLs = git(["ls-tree", baseHead, "--", p]).trim();
61126
+ const branchLs = git(["ls-tree", branchHead, "--", p]).trim();
61127
+ const isGitlink = /(^|\s)160000\s/.test(baseLs) || /(^|\s)160000\s/.test(branchLs);
61128
+ if (!isGitlink) continue;
61129
+ baseCommit = baseLs.split(/\s+/)[2];
61130
+ branchCommit = branchLs.split(/\s+/)[2];
61131
+ } catch {
61132
+ continue;
61133
+ }
61134
+ const submoduleRepo = (0, import_path14.join)(repoRoot, p);
61135
+ let fastForward;
61136
+ let reachableFromOriginMain;
61137
+ if (branchCommit) {
61138
+ if (baseCommit) {
61139
+ fastForward = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", baseCommit, branchCommit]);
61140
+ }
61141
+ reachableFromOriginMain = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", branchCommit, "refs/remotes/origin/main"]);
61142
+ }
61143
+ submoduleGitlinks.push({ path: p, baseCommit, branchCommit, fastForward, reachableFromOriginMain });
61144
+ }
61145
+ } catch {
61146
+ }
61147
+ if (submoduleGitlinks.length) evidence.submoduleGitlinks = submoduleGitlinks;
61148
+ const gitlinkFf = summary.gitlinkTrivialFastForward;
61149
+ const noResidualDiff = !evidence.diffStat && (!summary.actualPatchId || summary.actualPatchId === "");
61150
+ if (ahead === 0 && behind === 0 && noResidualDiff) {
61151
+ return {
61152
+ detailedReason: "already_converged",
61153
+ detailedReasonDescription: "Branch is already identical to the target base (ahead 0, behind 0, no residual diff); the merge would be a no-op.",
61154
+ recommendedAction: "Treat as already converged \u2014 no merge needed. Verify with `git range-diff` / patch-id, then mark the branch merged (or clean up the worktree).",
61155
+ evidence
61156
+ };
61157
+ }
61158
+ const unreachable = submoduleGitlinks.filter((g) => g.reachableFromOriginMain === false);
61159
+ if (unreachable.length > 0) {
61160
+ const paths = unreachable.map((g) => g.path).join(", ");
61161
+ return {
61162
+ detailedReason: "submodule_unreachable",
61163
+ detailedReasonDescription: `Submodule gitlink commit(s) not reachable from submodule origin/main (publish needed): ${paths}.`,
61164
+ recommendedAction: `Publish the submodule commit(s) to submodule origin/main, then retry mesh_refine_node (policy allowAutoPublishSubmoduleMainCommits=${autoPublish === void 0 ? "unknown" : autoPublish}).`,
61165
+ evidence
61166
+ };
61167
+ }
61168
+ const changedGitlinks = submoduleGitlinks.length > 0;
61169
+ const allGitlinksFf = changedGitlinks && submoduleGitlinks.every((g) => g.fastForward === true);
61170
+ const gateSawUnresolvedGitlinkFf = gitlinkFf?.resolved === false && Array.isArray(gitlinkFf.gitlinks) && gitlinkFf.gitlinks.some((g) => g.fastForward);
61171
+ if (baseIsAncestor && (evidence.patchIdEqual || allGitlinksFf || gateSawUnresolvedGitlinkFf)) {
61172
+ return {
61173
+ detailedReason: "trivial_ff_misjudgment",
61174
+ detailedReasonDescription: "HEAD descends the target base and the patch content matches; the block is a submodule gitlink trivial fast-forward that merge-tree refused, not a real divergence.",
61175
+ recommendedAction: "Converge via the strict fast-forward-only bypass (verify HEAD descends origin/main and patch-id equality, then merge --ff-only) instead of the refine gate.",
61176
+ evidence
61177
+ };
61178
+ }
61179
+ if (!baseIsAncestor) {
61180
+ return {
61181
+ detailedReason: "base_divergence",
61182
+ detailedReasonDescription: `Worktree base has diverged from ${targetBaseRef} (HEAD is not a descendant; ahead ${ahead}, behind ${behind}).`,
61183
+ recommendedAction: `Rebase the branch onto ${targetBaseRef}, then retry mesh_refine_node.`,
61184
+ evidence
61185
+ };
61186
+ }
61187
+ return {
61188
+ detailedReason: "actual_patch_diff",
61189
+ detailedReasonDescription: "The merge introduces content not equivalent to the branch's cumulative patch (expected tree vs actual merge diff differ).",
61190
+ recommendedAction: "Manual review required \u2014 inspect the residual diff; the branch content is not patch-equivalent to a clean merge onto the base.",
61191
+ evidence
61192
+ };
61193
+ } catch (e) {
61194
+ evidence.classifierError = e?.message || String(e);
61195
+ return {
61196
+ detailedReason: "unclassified",
61197
+ detailedReasonDescription: "Patch-equivalence sub-cause could not be classified (git inspection failed); see classifierError.",
61198
+ recommendedAction: "Inspect the refineStages and patchEquivalence summary manually to determine the cause.",
61199
+ evidence
61200
+ };
61201
+ }
61202
+ }
61203
+ function execGitOk(cwd, args) {
61204
+ try {
61205
+ (0, import_node_child_process6.execFileSync)(GIT2, args, { cwd, encoding: "utf8", maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES, windowsHide: true });
61206
+ return true;
61207
+ } catch {
61208
+ return false;
61209
+ }
61210
+ }
60498
61211
  async function checkWorktreeChangesPatchEquivalentInRef(repoRoot, ref, worktreeHead) {
60499
61212
  const startedAt = Date.now();
60500
61213
  try {
@@ -61814,9 +62527,23 @@ async function refineSyncBaseStage(self, ctx) {
61814
62527
  error: submoduleHintPatchEquivalence.error,
61815
62528
  actionableHint: submoduleHintPatchEquivalence.actionableHint
61816
62529
  });
62530
+ const classification = await classifyPatchEquivalenceFailure(
62531
+ repoRoot,
62532
+ baseHead,
62533
+ ctx.branchHead,
62534
+ submoduleHintPatchEquivalence,
62535
+ {
62536
+ targetBaseRef: baseHead,
62537
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(ctx.mesh, node.workspace).enabled
62538
+ }
62539
+ );
61817
62540
  return { kind: "terminal", result: {
61818
62541
  success: false,
61819
62542
  code: "patch_equivalence_failed",
62543
+ detailedReason: classification.detailedReason,
62544
+ detailedReasonDescription: classification.detailedReasonDescription,
62545
+ recommendedAction: classification.recommendedAction,
62546
+ evidence: classification.evidence,
61820
62547
  convergenceStatus: "blocked_review",
61821
62548
  error: "Refinery patch-equivalence preflight failed (submodule gitlink conflict); merge/refine was not attempted.",
61822
62549
  branch,
@@ -61956,7 +62683,7 @@ ${tail}` : ""
61956
62683
  return { kind: "continue", ctx };
61957
62684
  }
61958
62685
  async function refinePatchEquivalenceStage(self, ctx) {
61959
- const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, validationSummary, refineStages } = ctx;
62686
+ const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, mesh, node, validationSummary, refineStages } = ctx;
61960
62687
  const branchHead = ctx.branchHead;
61961
62688
  const patchEquivalenceStarted = Date.now();
61962
62689
  const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
@@ -61970,9 +62697,27 @@ async function refinePatchEquivalenceStage(self, ctx) {
61970
62697
  if (!patchEquivalence.equivalent) {
61971
62698
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
61972
62699
  if (!alreadyMergedViaOtherPath) {
62700
+ const classification = await classifyPatchEquivalenceFailure(
62701
+ repoRoot,
62702
+ baseHead,
62703
+ branchHead,
62704
+ patchEquivalence,
62705
+ {
62706
+ targetBaseRef: baseHead,
62707
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace).enabled
62708
+ }
62709
+ );
62710
+ recordMeshRefineStage(refineStages, "patch_equivalence_classification", "failed", patchEquivalenceStarted, {
62711
+ detailedReason: classification.detailedReason,
62712
+ recommendedAction: classification.recommendedAction
62713
+ });
61973
62714
  return { kind: "terminal", result: {
61974
62715
  success: false,
61975
62716
  code: "patch_equivalence_failed",
62717
+ detailedReason: classification.detailedReason,
62718
+ detailedReasonDescription: classification.detailedReasonDescription,
62719
+ recommendedAction: classification.recommendedAction,
62720
+ evidence: classification.evidence,
61976
62721
  convergenceStatus: "blocked_review",
61977
62722
  error: "Refinery patch-equivalence preflight failed; merge/refine was not attempted.",
61978
62723
  branch,
@@ -63079,6 +63824,10 @@ async function finishMeshRefineJob(self, handle, args) {
63079
63824
  };
63080
63825
  if (typeof result.error === "string") ctx.error = result.error;
63081
63826
  if (typeof result.blockedReason === "string") ctx.blockedReason = result.blockedReason;
63827
+ if (typeof result.detailedReason === "string") ctx.detailedReason = result.detailedReason;
63828
+ if (typeof result.detailedReasonDescription === "string") ctx.detailedReasonDescription = result.detailedReasonDescription;
63829
+ if (typeof result.recommendedAction === "string") ctx.recommendedAction = result.recommendedAction;
63830
+ if (result.evidence && typeof result.evidence === "object") ctx.evidence = result.evidence;
63082
63831
  if (stage === "patch_equivalence" && result.patchEquivalence) {
63083
63832
  const pe = result.patchEquivalence;
63084
63833
  ctx.details = {
@@ -63086,7 +63835,10 @@ async function finishMeshRefineJob(self, handle, args) {
63086
63835
  actualPatchId: pe.actualPatchId,
63087
63836
  status: pe.status,
63088
63837
  actionableHint: pe.actionableHint,
63089
- error: pe.error
63838
+ error: pe.error,
63839
+ ...typeof result.detailedReason === "string" ? { detailedReason: result.detailedReason } : {},
63840
+ ...typeof result.recommendedAction === "string" ? { recommendedAction: result.recommendedAction } : {},
63841
+ ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {}
63090
63842
  };
63091
63843
  }
63092
63844
  if (stage === "submodule_reachability" && Array.isArray(result.unreachableSubmoduleCommits)) {
@@ -64056,7 +64808,22 @@ var MESH_FORWARDABLE_SESSION_COMMANDS = /* @__PURE__ */ new Set([
64056
64808
  // worker session that reaches the wrong daemon used to fuzzy-inject the task body into that
64057
64809
  // daemon's own CLI session (TASKECHO coordinator self-echo). Forwarding to the owning daemon
64058
64810
  // delivers it to the real worker instead. (findAdapter is also fail-closed as the backstop.)
64059
- "agent_command"
64811
+ "agent_command",
64812
+ // read_terminal (MESH-READ-TERMINAL feature 2): mesh_read_terminal reads the CURRENT
64813
+ // rendered PTY viewport of a specific worker session. The live viewport lives ONLY on the
64814
+ // OWNING session's adapter, so when the target is a REMOTE worker the coordinator has no
64815
+ // local instance and the handler would return 'Session not found' — the exact
64816
+ // remote-worker forwarding gap of mission 6938892f. Forward it to the owning worker daemon
64817
+ // so it reads its own live screen. (It is read-only; unlike the mutations above it makes no
64818
+ // state change, but it is session-scoped identically and must reach the owning daemon.)
64819
+ "read_terminal",
64820
+ // send_keys (MESH-SEND-KEYS feature 3): mesh_send_keys injects a structured key sequence
64821
+ // into a specific worker session's PTY. The live PTY lives ONLY on the OWNING session's
64822
+ // adapter, so a remote-worker target must be forwarded to the owning daemon or the handler
64823
+ // returns 'Session not found' (same class as mission 6938892f). Unlike read_terminal this
64824
+ // MUTATES the worker PTY, so forwarding to the real owner (not a wrong local session) is
64825
+ // doubly important. The daemon re-enforces the destructive-key confirm gate after the forward.
64826
+ "send_keys"
64060
64827
  ]);
64061
64828
  function normalizeCommandSource(source) {
64062
64829
  switch (source) {
@@ -66201,7 +66968,13 @@ var ProviderInstanceManager = class {
66201
66968
  if (this.tickTimer) return;
66202
66969
  this.tickInterval = intervalMs || this.tickInterval;
66203
66970
  this.tickTimer = setInterval(async () => {
66971
+ const now = Date.now();
66204
66972
  for (const [id, instance] of this.instances) {
66973
+ try {
66974
+ instance.checkMeshWorkerStall?.(now);
66975
+ } catch (e) {
66976
+ LOG.warn("InstanceMgr", `[InstanceManager] Mesh stall check failed for ${id}: ${e.message}`);
66977
+ }
66205
66978
  try {
66206
66979
  await instance.onTick();
66207
66980
  } catch (e) {
@@ -71674,7 +72447,7 @@ var SessionHostPtyTransportFactory = class {
71674
72447
  };
71675
72448
 
71676
72449
  // src/cli-adapters/raw-terminal-io.ts
71677
- var import_crypto10 = require("crypto");
72450
+ var import_crypto11 = require("crypto");
71678
72451
  var import_session_host_core11 = require("@adhdev/session-host-core");
71679
72452
  var BASE_KEY_SEQUENCES = {
71680
72453
  enter: "\r",
@@ -71772,7 +72545,7 @@ var RawTerminalAttachment = class _RawTerminalAttachment {
71772
72545
  const sessionId = String(options.sessionId || "").trim();
71773
72546
  if (!sessionId) throw new Error("sessionId is required");
71774
72547
  const mode = options.mode || "read";
71775
- const clientId = options.clientId || `raw-terminal-${process.pid}-${(0, import_crypto10.randomUUID)().slice(0, 8)}`;
72548
+ const clientId = options.clientId || `raw-terminal-${process.pid}-${(0, import_crypto11.randomUUID)().slice(0, 8)}`;
71776
72549
  const client = options.client || new import_session_host_core11.SessionHostClient({ endpoint: options.endpoint });
71777
72550
  await client.connect();
71778
72551
  const attachResponse = await client.request({
@@ -73338,6 +74111,7 @@ var V1_CONTRACT_VERSION = "1.0.0";
73338
74111
  resetConfig,
73339
74112
  resetDebugRuntimeConfig,
73340
74113
  resetState,
74114
+ resolveAllowSendKeysDestructive,
73341
74115
  resolveAutoConvergeCodeChange,
73342
74116
  resolveChatMessageKind,
73343
74117
  resolveConvergeRequiredTags,