@adhdev/daemon-core 1.0.18-rc.18 → 1.0.18-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/cli-adapters/cli-state-engine.d.ts +0 -77
  2. package/dist/cli-adapters/provider-cli-shared.d.ts +0 -33
  3. package/dist/cli-adapters/pty-transport.d.ts +1 -2
  4. package/dist/commands/cli-manager.d.ts +0 -9
  5. package/dist/commands/upgrade-helper.d.ts +0 -1
  6. package/dist/commands/windows-atomic-upgrade.d.ts +1 -17
  7. package/dist/config/mesh-json-config.d.ts +0 -62
  8. package/dist/index.d.ts +2 -4
  9. package/dist/index.js +4143 -6578
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +4138 -6570
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/coordinator-prompt.d.ts +0 -76
  14. package/dist/mesh/mesh-active-work.d.ts +1 -1
  15. package/dist/mesh/mesh-ledger.d.ts +1 -28
  16. package/dist/mesh/mesh-node-identity.d.ts +0 -23
  17. package/dist/mesh/mesh-refine-gates.d.ts +0 -89
  18. package/dist/mesh/mesh-remote-event-pull.d.ts +0 -3
  19. package/dist/mesh/mesh-runtime-store.d.ts +0 -11
  20. package/dist/mesh/mesh-work-queue.d.ts +1 -24
  21. package/dist/providers/chat-message-normalization.d.ts +0 -22
  22. package/dist/providers/cli-provider-instance-types.d.ts +0 -4
  23. package/dist/providers/cli-provider-instance.d.ts +0 -78
  24. package/dist/providers/contracts.d.ts +0 -17
  25. package/dist/providers/provider-schema.d.ts +0 -1
  26. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +0 -14
  27. package/dist/providers/types/interactive-prompt.d.ts +0 -18
  28. package/dist/repo-mesh-types.d.ts +6 -38
  29. package/dist/shared-types.d.ts +2 -4
  30. package/package.json +3 -3
  31. package/src/boot/daemon-lifecycle.ts +0 -10
  32. package/src/cli-adapters/cli-state-engine.ts +3 -220
  33. package/src/cli-adapters/provider-cli-adapter.ts +11 -41
  34. package/src/cli-adapters/provider-cli-shared.ts +0 -41
  35. package/src/cli-adapters/pty-transport.d.ts +1 -2
  36. package/src/cli-adapters/pty-transport.ts +1 -1
  37. package/src/cli-adapters/session-host-transport.ts +3 -8
  38. package/src/commands/cli-manager.ts +8 -72
  39. package/src/commands/high-family/mesh-coordinator-launch.ts +2 -17
  40. package/src/commands/high-family/mesh-events.ts +2 -13
  41. package/src/commands/med-family/mesh-crud.ts +0 -155
  42. package/src/commands/med-family/mesh-queue.ts +1 -74
  43. package/src/commands/router-refine.ts +4 -71
  44. package/src/commands/router.ts +0 -8
  45. package/src/commands/upgrade-helper.ts +82 -106
  46. package/src/commands/windows-atomic-upgrade.ts +35 -281
  47. package/src/config/mesh-json-config.ts +0 -111
  48. package/src/index.ts +1 -21
  49. package/src/mesh/coordinator-prompt.ts +11 -258
  50. package/src/mesh/mesh-active-work.ts +1 -11
  51. package/src/mesh/mesh-completion-synthesis.ts +1 -12
  52. package/src/mesh/mesh-event-classify.ts +0 -19
  53. package/src/mesh/mesh-event-forwarding.ts +6 -64
  54. package/src/mesh/mesh-events-utils.ts +0 -42
  55. package/src/mesh/mesh-ledger.ts +0 -77
  56. package/src/mesh/mesh-node-identity.ts +0 -49
  57. package/src/mesh/mesh-queue-assignment.ts +11 -210
  58. package/src/mesh/mesh-reconcile-loop.ts +3 -306
  59. package/src/mesh/mesh-refine-gates.ts +0 -300
  60. package/src/mesh/mesh-remote-event-pull.ts +47 -68
  61. package/src/mesh/mesh-runtime-store.ts +0 -21
  62. package/src/mesh/mesh-work-queue.ts +2 -85
  63. package/src/providers/chat-message-normalization.ts +0 -53
  64. package/src/providers/cli-provider-instance-types.ts +0 -53
  65. package/src/providers/cli-provider-instance.ts +15 -497
  66. package/src/providers/contracts.ts +1 -25
  67. package/src/providers/provider-schema.ts +0 -83
  68. package/src/providers/sdk/v1/builders/cli/detect-status.ts +0 -23
  69. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +0 -20
  70. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +0 -44
  71. package/src/providers/types/interactive-prompt.ts +0 -77
  72. package/src/repo-mesh-types.ts +12 -112
  73. package/src/session-host/managed-host.ts +0 -34
  74. package/src/shared-types.ts +1 -9
  75. package/src/status/reporter.ts +0 -1
  76. package/src/status/snapshot.ts +0 -2
  77. package/dist/commands/process-lifecycle.d.ts +0 -43
  78. package/dist/mesh/mesh-disk-retention.d.ts +0 -105
  79. package/dist/providers/auto-approve-modes.d.ts +0 -14
  80. package/src/commands/process-lifecycle.ts +0 -248
  81. package/src/mesh/mesh-disk-retention.ts +0 -370
  82. package/src/providers/auto-approve-modes.ts +0 -97
@@ -14,8 +14,6 @@ import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
14
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
15
15
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
16
16
  import type { MagiKindPanelMap, DifficultyBrainMap, NodeCapabilitySlot } from '@adhdev/mesh-shared';
17
- import type { ProviderModule } from './providers/contracts.js';
18
- import type { RepoMeshDeclarativeConfig } from './config/mesh-json-config.js';
19
17
  export interface RepoMesh {
20
18
  id: string;
21
19
  name: string;
@@ -257,8 +255,6 @@ export interface RepoMeshPolicy {
257
255
  * A node policy may override this per-node (RepoMeshNodePolicy.delegatedWorkerAutoApprove).
258
256
  */
259
257
  delegatedWorkerAutoApprove?: boolean;
260
- /** Explicit opt-in required before delegated workers may use a dangerous provider mode. */
261
- delegatedWorkerDangerousModeAllow?: boolean;
262
258
  /**
263
259
  * MESH-SEND-KEYS (feature 3): opt-in to allow the coordinator to inject
264
260
  * DESTRUCTIVE keys (CTRL_C / ESC) into a worker PTY via mesh_send_keys. These
@@ -410,8 +406,6 @@ export interface RepoMeshNodePolicy {
410
406
  * precedence over the mesh-level policy for worker sessions launched onto this node.
411
407
  */
412
408
  delegatedWorkerAutoApprove?: boolean;
413
- /** Per-node override for dangerous delegated worker mode authorization. */
414
- delegatedWorkerDangerousModeAllow?: boolean;
415
409
  /**
416
410
  * MESH-SEND-KEYS (feature 3): per-node override for
417
411
  * RepoMeshPolicy.allowSendKeysDestructive.
@@ -530,39 +524,13 @@ export declare function normalizeAutoFastForwardPolicy(value: unknown): NonNulla
530
524
  */
531
525
  export declare function mergeAndNormalizePolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy;
532
526
  /**
533
- * Resolve delegated worker auto-approve. Legacy providers return a boolean. Providers
534
- * with modes return a mode id, except a dangerous mode is downgraded to a
535
- * non-dangerous PTY mode unless mesh/node policy explicitly opts in.
536
- *
537
- * THREE EXPLICIT STAGES do not collapse them; the ordering is a hard MAGI
538
- * invariant:
539
- *
540
- * ① ENABLE gate (machine-local policy only): node boolean > mesh boolean.
541
- * `enabled=false` returns `false` IMMEDIATELY, BEFORE any mode selection.
542
- * The repo `mesh.json` providerDefaults has ZERO influence here — a
543
- * node/mesh opt-out is never overridden by a repo-declared requested mode.
544
- *
545
- * ② MODE selection (only when enabled === true):
546
- * task override [FUTURE — param reserved below, not yet wired] >
547
- * repo mesh.json providerDefaults.autoApproveModes[providerType] >
548
- * provider spec autoApproveModes.default.
549
- * A repo-requested mode ID is adopted ONLY when it exists in the provider's
550
- * own `autoApproveModes.modes`; an unknown/stale/typo'd ID is IGNORED and we
551
- * fall back to the provider default (fail-closed: never coerce into a
552
- * dangerous mode via a bad ID).
553
- *
554
- * ③ DANGEROUS gate: whichever mode stage ② picked, if it is dangerous and the
555
- * machine-local delegatedWorkerDangerousModeAllow is not set, downgrade to a
556
- * non-dangerous PTY-parse mode (or `false` if none exists).
527
+ * Resolve whether a delegated worker session launched onto `nodePolicy` (within a mesh
528
+ * governed by `meshPolicy`) should auto-approve. Precedence: node override mesh policy
529
+ * default true. The result is stamped into the worker launch settings envelope as
530
+ * `autoApprove`; it wins over the global per-provider-type autoApprove config because the
531
+ * launch path merges the envelope as a settingsOverride on top of the provider defaults.
557
532
  */
558
- export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, provider?: Pick<ProviderModule, 'autoApproveModes'> | null, repoConfig?: RepoMeshDeclarativeConfig | null, providerType?: string | null): boolean | string;
559
- export declare function resolveDelegatedWorkerDangerousModeAllow(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerDangerousModeAllow'> | null): boolean;
560
- /** Shape a boolean-or-mode resolution for the settings precedence contract. */
561
- export declare function delegatedWorkerAutoApproveSettings(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, provider?: Pick<ProviderModule, 'autoApproveModes'> | null, repoConfig?: RepoMeshDeclarativeConfig | null, providerType?: string | null): {
562
- autoApprove: boolean | undefined;
563
- autoApproveMode: string | undefined;
564
- delegatedWorkerDangerousModeAllow: boolean;
565
- };
533
+ export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove'> | null): boolean;
566
534
  /**
567
535
  * MESH-SEND-KEYS (feature 3): resolve whether DESTRUCTIVE key injection
568
536
  * (CTRL_C/ESC via mesh_send_keys) is permitted for a node. Node policy overrides
@@ -13,7 +13,7 @@ export type { ProviderState, ProviderStatus, ActiveChatData, IdeProviderState, C
13
13
  export type { ProviderErrorReason } from './providers/provider-instance.js';
14
14
  import type { ActiveChatData as _ActiveChatData, ProviderErrorReason as _ProviderErrorReason } from './providers/provider-instance.js';
15
15
  import type { WorkspaceEntry } from './config/workspaces.js';
16
- import type { AutoApproveModesConfig, ProviderMeshCoordinatorConfig, ProviderResumeCapability } from './providers/contracts.js';
16
+ import type { ProviderMeshCoordinatorConfig, ProviderResumeCapability } from './providers/contracts.js';
17
17
  import type { GitCompactSummary, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams } from './git/git-types.js';
18
18
  import type { InteractivePrompt } from './providers/types/interactive-prompt.js';
19
19
  export type { GitCommandName, GitCompactSummary, GitDiffSummary, GitFailureReason, GitFileChange, GitFileChangeStatus, GitRepoIdentity, GitRepoStatus, GitSnapshot, GitSnapshotCompareSummary, GitSnapshotReason, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams, } from './git/git-types.js';
@@ -428,8 +428,6 @@ export interface AvailableProviderInfo {
428
428
  lastVerification?: MachineProviderCheckResult;
429
429
  /** Provider-declared Repo Mesh coordinator/MCP behavior. */
430
430
  meshCoordinator?: ProviderMeshCoordinatorConfig;
431
- /** Provider-declared auto-approve choices shown by session launch UIs. */
432
- autoApproveModes?: AutoApproveModesConfig;
433
431
  /** BRAIN-ROUTING: suggested model values for the new-session model dropdown. */
434
432
  modelOptions?: string[];
435
433
  /** BRAIN-ROUTING: reasoning-effort values for the new-session thinking dropdown. */
@@ -626,7 +624,7 @@ export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummar
626
624
  p2p?: StatusReportPayload['p2p'];
627
625
  timestamp?: number;
628
626
  }
629
- export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:waiting_choice' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
627
+ export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
630
628
  /** Minimal daemon-originated event payload relayed through the server. */
631
629
  export interface DaemonStatusEventPayload {
632
630
  event: DaemonStatusEventName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.18-rc.18",
3
+ "version": "1.0.18-rc.2",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "1.0.18-rc.18",
51
- "@adhdev/session-host-core": "1.0.18-rc.18",
50
+ "@adhdev/mesh-shared": "1.0.18-rc.2",
51
+ "@adhdev/session-host-core": "1.0.18-rc.2",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -39,7 +39,6 @@ import type { IdeProviderInstance } from '../providers/ide-provider-instance.js'
39
39
  import { createDefaultGitCommandServices } from '../git/git-commands.js';
40
40
  import { setupMeshEventForwarding } from '../mesh/mesh-events.js';
41
41
  import { setupMeshReconcileLoop } from '../mesh/mesh-reconcile-loop.js';
42
- import { MeshRuntimeStore } from '../mesh/mesh-runtime-store.js';
43
42
  import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
44
43
  import { applyProcessHardening } from './process-hardening.js';
45
44
  import { installProviderProcessShim } from '../providers/sdk/v1/sandbox/require-whitelist.js';
@@ -504,13 +503,4 @@ export async function shutdownDaemonComponents(components: DaemonComponents): Pr
504
503
  try { m.disconnect(); } catch { /* noop */ }
505
504
  }
506
505
  cdpManagers.clear();
507
-
508
- // 7. VACUUM the mesh runtime DB. Retention prunes rows with DELETE (frees pages
509
- // inside the file but never shrinks it); the mesh-runtime.db grew to hundreds of
510
- // MB (mission 86def38d disk-accumulation bootstrap failure) because it was never
511
- // compacted. Do this LAST, after all writers (reconcile loop, CLIs, instances)
512
- // are stopped, so nothing contends for the exclusive VACUUM lock. Best-effort —
513
- // vacuum() swallows its own errors; the try/catch guards the getInstance() throw
514
- // when the store never opened (degraded JSONL-only mode).
515
- try { MeshRuntimeStore.getInstance().vacuum(); } catch { /* store unavailable — nothing to vacuum */ }
516
506
  }
@@ -15,7 +15,6 @@ import {
15
15
  import {
16
16
  buildCliScreenSnapshot,
17
17
  compactPromptText,
18
- isPurePtyTranscriptProvider,
19
18
  normalizePromptText,
20
19
  promptLikelyVisible,
21
20
  type CliChatMessage,
@@ -87,18 +86,6 @@ interface IdleFinishCandidate {
87
86
  assistantLength: number;
88
87
  }
89
88
 
90
- /**
91
- * Minimal shape computeApprovalContentSignature / isStaleResolvedApproval
92
- * actually need — deliberately narrower than the full CliBufferSnapshot so
93
- * callers outside the settled-eval loop (e.g. the adapter's startup-gate
94
- * modal parse in getStatus/getDebugState) can supply just these two fields
95
- * without having to fabricate an entire snapshot.
96
- */
97
- interface ApprovalSignatureSnapshot {
98
- screenText?: string;
99
- accumulatedBuffer?: string;
100
- }
101
-
102
89
  interface SettledEvalContext {
103
90
  now: number;
104
91
  modal: { message: string; buttons: string[] } | null;
@@ -168,22 +155,6 @@ export class CliStateEngine {
168
155
  // ── Approval ─────────────────────────────────────
169
156
  lastApprovalResolvedAt = 0;
170
157
  lastResolvedModalMessage = '';
171
- /**
172
- * Normalized approval-context signature captured at resolve time (see
173
- * `computeApprovalContentSignature`) — the screen text with blank-line
174
- * padding and any manifest-declared chrome (transcriptPty.chromePatterns,
175
- * spinner.patterns) stripped out. Used by applyWaitingApproval's
176
- * isStaleResolvedRepaint check to tell "the same already-answered modal,
177
- * re-parsed from a screen that has not meaningfully changed" apart from
178
- * "a genuinely new approval" — content-based, not time-based, because
179
- * ordinary TUI chrome (status bar, context meter, blank-line repaint)
180
- * keeps producing fresh PTY bytes on an otherwise-unchanged screen and
181
- * defeats any output-timestamp discriminator within a few hundred ms.
182
- * Cleared whenever a turn starts or the session tears down (see
183
- * onTurnStarted / resetActiveTurnState / onPtyExit) so a next-turn
184
- * approval is never compared against stale prior-turn state.
185
- */
186
- lastApprovalResolvedContentSignature = '';
187
158
  /**
188
159
  * Monotonic counter bumped every time the FSM *enters* waiting_approval
189
160
  * with a freshly captured modal (see `applyWaitingApproval`). It is the
@@ -333,61 +304,12 @@ export class CliStateEngine {
333
304
  // anchor its window on dispatch time rather than completion time.
334
305
  this.currentTurnStartedAt = Date.now();
335
306
  this.responseEpoch += 1;
336
- // A new turn's own prompt echo is real new content, so the stale-repaint
337
- // guard's signature would naturally drift anyway — but clear the
338
- // resolve-time bookkeeping explicitly here too, so a next-turn approval
339
- // that happens to share message text with a previous turn's already-
340
- // resolved one is never compared against stale prior-turn state.
341
- this.clearApprovalResolutionMemory();
342
- // (fix: kimi K3 send→idle-looking→generating lag / missed-generating on
343
- // fast tool turns) For transcriptAuthority:'provider' providers (kimi,
344
- // and other native-transcript sources), the PTY-scanned parser always
345
- // returns messages:[] — the provider owns the transcript, not the PTY —
346
- // so evaluateSettled's shouldHoldGenerating fast-path exception
347
- // (hasFinalCurrentTurnAssistant) can never release early and
348
- // recent_activity_hold ends up carrying the ENTIRE "is this turn still
349
- // generating" signal for these providers regardless of what the spinner
350
- // script does. That fallback only fires once a settle tick actually
351
- // runs, which needs PTY output to schedule — for a model that "thinks"
352
- // silently for many seconds before its first repaint (observed 12-20s
353
- // for Kimi K3's default "high" effort), the dashboard looks idle for
354
- // that whole window even though the turn was already accepted. Worse,
355
- // a turn that fully completes (tool calls + reply) within a single
356
- // settle debounce window can go straight idle→idle with no visible
357
- // generating state at all (observed live with a yolo tool-use turn).
358
- // onTurnStarted is the authoritative "a turn was just submitted" signal
359
- // — promote to generating immediately instead of waiting on PTY-driven
360
- // detection. This does not weaken completion detection: applyIdle /
361
- // finishResponse (authoritative settled evidence) still own the actual
362
- // idle transition, unchanged. Scoped to transcriptAuthority:'provider'
363
- // only, so PTY-authoritative providers (whose spinner/settled parsing
364
- // already drives generating promptly) are unaffected.
365
- //
366
- // (fix: kimi pure-PTY completion-emit) The pure-PTY full-buffer class
367
- // (kimi and kin — see isPurePtyTranscriptProvider) is NOT
368
- // transcriptAuthority:'provider', so without this it stays idle when a
369
- // prompt is submitted from idle: the FSM never crosses generating→idle,
370
- // detectStatusTransition's generating|waiting_approval→idle arm never
371
- // runs, and agent:generating_completed is never emitted — the mesh
372
- // coordinator leaves the task 'assigned' and the status-agnostic stall
373
- // watchdog then false-fires task_stalled on the finished-but-idle
374
- // session. Promoting this class to generating on turn-start makes the
375
- // existing PTY-parsed final-assistant idle gate produce a real
376
- // generating→idle completion edge. Same idle safety as above: applyIdle
377
- // / finishResponse still own the actual idle transition.
378
- const promoteOnTurnStart = this.provider.transcriptAuthority === 'provider'
379
- || isPurePtyTranscriptProvider(this.provider);
380
- if (promoteOnTurnStart && this.currentStatus !== 'waiting_approval') {
381
- this.setStatus('generating', 'turn_started');
382
- this.callbacks.onStatusChange();
383
- }
384
307
  }
385
308
 
386
309
  /** Called when PTY exits */
387
310
  onPtyExit(): void {
388
311
  this.clearAllTimers();
389
312
  this.setStatus('stopped', 'pty_exit');
390
- this.clearApprovalResolutionMemory();
391
313
  }
392
314
 
393
315
  /** Called when adapter starts up successfully */
@@ -460,11 +382,6 @@ export class CliStateEngine {
460
382
  this.activeModal = null;
461
383
  this.lastApprovalResolvedAt = Date.now();
462
384
  this.lastResolvedModalMessage = currentModalMessage;
463
- // Snapshot the chrome-stripped screen at the moment of resolve — the
464
- // baseline applyWaitingApproval's isStaleResolvedRepaint check compares
465
- // against to tell a stale re-parse of THIS modal apart from a
466
- // genuinely new one, regardless of how much wall-clock time passes.
467
- this.lastApprovalResolvedContentSignature = this.computeApprovalContentSignature(snap);
468
385
  this.lastResolvedEntrySeq = this.approvalEntrySeq;
469
386
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
470
387
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
@@ -582,7 +499,6 @@ export class CliStateEngine {
582
499
  this.pendingScriptStatusSince = 0;
583
500
  this.approvalResumeDeferSince = 0;
584
501
  this.approvalResumeDeferEpoch = -1;
585
- this.clearApprovalResolutionMemory();
586
502
  }
587
503
 
588
504
  clearIdleFinishCandidate(reason: string): void {
@@ -763,9 +679,7 @@ export class CliStateEngine {
763
679
  if (!status) return;
764
680
 
765
681
  const prevStatus = this.currentStatus;
766
- const ctx: SettledEvalContext = {
767
- now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus,
768
- };
682
+ const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
769
683
 
770
684
  if (!this.applyPendingScriptStatusDebounce(ctx)) return;
771
685
 
@@ -829,7 +743,7 @@ export class CliStateEngine {
829
743
  this.applyError(ctx, session);
830
744
  return;
831
745
  }
832
- if (status === 'waiting_approval') { this.applyWaitingApproval(ctx, snap); return; }
746
+ if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
833
747
  if (status === 'generating') { this.applyGenerating(ctx); return; }
834
748
  if (status === 'idle') { this.applyIdle(ctx, snap, now); }
835
749
  }
@@ -887,7 +801,7 @@ export class CliStateEngine {
887
801
  this.callbacks.onStatusChange();
888
802
  }
889
803
 
890
- private applyWaitingApproval(ctx: SettledEvalContext, snap: CliBufferSnapshot): void {
804
+ private applyWaitingApproval(ctx: SettledEvalContext): void {
891
805
  const { modal } = ctx;
892
806
  this.clearIdleFinishCandidate('waiting_approval');
893
807
  const inCooldown = this.lastApprovalResolvedAt
@@ -978,51 +892,6 @@ export class CliStateEngine {
978
892
  }
979
893
  return;
980
894
  }
981
- // (fix: kimi stale-approval re-latch, observed live on kimi-code
982
- // v0.28.1/K3) A freshly-*parsed* modal is not proof the CLI is
983
- // presenting it right now — parseApproval scans an accumulated
984
- // raw-output window (recentOutputBuffer / window-around-question
985
- // scope), which can still contain the text of an approval that was
986
- // ALREADY resolved a moment ago and re-surface it on the very next
987
- // settle pass, even though resolveModal() already wrote the key and
988
- // cleared activeModal. Reproduced live: after approving a tool call,
989
- // the FSM re-latched `waiting_approval` on the identical
990
- // already-answered question with NO further genuinely new content
991
- // ever arriving afterward — the dashboard stayed wedged on "waiting
992
- // for approval" until the 5-minute maxResponse watchdog forced a
993
- // recheck.
994
- //
995
- // An output-TIMESTAMP discriminator ("has any PTY byte arrived since
996
- // the resolve") was tried first and found insufficient: a live
997
- // standalone repro showed kimi's own idle-screen chrome (status bar,
998
- // context meter, blank-line repaint) advances lastNonEmptyOutputAt
999
- // within ~300ms of the resolve even though NOTHING approval-relevant
1000
- // changed, so the guard stopped protecting almost immediately instead
1001
- // of for as long as the staleness actually persisted (observed 30s+).
1002
- //
1003
- // Reject a recapture only when ALL of: (a) this is the first capture
1004
- // since the last resolve (`!this.activeModal` — a genuinely repeated
1005
- // approval re-enters this branch too, since resolveModal always nulls
1006
- // activeModal), (b) the message text matches the one we just
1007
- // resolved, AND (c) the chrome-stripped approval-context signature
1008
- // (computeApprovalContentSignature — screen text with blank-line
1009
- // padding and manifest-declared chrome removed) is UNCHANGED from
1010
- // the signature captured at resolve time. (c) is the load-bearing,
1011
- // content-based condition, deliberately not time-bounded: it keeps
1012
- // rejecting for as long as nothing approval-relevant changes,
1013
- // regardless of how many chrome-only repaints occur or how much
1014
- // wall-clock time passes, and it stops rejecting the instant real
1015
- // new content (tool output, a fresh conversational turn) appears.
1016
- // This also keeps consecutive-approvals-with-identical-text (e.g.
1017
- // two back-to-back "Allow Bash command?" prompts) working correctly:
1018
- // a real follow-up approval necessarily means the CLI produced real
1019
- // new output first (running the previous tool, then asking again),
1020
- // which changes the signature regardless of shared message text.
1021
- const isStaleResolvedRepaint = !this.activeModal && this.isStaleResolvedApproval(modal, snap);
1022
- if (isStaleResolvedRepaint) {
1023
- LOG.debug('CLI', `[${this.provider.type}] ignoring stale re-parsed approval matching the just-resolved modal (approval-context signature unchanged)`);
1024
- return;
1025
- }
1026
895
  this.modalLostAt = 0;
1027
896
  this.isWaitingForResponse = true;
1028
897
  this.setStatus('waiting_approval', 'script_detect');
@@ -1387,92 +1256,6 @@ export class CliStateEngine {
1387
1256
 
1388
1257
  // ─── Helpers ────────────────────────────────────────────────────────────
1389
1258
 
1390
- /**
1391
- * Derive a stable approval-context signature from the current screen,
1392
- * with blank-line padding and any manifest-declared chrome stripped out.
1393
- *
1394
- * Generic by design — no kimi- or provider-specific hardcoding: it reads
1395
- * whatever `tui.transcriptPty.chromePatterns` and `tui.spinner.patterns`
1396
- * the ACTIVE provider's own manifest already declares (present on any
1397
- * declarative-TUI provider; simply absent/empty for scripted providers,
1398
- * in which case this degrades to blank-line stripping only — never worse
1399
- * than comparing the raw screen). Those pattern lists exist precisely to
1400
- * name "known volatile repaint noise" (status bar, context meter, spinner
1401
- * ticks, banners) — reusing them here means the SAME declared knowledge
1402
- * that governs transcript-chrome stripping also governs staleness
1403
- * detection, instead of re-encoding provider knowledge into daemon-core.
1404
- *
1405
- * Deliberately NOT a hash of the whole screen/buffer: an ordinary TUI
1406
- * repaints its footer/status/context-meter chrome continuously even while
1407
- * genuinely idle, so a raw whole-screen or whole-buffer fingerprint (or a
1408
- * mere "did any bytes arrive" timestamp) changes on every repaint tick
1409
- * regardless of whether anything approval-relevant actually happened.
1410
- * Stripping the declared chrome first yields a signature that only
1411
- * changes when the surrounding conversation/tool-output content itself
1412
- * changes — exactly the discriminator applyWaitingApproval's
1413
- * isStaleResolvedRepaint check needs.
1414
- */
1415
- private computeApprovalContentSignature(snap: ApprovalSignatureSnapshot): string {
1416
- const screenText = snap.screenText || snap.accumulatedBuffer || '';
1417
- if (!screenText) return '';
1418
- const tui = (this.provider as { tui?: Record<string, any> }).tui;
1419
- const patternSpecs: Array<{ regex?: unknown; flags?: unknown }> = [
1420
- ...(Array.isArray(tui?.transcriptPty?.chromePatterns) ? tui!.transcriptPty.chromePatterns : []),
1421
- ...(Array.isArray(tui?.spinner?.patterns) ? tui!.spinner.patterns : []),
1422
- ];
1423
- const chromeRegexes: RegExp[] = [];
1424
- for (const spec of patternSpecs) {
1425
- if (spec && typeof spec.regex === 'string') {
1426
- try {
1427
- chromeRegexes.push(new RegExp(spec.regex, typeof spec.flags === 'string' ? spec.flags : ''));
1428
- } catch {
1429
- // Ignore an unparseable manifest regex — signature just skips that filter.
1430
- }
1431
- }
1432
- }
1433
- const kept: string[] = [];
1434
- for (const rawLine of screenText.split('\n')) {
1435
- const line = rawLine.trim();
1436
- if (!line) continue; // strip blank-line repaint padding
1437
- if (chromeRegexes.some((re) => re.test(line))) continue; // strip declared chrome
1438
- kept.push(line);
1439
- }
1440
- return kept.join('\n');
1441
- }
1442
-
1443
- /**
1444
- * True when `modal` is a stale re-parse of an already-resolved approval:
1445
- * same message text, and the chrome-stripped approval-context signature
1446
- * of `snap` is unchanged from the signature captured at resolve time.
1447
- *
1448
- * Public and reused verbatim by BOTH the settled-eval capture path
1449
- * (applyWaitingApproval, below) and any OUTSIDE re-parse the adapter
1450
- * performs independently of the settle loop — e.g. provider-cli-adapter's
1451
- * getStatus()/getDebugState() startup-gate modal detection, which reads
1452
- * `recentOutputBuffer` directly while `startupParseGate` is open and can
1453
- * re-surface the same already-resolved modal before the gate closes.
1454
- * Centralizing the discriminator here means there is exactly ONE
1455
- * definition of "stale" for the whole session — no divergent duplicate
1456
- * heuristic re-implemented per call site.
1457
- */
1458
- isStaleResolvedApproval(modal: { message: string; buttons: string[] } | null, snap: ApprovalSignatureSnapshot): boolean {
1459
- if (!modal) return false;
1460
- const normalizedMessage = typeof modal.message === 'string' ? modal.message.trim() : '';
1461
- if (!normalizedMessage) return false;
1462
- return this.lastApprovalResolvedAt > 0
1463
- && normalizedMessage === this.lastResolvedModalMessage
1464
- && this.computeApprovalContentSignature(snap) === this.lastApprovalResolvedContentSignature;
1465
- }
1466
-
1467
- /** Clear all resolve-time approval bookkeeping (message, timestamp, content
1468
- * signature) — called at turn/session boundaries so a next-turn or
1469
- * next-session approval is never compared against stale prior state. */
1470
- private clearApprovalResolutionMemory(): void {
1471
- this.lastApprovalResolvedAt = 0;
1472
- this.lastResolvedModalMessage = '';
1473
- this.lastApprovalResolvedContentSignature = '';
1474
- }
1475
-
1476
1259
  /**
1477
1260
  * Schedule one more settled evaluation while pinned to `waiting_approval`
1478
1261
  * with no actionable modal. The settled FSM normally only re-runs on new
@@ -39,7 +39,6 @@ import {
39
39
  compactPromptText,
40
40
  estimatePromptDisplayLines,
41
41
  extractPromptRetrySnippet,
42
- isPurePtyTranscriptProvider,
43
42
  listCliScriptNames,
44
43
  normalizePromptText,
45
44
  normalizeScreenSnapshot,
@@ -457,7 +456,12 @@ export class ProviderCliAdapter implements CliAdapter {
457
456
  * provider-owned) so no other provider's turn-scoped parse changes.
458
457
  */
459
458
  private parsesFullPtyTranscriptFromBuffer(): boolean {
460
- return isPurePtyTranscriptProvider(this.provider);
459
+ if (this.providerOwnsTranscript()) return false;
460
+ // nativeHistory is a top-level provider field not surfaced on
461
+ // CliProviderModule; read it via the same structural cast used for `tui`.
462
+ if ((this.provider as { nativeHistory?: unknown }).nativeHistory) return false;
463
+ const transcriptPty = (this.provider.tui as { transcriptPty?: { scope?: unknown } } | undefined)?.transcriptPty;
464
+ return transcriptPty?.scope === 'buffer';
461
465
  }
462
466
 
463
467
  /**
@@ -703,10 +707,8 @@ export class ProviderCliAdapter implements CliAdapter {
703
707
  }
704
708
  });
705
709
 
706
- this.ptyProcess.onExit(({ exitCode, signal }: { exitCode: number | null; signal?: number | null }) => {
707
- // Preserve the unknown case: a null exitCode (signal-terminated or
708
- // otherwise unreported) is logged as "unknown", never as exit 0.
709
- LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode === null || exitCode === undefined ? 'unknown' : exitCode}${signal ? ` (signal ${signal})` : ''}`);
710
+ this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
711
+ LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
710
712
  this.flushPendingOutputParse();
711
713
  this.ptyProcess = null;
712
714
  this.engine.onPtyExit();
@@ -1070,19 +1072,7 @@ export class ProviderCliAdapter implements CliAdapter {
1070
1072
 
1071
1073
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1072
1074
  const allowParse = options.allowParse !== false;
1073
- let startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1074
- // (fix: kimi startup-gate stale-approval bypass) startupParseGate can
1075
- // still be open by the time a real approval is requested AND resolved
1076
- // — this ad-hoc parse of recentOutputBuffer runs independently of the
1077
- // settled-eval loop and previously had no staleness protection at all,
1078
- // so it kept re-surfacing the identical already-resolved modal for as
1079
- // long as its text remained anywhere in the rolling buffer, bypassing
1080
- // engine.activeModal (which resolveModal() had already correctly
1081
- // cleared). Reuse the SAME discriminator the settle loop uses instead
1082
- // of inventing a second one here.
1083
- if (startupModal && this.engine.isStaleResolvedApproval(startupModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
1084
- startupModal = null;
1085
- }
1075
+ const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1086
1076
  const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
1087
1077
  ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
1088
1078
  : null;
@@ -1189,20 +1179,7 @@ export class ProviderCliAdapter implements CliAdapter {
1189
1179
  // and let a later evaluate find the complete modal.
1190
1180
  const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
1191
1181
  && liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
1192
- // (fix: kimi live-detect stale-approval bypass) This fallback is
1193
- // NOT gated by startupParseGate — it runs on every getStatus()
1194
- // call (including the periodic background status heartbeat)
1195
- // for as long as isWaitingForResponse stays true, which for a
1196
- // provider that keeps "thinking" after the approved tool call
1197
- // can be the whole rest of the turn. Its own unguarded re-parse
1198
- // of recentOutputBuffer/terminalScreen previously kept
1199
- // re-writing engine.activeModal directly — bypassing setStatus
1200
- // (so rawStatus never even flipped) and bypassing
1201
- // applyWaitingApproval's staleness guard entirely, silently
1202
- // re-corrupting the engine's own state moments after
1203
- // resolveModal() had correctly cleared it. Reuse the same
1204
- // engine-owned discriminator here too.
1205
- if (liveModal && buttonsOk && !this.engine.isStaleResolvedApproval(liveModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
1182
+ if (liveModal && buttonsOk) {
1206
1183
  effectiveModal = liveModal;
1207
1184
  if (!this.engine.activeModal) this.engine.activeModal = liveModal;
1208
1185
  }
@@ -2497,14 +2474,7 @@ export class ProviderCliAdapter implements CliAdapter {
2497
2474
 
2498
2475
  getDebugState(): Record<string, any> {
2499
2476
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2500
- let startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2501
- // (fix: kimi startup-gate stale-approval bypass) See the matching
2502
- // comment in getStatus() — this ad-hoc startup-gate parse bypassed
2503
- // engine.activeModal's staleness protection entirely. Reuse the same
2504
- // engine-owned discriminator rather than duplicating it here.
2505
- if (startupModal && this.engine.isStaleResolvedApproval(startupModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
2506
- startupModal = null;
2507
- }
2477
+ const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2508
2478
  const startupDetectedStatus = this.startupParseGate && !startupModal
2509
2479
  ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2510
2480
  : null;
@@ -254,47 +254,6 @@ export interface CliProviderModule {
254
254
  _versionWarning?: string | null;
255
255
  }
256
256
 
257
- /**
258
- * PURE-PTY TRANSCRIPT CLASS predicate (kimi and kin).
259
- *
260
- * A provider is "pure-PTY full-buffer" when it reconstructs its ENTIRE transcript
261
- * from the rendered PTY buffer on every read and has NO alternate transcript
262
- * source:
263
- * - transcriptAuthority !== 'provider' (the daemon PTY parser owns the transcript,
264
- * not a provider-side canonical source)
265
- * - NO nativeHistory (no on-disk / native-source history to fall back to)
266
- * - tui.transcriptPty.scope === 'buffer' (the parser walks the full rendered buffer)
267
- *
268
- * This class is invisible to two provider-authority-keyed code paths that other
269
- * providers rely on for mesh completion semantics:
270
- * 1. CliStateEngine.onTurnStarted only promotes to 'generating' for
271
- * transcriptAuthority==='provider' providers — so a pure-PTY session that
272
- * submits a prompt while already idle collapses idle→idle, the
273
- * generating→idle edge never occurs, and agent:generating_completed is never
274
- * emitted (the coordinator ledger leaves the task 'assigned' forever).
275
- * 2. checkMeshWorkerStall's native-transcript completion reconcile is gated on
276
- * the native-source shape, so a finished pure-PTY worker's static idle is
277
- * misread as monitor:no_progress (a false task_stalled).
278
- *
279
- * The runtime capability, NOT any single spec field value, is authoritative: a
280
- * given kimi manifest checkout may declare nativeHistory/transcriptAuthority, but
281
- * the live-loaded pure-PTY session has none. Callers that only hold a
282
- * CliProviderModule (the engine) share this exact predicate with the adapter
283
- * (parsesFullPtyTranscriptFromBuffer) so the two never drift.
284
- */
285
- export function isPurePtyTranscriptProvider(provider: {
286
- transcriptAuthority?: 'provider' | 'daemon';
287
- nativeHistory?: unknown;
288
- tui?: Record<string, unknown>;
289
- }): boolean {
290
- if (provider.transcriptAuthority === 'provider') return false;
291
- // nativeHistory is a top-level provider field not surfaced on
292
- // CliProviderModule; read it via the same structural shape the adapter uses.
293
- if (provider.nativeHistory) return false;
294
- const transcriptPty = (provider.tui as { transcriptPty?: { scope?: unknown } } | undefined)?.transcriptPty;
295
- return transcriptPty?.scope === 'buffer';
296
- }
297
-
298
257
  function stripAnsi(str: string): string {
299
258
  // eslint-disable-next-line no-control-regex
300
259
  return str
@@ -37,8 +37,7 @@ export interface PtyRuntimeTransport {
37
37
  getMetadata?(): PtyRuntimeMetadata | null;
38
38
  onData(callback: (data: string) => void): void;
39
39
  onExit(callback: (info: {
40
- exitCode: number | null;
41
- signal?: number | null;
40
+ exitCode: number;
42
41
  }) => void): void;
43
42
  }
44
43
  export interface PtyTransportFactory {
@@ -62,7 +62,7 @@ export interface PtyRuntimeTransport {
62
62
  updateMeta?(meta: Record<string, unknown>, replace?: boolean): void;
63
63
  getMetadata?(): PtyRuntimeMetadata | null;
64
64
  onData(callback: (data: string) => void): void;
65
- onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void;
65
+ onExit(callback: (info: { exitCode: number }) => void): void;
66
66
  }
67
67
 
68
68
  export interface PtyTransportFactory {
@@ -42,7 +42,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
42
42
 
43
43
  private readonly client: SessionHostClient;
44
44
  private readonly dataCallbacks = new Set<(data: string) => void>();
45
- private readonly exitCallbacks = new Set<(info: { exitCode: number | null; signal?: number | null }) => void>();
45
+ private readonly exitCallbacks = new Set<(info: { exitCode: number }) => void>();
46
46
  private readonly pendingOutput: string[] = [];
47
47
  private operationChain = Promise.resolve();
48
48
  private unsubscribe: (() => void) | null = null;
@@ -79,7 +79,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
79
79
  }
80
80
  }
81
81
 
82
- onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void {
82
+ onExit(callback: (info: { exitCode: number }) => void): void {
83
83
  this.exitCallbacks.add(callback);
84
84
  }
85
85
 
@@ -361,13 +361,8 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
361
361
  return;
362
362
  }
363
363
  if (event.type === 'session_exit') {
364
- // Preserve the nullable/unknown exitCode and signal exactly as the
365
- // session host reported them — never collapse null to 0, which would
366
- // make a signal-terminated process indistinguishable from a clean exit.
367
- const exitCode = typeof event.exitCode === 'number' ? event.exitCode : null;
368
- const signal = typeof event.signal === 'number' ? event.signal : null;
369
364
  for (const callback of this.exitCallbacks) {
370
- callback({ exitCode, signal });
365
+ callback({ exitCode: event.exitCode ?? 0 });
371
366
  }
372
367
  void this.closeClient(false);
373
368
  }