@sovorn/pi-session-memory 0.1.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/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # pi-session-memory
2
+
3
+ [![Pi package](https://img.shields.io/badge/%CF%80-Pi_package-f0b429?style=flat-square)](https://pi.dev/packages/@sovorn/pi-session-memory)
4
+ [![npm](https://img.shields.io/npm/v/@sovorn/pi-session-memory?style=flat-square&logo=npm)](https://www.npmjs.com/package/@sovorn/pi-session-memory)
5
+ [![GitHub](https://img.shields.io/badge/GitHub-sovorn--c%2Fpi--session--memory-181717?style=flat-square&logo=github)](https://github.com/sovorn-c/pi-session-memory)
6
+
7
+ Session memory for [Pi](https://github.com/earendil-works/pi). It keeps a few short, source-linked notes for the coding session you have open. A local Laya model decides when a note is worth keeping and which note a later turn might need. Your configured Pi model writes the note only after you allow it for that session. If the extension or Laya fails, Pi keeps working with its normal context.
8
+
9
+ Generation is off by default. Install it, run `/memory status`, and nothing is generated.
10
+
11
+ ## What it does
12
+
13
+ - Stores observations and reflections in the current Pi session, linked to the original evidence.
14
+ - Uses local Laya decisions to select a small working set of notes for later turns.
15
+ - Lets the model request supporting notes or raw entries with `hydrate_session_memory`.
16
+ - Leaves Pi's history and compaction intact. On memory failures, Pi continues with native context.
17
+
18
+ This is an experiment, not cross-session search. No usefulness gain has been demonstrated yet. See [known limitations](#known-limitations) before enabling generation.
19
+
20
+ ## Install
21
+
22
+ This repository is already a Pi package. Pi loads `./src/extension.ts` directly, with no build step. Choose one source below; you do not need to install it more than once.
23
+
24
+ ### From GitHub
25
+
26
+ ```sh
27
+ pi install git:github.com/sovorn-c/pi-session-memory
28
+ ```
29
+
30
+ Pi clones the repository and records the source in your settings. To try it for one run without saving an install:
31
+
32
+ ```sh
33
+ pi -e git:github.com/sovorn-c/pi-session-memory
34
+ ```
35
+
36
+ ### npm
37
+
38
+ Install [`@sovorn/pi-session-memory`](https://www.npmjs.com/package/@sovorn/pi-session-memory) through Pi. The unscoped name belongs to a different project.
39
+
40
+ ```sh
41
+ pi install npm:@sovorn/pi-session-memory
42
+ ```
43
+
44
+ Pi manages the npm download and extension registration. `npm install` alone does not register the extension in Pi. The `pi-package` keyword makes a published package eligible for the [Pi package gallery](https://pi.dev/packages/@sovorn/pi-session-memory).
45
+
46
+ ### From a local checkout
47
+
48
+ ```sh
49
+ git clone https://github.com/sovorn-c/pi-session-memory.git
50
+ pi install /path/to/pi-session-memory
51
+ ```
52
+
53
+ Replace `/path/to/pi-session-memory` with the cloned directory. Pi records the path in `settings.json`; it does not copy the directory. To load only its entry for one run:
54
+
55
+ ```sh
56
+ pi -e /path/to/pi-session-memory/src/extension.ts
57
+ ```
58
+
59
+ After install, start Pi and run `/memory status`. It shows generation, config path, Python command, and token cadence. Installation does not install Python, Laya, or model weights. Complete [Laya setup](#laya-setup) before enabling generation.
60
+
61
+ ## Requirements
62
+
63
+ You need Pi and Python 3.11. This tree was checked on macOS arm64 with Pi 1.0.2, Node v26.7.0, and Python 3.11.15.
64
+
65
+ Laya 0.3.7 and its typed-decisions checkpoint are separate prerequisites. The worker checks the exact tested code revision and model weights before loading. See [Laya setup](#laya-setup) for those pins.
66
+
67
+ ## Configure
68
+
69
+ Optional. With no config file, generation stays off and the other defaults below apply.
70
+
71
+ The file is `<Pi agent dir>/pi-session-memory/config.json`. The agent directory is usually `~/.pi/agent`. `PI_CODING_AGENT_DIR` overrides it. You create the file yourself. The extension reads it when a session starts and never writes it. A missing file uses the defaults. A malformed file uses the defaults and warns once.
72
+
73
+ | Key | Type | Default |
74
+ | --- | --- | --- |
75
+ | `generation` | boolean | `false` |
76
+ | `python` | non-empty string, optional | `python3.11` |
77
+ | `observeAfterTokens` | integer >= 1 | `10000` |
78
+ | `reflectAfterTokens` | integer >= 1 | `20000` |
79
+
80
+ `PI_SESSION_MEMORY_PYTHON` overrides `python`. `PI_SESSION_MEMORY_LAYA_CHECKPOINT` overrides the checkpoint directory. For the Python command, the environment variable wins, then the config value, then `python3.11`.
81
+
82
+ How many notes can be considered (6, maximum 8) and how long a projected note can be (4000 characters, maximum 6000) are internal limits, not settings. Edit the file, then `/reload` or start Pi again.
83
+
84
+ Example, still off until you also confirm in the session:
85
+
86
+ ```json
87
+ {
88
+ "generation": false,
89
+ "python": "python3.11",
90
+ "observeAfterTokens": 10000,
91
+ "reflectAfterTokens": 20000
92
+ }
93
+ ```
94
+
95
+ ## Use
96
+
97
+ `/memory` with no arguments is the same as `/memory status`.
98
+
99
+ `/memory status` prints whether generation is on, the config path, the Python command, and the cadence. It does not start Laya and does not call your model.
100
+
101
+ `/memory on` enables generation for this session only. `/memory on` is not consent. The first time this session is about to write a memory, Pi asks you to confirm. Nothing is sent until you confirm.
102
+
103
+ `/memory off` disables generation for this session and drops a confirmation already stored for it. Neither command writes the config file or the session file.
104
+
105
+ A new session, a fork, a resume, or `/reload` clears that session switch. You are asked again before the next write.
106
+
107
+ When a later request needs an earlier note, local Laya may select one. Projected memory reaches the normal Pi provider inside the request Pi was already going to send. The model can call `hydrate_session_memory` to read a reflection, the observation it came from, or the exact linked raw entries. A missing link is reported. A replacement is not invented.
108
+
109
+ ## Disable and remove
110
+
111
+ `/memory off` turns generation off for the current session. The extension stays loaded.
112
+
113
+ `pi --no-extensions` disables every extension for that run, including this one if you installed it.
114
+
115
+ Remove the source you installed:
116
+
117
+ ```sh
118
+ # GitHub install:
119
+ pi remove git:github.com/sovorn-c/pi-session-memory
120
+
121
+ # npm install:
122
+ pi remove npm:@sovorn/pi-session-memory
123
+
124
+ # Local checkout:
125
+ pi remove /path/to/pi-session-memory
126
+ ```
127
+
128
+ Remove drops the package source from `settings.json`. Your session files and any Laya cache stay where they are.
129
+
130
+ ## What gets sent
131
+
132
+ Generation is off by default. A write needs both a switch and an interactive confirmation: `generation: true` in the config, or `/memory on`, and then your answer for that session. `/memory on` is not consent. Pi shows this question:
133
+
134
+ > Session-derived text and its source entry IDs will be sent to the currently configured Pi model/provider only after a local Laya gate accepts. Laya runs locally. The Pi session remains canonical; generated memories are appended as non-context session entries. Do you allow this for the current session?
135
+
136
+ There is one provider call site, `modelRegistry.complete`. Decline, and that write does not happen. Projected memory reaches the normal Pi provider as part of a request Pi was already sending. `hydrate_session_memory` returns entries already in the session and does not call the provider.
137
+
138
+ The extension runs with Pi's permissions. Behavior lives under `src/` and `worker/`. A static scan of `src/` and `worker/` finds no telemetry client and no network client. Recorded checks set `PI_OFFLINE=1`, `PI_TELEMETRY=0`, `HF_HUB_OFFLINE=1`, and `TRANSFORMERS_OFFLINE=1`.
139
+
140
+ ## What one trial showed
141
+
142
+ One synthetic trial compared a native arm with a memory arm. The native arm ran first. The result was no demonstrated benefit. The keyword oracle did not show a gain. Token usage was not recorded. The model was `openai-codex/gpt-6-luna` at thinking low, with 8 provider calls and 3 Laya decisions.
143
+
144
+ ## Known limitations
145
+
146
+ - Checked on macOS arm64, English, and one Pi session. There is no cross-session memory.
147
+ - Provider-backed memory formation is not verified on Pi 1.0.2. It was last verified on Pi 0.87.1. Pi 1.0.2 checks so far used generation off, in no-provider runs.
148
+ - In RPC mode the confirmation waits for the client's answer.
149
+ - The first Laya load can take seconds. Laya confidence is uncalibrated for some choices.
150
+ - No build, lint, typecheck, or CI is configured. Laya setup is machine-specific.
151
+ - This is experimental. A local source security review found no blocking findings; it does not establish provider-backed compatibility.
152
+
153
+ ## Laya setup
154
+
155
+ Point `PI_SESSION_MEMORY_PYTHON` at a Python 3.11 interpreter that can import Laya. The pins below identify the tested dependency and model file. Pi and npm do not require these pins; this worker does. A mismatch leaves Pi running without memory.
156
+
157
+ <details>
158
+ <summary>Pinned Laya installation and model verification</summary>
159
+
160
+ Laya is 0.3.7 at commit `010bacef009c855ccba814b51f7c8e1d38ab5e3f`. The checkpoint revision is `f9ab0b228f0fc0f14d873dbc99038f135c2da1b2`. The `model.safetensors` SHA-256 is `4fa56de72383a9d3efa9cfa78955733c81b9fc8067a587ca4beb82c78107a24e`.
161
+
162
+ This VCS-form install is not run by this project:
163
+
164
+ ```sh
165
+ uv pip install "laya @ git+https://github.com/NandhaKishorM/laya@010bacef009c855ccba814b51f7c8e1d38ab5e3f"
166
+ ```
167
+
168
+ No download command is verified here. The checkpoint is the Hugging Face cache snapshot `models--convaiinnovations--laya-typed-decisions/snapshots/f9ab0b228f0fc0f14d873dbc99038f135c2da1b2`. The hub directory is `HF_HUB_CACHE`, otherwise `$HF_HOME/hub`, otherwise `$XDG_CACHE_HOME/huggingface/hub`, otherwise `~/.cache/huggingface/hub`. `PI_SESSION_MEMORY_LAYA_CHECKPOINT` overrides that directory. Check the weights with `shasum -a 256 model.safetensors` and compare it with the SHA-256 above. To refuse a hub fetch, set `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1`.
169
+
170
+ </details>
171
+
172
+ ## Check your install
173
+
174
+ Start Pi and run `/memory status`. This checks that the extension loaded without starting Laya or sending a provider request. Use `/memory on` only after setup; review the confirmation before allowing generation.
175
+
176
+ <details>
177
+ <summary>Developer checks from a source checkout</summary>
178
+
179
+ The first block needs no Laya cache and sends no provider request. It installs the package into a temporary Pi agent directory, checks that `/memory` is listed, checks that `pi --no-extensions` hides it, then removes it. The second block needs the two environment variables. The doc test skips that block when they are absent.
180
+
181
+ <!-- verify-install:start -->
182
+ ```sh
183
+ set -euo pipefail
184
+ agent="$(mktemp -d)"
185
+ trap 'rm -rf "$agent"' EXIT
186
+ export PI_CODING_AGENT_DIR="$agent"
187
+ export PI_OFFLINE=1
188
+ pi --version
189
+ node --version
190
+ pi install "$PWD"
191
+ loaded="$(printf '%s\n' '{"type":"get_commands","id":"commands"}' | pi --mode rpc --no-session)"
192
+ printf '%s\n' "$loaded" | grep -F '"name":"memory"' >/dev/null
193
+ disabled="$(printf '%s\n' '{"type":"get_commands","id":"commands"}' | pi --mode rpc --no-session --no-extensions)"
194
+ if printf '%s\n' "$disabled" | grep -F '"name":"memory"' >/dev/null; then
195
+ echo "memory listed while extensions are disabled" >&2
196
+ exit 1
197
+ fi
198
+ pi remove "$PWD"
199
+ removed="$(printf '%s\n' '{"type":"get_commands","id":"commands"}' | pi --mode rpc --no-session)"
200
+ if printf '%s\n' "$removed" | grep -F '"name":"memory"' >/dev/null; then
201
+ echo "memory still listed after remove" >&2
202
+ exit 1
203
+ fi
204
+ ```
205
+ <!-- verify-install:end -->
206
+
207
+ <!-- verify-laya:start -->
208
+ ```sh
209
+ set -euo pipefail
210
+ : "${PI_SESSION_MEMORY_PYTHON:?set PI_SESSION_MEMORY_PYTHON}"
211
+ : "${PI_SESSION_MEMORY_LAYA_CHECKPOINT:?set PI_SESSION_MEMORY_LAYA_CHECKPOINT}"
212
+ unset NODE_TEST_CONTEXT
213
+ "$PI_SESSION_MEMORY_PYTHON" -m unittest discover -s worker/tests -p 'test_*.py'
214
+ node --test test/pi/projection.test.ts
215
+ ```
216
+ <!-- verify-laya:end -->
217
+
218
+ </details>
219
+
220
+ ## Status
221
+
222
+ Experimental. A local source security review found no blocking findings. This is not a guarantee of security. No license has been selected; publication alone does not grant permission to reuse the code. GitHub, npm, and local-path installation use the same Pi package manifest.
223
+
224
+ Report problems or discuss the experiment in [GitHub issues](https://github.com/sovorn-c/pi-session-memory/issues). Include your Pi version, Python version, and `/memory status` output. Do not post session text, credentials, or private paths.
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@sovorn/pi-session-memory",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Pi extension that keeps small, source-linked memories of a coding session.",
6
+ "homepage": "https://github.com/sovorn-c/pi-session-memory#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sovorn-c/pi-session-memory.git"
10
+ },
11
+ "bugs": { "url": "https://github.com/sovorn-c/pi-session-memory/issues" },
12
+ "files": ["src/", "worker/*.py"],
13
+ "publishConfig": { "access": "public" },
14
+ "keywords": ["pi-package", "pi-extension", "session-memory"],
15
+ "pi": { "extensions": ["./src/extension.ts"] },
16
+ "peerDependencies": { "@earendil-works/pi-coding-agent": "*" }
17
+ }
package/src/config.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export const DEFAULT_GENERATION = false;
5
+ export const DEFAULT_OBSERVE_AFTER_TOKENS = 10_000;
6
+ export const DEFAULT_REFLECT_AFTER_TOKENS = 20_000;
7
+ export const DEFAULT_PYTHON = "python3.11";
8
+ export const MAX_CONFIG_BYTES = 16_384;
9
+
10
+ export const CONFIG_KEYS = {
11
+ generation: { type: "boolean", default: DEFAULT_GENERATION },
12
+ python: { type: "string", optional: true },
13
+ observeAfterTokens: { type: "integer", default: DEFAULT_OBSERVE_AFTER_TOKENS },
14
+ reflectAfterTokens: { type: "integer", default: DEFAULT_REFLECT_AFTER_TOKENS },
15
+ } as const;
16
+
17
+ export interface MemorySettings {
18
+ generation: boolean;
19
+ python?: string;
20
+ observeAfterTokens: number;
21
+ reflectAfterTokens: number;
22
+ }
23
+
24
+ export type ConfigState = "missing" | "found" | "unusable";
25
+ export type PythonSource = "env" | "config" | "default";
26
+
27
+ export interface LoadedMemoryConfig {
28
+ path: string;
29
+ settings: MemorySettings;
30
+ notice: string | null;
31
+ state: ConfigState;
32
+ }
33
+
34
+ export interface PythonResolution {
35
+ command: string;
36
+ source: PythonSource;
37
+ }
38
+
39
+ const KNOWN_KEYS = ["generation", "python", "observeAfterTokens", "reflectAfterTokens"] as const;
40
+
41
+ export function configPath(agentDir: string): string {
42
+ return join(agentDir, "pi-session-memory", "config.json");
43
+ }
44
+
45
+ export function loadMemoryConfig(agentDir: string): LoadedMemoryConfig {
46
+ const path = configPath(agentDir);
47
+ const missing: LoadedMemoryConfig = { path, settings: defaultSettings(), notice: null, state: "missing" };
48
+ let size = 0;
49
+ try {
50
+ const info = statSync(path);
51
+ if (info.isDirectory()) return unusable(path, "config path is a directory");
52
+ size = info.size;
53
+ } catch (error) {
54
+ if (errorCode(error) === "ENOENT") return missing;
55
+ return unusable(path, "config file is unreadable");
56
+ }
57
+ if (size > MAX_CONFIG_BYTES) return unusable(path, "config is larger than 16384 bytes");
58
+
59
+ let text: string;
60
+ try {
61
+ text = readFileSync(path, "utf8");
62
+ } catch {
63
+ return unusable(path, "config file is unreadable");
64
+ }
65
+ if (text.length === 0) return unusable(path, "config file is empty");
66
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
67
+
68
+ let parsed: unknown;
69
+ try {
70
+ parsed = JSON.parse(text);
71
+ } catch {
72
+ return unusable(path, "malformed JSON");
73
+ }
74
+ if (Array.isArray(parsed) || typeof parsed === "string" || !isRecord(parsed)) {
75
+ return unusable(path, "config root must be an object");
76
+ }
77
+ return applyObject(path, parsed);
78
+ }
79
+
80
+ export function resolvePython(env: { PI_SESSION_MEMORY_PYTHON?: string }, settings: MemorySettings): PythonResolution {
81
+ const fromEnv = env.PI_SESSION_MEMORY_PYTHON;
82
+ if (typeof fromEnv === "string" && fromEnv.length > 0) return { command: fromEnv, source: "env" };
83
+ if (settings.python && settings.python.trim().length > 0) return { command: settings.python.trim(), source: "config" };
84
+ return { command: DEFAULT_PYTHON, source: "default" };
85
+ }
86
+
87
+ function applyObject(path: string, parsed: Record<string, unknown>): LoadedMemoryConfig {
88
+ const settings = defaultSettings();
89
+ const reasons: string[] = [];
90
+ const unknown = Object.keys(parsed).filter((key) => !KNOWN_KEYS.includes(key as typeof KNOWN_KEYS[number])).sort();
91
+ if (unknown.length > 0) reasons.push(`ignored unknown keys: ${unknown.join(", ")}`);
92
+
93
+ if ("generation" in parsed) {
94
+ if (typeof parsed.generation === "boolean") settings.generation = parsed.generation;
95
+ else {
96
+ settings.generation = false;
97
+ reasons.push("generation: expected boolean");
98
+ }
99
+ }
100
+ if ("python" in parsed) {
101
+ if (typeof parsed.python === "string" && parsed.python.trim().length > 0) settings.python = parsed.python.trim();
102
+ else reasons.push("python: expected non-empty string");
103
+ }
104
+ if ("observeAfterTokens" in parsed) settings.observeAfterTokens = readCadence(parsed.observeAfterTokens, "observeAfterTokens", reasons);
105
+ if ("reflectAfterTokens" in parsed) settings.reflectAfterTokens = readCadence(parsed.reflectAfterTokens, "reflectAfterTokens", reasons);
106
+
107
+ const hardFailure = reasons.some((reason) => !reason.startsWith("ignored unknown keys:"));
108
+ return {
109
+ path,
110
+ settings,
111
+ notice: reasons.length > 0 ? reasons.join("; ") : null,
112
+ state: hardFailure ? "unusable" : "found",
113
+ };
114
+ }
115
+
116
+ function readCadence(value: unknown, key: string, reasons: string[]): number {
117
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) return value;
118
+ reasons.push(`${key}: expected integer >= 1`);
119
+ return key === "reflectAfterTokens" ? DEFAULT_REFLECT_AFTER_TOKENS : DEFAULT_OBSERVE_AFTER_TOKENS;
120
+ }
121
+
122
+ function defaultSettings(): MemorySettings {
123
+ return {
124
+ generation: DEFAULT_GENERATION,
125
+ observeAfterTokens: DEFAULT_OBSERVE_AFTER_TOKENS,
126
+ reflectAfterTokens: DEFAULT_REFLECT_AFTER_TOKENS,
127
+ };
128
+ }
129
+
130
+ function unusable(path: string, notice: string): LoadedMemoryConfig {
131
+ return { path, settings: defaultSettings(), notice, state: "unusable" };
132
+ }
133
+
134
+ function errorCode(error: unknown): string | undefined {
135
+ return isRecord(error) && typeof error.code === "string" ? error.code : undefined;
136
+ }
137
+
138
+ function isRecord(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
140
+ }
@@ -0,0 +1,10 @@
1
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerFormation } from "./formation.ts";
3
+ import { registerHydration } from "./hydration.ts";
4
+
5
+ export { CONFIRM_TITLE, DISCLOSURE } from "./formation.ts";
6
+
7
+ export default function (pi: ExtensionAPI): void {
8
+ registerFormation(pi, { agentDir: () => getAgentDir() });
9
+ registerHydration(pi);
10
+ }