@adhdev/daemon-core 0.9.82-rc.536 → 0.9.82-rc.538

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.
@@ -41,6 +41,27 @@ export declare class ChatSourceRegistry {
41
41
  private readonly records;
42
42
  /** Snapshot of current state for diagnostics. Does not mutate. */
43
43
  getState(key: ChatSourceSessionKey): ChatSourceState;
44
+ /**
45
+ * Opaque snapshot of a session's FULL record (state + lock + transitions),
46
+ * for callers that need to speculatively `observe()` and then roll back if
47
+ * the resulting decision is undesirable (STICKY-NATIVE hold: a trusted
48
+ * exact-identity session must not flip to PTY on a transient native gap).
49
+ * Returns undefined when the session has no record yet. The snapshot is a
50
+ * shallow copy — transitions array is copied so a later append does not
51
+ * mutate it.
52
+ */
53
+ snapshotRecord(key: ChatSourceSessionKey): {
54
+ state: ChatSourceState;
55
+ lockedSince: number | undefined;
56
+ transitions: ChatSourceTransition[];
57
+ } | undefined;
58
+ /** Restore a previously snapshotted record, undoing a speculative observe.
59
+ * Passing undefined clears the key (it had no record when snapshotted). */
60
+ restoreRecord(key: ChatSourceSessionKey, snapshot: {
61
+ state: ChatSourceState;
62
+ lockedSince: number | undefined;
63
+ transitions: ChatSourceTransition[];
64
+ } | undefined): void;
44
65
  /** Recent transitions, newest last. Empty array when nothing has happened. */
45
66
  getTransitions(key: ChatSourceSessionKey): ReadonlyArray<ChatSourceTransition>;
46
67
  /** Drop a session. Caller should invoke this when the session is destroyed
@@ -105,6 +105,7 @@ export declare class CliStateEngine {
105
105
  * paint and made the engine type "1" repeatedly into the prompt.
106
106
  */
107
107
  modalLostAt: number;
108
+ private modalLostRecheckTimer;
108
109
  private approvalExitTimeout;
109
110
  responseEpoch: number;
110
111
  submitPendingUntil: number;
@@ -230,6 +231,18 @@ export declare class CliStateEngine {
230
231
  finishResponse(): void;
231
232
  private scheduleIdleFinish;
232
233
  private cancelPendingIdleFinish;
234
+ /**
235
+ * Schedule one more settled evaluation while pinned to `waiting_approval`
236
+ * with no actionable modal. The settled FSM normally only re-runs on new
237
+ * PTY output; a provider whose modal cue lingers in a form detectStatus
238
+ * still matches (e.g. kimi's questionPattern hitting the user echo) but
239
+ * whose PTY has gone quiet would never get another evaluation, latching
240
+ * `waiting_approval` forever. This timer guarantees the modal-lost recovery
241
+ * in `applyWaitingApproval` is reached even against a silent PTY. It is a
242
+ * no-op once the FSM leaves `waiting_approval` (the re-evaluation itself
243
+ * takes the recovery branch and clears the state).
244
+ */
245
+ private armModalLostRecheck;
233
246
  private armApprovalExitTimeout;
234
247
  private armIdleFinishCandidate;
235
248
  private shouldDeferIdleTimeoutFinish;
package/dist/index.js CHANGED
@@ -419,10 +419,10 @@ function readInjected(value) {
419
419
  }
420
420
  function getDaemonBuildInfo() {
421
421
  if (cached) return cached;
422
- const commit = readInjected(true ? "e7001a2fe5b5af35d3710c964727f7e4d735e852" : void 0) ?? "unknown";
423
- const commitShort = readInjected(true ? "e7001a2f" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
424
- const version = readInjected(true ? "0.9.82-rc.536" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
425
- const builtAt = readInjected(true ? "2026-07-15T11:34:59.318Z" : void 0);
422
+ const commit = readInjected(true ? "35ac849eca76162066561b27633e11827fa19a73" : void 0) ?? "unknown";
423
+ const commitShort = readInjected(true ? "35ac849e" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
424
+ const version = readInjected(true ? "0.9.82-rc.538" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
425
+ const builtAt = readInjected(true ? "2026-07-15T15:51:55.785Z" : void 0);
426
426
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
427
427
  return cached;
428
428
  }
@@ -5712,6 +5712,8 @@ __export(mesh_work_queue_exports, {
5712
5712
  MESH_TASK_MODES: () => MESH_TASK_MODES,
5713
5713
  MESH_TASK_PRIORITIES: () => MESH_TASK_PRIORITIES,
5714
5714
  NOT_BEFORE_RELATIVE_THRESHOLD_MS: () => NOT_BEFORE_RELATIVE_THRESHOLD_MS,
5715
+ REDRIVE_RECLAIM_REASONS: () => REDRIVE_RECLAIM_REASONS,
5716
+ REDRIVE_SUPERSEDE_WINDOW_MS: () => REDRIVE_SUPERSEDE_WINDOW_MS,
5715
5717
  __clearDirectDispatchesForTests: () => __clearDirectDispatchesForTests,
5716
5718
  __clearMeshQueueForTests: () => __clearMeshQueueForTests,
5717
5719
  __replaceMeshQueueForTests: () => __replaceMeshQueueForTests,
@@ -6007,10 +6009,21 @@ function normalizeMeshCapabilityTags(value) {
6007
6009
  return true;
6008
6010
  });
6009
6011
  }
6010
- function firstProviderPriority(policy) {
6011
- const raw = policy && typeof policy === "object" && !Array.isArray(policy) ? policy.providerPriority : void 0;
6012
- if (!Array.isArray(raw)) return void 0;
6013
- return raw.find((type) => typeof type === "string" && type.trim())?.trim();
6012
+ function readNodeProviderTypes(policy) {
6013
+ const record = policy && typeof policy === "object" && !Array.isArray(policy) ? policy : {};
6014
+ const seen = /* @__PURE__ */ new Set();
6015
+ const out = [];
6016
+ const push = (type) => {
6017
+ const trimmed = typeof type === "string" ? type.trim() : "";
6018
+ if (!trimmed || seen.has(trimmed)) return;
6019
+ seen.add(trimmed);
6020
+ out.push(trimmed);
6021
+ };
6022
+ for (const slot of normalizeNodeCapabilitySlots(record.slots)) push(slot.provider);
6023
+ if (Array.isArray(record.providerPriority)) {
6024
+ for (const type of record.providerPriority) push(type);
6025
+ }
6026
+ return out;
6014
6027
  }
6015
6028
  function readNodeOverride(node, key2) {
6016
6029
  const overrides = node?.userOverrides;
@@ -6023,7 +6036,8 @@ function readNodeReporter(node, key2) {
6023
6036
  return typeof value === "string" && value.trim() ? value.trim() : null;
6024
6037
  }
6025
6038
  function buildMeshNodeCapabilityTags(node, providerType) {
6026
- const provider = typeof providerType === "string" && providerType.trim() ? providerType.trim() : firstProviderPriority(node?.policy);
6039
+ const pinnedProvider = typeof providerType === "string" && providerType.trim() ? providerType.trim() : void 0;
6040
+ const providerTags = pinnedProvider ? [pinnedProvider] : readNodeProviderTypes(node?.policy);
6027
6041
  const worktreeBranch = typeof node?.worktreeBranch === "string" && node.worktreeBranch.trim() ? node.worktreeBranch.trim() : null;
6028
6042
  const os32 = readNodeOverride(node, "platform") ?? readNodeReporter(node, "platform") ?? process.platform;
6029
6043
  const arch2 = readNodeOverride(node, "arch") ?? readNodeReporter(node, "arch") ?? process.arch;
@@ -6031,7 +6045,7 @@ function buildMeshNodeCapabilityTags(node, providerType) {
6031
6045
  ...Array.isArray(node?.capabilities) ? node.capabilities : [],
6032
6046
  `os=${os32}`,
6033
6047
  `arch=${arch2}`,
6034
- ...provider ? [`provider=${provider}`] : [],
6048
+ ...providerTags.map((p) => `provider=${p}`),
6035
6049
  // Worktree nodes automatically expose a "worktree=<branch>" tag so that
6036
6050
  // mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
6037
6051
  // only to the matching worktree node.
@@ -6534,7 +6548,7 @@ function recordMeshToolCall(opts) {
6534
6548
  return { rateLimitExceeded: false, callsInWindow: 0, advisory: null };
6535
6549
  }
6536
6550
  }
6537
- var import_crypto6, ACTIVE_MESH_QUEUE_STATUSES, HISTORICAL_MESH_QUEUE_STATUSES, MESH_TASK_MODES, MESH_TASK_PRIORITIES, NOT_BEFORE_RELATIVE_THRESHOLD_MS, LIVE_DEBUG_READONLY_FORBIDDEN, NEGATION_CUES, NEGATION_WINDOW_TOKENS, GIT_MUTATION_SUBCOMMANDS, GIT_STASH_READONLY_SUBCOMMANDS, DEPENDENCY_FAILURE_TERMINALS, MAX_STRANDED_RECLAIMS;
6551
+ var import_crypto6, ACTIVE_MESH_QUEUE_STATUSES, HISTORICAL_MESH_QUEUE_STATUSES, MESH_TASK_MODES, MESH_TASK_PRIORITIES, NOT_BEFORE_RELATIVE_THRESHOLD_MS, LIVE_DEBUG_READONLY_FORBIDDEN, NEGATION_CUES, NEGATION_WINDOW_TOKENS, GIT_MUTATION_SUBCOMMANDS, GIT_STASH_READONLY_SUBCOMMANDS, DEPENDENCY_FAILURE_TERMINALS, MAX_STRANDED_RECLAIMS, REDRIVE_RECLAIM_REASONS, REDRIVE_SUPERSEDE_WINDOW_MS;
6538
6552
  var init_mesh_work_queue = __esm({
6539
6553
  "src/mesh/mesh-work-queue.ts"() {
6540
6554
  "use strict";
@@ -6603,6 +6617,12 @@ var init_mesh_work_queue = __esm({
6603
6617
  GIT_STASH_READONLY_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "show"]);
6604
6618
  DEPENDENCY_FAILURE_TERMINALS = /* @__PURE__ */ new Set(["failed", "cancelled"]);
6605
6619
  MAX_STRANDED_RECLAIMS = 3;
6620
+ REDRIVE_RECLAIM_REASONS = /* @__PURE__ */ new Set([
6621
+ "delivered_no_turn_deadline",
6622
+ "reclaim_after_unknown_grace",
6623
+ "delivered_not_consumed_redrive"
6624
+ ]);
6625
+ REDRIVE_SUPERSEDE_WINDOW_MS = 5 * 6e4;
6606
6626
  }
6607
6627
  });
6608
6628
 
@@ -16841,7 +16861,8 @@ async function maybeAutoLaunchOneQueueSession(components, meshId, mesh) {
16841
16861
  if (task.targetNodeId && !meshNodeIdMatches(node, task.targetNodeId)) return false;
16842
16862
  if (task.taskMode === "convergence" && node?.isLocalWorktree === true) return false;
16843
16863
  if (task.requiredTags?.length) {
16844
- const priorities = normalizeProviderPriority2(node?.policy);
16864
+ const slotProviders = resolveNodeCapabilitySlots(node).map((s2) => s2.provider).filter(Boolean);
16865
+ const priorities = slotProviders.length ? slotProviders : normalizeProviderPriority2(node?.policy);
16845
16866
  const providerCandidates = priorities.length ? priorities : [void 0];
16846
16867
  return providerCandidates.some(
16847
16868
  (p) => nodeSatisfiesRequiredTags(task.requiredTags, buildMeshNodeCapabilityTags(node, p))
@@ -20430,6 +20451,52 @@ function stopStaleMeshWorker(components, args) {
20430
20451
  LOG.warn("MeshQueue", `stopStaleMeshWorker error for ${sessionId}: ${e?.message || e}`);
20431
20452
  }
20432
20453
  }
20454
+ function supersedeRedriveReclaimForLateCompletion(components, meshId, row, completingSessionId, outcome, args) {
20455
+ if (!row.requeueReason || !REDRIVE_RECLAIM_REASONS.has(row.requeueReason)) return false;
20456
+ if (row.status === "completed" || row.status === "failed" || row.status === "cancelled") return false;
20457
+ const requeuedAtMs = Date.parse(row.requeuedAt ?? "");
20458
+ if (!Number.isFinite(requeuedAtMs)) return false;
20459
+ if (Date.now() - requeuedAtMs > REDRIVE_SUPERSEDE_WINDOW_MS) return false;
20460
+ const reDispatchedSessionId = row.assignedSessionId;
20461
+ if (row.status === "assigned" && reDispatchedSessionId && !sessionIdsEquivalent(reDispatchedSessionId, completingSessionId)) {
20462
+ stopStaleMeshWorker(components, {
20463
+ meshId,
20464
+ sessionId: reDispatchedSessionId,
20465
+ nodeId: row.assignedNodeId,
20466
+ providerType: row.assignedProviderType
20467
+ });
20468
+ }
20469
+ endTaskDispatchInFlight(meshId, row.id);
20470
+ updateTaskStatus(meshId, row.id, outcome === "completed" ? "completed" : "failed");
20471
+ if (!findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
20472
+ try {
20473
+ appendLedgerEntry(meshId, {
20474
+ kind: outcome === "completed" ? "task_completed" : "task_failed",
20475
+ sessionId: completingSessionId,
20476
+ nodeId: readNonEmptyString2(args.nodeId) || readNonEmptyString2(args.metadataEvent.meshNodeId) || void 0,
20477
+ providerType: readNonEmptyString2(args.metadataEvent.providerType) || void 0,
20478
+ payload: {
20479
+ taskId: row.id,
20480
+ event: args.event,
20481
+ source: "redrive_late_completion_supersede",
20482
+ reclaimReason: row.requeueReason,
20483
+ reclaimAgeMs: Date.now() - requeuedAtMs,
20484
+ finalSummary: readNonEmptyString2(args.metadataEvent.finalSummary) || void 0
20485
+ }
20486
+ });
20487
+ } catch {
20488
+ }
20489
+ }
20490
+ LOG.warn("MeshQueue", `Late completion superseded re-drive for task ${row.id} on mesh ${meshId} (reclaimed '${row.requeueReason}' ${Math.round((Date.now() - requeuedAtMs) / 1e3)}s ago; completing session ${completingSessionId}) \u2192 flipped ${outcome}${row.status === "assigned" && reDispatchedSessionId && !sessionIdsEquivalent(reDispatchedSessionId, completingSessionId) ? `, stopped duplicate re-dispatch on ${reDispatchedSessionId}` : ""}`);
20491
+ traceMeshEventDrop("redrive_late_completion_supersede", {
20492
+ taskId: row.id,
20493
+ sessionId: completingSessionId,
20494
+ nodeId: row.assignedNodeId ?? args.nodeId,
20495
+ meshId,
20496
+ event: args.event
20497
+ }, `${row.requeueReason} ${Math.round((Date.now() - requeuedAtMs) / 1e3)}s \u2192 ${outcome}`);
20498
+ return true;
20499
+ }
20433
20500
  function injectMeshSystemMessage(components, args) {
20434
20501
  const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
20435
20502
  const eventNodeId = readNonEmptyString2(args.nodeId) || readNonEmptyString2(args.metadataEvent.meshNodeId);
@@ -20541,6 +20608,7 @@ function injectMeshSystemMessage(components, args) {
20541
20608
  }
20542
20609
  });
20543
20610
  }
20611
+ } else if (strandedRow && supersedeRedriveReclaimForLateCompletion(components, args.meshId, strandedRow, sessionId, outcome, args)) {
20544
20612
  }
20545
20613
  } catch {
20546
20614
  }
@@ -21867,6 +21935,50 @@ async function autoPruneStaleDirectDispatches(components, mesh, selfIds, localDa
21867
21935
  LOG.info("MeshReconcile", `Auto-pruned ${result.prunedCount} orphaned direct dispatch record(s) for mesh ${mesh.id}`);
21868
21936
  }
21869
21937
  }
21938
+ async function pollAssignedTaskTerminalEvidence(components, mesh, row) {
21939
+ const sessionId = readNonEmptyString2(row.assignedSessionId);
21940
+ const nodeId = readNonEmptyString2(row.assignedNodeId);
21941
+ if (!sessionId || !nodeId) return null;
21942
+ const node = (mesh.nodes ?? []).find((n) => n.id === nodeId);
21943
+ const nodeDaemonId = readNonEmptyString2(node?.daemonId);
21944
+ const localDaemonId = readNonEmptyString2(components.statusInstanceId);
21945
+ const isLocalNode = !nodeDaemonId || daemonIdsEquivalent(nodeDaemonId, localDaemonId) || !!components.instanceManager.getInstance(sessionId);
21946
+ const providerType = readNonEmptyString2(row.assignedProviderType);
21947
+ const readArgs = {
21948
+ sessionId,
21949
+ targetSessionId: sessionId,
21950
+ tailLimit: 10,
21951
+ ...node?.workspace ? { workspace: node.workspace } : {},
21952
+ ...providerType ? { agentType: providerType, providerType } : {}
21953
+ };
21954
+ let payload = null;
21955
+ try {
21956
+ if (isLocalNode) {
21957
+ const result = await components.commandHandler?.handle("read_chat", readArgs);
21958
+ if (result && result.success === false) return null;
21959
+ payload = unwrapReadChatPayload(result);
21960
+ } else if (components.dispatchMeshCommand) {
21961
+ const result = await components.dispatchMeshCommand(nodeDaemonId, "read_chat", readArgs);
21962
+ payload = unwrapReadChatPayload(result);
21963
+ if (payload && payload.success === false) return null;
21964
+ } else {
21965
+ return null;
21966
+ }
21967
+ } catch {
21968
+ return null;
21969
+ }
21970
+ if (!payload) return null;
21971
+ if (readChatPayloadStatus(payload) !== "idle") return null;
21972
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
21973
+ const evidence = extractFinalAssistantSummaryEvidence(messages);
21974
+ if (!evidence.finalSummary) return null;
21975
+ const dispatchedAtMs = Date.parse(readNonEmptyString2(row.dispatchTimestamp));
21976
+ const transcriptAtMs = Date.parse(evidence.transcriptMessageAt ?? "");
21977
+ if (!(Number.isFinite(dispatchedAtMs) && Number.isFinite(transcriptAtMs) && transcriptAtMs >= dispatchedAtMs)) {
21978
+ return null;
21979
+ }
21980
+ return "completed";
21981
+ }
21870
21982
  var init_mesh_completion_synthesis = __esm({
21871
21983
  "src/mesh/mesh-completion-synthesis.ts"() {
21872
21984
  "use strict";
@@ -22143,7 +22255,8 @@ function drainAndDeliverApprovalNudges(meshId, drainDaemonIds, localDaemonId, me
22143
22255
  }
22144
22256
  return delivered;
22145
22257
  }
22146
- function recoverStrandedAssignedDispatches(components, meshId, store) {
22258
+ async function recoverStrandedAssignedDispatches(components, mesh, store) {
22259
+ const meshId = mesh.id;
22147
22260
  const assigned = getQueue(meshId, { status: ["assigned"] });
22148
22261
  if (!assigned.length) return;
22149
22262
  const nowMs = Date.now();
@@ -22250,6 +22363,36 @@ function recoverStrandedAssignedDispatches(components, meshId, store) {
22250
22363
  }
22251
22364
  reclaimReason = "reclaim_after_unknown_grace";
22252
22365
  }
22366
+ const terminalEvidence = await pollAssignedTaskTerminalEvidence(components, mesh, row);
22367
+ if (terminalEvidence) {
22368
+ deliveredNoTurnUnknownStreak.delete(streakKey);
22369
+ updateTaskStatus(meshId, row.id, terminalEvidence);
22370
+ if (!findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
22371
+ try {
22372
+ appendLedgerEntry(meshId, {
22373
+ kind: terminalEvidence === "completed" ? "task_completed" : "task_failed",
22374
+ nodeId: row.assignedNodeId,
22375
+ sessionId: row.assignedSessionId,
22376
+ providerType: row.assignedProviderType,
22377
+ payload: {
22378
+ taskId: row.id,
22379
+ event: "agent:generating_completed",
22380
+ source: "redrive_deadline_transcript_evidence"
22381
+ }
22382
+ });
22383
+ } catch {
22384
+ }
22385
+ }
22386
+ LOG.warn("MeshReconcile", `Skipped delivered-no-turn re-drive for task ${row.id} on mesh ${meshId} (node=${row.assignedNodeId ?? "?"} session=${row.assignedSessionId ?? "?"}): worker transcript is idle with a final assistant message after dispatch \u2014 the completion event was lost/late, task is ${terminalEvidence}, NOT re-driving`);
22387
+ traceMeshEventDrop("redrive_deadline_transcript_completed", {
22388
+ taskId: row.id,
22389
+ sessionId: row.assignedSessionId,
22390
+ nodeId: row.assignedNodeId,
22391
+ meshId,
22392
+ event: "agent:generating_completed"
22393
+ }, `${reclaimReason} \u2192 transcript ${terminalEvidence}`);
22394
+ continue;
22395
+ }
22253
22396
  const reclaimedLost = reclaimStrandedAssignedTask(meshId, row.id, {
22254
22397
  reason: reclaimReason,
22255
22398
  ageMs: nowMs - dispatchedAtMs
@@ -22376,7 +22519,7 @@ async function runMeshReconcileTick(components) {
22376
22519
  const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
22377
22520
  if (!daemonHostsMesh(mesh, selfIds)) continue;
22378
22521
  try {
22379
- recoverStrandedAssignedDispatches(components, mesh.id, store);
22522
+ await recoverStrandedAssignedDispatches(components, mesh, store);
22380
22523
  } catch (e) {
22381
22524
  LOG.warn("MeshReconcile", `Assigned-stranded watchdog failed for mesh ${mesh.id}: ${e?.message || e}`);
22382
22525
  }
@@ -24551,6 +24694,70 @@ function modalMatches(spec, input) {
24551
24694
  if (buttonBlockApprovalCue(spec, text)) return true;
24552
24695
  return false;
24553
24696
  }
24697
+ function lastModalCueLine(spec, screenText) {
24698
+ if (!screenText) return -1;
24699
+ const lines = screenText.split("\n");
24700
+ const question = compile2(spec.questionPattern, spec.questionFlags ?? "i");
24701
+ const variants = (spec.questionVariants ?? []).map((v) => compile2(v.regex, v.flags ?? "i"));
24702
+ const buttonFlags = spec.buttonFlags && spec.buttonFlags.includes("m") ? spec.buttonFlags : `${spec.buttonFlags ?? ""}m`;
24703
+ const buttonRe = compile2(spec.buttonPattern, buttonFlags);
24704
+ let last = -1;
24705
+ for (let i = 0; i < lines.length; i++) {
24706
+ const line = lines[i];
24707
+ question.lastIndex = 0;
24708
+ if (question.test(line)) {
24709
+ last = i;
24710
+ continue;
24711
+ }
24712
+ if (variants.some((re) => {
24713
+ re.lastIndex = 0;
24714
+ return re.test(line);
24715
+ })) {
24716
+ last = i;
24717
+ continue;
24718
+ }
24719
+ buttonRe.lastIndex = 0;
24720
+ if (buttonRe.test(line)) {
24721
+ last = i;
24722
+ continue;
24723
+ }
24724
+ }
24725
+ return last;
24726
+ }
24727
+ function modalSupersededBySettledPrompt(modalSpec, settledSpec, settled, input) {
24728
+ if (!settled || !settledSpec) return false;
24729
+ if (settledSpec.scope === "whole-screen") return false;
24730
+ const screenText = input.screenText ?? "";
24731
+ if (!screenText) return false;
24732
+ const modalLine = lastModalCueLine(modalSpec, screenText);
24733
+ if (modalLine < 0) return false;
24734
+ const lines = screenText.split("\n");
24735
+ const below = lines.slice(modalLine + 1);
24736
+ if (below.length === 0) return false;
24737
+ const belowText = below.join("\n");
24738
+ if (!settled.prompt.test(belowText)) return false;
24739
+ if (settled.footers.length > 0 && !settled.footers.every((f) => f.test(belowText))) return false;
24740
+ const question = compile2(modalSpec.questionPattern, modalSpec.questionFlags ?? "i");
24741
+ const variants = (modalSpec.questionVariants ?? []).map((v) => compile2(v.regex, v.flags ?? "i"));
24742
+ const buttonFlags = modalSpec.buttonFlags && modalSpec.buttonFlags.includes("m") ? modalSpec.buttonFlags : `${modalSpec.buttonFlags ?? ""}m`;
24743
+ const buttonRe = compile2(modalSpec.buttonPattern, buttonFlags);
24744
+ const isModalCueLine = (line) => {
24745
+ question.lastIndex = 0;
24746
+ if (question.test(line)) return true;
24747
+ if (variants.some((re) => {
24748
+ re.lastIndex = 0;
24749
+ return re.test(line);
24750
+ })) return true;
24751
+ buttonRe.lastIndex = 0;
24752
+ return buttonRe.test(line);
24753
+ };
24754
+ const settledPromptLineRe = compile2(settledSpec.regex, (settledSpec.flags ?? "m").includes("m") ? settledSpec.flags ?? "m" : `${settledSpec.flags ?? ""}m`);
24755
+ const isSettledLine = (line) => {
24756
+ settledPromptLineRe.lastIndex = 0;
24757
+ return settledPromptLineRe.test(line);
24758
+ };
24759
+ return below.some((line) => line.trim() !== "" && !isModalCueLine(line) && !isSettledLine(line));
24760
+ }
24554
24761
  function evaluateGroup(group, spec, input, compiled) {
24555
24762
  switch (group) {
24556
24763
  case "spinner": {
@@ -24560,7 +24767,9 @@ function evaluateGroup(group, spec, input, compiled) {
24560
24767
  }
24561
24768
  case "modal": {
24562
24769
  if (!spec.modal) return null;
24563
- return modalMatches(spec.modal, input) ? "waiting_approval" : null;
24770
+ if (!modalMatches(spec.modal, input)) return null;
24771
+ if (modalSupersededBySettledPrompt(spec.modal, spec.settledPrompt, compiled.settled, input)) return null;
24772
+ return "waiting_approval";
24564
24773
  }
24565
24774
  case "settled-prompt": {
24566
24775
  if (!spec.settledPrompt || !compiled.settled) return null;
@@ -24618,6 +24827,21 @@ function compile3(re, flags) {
24618
24827
  }
24619
24828
  function findQuestionLineIndex(spec, lines) {
24620
24829
  const primary = compile3(spec.questionPattern, spec.questionFlags ?? "i");
24830
+ const buttonFlags = spec.buttonFlags && spec.buttonFlags.includes("m") ? spec.buttonFlags : `${spec.buttonFlags ?? ""}m`;
24831
+ const buttonRe = compile3(spec.buttonPattern, buttonFlags);
24832
+ const isButtonLine = (line) => {
24833
+ buttonRe.lastIndex = 0;
24834
+ return buttonRe.test(line);
24835
+ };
24836
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
24837
+ if (primary.test(lines[i]) && !isButtonLine(lines[i])) return { index: i, matchedSource: "primary" };
24838
+ }
24839
+ for (const variant of spec.questionVariants ?? []) {
24840
+ const re = compile3(variant.regex, variant.flags ?? "i");
24841
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
24842
+ if (re.test(lines[i]) && !isButtonLine(lines[i])) return { index: i, matchedSource: variant.label ?? "variant" };
24843
+ }
24844
+ }
24621
24845
  for (let i = lines.length - 1; i >= 0; i -= 1) {
24622
24846
  if (primary.test(lines[i])) return { index: i, matchedSource: "primary" };
24623
24847
  }
@@ -25347,6 +25571,7 @@ var init_cli_state_engine = __esm({
25347
25571
  * paint and made the engine type "1" repeatedly into the prompt.
25348
25572
  */
25349
25573
  modalLostAt = 0;
25574
+ modalLostRecheckTimer = null;
25350
25575
  approvalExitTimeout = null;
25351
25576
  // ── Response tracking ────────────────────────────
25352
25577
  responseEpoch = 0;
@@ -25398,6 +25623,10 @@ var init_cli_state_engine = __esm({
25398
25623
  setStatus(status, trigger) {
25399
25624
  const prev = this.currentStatus;
25400
25625
  if (prev === status) return;
25626
+ if (prev === "waiting_approval" && this.modalLostRecheckTimer) {
25627
+ clearTimeout(this.modalLostRecheckTimer);
25628
+ this.modalLostRecheckTimer = null;
25629
+ }
25401
25630
  this.currentStatus = status;
25402
25631
  this.statusHistory.push({ status, at: Date.now(), trigger });
25403
25632
  if (this.statusHistory.length > 50) this.statusHistory.shift();
@@ -25558,6 +25787,10 @@ var init_cli_state_engine = __esm({
25558
25787
  clearTimeout(this.approvalExitTimeout);
25559
25788
  this.approvalExitTimeout = null;
25560
25789
  }
25790
+ if (this.modalLostRecheckTimer) {
25791
+ clearTimeout(this.modalLostRecheckTimer);
25792
+ this.modalLostRecheckTimer = null;
25793
+ }
25561
25794
  if (this.finishRetryTimer) {
25562
25795
  clearTimeout(this.finishRetryTimer);
25563
25796
  this.finishRetryTimer = null;
@@ -25869,7 +26102,7 @@ var init_cli_state_engine = __esm({
25869
26102
  if (!inCooldown || modal) {
25870
26103
  if (!modal) {
25871
26104
  LOG.warn("CLI", `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
25872
- if (this.currentStatus === "waiting_approval" && this.activeModal) {
26105
+ if (this.currentStatus === "waiting_approval") {
25873
26106
  const lostAt = this.modalLostAt || Date.now();
25874
26107
  if (!this.modalLostAt) this.modalLostAt = lostAt;
25875
26108
  if (Date.now() - lostAt >= this.timeouts.approvalCooldown) {
@@ -25877,6 +26110,8 @@ var init_cli_state_engine = __esm({
25877
26110
  this.modalLostAt = 0;
25878
26111
  this.setStatus("generating", "approval_lost_modal");
25879
26112
  this.callbacks.onStatusChange();
26113
+ } else {
26114
+ this.armModalLostRecheck();
25880
26115
  }
25881
26116
  }
25882
26117
  return;
@@ -26138,6 +26373,25 @@ var init_cli_state_engine = __esm({
26138
26373
  this.recordTrace("idle_finish_cancelled", { trigger: reason });
26139
26374
  }
26140
26375
  // ─── Helpers ────────────────────────────────────────────────────────────
26376
+ /**
26377
+ * Schedule one more settled evaluation while pinned to `waiting_approval`
26378
+ * with no actionable modal. The settled FSM normally only re-runs on new
26379
+ * PTY output; a provider whose modal cue lingers in a form detectStatus
26380
+ * still matches (e.g. kimi's questionPattern hitting the user echo) but
26381
+ * whose PTY has gone quiet would never get another evaluation, latching
26382
+ * `waiting_approval` forever. This timer guarantees the modal-lost recovery
26383
+ * in `applyWaitingApproval` is reached even against a silent PTY. It is a
26384
+ * no-op once the FSM leaves `waiting_approval` (the re-evaluation itself
26385
+ * takes the recovery branch and clears the state).
26386
+ */
26387
+ armModalLostRecheck() {
26388
+ if (this.modalLostRecheckTimer) return;
26389
+ this.modalLostRecheckTimer = setTimeout(() => {
26390
+ this.modalLostRecheckTimer = null;
26391
+ if (this.currentStatus !== "waiting_approval") return;
26392
+ this.evaluateSettled(this.transport.getSnapshot());
26393
+ }, this.timeouts.approvalCooldown);
26394
+ }
26141
26395
  armApprovalExitTimeout() {
26142
26396
  if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
26143
26397
  this.approvalExitTimeout = setTimeout(() => {
@@ -26731,6 +26985,7 @@ var init_provider_cli_adapter = __esm({
26731
26985
  const staleSnapshotLooksActive = activeScreenPattern.test(lastSnapshot);
26732
26986
  const currentScreenLooksIdle = /(?:^|\n|\r)\s*[❯›>]\s*(?:Try\s+["“][^\n\r"”]+["”])?\s*(?:\n|\r|$)/.test(screenText) && !activeScreenPattern.test(screenText);
26733
26987
  if (staleSnapshotLooksActive && currentScreenLooksIdle) return screenText;
26988
+ if (this.runDetectStatus(screenText) === "idle") return screenText;
26734
26989
  if (currentSnapshot.length >= lastSnapshot.length) return screenText;
26735
26990
  return `${screenText}
26736
26991
  ${lastSnapshot}`;
@@ -35979,6 +36234,33 @@ var ChatSourceRegistry = class {
35979
36234
  getState(key2) {
35980
36235
  return this.records.get(key2)?.state ?? INITIAL_CHAT_SOURCE_STATE;
35981
36236
  }
36237
+ /**
36238
+ * Opaque snapshot of a session's FULL record (state + lock + transitions),
36239
+ * for callers that need to speculatively `observe()` and then roll back if
36240
+ * the resulting decision is undesirable (STICKY-NATIVE hold: a trusted
36241
+ * exact-identity session must not flip to PTY on a transient native gap).
36242
+ * Returns undefined when the session has no record yet. The snapshot is a
36243
+ * shallow copy — transitions array is copied so a later append does not
36244
+ * mutate it.
36245
+ */
36246
+ snapshotRecord(key2) {
36247
+ const rec = this.records.get(key2);
36248
+ if (!rec) return void 0;
36249
+ return { state: rec.state, lockedSince: rec.lockedSince, transitions: [...rec.transitions] };
36250
+ }
36251
+ /** Restore a previously snapshotted record, undoing a speculative observe.
36252
+ * Passing undefined clears the key (it had no record when snapshotted). */
36253
+ restoreRecord(key2, snapshot) {
36254
+ if (!snapshot) {
36255
+ this.records.delete(key2);
36256
+ return;
36257
+ }
36258
+ this.records.set(key2, {
36259
+ state: snapshot.state,
36260
+ lockedSince: snapshot.lockedSince,
36261
+ transitions: [...snapshot.transitions]
36262
+ });
36263
+ }
35982
36264
  /** Recent transitions, newest last. Empty array when nothing has happened. */
35983
36265
  getTransitions(key2) {
35984
36266
  return this.records.get(key2)?.transitions ?? [];
@@ -36402,7 +36684,52 @@ function decideCliReadChatSource(args) {
36402
36684
  const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
36403
36685
  const observation = buildObservationForCli(args, supportsNative);
36404
36686
  const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
36687
+ const priorSnapshot = CHAT_SOURCE_REGISTRY.snapshotRecord(sessionKey);
36688
+ const priorState = priorSnapshot?.state ?? CHAT_SOURCE_REGISTRY.getState(sessionKey);
36689
+ const eligibleForStickyHold = args.trustedExactNativeIdentity === true && (priorState.name === "NativeLocked" || priorState.name === "Recovering");
36405
36690
  let decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
36691
+ if (eligibleForStickyHold && decision.selected === "pty-parser") {
36692
+ CHAT_SOURCE_REGISTRY.restoreRecord(sessionKey, priorSnapshot);
36693
+ const heldNativeMessages = observation.kind === "native_present" ? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult) : [];
36694
+ const messageSource2 = buildCliMessageSourceProvenance({
36695
+ selected: "native-history",
36696
+ provider: args.providerType,
36697
+ nativeHandle: typeof args.nativeHistoryResult?.providerSessionId === "string" ? args.nativeHistoryResult.providerSessionId : void 0,
36698
+ sessionWorkspace: args.sessionWorkspace,
36699
+ intendedWorkspace: args.intendedWorkspace,
36700
+ transcriptWorkspace: void 0,
36701
+ fallbackReason: "native_history_transient_gap_held",
36702
+ nativeSource: "provider-native",
36703
+ sourcePath: typeof args.nativeHistoryResult?.sourcePath === "string" ? args.nativeHistoryResult.sourcePath : void 0,
36704
+ sourceMtimeMs: typeof args.nativeHistoryResult?.sourceMtimeMs === "number" ? args.nativeHistoryResult.sourceMtimeMs : void 0,
36705
+ nativeHistoryCoverage: void 0,
36706
+ partialReason: void 0,
36707
+ unavailableReason: observation.kind === "native_unavailable" ? observation.reason : void 0,
36708
+ nativeMessages: heldNativeMessages,
36709
+ ptyMessages: args.ptyMessages,
36710
+ returnedMessages: heldNativeMessages,
36711
+ safeMapping: args.safeMapping,
36712
+ freshEnough: true,
36713
+ ptyStatusApprovalOnly: true
36714
+ });
36715
+ return {
36716
+ decision: {
36717
+ selected: "native-history",
36718
+ nextState: priorState,
36719
+ transition: {
36720
+ fromState: priorState.name,
36721
+ toState: priorState.name,
36722
+ event: "NoOp",
36723
+ cause: decision.transition.cause,
36724
+ at: Date.now()
36725
+ },
36726
+ lockState: { locked: priorState.name === "NativeLocked" }
36727
+ },
36728
+ messageSource: messageSource2,
36729
+ nativeMessages: heldNativeMessages,
36730
+ nativeSelected: true
36731
+ };
36732
+ }
36406
36733
  if (decision.selected === "pty-parser" && args.trustedExactNativeIdentity === true && args.safeMapping && args.ptyMessages.length === 0 && observation.kind === "native_present" && observation.coverage !== "partial" && observation.messages.length > 0) {
36407
36734
  CHAT_SOURCE_REGISTRY.clear(sessionKey);
36408
36735
  decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
@@ -36722,7 +37049,7 @@ function sessionSpawnEnvFromAdapter(h, targetSessionId) {
36722
37049
  function readCliProviderNativeHistory(agentStr, args) {
36723
37050
  const canBindFromLiveSession = !args.historySessionId && typeof args.sessionStartedAtMs === "number" && args.sessionStartedAtMs > 0 && typeof args.workspace === "string" && args.workspace.trim().length > 0;
36724
37051
  const pinnedProviderSessionId = typeof args.pinnedProviderSessionId === "string" ? args.pinnedProviderSessionId.trim() : "";
36725
- const effectiveHistorySessionId = args.historySessionId || (!canBindFromLiveSession ? pinnedProviderSessionId : "");
37052
+ const effectiveHistorySessionId = args.historySessionId || pinnedProviderSessionId || "";
36726
37053
  const workspaceLatestFallback = !effectiveHistorySessionId && !canBindFromLiveSession && !pinnedProviderSessionId && args.allowWorkspaceLatestFallback === true && typeof args.workspace === "string" && args.workspace.trim().length > 0;
36727
37054
  if (!effectiveHistorySessionId && !canBindFromLiveSession && !workspaceLatestFallback) {
36728
37055
  return {