pi-observational-memory-extension 0.1.2 → 0.2.0

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,13 +1,39 @@
1
1
  {
2
2
  "name": "pi-observational-memory-extension",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Mastra-style Observational Memory extension for Pi compaction and runtime context.",
5
5
  "license": "MIT",
6
6
  "author": "Nikita Nosov <20nik.nosov21@gmail.com>",
7
7
  "type": "module",
8
- "keywords": ["pi-package", "pi", "pi-coding-agent", "observational-memory", "memory", "compaction"],
9
- "files": ["extensions", "docs", "scripts", "README.md", "LICENSE", "CHANGELOG.md"],
10
- "pi": { "extensions": ["./extensions"] },
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi",
11
+ "pi-coding-agent",
12
+ "observational-memory",
13
+ "memory",
14
+ "compaction"
15
+ ],
16
+ "homepage": "https://github.com/nik1t7n/pi-observational-memory-extension#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/nik1t7n/pi-observational-memory-extension.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/nik1t7n/pi-observational-memory-extension/issues"
23
+ },
24
+ "files": [
25
+ "extensions",
26
+ "docs",
27
+ "scripts",
28
+ "README.md",
29
+ "LICENSE",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "pi": {
33
+ "extensions": [
34
+ "./extensions"
35
+ ]
36
+ },
11
37
  "peerDependencies": {
12
38
  "@earendil-works/pi-coding-agent": "*",
13
39
  "@earendil-works/pi-ai": "*",
@@ -24,6 +50,7 @@
24
50
  "typecheck": "tsc --noEmit",
25
51
  "validate": "node scripts/validate.mjs",
26
52
  "pack:check": "npm pack --dry-run",
27
- "prepublishOnly": "npm run validate && npm run typecheck && npm run pack:check"
53
+ "prepublishOnly": "npm run validate && npm run typecheck && npm test && npm run pack:check",
54
+ "test": "node scripts/test.mjs"
28
55
  }
29
56
  }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ const dist = ".tmp-test-dist";
10
+ await rm(dist, { recursive: true, force: true });
11
+ execFileSync("npx", ["tsc", "--outDir", dist, "--declaration", "false", "--noEmit", "false", "--rootDir", "."], { stdio: "inherit" });
12
+ const { __test } = await import(`../${dist}/extensions/index.js?${Date.now()}`);
13
+
14
+ // Secret redaction: state/debug/observer text must not persist common tokens.
15
+ const redacted = __test.redactSecrets("npm_abcdefghijklmnopqrstuvwxyz sk-abcdefghijklmnopqrstuvwxyz Bearer abcdefghijklmnopqrstuvwxyz token=supersecrettoken");
16
+ assert(!redacted.includes("npm_abcdefghijklmnopqrstuvwxyz"));
17
+ assert(!redacted.includes("sk-abcdefghijklmnopqrstuvwxyz"));
18
+ assert(!redacted.includes("Bearer abcdefghijklmnopqrstuvwxyz"));
19
+ assert(!redacted.includes("supersecrettoken"));
20
+ assert(redacted.includes("[REDACTED_NPM_TOKEN]"));
21
+
22
+ // Observer prompt limiting: previous observations are capped to a safe tail.
23
+ const huge = Array.from({ length: 5000 }, (_, i) => `* 🔴 old observation ${i}`).join("\n");
24
+ const prompt = __test.buildObserverTaskPrompt(huge, { priorCurrentTask: "continue tests" });
25
+ assert(prompt.length < huge.length / 2, "observer prompt should not include full previous observations");
26
+ assert(prompt.includes("truncated"));
27
+ assert(prompt.includes("continue tests"));
28
+
29
+ // Stale lock recovery: old operation locks are cleared and recorded as errors.
30
+ const state = __test.createDefaultState({ sessionId: "s", cwd: process.cwd() });
31
+ state.operationLock = { type: "observation", startedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString() };
32
+ state.status = "observing";
33
+ assert.equal(__test.recoverStaleOperationLock(state), true);
34
+ assert.equal(state.operationLock, undefined);
35
+ assert.equal(state.status, "idle");
36
+ assert.match(state.lastError, /Recovered stale OM operation lock/);
37
+
38
+ // Settings parser: full /om set surface mutates persisted config safely.
39
+ __test.applySetting(state, "observation-model", "google/gemini-2.5-flash");
40
+ __test.applySetting(state, "reflection-threshold", "12345");
41
+ __test.applySetting(state, "caveman", "on");
42
+ __test.applySetting(state, "attachments", "off");
43
+ __test.applySetting(state, "retrieval", "local");
44
+ __test.applySetting(state, "retrieval-top-k", "4");
45
+ __test.applySetting(state, "retrieval-threshold", "25%");
46
+ __test.applySetting(state, "scope", "project");
47
+ assert.equal(state.settings.observationModel, "google/gemini-2.5-flash");
48
+ assert.equal(state.thresholds.reflection, 12345);
49
+ assert.equal(state.settings.caveman, true);
50
+ assert.equal(state.settings.observeAttachments, "off");
51
+ assert.equal(state.settings.retrieval.provider, "local");
52
+ assert.equal(state.settings.retrieval.topK, 4);
53
+ assert.equal(state.settings.retrieval.threshold, 0.25);
54
+ assert.equal(state.scope, "project");
55
+ state.vectorIndex = { provider: "local", model: "local/hash-bow-v1", scopeKey: "session:old", updatedAt: new Date().toISOString(), entries: [] };
56
+ __test.applySetting(state, "scope", "session");
57
+ assert.equal(state.scope, "session");
58
+ assert.equal(state.vectorIndex, undefined);
59
+ assert.throws(() => __test.applySetting(state, "buffer-activation", "2"), /ratio/);
60
+
61
+ // Migration/normalization: v1 state keeps observations and receives v2 settings.
62
+ const migrated = __test.normalizeState({ version: 1, observations: "token=do-not-keep-this-secret", thresholds: { observation: 10 } }, { sessionId: "m", cwd: process.cwd() });
63
+ assert.equal(migrated.version, 3);
64
+ assert.equal(migrated.thresholds.observation, 10);
65
+ assert.equal(migrated.settings.caveman, false);
66
+ assert.equal(migrated.settings.observeAttachments, "auto");
67
+ assert.equal(migrated.settings.retrieval.provider, "off");
68
+ assert(!migrated.observations.includes("do-not-keep-this-secret"));
69
+
70
+ // Duplicate suppression and local retrieval: exact/similar repeats are skipped, relevant chunks rank first.
71
+ const deduped = __test.suppressDuplicateObservations("* 🔴 User likes Pi loopflows", "* 🔴 User likes Pi loopflows\n* 🟡 Vector retrieval implemented locally");
72
+ assert(!deduped.includes("User likes Pi loopflows"));
73
+ assert(deduped.includes("Vector retrieval"));
74
+ state.observations = "* 🔴 User builds Pi loopflows\n* 🟡 unrelated cooking note\n* ✅ Vector retrieval implemented locally";
75
+ state.vectorIndex = __test.buildLocalVectorIndex(state);
76
+ const retrieved = await __test.retrieveRelevantObservations({ }, state, "local vector retrieval for loopflows");
77
+ assert(retrieved.includes("Vector retrieval"));
78
+ assert(__test.formatVectorStatus(state).includes("provider: local"));
79
+
80
+ // Attachment observation: auto mode accepts small images, rejects non-images and oversized payloads.
81
+ state.settings.observeAttachments = "auto";
82
+ assert.equal(__test.shouldObserveAttachment({ type: "image", mimeType: "image/png", data: "abc" }, state), true);
83
+ assert.equal(__test.shouldObserveAttachment({ type: "image", mimeType: "application/pdf", data: "abc" }, state), false);
84
+ assert.equal(__test.shouldObserveAttachment({ type: "image", mimeType: "image/png", data: "x".repeat(3_000_000) }, state), false);
85
+ state.settings.observeAttachments = "off";
86
+ assert.equal(__test.shouldObserveAttachment({ type: "image", mimeType: "image/png", data: "abc" }, state), false);
87
+
88
+ // Attachment input builder: off/auto never leaks base64 into text; on + unsupported model hard-fails; Gemini model includes image part.
89
+ const attachmentEntry = {
90
+ id: "att-1",
91
+ type: "message",
92
+ timestamp: new Date().toISOString(),
93
+ message: { role: "user", content: [{ type: "text", text: "see image" }, { type: "image", mimeType: "image/png", data: "aGVsbG8=" }] },
94
+ };
95
+ state.settings.observationModel = "local/text-only";
96
+ state.settings.observeAttachments = "off";
97
+ let built = __test.buildObserverInput({ model: { provider: "local", id: "text-only" } }, [attachmentEntry], state);
98
+ assert(built.text.includes("Attachment omitted"));
99
+ assert(!built.text.includes("aGVsbG8="));
100
+ assert.equal(built.attachmentParts.length, 0);
101
+ state.settings.observeAttachments = "auto";
102
+ built = __test.buildObserverInput({ model: { provider: "local", id: "text-only" } }, [attachmentEntry], state);
103
+ assert(built.text.includes("unsupported observer model"));
104
+ assert.equal(built.attachmentParts.length, 0);
105
+ state.settings.observeAttachments = "on";
106
+ assert.throws(() => __test.buildObserverInput({ model: { provider: "local", id: "text-only" } }, [attachmentEntry], state), /unsupported observer model/);
107
+ state.settings.observationModel = "google/gemini-2.5-flash";
108
+ built = __test.buildObserverInput({ model: { provider: "google", id: "gemini-2.5-flash" } }, [attachmentEntry], state);
109
+ assert.equal(built.attachmentParts.length, 1);
110
+ assert.equal(built.attachmentParts[0].type, "image");
111
+
112
+ // Atomic write: writes valid JSON, creates backup on subsequent write, removes temp files.
113
+ const dir = await mkdtemp(join(tmpdir(), "pi-om-test-"));
114
+ try {
115
+ const file = join(dir, "state.json");
116
+ await __test.atomicWriteJson(file, { a: 1, accessToken: "supersecrettoken", observationTokens: 238, pendingMessageTokens: 12, observations: 'Secret check: api_key="sk-live-test-should-redact-1234567890" stays JSON-safe' });
117
+ const firstWrite = JSON.parse(await readFile(file, "utf8"));
118
+ assert.equal(firstWrite.accessToken, "[REDACTED_SECRET]");
119
+ assert.equal(firstWrite.observationTokens, 238);
120
+ assert.equal(firstWrite.pendingMessageTokens, 12);
121
+ assert(firstWrite.observations.includes("[REDACTED_SECRET]"));
122
+ assert(!firstWrite.observations.includes("sk-live-test-should-redact"));
123
+ await __test.atomicWriteJson(file, { a: 2 });
124
+ assert.equal(JSON.parse(await readFile(file, "utf8")).a, 2);
125
+ assert.equal(existsSync(`${file}.bak`), true);
126
+ } finally {
127
+ await rm(dir, { recursive: true, force: true });
128
+ await rm(dist, { recursive: true, force: true });
129
+ }
130
+
131
+ console.log("pi-observational-memory-extension tests passed");