pi-observational-memory 1.0.2 → 1.0.3

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.3",
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 localDate(epochMs: number): string {
12
+ const d = new Date(epochMs);
13
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
14
+ }
15
+
16
+ function localTime(epochMs: number): string {
17
+ const d = new Date(epochMs);
18
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
19
+ }
20
+
21
+ function serializeWithTimestamps(messages: Message[]): string {
22
+ return messages
23
+ .map((msg): string | null => {
24
+ const time = localTime(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}]: ${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}]: ${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}]: ${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,28 @@ 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));
134
+ // Bug 1: use local date, not UTC
83
135
  const now = new Date();
84
- const dateStr = now.toISOString().split("T")[0];
85
- const timeStr = now.toTimeString().slice(0, 5);
136
+ const dateStr = localDate(now.getTime());
137
+ const timeStr = localTime(now.getTime());
138
+
139
+ // Bug 2: serialize with per-message timestamps
140
+ const llmMessages = convertToLlm(allMessages);
141
+ const conversationText = serializeWithTimestamps(llmMessages);
142
+
143
+ // Bug 3: inform the Observer of the message date range
144
+ let dateRangeNote = "";
145
+ if (llmMessages.length > 0) {
146
+ const timestamps = llmMessages.map((m) => m.timestamp);
147
+ const firstTs = timestamps.reduce((a, b) => Math.min(a, b));
148
+ const lastTs = timestamps.reduce((a, b) => Math.max(a, b));
149
+ const firstMsgDate = localDate(firstTs);
150
+ const lastMsgDate = localDate(lastTs);
151
+ dateRangeNote =
152
+ firstMsgDate === lastMsgDate
153
+ ? ` Messages in this batch are from ${firstMsgDate}.`
154
+ : ` Messages in this batch span ${firstMsgDate} to ${lastMsgDate}.`;
155
+ }
86
156
 
87
157
  ctx.ui.notify("Observational memory: running observer...", "info");
88
158
 
@@ -101,7 +171,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
101
171
  content: [
102
172
  {
103
173
  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>`,
174
+ text: `Today is ${dateStr}, current time is ${timeStr}.${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
175
  },
106
176
  ],
107
177
  timestamp: Date.now(),
@@ -209,8 +279,8 @@ export default function observationalMemory(pi: ExtensionAPI) {
209
279
  "",
210
280
  "── Parameters ──",
211
281
  `Observation threshold: ${config.observationThreshold.toLocaleString()}`,
212
- `Reflection threshold: ${config.reflectionThreshold.toLocaleString()}`,
213
- `Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction)`,
282
+ `Reflection threshold: ${config.reflectionThreshold.toLocaleString()} (interpreted as observations token budget)`,
283
+ `Keep recent tokens: ${keepRecentTokens.toLocaleString()} (pi compaction, how many tokens in raw messages)`,
214
284
  ];
215
285
 
216
286
  ctx.ui.notify(lines.join("\n"), "info");
@@ -257,7 +327,7 @@ export default function observationalMemory(pi: ExtensionAPI) {
257
327
  sections.push("");
258
328
  sections.push("── Raw Messages ──");
259
329
  if (rawMessages.length > 0) {
260
- sections.push(serializeConversation(convertToLlm(rawMessages)));
330
+ sections.push(serializeWithTimestamps(convertToLlm(rawMessages)));
261
331
  } else {
262
332
  sections.push("(none)");
263
333
  }
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]\`, \`[Assistant @ HH:MM]\`, and \`[Tool result for <name> @ HH:MM]\` — use those inline timestamps when assigning times to your observations.
2
2
 
3
3
  Format as a date-grouped log:
4
4
 
5
5
  Date: YYYY-MM-DD
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: