pi-observational-memory 2.4.0 → 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 +1 -1
- package/src/commands/status.ts +4 -3
- package/src/commands/view.ts +5 -3
- package/src/compaction.ts +2 -2
- package/src/hooks/compaction-hook.ts +2 -2
- package/src/observer.ts +3 -1
- package/src/prompts.ts +10 -10
- package/src/serialize.ts +3 -1
package/package.json
CHANGED
package/src/commands/status.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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));
|
package/src/commands/view.ts
CHANGED
|
@@ -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
|
|
21
|
+
const committedObsTokens = estimateObservationPoolTokens(committedObs);
|
|
21
22
|
const committedObsCount = committedObs.length;
|
|
22
23
|
|
|
23
|
-
const pendingObsTokens = pendingObs
|
|
24
|
+
const pendingObsTokens = estimateObservationPoolTokens(pendingObs);
|
|
24
25
|
const pendingObsCount = pendingObs.length;
|
|
25
26
|
|
|
26
27
|
const totalObsCount = committedObsCount + pendingObsCount;
|
|
27
|
-
const
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
212
|
-
- [coverage: reinforced] means 4 or more current provenance-backed reflections cite this observation.
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
package/src/serialize.ts
CHANGED
|
@@ -58,8 +58,10 @@ function textAndPlaceholders(
|
|
|
58
58
|
return parts.join("\n");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function textOnly(content:
|
|
61
|
+
function textOnly(content: unknown): string {
|
|
62
|
+
if (content == null) return "";
|
|
62
63
|
if (typeof content === "string") return content;
|
|
64
|
+
if (!Array.isArray(content)) return "";
|
|
63
65
|
return content
|
|
64
66
|
.filter((b): b is TextContent => b?.type === "text" && typeof b.text === "string")
|
|
65
67
|
.map((b) => b.text)
|