@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +350 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +342 -22
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +39 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +28 -2
- package/package.json +2 -2
- package/src/commands/high-family/mesh-coordinator-launch.ts +4 -2
- package/src/index.ts +1 -1
- package/src/mesh/coordinator-prompt.ts +1 -0
- package/src/mesh/mesh-ledger.ts +185 -0
- package/src/providers/cli-provider-instance.ts +35 -9
- package/src/providers/native-history/antigravity-cli-transcript.ts +367 -25
- package/src/providers/native-history/dispatcher.ts +42 -14
|
@@ -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
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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 {
|
|
105
|
-
|
|
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 |
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
2630
|
-
// instead of firing an early completion
|
|
2631
|
-
|
|
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
|
-
? (
|
|
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}
|
|
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') {
|