@adhdev/daemon-core 1.0.18-rc.5 → 1.0.18-rc.7
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/index.js +3747 -3394
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3746 -3387
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +32 -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 +1 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +5 -0
- package/src/commands/windows-atomic-upgrade.ts +19 -1
- package/src/config/mesh-json-config.ts +4 -0
- package/src/mesh/coordinator-prompt.ts +79 -8
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-ledger.ts +72 -0
- 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 +65 -0
|
@@ -68,6 +68,20 @@ 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;
|
|
71
85
|
}
|
|
72
86
|
export interface CoordinatorPromptContext {
|
|
73
87
|
mesh: LocalMeshEntry;
|
|
@@ -102,6 +116,24 @@ export interface CoordinatorPromptContext {
|
|
|
102
116
|
magiKindPanels?: MagiKindPanelMap;
|
|
103
117
|
}
|
|
104
118
|
export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
|
|
119
|
+
/**
|
|
120
|
+
* Operating-notes lifecycle selection (read-side, minimal first cut). Given the
|
|
121
|
+
* effective notes (oldest-first, ledger order) and the current time, produce the
|
|
122
|
+
* ordered, capped list that rides into the prompt:
|
|
123
|
+
* (i) ALWAYS include pinned notes.
|
|
124
|
+
* (ii) drop expired UNPINNED notes (per category TTL / explicit expiresAt).
|
|
125
|
+
* (iii) rank pinned-first, then durable-category, then recency (newest first).
|
|
126
|
+
* (iv) apply the existing OPERATING_NOTES_PROMPT_CAP to the ranked list.
|
|
127
|
+
* Pure + deterministic given `now`. Returns { shown, omittedCount }.
|
|
128
|
+
*
|
|
129
|
+
* Ranking is stable on the original ledger index so, within a tier, the original
|
|
130
|
+
* oldest-first order is preserved before the recency comparison flips it — i.e.
|
|
131
|
+
* newest notes lead each tier. The cap keeps the leading (highest-priority) N.
|
|
132
|
+
*/
|
|
133
|
+
export declare function selectOperatingNotesForPrompt(notes: CoordinatorOperatingNote[], now: number, cap?: number): {
|
|
134
|
+
shown: CoordinatorOperatingNote[];
|
|
135
|
+
omittedCount: number;
|
|
136
|
+
};
|
|
105
137
|
/**
|
|
106
138
|
* Render the machine-local MAGI kind-panel bindings so the coordinator KNOWS
|
|
107
139
|
* which cross-verification panels (rca / design / claim_audit / freeform) are
|
|
@@ -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;
|
|
@@ -473,6 +473,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
473
473
|
private completionFinalSummary;
|
|
474
474
|
private buildCompletedFinalizationDiagnostic;
|
|
475
475
|
private hasAdapterPendingResponse;
|
|
476
|
+
private antigravityHoldPtyStillActive;
|
|
476
477
|
private shouldSuppressStaleParsedBusyStatus;
|
|
477
478
|
private getCompletedFinalizationBlock;
|
|
478
479
|
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.7",
|
|
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.7",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.18-rc.7",
|
|
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
|
}
|
|
@@ -131,6 +131,11 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
131
131
|
category,
|
|
132
132
|
createdAt: typeof p.createdAt === 'string' ? p.createdAt : e.timestamp,
|
|
133
133
|
sourceCoordinator: typeof p.sourceCoordinator === 'string' ? p.sourceCoordinator : undefined,
|
|
134
|
+
// Operating-notes lifecycle: thread pinned/expiresAt so the
|
|
135
|
+
// injection-side selection can honor them. Legacy notes lack
|
|
136
|
+
// these → pinned defaults false, expiry governed by category TTL.
|
|
137
|
+
pinned: p.pinned === true,
|
|
138
|
+
...(typeof p.expiresAt === 'string' ? { expiresAt: p.expiresAt } : {}),
|
|
134
139
|
};
|
|
135
140
|
})
|
|
136
141
|
.filter((n): n is NonNullable<typeof n> => n !== null);
|
|
@@ -162,13 +162,31 @@ function quotePowerShellLiteral(value: string): string {
|
|
|
162
162
|
function pinStagedShims(prefix: string, portableNode: string, cliEntry: string): void {
|
|
163
163
|
const cmdPath = path.join(prefix, 'adhdev.cmd');
|
|
164
164
|
const ps1Path = path.join(prefix, 'adhdev.ps1');
|
|
165
|
+
// The no-extension `adhdev` shim is what `where.exe adhdev` can resolve first
|
|
166
|
+
// and what git-bash / MSYS / WSL and `spawnSync('adhdev')` invoke. npm generates
|
|
167
|
+
// it as a POSIX `sh` shim whose ELSE branch falls back to the FIRST `node` on
|
|
168
|
+
// PATH (`else exec node ...`). On a box where system PATH node is v24, that
|
|
169
|
+
// fallback trips adhdev's own Node-24 guard and `adhdev doctor` reports the
|
|
170
|
+
// runtime surface broken. Rewrite it too so it hard-codes the portable Node 22
|
|
171
|
+
// absolute path with NO system-node fallback — mirroring the .cmd/.ps1 pins.
|
|
172
|
+
const noExtPath = path.join(prefix, 'adhdev');
|
|
165
173
|
const cmd = `@echo off\r\n"${portableNode}" "${cliEntry}" %*\r\n`;
|
|
166
174
|
const ps1 = `#!/usr/bin/env pwsh\r\n& ${quotePowerShellLiteral(portableNode)} ${quotePowerShellLiteral(cliEntry)} @args\r\nexit $LASTEXITCODE\r\n`;
|
|
175
|
+
// sh shim: single unconditional exec of the pinned node — no `if -x node` /
|
|
176
|
+
// `else exec node` branch, so PATH ordering can never shadow the runtime.
|
|
177
|
+
const noExt = `#!/bin/sh\nexec "${portableNode}" "${cliEntry}" "$@"\n`;
|
|
167
178
|
fs.writeFileSync(cmdPath, cmd, 'ascii');
|
|
168
179
|
fs.writeFileSync(ps1Path, ps1, 'utf8');
|
|
180
|
+
fs.writeFileSync(noExtPath, noExt, 'ascii');
|
|
169
181
|
const cmdReadback = fs.readFileSync(cmdPath, 'utf8');
|
|
170
182
|
const ps1Readback = fs.readFileSync(ps1Path, 'utf8');
|
|
171
|
-
|
|
183
|
+
const noExtReadback = fs.readFileSync(noExtPath, 'utf8');
|
|
184
|
+
if (
|
|
185
|
+
!cmdReadback.includes(portableNode)
|
|
186
|
+
|| !ps1Readback.includes(portableNode)
|
|
187
|
+
|| !noExtReadback.includes(portableNode)
|
|
188
|
+
|| /(^|\s)exec\s+node(\s|$)/m.test(noExtReadback)
|
|
189
|
+
) {
|
|
172
190
|
throw new Error('portable Node 22 pin validation failed');
|
|
173
191
|
}
|
|
174
192
|
}
|
|
@@ -160,6 +160,10 @@ function normalizeOperatingNote(value: unknown): CoordinatorOperatingNote | null
|
|
|
160
160
|
...(category ? { category } : {}),
|
|
161
161
|
...(typeof value.createdAt === 'string' ? { createdAt: value.createdAt } : {}),
|
|
162
162
|
...(typeof value.sourceCoordinator === 'string' ? { sourceCoordinator: value.sourceCoordinator } : {}),
|
|
163
|
+
// Operating-notes lifecycle: a repo-declared note may pin itself or set an
|
|
164
|
+
// explicit expiry, same as a runtime-recorded one.
|
|
165
|
+
...(value.pinned === true ? { pinned: true } : {}),
|
|
166
|
+
...(typeof value.expiresAt === 'string' ? { expiresAt: value.expiresAt } : {}),
|
|
163
167
|
};
|
|
164
168
|
}
|
|
165
169
|
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
import { mergeAndNormalizePolicy, resolveProviderMaxParallel } from '../repo-mesh-types.js';
|
|
35
35
|
import { getDifficultyBrains } from '../config/mesh-config.js';
|
|
36
36
|
import { resolveNodeCapabilitySlots } from './mesh-node-slots.js';
|
|
37
|
+
import { isNoteExpired, OPERATING_NOTE_CATEGORY_TTL_DAYS } from './mesh-ledger.js';
|
|
37
38
|
import { MESH_TASK_DIFFICULTIES } from '@adhdev/mesh-shared';
|
|
38
39
|
import type { MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
|
|
39
40
|
|
|
@@ -84,6 +85,20 @@ export interface CoordinatorOperatingNote {
|
|
|
84
85
|
category?: 'provider_quirk' | 'pattern_to_avoid' | 'recovery_lesson';
|
|
85
86
|
createdAt?: string;
|
|
86
87
|
sourceCoordinator?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Operating-notes lifecycle (minimal first cut). When true the note ALWAYS
|
|
90
|
+
* rides into the coordinator prompt — it is never dropped by TTL expiry and
|
|
91
|
+
* survives the injection cap ahead of unpinned notes. Legacy notes without
|
|
92
|
+
* this field default to false.
|
|
93
|
+
*/
|
|
94
|
+
pinned?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Optional explicit expiry ISO timestamp. When set and in the past, an
|
|
97
|
+
* UNPINNED note is dropped from the injected prompt (read-side only — the
|
|
98
|
+
* ledger entry is never pruned by age). When absent, the category→TTL map
|
|
99
|
+
* (see isNoteExpired) governs expiry; pinned notes never expire regardless.
|
|
100
|
+
*/
|
|
101
|
+
expiresAt?: string;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
// ─── Prompt Builder ─────────────────────────────
|
|
@@ -582,11 +597,65 @@ function buildRecentActivitySection(activity?: CoordinatorRecentActivity): strin
|
|
|
582
597
|
const OPERATING_NOTES_PROMPT_CAP = 20;
|
|
583
598
|
const OPERATING_NOTE_MAX_CHARS = 300;
|
|
584
599
|
|
|
585
|
-
|
|
600
|
+
/**
|
|
601
|
+
* A category is "durable" (survives the cap ahead of recency) when it has no
|
|
602
|
+
* TTL entry — provider_quirk, uncategorized, or any unknown category. This
|
|
603
|
+
* mirrors isNoteExpired's durability rule so ranking and expiry agree.
|
|
604
|
+
*/
|
|
605
|
+
function isDurableCategory(category?: string): boolean {
|
|
606
|
+
if (!category) return true;
|
|
607
|
+
return !(category in OPERATING_NOTE_CATEGORY_TTL_DAYS);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Operating-notes lifecycle selection (read-side, minimal first cut). Given the
|
|
612
|
+
* effective notes (oldest-first, ledger order) and the current time, produce the
|
|
613
|
+
* ordered, capped list that rides into the prompt:
|
|
614
|
+
* (i) ALWAYS include pinned notes.
|
|
615
|
+
* (ii) drop expired UNPINNED notes (per category TTL / explicit expiresAt).
|
|
616
|
+
* (iii) rank pinned-first, then durable-category, then recency (newest first).
|
|
617
|
+
* (iv) apply the existing OPERATING_NOTES_PROMPT_CAP to the ranked list.
|
|
618
|
+
* Pure + deterministic given `now`. Returns { shown, omittedCount }.
|
|
619
|
+
*
|
|
620
|
+
* Ranking is stable on the original ledger index so, within a tier, the original
|
|
621
|
+
* oldest-first order is preserved before the recency comparison flips it — i.e.
|
|
622
|
+
* newest notes lead each tier. The cap keeps the leading (highest-priority) N.
|
|
623
|
+
*/
|
|
624
|
+
export function selectOperatingNotesForPrompt(
|
|
625
|
+
notes: CoordinatorOperatingNote[],
|
|
626
|
+
now: number,
|
|
627
|
+
cap: number = OPERATING_NOTES_PROMPT_CAP,
|
|
628
|
+
): { shown: CoordinatorOperatingNote[]; omittedCount: number } {
|
|
586
629
|
const valid = Array.isArray(notes)
|
|
587
630
|
? notes.filter(n => n && typeof n.text === 'string' && n.text.trim())
|
|
588
631
|
: [];
|
|
589
|
-
|
|
632
|
+
|
|
633
|
+
// (i)+(ii): keep pinned always; drop expired unpinned. Retain original index
|
|
634
|
+
// for a stable recency tiebreak (later index == newer, ledger is oldest-first).
|
|
635
|
+
const kept = valid
|
|
636
|
+
.map((note, index) => ({ note, index }))
|
|
637
|
+
.filter(({ note }) => note.pinned || !isNoteExpired(note as any, now));
|
|
638
|
+
|
|
639
|
+
// (iii): rank pinned > durable > recency (newest first within a tier).
|
|
640
|
+
const rank = (n: CoordinatorOperatingNote): number =>
|
|
641
|
+
n.pinned ? 0 : isDurableCategory(n.category) ? 1 : 2;
|
|
642
|
+
kept.sort((a, b) => {
|
|
643
|
+
const r = rank(a.note) - rank(b.note);
|
|
644
|
+
if (r !== 0) return r;
|
|
645
|
+
return b.index - a.index; // newer (higher index) first
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// (iv): apply the existing cap to the ranked list.
|
|
649
|
+
const omittedCount = Math.max(0, kept.length - cap);
|
|
650
|
+
const shown = (omittedCount > 0 ? kept.slice(0, cap) : kept).map(k => k.note);
|
|
651
|
+
return { shown, omittedCount };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildOperatingNotesSection(notes?: CoordinatorOperatingNote[], now: number = Date.now()): string {
|
|
655
|
+
const hasAny = Array.isArray(notes)
|
|
656
|
+
? notes.some(n => n && typeof n.text === 'string' && n.text.trim())
|
|
657
|
+
: false;
|
|
658
|
+
if (!hasAny) return '';
|
|
590
659
|
|
|
591
660
|
const categoryLabel: Record<string, string> = {
|
|
592
661
|
provider_quirk: 'provider quirk',
|
|
@@ -594,21 +663,23 @@ function buildOperatingNotesSection(notes?: CoordinatorOperatingNote[]): string
|
|
|
594
663
|
recovery_lesson: 'recovery lesson',
|
|
595
664
|
};
|
|
596
665
|
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
const shown
|
|
666
|
+
// Lifecycle selection: pinned-always + expired-unpinned-dropped + rank
|
|
667
|
+
// (pinned > durable > recency) THEN the existing cap. `shown` is already
|
|
668
|
+
// highest-priority-first; the cap kept the leading N.
|
|
669
|
+
const { shown, omittedCount } = selectOperatingNotesForPrompt(notes ?? [], now);
|
|
670
|
+
if (shown.length === 0) return '';
|
|
601
671
|
|
|
602
672
|
const lines: string[] = ['## Operating Notes', ''];
|
|
603
673
|
lines.push('Lessons earlier coordinators on this mesh recorded via `mesh_record_note`. Treat them as accumulated operating knowledge — apply them. When you learn a durable lesson (a provider quirk, a pattern to avoid, a recovery lesson), record it with `mesh_record_note` so future coordinators inherit it.');
|
|
604
674
|
lines.push('');
|
|
605
675
|
for (const n of shown) {
|
|
676
|
+
const pin = n.pinned ? '📌 ' : '';
|
|
606
677
|
const cat = n.category && categoryLabel[n.category] ? `[${categoryLabel[n.category]}] ` : '';
|
|
607
|
-
lines.push(`- ${cat}${truncateNote(n.text.trim())}`);
|
|
678
|
+
lines.push(`- ${pin}${cat}${truncateNote(n.text.trim())}`);
|
|
608
679
|
}
|
|
609
680
|
if (omittedCount > 0) {
|
|
610
681
|
lines.push('');
|
|
611
|
-
lines.push(`_${omittedCount}
|
|
682
|
+
lines.push(`_${omittedCount} lower-priority note${omittedCount === 1 ? '' : 's'} omitted (kept in ledger; expired-and-unpinned notes are also hidden from this list but retained for audit; prune with \`mesh_forget_note\`)._`);
|
|
612
683
|
}
|
|
613
684
|
return lines.join('\n');
|
|
614
685
|
}
|