pi-observational-memory 2.4.3 → 3.0.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 (38) hide show
  1. package/README.md +318 -103
  2. package/package.json +9 -9
  3. package/src/agents/dropper/agent.ts +281 -0
  4. package/src/agents/dropper/coverage.ts +128 -0
  5. package/src/agents/dropper/pool.ts +67 -0
  6. package/src/agents/dropper/prompts.ts +48 -0
  7. package/src/{observer.ts → agents/observer/agent.ts} +11 -13
  8. package/src/agents/observer/prompts.ts +119 -0
  9. package/src/agents/reflector/agent.ts +203 -0
  10. package/src/agents/reflector/prompts.ts +81 -0
  11. package/src/clipboard.ts +63 -0
  12. package/src/commands/status.ts +79 -78
  13. package/src/commands/view.ts +58 -66
  14. package/src/config.ts +78 -55
  15. package/src/debug-log.ts +24 -5
  16. package/src/hooks/compaction-hook.ts +24 -369
  17. package/src/hooks/compaction-trigger.ts +12 -21
  18. package/src/hooks/consolidation-trigger.ts +368 -0
  19. package/src/index.ts +3 -3
  20. package/src/model-budget.ts +1 -1
  21. package/src/runtime.ts +46 -19
  22. package/src/serialize.ts +1 -1
  23. package/src/session-ledger/fold.ts +100 -0
  24. package/src/session-ledger/index.ts +6 -0
  25. package/src/session-ledger/progress.ts +129 -0
  26. package/src/session-ledger/projection.ts +220 -0
  27. package/src/session-ledger/recall.ts +237 -0
  28. package/src/session-ledger/render-summary.ts +31 -0
  29. package/src/session-ledger/types.ts +200 -0
  30. package/src/tokens.ts +1 -1
  31. package/src/tools/recall-observation.ts +84 -214
  32. package/src/branch.ts +0 -577
  33. package/src/compaction.ts +0 -1030
  34. package/src/hooks/observer-trigger.ts +0 -129
  35. package/src/progress.ts +0 -155
  36. package/src/prompts.ts +0 -302
  37. package/src/relevance.ts +0 -15
  38. package/src/types.ts +0 -155
@@ -1,396 +1,51 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Text } from "@mariozechner/pi-tui";
3
- import { debugLog, withDebugLogContext } from "../debug-log.js";
4
- import { resolveTurnLimits } from "../config.js";
5
- import {
6
- collectObservationsByCoverage,
7
- findLastCompactionIndex,
8
- gapRawEntries,
9
- getMemoryState,
10
- } from "../branch.js";
11
- import {
12
- REFLECTOR_MAX_PASSES,
13
- coverageTagCounts,
14
- migrateLegacyReflections,
15
- observationPoolTokens,
16
- renderSummary,
17
- runPruner,
18
- runReflector,
19
- type CoverageTagCounts,
20
- type PrunerResult,
21
- type ReflectorStats,
22
- } from "../compaction.js";
23
- import { observationsToPromptLines, runObserver } from "../observer.js";
24
- import { CompactionProgressTracker } from "../progress.js";
25
- import type { Runtime } from "../runtime.js";
26
- import { serializeSourceAddressedBranchEntries } from "../serialize.js";
27
- import { estimateStringTokens } from "../tokens.js";
28
- import {
29
- OBSERVATION_CUSTOM_TYPE,
30
- reflectionToPromptLine,
31
- type MemoryDetailsV4,
32
- type MemoryReflection,
33
- type ObservationEntryData,
34
- type ObservationRecord,
35
- } from "../types.js";
36
-
37
- function plural(count: number, singular: string, pluralForm = `${singular}s`): string {
38
- return `${count.toLocaleString()} ${count === 1 ? singular : pluralForm}`;
39
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
40
2
 
41
- function formatCoverageCounts(counts: CoverageTagCounts): string {
42
- return `${counts.uncited.toLocaleString()}/${counts.cited.toLocaleString()}/${counts.reinforced.toLocaleString()} uncited/cited/reinforced`;
43
- }
3
+ import type { Runtime } from "../runtime.js";
4
+ import { buildCompactionProjection, renderSummary, type Entry } from "../session-ledger/index.js";
44
5
 
45
- function formatReflectorStats(stats: ReflectorStats): string {
46
- const failed = stats.failedPass === undefined ? "" : `, failed pass ${stats.failedPass}`;
47
- return `reflector ${plural(stats.toolCalls, "tool call")}, +${stats.added.toLocaleString()} added, ${stats.merged.toLocaleString()} merged, ${stats.promoted.toLocaleString()} promoted, ${stats.duplicates.toLocaleString()} duplicate/no-op, ${stats.unsupported.toLocaleString()} unsupported${failed}`;
48
- }
6
+ const DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS = 20_000;
49
7
 
50
- function formatPrunerStats(result: PrunerResult): string {
51
- return `pruner dropped ${plural(result.droppedIds.length, "observation")} in ${plural(result.passes.length, "pass", "passes")}, stop: ${result.stopReason}`;
8
+ function observationsPoolMaxTokens(runtime: Runtime): number {
9
+ const value = (runtime.config as { observationsPoolMaxTokens?: unknown }).observationsPoolMaxTokens;
10
+ return typeof value === "number" && Number.isFinite(value) && value > 0
11
+ ? value
12
+ : DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS;
52
13
  }
53
14
 
54
15
  export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void {
55
- pi.on("session_before_compact", async (event, ctx) => {
16
+ pi.on("session_before_compact", async (event: any, ctx: any) => {
56
17
  if (runtime.compactHookInFlight) {
57
- if (ctx.hasUI) ctx.ui.notify(
58
- "Observational memory: another compaction is already in progress; cancelling duplicate",
59
- "warning",
60
- );
18
+ if (ctx.hasUI) {
19
+ ctx.ui.notify(
20
+ "Observational memory: another compaction is already in progress; cancelling duplicate",
21
+ "warning",
22
+ );
23
+ }
61
24
  return { cancel: true };
62
25
  }
26
+
63
27
  runtime.compactHookInFlight = true;
64
- const progress = new CompactionProgressTracker();
65
- const WIDGET_NAME = "om_compact_progress";
66
- let clearWidget = () => {};
67
28
  try {
68
29
  runtime.ensureConfig(ctx.cwd);
69
- const runId = `compaction-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
70
- return await withDebugLogContext({ enabled: runtime.config.debugLog === true, cwd: ctx.cwd, runId }, async () => {
71
- const { preparation, branchEntries, signal } = event;
30
+ const { preparation, branchEntries } = event;
72
31
  const { firstKeptEntryId, tokensBefore } = preparation;
73
-
74
- // Capture ctx properties synchronously — after multiple awaits below,
75
- // the extension ctx may become stale (e.g. after session replacement/reload).
76
- const hasUI = ctx.hasUI;
77
- const ui = ctx.ui;
78
- const turnLimits = resolveTurnLimits(runtime.config);
79
- debugLog("compaction.start", {
32
+ const projection = buildCompactionProjection(
33
+ branchEntries as Entry[],
80
34
  firstKeptEntryId,
81
- tokensBefore,
82
- branchEntryCount: branchEntries.length,
83
- reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
84
- turnLimits,
85
- legacyCompactionMaxToolCalls: runtime.config.compactionMaxToolCalls,
86
- });
87
-
88
- const resolved = await runtime.resolveModel(ctx as any);
89
- if (!resolved.ok) {
90
- debugLog("compaction.model_unavailable", { reason: resolved.reason });
91
- if (hasUI) ui?.notify(
92
- `Observational memory: cannot compact — ${resolved.reason}. ` +
93
- "Fix the model/API key and try /compact manually.",
94
- "error",
95
- );
96
- return { cancel: true };
97
- }
98
- runtime.resolveFailureNotified = false;
99
-
100
- const updateWidget = () => {
101
- if (!hasUI || !ui) return;
102
- if (!progress.getPhase()) {
103
- ui.setWidget(WIDGET_NAME, undefined);
104
- return;
105
- }
106
- ui.setWidget(WIDGET_NAME, (_tui: any, theme: any) => {
107
- return new Text(
108
- progress.formatWidget(theme),
109
- 0, 0,
110
- );
111
- });
112
- };
113
- clearWidget = () => {
114
- if (hasUI && ui) ui.setWidget(WIDGET_NAME, undefined);
115
- };
116
-
117
- let entries = branchEntries as Parameters<typeof getMemoryState>[0];
118
-
119
- if (runtime.observerPromise) {
120
- try { await runtime.observerPromise; } catch { /* already notified via launchObserverTask */ }
121
- // In-flight observer may have appended a new observation entry during the await;
122
- // refresh from sessionManager so gap computation and coverage collection see it
123
- entries = ctx.sessionManager.getBranch() as typeof entries;
124
- }
125
-
126
- const memoryState = getMemoryState(entries);
127
- debugLog("compaction.memory_state", {
128
- committedObservations: memoryState.committedObs.length,
129
- pendingObservations: memoryState.pendingObs.length,
130
- reflections: memoryState.reflections.length,
131
- });
132
-
133
- let gapObservationData: ObservationEntryData | null = null;
134
- const gap = gapRawEntries(entries, firstKeptEntryId);
135
- if (gap.length > 0) {
136
- const { text: gapChunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(gap);
137
- if (gapChunk.trim() && sourceEntryIds.length > 0) {
138
- const gapFromId = gap[0].id;
139
- const gapUpToId = gap[gap.length - 1].id;
140
- const priorObservationLines = observationsToPromptLines([
141
- ...memoryState.committedObs,
142
- ...memoryState.pendingObs,
143
- ]);
144
- const gapTokenEstimate = estimateStringTokens(gapChunk);
145
- debugLog("compaction.sync_catchup.start", {
146
- gapEntryCount: gap.length,
147
- sourceEntryIds,
148
- gapFromId,
149
- gapUpToId,
150
- tokenEstimate: gapTokenEstimate,
151
- });
152
- if (hasUI) ui?.notify(
153
- `Observational memory: sync catch-up observer running on ~${gapTokenEstimate.toLocaleString()}-token gap`,
154
- "info",
155
- );
156
- progress.setPhase("observer", 1, 1);
157
- updateWidget();
158
- runtime.observerInFlight = true;
159
- const gapCall = runObserver({
160
- model: resolved.model as any,
161
- apiKey: resolved.apiKey,
162
- headers: resolved.headers,
163
- priorReflections: memoryState.reflections.map(reflectionToPromptLine),
164
- priorObservations: priorObservationLines,
165
- chunk: gapChunk,
166
- allowedSourceEntryIds: sourceEntryIds,
167
- signal,
168
- maxTurns: turnLimits.observerMaxTurnsPerRun,
169
- thinkingLevel: runtime.config.thinkingLevel,
170
- });
171
- const gapPromise: Promise<void> = gapCall.then(() => undefined, () => undefined);
172
- runtime.observerPromise = gapPromise;
173
- try {
174
- const records = await gapCall;
175
- if (records && records.length > 0) {
176
- const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
177
- gapObservationData = {
178
- records,
179
- coversFromId: gapFromId,
180
- coversUpToId: gapUpToId,
181
- tokenCount: observationTokens,
182
- };
183
- debugLog("compaction.sync_catchup.records", {
184
- count: records.length,
185
- observationTokens,
186
- coversFromId: gapFromId,
187
- coversUpToId: gapUpToId,
188
- records,
189
- });
190
- pi.appendEntry(OBSERVATION_CUSTOM_TYPE, gapObservationData);
191
- if (hasUI && ui) ui.notify(
192
- `Observational memory: sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens)`,
193
- "info",
194
- );
195
- } else if (hasUI && ui) {
196
- debugLog("compaction.sync_catchup.empty", { gapEntryCount: gap.length });
197
- ui.notify(
198
- "Observational memory: sync catch-up observer returned empty — proceeding with compaction",
199
- "warning",
200
- );
201
- }
202
- } catch (error) {
203
- const msg = error instanceof Error ? error.message : String(error);
204
- debugLog("compaction.sync_catchup.error", { gapEntryCount: gap.length, errorMessage: msg });
205
- if (hasUI && ui) ui.notify(
206
- `Observational memory: sync catch-up observer failed: ${msg}. Cancelling compaction — ${gap.length} unobserved raw entries would be pruned without coverage. Try /compact again.`,
207
- "warning",
208
- );
209
- return { cancel: true };
210
- } finally {
211
- runtime.observerInFlight = false;
212
- if (runtime.observerPromise === gapPromise) runtime.observerPromise = null;
213
- }
214
- }
215
- }
216
-
217
- const priorCompactionIdx = findLastCompactionIndex(entries);
218
- const priorFirstKeptEntryId = priorCompactionIdx >= 0 ? entries[priorCompactionIdx].firstKeptEntryId : undefined;
219
- const deltaObservationData = collectObservationsByCoverage(entries, priorFirstKeptEntryId, firstKeptEntryId);
220
- if (gapObservationData) deltaObservationData.push(gapObservationData);
221
- debugLog("compaction.delta", {
222
- priorFirstKeptEntryId,
223
- firstKeptEntryId,
224
- deltaObservationEntries: deltaObservationData.length,
225
- deltaObservationRecords: deltaObservationData.reduce((sum, data) => sum + data.records.length, 0),
226
- gapObservationRecords: gapObservationData?.records.length ?? 0,
227
- });
228
-
229
- if (deltaObservationData.length === 0) {
230
- // No new observations since last compaction. If we have existing memory,
231
- // carry it forward in a no-op compaction so it survives Pi's compaction.
232
- // If there is truly nothing (no prior memory either), cancel.
233
- if (memoryState.committedObs.length === 0 && memoryState.reflections.length === 0) {
234
- debugLog("compaction.no_delta_cancel", {
235
- committedObservations: memoryState.committedObs.length,
236
- pendingObservations: memoryState.pendingObs.length,
237
- reflections: memoryState.reflections.length,
238
- });
239
- if (hasUI) {
240
- ui?.notify(
241
- `Observational memory: nothing to compact yet — ${plural(memoryState.committedObs.length, "committed observation")} and ${plural(memoryState.pendingObs.length, "pending observation")}; no eligible delta before compact boundary`,
242
- "warning",
243
- );
244
- }
245
- return { cancel: true };
246
- }
247
-
248
- // Carry forward existing memory without running reflector/pruner
249
- const workingReflections: MemoryReflection[] = migrateLegacyReflections(memoryState.reflections);
250
- debugLog("compaction.no_delta_carry_forward", {
251
- observations: memoryState.committedObs.length,
252
- reflections: workingReflections.length,
253
- });
254
- const summary = renderSummary(workingReflections, memoryState.committedObs);
255
- const details: MemoryDetailsV4 = {
256
- type: "observational-memory",
257
- version: 4,
258
- observations: memoryState.committedObs,
259
- reflections: workingReflections,
260
- };
261
- if (hasUI) ui?.notify(
262
- `Observational memory: no new observations — carrying forward ${memoryState.committedObs.length} observation${memoryState.committedObs.length === 1 ? "" : "s"}, ${workingReflections.length} reflection${workingReflections.length === 1 ? "" : "s"}`,
263
- "info",
264
- );
265
- return {
266
- compaction: {
267
- summary,
268
- firstKeptEntryId,
269
- tokensBefore,
270
- details,
271
- },
272
- };
273
- }
274
-
275
- const workingReflections: MemoryReflection[] = migrateLegacyReflections(memoryState.reflections);
276
- const workingObservations: ObservationRecord[] = [
277
- ...memoryState.committedObs,
278
- ...deltaObservationData.flatMap((d) => d.records),
279
- ];
280
-
281
- const observationTokens = observationPoolTokens(workingObservations);
282
- debugLog("compaction.reflect_prune.gate", {
283
- observationTokens,
284
- reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
285
- willRun: observationTokens >= runtime.config.reflectionThresholdTokens,
286
- workingObservations: workingObservations.length,
287
- workingReflections: workingReflections.length,
288
- });
289
-
290
- let finalReflections = workingReflections;
291
- let finalObservations = workingObservations;
292
-
293
- if (observationTokens >= runtime.config.reflectionThresholdTokens) {
294
- try {
295
- debugLog("compaction.reflect_prune.start", {
296
- workingObservations: workingObservations.length,
297
- workingReflections: workingReflections.length,
298
- observationTokens,
299
- });
300
- if (hasUI) ui?.notify("Observational memory: running reflector + pruner...", "info");
301
- progress.setPhase("reflector", 1, REFLECTOR_MAX_PASSES);
302
- progress.setStartingCounts(workingReflections.length, workingObservations.length);
303
- updateWidget();
304
- const coverageBefore = coverageTagCounts(workingReflections, workingObservations);
305
- const reflectorResult = await runReflector(
306
- { model: resolved.model as any, apiKey: resolved.apiKey, headers: resolved.headers, signal, onEvent: (event) => { progress.onEvent(event); updateWidget(); }, maxTurns: turnLimits.reflectorMaxTurnsPerPass, thinkingLevel: runtime.config.thinkingLevel },
307
- workingReflections,
308
- workingObservations,
309
- (pass, max) => { progress.setPhase("reflector", pass, max); updateWidget(); },
310
- );
311
- finalReflections = reflectorResult.reflections;
312
- const coverageAfter = coverageTagCounts(finalReflections, workingObservations);
313
- debugLog("compaction.reflector.result", {
314
- stats: reflectorResult.stats,
315
- coverageBefore,
316
- coverageAfter,
317
- beforeReflections: workingReflections.length,
318
- afterReflections: finalReflections.length,
319
- });
320
-
321
- const prunerResult = await runPruner(
322
- { model: resolved.model as any, apiKey: resolved.apiKey, headers: resolved.headers, signal, onEvent: (event) => { progress.onEvent(event); updateWidget(); }, maxTurns: turnLimits.prunerMaxTurnsPerPass, thinkingLevel: runtime.config.thinkingLevel },
323
- finalReflections,
324
- workingObservations,
325
- runtime.config.reflectionThresholdTokens,
326
- (pass, max) => { progress.setPhase("pruner", pass, max); updateWidget(); },
327
- );
328
- finalObservations = prunerResult.observations;
329
- debugLog("compaction.pruner.result", {
330
- stopReason: prunerResult.stopReason,
331
- fellBack: prunerResult.fellBack,
332
- droppedIds: prunerResult.droppedIds,
333
- passes: prunerResult.passes,
334
- beforeObservations: workingObservations.length,
335
- afterObservations: finalObservations.length,
336
- });
337
- updateWidget();
338
- if (hasUI) {
339
- ui?.notify(
340
- `Observational memory: diagnostics — ${formatReflectorStats(reflectorResult.stats)}; coverage ${formatCoverageCounts(coverageBefore)} → ${formatCoverageCounts(coverageAfter)}; ${formatPrunerStats(prunerResult)}`,
341
- "info",
342
- );
343
- }
344
- if (prunerResult.fellBack && hasUI) {
345
- ui?.notify(
346
- "Observational memory: pruner run failed; kept observation set unchanged",
347
- "warning",
348
- );
349
- }
350
- } catch (error) {
351
- const msg = error instanceof Error ? error.message : String(error);
352
- debugLog("compaction.reflect_prune.error", { errorMessage: msg });
353
- if (hasUI) ui?.notify(`Observational memory: reflect/prune failed: ${msg}`, "warning");
354
- }
355
- }
356
-
357
- const summary = renderSummary(finalReflections, finalObservations);
358
-
359
- if (finalObservations.length === 0) {
360
- throw new Error("invariant violated: finalObservations empty after delta guard");
361
- }
362
-
363
- const details: MemoryDetailsV4 = {
364
- type: "observational-memory",
365
- version: 4,
366
- observations: finalObservations,
367
- reflections: finalReflections,
368
- };
369
- debugLog("compaction.result", {
370
- finalObservations: finalObservations.length,
371
- finalReflections: finalReflections.length,
372
- firstKeptEntryId,
373
- tokensBefore,
374
- });
375
-
376
- if (hasUI) ui?.notify(
377
- `Observational memory: compaction assembled — ${finalObservations.length} observation${finalObservations.length === 1 ? "" : "s"}, ${finalReflections.length} reflection${finalReflections.length === 1 ? "" : "s"}`,
378
- "info",
35
+ { observationsPoolMaxTokens: observationsPoolMaxTokens(runtime) },
379
36
  );
37
+ const summary = renderSummary(projection.reflections, projection.observations);
380
38
 
381
39
  return {
382
40
  compaction: {
383
41
  summary,
384
42
  firstKeptEntryId,
385
43
  tokensBefore,
386
- details,
44
+ details: projection.details,
387
45
  },
388
46
  };
389
- });
390
47
  } finally {
391
48
  runtime.compactHookInFlight = false;
392
- progress.clear();
393
- clearWidget();
394
49
  }
395
50
  });
396
- }
51
+ }
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { rawTokensSinceLastCompaction } from "../branch.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { rawTokensSinceLastCompaction, type Entry } from "../session-ledger/index.js";
3
3
  import type { Runtime } from "../runtime.js";
4
4
 
5
5
  /**
@@ -11,7 +11,7 @@ const RETRYABLE_ERROR_RE =
11
11
  /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
12
12
 
13
13
  export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
14
- pi.on("agent_end", (event, ctx) => {
14
+ pi.on("agent_end", (event: any, ctx: any) => {
15
15
  runtime.ensureConfig(ctx.cwd);
16
16
  if (runtime.config.passive === true) return;
17
17
  if (runtime.compactInFlight) return;
@@ -31,9 +31,9 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
31
31
  return;
32
32
  }
33
33
 
34
- const entries = ctx.sessionManager.getBranch() as Parameters<typeof rawTokensSinceLastCompaction>[0];
34
+ const entries = ctx.sessionManager.getBranch() as Entry[];
35
35
  const tokens = rawTokensSinceLastCompaction(entries);
36
- if (tokens < runtime.config.compactionThresholdTokens) return;
36
+ if (tokens < runtime.config.compactAfterTokens) return;
37
37
 
38
38
  // Capture ctx properties synchronously — the setTimeout + async work below
39
39
  // may outlive the extension ctx (stale after session replacement/reload).
@@ -46,31 +46,22 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
46
46
  );
47
47
 
48
48
  runtime.compactInFlight = true;
49
- setTimeout(async () => {
50
- if (runtime.observerPromise) {
51
- try {
52
- await runtime.observerPromise;
53
- } catch {
54
- // errors already surfaced via launchObserverTask
55
- }
56
- }
57
- // After awaiting observerPromise, ctx may be stale.
58
- // Use captured hasUI/ui for notification; wrap ctx access in try/catch.
49
+ setTimeout(() => {
59
50
  try {
60
51
  if (!ctx.isIdle()) {
61
52
  runtime.compactInFlight = false;
62
53
  if (hasUI) ui?.notify(
63
- "Observational memory: compaction deferred — agent became busy after observer wait",
54
+ "Observational memory: compaction deferred — agent became busy before compaction",
64
55
  "info",
65
56
  );
66
57
  return;
67
58
  }
68
- const currentEntries = ctx.sessionManager.getBranch() as Parameters<typeof rawTokensSinceLastCompaction>[0];
59
+ const currentEntries = ctx.sessionManager.getBranch() as Entry[];
69
60
  const currentTokens = rawTokensSinceLastCompaction(currentEntries);
70
- if (currentTokens < runtime.config.compactionThresholdTokens) {
61
+ if (currentTokens < runtime.config.compactAfterTokens) {
71
62
  runtime.compactInFlight = false;
72
63
  if (hasUI) ui?.notify(
73
- "Observational memory: compaction skipped — another compaction already ran during observer wait",
64
+ "Observational memory: compaction skipped — another compaction already ran before deferred compaction",
74
65
  "info",
75
66
  );
76
67
  return;
@@ -80,7 +71,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
80
71
  runtime.compactInFlight = false;
81
72
  if (hasUI) ui?.notify("Observational memory: compaction complete", "info");
82
73
  },
83
- onError: (error) => {
74
+ onError: (error: { message: string }) => {
84
75
  runtime.compactInFlight = false;
85
76
  if (error.message === "Compaction cancelled") {
86
77
  // We already notified the user with the real reason before returning { cancel: true }.
@@ -96,4 +87,4 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
96
87
  }
97
88
  }, 0);
98
89
  });
99
- }
90
+ }