@adhdev/daemon-core 1.0.18-rc.1 → 1.0.18-rc.10
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.
- package/dist/cli-adapters/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +7 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +63 -0
- package/dist/index.js +5818 -4496
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5853 -4525
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +204 -3
- package/src/cli-adapters/provider-cli-adapter.ts +39 -5
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/upgrade-helper.ts +146 -79
- package/src/commands/windows-atomic-upgrade.ts +631 -0
- package/src/config/mesh-json-config.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +256 -10
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-ledger.ts +72 -0
- package/src/mesh/mesh-queue-assignment.ts +126 -1
- package/src/mesh/mesh-reconcile-loop.ts +49 -0
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/providers/cli-provider-instance-types.ts +25 -0
- package/src/providers/cli-provider-instance.ts +116 -0
- package/src/session-host/managed-host.ts +34 -0
|
@@ -68,6 +68,41 @@ export interface CoordinatorOperatingNote {
|
|
|
68
68
|
category?: 'provider_quirk' | 'pattern_to_avoid' | 'recovery_lesson';
|
|
69
69
|
createdAt?: string;
|
|
70
70
|
sourceCoordinator?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Operating-notes lifecycle (minimal first cut). When true the note ALWAYS
|
|
73
|
+
* rides into the coordinator prompt — it is never dropped by TTL expiry and
|
|
74
|
+
* survives the injection cap ahead of unpinned notes. Legacy notes without
|
|
75
|
+
* this field default to false.
|
|
76
|
+
*/
|
|
77
|
+
pinned?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Optional explicit expiry ISO timestamp. When set and in the past, an
|
|
80
|
+
* UNPINNED note is dropped from the injected prompt (read-side only — the
|
|
81
|
+
* ledger entry is never pruned by age). When absent, the category→TTL map
|
|
82
|
+
* (see isNoteExpired) governs expiry; pinned notes never expire regardless.
|
|
83
|
+
*/
|
|
84
|
+
expiresAt?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Phase 2 — the ledger id of this note, when known. Threaded from the ledger
|
|
87
|
+
* entry id at launch so version-supersede can target a specific note and
|
|
88
|
+
* same-class folding can list the subsumed ids. Repo-declared notes may lack
|
|
89
|
+
* one; absent is fine (folding/supersede fall back to the subject key).
|
|
90
|
+
*/
|
|
91
|
+
noteId?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Phase 2 (b) version-supersede — an optional note-id or stable subject-key
|
|
94
|
+
* this note replaces. At injection, any earlier LIVE note whose `noteId` or
|
|
95
|
+
* `subjectKey` matches this value is hidden from the prompt (the ledger entry
|
|
96
|
+
* is retained for audit). Optional and lossless: absent = supersedes nothing.
|
|
97
|
+
*/
|
|
98
|
+
supersedes?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Phase 2 (b)/(c) — an optional stable subject-key grouping notes about the
|
|
101
|
+
* same subject. Drives version-supersede targeting and same-class folding.
|
|
102
|
+
* When absent, folding derives a key from a leading `[tag]` bracket instead,
|
|
103
|
+
* so legacy notes still collapse by their conventional tag prefix.
|
|
104
|
+
*/
|
|
105
|
+
subjectKey?: string;
|
|
71
106
|
}
|
|
72
107
|
export interface CoordinatorPromptContext {
|
|
73
108
|
mesh: LocalMeshEntry;
|
|
@@ -102,6 +137,46 @@ export interface CoordinatorPromptContext {
|
|
|
102
137
|
magiKindPanels?: MagiKindPanelMap;
|
|
103
138
|
}
|
|
104
139
|
export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
|
|
140
|
+
/**
|
|
141
|
+
* Phase 2 (c) — deterministic same-class fold. Given ranked notes (already
|
|
142
|
+
* highest-priority first), collapse runs of UNPINNED notes that share the same
|
|
143
|
+
* category AND subject key into a single injected entry: keep the highest-ranked
|
|
144
|
+
* (first-seen) note and record the note-ids/count it subsumes so the rendered
|
|
145
|
+
* line can say "(+N earlier)". Pinned notes never fold — each pinned note is
|
|
146
|
+
* author-opted-in and shown verbatim. The store is untouched; this is a pure,
|
|
147
|
+
* model-free text fold at injection time.
|
|
148
|
+
*
|
|
149
|
+
* Returns the folded note list (order preserved) with per-entry subsumed info.
|
|
150
|
+
*/
|
|
151
|
+
interface FoldedNote {
|
|
152
|
+
note: CoordinatorOperatingNote;
|
|
153
|
+
/** note-ids of same-class notes this entry subsumes (may be empty). */
|
|
154
|
+
subsumedIds: string[];
|
|
155
|
+
/** count of subsumed notes (== subsumedIds.length, but counts id-less ones too). */
|
|
156
|
+
subsumedCount: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Operating-notes lifecycle selection (read-side). Given the effective notes
|
|
160
|
+
* (oldest-first, ledger order) and the current time, produce the ordered list
|
|
161
|
+
* that rides into the prompt:
|
|
162
|
+
* (i) ALWAYS include pinned notes.
|
|
163
|
+
* (ii) drop expired UNPINNED notes (per category TTL / explicit expiresAt).
|
|
164
|
+
* (iii) Phase 2 (b) drop any note SUPERSEDED by a later live note (by noteId
|
|
165
|
+
* or subjectKey); pinned notes are never dropped by supersede.
|
|
166
|
+
* (iv) rank pinned-first, then durable-category, then recency (newest first).
|
|
167
|
+
* (v) Phase 2 (c) fold same-category/same-subject unpinned runs into one.
|
|
168
|
+
* (vi) Phase 2 (d) fill up to a byte budget AND a count cap; pinned always
|
|
169
|
+
* kept even if they alone exceed the byte budget.
|
|
170
|
+
* Pure + deterministic given `now`. Returns { shown, omittedCount } where `shown`
|
|
171
|
+
* carries per-entry fold info for the renderer.
|
|
172
|
+
*
|
|
173
|
+
* Ranking is stable on the original ledger index so, within a tier, newest notes
|
|
174
|
+
* lead. The caps keep the leading (highest-priority) entries.
|
|
175
|
+
*/
|
|
176
|
+
export declare function selectOperatingNotesForPrompt(notes: CoordinatorOperatingNote[], now: number, cap?: number, byteBudget?: number): {
|
|
177
|
+
shown: FoldedNote[];
|
|
178
|
+
omittedCount: number;
|
|
179
|
+
};
|
|
105
180
|
/**
|
|
106
181
|
* Render the machine-local MAGI kind-panel bindings so the coordinator KNOWS
|
|
107
182
|
* which cross-verification panels (rca / design / claim_audit / freeform) are
|
|
@@ -114,3 +189,4 @@ export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptConte
|
|
|
114
189
|
* That keeps a MAGI-less mesh's prompt byte-identical to before.
|
|
115
190
|
*/
|
|
116
191
|
export declare function buildMagiKindPanelsSection(panels: MagiKindPanelMap | undefined | null): string | null;
|
|
192
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { SessionHostSurfaceRecordLike } from '../session-host/runtime-surface.js';
|
|
2
|
+
import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
3
|
+
export declare const DAY_MS: number;
|
|
4
|
+
/** JSONL ledger files are legacy after the SQLite ledger — 30-day lifetime. */
|
|
5
|
+
export declare const LEDGER_JSONL_MAX_AGE_MS: number;
|
|
6
|
+
/** Terminated session-host runtimes: conservative 14-day retention. */
|
|
7
|
+
export declare const SESSION_HOST_RUNTIME_MAX_AGE_MS: number;
|
|
8
|
+
/** mesh-runtime.db.bak-* backups: 7-day retention. */
|
|
9
|
+
export declare const DB_BAK_MAX_AGE_MS: number;
|
|
10
|
+
/** A file candidate for age-based pruning: absolute path + last-modified epoch ms. */
|
|
11
|
+
export interface AgedFile {
|
|
12
|
+
path: string;
|
|
13
|
+
mtimeMs: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* PURE. Select the JSONL ledger files whose mtime is older than `maxAgeMs`
|
|
17
|
+
* relative to `now`. A file exactly at the threshold is KEPT (strict `>`), so a
|
|
18
|
+
* 30-day-old file survives its 30th day and is pruned on the 31st. Deterministic:
|
|
19
|
+
* no fs access, no clock read.
|
|
20
|
+
*/
|
|
21
|
+
export declare function selectExpiredLedgerJsonl(files: AgedFile[], now: number, maxAgeMs?: number): AgedFile[];
|
|
22
|
+
/** A parsed session-host runtime file: its path, mtime, and the wrapped record. */
|
|
23
|
+
export interface SessionHostRuntimeFile {
|
|
24
|
+
path: string;
|
|
25
|
+
mtimeMs: number;
|
|
26
|
+
/** The `record` object from the on-disk `{ record, snapshot, updatedAt }` file. */
|
|
27
|
+
record: SessionHostSurfaceRecordLike | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* PURE. Select the session-host runtime files safe to delete: ONLY those whose
|
|
31
|
+
* runtime is terminated/dead (NOT a live runtime — decided by the session-host-core
|
|
32
|
+
* SSOT `isSessionHostLiveRuntime`) AND older than `maxAgeMs`. A live runtime, or a
|
|
33
|
+
* dead-but-recent one, is always kept. A file whose record failed to parse is treated
|
|
34
|
+
* as NON-live but is still age-gated, so a corrupt-but-fresh file is never removed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function selectExpiredSessionHostRuntimes(files: SessionHostRuntimeFile[], now: number, maxAgeMs?: number): SessionHostRuntimeFile[];
|
|
37
|
+
/** True for a `mesh-runtime.db.bak-*` backup filename (basename only). */
|
|
38
|
+
export declare function isDbBackupFileName(name: string): boolean;
|
|
39
|
+
/** PURE. Select `.bak-*` backups older than `maxAgeMs`. Strict `>` (see ledger). */
|
|
40
|
+
export declare function selectExpiredDbBackups(files: AgedFile[], now: number, maxAgeMs?: number): AgedFile[];
|
|
41
|
+
/** Minimal shape needed to decide whether a worktree path is orphaned. */
|
|
42
|
+
export interface WorktreePathLike {
|
|
43
|
+
path: string;
|
|
44
|
+
bare?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** A live mesh node's known workspace paths (self workspace + repoRoot, normalized). */
|
|
47
|
+
export interface LiveNodeWorkspaceLike {
|
|
48
|
+
workspace?: string;
|
|
49
|
+
repoRoot?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* PURE. Given the worktrees git reports on disk and the set of live-node workspace
|
|
53
|
+
* paths, return the worktrees that have NO matching live node — the orphan
|
|
54
|
+
* cleanup_candidates.
|
|
55
|
+
*
|
|
56
|
+
* SAFETY:
|
|
57
|
+
* - `mainWorktreePath` (the primary repo checkout, i.e. worktree[0]) is NEVER an
|
|
58
|
+
* orphan — it is the base repo, not a mesh clone.
|
|
59
|
+
* - `bare` worktrees are skipped (git's internal bookkeeping, not a node checkout).
|
|
60
|
+
* - Matching is path-equality after trailing-separator normalization against the
|
|
61
|
+
* union of every live node's `workspace` and `repoRoot`.
|
|
62
|
+
* This is DETECTION ONLY — the caller signals a cleanup_candidate; it must not delete.
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectOrphanWorktrees(worktrees: WorktreePathLike[], liveNodes: LiveNodeWorkspaceLike[], mainWorktreePath: string): WorktreePathLike[];
|
|
65
|
+
/**
|
|
66
|
+
* Prune expired legacy JSONL ledger files under ~/.adhdev/mesh-ledger/.
|
|
67
|
+
* Matches only `*.jsonl` (never the SQLite .db / -wal / -shm files). Returns the
|
|
68
|
+
* count deleted. Best-effort: individual unlink failures are logged, not thrown.
|
|
69
|
+
*/
|
|
70
|
+
export declare function pruneExpiredLedgerJsonl(now?: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Prune expired `mesh-runtime.db.bak-*` backups under ~/.adhdev/mesh-ledger/.
|
|
73
|
+
* Never touches the live DB (only names matching the .bak- prefix). Returns count.
|
|
74
|
+
*/
|
|
75
|
+
export declare function pruneExpiredDbBackups(now?: number): number;
|
|
76
|
+
/**
|
|
77
|
+
* Prune terminated session-host runtime files older than 14 days across every
|
|
78
|
+
* ~/.adhdev/session-host/<app>/runtimes/ directory. A LIVE runtime is never deleted
|
|
79
|
+
* regardless of age (isSessionHostLiveRuntime SSOT). Returns count deleted.
|
|
80
|
+
*/
|
|
81
|
+
export declare function pruneExpiredSessionHostRuntimes(now?: number): number;
|
|
82
|
+
/**
|
|
83
|
+
* Run the file-deleting retention passes (JSONL ledger, DB backups, session-host
|
|
84
|
+
* runtimes) once. Each pass is isolated so one failing pass never blocks the others.
|
|
85
|
+
* Orphan-worktree DETECTION is driven separately in the reconcile loop (it needs the
|
|
86
|
+
* live mesh config + git worktree list and emits a ledger signal rather than deleting).
|
|
87
|
+
*/
|
|
88
|
+
export declare function runDiskRetentionSweep(now?: number): {
|
|
89
|
+
ledgerJsonl: number;
|
|
90
|
+
dbBackups: number;
|
|
91
|
+
sessionHostRuntimes: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Detect orphaned worktrees for ONE mesh and emit a `worktree_cleanup_candidate`
|
|
95
|
+
* ledger signal for each — DETECTION ONLY, never deletes. An orphan is a git worktree
|
|
96
|
+
* on disk with no matching live mesh node (compared by path). It:
|
|
97
|
+
* 1. picks a base (non-worktree) node owned by this mesh to anchor `git worktree list`;
|
|
98
|
+
* 2. diffs the reported worktrees against the union of every node's workspace/repoRoot;
|
|
99
|
+
* 3. skips the main worktree + bare entries (detectOrphanWorktrees safety);
|
|
100
|
+
* 4. suppresses a repeat for a worktreePath already signalled within the recent window
|
|
101
|
+
* (idempotent re-emit guard), so the hourly sweep doesn't spam the ledger.
|
|
102
|
+
* Returns the list of newly-signalled orphan paths. Best-effort: git/ledger failures are
|
|
103
|
+
* logged and yield an empty result, never thrown.
|
|
104
|
+
*/
|
|
105
|
+
export declare function detectAndSignalOrphanWorktrees(mesh: LocalMeshEntry, now?: number): Promise<string[]>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
17
|
import { type MeshLedgerOriginatingCoordinatorV2 } from './contracts.js';
|
|
18
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection';
|
|
18
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
|
|
19
19
|
export interface MeshLedgerEntry {
|
|
20
20
|
id: string;
|
|
21
21
|
meshId: string;
|
|
@@ -162,6 +162,33 @@ export declare const OPERATING_NOTE_KIND: MeshLedgerKind;
|
|
|
162
162
|
export declare const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind;
|
|
163
163
|
export declare const OPERATING_NOTE_DEDUPE_WINDOW = 40;
|
|
164
164
|
export declare const OPERATING_NOTE_KEEP_LATEST = 100;
|
|
165
|
+
export declare const OPERATING_NOTE_CATEGORY_TTL_DAYS: Readonly<Record<string, number>>;
|
|
166
|
+
/**
|
|
167
|
+
* Shape isNoteExpired reads. Structural so mesh-ledger stays free of a
|
|
168
|
+
* coordinator-prompt import (CoordinatorOperatingNote satisfies this).
|
|
169
|
+
*/
|
|
170
|
+
export interface OperatingNoteExpiryInput {
|
|
171
|
+
category?: string;
|
|
172
|
+
pinned?: boolean;
|
|
173
|
+
createdAt?: string;
|
|
174
|
+
/** Explicit expiry override; wins over the category TTL when parseable. */
|
|
175
|
+
expiresAt?: string;
|
|
176
|
+
/** Fallback creation time (ledger entry timestamp) when createdAt absent. */
|
|
177
|
+
timestamp?: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Pure helper: is this UNPINNED operating note expired as of `now` (epoch ms)?
|
|
181
|
+
*
|
|
182
|
+
* Rules:
|
|
183
|
+
* - pinned notes NEVER expire (always false).
|
|
184
|
+
* - an explicit, parseable `expiresAt` in the past → expired.
|
|
185
|
+
* - otherwise the category TTL applies; a durable category (provider_quirk,
|
|
186
|
+
* uncategorized, or any category not in the TTL map) never expires.
|
|
187
|
+
* - age is measured from createdAt, falling back to `timestamp` (ledger entry
|
|
188
|
+
* time). If neither is a valid date, the note is treated as NOT expired
|
|
189
|
+
* (never silently drop a note we cannot age).
|
|
190
|
+
*/
|
|
191
|
+
export declare function isNoteExpired(note: OperatingNoteExpiryInput, now: number): boolean;
|
|
165
192
|
export declare function getLedgerDir(): string;
|
|
166
193
|
/**
|
|
167
194
|
* Footer to append to worker task messages so workers output structured results
|
|
@@ -23,6 +23,17 @@ export declare class MeshRuntimeStore {
|
|
|
23
23
|
private static loggedGetInstanceFailure;
|
|
24
24
|
static getInstance(): MeshRuntimeStore;
|
|
25
25
|
static resetForTests(): void;
|
|
26
|
+
/**
|
|
27
|
+
* VACUUM the SQLite database to reclaim on-disk space. Retention prunes rows
|
|
28
|
+
* with DELETE, which frees pages inside the file but does NOT shrink it — the
|
|
29
|
+
* mesh-runtime.db grew to hundreds of MB (mission 86def38d disk-accumulation
|
|
30
|
+
* bootstrap failure) precisely because the file was never compacted. This
|
|
31
|
+
* rewrites the DB into a minimal footprint. Best-effort: a VACUUM failure (e.g.
|
|
32
|
+
* insufficient temp space, a read lock) is logged and swallowed so it can never
|
|
33
|
+
* block daemon shutdown. Called once on shutdown (see daemon-lifecycle), never
|
|
34
|
+
* on the hot path — VACUUM takes an exclusive lock and rewrites the whole file.
|
|
35
|
+
*/
|
|
36
|
+
vacuum(): void;
|
|
26
37
|
close(): void;
|
|
27
38
|
transaction<T>(fn: () => T): T;
|
|
28
39
|
private migrate;
|
|
@@ -39,6 +39,8 @@ export declare const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
|
39
39
|
export declare const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30000;
|
|
40
40
|
export declare const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 4000;
|
|
41
41
|
export declare const PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS = 1200;
|
|
42
|
+
export declare const ANTIGRAVITY_HOLD_QUIET_DWELL_MS = 3000;
|
|
43
|
+
export declare const ANTIGRAVITY_HOLD_HARD_CAP_MS: number;
|
|
42
44
|
export declare const BACKGROUND_TASK_HOLD_MAX_MS: number;
|
|
43
45
|
export declare const USER_INPUT_ACK_DEDUP_WINDOW_MS = 60000;
|
|
44
46
|
export declare const STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS = 12000;
|
|
@@ -201,6 +201,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
201
201
|
private generatingDebounceTimer;
|
|
202
202
|
private generatingDebouncePending;
|
|
203
203
|
private lastApprovalEventFingerprint;
|
|
204
|
+
private lastInteractivePromptEventKey;
|
|
204
205
|
private autoApproveBusy;
|
|
205
206
|
private autoApproveBusyTimer;
|
|
206
207
|
private lastAutoApprovalSignature;
|
|
@@ -473,6 +474,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
473
474
|
private completionFinalSummary;
|
|
474
475
|
private buildCompletedFinalizationDiagnostic;
|
|
475
476
|
private hasAdapterPendingResponse;
|
|
477
|
+
private antigravityHoldPtyStillActive;
|
|
476
478
|
private shouldSuppressStaleParsedBusyStatus;
|
|
477
479
|
private getCompletedFinalizationBlock;
|
|
478
480
|
private hasApprovalResolutionEvidence;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.18-rc.
|
|
3
|
+
"version": "1.0.18-rc.10",
|
|
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.
|
|
51
|
-
"@adhdev/session-host-core": "1.0.18-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.18-rc.10",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.18-rc.10",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -39,6 +39,7 @@ 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';
|
|
42
43
|
import { loadMeshCoordinatorRegistry } from '../mesh/coordinator-registry.js';
|
|
43
44
|
import { applyProcessHardening } from './process-hardening.js';
|
|
44
45
|
import { installProviderProcessShim } from '../providers/sdk/v1/sandbox/require-whitelist.js';
|
|
@@ -503,4 +504,13 @@ export async function shutdownDaemonComponents(components: DaemonComponents): Pr
|
|
|
503
504
|
try { m.disconnect(); } catch { /* noop */ }
|
|
504
505
|
}
|
|
505
506
|
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 */ }
|
|
506
516
|
}
|
|
@@ -86,6 +86,18 @@ interface IdleFinishCandidate {
|
|
|
86
86
|
assistantLength: number;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Minimal shape computeApprovalContentSignature / isStaleResolvedApproval
|
|
91
|
+
* actually need — deliberately narrower than the full CliBufferSnapshot so
|
|
92
|
+
* callers outside the settled-eval loop (e.g. the adapter's startup-gate
|
|
93
|
+
* modal parse in getStatus/getDebugState) can supply just these two fields
|
|
94
|
+
* without having to fabricate an entire snapshot.
|
|
95
|
+
*/
|
|
96
|
+
interface ApprovalSignatureSnapshot {
|
|
97
|
+
screenText?: string;
|
|
98
|
+
accumulatedBuffer?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
interface SettledEvalContext {
|
|
90
102
|
now: number;
|
|
91
103
|
modal: { message: string; buttons: string[] } | null;
|
|
@@ -155,6 +167,22 @@ export class CliStateEngine {
|
|
|
155
167
|
// ── Approval ─────────────────────────────────────
|
|
156
168
|
lastApprovalResolvedAt = 0;
|
|
157
169
|
lastResolvedModalMessage = '';
|
|
170
|
+
/**
|
|
171
|
+
* Normalized approval-context signature captured at resolve time (see
|
|
172
|
+
* `computeApprovalContentSignature`) — the screen text with blank-line
|
|
173
|
+
* padding and any manifest-declared chrome (transcriptPty.chromePatterns,
|
|
174
|
+
* spinner.patterns) stripped out. Used by applyWaitingApproval's
|
|
175
|
+
* isStaleResolvedRepaint check to tell "the same already-answered modal,
|
|
176
|
+
* re-parsed from a screen that has not meaningfully changed" apart from
|
|
177
|
+
* "a genuinely new approval" — content-based, not time-based, because
|
|
178
|
+
* ordinary TUI chrome (status bar, context meter, blank-line repaint)
|
|
179
|
+
* keeps producing fresh PTY bytes on an otherwise-unchanged screen and
|
|
180
|
+
* defeats any output-timestamp discriminator within a few hundred ms.
|
|
181
|
+
* Cleared whenever a turn starts or the session tears down (see
|
|
182
|
+
* onTurnStarted / resetActiveTurnState / onPtyExit) so a next-turn
|
|
183
|
+
* approval is never compared against stale prior-turn state.
|
|
184
|
+
*/
|
|
185
|
+
lastApprovalResolvedContentSignature = '';
|
|
158
186
|
/**
|
|
159
187
|
* Monotonic counter bumped every time the FSM *enters* waiting_approval
|
|
160
188
|
* with a freshly captured modal (see `applyWaitingApproval`). It is the
|
|
@@ -304,12 +332,46 @@ export class CliStateEngine {
|
|
|
304
332
|
// anchor its window on dispatch time rather than completion time.
|
|
305
333
|
this.currentTurnStartedAt = Date.now();
|
|
306
334
|
this.responseEpoch += 1;
|
|
335
|
+
// A new turn's own prompt echo is real new content, so the stale-repaint
|
|
336
|
+
// guard's signature would naturally drift anyway — but clear the
|
|
337
|
+
// resolve-time bookkeeping explicitly here too, so a next-turn approval
|
|
338
|
+
// that happens to share message text with a previous turn's already-
|
|
339
|
+
// resolved one is never compared against stale prior-turn state.
|
|
340
|
+
this.clearApprovalResolutionMemory();
|
|
341
|
+
// (fix: kimi K3 send→idle-looking→generating lag / missed-generating on
|
|
342
|
+
// fast tool turns) For transcriptAuthority:'provider' providers (kimi,
|
|
343
|
+
// and other native-transcript sources), the PTY-scanned parser always
|
|
344
|
+
// returns messages:[] — the provider owns the transcript, not the PTY —
|
|
345
|
+
// so evaluateSettled's shouldHoldGenerating fast-path exception
|
|
346
|
+
// (hasFinalCurrentTurnAssistant) can never release early and
|
|
347
|
+
// recent_activity_hold ends up carrying the ENTIRE "is this turn still
|
|
348
|
+
// generating" signal for these providers regardless of what the spinner
|
|
349
|
+
// script does. That fallback only fires once a settle tick actually
|
|
350
|
+
// runs, which needs PTY output to schedule — for a model that "thinks"
|
|
351
|
+
// silently for many seconds before its first repaint (observed 12-20s
|
|
352
|
+
// for Kimi K3's default "high" effort), the dashboard looks idle for
|
|
353
|
+
// that whole window even though the turn was already accepted. Worse,
|
|
354
|
+
// a turn that fully completes (tool calls + reply) within a single
|
|
355
|
+
// settle debounce window can go straight idle→idle with no visible
|
|
356
|
+
// generating state at all (observed live with a yolo tool-use turn).
|
|
357
|
+
// onTurnStarted is the authoritative "a turn was just submitted" signal
|
|
358
|
+
// — promote to generating immediately instead of waiting on PTY-driven
|
|
359
|
+
// detection. This does not weaken completion detection: applyIdle /
|
|
360
|
+
// finishResponse (authoritative settled evidence) still own the actual
|
|
361
|
+
// idle transition, unchanged. Scoped to transcriptAuthority:'provider'
|
|
362
|
+
// only, so PTY-authoritative providers (whose spinner/settled parsing
|
|
363
|
+
// already drives generating promptly) are unaffected.
|
|
364
|
+
if (this.provider.transcriptAuthority === 'provider' && this.currentStatus !== 'waiting_approval') {
|
|
365
|
+
this.setStatus('generating', 'turn_started');
|
|
366
|
+
this.callbacks.onStatusChange();
|
|
367
|
+
}
|
|
307
368
|
}
|
|
308
369
|
|
|
309
370
|
/** Called when PTY exits */
|
|
310
371
|
onPtyExit(): void {
|
|
311
372
|
this.clearAllTimers();
|
|
312
373
|
this.setStatus('stopped', 'pty_exit');
|
|
374
|
+
this.clearApprovalResolutionMemory();
|
|
313
375
|
}
|
|
314
376
|
|
|
315
377
|
/** Called when adapter starts up successfully */
|
|
@@ -382,6 +444,11 @@ export class CliStateEngine {
|
|
|
382
444
|
this.activeModal = null;
|
|
383
445
|
this.lastApprovalResolvedAt = Date.now();
|
|
384
446
|
this.lastResolvedModalMessage = currentModalMessage;
|
|
447
|
+
// Snapshot the chrome-stripped screen at the moment of resolve — the
|
|
448
|
+
// baseline applyWaitingApproval's isStaleResolvedRepaint check compares
|
|
449
|
+
// against to tell a stale re-parse of THIS modal apart from a
|
|
450
|
+
// genuinely new one, regardless of how much wall-clock time passes.
|
|
451
|
+
this.lastApprovalResolvedContentSignature = this.computeApprovalContentSignature(snap);
|
|
385
452
|
this.lastResolvedEntrySeq = this.approvalEntrySeq;
|
|
386
453
|
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
387
454
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
@@ -499,6 +566,7 @@ export class CliStateEngine {
|
|
|
499
566
|
this.pendingScriptStatusSince = 0;
|
|
500
567
|
this.approvalResumeDeferSince = 0;
|
|
501
568
|
this.approvalResumeDeferEpoch = -1;
|
|
569
|
+
this.clearApprovalResolutionMemory();
|
|
502
570
|
}
|
|
503
571
|
|
|
504
572
|
clearIdleFinishCandidate(reason: string): void {
|
|
@@ -679,7 +747,9 @@ export class CliStateEngine {
|
|
|
679
747
|
if (!status) return;
|
|
680
748
|
|
|
681
749
|
const prevStatus = this.currentStatus;
|
|
682
|
-
const ctx: SettledEvalContext = {
|
|
750
|
+
const ctx: SettledEvalContext = {
|
|
751
|
+
now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus,
|
|
752
|
+
};
|
|
683
753
|
|
|
684
754
|
if (!this.applyPendingScriptStatusDebounce(ctx)) return;
|
|
685
755
|
|
|
@@ -743,7 +813,7 @@ export class CliStateEngine {
|
|
|
743
813
|
this.applyError(ctx, session);
|
|
744
814
|
return;
|
|
745
815
|
}
|
|
746
|
-
if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
|
|
816
|
+
if (status === 'waiting_approval') { this.applyWaitingApproval(ctx, snap); return; }
|
|
747
817
|
if (status === 'generating') { this.applyGenerating(ctx); return; }
|
|
748
818
|
if (status === 'idle') { this.applyIdle(ctx, snap, now); }
|
|
749
819
|
}
|
|
@@ -801,7 +871,7 @@ export class CliStateEngine {
|
|
|
801
871
|
this.callbacks.onStatusChange();
|
|
802
872
|
}
|
|
803
873
|
|
|
804
|
-
private applyWaitingApproval(ctx: SettledEvalContext): void {
|
|
874
|
+
private applyWaitingApproval(ctx: SettledEvalContext, snap: CliBufferSnapshot): void {
|
|
805
875
|
const { modal } = ctx;
|
|
806
876
|
this.clearIdleFinishCandidate('waiting_approval');
|
|
807
877
|
const inCooldown = this.lastApprovalResolvedAt
|
|
@@ -892,6 +962,51 @@ export class CliStateEngine {
|
|
|
892
962
|
}
|
|
893
963
|
return;
|
|
894
964
|
}
|
|
965
|
+
// (fix: kimi stale-approval re-latch, observed live on kimi-code
|
|
966
|
+
// v0.28.1/K3) A freshly-*parsed* modal is not proof the CLI is
|
|
967
|
+
// presenting it right now — parseApproval scans an accumulated
|
|
968
|
+
// raw-output window (recentOutputBuffer / window-around-question
|
|
969
|
+
// scope), which can still contain the text of an approval that was
|
|
970
|
+
// ALREADY resolved a moment ago and re-surface it on the very next
|
|
971
|
+
// settle pass, even though resolveModal() already wrote the key and
|
|
972
|
+
// cleared activeModal. Reproduced live: after approving a tool call,
|
|
973
|
+
// the FSM re-latched `waiting_approval` on the identical
|
|
974
|
+
// already-answered question with NO further genuinely new content
|
|
975
|
+
// ever arriving afterward — the dashboard stayed wedged on "waiting
|
|
976
|
+
// for approval" until the 5-minute maxResponse watchdog forced a
|
|
977
|
+
// recheck.
|
|
978
|
+
//
|
|
979
|
+
// An output-TIMESTAMP discriminator ("has any PTY byte arrived since
|
|
980
|
+
// the resolve") was tried first and found insufficient: a live
|
|
981
|
+
// standalone repro showed kimi's own idle-screen chrome (status bar,
|
|
982
|
+
// context meter, blank-line repaint) advances lastNonEmptyOutputAt
|
|
983
|
+
// within ~300ms of the resolve even though NOTHING approval-relevant
|
|
984
|
+
// changed, so the guard stopped protecting almost immediately instead
|
|
985
|
+
// of for as long as the staleness actually persisted (observed 30s+).
|
|
986
|
+
//
|
|
987
|
+
// Reject a recapture only when ALL of: (a) this is the first capture
|
|
988
|
+
// since the last resolve (`!this.activeModal` — a genuinely repeated
|
|
989
|
+
// approval re-enters this branch too, since resolveModal always nulls
|
|
990
|
+
// activeModal), (b) the message text matches the one we just
|
|
991
|
+
// resolved, AND (c) the chrome-stripped approval-context signature
|
|
992
|
+
// (computeApprovalContentSignature — screen text with blank-line
|
|
993
|
+
// padding and manifest-declared chrome removed) is UNCHANGED from
|
|
994
|
+
// the signature captured at resolve time. (c) is the load-bearing,
|
|
995
|
+
// content-based condition, deliberately not time-bounded: it keeps
|
|
996
|
+
// rejecting for as long as nothing approval-relevant changes,
|
|
997
|
+
// regardless of how many chrome-only repaints occur or how much
|
|
998
|
+
// wall-clock time passes, and it stops rejecting the instant real
|
|
999
|
+
// new content (tool output, a fresh conversational turn) appears.
|
|
1000
|
+
// This also keeps consecutive-approvals-with-identical-text (e.g.
|
|
1001
|
+
// two back-to-back "Allow Bash command?" prompts) working correctly:
|
|
1002
|
+
// a real follow-up approval necessarily means the CLI produced real
|
|
1003
|
+
// new output first (running the previous tool, then asking again),
|
|
1004
|
+
// which changes the signature regardless of shared message text.
|
|
1005
|
+
const isStaleResolvedRepaint = !this.activeModal && this.isStaleResolvedApproval(modal, snap);
|
|
1006
|
+
if (isStaleResolvedRepaint) {
|
|
1007
|
+
LOG.debug('CLI', `[${this.provider.type}] ignoring stale re-parsed approval matching the just-resolved modal (approval-context signature unchanged)`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
895
1010
|
this.modalLostAt = 0;
|
|
896
1011
|
this.isWaitingForResponse = true;
|
|
897
1012
|
this.setStatus('waiting_approval', 'script_detect');
|
|
@@ -1256,6 +1371,92 @@ export class CliStateEngine {
|
|
|
1256
1371
|
|
|
1257
1372
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
1258
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* Derive a stable approval-context signature from the current screen,
|
|
1376
|
+
* with blank-line padding and any manifest-declared chrome stripped out.
|
|
1377
|
+
*
|
|
1378
|
+
* Generic by design — no kimi- or provider-specific hardcoding: it reads
|
|
1379
|
+
* whatever `tui.transcriptPty.chromePatterns` and `tui.spinner.patterns`
|
|
1380
|
+
* the ACTIVE provider's own manifest already declares (present on any
|
|
1381
|
+
* declarative-TUI provider; simply absent/empty for scripted providers,
|
|
1382
|
+
* in which case this degrades to blank-line stripping only — never worse
|
|
1383
|
+
* than comparing the raw screen). Those pattern lists exist precisely to
|
|
1384
|
+
* name "known volatile repaint noise" (status bar, context meter, spinner
|
|
1385
|
+
* ticks, banners) — reusing them here means the SAME declared knowledge
|
|
1386
|
+
* that governs transcript-chrome stripping also governs staleness
|
|
1387
|
+
* detection, instead of re-encoding provider knowledge into daemon-core.
|
|
1388
|
+
*
|
|
1389
|
+
* Deliberately NOT a hash of the whole screen/buffer: an ordinary TUI
|
|
1390
|
+
* repaints its footer/status/context-meter chrome continuously even while
|
|
1391
|
+
* genuinely idle, so a raw whole-screen or whole-buffer fingerprint (or a
|
|
1392
|
+
* mere "did any bytes arrive" timestamp) changes on every repaint tick
|
|
1393
|
+
* regardless of whether anything approval-relevant actually happened.
|
|
1394
|
+
* Stripping the declared chrome first yields a signature that only
|
|
1395
|
+
* changes when the surrounding conversation/tool-output content itself
|
|
1396
|
+
* changes — exactly the discriminator applyWaitingApproval's
|
|
1397
|
+
* isStaleResolvedRepaint check needs.
|
|
1398
|
+
*/
|
|
1399
|
+
private computeApprovalContentSignature(snap: ApprovalSignatureSnapshot): string {
|
|
1400
|
+
const screenText = snap.screenText || snap.accumulatedBuffer || '';
|
|
1401
|
+
if (!screenText) return '';
|
|
1402
|
+
const tui = (this.provider as { tui?: Record<string, any> }).tui;
|
|
1403
|
+
const patternSpecs: Array<{ regex?: unknown; flags?: unknown }> = [
|
|
1404
|
+
...(Array.isArray(tui?.transcriptPty?.chromePatterns) ? tui!.transcriptPty.chromePatterns : []),
|
|
1405
|
+
...(Array.isArray(tui?.spinner?.patterns) ? tui!.spinner.patterns : []),
|
|
1406
|
+
];
|
|
1407
|
+
const chromeRegexes: RegExp[] = [];
|
|
1408
|
+
for (const spec of patternSpecs) {
|
|
1409
|
+
if (spec && typeof spec.regex === 'string') {
|
|
1410
|
+
try {
|
|
1411
|
+
chromeRegexes.push(new RegExp(spec.regex, typeof spec.flags === 'string' ? spec.flags : ''));
|
|
1412
|
+
} catch {
|
|
1413
|
+
// Ignore an unparseable manifest regex — signature just skips that filter.
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const kept: string[] = [];
|
|
1418
|
+
for (const rawLine of screenText.split('\n')) {
|
|
1419
|
+
const line = rawLine.trim();
|
|
1420
|
+
if (!line) continue; // strip blank-line repaint padding
|
|
1421
|
+
if (chromeRegexes.some((re) => re.test(line))) continue; // strip declared chrome
|
|
1422
|
+
kept.push(line);
|
|
1423
|
+
}
|
|
1424
|
+
return kept.join('\n');
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* True when `modal` is a stale re-parse of an already-resolved approval:
|
|
1429
|
+
* same message text, and the chrome-stripped approval-context signature
|
|
1430
|
+
* of `snap` is unchanged from the signature captured at resolve time.
|
|
1431
|
+
*
|
|
1432
|
+
* Public and reused verbatim by BOTH the settled-eval capture path
|
|
1433
|
+
* (applyWaitingApproval, below) and any OUTSIDE re-parse the adapter
|
|
1434
|
+
* performs independently of the settle loop — e.g. provider-cli-adapter's
|
|
1435
|
+
* getStatus()/getDebugState() startup-gate modal detection, which reads
|
|
1436
|
+
* `recentOutputBuffer` directly while `startupParseGate` is open and can
|
|
1437
|
+
* re-surface the same already-resolved modal before the gate closes.
|
|
1438
|
+
* Centralizing the discriminator here means there is exactly ONE
|
|
1439
|
+
* definition of "stale" for the whole session — no divergent duplicate
|
|
1440
|
+
* heuristic re-implemented per call site.
|
|
1441
|
+
*/
|
|
1442
|
+
isStaleResolvedApproval(modal: { message: string; buttons: string[] } | null, snap: ApprovalSignatureSnapshot): boolean {
|
|
1443
|
+
if (!modal) return false;
|
|
1444
|
+
const normalizedMessage = typeof modal.message === 'string' ? modal.message.trim() : '';
|
|
1445
|
+
if (!normalizedMessage) return false;
|
|
1446
|
+
return this.lastApprovalResolvedAt > 0
|
|
1447
|
+
&& normalizedMessage === this.lastResolvedModalMessage
|
|
1448
|
+
&& this.computeApprovalContentSignature(snap) === this.lastApprovalResolvedContentSignature;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** Clear all resolve-time approval bookkeeping (message, timestamp, content
|
|
1452
|
+
* signature) — called at turn/session boundaries so a next-turn or
|
|
1453
|
+
* next-session approval is never compared against stale prior state. */
|
|
1454
|
+
private clearApprovalResolutionMemory(): void {
|
|
1455
|
+
this.lastApprovalResolvedAt = 0;
|
|
1456
|
+
this.lastResolvedModalMessage = '';
|
|
1457
|
+
this.lastApprovalResolvedContentSignature = '';
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1259
1460
|
/**
|
|
1260
1461
|
* Schedule one more settled evaluation while pinned to `waiting_approval`
|
|
1261
1462
|
* with no actionable modal. The settled FSM normally only re-runs on new
|