pi-fold 3.1.0 → 4.0.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 +18 -21
- package/extensions/active-context.ts +138 -282
- package/extensions/index.js +5 -1
- package/extensions/lib/curation.ts +130 -35
- package/extensions/lib/editor-ui.ts +133 -25
- package/extensions/lib/measurement.ts +47 -0
- package/extensions/lib/persistence.ts +5 -36
- package/extensions/lib/policy.ts +17 -39
- package/extensions/lib/tool-surface.ts +2 -31
- package/extensions/settings.ts +114 -20
- package/package.json +1 -1
package/extensions/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export {
|
|
|
11
11
|
|
|
12
12
|
const PUBLIC_OPTIONS = Object.freeze([
|
|
13
13
|
"thresholds", "providerInputBudget", "blacklistAutoFoldTools", "toolFoldThreshold",
|
|
14
|
-
"
|
|
14
|
+
"postFoldNotice",
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
const GENERATOR_DELETED = "the model brief generator is deleted (2026-08-14): across 15 sealed " +
|
|
@@ -25,6 +25,10 @@ const INVERTED_AUTO_FOLD_LIST = "renamed blacklistAutoFoldTools, and the sense i
|
|
|
25
25
|
"it meant to permit";
|
|
26
26
|
|
|
27
27
|
const REFUSED_OPTIONS = Object.freeze({
|
|
28
|
+
workingMemory: "deleted 2026-08-28: a session-scoped dictionary with remember and recall, "
|
|
29
|
+
+ "offered to one sealed run that never called it once, wire-proven. Retention is a durable "
|
|
30
|
+
+ "store's job and folding is the rotation half, so a second unused memory surface was the "
|
|
31
|
+
+ "bloat the KISS rule exists to remove. Sealed state carrying it still loads, entries dropped",
|
|
28
32
|
guidance: "deleted 2026-08-23: it switched the copy that taught the agent to " +
|
|
29
33
|
"CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the frontier and " +
|
|
30
34
|
"the agent annotates what it cut, so the guidance it switched is replaced wholesale by " +
|
|
@@ -73,41 +73,6 @@ export function foldNoticeText(input: {
|
|
|
73
73
|
`[${brand} folds] Pending folds are waiting for briefs; the list is unavailable this pass.`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
* THE WORKING MEMORY'S TABLE OF CONTENTS (Shane, 2026-08-26). One copy rides the
|
|
78
|
-
* projection, refreshed at each commit by the carrier freeze: keys only, freshest first,
|
|
79
|
-
* because the bodies live in state and are read on demand. The instruction is fixed and
|
|
80
|
-
* the key list is the variable part, exactly as the fold notice above, and a list that
|
|
81
|
-
* stops early counts what it dropped (gate 136's law). It shares FOLD_NOTICE_BYTES: both
|
|
82
|
-
* are one-glance carriers and a second bound would be a knob with no second job.
|
|
83
|
-
*/
|
|
84
|
-
export function memoryTocText(input: {
|
|
85
|
-
entries: ReadonlyArray<{ key: string; chars: number }>;
|
|
86
|
-
toolName: string;
|
|
87
|
-
brandNoun?: string;
|
|
88
|
-
}): string {
|
|
89
|
-
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
90
|
-
const head = [
|
|
91
|
-
`[${brand} memory] Your working memory holds ${input.entries.length} ` +
|
|
92
|
-
`entr${input.entries.length === 1 ? "y" : "ies"}, freshest first.`,
|
|
93
|
-
`Read any of them with ${input.toolName} {"action":"recall","keys":["<key>"]}. ` +
|
|
94
|
-
`Update an entry whose facts have changed, or add one, with ${input.toolName} ` +
|
|
95
|
-
'{"action":"remember","key":"<key>","body":"..."}; an empty body removes the entry.',
|
|
96
|
-
].join("\n");
|
|
97
|
-
const rows = input.entries.map((entry) => `${entry.key} (${entry.chars} chars)`);
|
|
98
|
-
const overflowText = (seated: number) =>
|
|
99
|
-
`${input.toolName} {"action":"recall"} lists the other ${input.entries.length - seated}.`;
|
|
100
|
-
let seated = rows.length;
|
|
101
|
-
let list = `Entries: ${rows.join("; ")}.`;
|
|
102
|
-
while (seated > 0 && Buffer.byteLength(`${head}\n${list}`, "utf8") > FOLD_NOTICE_BYTES) {
|
|
103
|
-
seated -= 1;
|
|
104
|
-
list = seated === 0
|
|
105
|
-
? overflowText(0)
|
|
106
|
-
: `Entries: ${rows.slice(0, seated).join("; ")}. ${overflowText(seated)}`;
|
|
107
|
-
}
|
|
108
|
-
return boundReceiptText(`${head}\n${list}`, FOLD_NOTICE_BYTES,
|
|
109
|
-
`[${brand} memory] Working memory has entries; the list is unavailable this pass.`);
|
|
110
|
-
}
|
|
111
76
|
|
|
112
77
|
export interface ContextReceipt {
|
|
113
78
|
kind: string;
|
|
@@ -213,3 +178,133 @@ export function receiptBlockText(input: {
|
|
|
213
178
|
}
|
|
214
179
|
return [header, ...kept, verbs].join("\n");
|
|
215
180
|
}
|
|
181
|
+
|
|
182
|
+
const STATUS_BAR_WIDTH = 24;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* /fold-status: THE TEXT VIEW, FOR A PERSON.
|
|
186
|
+
*
|
|
187
|
+
* What this replaced said, in full: "Active context: 12 fold(s), roots a1b2, c3d4. Use
|
|
188
|
+
* pi_fold_context status for exact recursive actions." Three things were wrong with it.
|
|
189
|
+
* It told a HUMAN to call an AGENT tool they have no way to invoke. It printed fold ids,
|
|
190
|
+
* which are addresses for a machine and noise for a reader. And it answered a question
|
|
191
|
+
* nobody asks (how many folds exist) while leaving the only ones they do ask unanswered:
|
|
192
|
+
* how full am I, when does something happen, and what can I do about it.
|
|
193
|
+
*
|
|
194
|
+
* The command survives rather than being deleted because it is the documented fallback
|
|
195
|
+
* when /fold-editor cannot open, which is any session without an interactive UI. That
|
|
196
|
+
* makes it the ONLY window view some deployments ever get, so it carries what the editor
|
|
197
|
+
* header carries and closes by naming the two commands that act.
|
|
198
|
+
*
|
|
199
|
+
* Occupancy is stated as the provider's own measurement when there is one, and as
|
|
200
|
+
* "not measured yet" when there is not. It is never guessed: a made-up percentage is
|
|
201
|
+
* worse than an absent one, and this runtime has already paid once for treating an
|
|
202
|
+
* estimate as though it were a fact.
|
|
203
|
+
*/
|
|
204
|
+
export function foldStatusText(input: {
|
|
205
|
+
brand?: string;
|
|
206
|
+
usedTokens: number | null;
|
|
207
|
+
budgetTokens: number;
|
|
208
|
+
commitAtShare: number;
|
|
209
|
+
aimShare: number;
|
|
210
|
+
roots: number;
|
|
211
|
+
totalFolds: number;
|
|
212
|
+
stagedMarks: number;
|
|
213
|
+
stagedTokens: number;
|
|
214
|
+
pinned: number;
|
|
215
|
+
suspended: string | null;
|
|
216
|
+
foldCommand: string;
|
|
217
|
+
editorCommand: string;
|
|
218
|
+
}): string {
|
|
219
|
+
const brand = contextBrand(input.brand ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
220
|
+
const n = (value: number): string => Math.round(value).toLocaleString("en-US");
|
|
221
|
+
const lines: string[] = [`[${brand} status]`];
|
|
222
|
+
|
|
223
|
+
if (input.usedTokens === null || !(input.budgetTokens > 0)) {
|
|
224
|
+
// "provider" is infrastructure vocabulary and "sets it" names no observable event.
|
|
225
|
+
lines.push(" Window not measured yet; the first model response will measure it.");
|
|
226
|
+
} else {
|
|
227
|
+
const share = input.usedTokens / input.budgetTokens;
|
|
228
|
+
const filled = Math.max(0, Math.min(STATUS_BAR_WIDTH, Math.round(share * STATUS_BAR_WIDTH)));
|
|
229
|
+
const bar = `${"▇".repeat(filled)}${"░".repeat(STATUS_BAR_WIDTH - filled)}`;
|
|
230
|
+
lines.push(` Window ${bar} ${Math.round(share * 100)}% ` +
|
|
231
|
+
`${n(input.usedTokens)} / ${n(input.budgetTokens)} tokens`);
|
|
232
|
+
const commitAt = input.commitAtShare * input.budgetTokens;
|
|
233
|
+
// The headroom is what a person actually wants: not the trigger's share, but how much
|
|
234
|
+
// further this session goes before anything happens to it.
|
|
235
|
+
//
|
|
236
|
+
// A SUSPENDED SESSION IS PROMISED NOTHING. This line used to say "the next pass folds
|
|
237
|
+
// down toward 20%" one line above "Automatic folding is suspended", so the surface
|
|
238
|
+
// contradicted itself about the only thing the reader opened it to find out.
|
|
239
|
+
lines.push(input.suspended
|
|
240
|
+
? ` At the ${Math.round(input.commitAtShare * 100)}% commit point.`
|
|
241
|
+
: share >= input.commitAtShare
|
|
242
|
+
? ` At the ${Math.round(input.commitAtShare * 100)}% commit point; ` +
|
|
243
|
+
`folding now targets ${Math.round(input.aimShare * 100)}%.`
|
|
244
|
+
// "Commits at 80%" reads for a beat as a plural noun, or as a verb with no
|
|
245
|
+
// subject, and the missing subject is the PRODUCT: no surface anywhere said the
|
|
246
|
+
// runtime folds BY ITSELF, so a reader assembling this line with "/fold commits
|
|
247
|
+
// them now" concludes they have installed a manual tool.
|
|
248
|
+
: ` Folds automatically at ${Math.round(input.commitAtShare * 100)}%; ` +
|
|
249
|
+
`${n(commitAt - input.usedTokens)} tokens away.`);
|
|
250
|
+
}
|
|
251
|
+
// THE STOP SITS DIRECTLY UNDER THE WINDOW, not below three rows of routine counts.
|
|
252
|
+
// The order answers the reader's questions in the order they ask them: are we at the
|
|
253
|
+
// threshold, will anything fold, and why not.
|
|
254
|
+
if (input.suspended) {
|
|
255
|
+
lines.push(" STOPPED Automatic folding is suspended.");
|
|
256
|
+
lines.push(` Reason: ${oneLine(input.suspended, 160)}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// VISIBLE FIRST, AND THE UNIT NAMED. The always-on line counts visible roots and this
|
|
260
|
+
// one led with the total, so the two surfaces looked like they disagreed about how many
|
|
261
|
+
// folds exist. "Top level" is tree-implementation language; "visible" is what a reader
|
|
262
|
+
// can check against the editor in front of them.
|
|
263
|
+
lines.push(input.totalFolds === 0
|
|
264
|
+
? " Folded nothing yet."
|
|
265
|
+
// "Each keeps its exact source" says the bytes survive but never says a SUMMARY stands
|
|
266
|
+
// where they were, and the likeliest wrong model a reader forms is that folded
|
|
267
|
+
// material is simply gone. One clause teaches what a fold actually is.
|
|
268
|
+
: ` Folded ${n(input.roots)} here, ${n(input.totalFolds)} with nesting; ` +
|
|
269
|
+
"each a summary over its exact source.");
|
|
270
|
+
|
|
271
|
+
if (input.stagedMarks > 0) {
|
|
272
|
+
// TWO CORRECTIONS IN ONE LINE.
|
|
273
|
+
//
|
|
274
|
+
// It said "holding about 28,400 tokens" about a number that is the sum of
|
|
275
|
+
// markFreedBytes: what a commit FREES, which is the same figure the editor header
|
|
276
|
+
// prints as "frees". One surface called it held and the other called it freed, and
|
|
277
|
+
// held was the wrong one.
|
|
278
|
+
//
|
|
279
|
+
// And the brief count is gone, input field and all. It counted provenance "supplied"
|
|
280
|
+
// or "augmented", and a brief the USER types on the editor's brief line is "supplied",
|
|
281
|
+
// so the row told a person the sentence they had just written was somebody else's.
|
|
282
|
+
// Nothing on any surface can add a brief to an already-staged mark either, so it was
|
|
283
|
+
// a number that could be wrong and could not be acted on.
|
|
284
|
+
//
|
|
285
|
+
// What remains leads with the fact that stops "staged" being read as "already
|
|
286
|
+
// applied", and fits inside 80 columns, where the old 105-character line wrapped with
|
|
287
|
+
// its continuation flush left inside the label field, reading as a fifth label.
|
|
288
|
+
lines.push(` Staged ${n(input.stagedMarks)} fold${input.stagedMarks === 1 ? "" : "s"}; ` +
|
|
289
|
+
`nothing moves until a commit frees about ${n(input.stagedTokens)} tokens.`);
|
|
290
|
+
}
|
|
291
|
+
if (input.pinned > 0) {
|
|
292
|
+
lines.push(` Pinned ${n(input.pinned)} entr${input.pinned === 1 ? "y" : "ies"} ` +
|
|
293
|
+
`stay${input.pinned === 1 ? "s" : ""} raw; ` +
|
|
294
|
+
`${input.pinned === 1 ? "it does" : "they do"} not free space.`);
|
|
295
|
+
}
|
|
296
|
+
// The closing line offers only what is actually available: proposing a commit with
|
|
297
|
+
// nothing staged is an instruction that does nothing, which is the failure the deleted
|
|
298
|
+
// fence made unforgivable.
|
|
299
|
+
// "commits them" took its antecedent from whichever row happened to come last, which is
|
|
300
|
+
// Pinned as often as Staged. Name the object.
|
|
301
|
+
lines.push(input.stagedMarks > 0
|
|
302
|
+
// 78, not 80: pi-tui wraps at width minus two, so an 80-column line still wraps on an
|
|
303
|
+
// 80-column terminal, and its continuation lands flush left in the label field.
|
|
304
|
+
? ` ${input.foldCommand} commits the staged folds; ${input.editorCommand} opens the window to steer it.`
|
|
305
|
+
// THE FRESH SESSION IS WHERE THE WRONG MODEL FORMS, so this is the line that has to
|
|
306
|
+
// say folding is automatic. A reader who meets the package here and reads only
|
|
307
|
+
// command names concludes nothing happens unless they type one.
|
|
308
|
+
: ` Folding is automatic; ${input.editorCommand} shows the window and ${input.foldCommand} commits early.`);
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
@@ -50,6 +50,9 @@ export interface FoldEditorData {
|
|
|
50
50
|
budgetTokens: number;
|
|
51
51
|
commitOccupancy: number;
|
|
52
52
|
commitDue: boolean;
|
|
53
|
+
/** Automatic folding has stopped. The editor is the surface a person opens in order
|
|
54
|
+
* to ACT, and without this it priced a countdown to a commit that cannot fire. */
|
|
55
|
+
suspended?: boolean;
|
|
53
56
|
};
|
|
54
57
|
/** Top-level blocks only: raw gaps and ROOT folds, in window order. */
|
|
55
58
|
blocks: FoldEditorBlock[];
|
|
@@ -72,13 +75,38 @@ export function occupancyBar(usedTokens: number | null, budgetTokens: number): s
|
|
|
72
75
|
? Math.max(0, Math.min(1, usedTokens / budgetTokens))
|
|
73
76
|
: 0;
|
|
74
77
|
const filled = Math.round(ratio * BAR_WIDTH);
|
|
75
|
-
|
|
78
|
+
// THE SAME ALPHABET /fold-status USES. The editor drew its empty cells with the middle
|
|
79
|
+
// dot that also separates the fields on that very line, so the bar's tail and the
|
|
80
|
+
// line's punctuation were the same glyph doing two jobs.
|
|
81
|
+
return `${"▇".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}`;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
function shortId(id: string): string {
|
|
79
85
|
return id.length > 12 ? id.slice(0, 12) : id;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/**
|
|
89
|
+
* MARK ORIGINS IN THE READER'S OWN WORDS. The state's names are for the state: "ladder"
|
|
90
|
+
* is this runtime's internal name for its own automatic marking pass, and a person
|
|
91
|
+
* reading a proposed row needs to know WHO proposed the fold, not which code path
|
|
92
|
+
* staged it. An unrecognized origin passes through rather than being hidden, because a
|
|
93
|
+
* name we do not have a word for is still better than a blank.
|
|
94
|
+
*
|
|
95
|
+
* ONE WORD FOR ONE THING (2026-08-28): the rows said PROPOSED while the header said
|
|
96
|
+
* staged, for the same marks on the same screen, which asks a reader to learn that two
|
|
97
|
+
* words mean one thing before they can read either line.
|
|
98
|
+
*/
|
|
99
|
+
const ORIGIN_WORDS: Record<string, string> = {
|
|
100
|
+
ladder: "automatic",
|
|
101
|
+
agent: "the model",
|
|
102
|
+
user: "you",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function originWord(origin: string | undefined): string {
|
|
106
|
+
if (!origin) return "staged";
|
|
107
|
+
return ORIGIN_WORDS[origin] ?? origin;
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
/**
|
|
83
111
|
* Assemble the top-level window stream. Committed folds nest by containment (a fold
|
|
84
112
|
* whose span sits inside another becomes a CHILD of the innermost container), so the
|
|
@@ -165,7 +193,9 @@ export function buildFoldEditorData(
|
|
|
165
193
|
if (block.type === "raw") {
|
|
166
194
|
const roles = new Map<string, number>();
|
|
167
195
|
for (const entry of block.entries) roles.set(entry.role, (roles.get(entry.role) ?? 0) + 1);
|
|
168
|
-
|
|
196
|
+
// "user×2 assistant×3" put the count behind the same bare multiplication sign the
|
|
197
|
+
// fold rows just lost. Count first, noun second, like everything else here.
|
|
198
|
+
block.rolesSummary = [...roles.entries()].map(([role, count]) => `${count} ${role}`).join(", ");
|
|
169
199
|
}
|
|
170
200
|
}
|
|
171
201
|
|
|
@@ -326,12 +356,25 @@ export class FoldEditorView {
|
|
|
326
356
|
const foldKey = `${keyPrefix}${block.id}`;
|
|
327
357
|
const isOpen = this.expanded.has(foldKey);
|
|
328
358
|
const pad = " ".repeat(depth);
|
|
329
|
-
|
|
359
|
+
// THE ROW SAYS WHAT WAS FOLDED (2026-08-28). It read "fold fold_a1b2c3 chapter ×9":
|
|
360
|
+
// the word "fold" twice (the ▸ glyph is already the word), thirteen columns of an
|
|
361
|
+
// id nothing in this view accepts as input, and the brief that says what the fold
|
|
362
|
+
// COVERS hidden behind Enter. Twelve roots differed only by hex, which recreates
|
|
363
|
+
// inside the editor the exact defect /fold-status was rewritten to remove.
|
|
364
|
+
//
|
|
365
|
+
// The id is dropped rather than trailed: it is unusable here, the expanded state
|
|
366
|
+
// still carries it on the end bookend, and a trailing id is what truncation eats
|
|
367
|
+
// first anyway. "×9" becomes a counted noun, because ×9 could be entries, nesting
|
|
368
|
+
// depth or generations and the reader has no way to tell which.
|
|
369
|
+
const kindLabel = `${block.kind} · ${block.sourceCount} ` +
|
|
370
|
+
`${block.sourceCount === 1 ? "entry" : "entries"}`;
|
|
330
371
|
const marker = isOpen ? "\u25bc" : "\u25b8";
|
|
331
372
|
const foldCursor = this.selectedKey === foldKey ? "\u276f" : " ";
|
|
373
|
+
const [foldHead] = briefChunks(block.brief ?? "");
|
|
332
374
|
rows.push({
|
|
333
375
|
key: foldKey,
|
|
334
|
-
text: `${foldCursor}${this.color("success", `${pad}${marker}
|
|
376
|
+
text: `${foldCursor}${this.color("success", `${pad}${marker} ${kindLabel}`)}` +
|
|
377
|
+
`${foldHead ? ` · ${foldHead}` : ""}`,
|
|
335
378
|
toggleId: foldKey,
|
|
336
379
|
});
|
|
337
380
|
if (!isOpen) return;
|
|
@@ -391,10 +434,17 @@ export class FoldEditorView {
|
|
|
391
434
|
if (block.type === "proposed") {
|
|
392
435
|
const isLadder = block.origin === "ladder";
|
|
393
436
|
const color = isLadder ? "warning" : "accent";
|
|
394
|
-
const sizeNote = typeof block.tokens === "number"
|
|
437
|
+
const sizeNote = typeof block.tokens === "number"
|
|
438
|
+
? ` · frees ~${block.tokens.toLocaleString("en-US")} tokens` : "";
|
|
439
|
+
const [stagedHead] = briefChunks(block.brief ?? "");
|
|
395
440
|
rows.push({
|
|
396
441
|
key: block.id,
|
|
397
|
-
|
|
442
|
+
// Same treatment as the fold row: no id, a counted noun, and the brief seated
|
|
443
|
+
// as the tail so it is what degrades if the terminal is narrow.
|
|
444
|
+
text: `${cursor}${this.color(color, `\u25c7 staged by ${originWord(block.origin)}`)}` +
|
|
445
|
+
` · ${block.sourceCount} ${block.sourceCount === 1 ? "entry" : "entries"}${sizeNote}` +
|
|
446
|
+
`${stagedHead ? ` · ${stagedHead}` : ""}` +
|
|
447
|
+
`${this.selectedKey === block.id ? " · u:withdraw" : ""}`,
|
|
398
448
|
toggleId: block.id,
|
|
399
449
|
proposedMarkId: block.id,
|
|
400
450
|
});
|
|
@@ -421,7 +471,8 @@ export class FoldEditorView {
|
|
|
421
471
|
}
|
|
422
472
|
}
|
|
423
473
|
} else {
|
|
424
|
-
const label = `\u00b7 raw
|
|
474
|
+
const label = `\u00b7 raw · ${block.sourceCount} ` +
|
|
475
|
+
`${block.sourceCount === 1 ? "entry" : "entries"}` +
|
|
425
476
|
`${block.rolesSummary ? ` ${block.rolesSummary}` : ""}`;
|
|
426
477
|
rows.push({
|
|
427
478
|
key: block.id,
|
|
@@ -675,29 +726,79 @@ export class FoldEditorView {
|
|
|
675
726
|
const lines: string[] = [];
|
|
676
727
|
const { occupancy, pending } = this.data;
|
|
677
728
|
lines.push(truncateToWidth(`── ${this.data.title} ──`, width));
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
729
|
+
// UNMEASURED IS PROSE, NEVER A BAR. An empty bar beside "?%" reads to the eye as
|
|
730
|
+
// a window that is 0 percent full, which is a guess wearing the costume of a
|
|
731
|
+
// measurement: the exact failure the other three surfaces already refuse, on the
|
|
732
|
+
// one surface the rule was never checked against. Nothing here is estimated.
|
|
733
|
+
if (occupancy.usedTokens === null || !(occupancy.budgetTokens > 0)) {
|
|
734
|
+
lines.push(truncateToWidth(
|
|
735
|
+
"window not measured yet; the first model response will measure it", width));
|
|
736
|
+
for (const line of this.stagedLines()) lines.push(truncateToWidth(line, width));
|
|
737
|
+
return lines;
|
|
738
|
+
}
|
|
739
|
+
// A WHOLE PERCENT. The tenth was false precision on a figure that moves with every
|
|
740
|
+
// message, and it read as a measurement finer than the thing being measured. The
|
|
741
|
+
// other three human surfaces round, so this one does too. Past the guard above,
|
|
742
|
+
// both values are known, so neither can render as "?".
|
|
743
|
+
const percent = Math.round((occupancy.usedTokens / occupancy.budgetTokens) * 100);
|
|
744
|
+
const usedTokensText = occupancy.usedTokens.toLocaleString("en-US");
|
|
745
|
+
// The DISTANCE to the commit, not just the share it fires at. "commit at 80%" is
|
|
746
|
+
// a property of the configuration; "29,674 to go" is the answer to the question
|
|
747
|
+
// the reader actually has, and it is the same number /fold-status states.
|
|
748
|
+
const headroom = occupancy.usedTokens !== null && occupancy.budgetTokens > 0 && !occupancy.commitDue
|
|
749
|
+
? ` (${Math.round(occupancy.commitOccupancy * occupancy.budgetTokens - occupancy.usedTokens)
|
|
750
|
+
.toLocaleString("en-US")} to go)`
|
|
751
|
+
: "";
|
|
752
|
+
// Same ordering rule as the line below it: the raw token pair is the most
|
|
753
|
+
// expendable thing here, because the percentage in front of it already says what
|
|
754
|
+
// it says. At 80 columns the first draft cut the headroom instead.
|
|
755
|
+
// A STOPPED WINDOW IS TOLD SO, NOT GIVEN A COUNTDOWN. Every other surface reports
|
|
756
|
+
// the suspension and this one alone kept pricing the distance to a commit that
|
|
757
|
+
// will not happen, which is the one state here that can cost somebody a session.
|
|
758
|
+
const timing = occupancy.suspended
|
|
759
|
+
? " · FOLDING STOPPED"
|
|
760
|
+
: ` · commit at ${(occupancy.commitOccupancy * 100).toFixed(0)}%${headroom}` +
|
|
761
|
+
`${occupancy.commitDue ? " · COMMIT DUE" : ""}`;
|
|
684
762
|
lines.push(truncateToWidth(
|
|
685
763
|
`${occupancyBar(occupancy.usedTokens, occupancy.budgetTokens)} ` +
|
|
686
|
-
`${percent}
|
|
687
|
-
`
|
|
688
|
-
`${occupancy.commitDue ? " · COMMIT DUE" : ""}`,
|
|
689
|
-
width,
|
|
690
|
-
));
|
|
691
|
-
lines.push(truncateToWidth(
|
|
692
|
-
`staged marks: ${pending.count} (${pending.agentMarks} agent, ${pending.ladderMarks} ladder` +
|
|
693
|
-
`${pending.userMarks > 0 ? `, ${pending.userMarks} you` : ""})` +
|
|
694
|
-
` · about ${pending.freedTokens.toLocaleString("en-US")} tokens freed when committed · ` +
|
|
695
|
-
`pinned: ${this.data.pinned.length}`,
|
|
764
|
+
`${percent}%${timing}` +
|
|
765
|
+
` · ${usedTokensText}/${occupancy.budgetTokens.toLocaleString("en-US")}`,
|
|
696
766
|
width,
|
|
697
767
|
));
|
|
768
|
+
for (const line of this.stagedLines()) lines.push(truncateToWidth(line, width));
|
|
698
769
|
return lines;
|
|
699
770
|
}
|
|
700
771
|
|
|
772
|
+
/**
|
|
773
|
+
* ORDERED SO TRUNCATION COSTS THE LEAST. This line is cut to the terminal's width, and
|
|
774
|
+
* at 100 columns the first draft lost the pin count off the end while spending its
|
|
775
|
+
* width on who staged what. What the marks free and what is pinned are facts about the
|
|
776
|
+
* window; the origin breakdown is colour, so it goes last.
|
|
777
|
+
*/
|
|
778
|
+
private stagedLines(): string[] {
|
|
779
|
+
const { pending } = this.data;
|
|
780
|
+
// The same vocabulary as ORIGIN_WORDS above, inflected for a count rather than a
|
|
781
|
+
// label: "1 automatic" and "2 by the model" read where "2 the model" does not.
|
|
782
|
+
// Only origins that actually staged something are listed, because a run of zeroes
|
|
783
|
+
// spends the line's width saying nothing happened three different ways.
|
|
784
|
+
const origins = [
|
|
785
|
+
pending.ladderMarks > 0 ? `${pending.ladderMarks} automatic` : "",
|
|
786
|
+
pending.agentMarks > 0 ? `${pending.agentMarks} by the model` : "",
|
|
787
|
+
pending.userMarks > 0 ? `${pending.userMarks} yours` : "",
|
|
788
|
+
].filter((part) => part !== "").join(", ");
|
|
789
|
+
// TWO LINES RATHER THAN ONE THAT RUNS OFF THE TERMINAL. At 97 characters the single
|
|
790
|
+
// line ended mid-phrase at "2 by the model" on any normal window; vertical space is
|
|
791
|
+
// cheaper than a sentence cut in half. The facts about the window come first and
|
|
792
|
+
// who staged what follows, so a narrow terminal still keeps the first line whole.
|
|
793
|
+
// "folds" not "marks": /fold-status and the always-on line both say folds, and one
|
|
794
|
+
// object carrying two nouns on adjacent surfaces is a thing to learn for nothing.
|
|
795
|
+
const staged = [`staged: ${pending.count} fold${pending.count === 1 ? "" : "s"}` +
|
|
796
|
+
` · frees ~${pending.freedTokens.toLocaleString("en-US")} tokens at commit` +
|
|
797
|
+
` · ${this.data.pinned.length} pinned`];
|
|
798
|
+
if (origins) staged.push(`staged by: ${origins}`);
|
|
799
|
+
return staged;
|
|
800
|
+
}
|
|
801
|
+
|
|
701
802
|
render(width: number): string[] {
|
|
702
803
|
const header = this.headerLines(width);
|
|
703
804
|
const rows = this.visibleRows();
|
|
@@ -707,7 +808,11 @@ export class FoldEditorView {
|
|
|
707
808
|
this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, rows.length - MAX_VISIBLE_ROWS)));
|
|
708
809
|
const body = rows
|
|
709
810
|
.slice(this.scroll, this.scroll + MAX_VISIBLE_ROWS)
|
|
710
|
-
|
|
811
|
+
// THE PROJECT'S OWN BOUNDING LAW (gate 136): "..." means content continues in the
|
|
812
|
+
// source and never appears when nothing was cut. Header lines already take the
|
|
813
|
+
// default marker; body rows passed "" and were cut mid-word in silence, so the
|
|
814
|
+
// display layer contradicted the rule the briefs it displays all obey.
|
|
815
|
+
.map((row) => truncateToWidth(row.text, width));
|
|
711
816
|
// THE WOULD-BE FOLD SIZE (Shane, 2026-08-23): while a span is being laid down,
|
|
712
817
|
// the footer states what the second mark point would commit, in the runtime's
|
|
713
818
|
// own token arithmetic -- the same numbers the staged mark will answer with.
|
|
@@ -733,7 +838,10 @@ export class FoldEditorView {
|
|
|
733
838
|
"",
|
|
734
839
|
...body,
|
|
735
840
|
...footers,
|
|
736
|
-
|
|
841
|
+
// p AND u CHANGE STATE AND WERE NAMED NOWHERE, while the header prints a pin count
|
|
842
|
+
// on every render. "raw span" stays: it is the word explaining why m does
|
|
843
|
+
// nothing on a fold row. The width comes out of enter and arrows instead.
|
|
844
|
+
truncateToWidth("enter:open · arrows:move · m:mark raw span · p:pin · u:withdraw · esc:close", width),
|
|
737
845
|
];
|
|
738
846
|
}
|
|
739
847
|
}
|
|
@@ -570,3 +570,50 @@ export function parseNativeCompactionCompletion(value: unknown, expectedSessionI
|
|
|
570
570
|
}
|
|
571
571
|
return { ...(clone(value) as unknown as NativeCompactionCompletionReceipt), decision };
|
|
572
572
|
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* AN IMAGE IS NOT TEXT, AND MEASURING IT AS TEXT KILLS SESSIONS (2026-08-28).
|
|
576
|
+
*
|
|
577
|
+
* The projection is sized by serializing it and counting UTF-8 bytes, which is right for
|
|
578
|
+
* prose and catastrophically wrong for an image: a content block carries its pixels as
|
|
579
|
+
* base64, so a screenshot lands as several hundred thousand characters that the estimator
|
|
580
|
+
* then divides by a chars-per-token ratio learned from PROSE. A live session read three
|
|
581
|
+
* PNGs of 364,801, 631,045 and 827,513 bytes; the projection went 636,687 chars to
|
|
582
|
+
* 1,268,275 in one step and the estimate went 110,687 tokens to 220,883, while the
|
|
583
|
+
* provider's own count for that same request came back 111,837. Every token of the
|
|
584
|
+
* 109,046 difference was base64 counted as if it were sentences.
|
|
585
|
+
*
|
|
586
|
+
* The calibration cannot learn its way out, and that is the part worth understanding.
|
|
587
|
+
* `projectionCharsPerToken` takes the MINIMUM ratio over its window so the estimate never
|
|
588
|
+
* under-reads. An image pushes the true ratio UP, so the minimum discards exactly the
|
|
589
|
+
* sample that would have corrected it and keeps dividing base64 by the prose ratio. The
|
|
590
|
+
* conservative rule that protects against under-reading is what locks in the over-read.
|
|
591
|
+
*
|
|
592
|
+
* So image payload is removed from the character count and replaced by a flat per-image
|
|
593
|
+
* token cost. The count cannot be exact across providers, and it does not need to be: it
|
|
594
|
+
* needs to be the right order of magnitude instead of wrong by seventy times, and the
|
|
595
|
+
* provider anchor corrects the remainder the moment a real measurement lands.
|
|
596
|
+
*/
|
|
597
|
+
export function imageMass(projected: unknown[]): { base64Chars: number; images: number } {
|
|
598
|
+
let base64Chars = 0;
|
|
599
|
+
let images = 0;
|
|
600
|
+
for (const message of projected) {
|
|
601
|
+
const content = ownValue(message, "content");
|
|
602
|
+
const blocks = denseOwnArrayValues(content);
|
|
603
|
+
if (!blocks) continue;
|
|
604
|
+
for (const block of blocks) {
|
|
605
|
+
if (ownValue(block, "type") !== "image") continue;
|
|
606
|
+
images += 1;
|
|
607
|
+
// Two shapes reach here: the flat `data` a Pi tool result carries, and the nested
|
|
608
|
+
// `source.data` of a provider-native block. Both are counted; neither is required.
|
|
609
|
+
const direct = ownValue(block, "data");
|
|
610
|
+
const nested = ownValue(ownValue(block, "source"), "data");
|
|
611
|
+
const payload = typeof direct === "string" ? direct
|
|
612
|
+
: typeof nested === "string" ? nested : "";
|
|
613
|
+
// The serialization counts the JSON-escaped string, and base64 has no character
|
|
614
|
+
// JSON escapes, so its byte length in the serialized form is its own length.
|
|
615
|
+
base64Chars += bytes(payload);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return { base64Chars, images };
|
|
619
|
+
}
|
|
@@ -36,7 +36,6 @@ import type {
|
|
|
36
36
|
BriefOverride,
|
|
37
37
|
BriefProvenance,
|
|
38
38
|
ToolClip,
|
|
39
|
-
WorkingMemoryEntry,
|
|
40
39
|
FoldKind,
|
|
41
40
|
FoldPart,
|
|
42
41
|
FoldRecordEntry,
|
|
@@ -330,6 +329,10 @@ export function parseActiveContextState(
|
|
|
330
329
|
const hasPinnedPeeks = recordLike && Object.prototype.hasOwnProperty.call(value, "pinnedPeeks");
|
|
331
330
|
const hasBriefs = recordLike && Object.prototype.hasOwnProperty.call(value, "briefs");
|
|
332
331
|
const hasClips = recordLike && Object.prototype.hasOwnProperty.call(value, "clips");
|
|
332
|
+
// Read and dropped, never stored (2026-08-28): the working memory is deleted, and a
|
|
333
|
+
// sealed state that carries it must still load. Tolerating the key while discarding the
|
|
334
|
+
// value is the legacy-rider treatment, not the retired-field refusal: sol-20260826-full2
|
|
335
|
+
// reps 4 and 6 ran with it, and a campaign this package still cites has to stay readable.
|
|
333
336
|
const hasMemory = recordLike && Object.prototype.hasOwnProperty.call(value, "memory");
|
|
334
337
|
// Read and dropped, never stored: see legacyRiderStateSha256. A v1 state has no digest
|
|
335
338
|
// to reproduce, so here the tolerance is the whole of it.
|
|
@@ -377,7 +380,6 @@ export function parseActiveContextState(
|
|
|
377
380
|
}
|
|
378
381
|
const briefs = hasBriefs ? parseBriefOverrides(ownValue(value, "briefs"), ids) : {};
|
|
379
382
|
const clips = hasClips ? parseToolClips(ownValue(value, "clips")) : [];
|
|
380
|
-
const memory = hasMemory ? parseWorkingMemory(ownValue(value, "memory")) : [];
|
|
381
383
|
const source = clone(value) as unknown as ActiveContextState;
|
|
382
384
|
return {
|
|
383
385
|
version: 1,
|
|
@@ -393,7 +395,6 @@ export function parseActiveContextState(
|
|
|
393
395
|
...(marks.length ? { pendingMarks: marks } : {}),
|
|
394
396
|
...(Object.keys(briefs).length ? { briefs } : {}),
|
|
395
397
|
...(clips.length ? { clips } : {}),
|
|
396
|
-
...(memory.length ? { memory } : {}),
|
|
397
398
|
...(hasAdvisory
|
|
398
399
|
? { advisory: clone(source.advisory!) }
|
|
399
400
|
: defaultAdvisory ? { advisory: { highWater: 0, delivered: {} } } : {}),
|
|
@@ -418,30 +419,6 @@ function parseToolClips(value: unknown): ToolClip[] {
|
|
|
418
419
|
return clips;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
function parseWorkingMemory(value: unknown): WorkingMemoryEntry[] {
|
|
422
|
-
const entries = denseOwnArrayValues(value);
|
|
423
|
-
if (!entries) throw new Error("Invalid active-context working memory");
|
|
424
|
-
const memory = entries.map((entry) => {
|
|
425
|
-
const key = ownValue(entry, "key");
|
|
426
|
-
const body = ownValue(entry, "body");
|
|
427
|
-
const ordinal = ownValue(entry, "ordinal");
|
|
428
|
-
if (!exactRecord(entry, ["key", "body", "ordinal"]) ||
|
|
429
|
-
typeof key !== "string" || !key || key.length > MEMORY_KEY_CHARS ||
|
|
430
|
-
typeof body !== "string" || !body || body.length > MEMORY_BODY_CHARS ||
|
|
431
|
-
!Number.isSafeInteger(ordinal) || Number(ordinal) < 0) {
|
|
432
|
-
throw new Error("Invalid active-context working-memory entry");
|
|
433
|
-
}
|
|
434
|
-
return { key, body, ordinal: Number(ordinal) };
|
|
435
|
-
});
|
|
436
|
-
if (memory.length > MEMORY_KEYS_MAX) {
|
|
437
|
-
throw new Error(`Invalid active-context working memory: over ${MEMORY_KEYS_MAX} entries`);
|
|
438
|
-
}
|
|
439
|
-
if (new Set(memory.map((entry) => entry.key)).size !== memory.length) {
|
|
440
|
-
throw new Error("Invalid active-context working memory: duplicate key");
|
|
441
|
-
}
|
|
442
|
-
return memory;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
422
|
export function clearPrepared(state: ActiveContextState): ActiveContextState {
|
|
446
423
|
const next = { ...state };
|
|
447
424
|
delete next.prepared;
|
|
@@ -557,7 +534,6 @@ export function sameStateProjection(left: ActiveContextState, right: ActiveConte
|
|
|
557
534
|
if (normalizedState.pendingMarks && normalizedState.pendingMarks.length === 0) delete normalizedState.pendingMarks;
|
|
558
535
|
if (normalizedState.briefs && !Object.keys(normalizedState.briefs).length) delete normalizedState.briefs;
|
|
559
536
|
if (normalizedState.clips && normalizedState.clips.length === 0) delete normalizedState.clips;
|
|
560
|
-
if (normalizedState.memory && normalizedState.memory.length === 0) delete normalizedState.memory;
|
|
561
537
|
return normalizedState;
|
|
562
538
|
};
|
|
563
539
|
return stableStringify(normalized(left)) === stableStringify(normalized(right));
|
|
@@ -711,7 +687,6 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
711
687
|
);
|
|
712
688
|
if (hasWireClips) parseToolClips(ownValue(value, "clips"));
|
|
713
689
|
if (hasAddClips) parseToolClips(ownValue(value, "addClips"));
|
|
714
|
-
if (hasWireMemory) parseWorkingMemory(ownValue(value, "memory"));
|
|
715
690
|
if (hasClipBase) {
|
|
716
691
|
const base = ownValue(value, "clipBase");
|
|
717
692
|
if (!Number.isSafeInteger(base) || Number(base) < 0) throw new Error("Invalid active-context clip base");
|
|
@@ -837,7 +812,7 @@ export function stateFromFoldRefs(
|
|
|
837
812
|
wire: Pick<
|
|
838
813
|
ActiveContextCheckpointV2,
|
|
839
814
|
"sessionId" | "revision" | "expanded" | "protected" | "prepared" | "advisory" |
|
|
840
|
-
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips"
|
|
815
|
+
"tokensSinceToolFold" | "leases" | "pendingMarks" | "briefs" | "clips"
|
|
841
816
|
>,
|
|
842
817
|
refs: FoldRecordRef[],
|
|
843
818
|
records: Map<string, FoldRecordEntry>,
|
|
@@ -862,7 +837,6 @@ export function stateFromFoldRefs(
|
|
|
862
837
|
...(wire.pendingMarks?.length ? { pendingMarks: clone(wire.pendingMarks) } : {}),
|
|
863
838
|
...(wire.briefs && Object.keys(wire.briefs).length ? { briefs: clone(wire.briefs) } : {}),
|
|
864
839
|
...(wire.clips?.length ? { clips: clone(wire.clips) } : {}),
|
|
865
|
-
...(wire.memory?.length ? { memory: clone(wire.memory) } : {}),
|
|
866
840
|
...(wire.advisory === undefined ? {} : { advisory: clone(wire.advisory) }),
|
|
867
841
|
...(wire.prepared === null || wire.prepared === undefined ? {} : { prepared: clone(wire.prepared) }),
|
|
868
842
|
};
|
|
@@ -1082,7 +1056,6 @@ export function materializeStatePersistence(
|
|
|
1082
1056
|
briefs: Object.keys(briefs).length ? briefs : undefined,
|
|
1083
1057
|
pendingMarks: marks.length ? marks : undefined,
|
|
1084
1058
|
clips: clips.length ? clips : undefined,
|
|
1085
|
-
memory: wire.memory?.length ? clone(wire.memory) : undefined,
|
|
1086
1059
|
},
|
|
1087
1060
|
[...byId.values()],
|
|
1088
1061
|
records,
|
|
@@ -1143,7 +1116,6 @@ export function makeStateCheckpoint(state: ActiveContextState): ActiveContextChe
|
|
|
1143
1116
|
...(state.pendingMarks?.length ? { pendingMarks: clone(state.pendingMarks) } : {}),
|
|
1144
1117
|
...(state.briefs && Object.keys(state.briefs).length ? { briefs: clone(state.briefs) } : {}),
|
|
1145
1118
|
...(state.clips?.length ? { clips: clone(state.clips) } : {}),
|
|
1146
|
-
...(state.memory?.length ? { memory: clone(state.memory) } : {}),
|
|
1147
1119
|
...(state.advisory ? { advisory: clone(state.advisory) } : {}),
|
|
1148
1120
|
stateSha256: semanticStateSha256(state),
|
|
1149
1121
|
}, state.sessionId) as ActiveContextCheckpointV2;
|
|
@@ -1234,9 +1206,6 @@ export function makeStateDelta(previous: ActiveContextState, next: ActiveContext
|
|
|
1234
1206
|
...(Object.keys(addBriefs).length ? { addBriefs } : {}),
|
|
1235
1207
|
...(removeBriefIds.length ? { removeBriefIds } : {}),
|
|
1236
1208
|
...(clipsTravel ? { clipBase: clipBase, ...(addClips.length ? { addClips: clone(addClips) } : {}) } : {}),
|
|
1237
|
-
// The memory travels whole when it exists: bounded at MEMORY_KEYS_MAX entries and
|
|
1238
|
-
// edited in place, so the change encodings above have nothing to save here.
|
|
1239
|
-
...(next.memory?.length ? { memory: clone(next.memory) } : {}),
|
|
1240
1209
|
...(next.advisory ? { advisory: clone(next.advisory) } : {}),
|
|
1241
1210
|
stateSha256: semanticStateSha256(next),
|
|
1242
1211
|
}, next.sessionId) as ActiveContextDeltaV2;
|