@adhdev/daemon-core 0.9.82-rc.443 → 0.9.82-rc.445

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.
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { EventEmitter } from 'events';
16
16
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
17
- 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' | 'task_reclaimed' | 'coordinator_operating_note' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis';
17
+ 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' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis';
18
18
  export interface MeshLedgerEntry {
19
19
  id: string;
20
20
  meshId: string;
@@ -151,6 +151,10 @@ export interface AppendRemoteLedgerResult {
151
151
  entries: MeshLedgerEntry[];
152
152
  }
153
153
  export declare const MAX_LEDGER_SLICE_LIMIT = 500;
154
+ export declare const OPERATING_NOTE_KIND: MeshLedgerKind;
155
+ export declare const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind;
156
+ export declare const OPERATING_NOTE_DEDUPE_WINDOW = 40;
157
+ export declare const OPERATING_NOTE_KEEP_LATEST = 100;
154
158
  export declare function getLedgerDir(): string;
155
159
  /**
156
160
  * Footer to append to worker task messages so workers output structured results
@@ -180,6 +184,40 @@ export declare function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvi
180
184
  */
181
185
  export declare const meshLedgerEvents: EventEmitter<[never]>;
182
186
  export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
187
+ /** True if the operating note is retracted by any tombstone in `tombstones`. */
188
+ export declare function isOperatingNoteTombstoned(entry: Pick<MeshLedgerEntry, 'id' | 'payload'>, tombstones: {
189
+ ids: Set<string>;
190
+ fingerprints: Set<string>;
191
+ }): boolean;
192
+ /**
193
+ * Fix (2) supersede/remove: append a tombstone that retracts a coordinator
194
+ * operating note. Targets by note id and/or by exact trimmed text (a text target
195
+ * retracts every note with that text). History is preserved — the notes stay in
196
+ * the ledger but readers filter them out. Returns how many currently-live notes
197
+ * the tombstone will hide.
198
+ */
199
+ export declare function tombstoneOperatingNote(meshId: string, target: {
200
+ noteId?: string;
201
+ text?: string;
202
+ reason?: string;
203
+ }): {
204
+ tombstone: MeshLedgerEntry;
205
+ matched: number;
206
+ };
207
+ /**
208
+ * Read live operating notes (tombstoned notes filtered out), oldest→newest.
209
+ * `tail` bounds the number of live notes returned (the freshest N).
210
+ */
211
+ export declare function readOperatingNotes(meshId: string, opts?: {
212
+ tail?: number;
213
+ }): MeshLedgerEntry[];
214
+ /**
215
+ * Fix (3) keep-latest-N prune for coordinator_operating_note. Removes, from the
216
+ * store, (a) every note retracted by a tombstone, and (b) the oldest live notes
217
+ * beyond `keepLatest`. Tombstone entries themselves are retained as an audit trail
218
+ * of what was forgotten. Returns the number of note entries removed.
219
+ */
220
+ export declare function pruneOperatingNotes(meshId: string, keepLatest?: number): number;
183
221
  /**
184
222
  * Append entries received over local-first/P2P ledger replication to the local ledger.
185
223
  * This skips deduplicated entries and rejects malformed/cross-mesh entries.
@@ -286,6 +286,7 @@ export declare class CliProviderInstance implements ProviderInstance {
286
286
  private approvalResolutionFinalizationBlock;
287
287
  private scheduleCompletedDebounceFlush;
288
288
  private isMeshWorkerSession;
289
+ private isAutonomousMeshSession;
289
290
  /**
290
291
  * ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
291
292
  * Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
@@ -12,11 +12,35 @@
12
12
  * { source: 'USER_EXPLICIT'|'MODEL', type: string, content: string, status: 'DONE'|..., created_at: number }
13
13
  *
14
14
  * 3. ~/.gemini/antigravity-cli/conversations/<uuid>.pb
15
- * Protobuf binary — schema not publicly documented. Adapter extracts
15
+ * Legacy protobuf binary — schema not publicly documented. Adapter extracts
16
16
  * printable UTF-8 text runs as best-effort content (no proto library needed).
17
17
  *
18
+ * 4. ~/.gemini/antigravity-cli/conversations/<uuid>.db ← current format
19
+ * Per-session SQLite database. Recent antigravity migrated conversation
20
+ * storage from .pb (+ brain/*.jsonl) to a per-session SQLite db. The
21
+ * schema is a trajectory of `steps`, NOT a simple messages(role,content)
22
+ * table:
23
+ * steps(idx INTEGER PK, step_type INTEGER, status INTEGER,
24
+ * step_payload BLOB [protobuf], ...)
25
+ * Each `step_payload` is a protobuf message. Empirically (introspected
26
+ * from real stores):
27
+ * - step_type 14 → a USER turn. The prompt text is the largest
28
+ * contiguous UTF-8 run inside the payload (field 19 subtree).
29
+ * - step_type 15 → a MODEL/assistant turn. The assistant's final
30
+ * natural-language answer lives at payload field 20 → field 1
31
+ * (identical to field 8). Field 20 → field 3 is the internal
32
+ * reasoning summary and is intentionally NOT surfaced.
33
+ * - other step types are tool calls / ephemeral system context.
34
+ * We read the blobs with a tiny dependency-free protobuf field walker
35
+ * (no proto schema / codegen needed) and map the two message step types.
36
+ * Because the daemon does NOT read this db, native history previously
37
+ * returned 0 rows for these sessions and read_chat fell back to the
38
+ * pty parser (which only echoes the user's own input) — assistant
39
+ * answers appeared lost even though they were on disk.
40
+ *
18
41
  * This adapter provides:
19
- * - Full coverage when a brain transcript exists (authoritative source).
42
+ * - Full coverage from a per-session .db (current format) — preferred.
43
+ * - Full coverage when a brain transcript exists (legacy authoritative source).
20
44
  * - Partial coverage (user prompts only) from history.jsonl as fallback.
21
45
  * - Best-effort raw-string extraction from .pb files when no other source exists.
22
46
  *
@@ -24,6 +48,7 @@
24
48
  * ~/.gemini/antigravity-cli/history.jsonl
25
49
  * ~/.gemini/antigravity-cli/brain/{uuid}/.system_generated/logs/transcript*.jsonl
26
50
  * ~/.gemini/antigravity-cli/conversations/{uuid}.pb
51
+ * ~/.gemini/antigravity-cli/conversations/{uuid}.db
27
52
  *
28
53
  * OSS code (AGPL-3.0). Must not import from packages/ (proprietary).
29
54
  */
@@ -79,6 +104,7 @@ export interface NativeHistorySessionMeta {
79
104
  * `sessionPath` is the absolute path to one of:
80
105
  * - A brain transcript JSONL: ~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript*.jsonl
81
106
  * - The shared history.jsonl: ~/.gemini/antigravity-cli/history.jsonl
107
+ * - A conversation SQLite db: ~/.gemini/antigravity-cli/conversations/<uuid>.db
82
108
  * - A conversation protobuf: ~/.gemini/antigravity-cli/conversations/<uuid>.pb
83
109
  *
84
110
  * The session UUID is inferred from the directory name (brain), filename (pb), or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.443",
3
+ "version": "0.9.82-rc.445",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.443",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.445",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -101,8 +101,10 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
101
101
  // Best-effort: a read failure just omits the section.
102
102
  const buildOperatingNotesBestEffort = async (id: string) => {
103
103
  try {
104
- const { readLedgerEntries } = await import('../../mesh/mesh-ledger.js');
105
- const noteEntries = readLedgerEntries(id, { kind: ['coordinator_operating_note'], tail: 20 });
104
+ const { readOperatingNotes } = await import('../../mesh/mesh-ledger.js');
105
+ // readOperatingNotes filters out tombstoned (forgotten) notes so a
106
+ // retracted lesson never rides into the prompt. Newest last; tail 20.
107
+ const noteEntries = readOperatingNotes(id, { tail: 20 });
106
108
  const notes = noteEntries
107
109
  .map((e) => {
108
110
  const p = (e.payload || {}) as Record<string, unknown>;
package/src/index.ts CHANGED
@@ -262,7 +262,7 @@ export { loadRepoSettings } from './config/repo-settings.js';
262
262
  export type { RepoSettings, LoadRepoSettingsOptions } from './config/repo-settings.js';
263
263
 
264
264
  // ── Mesh Task Ledger ──
265
- export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
265
+ export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
266
266
  export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext, MeshTaskCompletionEvidence, MeshWorkerResultArtifact, MeshProcessArtifact, MeshValidationResultArtifact } from './mesh/mesh-ledger.js';
267
267
  export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
268
268
  export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
@@ -490,6 +490,7 @@ const TOOLS_SECTION = `## Available Tools
490
490
  | \`mesh_read_debug\` | Collect a daemon-side chat/parser debug bundle for a session |
491
491
  | \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
492
492
  | \`mesh_record_note\` | Record a durable, provider-neutral operating note (provider quirk / pattern to avoid / recovery lesson). Future coordinators see it under "## Operating Notes" at launch |
493
+ | \`mesh_forget_note\` | Retract a stale/wrong operating note by note_id or exact text so it stops riding into future coordinators' prompts (append-only tombstone; history preserved) |
493
494
  | \`mesh_git_status\` | Check git status on a specific node |
494
495
  | \`mesh_read_node_logs\` | Fetch a remote node's daemon log tail directly over P2P (grep/since/byte-bounded, secrets redacted) — no session/PowerShell needed to debug a node's daemon |
495
496
  | \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
@@ -49,6 +49,11 @@ export type MeshLedgerKind =
49
49
  // the ledger so it survives coordinator restarts and is provider-neutral.
50
50
  // payload: { text, category?, createdAt?, sourceCoordinator? }
51
51
  | 'coordinator_operating_note'
52
+ // Retraction of a coordinator_operating_note. Append-only (history preserved);
53
+ // readers filter out the targeted note so it leaves the prompt/list. Targets by
54
+ // note id (exact) and/or by trimmed-text fingerprint (matches all notes with that
55
+ // text). payload: { targetNoteId?, targetFingerprint?, reason?, forgottenAt? }
56
+ | 'coordinator_operating_note_tombstone'
52
57
  // Mission audit trail: mission record mutations (mesh_mission_upsert) so the
53
58
  // ledger captures mission lifecycle, not just task events. Without these a
54
59
  // mission create / goal rewrite / status transition left no ledger trace,
@@ -253,6 +258,32 @@ const ARCHIVABLE_KINDS: ReadonlySet<MeshLedgerKind> = new Set([
253
258
  const DEFAULT_LEDGER_SLICE_LIMIT = 100;
254
259
  export const MAX_LEDGER_SLICE_LIMIT = 500;
255
260
 
261
+ // ─── Operating-note growth control ──────────────
262
+ // coordinator_operating_note is append-only and, unlike task_* entries, is never
263
+ // archived by compactLedger (it is not in ARCHIVABLE_KINDS — it must survive
264
+ // restarts and there is no time-based cutoff for a "lesson"). Without dedicated
265
+ // controls the note set grows without bound and duplicate/stale notes crowd the
266
+ // bounded tail that rides into the coordinator prompt. These three constants back
267
+ // the three growth controls: dedupe-on-record, tombstone/forget, keep-latest-N.
268
+
269
+ // Kind marking an operating note as retracted. A tombstone is itself an
270
+ // append-only ledger entry (history is never destroyed); readers filter out the
271
+ // notes it targets. payload: { targetNoteId?, targetFingerprint?, reason? }
272
+ export const OPERATING_NOTE_KIND: MeshLedgerKind = 'coordinator_operating_note';
273
+ export const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind = 'coordinator_operating_note_tombstone';
274
+
275
+ // Dedupe window: when recording a note, if the same trimmed text already appears
276
+ // among the most recent OPERATING_NOTE_DEDUPE_WINDOW notes, the record is a no-op
277
+ // (the existing entry is returned). Keeps the prompt tail from filling with the
278
+ // same lesson recorded 20 times.
279
+ export const OPERATING_NOTE_DEDUPE_WINDOW = 40;
280
+
281
+ // Keep-latest-N: pruneOperatingNotes retains at most this many live (non-tombstoned)
282
+ // operating notes, removing the oldest surplus and any tombstoned notes from the
283
+ // store. The prompt reads a much smaller tail (20), so this bound never trims what
284
+ // a coordinator actually sees while still capping unbounded store growth.
285
+ export const OPERATING_NOTE_KEEP_LATEST = 100;
286
+
256
287
  // ─── Path Helpers ───────────────────────────────
257
288
 
258
289
  export function getLedgerDir(): string {
@@ -605,6 +636,23 @@ export function appendLedgerEntry(
605
636
  meshId: string,
606
637
  partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>,
607
638
  ): MeshLedgerEntry {
639
+ // Fix (1) dedupe-on-record: for a coordinator_operating_note, if the same
640
+ // trimmed text already appears among the most recent OPERATING_NOTE_DEDUPE_WINDOW
641
+ // notes, do NOT append a duplicate — return the existing entry so the bounded
642
+ // prompt tail can't be crowded by the same lesson recorded repeatedly. Other
643
+ // kinds (task_completed, …) are untouched.
644
+ if (partial.kind === OPERATING_NOTE_KIND) {
645
+ const text = operatingNoteText(partial.payload);
646
+ if (text) {
647
+ const recentNotes = readLedgerEntries(meshId, {
648
+ kind: [OPERATING_NOTE_KIND],
649
+ tail: OPERATING_NOTE_DEDUPE_WINDOW,
650
+ });
651
+ const existing = recentNotes.find(e => operatingNoteText(e.payload) === text);
652
+ if (existing) return existing;
653
+ }
654
+ }
655
+
608
656
  const entry: MeshLedgerEntry = {
609
657
  id: randomUUID(),
610
658
  meshId,
@@ -653,12 +701,149 @@ export function appendLedgerEntry(
653
701
  appendFileSync(filePath, line, { encoding: 'utf-8', mode: 0o600 });
654
702
  invalidateLedgerCache(meshId);
655
703
  meshLedgerEvents.emit('append', meshId, entry);
704
+ // Fix (3) keep-latest-N: operating notes are never archived by compactLedger,
705
+ // so cap their store footprint here. Runs only when a note (or its tombstone)
706
+ // is recorded, and is a no-op until the live-note count exceeds the bound.
707
+ if (entry.kind === OPERATING_NOTE_KIND || entry.kind === OPERATING_NOTE_TOMBSTONE_KIND) {
708
+ try { pruneOperatingNotes(meshId); } catch { /* prune is best-effort */ }
709
+ }
656
710
  return entry;
657
711
  } catch (e: any) {
658
712
  throw new Error(`Failed to append to ledger for mesh ${meshId}: ${e.message}`);
659
713
  }
660
714
  }
661
715
 
716
+ // ─── Operating-note growth controls ─────────────
717
+
718
+ /** Extract the trimmed note text from a coordinator_operating_note payload. */
719
+ function operatingNoteText(payload: Record<string, unknown> | undefined): string | undefined {
720
+ const text = payload && typeof payload.text === 'string' ? payload.text.trim() : '';
721
+ return text || undefined;
722
+ }
723
+
724
+ /**
725
+ * Set of trimmed-text fingerprints and note ids retracted by tombstone entries in
726
+ * the given entry set. A tombstone targets by note id and/or by text fingerprint.
727
+ */
728
+ function collectOperatingNoteTombstones(entries: MeshLedgerEntry[]): { ids: Set<string>; fingerprints: Set<string> } {
729
+ const ids = new Set<string>();
730
+ const fingerprints = new Set<string>();
731
+ for (const e of entries) {
732
+ if (e.kind !== OPERATING_NOTE_TOMBSTONE_KIND) continue;
733
+ const p = e.payload || {};
734
+ const targetId = typeof p.targetNoteId === 'string' ? p.targetNoteId.trim() : '';
735
+ const targetFp = typeof p.targetFingerprint === 'string' ? p.targetFingerprint.trim() : '';
736
+ if (targetId) ids.add(targetId);
737
+ if (targetFp) fingerprints.add(targetFp);
738
+ }
739
+ return { ids, fingerprints };
740
+ }
741
+
742
+ /** True if the operating note is retracted by any tombstone in `tombstones`. */
743
+ export function isOperatingNoteTombstoned(
744
+ entry: Pick<MeshLedgerEntry, 'id' | 'payload'>,
745
+ tombstones: { ids: Set<string>; fingerprints: Set<string> },
746
+ ): boolean {
747
+ if (tombstones.ids.has(entry.id)) return true;
748
+ const text = operatingNoteText(entry.payload);
749
+ return text ? tombstones.fingerprints.has(text) : false;
750
+ }
751
+
752
+ /**
753
+ * Fix (2) supersede/remove: append a tombstone that retracts a coordinator
754
+ * operating note. Targets by note id and/or by exact trimmed text (a text target
755
+ * retracts every note with that text). History is preserved — the notes stay in
756
+ * the ledger but readers filter them out. Returns how many currently-live notes
757
+ * the tombstone will hide.
758
+ */
759
+ export function tombstoneOperatingNote(
760
+ meshId: string,
761
+ target: { noteId?: string; text?: string; reason?: string },
762
+ ): { tombstone: MeshLedgerEntry; matched: number } {
763
+ const noteId = typeof target.noteId === 'string' ? target.noteId.trim() : '';
764
+ const fingerprint = typeof target.text === 'string' ? target.text.trim() : '';
765
+ if (!noteId && !fingerprint) {
766
+ throw new Error('tombstoneOperatingNote requires a noteId or text target');
767
+ }
768
+
769
+ // Count currently-live matches (not already tombstoned) for the caller's report.
770
+ const notes = readOperatingNotes(meshId);
771
+ const matched = notes.filter(n =>
772
+ (noteId && n.id === noteId) || (fingerprint && operatingNoteText(n.payload) === fingerprint),
773
+ ).length;
774
+
775
+ const tombstone = appendLedgerEntry(meshId, {
776
+ kind: OPERATING_NOTE_TOMBSTONE_KIND,
777
+ payload: {
778
+ ...(noteId ? { targetNoteId: noteId } : {}),
779
+ ...(fingerprint ? { targetFingerprint: fingerprint } : {}),
780
+ ...(target.reason && target.reason.trim() ? { reason: target.reason.trim() } : {}),
781
+ forgottenAt: new Date().toISOString(),
782
+ },
783
+ });
784
+ return { tombstone, matched };
785
+ }
786
+
787
+ /**
788
+ * Read live operating notes (tombstoned notes filtered out), oldest→newest.
789
+ * `tail` bounds the number of live notes returned (the freshest N).
790
+ */
791
+ export function readOperatingNotes(meshId: string, opts?: { tail?: number }): MeshLedgerEntry[] {
792
+ const raw = getCachedRawEntries(meshId);
793
+ const tombstones = collectOperatingNoteTombstones(raw);
794
+ let notes = raw.filter(e => e.kind === OPERATING_NOTE_KIND && !isOperatingNoteTombstoned(e, tombstones));
795
+ if (opts?.tail && opts.tail > 0 && notes.length > opts.tail) {
796
+ notes = notes.slice(-opts.tail);
797
+ }
798
+ return notes;
799
+ }
800
+
801
+ /**
802
+ * Fix (3) keep-latest-N prune for coordinator_operating_note. Removes, from the
803
+ * store, (a) every note retracted by a tombstone, and (b) the oldest live notes
804
+ * beyond `keepLatest`. Tombstone entries themselves are retained as an audit trail
805
+ * of what was forgotten. Returns the number of note entries removed.
806
+ */
807
+ export function pruneOperatingNotes(meshId: string, keepLatest: number = OPERATING_NOTE_KEEP_LATEST): number {
808
+ const raw = getCachedRawEntries(meshId);
809
+ const tombstones = collectOperatingNoteTombstones(raw);
810
+
811
+ const removeIds: string[] = [];
812
+ const liveNotes: MeshLedgerEntry[] = [];
813
+ for (const e of raw) {
814
+ if (e.kind !== OPERATING_NOTE_KIND) continue;
815
+ if (isOperatingNoteTombstoned(e, tombstones)) {
816
+ removeIds.push(e.id); // tombstoned notes are pruned first
817
+ } else {
818
+ liveNotes.push(e);
819
+ }
820
+ }
821
+
822
+ // Drop the oldest live notes beyond keepLatest; the freshest (prompt tail) survive.
823
+ const bound = Math.max(0, Math.floor(keepLatest));
824
+ if (liveNotes.length > bound) {
825
+ for (const e of liveNotes.slice(0, liveNotes.length - bound)) removeIds.push(e.id);
826
+ }
827
+
828
+ if (removeIds.length === 0) return 0;
829
+
830
+ try {
831
+ MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, removeIds);
832
+ } catch { /* store unavailable — JSONL rewrite below still trims */ }
833
+
834
+ // Rewrite the JSONL mirror without the pruned note entries so the export
835
+ // artifact stays consistent with the store.
836
+ try {
837
+ const remaining = readLedgerFile(meshId).filter(e => !removeIds.includes(e.id));
838
+ const filePath = getLedgerPath(meshId);
839
+ const lines = remaining.length ? remaining.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
840
+ writeFileSync(filePath, lines, { encoding: 'utf-8', mode: 0o600 });
841
+ } catch { /* JSONL rewrite best-effort; store is the primary read path */ }
842
+
843
+ invalidateLedgerCache(meshId);
844
+ return removeIds.length;
845
+ }
846
+
662
847
  function clampLedgerSliceLimit(limit: unknown): number {
663
848
  if (typeof limit !== 'number' || !Number.isFinite(limit)) return DEFAULT_LEDGER_SLICE_LIMIT;
664
849
  return Math.max(1, Math.min(MAX_LEDGER_SLICE_LIMIT, Math.floor(limit)));
@@ -1207,8 +1207,7 @@ export class CliProviderInstance implements ProviderInstance {
1207
1207
  * to the genuine-modal classification.
1208
1208
  */
1209
1209
  private isTransientToolConsent(now = Date.now()): boolean {
1210
- const isAutonomousMeshSession = this.isMeshWorkerSession() || !!this.settings.meshCoordinatorFor;
1211
- return isAutonomousMeshSession
1210
+ return this.isAutonomousMeshSession()
1212
1211
  && this.hasAdapterPendingResponse()
1213
1212
  && !this.manualAttendance.isAttended(now);
1214
1213
  }
@@ -1841,6 +1840,20 @@ export class CliProviderInstance implements ProviderInstance {
1841
1840
  || this.settings.meshNodeId || this.settings.launchedByCoordinator);
1842
1841
  }
1843
1842
 
1843
+ // FALSE-IDLE (self-coordinator settle): an autonomously-progressing mesh session
1844
+ // is either a delegated worker (isMeshWorkerSession) OR the coordinator's OWN
1845
+ // claude-cli session (meshCoordinatorFor). Both run auto-approved tool turns whose
1846
+ // inter-approval valley (busy→idle blip→generating re-entry ~0.5s later) must be
1847
+ // absorbed by the completedDebounce settle window, not flushed on the first idle
1848
+ // sample. The worker branch already gets NATIVE_HISTORY_MESH_IDLE_SETTLE_MS; the
1849
+ // self-coordinator session (worker markers absent, meshCoordinatorFor present) was
1850
+ // taking flushDelay=0 — no settle window — so its busyEpoch/lastOutputAt continuity
1851
+ // guard had no window to observe the valley and fired mid-turn "next-step" previews
1852
+ // as a finalSummary. Mirrors the isAutonomousMeshSession notion in isTransientToolConsent.
1853
+ private isAutonomousMeshSession(): boolean {
1854
+ return this.isMeshWorkerSession() || !!this.settings.meshCoordinatorFor;
1855
+ }
1856
+
1844
1857
  /**
1845
1858
  * ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
1846
1859
  * Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
@@ -2623,16 +2636,29 @@ export class CliProviderInstance implements ProviderInstance {
2623
2636
  };
2624
2637
  const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
2625
2638
  // (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the
2626
- // transcript is authoritative). For mesh worker sessions, give the
2627
- // generating→idle transition a short settle window so a background-child
2639
+ // transcript is authoritative). For autonomously-progressing mesh sessions,
2640
+ // give the generating→idle transition a short settle window so a background-child
2628
2641
  // false idle (quiet after a backgrounded test/command while the parent turn
2629
- // continues) gets caught by the resume guard in flushCompletedDebounceIfFinalized
2630
- // instead of firing an early completion the coordinator can never correct.
2631
- const meshWorkerSession = this.isMeshWorkerSession();
2642
+ // continues) or an inter-approval auto-approve valley gets caught by the resume
2643
+ // guard in flushCompletedDebounceIfFinalized instead of firing an early completion
2644
+ // the coordinator can never correct.
2645
+ //
2646
+ // (FALSE-IDLE self-coordinator settle) The settle window now covers BOTH mesh
2647
+ // worker sessions AND the coordinator's own claude-cli session (meshCoordinatorFor):
2648
+ // isAutonomousMeshSession(). Previously only isMeshWorkerSession() qualified, so a
2649
+ // self-coordinating daemon (worker + coordinator on the same daemon) ran the
2650
+ // coordinator's own turn at flushDelay=0 — no settle window at all — and the
2651
+ // busyEpoch/lastOutputAt continuity guard, being a flush-time point-check, had no
2652
+ // window in which to observe the ~0.5s auto-approve valley. Its mid-turn
2653
+ // "next-step" sentence was flushed as finalSummary. A genuinely non-mesh session
2654
+ // (neither worker nor self-coordinator) still flushes immediately (delay=0), so
2655
+ // no non-mesh behaviour changes; this only ADDS a settle window (strictly
2656
+ // stricter — the guard can only ever CANCEL a pending flush, never emit more).
2657
+ const meshSettleSession = this.isAutonomousMeshSession();
2632
2658
  const flushDelay = ownsExternalHistory
2633
- ? (meshWorkerSession ? NATIVE_HISTORY_MESH_IDLE_SETTLE_MS : 0)
2659
+ ? (meshSettleSession ? NATIVE_HISTORY_MESH_IDLE_SETTLE_MS : 0)
2634
2660
  : 3000;
2635
- LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} meshWorker=${meshWorkerSession} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
2661
+ LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} meshSettle=${meshSettleSession} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
2636
2662
  this.scheduleCompletedDebounceFlush(flushDelay);
2637
2663
  }
2638
2664
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {