@adhdev/daemon-core 1.0.18-rc.20 → 1.0.18-rc.3

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 (74) hide show
  1. package/dist/cli-adapters/provider-cli-shared.d.ts +0 -33
  2. package/dist/commands/cli-manager.d.ts +0 -9
  3. package/dist/commands/upgrade-helper.d.ts +0 -1
  4. package/dist/commands/windows-atomic-upgrade.d.ts +0 -5
  5. package/dist/config/mesh-json-config.d.ts +0 -62
  6. package/dist/index.d.ts +2 -4
  7. package/dist/index.js +3998 -6112
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +3855 -5966
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/mesh/coordinator-prompt.d.ts +0 -76
  12. package/dist/mesh/mesh-active-work.d.ts +1 -1
  13. package/dist/mesh/mesh-ledger.d.ts +1 -28
  14. package/dist/mesh/mesh-node-identity.d.ts +0 -23
  15. package/dist/mesh/mesh-refine-gates.d.ts +0 -89
  16. package/dist/mesh/mesh-remote-event-pull.d.ts +0 -3
  17. package/dist/mesh/mesh-runtime-store.d.ts +0 -11
  18. package/dist/mesh/mesh-work-queue.d.ts +1 -24
  19. package/dist/providers/chat-message-normalization.d.ts +0 -22
  20. package/dist/providers/cli-provider-instance-types.d.ts +0 -4
  21. package/dist/providers/cli-provider-instance.d.ts +0 -78
  22. package/dist/providers/contracts.d.ts +0 -17
  23. package/dist/providers/provider-schema.d.ts +0 -1
  24. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +0 -14
  25. package/dist/providers/types/interactive-prompt.d.ts +0 -18
  26. package/dist/repo-mesh-types.d.ts +6 -38
  27. package/dist/shared-types.d.ts +2 -4
  28. package/package.json +3 -3
  29. package/src/boot/daemon-lifecycle.ts +0 -10
  30. package/src/cli-adapters/cli-state-engine.ts +1 -17
  31. package/src/cli-adapters/provider-cli-adapter.ts +6 -2
  32. package/src/cli-adapters/provider-cli-shared.ts +0 -48
  33. package/src/commands/cli-manager.ts +8 -72
  34. package/src/commands/high-family/mesh-coordinator-launch.ts +2 -17
  35. package/src/commands/high-family/mesh-events.ts +2 -13
  36. package/src/commands/med-family/mesh-crud.ts +0 -155
  37. package/src/commands/med-family/mesh-queue.ts +1 -74
  38. package/src/commands/router-refine.ts +4 -71
  39. package/src/commands/router.ts +0 -8
  40. package/src/commands/upgrade-helper.ts +15 -78
  41. package/src/commands/windows-atomic-upgrade.ts +40 -194
  42. package/src/config/mesh-json-config.ts +0 -111
  43. package/src/index.ts +1 -21
  44. package/src/mesh/coordinator-prompt.ts +11 -258
  45. package/src/mesh/mesh-active-work.ts +1 -11
  46. package/src/mesh/mesh-completion-synthesis.ts +1 -12
  47. package/src/mesh/mesh-event-classify.ts +0 -19
  48. package/src/mesh/mesh-event-forwarding.ts +6 -64
  49. package/src/mesh/mesh-events-utils.ts +0 -42
  50. package/src/mesh/mesh-ledger.ts +0 -77
  51. package/src/mesh/mesh-node-identity.ts +0 -49
  52. package/src/mesh/mesh-queue-assignment.ts +11 -210
  53. package/src/mesh/mesh-reconcile-loop.ts +3 -306
  54. package/src/mesh/mesh-refine-gates.ts +0 -300
  55. package/src/mesh/mesh-remote-event-pull.ts +47 -68
  56. package/src/mesh/mesh-runtime-store.ts +0 -21
  57. package/src/mesh/mesh-work-queue.ts +2 -85
  58. package/src/providers/chat-message-normalization.ts +0 -53
  59. package/src/providers/cli-provider-instance-types.ts +0 -53
  60. package/src/providers/cli-provider-instance.ts +15 -497
  61. package/src/providers/contracts.ts +1 -25
  62. package/src/providers/provider-schema.ts +0 -83
  63. package/src/providers/sdk/v1/builders/cli/detect-status.ts +0 -23
  64. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +0 -20
  65. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +0 -44
  66. package/src/providers/types/interactive-prompt.ts +0 -77
  67. package/src/repo-mesh-types.ts +12 -112
  68. package/src/shared-types.ts +1 -9
  69. package/src/status/reporter.ts +0 -1
  70. package/src/status/snapshot.ts +0 -2
  71. package/dist/mesh/mesh-disk-retention.d.ts +0 -105
  72. package/dist/providers/auto-approve-modes.d.ts +0 -14
  73. package/src/mesh/mesh-disk-retention.ts +0 -370
  74. package/src/providers/auto-approve-modes.ts +0 -103
@@ -1,105 +0,0 @@
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[]>;
@@ -1,14 +0,0 @@
1
- import type { AutoApproveMode, AutoApproveModeRisk, AutoApproveModeStrategy, ProviderModule } from './contracts.js';
2
- export interface ResolvedAutoApproveMode {
3
- active: boolean;
4
- strategy: AutoApproveModeStrategy;
5
- modeId: string;
6
- }
7
- /** Runtime defense-in-depth for provider definitions that bypass schema validation. */
8
- export declare function deriveAutoApproveModeRisk(mode: Pick<AutoApproveMode, 'risk' | 'launchArgs'>): AutoApproveModeRisk;
9
- /**
10
- * Resolve new mode settings before the legacy boolean. A stale/unknown explicit
11
- * mode id fails closed instead of falling through to an enabled legacy setting.
12
- */
13
- export declare function resolveProviderAutoApproveMode(provider: ProviderModule | null | undefined, settings: Record<string, unknown> | undefined): ResolvedAutoApproveMode;
14
- export declare function findProviderAutoApproveMode(provider: ProviderModule | undefined, modeId: string): AutoApproveMode | undefined;
@@ -1,370 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // mesh-disk-retention — periodic disk / worktree retention for ~/.adhdev
3
- // ---------------------------------------------------------------------------
4
- // Legacy on-disk artifacts under ~/.adhdev accumulated with NO lifetime/GC and
5
- // grew the data volume until a refine bootstrap failed at 98% disk (mission
6
- // 86def38d). Immediate reclaim was manual; this module implements the code-level
7
- // RETENTION so it does not recur.
8
- //
9
- // What it prunes (all thresholds defensive — never touches a live/in-use file):
10
- // 1. JSONL ledger files ~/.adhdev/mesh-ledger/*.jsonl older than 30 days
11
- // (legacy after the SQLite ledger; zero lifetime before this).
12
- // 2. session-host runtimes ~/.adhdev/session-host/*/runtimes/*.json for
13
- // TERMINATED (dead) runtimes older than 14 days (live runtimes never touched).
14
- // 3. DB backups ~/.adhdev/mesh-ledger/mesh-runtime.db.bak-* older
15
- // than 7 days.
16
- // Plus DETECTION-ONLY orphan-worktree signalling (see mesh-reconcile-loop.ts):
17
- // 4. a worktree present on disk with no matching live mesh node is reported as a
18
- // cleanup_candidate ledger entry — NEVER auto-deleted (that stays manual /
19
- // coordinator-driven).
20
- //
21
- // The functions are split into PURE core selectors (age/orphan decisions, taking
22
- // explicit paths + a `now` timestamp so they are deterministic and require no fs
23
- // mocking in tests) and thin runtime wrappers that resolve the real ~/.adhdev
24
- // paths and perform the unlink. The pure selectors are the unit-tested surface.
25
- // ---------------------------------------------------------------------------
26
-
27
- import {
28
- existsSync,
29
- readdirSync,
30
- statSync,
31
- unlinkSync,
32
- readFileSync,
33
- } from 'fs';
34
- import { join } from 'path';
35
- import { getConfigDir } from '../config/config.js';
36
- import { getLedgerDir, appendLedgerEntry, readLedgerEntries } from './mesh-ledger.js';
37
- import { isSessionHostLiveRuntime } from '../session-host/runtime-surface.js';
38
- import type { SessionHostSurfaceRecordLike } from '../session-host/runtime-surface.js';
39
- import { listWorktrees } from '../git/git-worktree.js';
40
- import type { LocalMeshEntry } from '../repo-mesh-types.js';
41
- import { LOG } from '../logging/logger.js';
42
-
43
- // ─── Thresholds (all defensive) ────────────────────────────────────────────
44
- export const DAY_MS = 24 * 60 * 60 * 1000;
45
- /** JSONL ledger files are legacy after the SQLite ledger — 30-day lifetime. */
46
- export const LEDGER_JSONL_MAX_AGE_MS = 30 * DAY_MS;
47
- /** Terminated session-host runtimes: conservative 14-day retention. */
48
- export const SESSION_HOST_RUNTIME_MAX_AGE_MS = 14 * DAY_MS;
49
- /** mesh-runtime.db.bak-* backups: 7-day retention. */
50
- export const DB_BAK_MAX_AGE_MS = 7 * DAY_MS;
51
-
52
- // ─── (1) JSONL ledger retention ─────────────────────────────────────────────
53
-
54
- /** A file candidate for age-based pruning: absolute path + last-modified epoch ms. */
55
- export interface AgedFile {
56
- path: string;
57
- mtimeMs: number;
58
- }
59
-
60
- /**
61
- * PURE. Select the JSONL ledger files whose mtime is older than `maxAgeMs`
62
- * relative to `now`. A file exactly at the threshold is KEPT (strict `>`), so a
63
- * 30-day-old file survives its 30th day and is pruned on the 31st. Deterministic:
64
- * no fs access, no clock read.
65
- */
66
- export function selectExpiredLedgerJsonl(
67
- files: AgedFile[],
68
- now: number,
69
- maxAgeMs: number = LEDGER_JSONL_MAX_AGE_MS,
70
- ): AgedFile[] {
71
- return files.filter(f => now - f.mtimeMs > maxAgeMs);
72
- }
73
-
74
- // ─── (2) session-host runtime retention ─────────────────────────────────────
75
-
76
- /** A parsed session-host runtime file: its path, mtime, and the wrapped record. */
77
- export interface SessionHostRuntimeFile {
78
- path: string;
79
- mtimeMs: number;
80
- /** The `record` object from the on-disk `{ record, snapshot, updatedAt }` file. */
81
- record: SessionHostSurfaceRecordLike | null;
82
- }
83
-
84
- /**
85
- * PURE. Select the session-host runtime files safe to delete: ONLY those whose
86
- * runtime is terminated/dead (NOT a live runtime — decided by the session-host-core
87
- * SSOT `isSessionHostLiveRuntime`) AND older than `maxAgeMs`. A live runtime, or a
88
- * dead-but-recent one, is always kept. A file whose record failed to parse is treated
89
- * as NON-live but is still age-gated, so a corrupt-but-fresh file is never removed.
90
- */
91
- export function selectExpiredSessionHostRuntimes(
92
- files: SessionHostRuntimeFile[],
93
- now: number,
94
- maxAgeMs: number = SESSION_HOST_RUNTIME_MAX_AGE_MS,
95
- ): SessionHostRuntimeFile[] {
96
- return files.filter(f => {
97
- // Never delete a live runtime, regardless of age.
98
- if (isSessionHostLiveRuntime(f.record ?? undefined)) return false;
99
- // Terminated/dead: age-gate it.
100
- return now - f.mtimeMs > maxAgeMs;
101
- });
102
- }
103
-
104
- // ─── (3) DB backup retention ────────────────────────────────────────────────
105
-
106
- /** True for a `mesh-runtime.db.bak-*` backup filename (basename only). */
107
- export function isDbBackupFileName(name: string): boolean {
108
- return /^mesh-runtime\.db\.bak-/.test(name);
109
- }
110
-
111
- /** PURE. Select `.bak-*` backups older than `maxAgeMs`. Strict `>` (see ledger). */
112
- export function selectExpiredDbBackups(
113
- files: AgedFile[],
114
- now: number,
115
- maxAgeMs: number = DB_BAK_MAX_AGE_MS,
116
- ): AgedFile[] {
117
- return files.filter(f => now - f.mtimeMs > maxAgeMs);
118
- }
119
-
120
- // ─── (4) orphan worktree detection (detection-only) ─────────────────────────
121
-
122
- /** Minimal shape needed to decide whether a worktree path is orphaned. */
123
- export interface WorktreePathLike {
124
- path: string;
125
- bare?: boolean;
126
- }
127
-
128
- /** A live mesh node's known workspace paths (self workspace + repoRoot, normalized). */
129
- export interface LiveNodeWorkspaceLike {
130
- workspace?: string;
131
- repoRoot?: string;
132
- }
133
-
134
- function normalizePath(p: string): string {
135
- // Trim a single trailing separator so "/a/b/" and "/a/b" compare equal.
136
- // Case-preserving (git worktree list + meshes.json are both raw paths from the
137
- // same daemon, so a case-fold would be over-eager on case-sensitive FS).
138
- return p.replace(/[/\\]+$/, '');
139
- }
140
-
141
- /**
142
- * PURE. Given the worktrees git reports on disk and the set of live-node workspace
143
- * paths, return the worktrees that have NO matching live node — the orphan
144
- * cleanup_candidates.
145
- *
146
- * SAFETY:
147
- * - `mainWorktreePath` (the primary repo checkout, i.e. worktree[0]) is NEVER an
148
- * orphan — it is the base repo, not a mesh clone.
149
- * - `bare` worktrees are skipped (git's internal bookkeeping, not a node checkout).
150
- * - Matching is path-equality after trailing-separator normalization against the
151
- * union of every live node's `workspace` and `repoRoot`.
152
- * This is DETECTION ONLY — the caller signals a cleanup_candidate; it must not delete.
153
- */
154
- export function detectOrphanWorktrees(
155
- worktrees: WorktreePathLike[],
156
- liveNodes: LiveNodeWorkspaceLike[],
157
- mainWorktreePath: string,
158
- ): WorktreePathLike[] {
159
- const liveePaths = new Set<string>();
160
- for (const n of liveNodes) {
161
- if (n.workspace) liveePaths.add(normalizePath(n.workspace));
162
- if (n.repoRoot) liveePaths.add(normalizePath(n.repoRoot));
163
- }
164
- const mainNorm = normalizePath(mainWorktreePath);
165
- return worktrees.filter(wt => {
166
- if (wt.bare) return false;
167
- const norm = normalizePath(wt.path);
168
- if (norm === mainNorm) return false; // base repo checkout — never an orphan
169
- return !liveePaths.has(norm);
170
- });
171
- }
172
-
173
- // ─── Runtime wrappers (resolve real paths + perform the unlink) ──────────────
174
- // These are the side-effecting callers used by the reconcile loop. They are thin:
175
- // gather → delegate to a PURE selector → unlink. Kept out of the unit tests (which
176
- // target the deterministic selectors); their I/O is exercised by the daemon at runtime.
177
-
178
- function safeUnlink(path: string): boolean {
179
- try {
180
- unlinkSync(path);
181
- return true;
182
- } catch (e: any) {
183
- LOG.warn('DiskRetention', `Failed to delete ${path}: ${e?.message || e}`);
184
- return false;
185
- }
186
- }
187
-
188
- function listDirFiles(dir: string): AgedFile[] {
189
- if (!existsSync(dir)) return [];
190
- const out: AgedFile[] = [];
191
- let names: string[];
192
- try {
193
- names = readdirSync(dir);
194
- } catch {
195
- return [];
196
- }
197
- for (const name of names) {
198
- const path = join(dir, name);
199
- try {
200
- const st = statSync(path);
201
- if (st.isFile()) out.push({ path, mtimeMs: st.mtimeMs });
202
- } catch {
203
- // vanished between readdir and stat — skip
204
- }
205
- }
206
- return out;
207
- }
208
-
209
- /**
210
- * Prune expired legacy JSONL ledger files under ~/.adhdev/mesh-ledger/.
211
- * Matches only `*.jsonl` (never the SQLite .db / -wal / -shm files). Returns the
212
- * count deleted. Best-effort: individual unlink failures are logged, not thrown.
213
- */
214
- export function pruneExpiredLedgerJsonl(now: number = Date.now()): number {
215
- const dir = getLedgerDir();
216
- const jsonl = listDirFiles(dir).filter(f => f.path.endsWith('.jsonl'));
217
- const expired = selectExpiredLedgerJsonl(jsonl, now);
218
- let deleted = 0;
219
- for (const f of expired) if (safeUnlink(f.path)) deleted++;
220
- if (deleted > 0) LOG.info('DiskRetention', `Pruned ${deleted} JSONL ledger file(s) older than 30d`);
221
- return deleted;
222
- }
223
-
224
- /**
225
- * Prune expired `mesh-runtime.db.bak-*` backups under ~/.adhdev/mesh-ledger/.
226
- * Never touches the live DB (only names matching the .bak- prefix). Returns count.
227
- */
228
- export function pruneExpiredDbBackups(now: number = Date.now()): number {
229
- const dir = getLedgerDir();
230
- const baks = listDirFiles(dir).filter(f => isDbBackupFileName(f.path.split(/[/\\]/).pop() || ''));
231
- const expired = selectExpiredDbBackups(baks, now);
232
- let deleted = 0;
233
- for (const f of expired) if (safeUnlink(f.path)) deleted++;
234
- if (deleted > 0) LOG.info('DiskRetention', `Pruned ${deleted} mesh-runtime.db.bak-* backup(s) older than 7d`);
235
- return deleted;
236
- }
237
-
238
- /**
239
- * Prune terminated session-host runtime files older than 14 days across every
240
- * ~/.adhdev/session-host/<app>/runtimes/ directory. A LIVE runtime is never deleted
241
- * regardless of age (isSessionHostLiveRuntime SSOT). Returns count deleted.
242
- */
243
- export function pruneExpiredSessionHostRuntimes(now: number = Date.now()): number {
244
- const root = join(getConfigDir(), 'session-host');
245
- if (!existsSync(root)) return 0;
246
- let apps: string[];
247
- try {
248
- apps = readdirSync(root);
249
- } catch {
250
- return 0;
251
- }
252
- const candidates: SessionHostRuntimeFile[] = [];
253
- for (const app of apps) {
254
- const runtimesDir = join(root, app, 'runtimes');
255
- if (!existsSync(runtimesDir)) continue;
256
- for (const f of listDirFiles(runtimesDir)) {
257
- if (!f.path.endsWith('.json')) continue;
258
- let record: SessionHostRuntimeFile['record'] = null;
259
- try {
260
- const parsed = JSON.parse(readFileSync(f.path, 'utf-8'));
261
- const rec = parsed && typeof parsed === 'object' ? parsed.record : null;
262
- record = rec && typeof rec === 'object' ? (rec as SessionHostSurfaceRecordLike) : null;
263
- } catch {
264
- // Unparseable → treat as non-live; still age-gated by the selector,
265
- // so a corrupt-but-fresh file is preserved.
266
- record = null;
267
- }
268
- candidates.push({ path: f.path, mtimeMs: f.mtimeMs, record });
269
- }
270
- }
271
- const expired = selectExpiredSessionHostRuntimes(candidates, now);
272
- let deleted = 0;
273
- for (const f of expired) if (safeUnlink(f.path)) deleted++;
274
- if (deleted > 0) LOG.info('DiskRetention', `Pruned ${deleted} terminated session-host runtime file(s) older than 14d`);
275
- return deleted;
276
- }
277
-
278
- /**
279
- * Run the file-deleting retention passes (JSONL ledger, DB backups, session-host
280
- * runtimes) once. Each pass is isolated so one failing pass never blocks the others.
281
- * Orphan-worktree DETECTION is driven separately in the reconcile loop (it needs the
282
- * live mesh config + git worktree list and emits a ledger signal rather than deleting).
283
- */
284
- export function runDiskRetentionSweep(now: number = Date.now()): { ledgerJsonl: number; dbBackups: number; sessionHostRuntimes: number } {
285
- let ledgerJsonl = 0;
286
- let dbBackups = 0;
287
- let sessionHostRuntimes = 0;
288
- try { ledgerJsonl = pruneExpiredLedgerJsonl(now); } catch (e: any) { LOG.warn('DiskRetention', `Ledger JSONL prune failed: ${e?.message || e}`); }
289
- try { dbBackups = pruneExpiredDbBackups(now); } catch (e: any) { LOG.warn('DiskRetention', `DB backup prune failed: ${e?.message || e}`); }
290
- try { sessionHostRuntimes = pruneExpiredSessionHostRuntimes(now); } catch (e: any) { LOG.warn('DiskRetention', `Session-host runtime prune failed: ${e?.message || e}`); }
291
- return { ledgerJsonl, dbBackups, sessionHostRuntimes };
292
- }
293
-
294
- // ─── Orphan worktree detection (detection-only, emits cleanup_candidate) ──────
295
-
296
- /** How many recent worktree_cleanup_candidate entries to scan for the re-emit guard. */
297
- const ORPHAN_DEDUPE_WINDOW = 200;
298
-
299
- /**
300
- * Detect orphaned worktrees for ONE mesh and emit a `worktree_cleanup_candidate`
301
- * ledger signal for each — DETECTION ONLY, never deletes. An orphan is a git worktree
302
- * on disk with no matching live mesh node (compared by path). It:
303
- * 1. picks a base (non-worktree) node owned by this mesh to anchor `git worktree list`;
304
- * 2. diffs the reported worktrees against the union of every node's workspace/repoRoot;
305
- * 3. skips the main worktree + bare entries (detectOrphanWorktrees safety);
306
- * 4. suppresses a repeat for a worktreePath already signalled within the recent window
307
- * (idempotent re-emit guard), so the hourly sweep doesn't spam the ledger.
308
- * Returns the list of newly-signalled orphan paths. Best-effort: git/ledger failures are
309
- * logged and yield an empty result, never thrown.
310
- */
311
- export async function detectAndSignalOrphanWorktrees(
312
- mesh: LocalMeshEntry,
313
- now: number = Date.now(),
314
- ): Promise<string[]> {
315
- const nodes = Array.isArray(mesh.nodes) ? mesh.nodes : [];
316
- // Anchor: a base (non-worktree) node's repoRoot (preferred) or workspace. The base
317
- // node is the primary checkout; its git dir enumerates every worktree of the repo.
318
- const baseNode = nodes.find(n => !n.isLocalWorktree && (n.repoRoot || n.workspace));
319
- const repoRoot = baseNode?.repoRoot || baseNode?.workspace;
320
- if (!repoRoot) return []; // no local base checkout for this mesh on this daemon
321
-
322
- let worktrees: WorktreePathLike[];
323
- try {
324
- worktrees = await listWorktrees(repoRoot);
325
- } catch (e: any) {
326
- LOG.warn('DiskRetention', `git worktree list failed for mesh ${mesh.id} (${repoRoot}): ${e?.message || e}`);
327
- return [];
328
- }
329
- // git worktree list emits the main worktree first; treat it as the base repo.
330
- const mainWorktreePath = worktrees[0]?.path || repoRoot;
331
- const liveNodes: LiveNodeWorkspaceLike[] = nodes.map(n => ({ workspace: n.workspace, repoRoot: n.repoRoot }));
332
- const orphans = detectOrphanWorktrees(worktrees, liveNodes, mainWorktreePath);
333
- if (orphans.length === 0) return [];
334
-
335
- // Re-emit guard: skip a worktreePath already signalled in the recent window so the
336
- // hourly sweep is idempotent and does not flood the ledger with duplicates.
337
- let recentPaths = new Set<string>();
338
- try {
339
- const recent = readLedgerEntries(mesh.id, { kind: ['worktree_cleanup_candidate'], tail: ORPHAN_DEDUPE_WINDOW });
340
- for (const e of recent) {
341
- const p = typeof e.payload?.worktreePath === 'string' ? e.payload.worktreePath : '';
342
- if (p) recentPaths.add(p);
343
- }
344
- } catch {
345
- recentPaths = new Set();
346
- }
347
-
348
- const signalled: string[] = [];
349
- for (const wt of orphans) {
350
- if (recentPaths.has(wt.path)) continue;
351
- try {
352
- appendLedgerEntry(mesh.id, {
353
- kind: 'worktree_cleanup_candidate',
354
- payload: {
355
- worktreePath: wt.path,
356
- reason: 'no_matching_live_node',
357
- state: 'cleanup_candidate',
358
- detectedAt: new Date(now).toISOString(),
359
- },
360
- });
361
- signalled.push(wt.path);
362
- } catch (e: any) {
363
- LOG.warn('DiskRetention', `Failed to record orphan worktree signal for ${wt.path}: ${e?.message || e}`);
364
- }
365
- }
366
- if (signalled.length > 0) {
367
- LOG.info('DiskRetention', `Detected ${signalled.length} orphan worktree(s) for mesh ${mesh.id} (cleanup_candidate — NOT deleted): ${signalled.join(', ')}`);
368
- }
369
- return signalled;
370
- }
@@ -1,103 +0,0 @@
1
- import type {
2
- AutoApproveMode,
3
- AutoApproveModeRisk,
4
- AutoApproveModeStrategy,
5
- ProviderModule,
6
- } from './contracts.js';
7
-
8
- const KNOWN_DANGEROUS_LAUNCH_ARGS = new Set([
9
- '--dangerously-skip-permissions',
10
- '--dangerously-bypass-approvals-and-sandbox',
11
- 'bypassPermissions',
12
- 'sandbox_mode=danger-full-access',
13
- 'approval_policy=never',
14
- ]);
15
-
16
- export interface ResolvedAutoApproveMode {
17
- active: boolean;
18
- strategy: AutoApproveModeStrategy;
19
- modeId: string;
20
- }
21
-
22
- function isKnownDangerousLaunchArg(arg: string): boolean {
23
- const normalized = arg.trim();
24
- if (KNOWN_DANGEROUS_LAUNCH_ARGS.has(normalized)) return true;
25
- return normalized.startsWith('--dangerously-skip-permissions=')
26
- || normalized.startsWith('--dangerously-bypass-approvals-and-sandbox=');
27
- }
28
-
29
- /** Runtime defense-in-depth for provider definitions that bypass schema validation. */
30
- export function deriveAutoApproveModeRisk(mode: Pick<AutoApproveMode, 'risk' | 'launchArgs'>): AutoApproveModeRisk {
31
- return Array.isArray(mode.launchArgs) && mode.launchArgs.some(isKnownDangerousLaunchArg)
32
- ? 'dangerous'
33
- : mode.risk;
34
- }
35
-
36
- function inactiveMode(modeId = ''): ResolvedAutoApproveMode {
37
- return { active: false, strategy: 'pty-parse-default', modeId };
38
- }
39
-
40
- function resolveConfiguredMode(
41
- provider: ProviderModule,
42
- mode: AutoApproveMode,
43
- settings: Record<string, unknown> | undefined,
44
- ): ResolvedAutoApproveMode {
45
- if (mode.strategy === 'post-boot-command') return inactiveMode(mode.id);
46
- if (settings?.launchedByCoordinator === true
47
- && settings.delegatedWorkerDangerousModeAllow !== true
48
- && deriveAutoApproveModeRisk(mode) === 'dangerous') {
49
- const fallback = provider.autoApproveModes?.modes.find((candidate) =>
50
- candidate.strategy === 'pty-parse-default'
51
- && deriveAutoApproveModeRisk(candidate) !== 'dangerous');
52
- return fallback
53
- ? { active: true, strategy: fallback.strategy, modeId: fallback.id }
54
- : inactiveMode(mode.id);
55
- }
56
- return { active: true, strategy: mode.strategy, modeId: mode.id };
57
- }
58
-
59
- /**
60
- * Resolve new mode settings before the legacy boolean. A stale/unknown explicit
61
- * mode id fails closed instead of falling through to an enabled legacy setting.
62
- */
63
- export function resolveProviderAutoApproveMode(
64
- provider: ProviderModule | null | undefined,
65
- settings: Record<string, unknown> | undefined,
66
- ): ResolvedAutoApproveMode {
67
- // No provider (e.g. an instance whose module isn't set when a modal-park /
68
- // auto-approve classification runs) ⇒ auto-approve is inactive. this.provider is
69
- // genuinely nullable at runtime (the instance reads this.provider?.nativeHistory
70
- // elsewhere), and the sibling helper findProviderAutoApproveMode already guards
71
- // with provider?.autoApproveModes. Guard the entry so every caller is safe.
72
- if (!provider) return inactiveMode();
73
- const config = provider.autoApproveModes;
74
- const explicitModeId = settings?.autoApproveMode;
75
- if (typeof explicitModeId === 'string') {
76
- const mode = config?.modes.find((candidate) => candidate.id === explicitModeId);
77
- if (!mode) return inactiveMode(explicitModeId);
78
- return resolveConfiguredMode(provider, mode, settings);
79
- }
80
-
81
- const explicitLegacy = settings?.autoApprove;
82
- const providerLegacyDefault = provider.settings?.autoApprove?.default;
83
- const legacyActive = typeof explicitLegacy === 'boolean'
84
- ? explicitLegacy
85
- : typeof providerLegacyDefault === 'boolean'
86
- ? providerLegacyDefault
87
- : false;
88
- if (!legacyActive) return inactiveMode();
89
-
90
- if (!config) {
91
- return { active: true, strategy: 'pty-parse-default', modeId: 'legacy' };
92
- }
93
- const defaultMode = config.modes.find((mode) => mode.id === config.default);
94
- if (!defaultMode) return inactiveMode(config.default);
95
- return resolveConfiguredMode(provider, defaultMode, settings);
96
- }
97
-
98
- export function findProviderAutoApproveMode(
99
- provider: ProviderModule | undefined,
100
- modeId: string,
101
- ): AutoApproveMode | undefined {
102
- return provider?.autoApproveModes?.modes.find((mode) => mode.id === modeId);
103
- }