@quantmind-br/pi-goal-x 0.0.0-stage → 0.35.1

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/assets/badge-dark.svg +18 -0
  4. package/assets/badge-light.svg +18 -0
  5. package/docs/advanced-usage.md +106 -0
  6. package/docs/agent-flow-design.md +306 -0
  7. package/docs/agentic-runtime-prd.md +773 -0
  8. package/docs/architecture.md +368 -0
  9. package/docs/controls.md +140 -0
  10. package/docs/pi-autoresearch-survey.md +50 -0
  11. package/docs/pi-goal-x-unified-dashboard-plan.md +1667 -0
  12. package/docs/prompt-caching.md +31 -0
  13. package/docs/unified-dashboard.md +286 -0
  14. package/extensions/auditor-selector.ts +74 -0
  15. package/extensions/goal-accounting.ts +107 -0
  16. package/extensions/goal-activity.ts +202 -0
  17. package/extensions/goal-auditor.ts +480 -0
  18. package/extensions/goal-commands.ts +866 -0
  19. package/extensions/goal-compaction.ts +197 -0
  20. package/extensions/goal-completion.ts +173 -0
  21. package/extensions/goal-context.ts +12 -0
  22. package/extensions/goal-contract.ts +58 -0
  23. package/extensions/goal-controls.ts +202 -0
  24. package/extensions/goal-core-tools.ts +549 -0
  25. package/extensions/goal-core.ts +78 -0
  26. package/extensions/goal-detail.ts +65 -0
  27. package/extensions/goal-dialog-controls.ts +31 -0
  28. package/extensions/goal-draft.ts +244 -0
  29. package/extensions/goal-drafting.ts +535 -0
  30. package/extensions/goal-events.ts +692 -0
  31. package/extensions/goal-format.ts +376 -0
  32. package/extensions/goal-interruption.ts +53 -0
  33. package/extensions/goal-ledger-index.ts +68 -0
  34. package/extensions/goal-ledger.ts +1007 -0
  35. package/extensions/goal-live-retention.ts +239 -0
  36. package/extensions/goal-oracle.ts +346 -0
  37. package/extensions/goal-policy.ts +370 -0
  38. package/extensions/goal-pool.ts +103 -0
  39. package/extensions/goal-prompt-cache.ts +80 -0
  40. package/extensions/goal-questionnaire.ts +1073 -0
  41. package/extensions/goal-record.ts +388 -0
  42. package/extensions/goal-recovery.ts +256 -0
  43. package/extensions/goal-runtime.ts +277 -0
  44. package/extensions/goal-scheduler-state.ts +100 -0
  45. package/extensions/goal-scheduler.ts +436 -0
  46. package/extensions/goal-service.ts +843 -0
  47. package/extensions/goal-session-health.ts +130 -0
  48. package/extensions/goal-session-safety.ts +65 -0
  49. package/extensions/goal-settings.ts +1370 -0
  50. package/extensions/goal-state.ts +1069 -0
  51. package/extensions/goal-status.ts +305 -0
  52. package/extensions/goal-task-confirmation.ts +174 -0
  53. package/extensions/goal-task-count.ts +59 -0
  54. package/extensions/goal-task-derive.ts +46 -0
  55. package/extensions/goal-task-index.ts +57 -0
  56. package/extensions/goal-task-tools.ts +595 -0
  57. package/extensions/goal-tool-inputs.ts +102 -0
  58. package/extensions/goal-tool-names.ts +73 -0
  59. package/extensions/goal-tools.ts +16 -0
  60. package/extensions/goal-widget.ts +455 -0
  61. package/extensions/goal.ts +42 -0
  62. package/extensions/network-error-backoff.ts +55 -0
  63. package/extensions/prompts/goal-prompts.ts +330 -0
  64. package/extensions/storage/atomic-rename.ts +52 -0
  65. package/extensions/storage/goal-files.ts +695 -0
  66. package/extensions/storage/goal-lock.ts +131 -0
  67. package/extensions/storage/goal-root.ts +95 -0
  68. package/extensions/widgets/auditor-dashboard-model.ts +174 -0
  69. package/extensions/widgets/dialog-scaffold.ts +26 -0
  70. package/extensions/widgets/goal-controls-dialog.ts +329 -0
  71. package/extensions/widgets/goal-dashboard-model.ts +530 -0
  72. package/extensions/widgets/goal-dashboard-renderer.ts +674 -0
  73. package/extensions/widgets/goal-escape-dialog.ts +148 -0
  74. package/extensions/widgets/goal-notifications.ts +9 -0
  75. package/extensions/widgets/goal-widget.ts +723 -0
  76. package/extensions/widgets/task-list-overlay.ts +409 -0
  77. package/extensions/widgets/text-cache.ts +30 -0
  78. package/package.json +117 -4
  79. package/scripts/recover-session-checkpoints.mjs +289 -0
@@ -0,0 +1,1007 @@
1
+ import { goalStoragePath, ensureGoalStorageDirectory, isExternalGoalStorage, type GoalStorageContext } from "./storage/goal-root.ts";
2
+ import { indexedActivityEvents } from "./goal-activity.ts";
3
+ import { buildLedgerIndex, indexLedgerEvent, auditApplies, type GoalLedgerIndex } from "./goal-ledger-index.ts";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { renameAtomicSync } from "./storage/atomic-rename.ts";
7
+ import { nowIso, safeIdPart, type GoalRecord } from "./goal-record.ts";
8
+
9
+ export const GOAL_LEDGER_FILE = ".pi/goals/goal_events.jsonl";
10
+
11
+ export type GoalLedgerEvent =
12
+ | { type: "goal_created"; goalId: string; objective: string; sisyphus: boolean; autoContinue: boolean; at: string }
13
+ | { type: "goal_focused"; goalId: string; reason: string; at: string }
14
+ | { type: "goal_unfocused"; reason: string; at: string }
15
+ | { type: "goal_paused"; goalId: string; reason: string; suggestedAction?: string; status?: "paused"; source?: "user" | "agent"; at: string }
16
+ | { type: "goal_resumed"; goalId: string; reason: string; at: string }
17
+ | { type: "goal_tweaked"; goalId: string; changeSummary: string; at: string }
18
+ | { type: "auditor_toggled"; goalId: string; enabled: boolean; at: string }
19
+ | { type: "completion_requested"; goalId: string; summary?: string; at: string }
20
+ | { type: "audit_started"; goalId: string; auditId?: string; provider?: string; model?: string; thinkingLevel?: string; at: string }
21
+ | { type: "audit_result"; goalId: string; auditId?: string; verdict: "approved" | "disapproved" | "error"; report: string; at: string }
22
+ | { type: "audit_skipped"; goalId: string; auditId?: string; reason: "disabled" | "user_aborted"; provider?: string; model?: string; thinkingLevel?: string; at: string }
23
+ | { type: "goal_completed"; goalId: string; archivePath?: string; at: string }
24
+ | { type: "goal_archived"; goalId: string; archivePath: string; at: string }
25
+ | { type: "goal_archive_failed"; goalId: string; message: string; at: string }
26
+ | { type: "goal_aborted"; goalId: string; reason: string; archivePath?: string; at: string }
27
+ | { type: "task_list_set"; goalId: string; taskCount: number; blockCompletion: boolean; at: string }
28
+ | { type: "task_complete"; goalId: string; taskId: string; evidence?: string; at: string }
29
+ | { type: "task_skipped"; goalId: string; taskId: string; reason: string; at: string }
30
+ | { type: "task_reopened"; goalId: string; taskId: string; at: string }
31
+ | { type: "task_started"; goalId: string; taskId: string; at: string }
32
+ | { type: "goal_budget_changed"; goalId: string; oldBudget: number | null; newBudget: number | null; tokensUsed: number; at: string }
33
+ | { type: "goal_budget_limited"; goalId: string; budget: number; tokensUsed: number; at: string }
34
+ | { type: "goal_budget_warning"; goalId: string; budget: number; tokensUsed: number; pct: number; at: string }
35
+ | { type: "goal_stalled"; goalId: string; reason: string; at: string }
36
+ | { type: "goal_blocked"; goalId: string; reason: string; source: "agent" | "system"; at: string }
37
+ | { type: "oracle_started"; goalId: string; fingerprint: string; provider: string; model: string; thinkingLevel?: string; reason: string; at: string }
38
+ | { type: "oracle_result"; goalId: string; fingerprint: string; adviceId: string; disposition: "actionable" | "needs_human" | "insufficient_context"; summary: string; recommendedTitle?: string; at: string }
39
+ | { type: "oracle_failed"; goalId: string; fingerprint: string; attempt: number; errorCode: "config" | "provider" | "aborted" | "invalid_output"; message: string; at: string }
40
+ | { type: "oracle_followup_attempted"; goalId: string; fingerprint: string; adviceId: string; firstToolName: string; at: string };
41
+
42
+ export type GoalLedgerContext = GoalStorageContext;
43
+
44
+ export interface GoalLedgerReadResult {
45
+ events: GoalLedgerEvent[];
46
+ malformed: number;
47
+ /** Opaque generation for borrowed, read-only history views; replaced on append/refresh. */
48
+ revision?: object;
49
+ }
50
+
51
+ export interface ReconstructedGoalState {
52
+ goalId: string;
53
+ latestStatus: "active" | "paused" | "complete" | "aborted" | "unknown";
54
+ latestFocus: boolean;
55
+ latestPauseReason?: string;
56
+ latestPauseSuggestedAction?: string;
57
+ latestAuditId?: string;
58
+ latestAuditorResult?: { verdict: "approved" | "disapproved" | "error"; report: string; at: string };
59
+ /** Issue #26: bounded latest Oracle disposition for this goal. */
60
+ latestOracleResult?: { fingerprint: string; adviceId: string; disposition: "actionable" | "needs_human" | "insufficient_context"; summary: string; at: string };
61
+ createdAt?: string;
62
+ completedAt?: string;
63
+ abortedAt?: string;
64
+ tweakedAt?: string;
65
+ resumedAt?: string;
66
+ }
67
+
68
+ export interface ReconstructedLedgerState {
69
+ focusedGoalId: string | null;
70
+ goals: Map<string, ReconstructedGoalState>;
71
+ terminalGoals: Map<string, ReconstructedGoalState>;
72
+ }
73
+
74
+ function safeGoalId(value: string): string {
75
+ return safeIdPart(value);
76
+ }
77
+
78
+ export function goalLedgerPath(ctx: GoalLedgerContext): string {
79
+ return goalStoragePath(ctx, GOAL_LEDGER_FILE);
80
+ }
81
+
82
+ export type GoalLedgerAppendResult = { ok: true } | { ok: false; error: unknown };
83
+
84
+ /**
85
+ * Append one ledger event. Returns a discriminated result instead of swallowing
86
+ * both append attempts internally: the authoritative state write is never
87
+ * rolled back after a ledger failure, but callers (GoalService) route failures
88
+ * through the onDiagnostic hook so they stay observable.
89
+ *
90
+ * NAF: after a successful append the in-memory ledger cache is extended with
91
+ * the same event (sanitized), so the next readGoalLedger is zero-op and
92
+ * always current for extension-mediated writes.
93
+ */
94
+ export function appendGoalEvent(ctx: GoalLedgerContext, event: GoalLedgerEvent): GoalLedgerAppendResult {
95
+ const result = appendLedgerLines(ctx, [event]);
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Append several ledger events as one line block with the existing
101
+ * temp-write→read→append durability (P1-8): one mkdir, one temp write, one
102
+ * append, one unlink instead of N× the same sequence. NAF: extends the
103
+ * in-memory ledger cache in one step too.
104
+ */
105
+ export function appendGoalEvents(ctx: GoalLedgerContext, events: GoalLedgerEvent[]): GoalLedgerAppendResult {
106
+ if (events.length === 0) return { ok: true };
107
+ return appendLedgerLines(ctx, events);
108
+ }
109
+
110
+ function appendLedgerLines(ctx: GoalLedgerContext, events: GoalLedgerEvent[]): GoalLedgerAppendResult {
111
+ const filePath = goalLedgerPath(ctx);
112
+ const dir = path.dirname(filePath);
113
+ // NAF: per-dir memo — mkdir once per directory per process; steady-state
114
+ // appends skip it entirely (0 ops for the dir).
115
+ if (!ledgerDirsKnown.has(dir)) {
116
+ try {
117
+ ensureGoalStorageDirectory(ctx, ".pi/goals", false);
118
+ ledgerDirsKnown.add(dir);
119
+ } catch (err) {
120
+ return { ok: false, error: err };
121
+ }
122
+ }
123
+
124
+ const lines = events.map((event) => JSON.stringify(event) + "\n").join("");
125
+ // NAF: direct O_APPEND write (one op) instead of the temp-write→read→append
126
+ // dance — a single JSONL line (or one batched block) is appended atomically
127
+ // by the OS; torn-line handling lives in the reader, not here.
128
+ try {
129
+ if (isExternalGoalStorage(ctx)) {
130
+ ensureGoalStorageDirectory(ctx, ".pi/goals");
131
+ try { if (fs.lstatSync(filePath).isSymbolicLink()) throw new Error(`Goal ledger is a symlink: ${filePath}`); }
132
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
133
+ }
134
+ fs.appendFileSync(filePath, lines, "utf8");
135
+ } catch (err) {
136
+ return { ok: false, error: err };
137
+ }
138
+ extendLedgerCache(filePath, lines, events);
139
+ // Best-effort checkpoint maintenance: bounded cold starts depend on it, but
140
+ // the ledger append above is authoritative and never rolled back.
141
+ updateLedgerCheckpointAfterAppend(filePath, lines, events);
142
+ return { ok: true };
143
+ }
144
+
145
+ /** Directories whose ledger file exists (per-dir mkdir memo). */
146
+ const ledgerDirsKnown = new Set<string>();
147
+
148
+ /**
149
+ * Zero-op ledger cache (NAF 2026-08-06): keyed by absolute ledger path.
150
+ * Steady-state reads serve the cache with no fs ops; appendGoalEvent(s)
151
+ * extend it in memory (see extendLedgerCache). External (non-extension)
152
+ * edits to the ledger go stale mid-session (documented in the naf spec).
153
+ */
154
+ interface LedgerCacheEntry {
155
+ size: number;
156
+ mtimeMs: number;
157
+ chars: number;
158
+ events: GoalLedgerEvent[];
159
+ malformed: number;
160
+ revision: object;
161
+ }
162
+
163
+ const ledgerCache = new Map<string, LedgerCacheEntry>();
164
+
165
+ /**
166
+ * Session boundary (session_start / resume): drop the zero-op ledger cache and
167
+ * the checkpoint mirror so a new session re-reads the ledger fresh from disk.
168
+ */
169
+ export function invalidateGoalLedgerCache(): void {
170
+ ledgerCache.clear();
171
+ runtimeReady.clear();
172
+ checkpointCache.clear();
173
+ lastCheckpointDiskWrite.clear();
174
+ }
175
+
176
+ /** Keep the zero-op ledger cache in sync with an in-process append (no fs ops). */
177
+ function extendLedgerCache(filePath: string, lines: string, events: GoalLedgerEvent[]): void {
178
+ const cached = ledgerCache.get(filePath);
179
+ if (!cached) return;
180
+ for (const event of events) cached.events.push(sanitizeEvent(event));
181
+ ledgerCache.set(filePath, {
182
+ size: cached.size + Buffer.byteLength(lines, "utf8"),
183
+ mtimeMs: cached.mtimeMs,
184
+ chars: cached.chars + lines.length,
185
+ events: cached.events,
186
+ malformed: cached.malformed,
187
+ revision: {},
188
+ });
189
+ }
190
+
191
+ export function readGoalLedger(ctx: GoalLedgerContext): GoalLedgerReadResult {
192
+ const filePath = goalLedgerPath(ctx);
193
+ const cached = ledgerCache.get(filePath);
194
+ if (cached) {
195
+ // NAF zero-op steady state: no stat, no read, no parse. The cache is kept
196
+ // current by extendLedgerCache on every in-process append; external
197
+ // (non-extension) edits to the ledger go stale mid-session (documented).
198
+ return { events: cached.events, malformed: cached.malformed, revision: cached.revision };
199
+ }
200
+ return readGoalLedgerCold(ctx, filePath);
201
+ }
202
+
203
+ /** Cold read: full file read + parse, populating the zero-op cache. */
204
+ function readGoalLedgerCold(ctx: GoalLedgerContext, filePath: string): GoalLedgerReadResult {
205
+ let content: string;
206
+ try {
207
+ content = fs.readFileSync(filePath, "utf8");
208
+ } catch {
209
+ // Missing or unreadable: cache the empty result so repeated reads are zero-op.
210
+ const empty = {events: [], malformed: 0, revision: {}};
211
+ ledgerCache.set(filePath, {size: 0, mtimeMs: 0, chars: 0, ...empty});
212
+ return empty;
213
+ }
214
+ const parsed = parseLedgerLines(content);
215
+ const result = {...parsed, revision: {}};
216
+ ledgerCache.set(filePath, { size: Buffer.byteLength(content, "utf8"), mtimeMs: 0, chars: content.length, ...result });
217
+ return result;
218
+ }
219
+
220
+ /** Parse a JSONL ledger body into sanitized events + a malformed-line count. */
221
+ function parseLedgerLines(content: string): GoalLedgerReadResult {
222
+ const events: GoalLedgerEvent[] = [];
223
+ // Goal IDs repeat across most rows. A bounded per-read map avoids repeated sanitation.
224
+ const goalIds = new Map<string, string>();
225
+ let malformed = 0;
226
+ for (const line of content.split("\n")) {
227
+ const trimmed = line.trim();
228
+ if (!trimmed) continue;
229
+ try {
230
+ const parsed = JSON.parse(trimmed) as unknown;
231
+ if (isValidLedgerEvent(parsed)) {
232
+ if ("goalId" in parsed) {
233
+ let id = goalIds.get(parsed.goalId);
234
+ if (id === undefined) { id = safeGoalId(parsed.goalId); if (goalIds.size < 1024) goalIds.set(parsed.goalId, id); }
235
+ parsed.goalId = id;
236
+ }
237
+ events.push(parsed);
238
+ } else {
239
+ malformed++;
240
+ }
241
+ } catch {
242
+ malformed++;
243
+ }
244
+ }
245
+ return { events, malformed };
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Ledger checkpoint (reliability campaign 2026-08-09)
250
+ //
251
+ // The zero-op cache bounds steady-state reads, but a fresh session still reads
252
+ // and parses the complete JSONL ledger. The checkpoint bounds the COLD path:
253
+ // it stores the reconstructed accumulator (state + focus bookkeeping), the
254
+ // per-goal recent-event tails, and the byte position of the ledger it covers.
255
+ // A cold read then costs 2 fs ops (checkpoint read + stat) when covered, or a
256
+ // bounded positioned tail read when the ledger grew. The JSONL ledger itself
257
+ // is untouched and remains authoritative: the checkpoint is a best-effort
258
+ // optimization — missing/corrupt/version-mismatched checkpoints fall back to
259
+ // the full parse, and any coveredBytes mismatch (external edits, truncation)
260
+ // falls back or replays the tail.
261
+ // ---------------------------------------------------------------------------
262
+
263
+ export const LEDGER_CHECKPOINT_FILE = ".goal-ledger-checkpoint.json";
264
+ export const LEDGER_CHECKPOINT_VERSION = 4;
265
+ const CHECKPOINT_RECENT_CAP = 12;
266
+
267
+ /** In-memory ledger checkpoint (maps in native form). */
268
+ export interface LedgerCheckpoint {
269
+ version: typeof LEDGER_CHECKPOINT_VERSION;
270
+ format: "goal-ledger-checkpoint";
271
+ createdAt: string;
272
+ /** Byte length of the ledger covered (compared against stat().size). */
273
+ coveredBytes: number;
274
+ /** Number of ledger events covered. */
275
+ coveredEvents: number;
276
+ /** Pre-finalize accumulator (focus bookkeeping kept for incremental tail apply). */
277
+ acc: ReconstructAccumulator;
278
+ /** Per-goal recent-event tails (capped), already sanitized. */
279
+ recentEventsByGoal: Map<string, GoalLedgerEvent[]>;
280
+ runtimeIndex: Map<string, GoalLedgerIndex>;
281
+ }
282
+
283
+ export interface LedgerStateReadResult {
284
+ state: ReconstructedLedgerState;
285
+ recentEventsByGoal: Map<string, GoalLedgerEvent[]>;
286
+ malformed: number;
287
+ coveredBytes: number;
288
+ coveredEvents: number;
289
+ source: "cache" | "checkpoint" | "tail" | "full";
290
+ }
291
+
292
+ /** In-process checkpoint mirror (null = known absent). */
293
+ const checkpointCache = new Map<string, LedgerCheckpoint | null>();
294
+
295
+ /**
296
+ * Disk-write throttle for the mutation path: the in-memory mirror updates on
297
+ * every append (0 fs ops), but the atomic temp-write+rename lands at most once
298
+ * per CHECKPOINT_WRITE_INTERVAL appends or CHECKPOINT_WRITE_MIN_MS elapsed —
299
+ * keeping the NAF append headroom (B1.append.x4 <= 2 ops) while still bounding
300
+ * cold-start tails for long sessions. Staleness is always safe: a coveredBytes
301
+ * mismatch only costs a tail replay.
302
+ */
303
+ const CHECKPOINT_WRITE_INTERVAL = 32;
304
+ const CHECKPOINT_WRITE_MIN_MS = 2000;
305
+ const lastCheckpointDiskWrite = new Map<string, { appendsSinceWrite: number; at: number }>();
306
+
307
+ function goalIdOf(event: GoalLedgerEvent): string | null {
308
+ return "goalId" in event ? event.goalId : null;
309
+ }
310
+
311
+ function checkpointPathFor(filePath: string): string {
312
+ return path.join(path.dirname(filePath), LEDGER_CHECKPOINT_FILE);
313
+ }
314
+
315
+ /** JSON-safe checkpoint shape (Maps serialized as arrays). */
316
+ function checkpointToJson(cp: LedgerCheckpoint): unknown {
317
+ const goalStateToJson = (s: ReconstructedGoalState) => ({
318
+ goalId: s.goalId,
319
+ latestStatus: s.latestStatus,
320
+ latestFocus: s.latestFocus,
321
+ latestPauseReason: s.latestPauseReason,
322
+ latestPauseSuggestedAction: s.latestPauseSuggestedAction,
323
+ latestAuditorResult: s.latestAuditorResult,
324
+ latestAuditId: s.latestAuditId,
325
+ latestOracleResult: s.latestOracleResult,
326
+ createdAt: s.createdAt,
327
+ completedAt: s.completedAt,
328
+ abortedAt: s.abortedAt,
329
+ tweakedAt: s.tweakedAt,
330
+ resumedAt: s.resumedAt,
331
+ });
332
+ const mapToJson = (m: Map<string, ReconstructedGoalState>) => Array.from(m.values()).map(goalStateToJson);
333
+ return {
334
+ version: cp.version,
335
+ format: cp.format,
336
+ createdAt: cp.createdAt,
337
+ coveredBytes: cp.coveredBytes,
338
+ coveredEvents: cp.coveredEvents,
339
+ acc: {
340
+ goals: mapToJson(cp.acc.goals),
341
+ terminalGoals: mapToJson(cp.acc.terminalGoals),
342
+ focusedGoalId: cp.acc.focusedGoalId,
343
+ focusGeneration: cp.acc.focusGeneration,
344
+ focusGenByGoal: Array.from(cp.acc.focusGenByGoal.entries()),
345
+ },
346
+ recentEventsByGoal: Array.from(cp.recentEventsByGoal.entries()),
347
+ runtimeIndex: Array.from(cp.runtimeIndex, ([id, entry]) => [id, { ...entry, oracle: Array.from(entry.oracle) }]),
348
+ };
349
+ }
350
+
351
+ function parseGoalState(value: unknown): ReconstructedGoalState | null {
352
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
353
+ const o = value as Record<string, unknown>;
354
+ if (typeof o.goalId !== "string") return null;
355
+ const latestStatus = o.latestStatus;
356
+ if (latestStatus !== "active" && latestStatus !== "paused" && latestStatus !== "complete" && latestStatus !== "aborted" && latestStatus !== "unknown") return null;
357
+ const auditor = o.latestAuditorResult as Record<string, unknown> | undefined;
358
+ if (auditor !== undefined) {
359
+ if (auditor.verdict !== "approved" && auditor.verdict !== "disapproved" && auditor.verdict !== "error") return null;
360
+ if (typeof auditor.report !== "string" || typeof auditor.at !== "string") return null;
361
+ }
362
+ const oracle = o.latestOracleResult as Record<string, unknown> | undefined;
363
+ if (oracle !== undefined && (!oracle || typeof oracle.fingerprint !== "string" || typeof oracle.adviceId !== "string" || typeof oracle.summary !== "string" || typeof oracle.at !== "string"
364
+ || !["actionable", "needs_human", "insufficient_context"].includes(oracle.disposition as string))) return null;
365
+ return {
366
+ goalId: o.goalId,
367
+ latestStatus,
368
+ latestFocus: o.latestFocus === true,
369
+ latestAuditId: typeof o.latestAuditId === "string" ? o.latestAuditId : undefined,
370
+ latestOracleResult: oracle as ReconstructedGoalState["latestOracleResult"],
371
+ latestPauseReason: typeof o.latestPauseReason === "string" ? o.latestPauseReason : undefined,
372
+ latestPauseSuggestedAction: typeof o.latestPauseSuggestedAction === "string" ? o.latestPauseSuggestedAction : undefined,
373
+ latestAuditorResult: auditor
374
+ ? { verdict: auditor.verdict as "approved" | "disapproved" | "error", report: auditor.report as string, at: auditor.at as string }
375
+ : undefined,
376
+ createdAt: typeof o.createdAt === "string" ? o.createdAt : undefined,
377
+ completedAt: typeof o.completedAt === "string" ? o.completedAt : undefined,
378
+ abortedAt: typeof o.abortedAt === "string" ? o.abortedAt : undefined,
379
+ tweakedAt: typeof o.tweakedAt === "string" ? o.tweakedAt : undefined,
380
+ resumedAt: typeof o.resumedAt === "string" ? o.resumedAt : undefined,
381
+ };
382
+ }
383
+
384
+ /** Parse + validate a checkpoint file body; null on any mismatch (version/format/shape). */
385
+ function checkpointFromJson(value: unknown): LedgerCheckpoint | null {
386
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
387
+ const o = value as Record<string, unknown>;
388
+ if (o.version !== LEDGER_CHECKPOINT_VERSION) return null;
389
+ if (o.format !== "goal-ledger-checkpoint") return null;
390
+ if (typeof o.coveredBytes !== "number" || !Number.isSafeInteger(o.coveredBytes) || o.coveredBytes < 0) return null;
391
+ if (typeof o.coveredEvents !== "number" || !Number.isSafeInteger(o.coveredEvents) || o.coveredEvents < 0) return null;
392
+ const accRaw = o.acc as Record<string, unknown> | undefined;
393
+ if (!accRaw || typeof accRaw.focusGeneration !== "number" || !Number.isSafeInteger(accRaw.focusGeneration) || !Array.isArray(accRaw.goals) || !Array.isArray(accRaw.terminalGoals) || !Array.isArray(accRaw.focusGenByGoal) || !Array.isArray(o.recentEventsByGoal)) return null;
394
+ const goals = new Map<string, ReconstructedGoalState>();
395
+ for (const raw of Array.isArray(accRaw.goals) ? accRaw.goals : []) {
396
+ const s = parseGoalState(raw);
397
+ if (!s) return null;
398
+ goals.set(s.goalId, s);
399
+ }
400
+ const terminalGoals = new Map<string, ReconstructedGoalState>();
401
+ for (const raw of Array.isArray(accRaw.terminalGoals) ? accRaw.terminalGoals : []) {
402
+ const s = parseGoalState(raw);
403
+ if (!s) return null;
404
+ terminalGoals.set(s.goalId, s);
405
+ }
406
+ const focusGenByGoal = new Map<string, number>();
407
+ for (const [gid, gen] of Array.isArray(accRaw.focusGenByGoal) ? accRaw.focusGenByGoal : []) {
408
+ if (typeof gid === "string" && typeof gen === "number") focusGenByGoal.set(gid, gen);
409
+ }
410
+ const recentEventsByGoal = new Map<string, GoalLedgerEvent[]>();
411
+ for (const [gid, evs] of Array.isArray(o.recentEventsByGoal) ? o.recentEventsByGoal : []) {
412
+ if (typeof gid !== "string" || !Array.isArray(evs)) continue;
413
+ const clean: GoalLedgerEvent[] = [];
414
+ for (const ev of evs) {
415
+ if (isValidLedgerEvent(ev)) clean.push(sanitizeEvent(ev));
416
+ }
417
+ if (clean.length > 0) recentEventsByGoal.set(gid, clean);
418
+ }
419
+ if (!Array.isArray(o.runtimeIndex)) return null;
420
+ const runtimeIndex = new Map<string, GoalLedgerIndex>();
421
+ for (const pair of o.runtimeIndex) {
422
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") return null;
423
+ const entry = pair[1];
424
+ if (!entry || !Array.isArray(entry.recent) || !Array.isArray(entry.activity) || !Array.isArray(entry.oracle)) return null;
425
+ if (entry.recent.length > 12 || entry.activity.length > 64) return null;
426
+ if (entry.activity.length > 0 && !entry.lastActivityEvent) return null;
427
+ if (![...entry.recent, ...entry.activity, ...[entry.audit, entry.auditStarted, entry.settledAudit, entry.completion, entry.lifecycle, entry.lastActivityEvent].filter(Boolean)].every(isValidLedgerEvent)) return null;
428
+ if (!entry.oracle.every((p: unknown[]) => {
429
+ if (!Array.isArray(p) || p.length !== 2 || typeof p[0] !== "string" || !p[1] || typeof p[1] !== "object") return false;
430
+ const state = p[1] as Record<string, unknown>;
431
+ if (!Number.isSafeInteger(state.failedAttempts) || (state.failedAttempts as number) < 0 || typeof state.followupAttempted !== "boolean") return false;
432
+ const result = state.result as Record<string, unknown> | undefined;
433
+ if (result !== undefined && (!result || typeof result.adviceId !== "string" || typeof result.summary !== "string" || !["actionable", "needs_human", "insufficient_context"].includes(result.disposition as string))) return false;
434
+ const failure = state.lastFailure as Record<string, unknown> | undefined;
435
+ return failure === undefined || Boolean(failure && typeof failure.errorCode === "string" && typeof failure.message === "string");
436
+ })) return null;
437
+ runtimeIndex.set(pair[0], { ...entry, oracle: new Map(entry.oracle) });
438
+ }
439
+ return {
440
+ runtimeIndex,
441
+ version: LEDGER_CHECKPOINT_VERSION,
442
+ format: "goal-ledger-checkpoint",
443
+ createdAt: typeof o.createdAt === "string" ? o.createdAt : "",
444
+ coveredBytes: o.coveredBytes,
445
+ coveredEvents: o.coveredEvents,
446
+ acc: {
447
+ goals,
448
+ terminalGoals,
449
+ focusedGoalId: typeof accRaw.focusedGoalId === "string" ? accRaw.focusedGoalId : null,
450
+ focusGeneration: accRaw.focusGeneration,
451
+ focusGenByGoal,
452
+ },
453
+ recentEventsByGoal,
454
+ };
455
+ }
456
+
457
+ /** Atomic checkpoint write: temp file + rename in the ledger directory. Best-effort. */
458
+ function writeLedgerCheckpointAtomic(filePath: string, cp: LedgerCheckpoint): void {
459
+ const tmp = `${checkpointPathFor(filePath)}.tmp-${process.pid}`;
460
+ let written = false;
461
+ try {
462
+ const cpPath = checkpointPathFor(filePath);
463
+ fs.writeFileSync(tmp, JSON.stringify(checkpointToJson(cp)), "utf8");
464
+ written = true;
465
+ renameAtomicSync(tmp, cpPath);
466
+ } catch (err) {
467
+ if (written) { try { fs.unlinkSync(tmp); } catch { /* best effort */ } }
468
+ // The checkpoint is an optimization; a failed write only costs a full
469
+ // parse on the next cold read.
470
+ console.error("[goal-ledger] checkpoint write failed:", err);
471
+ }
472
+ }
473
+
474
+ /** Read + validate the checkpoint for a ledger path (cached per process). */
475
+ function readLedgerCheckpointFile(filePath: string): LedgerCheckpoint | null {
476
+ const cached = checkpointCache.get(filePath);
477
+ if (cached !== undefined) return cached;
478
+ let cp: LedgerCheckpoint | null = null;
479
+ try {
480
+ const raw = fs.readFileSync(checkpointPathFor(filePath), "utf8");
481
+ cp = checkpointFromJson(JSON.parse(raw));
482
+ } catch {
483
+ cp = null;
484
+ }
485
+ checkpointCache.set(filePath, cp);
486
+ return cp;
487
+ }
488
+
489
+ /** Append one event to a per-goal recent tail, keeping the newest cap entries. */
490
+ function appendRecent(recent: Map<string, GoalLedgerEvent[]>, goalId: string, event: GoalLedgerEvent): void {
491
+ const tail = recent.get(goalId) ?? [];
492
+ tail.push(event);
493
+ if (tail.length > CHECKPOINT_RECENT_CAP) tail.splice(0, tail.length - CHECKPOINT_RECENT_CAP);
494
+ recent.set(goalId, tail);
495
+ }
496
+
497
+ /** Build a checkpoint from full events (used to bootstrap / refresh). */
498
+ function buildCheckpointFromEvents(events: GoalLedgerEvent[], coveredBytes: number): LedgerCheckpoint {
499
+ const runtimeIndex = buildLedgerIndex(events);
500
+ return {
501
+ version: LEDGER_CHECKPOINT_VERSION,
502
+ format: "goal-ledger-checkpoint",
503
+ createdAt: nowIso(),
504
+ coveredBytes,
505
+ coveredEvents: events.length,
506
+ acc: applyLedgerEvents(freshAccumulator(), events),
507
+ recentEventsByGoal: new Map(Array.from(runtimeIndex, ([id, entry]) => [id, [...entry.recent]])),
508
+ runtimeIndex,
509
+ };
510
+ }
511
+
512
+ /** Positioned read of the ledger bytes past `offset`, parsed as JSONL. */
513
+ function readLedgerTailFrom(filePath: string, offset: number): GoalLedgerReadResult {
514
+ try {
515
+ const fd = fs.openSync(filePath, "r");
516
+ try {
517
+ const size = fs.fstatSync(fd).size;
518
+ const len = Math.max(0, size - offset);
519
+ const buf = Buffer.alloc(len);
520
+ if (len > 0) fs.readSync(fd, buf, 0, len, offset);
521
+ return parseLedgerLines(buf.toString("utf8"));
522
+ } finally {
523
+ fs.closeSync(fd);
524
+ }
525
+ } catch {
526
+ return { events: [], malformed: 0 };
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Maintain the checkpoint after an in-process append (best-effort).
532
+ *
533
+ * Requires an existing checkpoint or a warm full-event cache to extend the
534
+ * accumulator incrementally; otherwise the checkpoint is deferred to the next
535
+ * cold load (bootstrap). Failures are swallowed — the ledger append itself is
536
+ * authoritative and never rolled back.
537
+ */
538
+ function updateLedgerCheckpointAfterAppend(filePath: string, lines: string, events: GoalLedgerEvent[]): void {
539
+ try {
540
+ let cp = checkpointCache.get(filePath);
541
+ if (cp === undefined) cp = readLedgerCheckpointFile(filePath);
542
+ if (!cp) {
543
+ const cached = ledgerCache.get(filePath);
544
+ if (!cached) return;
545
+ cp = buildCheckpointFromEvents(cached.events, cached.size);
546
+ checkpointCache.set(filePath, cp);
547
+ writeLedgerCheckpointAtomic(filePath, cp);
548
+ lastCheckpointDiskWrite.set(filePath, { appendsSinceWrite: cp.coveredEvents, at: Date.now() });
549
+ return;
550
+ }
551
+ for (const event of events) { applyLedgerEvent(cp.acc, event); indexLedgerEvent(cp.runtimeIndex, event); }
552
+ cp.coveredBytes += Buffer.byteLength(lines, "utf8");
553
+ cp.coveredEvents += events.length;
554
+ for (const event of events) {
555
+ const goalId = goalIdOf(event);
556
+ if (goalId) appendRecent(cp.recentEventsByGoal, goalId, event);
557
+ }
558
+ cp.createdAt = nowIso();
559
+ checkpointCache.set(filePath, cp);
560
+ const last = lastCheckpointDiskWrite.get(filePath) ?? { appendsSinceWrite: 0, at: 0 };
561
+ const sinceWrite = cp.coveredEvents - last.appendsSinceWrite;
562
+ if (sinceWrite >= CHECKPOINT_WRITE_INTERVAL || Date.now() - last.at >= CHECKPOINT_WRITE_MIN_MS) {
563
+ writeLedgerCheckpointAtomic(filePath, cp);
564
+ lastCheckpointDiskWrite.set(filePath, { appendsSinceWrite: cp.coveredEvents, at: Date.now() });
565
+ }
566
+ } catch {
567
+ // Best-effort only.
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Bounded cold read of the ledger's reconstructed state (reliability campaign).
573
+ *
574
+ * Serves in this priority order:
575
+ * - warm cache: zero fs ops, same semantics as today's consumers;
576
+ * - fresh checkpoint: 2 fs ops (checkpoint read + stat);
577
+ * - checkpoint + grown ledger: positioned tail read + incremental replay;
578
+ * - no/valid-but-stale-beyond-use checkpoint: full parse + reconstruct, then
579
+ * writes a fresh checkpoint so the next session is bounded.
580
+ */
581
+ /** Current bounded runtime projections, initialized once from checkpoint + tail. */
582
+ function runtimeIndex(ctx: GoalLedgerContext): Map<string, GoalLedgerIndex> {
583
+ const filePath = goalLedgerPath(ctx);
584
+ if (!runtimeReady.has(filePath)) {
585
+ loadLedgerState(ctx);
586
+ const cp = checkpointCache.get(filePath);
587
+ if (!cp) {
588
+ const full = readGoalLedger(ctx);
589
+ checkpointCache.set(filePath, buildCheckpointFromEvents(full.events, ledgerCache.get(filePath)?.size ?? 0));
590
+ }
591
+ // Hot consumers do not retain a second full-history representation.
592
+ ledgerCache.delete(filePath);
593
+ runtimeReady.add(filePath);
594
+ }
595
+ return checkpointCache.get(filePath)!.runtimeIndex;
596
+ }
597
+ const runtimeReady = new Set<string>();
598
+ export function goalActivityEvents(ctx: GoalLedgerContext, goalId: string): GoalLedgerEvent[] {
599
+ return indexedActivityEvents(runtimeIndex(ctx).get(goalId)?.activity ?? []);
600
+ }
601
+ export function goalRuntimeEvents(ctx: GoalLedgerContext, goalId: string): GoalLedgerEvent[] {
602
+ const entry = runtimeIndex(ctx).get(goalId);
603
+ if (!entry) return [];
604
+ const recent = entry.recent;
605
+ const same = (a: GoalLedgerEvent | undefined, b: GoalLedgerEvent | undefined) => a === b || !!a && !!b && a.type === b.type && a.at === b.at && JSON.stringify(a) === JSON.stringify(b);
606
+ const prior = same(entry.settledAudit, entry.audit) ? undefined : entry.settledAudit;
607
+ const pinned = [prior, entry.completion, entry.auditStarted, entry.audit];
608
+ return [...pinned.filter((event, index): event is GoalLedgerEvent => !!event
609
+ && !recent.some(other => same(event, other))
610
+ && !pinned.slice(0, index).some(other => same(event, other))), ...recent];
611
+ }
612
+ export function goalOracleState(ctx: GoalLedgerContext, goalId: string, fingerprint: string) {
613
+ const state = runtimeIndex(ctx).get(goalId)?.oracle.get(fingerprint);
614
+ return state ? { ...state } : { failedAttempts: 0, followupAttempted: false };
615
+ }
616
+
617
+ export function loadLedgerState(ctx: GoalLedgerContext): LedgerStateReadResult {
618
+ const filePath = goalLedgerPath(ctx);
619
+ const cached = ledgerCache.get(filePath);
620
+ if (cached) {
621
+ let cp = checkpointCache.get(filePath);
622
+ if (!cp || cp.coveredBytes !== cached.size || cp.coveredEvents !== cached.events.length) {
623
+ cp = buildCheckpointFromEvents(cached.events, cached.size);
624
+ checkpointCache.set(filePath, cp);
625
+ }
626
+ return {
627
+ state: finalizeLedgerState(cloneAccumulator(cp.acc)),
628
+ recentEventsByGoal: new Map(cp.recentEventsByGoal),
629
+ malformed: cached.malformed,
630
+ coveredBytes: cached.size,
631
+ coveredEvents: cached.events.length,
632
+ source: "cache",
633
+ };
634
+ }
635
+ const cp = readLedgerCheckpointFile(filePath);
636
+ let size = 0;
637
+ try {
638
+ size = fs.statSync(filePath).size;
639
+ } catch {
640
+ size = 0;
641
+ }
642
+ if (cp && cp.coveredBytes === size) {
643
+ return {
644
+ state: finalizeLedgerState(cloneAccumulator(cp.acc)),
645
+ recentEventsByGoal: new Map(cp.recentEventsByGoal),
646
+ malformed: 0,
647
+ coveredBytes: cp.coveredBytes,
648
+ coveredEvents: cp.coveredEvents,
649
+ source: "checkpoint",
650
+ };
651
+ }
652
+ if (cp && cp.coveredBytes < size) {
653
+ const tail = readLedgerTailFrom(filePath, cp.coveredBytes);
654
+ const acc = cloneAccumulator(cp.acc);
655
+ const recent = new Map(cp.recentEventsByGoal);
656
+ for (const event of tail.events) {
657
+ applyLedgerEvent(acc, event);
658
+ indexLedgerEvent(cp.runtimeIndex, event);
659
+ const goalId = goalIdOf(event);
660
+ if (goalId) appendRecent(recent, goalId, event);
661
+ }
662
+ const state = finalizeLedgerState(acc);
663
+ const updated: LedgerCheckpoint = {
664
+ ...cp,
665
+ acc,
666
+ recentEventsByGoal: recent,
667
+ coveredBytes: size,
668
+ coveredEvents: cp.coveredEvents + tail.events.length,
669
+ createdAt: nowIso(),
670
+ };
671
+ checkpointCache.set(filePath, updated);
672
+ writeLedgerCheckpointAtomic(filePath, updated);
673
+ lastCheckpointDiskWrite.set(filePath, { appendsSinceWrite: updated.coveredEvents, at: Date.now() });
674
+ return { state, recentEventsByGoal: recent, malformed: tail.malformed, coveredBytes: size, coveredEvents: updated.coveredEvents, source: "tail" };
675
+ }
676
+ // Full fallback: missing/corrupt checkpoint, version mismatch, or the ledger
677
+ // was rewritten to be smaller than the checkpoint's coverage.
678
+ const full = readGoalLedgerCold(ctx, filePath);
679
+ const fresh = buildCheckpointFromEvents(full.events, size);
680
+ checkpointCache.set(filePath, fresh);
681
+ writeLedgerCheckpointAtomic(filePath, fresh);
682
+ lastCheckpointDiskWrite.set(filePath, { appendsSinceWrite: fresh.coveredEvents, at: Date.now() });
683
+ return {
684
+ state: finalizeLedgerState(cloneAccumulator(fresh.acc)),
685
+ recentEventsByGoal: new Map(fresh.recentEventsByGoal),
686
+ malformed: full.malformed,
687
+ coveredBytes: size,
688
+ coveredEvents: full.events.length,
689
+ source: "full",
690
+ };
691
+ }
692
+
693
+
694
+ function isValidLedgerEvent(value: unknown): value is GoalLedgerEvent {
695
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
696
+ const obj = value as Record<string, unknown>;
697
+ if (typeof obj.type !== "string") return false;
698
+ if (typeof obj.at !== "string") return false;
699
+ const type = obj.type as GoalLedgerEvent["type"];
700
+ if ((type === "audit_started" || type === "audit_result" || type === "audit_skipped") && obj.auditId !== undefined && (typeof obj.auditId !== "string" || !obj.auditId)) return false;
701
+ switch (type) {
702
+ case "goal_created":
703
+ return typeof obj.goalId === "string" && typeof obj.objective === "string" && typeof obj.sisyphus === "boolean" && typeof obj.autoContinue === "boolean";
704
+ case "goal_focused":
705
+ return typeof obj.goalId === "string" && typeof obj.reason === "string";
706
+ case "goal_unfocused":
707
+ return typeof obj.reason === "string";
708
+ case "goal_paused":
709
+ return typeof obj.goalId === "string" && typeof obj.reason === "string" && (obj.suggestedAction === undefined || typeof obj.suggestedAction === "string") && (obj.status === undefined || obj.status === "paused") && (obj.source === undefined || obj.source === "user" || obj.source === "agent");
710
+ case "goal_resumed":
711
+ return typeof obj.goalId === "string" && typeof obj.reason === "string";
712
+ case "goal_tweaked":
713
+ return typeof obj.goalId === "string" && typeof obj.changeSummary === "string";
714
+ case "auditor_toggled":
715
+ return typeof obj.goalId === "string" && typeof obj.enabled === "boolean";
716
+ case "completion_requested":
717
+ return typeof obj.goalId === "string" && (obj.summary === undefined || typeof obj.summary === "string");
718
+ case "audit_started":
719
+ return typeof obj.goalId === "string" && (obj.provider === undefined || typeof obj.provider === "string") && (obj.model === undefined || typeof obj.model === "string") && (obj.thinkingLevel === undefined || typeof obj.thinkingLevel === "string");
720
+ case "audit_result":
721
+ return typeof obj.goalId === "string" && (obj.verdict === "approved" || obj.verdict === "disapproved" || obj.verdict === "error") && typeof obj.report === "string";
722
+ case "audit_skipped":
723
+ return typeof obj.goalId === "string" && (obj.reason === "disabled" || obj.reason === "user_aborted") && (obj.provider === undefined || typeof obj.provider === "string") && (obj.model === undefined || typeof obj.model === "string") && (obj.thinkingLevel === undefined || typeof obj.thinkingLevel === "string");
724
+ case "goal_completed":
725
+ return typeof obj.goalId === "string" && (obj.archivePath === undefined || typeof obj.archivePath === "string");
726
+ case "goal_archived":
727
+ return typeof obj.goalId === "string" && typeof obj.archivePath === "string";
728
+ case "goal_archive_failed":
729
+ return typeof obj.goalId === "string" && typeof obj.message === "string";
730
+ case "goal_aborted":
731
+ return typeof obj.goalId === "string" && typeof obj.reason === "string" && (obj.archivePath === undefined || typeof obj.archivePath === "string");
732
+ case "task_list_set":
733
+ return typeof obj.goalId === "string" && typeof obj.taskCount === "number" && typeof obj.blockCompletion === "boolean";
734
+ case "task_complete":
735
+ return typeof obj.goalId === "string" && typeof obj.taskId === "string" && (obj.evidence === undefined || typeof obj.evidence === "string");
736
+ case "task_skipped":
737
+ return typeof obj.goalId === "string" && typeof obj.taskId === "string" && typeof obj.reason === "string";
738
+ case "task_reopened":
739
+ return typeof obj.goalId === "string" && typeof obj.taskId === "string";
740
+ case "task_started":
741
+ return typeof obj.goalId === "string" && typeof obj.taskId === "string";
742
+ case "goal_budget_changed":
743
+ return typeof obj.goalId === "string" && (obj.oldBudget === null || (Number.isSafeInteger(obj.oldBudget) && Number(obj.oldBudget) > 0)) && (obj.newBudget === null || (Number.isSafeInteger(obj.newBudget) && Number(obj.newBudget) > 0)) && typeof obj.tokensUsed === "number";
744
+ case "goal_budget_limited":
745
+ return typeof obj.goalId === "string" && typeof obj.budget === "number" && typeof obj.tokensUsed === "number";
746
+ case "goal_budget_warning":
747
+ return typeof obj.goalId === "string" && typeof obj.budget === "number" && typeof obj.tokensUsed === "number" && typeof obj.pct === "number";
748
+ case "goal_stalled":
749
+ return typeof obj.goalId === "string" && typeof obj.reason === "string";
750
+ case "goal_blocked":
751
+ return typeof obj.goalId === "string" && typeof obj.reason === "string" && (obj.source === "agent" || obj.source === "system");
752
+ case "oracle_started":
753
+ return typeof obj.goalId === "string" && typeof obj.fingerprint === "string" && typeof obj.provider === "string" && typeof obj.model === "string" && typeof obj.reason === "string" && (obj.thinkingLevel === undefined || typeof obj.thinkingLevel === "string");
754
+ case "oracle_result":
755
+ return typeof obj.goalId === "string" && typeof obj.fingerprint === "string" && typeof obj.adviceId === "string" && (obj.disposition === "actionable" || obj.disposition === "needs_human" || obj.disposition === "insufficient_context") && typeof obj.summary === "string";
756
+ case "oracle_failed":
757
+ return typeof obj.goalId === "string" && typeof obj.fingerprint === "string" && typeof obj.attempt === "number" && (obj.errorCode === "config" || obj.errorCode === "provider" || obj.errorCode === "aborted" || obj.errorCode === "invalid_output") && typeof obj.message === "string";
758
+ case "oracle_followup_attempted":
759
+ return typeof obj.goalId === "string" && typeof obj.fingerprint === "string" && typeof obj.adviceId === "string" && typeof obj.firstToolName === "string";
760
+ default:
761
+ return false;
762
+ }
763
+ }
764
+
765
+ function sanitizeEvent(event: GoalLedgerEvent, owned = false): GoalLedgerEvent {
766
+ if (!("goalId" in event)) return event;
767
+ const goalId = safeGoalId(event.goalId);
768
+ // Parsed JSON is privately owned; avoid allocating a second object per line.
769
+ // Caller-supplied append events still get a defensive copy.
770
+ if (owned) { event.goalId = goalId; return event; }
771
+ return { ...event, goalId };
772
+ }
773
+
774
+ export function reconstructGoalLedger(events: GoalLedgerEvent[]): ReconstructedLedgerState {
775
+ // Inline accumulator allocation (no helper call on the measured path).
776
+ return finalizeLedgerState(applyLedgerEvents({
777
+ goals: new Map(),
778
+ terminalGoals: new Map(),
779
+ focusedGoalId: null,
780
+ focusGeneration: 0,
781
+ focusGenByGoal: new Map(),
782
+ }, events));
783
+ }
784
+
785
+ /**
786
+ * Incremental reconstruction accumulator (checkpoint support).
787
+ *
788
+ * reconstructGoalLedger(events) === finalizeLedgerState(applyLedgerEvents(fresh(), events)),
789
+ * so a checkpoint can store the accumulator mid-stream and replay only the
790
+ * ledger tail by applying the remaining events, then finalizing once.
791
+ */
792
+ export interface ReconstructAccumulator {
793
+ goals: Map<string, ReconstructedGoalState>;
794
+ terminalGoals: Map<string, ReconstructedGoalState>;
795
+ focusedGoalId: string | null;
796
+ focusGeneration: number;
797
+ focusGenByGoal: Map<string, number>;
798
+ }
799
+
800
+ export function freshAccumulator(): ReconstructAccumulator {
801
+ return {
802
+ goals: new Map(),
803
+ terminalGoals: new Map(),
804
+ focusedGoalId: null,
805
+ focusGeneration: 0,
806
+ focusGenByGoal: new Map(),
807
+ };
808
+ }
809
+
810
+ function applyLedgerEvents(acc: ReconstructAccumulator, events: GoalLedgerEvent[]): ReconstructAccumulator {
811
+ // Locals instead of property chains: B3.reconstruct is measured over 10k
812
+ // event lists and must hold the NAF 10x headroom (0.3ms at 5k).
813
+ const goals = acc.goals;
814
+ const terminalGoals = acc.terminalGoals;
815
+ const focusGenByGoal = acc.focusGenByGoal;
816
+ let focusedGoalId = acc.focusedGoalId;
817
+ let focusGeneration = acc.focusGeneration;
818
+ for (const event of events) {
819
+ switch (event.type) {
820
+ case "goal_created": {
821
+ const state: ReconstructedGoalState = {
822
+ goalId: event.goalId,
823
+ latestStatus: "active",
824
+ latestFocus: false,
825
+ createdAt: event.at,
826
+ };
827
+ goals.set(event.goalId, state);
828
+ break;
829
+ }
830
+ case "goal_focused": {
831
+ focusedGoalId = event.goalId;
832
+ focusGeneration++;
833
+ const state = goals.get(event.goalId) ?? terminalGoals.get(event.goalId);
834
+ if (state) focusGenByGoal.set(event.goalId, focusGeneration);
835
+ break;
836
+ }
837
+ case "goal_unfocused": {
838
+ focusedGoalId = null;
839
+ focusGeneration++;
840
+ break;
841
+ }
842
+ case "goal_paused": {
843
+ const state = goals.get(event.goalId);
844
+ if (state) {
845
+ state.latestStatus = event.status ?? "paused";
846
+ state.latestPauseReason = event.reason;
847
+ state.latestPauseSuggestedAction = event.suggestedAction;
848
+ }
849
+ break;
850
+ }
851
+ case "goal_resumed": {
852
+ const state = goals.get(event.goalId);
853
+ if (state) {
854
+ state.latestStatus = "active";
855
+ state.resumedAt = event.at;
856
+ delete state.latestPauseReason;
857
+ delete state.latestPauseSuggestedAction;
858
+ }
859
+ break;
860
+ }
861
+ case "goal_tweaked": {
862
+ const state = goals.get(event.goalId);
863
+ if (state) state.tweakedAt = event.at;
864
+ break;
865
+ }
866
+ case "completion_requested": {
867
+ // No status change until audit_result or goal_completed
868
+ break;
869
+ }
870
+ case "audit_started":
871
+ case "audit_skipped":
872
+ case "audit_result": {
873
+ const state = goals.get(event.goalId) ?? terminalGoals.get(event.goalId);
874
+ if (state && auditApplies(state.latestAuditId, event)) {
875
+ state.latestAuditId = event.auditId;
876
+ state.latestAuditorResult = event.type === "audit_result"
877
+ ? {verdict: event.verdict, report: event.report, at: event.at} : undefined;
878
+ }
879
+ break;
880
+ }
881
+ case "goal_completed": {
882
+ let state = goals.get(event.goalId);
883
+ if (!state) {
884
+ state = { goalId: event.goalId, latestStatus: "complete", latestFocus: false };
885
+ }
886
+ state.latestStatus = "complete";
887
+ state.completedAt = event.at;
888
+ terminalGoals.set(event.goalId, state);
889
+ goals.delete(event.goalId);
890
+ break;
891
+ }
892
+ case "goal_aborted": {
893
+ let state = goals.get(event.goalId);
894
+ if (!state) {
895
+ state = { goalId: event.goalId, latestStatus: "aborted", latestFocus: false };
896
+ }
897
+ state.latestStatus = "aborted";
898
+ state.abortedAt = event.at;
899
+ terminalGoals.set(event.goalId, state);
900
+ goals.delete(event.goalId);
901
+ break;
902
+ }
903
+ case "oracle_started":
904
+ case "oracle_failed":
905
+ case "oracle_followup_attempted": {
906
+ // Bounded metadata only; no reconstructed state change.
907
+ break;
908
+ }
909
+ case "oracle_result": {
910
+ const oracleState = goals.get(event.goalId) ?? terminalGoals.get(event.goalId);
911
+ if (oracleState) {
912
+ oracleState.latestOracleResult = {
913
+ fingerprint: event.fingerprint,
914
+ adviceId: event.adviceId,
915
+ disposition: event.disposition,
916
+ summary: event.summary,
917
+ at: event.at,
918
+ };
919
+ }
920
+ break;
921
+ }
922
+ }
923
+ }
924
+ acc.focusedGoalId = focusedGoalId;
925
+ acc.focusGeneration = focusGeneration;
926
+ return acc;
927
+ }
928
+
929
+ /** Apply one ledger event (checkpoint incremental paths; not on the measured hot path). */
930
+ function applyLedgerEvent(acc: ReconstructAccumulator, event: GoalLedgerEvent): void {
931
+ applyLedgerEvents(acc, [event]);
932
+ }
933
+
934
+ /** Materialize focus flags once (O(goals)), clearing focus on terminal goals. */
935
+ function finalizeLedgerState(acc: ReconstructAccumulator): ReconstructedLedgerState {
936
+ const { goals, terminalGoals, focusGenByGoal } = acc;
937
+ const focusGeneration = acc.focusGeneration;
938
+ // Materialize the generation-based focus flags (O(goals) once, not per event).
939
+ for (const g of goals.values()) g.latestFocus = focusGenByGoal.get(g.goalId) === focusGeneration;
940
+ for (const g of terminalGoals.values()) g.latestFocus = focusGenByGoal.get(g.goalId) === focusGeneration;
941
+
942
+ // If the focused goal was moved to terminal (e.g., aborted/completed), clear focus.
943
+ if (acc.focusedGoalId && !goals.has(acc.focusedGoalId)) {
944
+ acc.focusedGoalId = null;
945
+ }
946
+
947
+ return { focusedGoalId: acc.focusedGoalId, goals, terminalGoals };
948
+ }
949
+
950
+ /** Shallow-clone an accumulator so finalizing (or extending) never mutates a cached copy. */
951
+ function cloneAccumulator(acc: ReconstructAccumulator): ReconstructAccumulator {
952
+ return {
953
+ goals: new Map(acc.goals),
954
+ terminalGoals: new Map(acc.terminalGoals),
955
+ focusedGoalId: acc.focusedGoalId,
956
+ focusGeneration: acc.focusGeneration,
957
+ focusGenByGoal: new Map(acc.focusGenByGoal),
958
+ };
959
+ }
960
+
961
+ type AuditEvent = Extract<GoalLedgerEvent, {type: "audit_started" | "audit_result" | "audit_skipped"}>;
962
+
963
+ /** New attempts supersede old verdicts; overlapping attempts match by identity. */
964
+ export function latestAuditForGoal(events: GoalLedgerEvent[], goalId: string): AuditEvent | undefined {
965
+ let latest: AuditEvent | undefined;
966
+ for (const event of events) {
967
+ if ((event.type === "audit_started" || event.type === "audit_result" || event.type === "audit_skipped") && event.goalId === goalId && auditApplies(latest?.auditId, event)) latest = event;
968
+ }
969
+ return latest;
970
+ }
971
+
972
+ export function latestAuditorResultForGoal(events: GoalLedgerEvent[], goalId: string): {verdict: "approved" | "disapproved" | "error"; report: string; at: string} | undefined {
973
+ const latest = latestAuditForGoal(events, goalId);
974
+ return latest?.type === "audit_result" ? {verdict: latest.verdict, report: latest.report, at: latest.at} : undefined;
975
+ }
976
+
977
+ export function latestSettledAuditorResultForGoal(events: GoalLedgerEvent[], goalId: string): { verdict: "approved" | "disapproved" | "error"; report: string; at: string } | undefined {
978
+ for (let i = events.length - 1; i >= 0; i--) {
979
+ const event = events[i]!
980
+ if (event.type === "audit_result" && event.goalId === goalId) {
981
+ return { verdict: event.verdict, report: event.report, at: event.at };
982
+ }
983
+ }
984
+ return undefined;
985
+ }
986
+
987
+ export function latestEventsForGoal(events: GoalLedgerEvent[], goalId: string, limit = 10): GoalLedgerEvent[] {
988
+ const result: GoalLedgerEvent[] = [];
989
+ for (let i = events.length - 1; i >= 0; i--) {
990
+ const event = events[i]!
991
+ if ("goalId" in event && event.goalId === goalId) {
992
+ result.unshift(event);
993
+ if (result.length >= limit) break;
994
+ }
995
+ }
996
+ return result;
997
+ }
998
+
999
+ export function latestGoalLifecycleEvent(events: GoalLedgerEvent[], goalId: string): GoalLedgerEvent | undefined {
1000
+ for (let i = events.length - 1; i >= 0; i--) {
1001
+ const event = events[i]!
1002
+ if ("goalId" in event && event.goalId === goalId) {
1003
+ return event;
1004
+ }
1005
+ }
1006
+ return undefined;
1007
+ }