pi-lossless 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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # pi-lossless
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ This software is an original implementation of the Lossless Context Management
4
+ (LCM) architecture, specified in the paper "Lossless Context Management" by
5
+ Clint Ehrlich and Theodore Blackman, Voltropy PBC
6
+ (https://papers.voltropy.com/LCM). The paper is credited as the algorithmic
7
+ specification; all code in this repository was written from that spec.
8
+
9
+ The "smart zone" framing (quality degradation at a token budget, not the
10
+ window limit) is credited to Matt Pocock, AI Coding Dictionary
11
+ (https://www.aihero.dev/ai-coding-dictionary/smart-zone).
12
+
13
+ Copyright (c) 2026 stainless-code
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # pi-lossless
2
+
3
+ **Lossless Context Management (LCM) for the Pi coding agent. Nothing is ever lost, and the model sees less.**
4
+
5
+ Install it, and a long session keeps working past the point where the model would
6
+ normally forget, while you can still read what it forgot. The extension
7
+ implements the LCM architecture (Ehrlich & Blackman, Voltropy). Aged
8
+ conversation becomes DAG summaries the model can navigate, while the full
9
+ verbatim history stays searchable and recoverable through `lcm_grep` /
10
+ `lcm_describe` / `lcm_expand_query`. Your session file is never modified, and the
11
+ store is a derived index you can delete and rebuild.
12
+
13
+ This package is the Pi adapter over
14
+ [`lossless-core`](https://npmx.dev/package/lossless-core), the engine that
15
+ ingests the history and builds the summaries.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pi install npm:pi-lossless
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ Zero config required, passive logging starts immediately, and swaps begin
26
+ once context crosses the soft threshold (default: 70% of the model's window).
27
+
28
+ ```bash
29
+ /lcm status # current projection state (messages, pin, occupancy, thresholds);
30
+ # the default subcommand, so bare /lcm is this
31
+ /lcm settings # the LCM Cockpit, live gauge, cycle-edit every threshold
32
+ /lcm backup # copy the live store to a timestamped file
33
+ /lcm export # write the full store as JSONL (verbatim; see Security notes)
34
+ /lcm report # metrics summary for the current session
35
+ /lcm doctor # diagnostics: store checks, thresholds, summarizer chain,
36
+ # footprint; add --repair to rebuild the index and drop the rows
37
+ # nothing can reach
38
+ /lcm gc # dry-run list of retired stores; add --apply to export then
39
+ # DELETE them (destructive; the export is written beside the
40
+ # stores as lcm-export-gc-<timestamp>.jsonl before any deletion)
41
+ /lcm migrate # dry-run list of stores left at an older schema; add --apply to
42
+ # upgrade them in place so cross-session search can read them
43
+ /lcm diagnose # one pasteable block for a bug report (see Reporting a problem);
44
+ # add --save <path> to write it to a file instead
45
+ ```
46
+
47
+ The model gets three recall tools: `lcm_grep` (search across ALL history,
48
+ including compacted-out content, and with `scope` across past sessions),
49
+ `lcm_describe` (inspect a summary, what reading its originals would cost, and the
50
+ files it covers), and `lcm_expand_query` (read the stored originals and answer
51
+ from them). Every summary in context carries an engine-written
52
+ `[lcm:summary #3 depth 0 span e12..e40]` header, and every grep hit names the
53
+ summary that covers it.
54
+
55
+ `lcm_grep` takes one of two things. `query` is a full-text search over the
56
+ token index: words, quoted phrases, `AND`/`OR`, prefixes. `pattern` is a
57
+ regular expression over the full text of every message, which is what a pointer,
58
+ a call, or a span needs, because an index cannot hold punctuation. A pattern is
59
+ case-insensitive unless `case_sensitive` is set. Both paths page: hits come 20
60
+ at a time, `offset` reaches the next page, and the query path reports its total
61
+ while the pattern path says whether more matches exist rather than claiming a
62
+ count it did not compute.
63
+
64
+ All three take an address when you have one, so a known id does not have to be
65
+ searched for: `lcm_grep` accepts `summary_id` to search inside one node, and
66
+ `lcm_expand_query` accepts `summary_id` to read a node's originals or
67
+ `entry_id` to read one message. An address the store does not hold is refused
68
+ before any model call.
69
+
70
+ `lcm_expand_query` runs a bounded reader: it expands the summaries in its own
71
+ context and returns findings, so a 200,000-character message is readable
72
+ without that text entering the main conversation. The default budget is 10,000
73
+ tokens of retrieved content over at most 6 reader turns; `max_tokens` changes
74
+ the budget per call.
75
+
76
+ ## Cross-session recall
77
+
78
+ A new session starts empty. `lcm_grep` reaches the ones before it with
79
+ `scope`: `"sessions"` searches this project's past sessions, `"all_sessions"`
80
+ every project, and both walk the most recent 20 past sessions and grep their
81
+ stores under a wall-clock deadline. Nothing is injected: a hit from a past session is a
82
+ pointer, labelled with that session, its date and its working directory, and the
83
+ model reads it with `lcm_expand_query({ session: "<hash>", entry_id: "<id>" })`,
84
+ which returns findings tagged as a past session's history. Cross-session recall
85
+ is therefore always a tool call the model chose, never text that appeared by
86
+ itself. A search stops at 20 stores, 40 hits, or 1.5 seconds, whichever comes
87
+ first.
88
+
89
+ The scan reads session files for their one-line header (`cwd`, id, timestamp)
90
+ and opens their stores read-only, so it never migrates, repairs or writes
91
+ anything it searches. A store written by an older build cannot answer this
92
+ build's queries and is counted as `older-generation` rather than upgraded in
93
+ place: `/lcm migrate` upgrades those stores, and a store becomes searchable
94
+ without it as soon as its own session next runs.
95
+
96
+ A store `/lcm gc` deletes stops being reachable by later searches: it is not only
97
+ that session's index, it is a past session the cross-session scope can no longer
98
+ read. The transcript export gc writes before deleting keeps the bytes, and
99
+ nothing re-indexes it. That export goes beside the stores it protects, next to
100
+ `/lcm backup`'s copies, never into the directory Pi was started from: it holds
101
+ stored bytes, secrets included.
102
+
103
+ ## Config (`~/.pi/agent/lcm.json`)
104
+
105
+ ```json
106
+ {
107
+ "smartZone": 110000,
108
+ "zones": {
109
+ "openrouter/z-ai/glm-5.3-flash": { "swapAtTokens": 110000, "recutAtTokens": 130000 }
110
+ },
111
+ "swapAtRatio": 0.7,
112
+ "recutAtRatio": 0.85,
113
+ "summarizer": ["openrouter/z-ai/glm-5.3-flash", "anthropic/claude-haiku-4-5", "auto"]
114
+ }
115
+ ```
116
+
117
+ - `smartZone`: easy mode, one number: swap at that many tokens; the recut
118
+ point is derived (× 1.18).
119
+ - `zones`: per-model threshold overrides, keyed `"provider/id"` (thresholds
120
+ only).
121
+ - `swapAtRatio` / `recutAtRatio`: ratio mode, the default when no token
122
+ thresholds are set.
123
+ - `summarizer`: one model (`"provider/id"`) or an ordered fallback chain. There
124
+ is no comma form, because a model id may contain a comma. `auto` is the
125
+ session model and nothing else, so it never means "pick something cheap".
126
+ Unset behaves as `auto`. The chain is tried in order on a model that is
127
+ missing, fails, or returns no text, and the session model is the last resort
128
+ unless the chain already names it. An entry that fails is demoted to the back
129
+ of the order for the rest of that compaction pass, so a broken primary is
130
+ paid for once per pass rather than once per chunk. A failure is a throw, a
131
+ response the
132
+ provider marked `error`, or one that stopped at the token cap (`length`),
133
+ which would otherwise store a half-finished summary. Duplicate and empty
134
+ entries are dropped, and an entry the registry cannot find is reported once
135
+ per session and recorded as a `summarizer-model` event. A chain that needed
136
+ more than one model records a `summarizer-fallback` event, which `/lcm
137
+ report` shows as `model-fallback`. `/lcm doctor` prints the chain that
138
+ actually runs, next to the config it came from, so `auto` expansion and the
139
+ appended session model are visible rather than implied. A substitution also
140
+ raises one UI warning per session.
141
+
142
+ - `largeFileChars` / `externalizeFiles`: a body read from a named path and at
143
+ least this many characters long becomes a file handle (path, content id, kind,
144
+ and a bounded preview) rather than a wall of text, and the summarizer reads
145
+ the handle instead of a clipped body. The bytes stay in the message row.
146
+ Default 32,000 characters, on.
147
+ - `summarizerTokensPerCall` / `summarizerTokensPerPass`: what one summarizer
148
+ request may cost before it is refused instead of paid for, and what one pass may
149
+ spend across all of its calls. The request's size is known before the call, so a
150
+ refusal happens without a provider call, and the pass total counts what the
151
+ provider reported. Both are backstops against a provider that answers and never
152
+ stops, and a pass that respects `summaryTokens` and `leafChunkTokens` spends a
153
+ fraction of either cap.
154
+ A pass that crosses one finishes its remaining chunks with the mechanical
155
+ truncate and records `summarizer-capped` with `source: "budget"`.
156
+
157
+ The file must be valid strict JSON, so `//` comments and trailing commas are
158
+ rejected, and a corrupt file is left untouched with defaults applied. A number
159
+ outside the range of its key is ignored like a wrong type, so a hand-edited
160
+ `"summaryTokens": 0` falls back to the default instead of condensing the whole
161
+ DAG on every pass. Anything the parse had to normalize or ignore is reported at
162
+ session start, or recorded as a `config-problem` metric when there is no UI to
163
+ report it in.
164
+ Threshold vocabularies are OR-mode per level (ratios / smartZone / token pair);
165
+ explicit tokens win. A safety clamp keeps the loop alive on small windows.
166
+
167
+ - An overwrite copies the file it replaces to `lcm.json.bak` first, and never
168
+ replaces a backup that already exists, so a mistargeted write stays
169
+ recoverable. Nothing else in LCM deletes this file.
170
+ - A process that looks like a test run (`NODE_ENV=test`, `VITEST`, or the
171
+ `LCM_TEST_GUARD` the test setup sets) refuses to touch a home that is not a
172
+ temp dir. `LCM_ALLOW_REAL_HOME=1` overrides that refusal for a live session
173
+ that inherits a test signal.
174
+
175
+ ## Reporting a problem
176
+
177
+ Run `/lcm diagnose` and paste the block it prints into the issue. It carries
178
+ what a diagnosis needs from this session and nothing else:
179
+
180
+ - this package's version, Pi's version, node, platform, arch, and the mode
181
+ the session runs in (TUI, print or RPC)
182
+ - the store tag (which is also the `session` field on every metric row), the
183
+ session file's basename, and the session file format version
184
+ - the store facts `/lcm doctor` prints: counts, size, integrity findings, the
185
+ depth histogram, the pinned projection, the thresholds, the model and the
186
+ summarizer chain
187
+ - the effective config, merged over the defaults, as JSON, and the config path
188
+ - this session's metrics: the row count, a tally by kind, the last context
189
+ decision, and up to 25 of the most recent rows as JSONL, with the count it
190
+ left out always printed
191
+
192
+ The block is capped at 8 KB. It carries no message body, no summary text, no
193
+ search query, and no absolute path outside your home directory; a path inside
194
+ it prints with `~`. Provider and host error text is masked (see Security
195
+ notes), so a key in a failure message cannot leave. Everything else in the
196
+ block is this package's own vocabulary: version numbers, counters, model ids,
197
+ entry ids and config values. Read it once before posting, and strip anything
198
+ personal.
199
+
200
+ A notification is rendered, not written, so your terminal wraps it and a copy
201
+ carries those breaks: a metric row can arrive split across two lines. Write the
202
+ same block to a file when the exact bytes matter, for example to attach it or
203
+ to parse the rows:
204
+
205
+ ```
206
+ /lcm diagnose --save lcm-diagnose.txt
207
+ ```
208
+
209
+ The file holds the bytes the notification would have carried, plus a final
210
+ newline, written at mode 0600; the notification names the path instead of
211
+ printing the block. `--save=<path>` is the same flag. `--save` with no path is
212
+ refused rather than quietly printing the block.
213
+
214
+ ## Security notes
215
+
216
+ - Stores are plaintext SQLite files under `~/.pi/agent/lcm/`, chmod 0600. A
217
+ store is the only copy of what the session produced, so it holds the secrets
218
+ the session contained, verbatim and unrewritten.
219
+ - Redaction is best-effort pattern matching, not a hard security boundary. It
220
+ runs on the way out, at every seam that hands stored text to a model, and it
221
+ never touches a stored row. Provider key shapes are matched exactly. An
222
+ assignment is rewritten when its value carries a digit, or when it is written
223
+ the way an env file writes it (`KEY=value`, no space before the `=`). A secret
224
+ of letters only, or a bare one broken up by punctuation such as a dot, passes
225
+ through.
226
+ - `redactSecrets: false` disables masking on every outbound seam. It changes
227
+ nothing on disk, because storage never consults it. A row that a read masks is
228
+ still in the store, and a later read with the toggle off returns it.
229
+ - The session file is the source of truth: delete the store DB under
230
+ `~/.pi/agent/lcm/` and restart to rebuild the index.
231
+ - `/lcm export`, `/lcm backup` and gc transcripts are store surfaces, so they
232
+ carry the stored bytes, secrets included. They are your own data and deserve
233
+ the same care as the original session file. An export carries committed
234
+ memory, so a pass that is still running is not in it, and an import is refused
235
+ until that pass finishes.
236
+ - Cross-session search reads files outside this session: line 1 of every session
237
+ file in the sessions root (its `cwd`, id and timestamp, never a message), and
238
+ the stores of the sessions that match the scope, opened read-only with writes
239
+ forbidden on the connection. Findings pass the same redaction as in-session
240
+ recall, and nothing from a past session is injected or stored into this one.
241
+ `/lcm migrate` is the only path that writes to a past session's store, and it
242
+ is explicit and dry-run by default.
243
+
244
+ ## How it works
245
+
246
+ - **Passive logging** (always): every turn ingests messages idempotently into
247
+ SQLite+FTS5, zero-cost continuity below the swap point.
248
+ - **Async regime**: past the swap point, the `context` hook swaps the aged
249
+ head for a summary pointer between turns. Swaps are commit-batched, the
250
+ projection stays byte-identical (pure appends, provider-cache-safe) until
251
+ the recut line, then one monotonic re-cut.
252
+ - **Blocking regime**: at Pi's own compaction limit, LCM replaces default
253
+ compaction with escalated DAG summaries. It never cancels, and on failure it
254
+ falls back to the Pi default.
255
+ - **Recursive DAG** (paper §2.1): each compaction summarizes only the
256
+ messages no leaf covers yet, then condenses the frontier one depth at a
257
+ time. A group is a run of adjacent nodes that share a depth: the oldest such
258
+ run at the shallowest depth that has one, at most `MAX_DAG_FANOUT` (8)
259
+ children, repeated while the pointer block is over the summary budget.
260
+ Nothing is summarized twice; every node expands back to its verbatim
261
+ messages through provenance. The projection carries every frontier id: when
262
+ the text does not fit the budget it is stubbed (header, a 200-character
263
+ slice, and a note naming `lcm_describe`) rather than dropped, and a frontier
264
+ that is still over budget is reported instead of resolved by eviction. The
265
+ depth cap of 8 is a structural assert, not a policy, and with 8 children per
266
+ node it covers 8^8 leaf chunks; a pass that cannot meet its budget records
267
+ `condensation-stalled`. The engine writes `[lcm:summary #id depth d span
268
+ a..b]` next to every summary the model sees (paper §2.4).
269
+ - **Three-level escalation** (paper Fig. 3): preserve-details → bullets →
270
+ deterministic truncate of the source text, accepted only on strict token
271
+ reduction. Level 3 is stored like any other level (tagged
272
+ `[lcm:truncated]`), which is what guarantees convergence. Level 2 keeps
273
+ level 1's output budget instead of a fraction of it, because a smaller cap
274
+ makes a reasoning model likelier to answer nothing, and level 3 already
275
+ covers the floor. A node whose text lands over twice its target earns one
276
+ level-2 make-smaller pass; the smaller text becomes the primary one and the
277
+ thorough text is kept as the node's rich tier.
278
+ - **Recall is one delegation, not a context flood**: `lcm_grep` and
279
+ `lcm_describe` run in the main loop, and reading originals goes through
280
+ `lcm_expand_query`, whose reader expands them in its own context and returns
281
+ findings. The reader reads at most `max_tokens` (default 10,000) over at most
282
+ 6 turns, and it pages inside a single message, so nothing stored is out of
283
+ reach. Paper Appendix C restricts raw expansion to a sub-agent; Pi has no
284
+ `Task` tool, so the reader is an in-process loop with its own context
285
+ and no write access to the store.
286
+ - **Raw blocks**: the derived text is what the index and the summarizer read,
287
+ and it is not always the whole message. A thinking block, an image, a
288
+ signature, or a block type the renderer does not know is kept as a payload on
289
+ the row, and a read returns it. A message whose derived text is complete
290
+ carries no payload, so the common case stays one copy. Payload size is
291
+ reported by `/lcm doctor`; a row written before this build keeps only the
292
+ derived text, and deleting the store and restarting rebuilds it from the
293
+ session file.
294
+ - **Tiered summaries**: when the ladder picks its terse rung over a thorough
295
+ one, the node keeps the thorough text as a second tier. The projection serves
296
+ the richest tier that fits its budget, so a frontier under pressure drops a
297
+ node to its shorter text before it drops to a 200-character stub, and the
298
+ pointer survives every rung. `lcm_describe` returns the thorough text and
299
+ names both sizes.
300
+ - **File handles (paper §2.2)**: a tool result above `largeFileChars` whose call
301
+ named a path becomes a file. The store keeps a descriptor (path, content id,
302
+ the kind the body turned out to be, an exploration summary, and a bounded head
303
+ of the body) beside the verbatim text, the summarizer reads that descriptor
304
+ instead of a clipped body, and the handle propagates up the DAG, so
305
+ `lcm_describe` on a condensed node lists the files its children cover, each
306
+ with the first line of its summary. The exploration summary is built by rule,
307
+ with no model call and no parser: code names its top-level declarations
308
+ (`TypeScript, 240 lines; 18 top-level declaration(s): function compact, class
309
+ Engine, ...`), JSON names its shape and one level of nesting (`array of 412
310
+ objects, keys: id, name`), JSONL names its row keys, a table names its columns,
311
+ Markdown names its headings, and text names its size and first line. A family
312
+ with no rule reports its size rather than guessing, names are capped at 12 with
313
+ `(+N more)`, and the whole summary line stays under a line of the preview. A body that large with no path is
314
+ stored as ordinary text and recorded as `large-inline`, so "no files here" is
315
+ told apart from a trigger that missed.
316
+ - **Procedural long-context evaluation (paper §4, §5)**: generated context of
317
+ any length with a known answer. A seeded generator writes turns with needles
318
+ (`N07=4821`) spread through them, the harness drives this build's own
319
+ compaction passes over the result, and then asks two questions: is every needle
320
+ still returned verbatim by address, covered by the DAG, and readable in one
321
+ bounded reader window, and can a question that needs all of them still be
322
+ answered from the surface a model sees without recall, with the recall path
323
+ counted separately and a failure count that has to stay zero. One small seed
324
+ runs in CI; longer ladders run on demand with `bun run eval` from a checkout of
325
+ the repository, which prints one row per length and appends an `eval` metrics
326
+ row per run.
327
+ - **Removed branches**: Pi's session tree can move the leaf, which takes entries
328
+ out of context without deleting them. Those entries are marked and their
329
+ bytes are kept: `lcm_grep` skips them unless `include_removed` is set, an
330
+ included hit is labelled `[removed]`, and an addressed read returns the text
331
+ with a marker. Navigating back clears the mark, so an abandoned branch
332
+ becomes visible again.
333
+ - **Integrity**: `/lcm doctor` runs checks over the store (full-text
334
+ index, summary spans, node shape, children that a committed node names,
335
+ duplicate spans, foreign keys, recorded schema version, runs whose writer is
336
+ gone, and how many messages have no leaf yet) and names a bounded sample of
337
+ offending ids. `--repair` rebuilds the index, drops summaries whose span names
338
+ an entry the store does not hold or whose child is gone, removes duplicate span
339
+ rows, keeping the row that can still be expanded, and reaps the rows a run
340
+ whose writer died left behind. A run this process wrote is left alone, and a
341
+ row whose pid an unrelated process now holds reads as live, so it waits for
342
+ that process to exit.
343
+ - **Telemetry**: `~/.pi/agent/lcm/metrics.jsonl` logs every context decision,
344
+ per-message provider usage (fresh vs cached, cost), one row per summarizer
345
+ call with the model, stage, and what the provider billed, one row per recall
346
+ call, one per externalized file, and one per branch change.
347
+ `/lcm doctor` reports the thresholds, the zone, and the summarizer chain that
348
+ are in effect, which answers what the log recorded after the fact.
349
+ - **Verbatim storage**: message text is stored whole, whatever its size.
350
+ `lcm_grep` with `query` matches the first 100,000 characters of a message,
351
+ because that is what the token index holds; with `pattern` it reads every
352
+ character, up to 200,000 per message and one second per call. A hit reports
353
+ the matching text with whitespace collapsed and the line clipped at 400
354
+ characters, and names the entry id that `lcm_expand_query` reads in full;
355
+ `/lcm export` writes every character. The bounds stay separate and each is
356
+ named in the output that hits it: the index reaches 100,000 characters, a
357
+ pattern reads 200,000 per message and stops at one second, one grep line is 400
358
+ characters, one reader window is 20,000 characters, and one call reads at most
359
+ `max_tokens`.
360
+
361
+ ## Documentation
362
+
363
+ | | |
364
+ | ---------------------------------------------------------------------- | --------------------------------------------------------- |
365
+ | [The docs site](https://stainless-code.com/lossless/) | How LCM behaves in a session, the config keys, the limits |
366
+ | [Comparison](https://stainless-code.com/lossless/reference/comparison) | The competing engines on one set of axes, verified cells |
367
+ | [CHANGELOG.md](./CHANGELOG.md) | Version history (changesets) |
368
+
369
+ ## Credits
370
+
371
+ - **Lossless Context Management (LCM)**, the algorithmic specification this
372
+ package implements: "Lossless Context Management", Clint Ehrlich and
373
+ Theodore Blackman, Voltropy PBC, <https://papers.voltropy.com/LCM>.
374
+ - **"Smart zone"**, the framing that model quality degrades at a token
375
+ budget, not at the context-window limit, Matt Pocock, AI Coding
376
+ Dictionary, <https://www.aihero.dev/ai-coding-dictionary/smart-zone>.
377
+ - All code is an original implementation written from the paper's spec.
378
+
379
+ ## License
380
+
381
+ MIT, see [LICENSE](./LICENSE).
@@ -0,0 +1,5 @@
1
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ //#region src/index.d.ts
3
+ export default function export_default(pi: ExtensionAPI): void;
4
+ //#endregion
5
+ //# sourceMappingURL=index.d.mts.map