pi-observational-memory 1.0.2 → 1.0.4

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": "1.0.2",
3
+ "version": "1.0.4",
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/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { completeSimple } from "@mariozechner/pi-ai";
1
+ import { completeSimple, type Message, type TextContent, type ToolCall, type ToolResultMessage } from "@mariozechner/pi-ai";
2
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
- import { convertToLlm, serializeConversation, SettingsManager } from "@mariozechner/pi-coding-agent";
3
+ import { convertToLlm, SettingsManager } from "@mariozechner/pi-coding-agent";
4
4
  import { DEFAULTS, loadConfig } from "./config.js";
5
5
  import type { Config } from "./config.js";
6
6
  import { CONTEXT_USAGE_INSTRUCTIONS, OBSERVER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
@@ -8,6 +8,52 @@ import { estimateRawTailTokens, estimateTokens, extractText } from "./tokens.js"
8
8
  import type { MemoryDetails, MemoryState } from "./types.js";
9
9
  import { isMemoryDetails } from "./types.js";
10
10
 
11
+ function utcDate(epochMs: number): string {
12
+ if (!Number.isFinite(epochMs)) return "????-??-??";
13
+ return new Date(epochMs).toISOString().slice(0, 10);
14
+ }
15
+
16
+ function utcTime(epochMs: number): string {
17
+ if (!Number.isFinite(epochMs)) return "??:??";
18
+ return new Date(epochMs).toISOString().slice(11, 16);
19
+ }
20
+
21
+ function serializeWithTimestamps(messages: Message[]): string {
22
+ return messages
23
+ .map((msg): string | null => {
24
+ const time = utcTime(msg.timestamp);
25
+ if (msg.role === "user") {
26
+ const text =
27
+ typeof msg.content === "string"
28
+ ? msg.content
29
+ : msg.content
30
+ .filter((b): b is TextContent => b.type === "text")
31
+ .map((b) => b.text)
32
+ .join("\n");
33
+ return `[User @ ${time} UTC]: ${text}`;
34
+ }
35
+ if (msg.role === "assistant") {
36
+ const parts = msg.content.map((b) => {
37
+ if (b.type === "text") return b.text;
38
+ if (b.type === "thinking") return b.redacted ? "" : `[thinking: ${b.thinking}]`;
39
+ if (b.type === "toolCall") return `[${b.name}(${JSON.stringify(b.arguments)})]`;
40
+ return "";
41
+ });
42
+ const body = parts.filter(Boolean).join("\n");
43
+ if (!body) return null;
44
+ return `[Assistant @ ${time} UTC]: ${body}`;
45
+ }
46
+ // toolResult
47
+ const text = msg.content
48
+ .filter((b): b is TextContent => b.type === "text")
49
+ .map((b) => b.text)
50
+ .join("\n");
51
+ return `[Tool result for ${(msg as ToolResultMessage).toolName} @ ${time} UTC]: ${text}`;
52
+ })
53
+ .filter((line): line is string => line !== null)
54
+ .join("\n\n");
55
+ }
56
+
11
57
  export default function observationalMemory(pi: ExtensionAPI) {
12
58
  let config: Config = { ...DEFAULTS };
13
59
  let state: MemoryState = { observations: "", reflections: "" };
@@ -42,16 +88,22 @@ export default function observationalMemory(pi: ExtensionAPI) {
42
88
  compactInFlight = false;
43
89
  return;
44
90
  }
45
- ctx.compact({
46
- onComplete: () => {
47
- compactInFlight = false;
48
- if (ctx.hasUI) ctx.ui.notify("Observational memory: compaction complete", "info");
49
- },
50
- onError: (error) => {
51
- compactInFlight = false;
52
- if (ctx.hasUI) ctx.ui.notify(`Observational memory: ${error.message}`, "error");
53
- },
54
- });
91
+ try {
92
+ ctx.compact({
93
+ onComplete: () => {
94
+ compactInFlight = false;
95
+ if (ctx.hasUI) ctx.ui.notify("Observational memory: compaction complete", "info");
96
+ },
97
+ onError: (error) => {
98
+ compactInFlight = false;
99
+ if (ctx.hasUI) ctx.ui.notify(`Observational memory: ${error.message}`, "error");
100
+ },
101
+ });
102
+ } catch (error) {
103
+ compactInFlight = false;
104
+ const msg = error instanceof Error ? error.message : String(error);
105
+ if (ctx.hasUI) ctx.ui.notify(`Observational memory: compact threw: ${msg}`, "error");
106
+ }
55
107
  }, 0);
56
108
  });
57
109
 
@@ -79,10 +131,25 @@ export default function observationalMemory(pi: ExtensionAPI) {
79
131
  const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
80
132
  if (allMessages.length === 0) return;
81
133
 
82
- const conversationText = serializeConversation(convertToLlm(allMessages));
83
134
  const now = new Date();
84
- const dateStr = now.toISOString().split("T")[0];
85
- const timeStr = now.toTimeString().slice(0, 5);
135
+ const dateStr = utcDate(now.getTime());
136
+ const timeStr = utcTime(now.getTime());
137
+
138
+ const llmMessages = convertToLlm(allMessages);
139
+ const conversationText = serializeWithTimestamps(llmMessages);
140
+
141
+ let dateRangeNote = "";
142
+ if (llmMessages.length > 0) {
143
+ const timestamps = llmMessages.map((m) => m.timestamp);
144
+ const firstTs = timestamps.reduce((a, b) => Math.min(a, b));
145
+ const lastTs = timestamps.reduce((a, b) => Math.max(a, b));
146
+ const firstMsgDate = utcDate(firstTs);
147
+ const lastMsgDate = utcDate(lastTs);
148
+ dateRangeNote =
149
+ firstMsgDate === lastMsgDate
150
+ ? ` Messages in this batch are from ${firstMsgDate} (UTC).`
151
+ : ` Messages in this batch span ${firstMsgDate} to ${lastMsgDate} (UTC).`;
152
+ }
86
153
 
87
154
  ctx.ui.notify("Observational memory: running observer...", "info");
88
155
 
@@ -101,7 +168,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
101
168
  content: [
102
169
  {
103
170
  type: "text" as const,
104
- text: `Today is ${dateStr}, current time is ${timeStr}.\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations || "(none yet)"}\n</current-observations>\n\nCompress the following conversation into new observations:\n\n<conversation>\n${conversationText}\n</conversation>`,
171
+ text: `Today is ${dateStr}, current time is ${timeStr} UTC.${dateRangeNote}\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations || "(none yet)"}\n</current-observations>\n\nCompress the following conversation into new observations:\n\n<conversation>\n${conversationText}\n</conversation>`,
105
172
  },
106
173
  ],
107
174
  timestamp: Date.now(),
@@ -141,7 +208,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
141
208
  content: [
142
209
  {
143
210
  type: "text" as const,
144
- text: `Today is ${dateStr}.\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations}\n</current-observations>\n\nGarbage-collect these observations. Promote long-lived facts to reflections, prune what's no longer needed, keep what's still active.`,
211
+ text: `Today is ${dateStr} (UTC).\n\n<current-reflections>\n${state.reflections || "(none yet)"}\n</current-reflections>\n\n<current-observations>\n${state.observations}\n</current-observations>\n\nGarbage-collect these observations. Promote long-lived facts to reflections, prune what's no longer needed, keep what's still active.`,
145
212
  },
146
213
  ],
147
214
  timestamp: Date.now(),
@@ -209,8 +276,8 @@ export default function observationalMemory(pi: ExtensionAPI) {
209
276
  "",
210
277
  "── Parameters ──",
211
278
  `Observation threshold: ${config.observationThreshold.toLocaleString()}`,
212
- `Reflection threshold: ${config.reflectionThreshold.toLocaleString()}`,
213
- `Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction)`,
279
+ `Reflection threshold: ${config.reflectionThreshold.toLocaleString()} (interpreted as observations token budget)`,
280
+ `Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction, how many tokens in raw messages)`,
214
281
  ];
215
282
 
216
283
  ctx.ui.notify(lines.join("\n"), "info");
@@ -257,7 +324,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
257
324
  sections.push("");
258
325
  sections.push("── Raw Messages ──");
259
326
  if (rawMessages.length > 0) {
260
- sections.push(serializeConversation(convertToLlm(rawMessages)));
327
+ sections.push(serializeWithTimestamps(convertToLlm(rawMessages)));
261
328
  } else {
262
329
  sections.push("(none)");
263
330
  }
package/src/prompts.ts CHANGED
@@ -1,11 +1,11 @@
1
- export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations.
1
+ export const OBSERVER_SYSTEM = `You are an observation agent for a coding assistant. Compress conversation messages into concise, timestamped observations. Messages arrive pre-timestamped as \`[User @ HH:MM UTC]\`, \`[Assistant @ HH:MM UTC]\`, and \`[Tool result for <name> @ HH:MM UTC]\` — use those inline timestamps when assigning times to your observations. All timestamps are UTC.
2
2
 
3
3
  Format as a date-grouped log:
4
4
 
5
- Date: YYYY-MM-DD
5
+ Date: YYYY-MM-DD (UTC)
6
6
  - 🔴 HH:MM Observation text
7
- - 🔴 HH:MM Sub-observation
8
- - 🟡 HH:MM Sub-observation
7
+ - Sub-observation (no timestamp if same moment)
8
+ - Sub-observation
9
9
  - 🟢 HH:MM Another observation
10
10
 
11
11
  Priority levels:
@@ -159,7 +159,9 @@ Output EXACTLY two sections with these tags:
159
159
  [Surviving observations in the same date-grouped log format — most should be preserved]
160
160
  </observations>
161
161
 
162
- Do NOT wrap output in code blocks or markdown fences.`;
162
+ Do NOT wrap output in code blocks or markdown fences.
163
+
164
+ All timestamps in observations are UTC.`;
163
165
 
164
166
  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
167