pi-observational-memory 2.4.1 → 2.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-observational-memory",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "Observational memory extension for pi — cache-friendly tiered compaction with observations and reflections.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,6 +5,7 @@ import {
5
5
  rawTokensSinceLastBound,
6
6
  rawTokensSinceLastCompaction,
7
7
  } from "../branch.js";
8
+ import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
8
9
  import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
9
10
  import type { Runtime } from "../runtime.js";
10
11
  import { estimateStringTokens } from "../tokens.js";
@@ -21,12 +22,12 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
21
22
 
22
23
  const { reflections: committedRefs, committedObs, pendingObs } = getMemoryState(entries);
23
24
  const committedRefItems = committedRefs as MemoryReflection[];
24
- const committedObsTokens = committedObs.reduce((s, r) => s + estimateStringTokens(r.content), 0);
25
+ const committedObsTokens = estimateObservationPoolTokens(committedObs);
25
26
  const committedObsCount = committedObs.length;
26
27
  const committedRefsTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
27
28
  const committedRefsCount = committedRefItems.length;
28
29
 
29
- const pendingObsTokens = pendingObs.reduce((s, r) => s + estimateStringTokens(r.content), 0);
30
+ const pendingObsTokens = estimateObservationPoolTokens(pendingObs);
30
31
  const pendingObsCount = pendingObs.length;
31
32
 
32
33
  const relevanceHistogram = countByRelevance([...committedObs, ...pendingObs]);
@@ -41,7 +42,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
41
42
  // at compaction entry. We over-count by the tail slice and can't predict the gap obs here.
42
43
  // Precise version would simulate the new firstKeptEntryId by walking back keepRecentTokens from
43
44
  // the branch tail and split pending into pre-tail vs tail-covering.
44
- const observationPoolTokens = committedObsTokens + pendingObsTokens;
45
+ const observationPoolTokens = estimateObservationPoolTokens([...committedObs, ...pendingObs]);
45
46
  const obsPct = Math.min(100, Math.round((sinceBound / obsThreshold) * 100));
46
47
  const compPct = Math.min(100, Math.round((sinceCompaction / compThreshold) * 100));
47
48
  const refPct = Math.min(100, Math.round((observationPoolTokens / refThreshold) * 100));
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { getMemoryState } from "../branch.js";
3
+ import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
3
4
  import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
4
5
  import type { Runtime } from "../runtime.js";
5
6
  import { estimateStringTokens } from "../tokens.js";
@@ -17,14 +18,15 @@ export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime): void {
17
18
  const committedRefTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
18
19
  const committedRefCount = committedRefItems.length;
19
20
 
20
- const committedObsTokens = committedObs.reduce((s, r) => s + estimateStringTokens(r.content), 0);
21
+ const committedObsTokens = estimateObservationPoolTokens(committedObs);
21
22
  const committedObsCount = committedObs.length;
22
23
 
23
- const pendingObsTokens = pendingObs.reduce((s, r) => s + estimateStringTokens(r.content), 0);
24
+ const pendingObsTokens = estimateObservationPoolTokens(pendingObs);
24
25
  const pendingObsCount = pendingObs.length;
25
26
 
26
27
  const totalObsCount = committedObsCount + pendingObsCount;
27
- const totalTokens = committedRefTokens + committedObsTokens + pendingObsTokens;
28
+ const totalObsTokens = estimateObservationPoolTokens([...committedObs, ...pendingObs]);
29
+ const totalTokens = committedRefTokens + totalObsTokens;
28
30
  const relevanceHistogram = countByRelevance([...committedObs, ...pendingObs]);
29
31
 
30
32
  const plural = (n: number, singular: string, plural: string) => (n === 1 ? singular : plural);
package/src/compaction.ts CHANGED
@@ -13,8 +13,8 @@ const REFLECTOR_MAX_PASSES = 3;
13
13
  const PRUNER_MAX_PASSES = 5;
14
14
  const PRUNER_TARGET_RATIO = 0.8;
15
15
 
16
- function observationPoolTokens(observations: ObservationRecord[]): number {
17
- return observations.reduce((sum, o) => sum + estimateStringTokens(o.content), 0);
16
+ export function observationPoolTokens(observations: ObservationRecord[]): number {
17
+ return estimateStringTokens(observationsToPromptLines(observations).join("\n"));
18
18
  }
19
19
 
20
20
  interface LlmArgs {
@@ -5,7 +5,7 @@ import {
5
5
  gapRawEntries,
6
6
  getMemoryState,
7
7
  } from "../branch.js";
8
- import { migrateLegacyReflections, renderSummary, runPruner, runReflector } from "../compaction.js";
8
+ import { migrateLegacyReflections, observationPoolTokens, renderSummary, runPruner, runReflector } from "../compaction.js";
9
9
  import { observationsToPromptLines, runObserver } from "../observer.js";
10
10
  import type { Runtime } from "../runtime.js";
11
11
  import { serializeSourceAddressedBranchEntries } from "../serialize.js";
@@ -141,7 +141,7 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
141
141
  ...deltaObservationData.flatMap((d) => d.records),
142
142
  ];
143
143
 
144
- const observationTokens = workingObservations.reduce((sum, o) => sum + estimateStringTokens(o.content), 0);
144
+ const observationTokens = observationPoolTokens(workingObservations);
145
145
 
146
146
  let finalReflections = workingReflections;
147
147
  let finalObservations = workingObservations;
package/src/observer.ts CHANGED
@@ -25,11 +25,13 @@ const RelevanceSchema = Type.Union([
25
25
  Type.Literal("critical"),
26
26
  ]);
27
27
 
28
+ export const OBSERVATION_TIMESTAMP_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$";
29
+
28
30
  const RecordObservationsSchema = Type.Object({
29
31
  observations: Type.Array(
30
32
  Type.Object({
31
33
  timestamp: Type.String({
32
- pattern: "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$",
34
+ pattern: OBSERVATION_TIMESTAMP_PATTERN,
33
35
  description: "Observation time in local 'YYYY-MM-DD HH:MM' format.",
34
36
  }),
35
37
  content: Type.String({
package/src/prompts.ts CHANGED
@@ -206,10 +206,10 @@ You receive:
206
206
  - Current observations (timestamped, relevance-tagged events to prune). Each is shown as "[id] YYYY-MM-DD HH:MM [relevance] [coverage: tag] content", where id is the 12-character hex handle you reference when dropping.
207
207
  - A pressure line stating pool size, target, tokens still to cut, and the current pass strategy.
208
208
 
209
- Coverage tags are advisory pruning signals derived from current provenance-backed reflection support ids:
209
+ Coverage tags are pruning signals derived from current provenance-backed reflection support ids. They are strong evidence, not blind commands:
210
210
  - [coverage: uncited] means no current provenance-backed reflection cites this observation. Prune cautiously, especially for medium/high/critical observations, because durable meaning may not be captured elsewhere.
211
- - [coverage: cited] means 1-3 current provenance-backed reflections cite this observation. It is a better pruning candidate when the reflection preserves equivalent meaning, but it is not automatically safe to drop.
212
- - [coverage: reinforced] means 4 or more current provenance-backed reflections cite this observation. Durable meaning is likely represented, but still preserve it if it carries exact errors, file paths, decisions, recent task state, user assertions, constraints, or nuance not captured with equivalent fidelity.
211
+ - [coverage: cited] means 1-3 current provenance-backed reflections cite this observation. Once it is old, it is a strong pruning candidate for low/medium observations when the reflection preserves equivalent meaning. Old high observations can also be dropped when the reflection captures the same fact, unless they carry current task state or exact details not captured with equivalent fidelity.
212
+ - [coverage: reinforced] means 4 or more current provenance-backed reflections cite this observation. Once it is old, it is a presumptive drop candidate because durable meaning is likely represented. Still preserve it if it carries current/recent task state, exact errors, file paths, commands, identifiers, user assertions, constraints, corrections, concrete completions, or nuance not captured with equivalent fidelity.
213
213
 
214
214
  How you work:
215
215
  1. Read reflections and the observation pool.
@@ -220,7 +220,7 @@ How you work:
220
220
  This agent may be invoked again in a follow-up pass if the pool is still over budget — focus each run on your next-weakest drops rather than trying to do everything in one call.
221
221
 
222
222
  What to drop (in priority order):
223
- - Signal-captured: observations tagged [coverage: cited] or [coverage: reinforced] whose durable meaning is captured by a reflection now in the reflections list. These are better pruning candidates, but still keep them when they contain exact details, user assertions, concrete completions, recent task state, or nuance not captured with equivalent fidelity.
223
+ - Signal-captured: observations tagged [coverage: reinforced] or [coverage: cited] whose durable meaning is captured by a reflection now in the reflections list. Old reinforced observations should usually be dropped unless they uniquely carry protected details. Old cited low/medium observations are strong drop candidates. Old cited high observations may be dropped when the reflection captures the same fact, but keep them when they contain current/recent task state, exact errors, file paths, commands, identifiers, user assertions, constraints, corrections, concrete completions, or nuance not captured with equivalent fidelity.
224
224
  - Superseded: directly contradicted or replaced by a newer observation.
225
225
  - Redundant: near-duplicate of another observation (keep the higher-relevance or more recent one).
226
226
  - Exhausted routine: tool-call acks, status updates, trivia that no longer affects the work.
@@ -232,8 +232,8 @@ Age-gradient rule. Recent observations carry working context the assistant still
232
232
 
233
233
  Relevance guidance:
234
234
  - "low": drop freely once reviewed. Why: these were marked low because they add little signal; keeping them crowds out more useful records.
235
- - "medium": drop when redundant with reflections or other observations, or when the task context has moved on.
236
- - "high": drop only when clearly superseded or already captured by a reflection.
235
+ - "medium": drop when redundant with reflections or other observations, especially when [coverage: cited] or [coverage: reinforced], or when the task context has moved on.
236
+ - "high": drop when clearly superseded or already captured by a reflection; for old [coverage: cited] or [coverage: reinforced] high observations, require only that the reflection captures the same durable fact and no protected exact detail is unique to the observation.
237
237
  - "critical": NEVER drop. These encode user identity, explicit corrections, and concrete completions. Why this matters: dropping a critical item causes the assistant to repeat finished work, contradict an explicit correction, or misrepresent who the user is. No amount of budget pressure justifies this.
238
238
 
239
239
  User assertions and concrete completions are never droppable, even at non-critical relevance. If the relevance was mis-labeled but the content is load-bearing (an assertion about the user or a marker that work is done), treat the content as authoritative and skip the drop.
@@ -254,7 +254,7 @@ If one of these categories is ALSO captured by an existing reflection with equiv
254
254
  BAD: drop "[id] 2025-12-04 14:30 [medium] Build failed: TS2322 at src/auth.ts:47 — Type 'string | undefined' is not assignable to type 'string'" because it is only medium and the task moved on.
255
255
  GOOD: keep that observation; it is a verbatim error the user hit, not captured in any reflection. Future debugging may need the exact code and location.
256
256
 
257
- When in doubt, prefer dropping cited or reinforced observations over uncited observations, but remember coverage tags are not commands. Reflections protect durable facts only when they preserve equivalent meaning. The only things you must preserve unconditionally are user assertions and concrete completions.
257
+ When in doubt, prefer dropping reinforced observations first, then cited observations, before uncited observations. Coverage tags are strong signals, not blind commands: reflections protect durable facts only when they preserve equivalent meaning. The only things you must preserve unconditionally are critical observations, user assertions, and concrete completions.
258
258
 
259
259
  What you CANNOT do:
260
260
  - You cannot merge observations. If two overlap, drop the weaker one.
@@ -281,9 +281,9 @@ export function buildReflectorPassGuidance(pass: number, maxPasses: number): str
281
281
  type PrunerPassTier = 1 | 2 | 3;
282
282
 
283
283
  const PRUNER_PASS_STRATEGIES: Record<PrunerPassTier, string> = {
284
- 1: `Pass strategy — clear-cut drops only. Prefer old low-value [coverage: cited] or [coverage: reinforced] observations when their durable meaning is represented by current reflections. Also remove exact duplicates, near-duplicates (keep the higher-relevance or more recent version), observations directly superseded by a newer one, and routine "low" tool-call acks. Do not touch ambiguous [coverage: uncited] cases on this pass — a follow-up pass will handle them if still needed.`,
285
- 2: `Pass strategy — topic compression. Drop "low" observations that cover the same territory as recent "medium" or "high" observations, especially when tagged [coverage: cited] or [coverage: reinforced]. Drop older "medium" observations whose substance is now covered by a reflection. Collapse sequences of repeated tool-call observations by keeping the one that captures the learning and dropping the rest.`,
286
- 3: `Pass strategy — aggressive age compression. In the older half of the pool, drop all but the outcome-bearing "low" and "medium" observations, preferring [coverage: cited] and [coverage: reinforced] over [coverage: uncited]. Keep the most recent ~30% of the pool at higher detail. Drop "high" observations only when a reflection clearly captures the same fact. NEVER drop "critical" items, user assertions, or concrete completions regardless of age.`,
284
+ 1: `Pass strategy — clear-cut drops only. Prefer old low-value [coverage: reinforced] observations, then old low/medium [coverage: cited] observations, when their durable meaning is represented by current reflections. Also remove exact duplicates, near-duplicates (keep the higher-relevance or more recent version), observations directly superseded by a newer one, and routine "low" tool-call acks. Do not touch ambiguous [coverage: uncited] cases on this pass — a follow-up pass will handle them if still needed.`,
285
+ 2: `Pass strategy — topic compression. Drop "low" observations that cover the same territory as recent "medium" or "high" observations, especially when tagged [coverage: cited] or [coverage: reinforced]. Treat old [coverage: reinforced] low/medium observations as default drops unless protected exact details are unique to them. Drop older [coverage: cited] "medium" observations whose substance is now covered by a reflection. Collapse sequences of repeated tool-call observations by keeping the one that captures the learning and dropping the rest.`,
286
+ 3: `Pass strategy — aggressive age compression. In the older half of the pool, drop all but the outcome-bearing "low" and "medium" observations, strongly preferring [coverage: reinforced] and [coverage: cited] over [coverage: uncited]. Keep the most recent ~30% of the pool at higher detail. Drop old [coverage: cited] or [coverage: reinforced] "high" observations when a reflection captures the same durable fact and the observation has no unique protected exact detail. NEVER drop "critical" items, user assertions, or concrete completions regardless of age.`,
287
287
  };
288
288
 
289
289
  export function buildPrunerPassGuidance(pass: number, maxPasses: number): string {