pi-observational-memory 0.1.7 → 1.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.
package/README.md CHANGED
@@ -1,35 +1,100 @@
1
1
  # pi-observational-memory
2
2
 
3
- Observational memory extension for [Pi](https://github.com/mariozechner/pi). Replaces Pi's default compaction with a two-tier system of **observations** (timestamped event log) and **reflections** (stable long-term facts), giving the agent persistent memory across long conversations.
3
+ **Make Pi sessions feel endless.**
4
4
 
5
- Inspired by [Mastra's Observational Memory](https://mastra.ai/blog/observational-memory#how-it-works) concept. This is an independent implementation adapted for Pi's extension system and compaction model.
5
+ Every session has a cliff. You're three hours in, the context window fills up, compaction runs, and suddenly the agent doesn't remember what you decided in hour one. You start repeating yourself. The session that was flowing now feels like a new conversation with an amnesiac.
6
+
7
+ pi-observational-memory pushes that cliff out far enough that you stop thinking about it. It replaces Pi's compaction summary with a two-tier memory system — **observations** (a timestamped, priority-tagged event log) and **reflections** (stable long-term facts) — so the agent carries forward *what* you decided, *when*, *why*, and what's already done. Not as prose that degrades with each compaction cycle, but as structured memory that stays sharp.
8
+
9
+ ```
10
+ <reflections>
11
+ - User works at Acme Corp, building "Acme Dashboard"
12
+ - Stack: Next.js 15, Supabase auth, server components with client-side hydration
13
+ - Hard constraint: ship by January 22nd 2026
14
+ </reflections>
15
+
16
+ <observations>
17
+ Date: 2026-01-15
18
+ - 🔴 14:30 User decided to switch from REST to GraphQL for the public API
19
+ - 🟡 14:32 Motivation: reduce over-fetching on mobile clients
20
+ - 🟢 14:35 Agent scaffolded GraphQL schema in src/schema.ts
21
+ - ✅ 14:50 GraphQL migration completed — user confirmed queries working
22
+ - 🔴 15:10 User wants rate limiting on all public endpoints
23
+ - 🟡 15:12 Prefers token bucket algorithm, 100 req/min per API key
24
+ </observations>
25
+ ```
26
+
27
+ Hour six should feel like hour one. The agent knows who you are, what you've built together, and what's left to do.
28
+
29
+ Pi's built-in compaction handles most sessions well — it tracks file operations, manages split turns, and keeps recent messages intact. This extension is for the sessions where "most" isn't enough: long builds, multi-feature sprints, and the kind of deep work where breaking flow to start a new session costs you more than the tokens.
30
+
31
+ Inspired by [Mastra's Observational Memory](https://mastra.ai/blog/observational-memory) research (94.87% on LongMemEval). This is an independent implementation built for Pi's extension system and compaction model.
32
+
33
+ ## Why this matters
34
+
35
+ Pi's default compaction summarizes old messages into prose and tracks which files were read and modified. This works well for short-to-medium sessions. But prose summaries are inherently lossy in ways that compound over time — the third compaction summarizes a summary of a summary, and specific decisions, timestamps, and completion states get flattened.
36
+
37
+ Observational memory uses a different format that's designed to survive repeated compaction cycles:
38
+
39
+ | What you get | Why it matters |
40
+ |---|---|
41
+ | 🔴 Priority tags | Agent knows what's important vs. what's noise |
42
+ | Timestamps | Temporal reasoning — agent knows *when* things happened |
43
+ | ✅ Completion markers | Agent won't redo finished work |
44
+ | State change tracking | "Switched from A to B" — no stale decisions |
45
+ | User quotes preserved | Your exact words survive compression |
46
+ | Reflections tier | Identity and constraints never get pruned |
6
47
 
7
48
  ## How it works
8
49
 
9
- When Pi's context window fills up, this extension intercepts the compaction event and runs two LLM passes:
50
+ Two LLM passes intercept Pi's compaction to produce structured output instead of prose:
51
+
52
+ ```
53
+ Raw messages accumulate
54
+
55
+ ▼ exceeds observationThreshold (default: 50k tokens)
56
+ ┌─────────┐
57
+ │ Observer │──▶ Compresses messages into timestamped,
58
+ └─────────┘ priority-tagged observations. Append-only.
59
+
60
+ ▼ observations exceed reflectionThreshold (default: 30k tokens)
61
+ ┌───────────┐
62
+ │ Reflector │──▶ Promotes stable facts to reflections.
63
+ └───────────┘ Prunes only what it's certain is dead.
64
+
65
+
66
+ ┌──────────────────────────────┐
67
+ │ Agent context: │
68
+ │ 1. System prompt │
69
+ │ 2. Reflections (stable) │
70
+ │ 3. Observations (event log) │
71
+ │ 4. Recent raw messages │
72
+ └──────────────────────────────┘
73
+ ```
10
74
 
11
- 1. **Observer**reads recent conversation messages and compresses them into concise, timestamped observations with priority levels (🔴 important, 🟡 maybe important, 🟢 info, completed).
12
- 2. **Reflector** — when observations grow past a threshold, promotes stable facts to reflections and prunes dead observations.
75
+ The observer is aggressive it compresses everything into dense observations. The reflector is conservative it only prunes what's clearly dead. Between reflector runs, no information is lost.
13
76
 
14
- The resulting `<reflections>` + `<observations>` block becomes the compaction summary that Pi injects at the top of the agent's context. Pi still keeps the most recent raw messages (controlled by `keepRecentTokens`), so the agent sees both structured memory and recent conversation.
77
+ For the full technical breakdown compaction lifecycle, state persistence, configuration interactions see **[docs/how-it-works.md](docs/how-it-works.md)**.
15
78
 
16
79
  ## Install
17
80
 
18
81
  ```bash
19
- npm install pi-observational-memory
82
+ pi install npm:pi-observational-memory
20
83
  ```
21
84
 
22
- Then symlink or copy the package into Pi's extensions directory:
85
+ Or from GitHub:
23
86
 
24
87
  ```bash
25
- ln -s $(npm root)/pi-observational-memory ~/.pi/extensions/pi-observational-memory
88
+ pi install git:github.com/elpapi42/pi-observational-memory
26
89
  ```
27
90
 
28
- Pi will discover it on next session start.
91
+ That's it. The extension hooks into Pi's compaction lifecycle automatically. No config file needed to start — defaults work well for most sessions.
29
92
 
30
93
  ## Configuration
31
94
 
32
- Create `~/.pi/agent/observational-memory.json`:
95
+ ### Extension settings
96
+
97
+ Create `~/.pi/agent/observational-memory.json` (or `.pi/observational-memory.json` per project):
33
98
 
34
99
  ```json
35
100
  {
@@ -38,29 +103,60 @@ Create `~/.pi/agent/observational-memory.json`:
38
103
  }
39
104
  ```
40
105
 
41
- | Field | Default | Description |
42
- |-------|---------|-------------|
43
- | `observationThreshold` | `50000` | Token count that triggers the observer |
44
- | `reflectionThreshold` | `30000` | Observation token count that triggers the reflector |
45
- | `compactionModel` | session model | Optional `{ "provider": "...", "id": "..." }` to use a different model for observation/reflection |
106
+ | Setting | Default | What it controls |
107
+ |---|---|---|
108
+ | `observationThreshold` | `50,000` tokens | How much raw conversation accumulates before the observer runs |
109
+ | `reflectionThreshold` | `30,000` tokens | How large observations grow before the reflector promotes and prunes |
110
+ | `compactionModel` | session model | Optional use a cheaper model for observer/reflector passes |
111
+
112
+ ### Using a cheaper model for compaction
46
113
 
47
- ### Custom model example
114
+ The observer and reflector don't need the same capabilities as your coding agent. Offload them to something fast and cheap:
48
115
 
49
116
  ```json
50
117
  {
51
- "observationThreshold": 6000,
52
- "reflectionThreshold": 1000,
53
118
  "compactionModel": { "provider": "openrouter", "id": "google/gemma-4-31b-it" }
54
119
  }
55
120
  ```
56
121
 
122
+ ### Pi compaction settings
123
+
124
+ The extension works with Pi's built-in compaction settings in `~/.pi/agent/settings.json`:
125
+
126
+ ```json
127
+ {
128
+ "compaction": {
129
+ "keepRecentTokens": 20000
130
+ }
131
+ }
132
+ ```
133
+
134
+ | Setting | Default | What it controls |
135
+ |---|---|---|
136
+ | `keepRecentTokens` | `20,000` | Tokens of recent conversation kept verbatim (not summarized) |
137
+ | `reserveTokens` | `16,384` | Headroom for LLM response; Pi auto-compacts when context exceeds `window - reserveTokens` |
138
+
139
+ **How they interact:** Pi decides *when* to compact and *how many recent messages to keep raw*. The extension decides *how* to compact (observations + reflections instead of a flat summary) and *when to proactively trigger* compaction before the window fills. Both paths end up at the same `session_before_compact` hook.
140
+
57
141
  ## Commands
58
142
 
59
143
  | Command | Description |
60
- |---------|-------------|
61
- | `/om-status` | Show token counts and current thresholds |
144
+ |---|---|
145
+ | `/om-status` | Token counts, thresholds, and when the next observer/reflector pass will trigger |
62
146
  | `/om-view` | Print current reflections and observations |
63
- | `/om-view --full` | Same as above, plus raw kept messages |
147
+ | `/om-view --full` | Same as above, plus the raw kept messages |
148
+
149
+ ## Design decisions
150
+
151
+ **Why observations are append-only.** Between reflector runs, nothing is lost. The observer only compresses new messages — it doesn't decide what to keep. This keeps the observer simple and predictable.
152
+
153
+ **Why the reflector is conservative.** It only prunes what it's certain is dead: completed tasks no longer referenced, superseded information, exact duplicates. Being old or low-priority is not a reason to prune. If in doubt, it keeps.
154
+
155
+ **Why user messages are captured near-verbatim.** When the context window shrinks, observations become the only record of what you said. Short messages are preserved exactly; long ones are summarized with key phrases quoted.
156
+
157
+ **Why state changes are explicit.** When you say "switching from A to B," the observation notes both the new state and what it replaces. This prevents the agent from acting on stale information after compaction.
158
+
159
+ **Why memory lives in the session.** State is stored in Pi's session entries as compaction `details` — no external database, no filesystem state, no separate sync. On session resume, the extension walks backward through entries to restore memory. If you use `/tree` to branch, each branch gets its own memory state.
64
160
 
65
161
  ## License
66
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-observational-memory",
3
- "version": "0.1.7",
3
+ "version": "1.0.1",
4
4
  "description": "Observational memory extension for pi — cache-friendly tiered compaction with observations and reflections.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
4
+
5
+ export interface Config {
6
+ observationThreshold: number;
7
+ reflectionThreshold: number;
8
+ compactionModel?: { provider: string; id: string };
9
+ }
10
+
11
+ export const DEFAULTS: Config = {
12
+ observationThreshold: 50_000,
13
+ reflectionThreshold: 30_000,
14
+ };
15
+
16
+ export function loadConfig(cwd: string): Config {
17
+ const globalPath = join(getAgentDir(), "observational-memory.json");
18
+ const projectPath = join(cwd, ".pi", "observational-memory.json");
19
+
20
+ let globalConfig: Partial<Config> = {};
21
+ let projectConfig: Partial<Config> = {};
22
+
23
+ if (existsSync(globalPath)) {
24
+ try {
25
+ globalConfig = JSON.parse(readFileSync(globalPath, "utf-8"));
26
+ } catch {}
27
+ }
28
+
29
+ if (existsSync(projectPath)) {
30
+ try {
31
+ projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
32
+ } catch {}
33
+ }
34
+
35
+ return { ...DEFAULTS, ...globalConfig, ...projectConfig };
36
+ }
package/src/index.ts CHANGED
@@ -1,312 +1,18 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
1
  import { completeSimple } from "@mariozechner/pi-ai";
4
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
- import {
6
- convertToLlm,
7
- estimateTokens as estimateMessageTokens,
8
- getAgentDir,
9
- serializeConversation,
10
- } from "@mariozechner/pi-coding-agent";
11
-
12
- // ============================================================================
13
- // Config
14
- // ============================================================================
15
-
16
- interface Config {
17
- observationThreshold: number;
18
- reflectionThreshold: number;
19
- compactionModel?: { provider: string; id: string };
20
- }
21
-
22
- const DEFAULTS: Config = {
23
- observationThreshold: 50_000,
24
- reflectionThreshold: 30_000,
25
- };
26
-
27
- function loadConfig(cwd: string): Config {
28
- const globalPath = join(getAgentDir(), "observational-memory.json");
29
- const projectPath = join(cwd, ".pi", "observational-memory.json");
30
-
31
- let globalConfig: Partial<Config> = {};
32
- let projectConfig: Partial<Config> = {};
33
-
34
- if (existsSync(globalPath)) {
35
- try {
36
- globalConfig = JSON.parse(readFileSync(globalPath, "utf-8"));
37
- } catch {}
38
- }
39
-
40
- if (existsSync(projectPath)) {
41
- try {
42
- projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
43
- } catch {}
44
- }
45
-
46
- return { ...DEFAULTS, ...globalConfig, ...projectConfig };
47
- }
48
-
49
- // ============================================================================
50
- // Types
51
- // ============================================================================
52
-
53
- interface MemoryState {
54
- observations: string;
55
- reflections: string;
56
- }
57
-
58
- interface MemoryDetails {
59
- type: "observational-memory";
60
- version: 1;
61
- observations: string;
62
- reflections: string;
63
- }
64
-
65
- // ============================================================================
66
- // Helpers
67
- // ============================================================================
68
-
69
- function isMemoryDetails(d: unknown): d is MemoryDetails {
70
- return !!d && typeof d === "object" && (d as Record<string, unknown>).type === "observational-memory";
71
- }
72
-
73
- function estimateTokens(text: string): number {
74
- return Math.ceil(text.length / 4);
75
- }
76
-
77
- function estimateRawTailTokens(
78
- entries: Array<{ type: string; message?: unknown; content?: unknown; firstKeptEntryId?: string; id?: string }>,
79
- ): number {
80
- let startIndex = 0;
81
- for (let i = entries.length - 1; i >= 0; i--) {
82
- if (entries[i].type === "compaction") {
83
- const keptId = entries[i].firstKeptEntryId;
84
- if (keptId) {
85
- for (let j = 0; j < entries.length; j++) {
86
- if (entries[j].id === keptId) {
87
- startIndex = j;
88
- break;
89
- }
90
- }
91
- } else {
92
- startIndex = i + 1;
93
- }
94
- break;
95
- }
96
- }
97
-
98
- let tokens = 0;
99
- for (let i = startIndex; i < entries.length; i++) {
100
- const entry = entries[i];
101
- if (entry.type === "message" && entry.message) {
102
- tokens += estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
103
- } else if (entry.type === "custom_message" && entry.content) {
104
- const content = entry.content;
105
- if (typeof content === "string") {
106
- tokens += Math.ceil(content.length / 4);
107
- } else if (Array.isArray(content)) {
108
- for (const block of content) {
109
- if (block.type === "text" && block.text) tokens += Math.ceil(block.text.length / 4);
110
- }
111
- }
112
- }
113
- }
114
- return tokens;
115
- }
116
-
117
- function extractText(response: { content: Array<{ type: string; text?: string }> }): string {
118
- return response.content
119
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
120
- .map((c) => c.text)
121
- .join("\n");
122
- }
123
-
124
- // ============================================================================
125
- // Prompts
126
- // ============================================================================
127
-
128
- const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
129
-
130
- Format as a date-grouped log:
131
-
132
- Date: YYYY-MM-DD
133
- - 🔴 HH:MM Observation text
134
- - 🔴 HH:MM Sub-observation
135
- - 🟡 HH:MM Sub-observation
136
- - 🟢 HH:MM Another observation
137
-
138
- Priority levels:
139
- - 🔴 Important: user goals, constraints, decisions, names, deadlines, architectural choices, bugs, errors
140
- - 🟡 Maybe important: questions asked, preferences, approaches considered, configuration details
141
- - 🟢 Info only: routine operations, minor details
142
- - ✅ Completed: a task, question, subtask, or issue is concretely resolved
143
-
144
- CRITICAL — DISTINGUISH USER ASSERTIONS FROM QUESTIONS:
145
-
146
- When the user TELLS you something about themselves, mark it as an assertion:
147
- - "I have two kids" → 🔴 User stated has two kids
148
- - "I work at Acme Corp" → 🔴 User stated works at Acme Corp
149
-
150
- When the user ASKS about something, mark it as a question/request:
151
- - "Can you help me with X?" → 🔴 User asked help with X
152
- - "What's the best way to do Y?" → 🟡 User asked best way to do Y
153
-
154
- Distinguish between QUESTIONS and STATEMENTS OF INTENT:
155
- - "Can you recommend..." → Question (extract as "User asked...")
156
- - "I'm looking forward to doing X" → Statement of intent (extract as "User stated they will do X (include date if mentioned)")
157
- - "I need to do X" → Statement of intent (extract as "User stated they need to do X")
158
-
159
- USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life. If a user previously stated something and later asks a question about the same topic, the assertion is the answer — the question doesn't invalidate what they already told you.
160
-
161
- Rules:
162
- - Group observations by date, with timestamps inline.
163
- - Use the three-date model when relevant: note the observation date, the referenced date (if the event refers to a different day), and a relative date (e.g. "2 days ago").
164
- - Nest related sub-observations under a parent observation.
165
- - Preserve exact file paths, function names, error messages, and technical details.
166
- - Focus on WHAT happened and WHY, not routine tool calls.
167
- - Each observation should be one concise line.
168
-
169
- CONTENT PRESERVATION:
170
-
171
- User message capture:
172
- - Short and medium-length user messages: capture nearly verbatim.
173
- - Very long user messages: summarize but quote key phrases that carry specific intent or meaning.
174
- - This is critical — when the conversation window shrinks, observations are the only record of what the user said.
175
-
176
- Preserve unusual phrasing — quote the user's exact words when non-standard:
177
- - BAD: User exercised.
178
- - GOOD: User stated they did a "movement session" (their term for exercise).
179
-
180
- Use precise action verbs — replace vague verbs with specific ones:
181
- - BAD: User is getting X.
182
- - GOOD: User subscribed to X. (if context confirms recurring delivery)
183
- - GOOD: User purchased X. (if context confirms one-time acquisition)
184
- Common: "getting regularly" → "subscribed to"; "got" → "purchased"/"received"/"was given"; "stopped getting" → "canceled"/"unsubscribed from"
185
- If the assistant confirms or clarifies the user's vague language, prefer the assistant's more precise terminology.
186
-
187
- Preserve distinguishing details in assistant-generated content:
188
- - BAD: Assistant recommended 5 hotels.
189
- - GOOD: Assistant recommended hotels: Hotel A (near station), Hotel B (budget-friendly), Hotel C (rooftop pool).
190
- - BAD: Assistant provided social media accounts.
191
- - GOOD: Assistant provided accounts: @user_one (portraits), @user_two (landscapes).
192
-
193
- Preserve specific technical/numerical values:
194
- - BAD: Assistant explained the performance improvements.
195
- - GOOD: Optimization achieved 43.7% faster load times, memory dropped from 2.8GB to 940MB.
196
-
197
- Preserve role/participation when user mentions their involvement:
198
- - BAD: User attended the company event.
199
- - GOOD: User was a presenter at the company event.
200
-
201
- Code context — always preserve: exact file paths with line numbers, error messages verbatim, function/variable names, architectural decisions and rationale.
202
-
203
- STATE CHANGES AND UPDATES:
204
- When a user indicates they are changing something, frame it as a state change that supersedes previous information:
205
- - "I'm going to start doing X instead of Y" → "User will start doing X (changing from Y)"
206
- - "I'm switching from A to B" → "User is switching from A to B"
207
- - "I moved my stuff to the new place" → "User moved to the new place (no longer at previous location)"
208
-
209
- If the new state contradicts or updates previous information, make that explicit:
210
- - BAD: User plans to use the new method.
211
- - GOOD: User will use the new method (replacing the old approach).
212
- - Do NOT repeat information already captured in existing reflections or observations.
213
- - Do NOT wrap output in code blocks or markdown fences.
214
-
215
- AVOIDING REPETITIVE OBSERVATIONS:
216
- - Do NOT repeat the same observation across multiple turns if there is no new information.
217
- - When the agent performs repeated similar actions (e.g., browsing files, running the same tool type multiple times), group them into a single parent observation with sub-bullets for each new result.
218
-
219
- BAD (repetitive):
220
- - 🟡 14:30 Agent used view tool on src/auth.ts
221
- - 🟡 14:31 Agent used view tool on src/users.ts
222
- - 🟡 14:32 Agent used view tool on src/routes.ts
223
-
224
- GOOD (grouped):
225
- - 🟡 14:30 Agent investigated auth flow
226
- - -> viewed src/auth.ts — found token validation logic
227
- - -> viewed src/users.ts — found user lookup by email
228
- - -> viewed src/routes.ts — found middleware chain
229
-
230
- Only add a new observation for a repeated action if the NEW result changes the picture.
231
-
232
- COMPLETION TRACKING:
233
- ✅ markers are explicit memory signals telling the assistant that work is finished and should not be repeated.
234
-
235
- Use ✅ when:
236
- - The user explicitly confirms something worked ("thanks, that fixed it", "got it", "perfect")
237
- - The assistant provided a definitive answer and the user moved on
238
- - A multi-step task reached its stated goal
239
- - The user acknowledged receipt of requested information
240
- - A concrete subtask or implementation step completed during ongoing work
241
-
242
- Do NOT use ✅ when:
243
- - The assistant merely responded — the user might follow up with corrections
244
- - The topic is paused but not resolved ("I'll try that later")
245
- - The user's reaction is ambiguous
246
-
247
- Two formats:
248
- As a sub-bullet under a parent observation:
249
- - 🔴 HH:MM User asked how to configure auth middleware
250
- - -> Agent explained JWT setup with code example
251
- - ✅ User confirmed auth is working
252
-
253
- Or standalone when closing a broader task:
254
- - ✅ HH:MM Auth configuration completed — user confirmed middleware is working
255
-
256
- Completion observations should be terse but specific about WHAT was completed. Prefer concrete resolved outcomes over abstract workflow status.`;
257
-
258
- const REFLECTOR_SYSTEM = `You are a reflection agent for a coding assistant. Your job is to maintain long-term reflections while preserving as many observations as possible.
259
-
260
- You will receive current reflections (long-term facts) and accumulated observations.
261
-
262
- Your task:
263
- 1. PROMOTE observations to reflections ONLY when they are clearly stable, long-lived facts:
264
- - User identity, role, preferences
265
- - Project goals and architecture decisions
266
- - Permanent constraints and requirements
267
- - Key technical decisions and their rationale
268
- After promoting, KEEP the original observation — do not remove it.
269
- 2. PRUNE observations ONLY when you are certain they are dead:
270
- - Tasks explicitly completed AND no longer referenced
271
- - Information directly contradicted or superseded by a newer observation
272
- - Exact duplicates of other observations
273
- When in doubt, KEEP the observation.
274
- IMPORTANT: Preserve ✅ completion markers — they tell the assistant what is already resolved and prevent repeated work. Preserve the concrete resolved outcome captured by ✅ markers. When pruning detailed steps of a completed task, keep the ✅ outcome line.
275
- USER ASSERTIONS vs QUESTIONS: "User stated: X" = authoritative assertion. "User asked: X" = question/request. When consolidating, USER ASSERTIONS TAKE PRECEDENCE. If you see both "User stated: has two kids" and later "User asked: how many kids?", keep the assertion — the question doesn't invalidate what they told you.
276
- 3. KEEP everything else. Most observations should survive. An observation being old or low-priority (🟢) is NOT a reason to remove it.
277
- 4. UPDATE reflections: merge new promoted facts into existing reflections. Remove reflections only if directly contradicted by observations.
278
-
279
- Output EXACTLY two sections with these tags:
280
-
281
- <reflections>
282
- [Updated long-term reflections — stable facts, one per line]
283
- </reflections>
284
-
285
- <observations>
286
- [Surviving observations in the same date-grouped log format — most should be preserved]
287
- </observations>
288
-
289
- Do NOT wrap output in code blocks or markdown fences.`;
290
-
291
- const CONTEXT_USAGE_INSTRUCTIONS = `KNOWLEDGE UPDATES: When observations contain conflicting information, prefer the MOST RECENT observation (check dates). Look for state-change phrases like "will start", "is switching", "changed to", "replacing" as indicators that older information has been superseded.
292
-
293
- PLANNED ACTIONS: If an observation says the user planned to do something and the date is now in the past, assume they completed the action unless there's evidence they didn't.
294
-
295
- USER ASSERTIONS: When observations contain both "User stated: X" and "User asked: X" about the same topic, the assertion is authoritative — the user is the source of truth about their own life.
296
-
297
- COMPLETION MARKERS: Observations marked with ✅ indicate completed work. Do not re-do or re-investigate tasks marked as complete unless the user explicitly asks to revisit them.`;
298
-
299
- // ============================================================================
300
- // Extension
301
- // ============================================================================
3
+ import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
4
+ import { DEFAULTS, loadConfig } from "./config.js";
5
+ import type { Config } from "./config.js";
6
+ import { CONTEXT_USAGE_INSTRUCTIONS, OBSERVER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
7
+ import { estimateRawTailTokens, estimateTokens, extractText } from "./tokens.js";
8
+ import type { MemoryDetails, MemoryState } from "./types.js";
9
+ import { isMemoryDetails } from "./types.js";
302
10
 
303
11
  export default function observationalMemory(pi: ExtensionAPI) {
304
12
  let config: Config = { ...DEFAULTS };
305
13
  let state: MemoryState = { observations: "", reflections: "" };
306
14
  let compactInFlight = false;
307
15
 
308
- // ---- Restore state from last compaction entry ----
309
-
310
16
  pi.on("session_start", (_event, ctx) => {
311
17
  config = loadConfig(ctx.cwd);
312
18
  state = { observations: "", reflections: "" };
@@ -323,8 +29,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
323
29
  }
324
30
  });
325
31
 
326
- // ---- Trigger compaction when raw tail exceeds threshold ----
327
-
328
32
  pi.on("agent_end", (_event, ctx) => {
329
33
  if (compactInFlight) return;
330
34
 
@@ -351,8 +55,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
351
55
  }, 0);
352
56
  });
353
57
 
354
- // ---- Custom compaction: observer + reflector ----
355
-
356
58
  pi.on("session_before_compact", async (event, ctx) => {
357
59
  const { preparation, signal } = event;
358
60
  const { messagesToSummarize, turnPrefixMessages, firstKeptEntryId, tokensBefore } = preparation;
@@ -382,8 +84,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
382
84
  const dateStr = now.toISOString().split("T")[0];
383
85
  const timeStr = now.toTimeString().slice(0, 5);
384
86
 
385
- // ---- Run observer ----
386
-
387
87
  ctx.ui.notify("Observational memory: running observer...", "info");
388
88
 
389
89
  try {
@@ -423,8 +123,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
423
123
  return;
424
124
  }
425
125
 
426
- // ---- Run reflector if observations are too large ----
427
-
428
126
  if (estimateTokens(state.observations) > config.reflectionThreshold) {
429
127
  ctx.ui.notify("Observational memory: running reflector...", "info");
430
128
 
@@ -465,8 +163,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
465
163
  }
466
164
  }
467
165
 
468
- // ---- Build summary ----
469
-
470
166
  let summary = "";
471
167
  if (state.reflections) {
472
168
  summary += `<reflections>\n${state.reflections}\n</reflections>\n\n`;
@@ -496,8 +192,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
496
192
  };
497
193
  });
498
194
 
499
- // ---- /om-status command ----
500
-
501
195
  pi.registerCommand("om-status", {
502
196
  description: "Show observational memory status",
503
197
  handler: async (_args, ctx) => {
@@ -521,8 +215,6 @@ export default function observationalMemory(pi: ExtensionAPI) {
521
215
  },
522
216
  });
523
217
 
524
- // ---- /om-view command ----
525
-
526
218
  pi.registerCommand("om-view", {
527
219
  description: "Print full observational memory contents (--full to include raw messages)",
528
220
  handler: async (args, ctx) => {
package/src/prompts.ts ADDED
@@ -0,0 +1,170 @@
1
+ export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
2
+
3
+ Format as a date-grouped log:
4
+
5
+ Date: YYYY-MM-DD
6
+ - 🔴 HH:MM Observation text
7
+ - 🔴 HH:MM Sub-observation
8
+ - 🟡 HH:MM Sub-observation
9
+ - 🟢 HH:MM Another observation
10
+
11
+ Priority levels:
12
+ - 🔴 Important: user goals, constraints, decisions, names, deadlines, architectural choices, bugs, errors
13
+ - 🟡 Maybe important: questions asked, preferences, approaches considered, configuration details
14
+ - 🟢 Info only: routine operations, minor details
15
+ - ✅ Completed: a task, question, subtask, or issue is concretely resolved
16
+
17
+ CRITICAL — DISTINGUISH USER ASSERTIONS FROM QUESTIONS:
18
+
19
+ When the user TELLS you something about themselves, mark it as an assertion:
20
+ - "I have two kids" → 🔴 User stated has two kids
21
+ - "I work at Acme Corp" → 🔴 User stated works at Acme Corp
22
+
23
+ When the user ASKS about something, mark it as a question/request:
24
+ - "Can you help me with X?" → 🔴 User asked help with X
25
+ - "What's the best way to do Y?" → 🟡 User asked best way to do Y
26
+
27
+ Distinguish between QUESTIONS and STATEMENTS OF INTENT:
28
+ - "Can you recommend..." → Question (extract as "User asked...")
29
+ - "I'm looking forward to doing X" → Statement of intent (extract as "User stated they will do X (include date if mentioned)")
30
+ - "I need to do X" → Statement of intent (extract as "User stated they need to do X")
31
+
32
+ USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life. If a user previously stated something and later asks a question about the same topic, the assertion is the answer — the question doesn't invalidate what they already told you.
33
+
34
+ Rules:
35
+ - Group observations by date, with timestamps inline.
36
+ - Use the three-date model when relevant: note the observation date, the referenced date (if the event refers to a different day), and a relative date (e.g. "2 days ago").
37
+ - Nest related sub-observations under a parent observation.
38
+ - Preserve exact file paths, function names, error messages, and technical details.
39
+ - Focus on WHAT happened and WHY, not routine tool calls.
40
+ - Each observation should be one concise line.
41
+
42
+ CONTENT PRESERVATION:
43
+
44
+ User message capture:
45
+ - Short and medium-length user messages: capture nearly verbatim.
46
+ - Very long user messages: summarize but quote key phrases that carry specific intent or meaning.
47
+ - This is critical — when the conversation window shrinks, observations are the only record of what the user said.
48
+
49
+ Preserve unusual phrasing — quote the user's exact words when non-standard:
50
+ - BAD: User exercised.
51
+ - GOOD: User stated they did a "movement session" (their term for exercise).
52
+
53
+ Use precise action verbs — replace vague verbs with specific ones:
54
+ - BAD: User is getting X.
55
+ - GOOD: User subscribed to X. (if context confirms recurring delivery)
56
+ - GOOD: User purchased X. (if context confirms one-time acquisition)
57
+ Common: "getting regularly" → "subscribed to"; "got" → "purchased"/"received"/"was given"; "stopped getting" → "canceled"/"unsubscribed from"
58
+ If the assistant confirms or clarifies the user's vague language, prefer the assistant's more precise terminology.
59
+
60
+ Preserve distinguishing details in assistant-generated content:
61
+ - BAD: Assistant recommended 5 hotels.
62
+ - GOOD: Assistant recommended hotels: Hotel A (near station), Hotel B (budget-friendly), Hotel C (rooftop pool).
63
+ - BAD: Assistant provided social media accounts.
64
+ - GOOD: Assistant provided accounts: @user_one (portraits), @user_two (landscapes).
65
+
66
+ Preserve specific technical/numerical values:
67
+ - BAD: Assistant explained the performance improvements.
68
+ - GOOD: Optimization achieved 43.7% faster load times, memory dropped from 2.8GB to 940MB.
69
+
70
+ Preserve role/participation when user mentions their involvement:
71
+ - BAD: User attended the company event.
72
+ - GOOD: User was a presenter at the company event.
73
+
74
+ Code context — always preserve: exact file paths with line numbers, error messages verbatim, function/variable names, architectural decisions and rationale.
75
+
76
+ STATE CHANGES AND UPDATES:
77
+ When a user indicates they are changing something, frame it as a state change that supersedes previous information:
78
+ - "I'm going to start doing X instead of Y" → "User will start doing X (changing from Y)"
79
+ - "I'm switching from A to B" → "User is switching from A to B"
80
+ - "I moved my stuff to the new place" → "User moved to the new place (no longer at previous location)"
81
+
82
+ If the new state contradicts or updates previous information, make that explicit:
83
+ - BAD: User plans to use the new method.
84
+ - GOOD: User will use the new method (replacing the old approach).
85
+ - Do NOT repeat information already captured in existing reflections or observations.
86
+ - Do NOT wrap output in code blocks or markdown fences.
87
+
88
+ AVOIDING REPETITIVE OBSERVATIONS:
89
+ - Do NOT repeat the same observation across multiple turns if there is no new information.
90
+ - When the agent performs repeated similar actions (e.g., browsing files, running the same tool type multiple times), group them into a single parent observation with sub-bullets for each new result.
91
+
92
+ BAD (repetitive):
93
+ - 🟡 14:30 Agent used view tool on src/auth.ts
94
+ - 🟡 14:31 Agent used view tool on src/users.ts
95
+ - 🟡 14:32 Agent used view tool on src/routes.ts
96
+
97
+ GOOD (grouped):
98
+ - 🟡 14:30 Agent investigated auth flow
99
+ - -> viewed src/auth.ts — found token validation logic
100
+ - -> viewed src/users.ts — found user lookup by email
101
+ - -> viewed src/routes.ts — found middleware chain
102
+
103
+ Only add a new observation for a repeated action if the NEW result changes the picture.
104
+
105
+ COMPLETION TRACKING:
106
+ ✅ markers are explicit memory signals telling the assistant that work is finished and should not be repeated.
107
+
108
+ Use ✅ when:
109
+ - The user explicitly confirms something worked ("thanks, that fixed it", "got it", "perfect")
110
+ - The assistant provided a definitive answer and the user moved on
111
+ - A multi-step task reached its stated goal
112
+ - The user acknowledged receipt of requested information
113
+ - A concrete subtask or implementation step completed during ongoing work
114
+
115
+ Do NOT use ✅ when:
116
+ - The assistant merely responded — the user might follow up with corrections
117
+ - The topic is paused but not resolved ("I'll try that later")
118
+ - The user's reaction is ambiguous
119
+
120
+ Two formats:
121
+ As a sub-bullet under a parent observation:
122
+ - 🔴 HH:MM User asked how to configure auth middleware
123
+ - -> Agent explained JWT setup with code example
124
+ - ✅ User confirmed auth is working
125
+
126
+ Or standalone when closing a broader task:
127
+ - ✅ HH:MM Auth configuration completed — user confirmed middleware is working
128
+
129
+ Completion observations should be terse but specific about WHAT was completed. Prefer concrete resolved outcomes over abstract workflow status.`;
130
+
131
+ export const REFLECTOR_SYSTEM = `You are a reflection agent for a coding assistant. Your job is to maintain long-term reflections while preserving as many observations as possible.
132
+
133
+ You will receive current reflections (long-term facts) and accumulated observations.
134
+
135
+ Your task:
136
+ 1. PROMOTE observations to reflections ONLY when they are clearly stable, long-lived facts:
137
+ - User identity, role, preferences
138
+ - Project goals and architecture decisions
139
+ - Permanent constraints and requirements
140
+ - Key technical decisions and their rationale
141
+ After promoting, KEEP the original observation — do not remove it.
142
+ 2. PRUNE observations ONLY when you are certain they are dead:
143
+ - Tasks explicitly completed AND no longer referenced
144
+ - Information directly contradicted or superseded by a newer observation
145
+ - Exact duplicates of other observations
146
+ When in doubt, KEEP the observation.
147
+ IMPORTANT: Preserve ✅ completion markers — they tell the assistant what is already resolved and prevent repeated work. Preserve the concrete resolved outcome captured by ✅ markers. When pruning detailed steps of a completed task, keep the ✅ outcome line.
148
+ USER ASSERTIONS vs QUESTIONS: "User stated: X" = authoritative assertion. "User asked: X" = question/request. When consolidating, USER ASSERTIONS TAKE PRECEDENCE. If you see both "User stated: has two kids" and later "User asked: how many kids?", keep the assertion — the question doesn't invalidate what they told you.
149
+ 3. KEEP everything else. Most observations should survive. An observation being old or low-priority (🟢) is NOT a reason to remove it.
150
+ 4. UPDATE reflections: merge new promoted facts into existing reflections. Remove reflections only if directly contradicted by observations.
151
+
152
+ Output EXACTLY two sections with these tags:
153
+
154
+ <reflections>
155
+ [Updated long-term reflections — stable facts, one per line]
156
+ </reflections>
157
+
158
+ <observations>
159
+ [Surviving observations in the same date-grouped log format — most should be preserved]
160
+ </observations>
161
+
162
+ Do NOT wrap output in code blocks or markdown fences.`;
163
+
164
+ export const CONTEXT_USAGE_INSTRUCTIONS = `KNOWLEDGE UPDATES: When observations contain conflicting information, prefer the MOST RECENT observation (check dates). Look for state-change phrases like "will start", "is switching", "changed to", "replacing" as indicators that older information has been superseded.
165
+
166
+ PLANNED ACTIONS: If an observation says the user planned to do something and the date is now in the past, assume they completed the action unless there's evidence they didn't.
167
+
168
+ USER ASSERTIONS: When observations contain both "User stated: X" and "User asked: X" about the same topic, the assertion is authoritative — the user is the source of truth about their own life.
169
+
170
+ COMPLETION MARKERS: Observations marked with ✅ indicate completed work. Do not re-do or re-investigate tasks marked as complete unless the user explicitly asks to revisit them.`;
package/src/tokens.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { estimateTokens as estimateMessageTokens } from "@mariozechner/pi-coding-agent";
2
+
3
+ export function estimateTokens(text: string): number {
4
+ return Math.ceil(text.length / 4);
5
+ }
6
+
7
+ export function estimateRawTailTokens(
8
+ entries: Array<{ type: string; message?: unknown; content?: unknown; firstKeptEntryId?: string; id?: string }>,
9
+ ): number {
10
+ let startIndex = 0;
11
+ for (let i = entries.length - 1; i >= 0; i--) {
12
+ if (entries[i].type === "compaction") {
13
+ const keptId = entries[i].firstKeptEntryId;
14
+ if (keptId) {
15
+ for (let j = 0; j < entries.length; j++) {
16
+ if (entries[j].id === keptId) {
17
+ startIndex = j;
18
+ break;
19
+ }
20
+ }
21
+ } else {
22
+ startIndex = i + 1;
23
+ }
24
+ break;
25
+ }
26
+ }
27
+
28
+ let tokens = 0;
29
+ for (let i = startIndex; i < entries.length; i++) {
30
+ const entry = entries[i];
31
+ if (entry.type === "message" && entry.message) {
32
+ tokens += estimateMessageTokens(entry.message as Parameters<typeof estimateMessageTokens>[0]);
33
+ } else if (entry.type === "custom_message" && entry.content) {
34
+ const content = entry.content;
35
+ if (typeof content === "string") {
36
+ tokens += Math.ceil(content.length / 4);
37
+ } else if (Array.isArray(content)) {
38
+ for (const block of content) {
39
+ if (block.type === "text" && block.text) tokens += Math.ceil(block.text.length / 4);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return tokens;
45
+ }
46
+
47
+ export function extractText(response: { content: Array<{ type: string; text?: string }> }): string {
48
+ return response.content
49
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
50
+ .map((c) => c.text)
51
+ .join("\n");
52
+ }
package/src/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ export interface MemoryState {
2
+ observations: string;
3
+ reflections: string;
4
+ }
5
+
6
+ export interface MemoryDetails {
7
+ type: "observational-memory";
8
+ version: 1;
9
+ observations: string;
10
+ reflections: string;
11
+ }
12
+
13
+ export function isMemoryDetails(d: unknown): d is MemoryDetails {
14
+ return !!d && typeof d === "object" && (d as Record<string, unknown>).type === "observational-memory";
15
+ }