@adhdev/daemon-core 0.9.82-rc.537 → 0.9.82-rc.539

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 ? "b47ad45cf2b2ad404d911162a9cadd7db2e274a6" : void 0) ?? "unknown";
423
- const commitShort = readInjected(true ? "b47ad45c" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
424
- const version = readInjected(true ? "0.9.82-rc.537" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
425
- const builtAt = readInjected(true ? "2026-07-15T13:12:55.213Z" : 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.539" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
425
+ const builtAt = readInjected(true ? "2026-07-15T15:55:08.195Z" : 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
  }
@@ -24684,6 +24827,21 @@ function compile3(re, flags) {
24684
24827
  }
24685
24828
  function findQuestionLineIndex(spec, lines) {
24686
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
+ }
24687
24845
  for (let i = lines.length - 1; i >= 0; i -= 1) {
24688
24846
  if (primary.test(lines[i])) return { index: i, matchedSource: "primary" };
24689
24847
  }
@@ -25413,6 +25571,7 @@ var init_cli_state_engine = __esm({
25413
25571
  * paint and made the engine type "1" repeatedly into the prompt.
25414
25572
  */
25415
25573
  modalLostAt = 0;
25574
+ modalLostRecheckTimer = null;
25416
25575
  approvalExitTimeout = null;
25417
25576
  // ── Response tracking ────────────────────────────
25418
25577
  responseEpoch = 0;
@@ -25464,6 +25623,10 @@ var init_cli_state_engine = __esm({
25464
25623
  setStatus(status, trigger) {
25465
25624
  const prev = this.currentStatus;
25466
25625
  if (prev === status) return;
25626
+ if (prev === "waiting_approval" && this.modalLostRecheckTimer) {
25627
+ clearTimeout(this.modalLostRecheckTimer);
25628
+ this.modalLostRecheckTimer = null;
25629
+ }
25467
25630
  this.currentStatus = status;
25468
25631
  this.statusHistory.push({ status, at: Date.now(), trigger });
25469
25632
  if (this.statusHistory.length > 50) this.statusHistory.shift();
@@ -25624,6 +25787,10 @@ var init_cli_state_engine = __esm({
25624
25787
  clearTimeout(this.approvalExitTimeout);
25625
25788
  this.approvalExitTimeout = null;
25626
25789
  }
25790
+ if (this.modalLostRecheckTimer) {
25791
+ clearTimeout(this.modalLostRecheckTimer);
25792
+ this.modalLostRecheckTimer = null;
25793
+ }
25627
25794
  if (this.finishRetryTimer) {
25628
25795
  clearTimeout(this.finishRetryTimer);
25629
25796
  this.finishRetryTimer = null;
@@ -25935,7 +26102,7 @@ var init_cli_state_engine = __esm({
25935
26102
  if (!inCooldown || modal) {
25936
26103
  if (!modal) {
25937
26104
  LOG.warn("CLI", `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
25938
- if (this.currentStatus === "waiting_approval" && this.activeModal) {
26105
+ if (this.currentStatus === "waiting_approval") {
25939
26106
  const lostAt = this.modalLostAt || Date.now();
25940
26107
  if (!this.modalLostAt) this.modalLostAt = lostAt;
25941
26108
  if (Date.now() - lostAt >= this.timeouts.approvalCooldown) {
@@ -25943,6 +26110,8 @@ var init_cli_state_engine = __esm({
25943
26110
  this.modalLostAt = 0;
25944
26111
  this.setStatus("generating", "approval_lost_modal");
25945
26112
  this.callbacks.onStatusChange();
26113
+ } else {
26114
+ this.armModalLostRecheck();
25946
26115
  }
25947
26116
  }
25948
26117
  return;
@@ -26204,6 +26373,25 @@ var init_cli_state_engine = __esm({
26204
26373
  this.recordTrace("idle_finish_cancelled", { trigger: reason });
26205
26374
  }
26206
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
+ }
26207
26395
  armApprovalExitTimeout() {
26208
26396
  if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
26209
26397
  this.approvalExitTimeout = setTimeout(() => {
@@ -36046,6 +36234,33 @@ var ChatSourceRegistry = class {
36046
36234
  getState(key2) {
36047
36235
  return this.records.get(key2)?.state ?? INITIAL_CHAT_SOURCE_STATE;
36048
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
+ }
36049
36264
  /** Recent transitions, newest last. Empty array when nothing has happened. */
36050
36265
  getTransitions(key2) {
36051
36266
  return this.records.get(key2)?.transitions ?? [];
@@ -36469,7 +36684,52 @@ function decideCliReadChatSource(args) {
36469
36684
  const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
36470
36685
  const observation = buildObservationForCli(args, supportsNative);
36471
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");
36472
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
+ }
36473
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) {
36474
36734
  CHAT_SOURCE_REGISTRY.clear(sessionKey);
36475
36735
  decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
@@ -36789,7 +37049,7 @@ function sessionSpawnEnvFromAdapter(h, targetSessionId) {
36789
37049
  function readCliProviderNativeHistory(agentStr, args) {
36790
37050
  const canBindFromLiveSession = !args.historySessionId && typeof args.sessionStartedAtMs === "number" && args.sessionStartedAtMs > 0 && typeof args.workspace === "string" && args.workspace.trim().length > 0;
36791
37051
  const pinnedProviderSessionId = typeof args.pinnedProviderSessionId === "string" ? args.pinnedProviderSessionId.trim() : "";
36792
- const effectiveHistorySessionId = args.historySessionId || (!canBindFromLiveSession ? pinnedProviderSessionId : "");
37052
+ const effectiveHistorySessionId = args.historySessionId || pinnedProviderSessionId || "";
36793
37053
  const workspaceLatestFallback = !effectiveHistorySessionId && !canBindFromLiveSession && !pinnedProviderSessionId && args.allowWorkspaceLatestFallback === true && typeof args.workspace === "string" && args.workspace.trim().length > 0;
36794
37054
  if (!effectiveHistorySessionId && !canBindFromLiveSession && !workspaceLatestFallback) {
36795
37055
  return {