pi-fold 3.0.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 +76 -212
- package/extensions/active-context.ts +457 -362
- package/extensions/evidence.js +1 -5
- package/extensions/index.js +8 -3
- package/extensions/json.ts +23 -1
- package/extensions/lib/canonical.ts +34 -0
- package/extensions/lib/curation.ts +141 -9
- package/extensions/lib/editor-ui.ts +133 -25
- package/extensions/lib/folding.ts +181 -17
- package/extensions/lib/instrumentation.ts +0 -6
- package/extensions/lib/measurement.ts +84 -4
- package/extensions/lib/persistence.ts +181 -9
- package/extensions/lib/policy.ts +80 -28
- package/extensions/lib/scheduling.ts +105 -18
- package/extensions/lib/selection.ts +112 -39
- package/extensions/lib/tool-surface.ts +6 -4
- package/extensions/lib/transcript.ts +65 -14
- package/extensions/settings.ts +123 -54
- package/package.json +1 -1
package/extensions/evidence.js
CHANGED
|
@@ -562,10 +562,6 @@ function mcpServerForTool(toolName, fallback) {
|
|
|
562
562
|
: fallback;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
function conventionalMcpTool(toolName) {
|
|
566
|
-
return mcpServerForTool(toolName, null) !== null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
565
|
export function registerEvidenceIngestion(pi, {
|
|
570
566
|
entryTypePrefix = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
571
567
|
} = {}) {
|
|
@@ -626,7 +622,7 @@ export function registerEvidenceIngestion(pi, {
|
|
|
626
622
|
return projectEvidenceText(text, descriptor, "utf8-head", truncation ? { truncation } : undefined);
|
|
627
623
|
}
|
|
628
624
|
|
|
629
|
-
if (
|
|
625
|
+
if (mcpServerForTool(toolName, null) !== null) {
|
|
630
626
|
const envelope = eventEnvelope(event);
|
|
631
627
|
const serialized = `${stableStringify(envelope)}\n`;
|
|
632
628
|
if (utf8Bytes(serialized) <= TOOL_RESULT_PROJECTION_BYTES) return;
|
package/extensions/index.js
CHANGED
|
@@ -10,7 +10,8 @@ export {
|
|
|
10
10
|
} from "./settings.ts";
|
|
11
11
|
|
|
12
12
|
const PUBLIC_OPTIONS = Object.freeze([
|
|
13
|
-
"thresholds", "providerInputBudget", "blacklistAutoFoldTools",
|
|
13
|
+
"thresholds", "providerInputBudget", "blacklistAutoFoldTools", "toolFoldThreshold",
|
|
14
|
+
"postFoldNotice",
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
17
|
const GENERATOR_DELETED = "the model brief generator is deleted (2026-08-14): across 15 sealed " +
|
|
@@ -24,11 +25,15 @@ const INVERTED_AUTO_FOLD_LIST = "renamed blacklistAutoFoldTools, and the sense i
|
|
|
24
25
|
"it meant to permit";
|
|
25
26
|
|
|
26
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",
|
|
27
32
|
guidance: "deleted 2026-08-23: it switched the copy that taught the agent to " +
|
|
28
33
|
"CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the frontier and " +
|
|
29
34
|
"the agent annotates what it cut, so the guidance it switched is replaced wholesale by " +
|
|
30
|
-
"the post-fold notice
|
|
31
|
-
"
|
|
35
|
+
"the post-fold notice. That notice has its own option, postFoldNotice, which defaults " +
|
|
36
|
+
"false: the deterministic shape is what the campaign measured",
|
|
32
37
|
toolName: "the deployment identity is hardwired to pi-fold: the tool is pi_fold_context",
|
|
33
38
|
toolLabel: "the deployment identity is hardwired to pi-fold",
|
|
34
39
|
brandNoun: "the deployment identity is hardwired to pi-fold",
|
package/extensions/json.ts
CHANGED
|
@@ -194,8 +194,30 @@ export function isObjectRef(value: unknown): value is EvidenceRef {
|
|
|
194
194
|
typeof sha256 === "string" && reflectApply(regexpTest, sha256Pattern, [sha256]);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// THE HOT PATH (issue #2, 2026-08-28). This is the most-called function in the package:
|
|
198
|
+
// every mark, span, claim, eligibility and measurement pass keys its refs through it,
|
|
199
|
+
// and a reporter profiling a 467KB window found the serializer under it at the top of a
|
|
200
|
+
// V8 profile with the process pegged at 80 percent for forty minutes. The general
|
|
201
|
+
// serializer earns its cost on arbitrary values, checking for proxies, cycles, accessors
|
|
202
|
+
// and non-plain prototypes; none of that is in question for three strings off a ref.
|
|
203
|
+
//
|
|
204
|
+
// The output is IDENTICAL, not merely equivalent, and that is the whole design of this
|
|
205
|
+
// shape: safeJson emits object keys in own-property order rather than sorted, so the
|
|
206
|
+
// literal below is the same byte sequence its object form produced, and string escaping
|
|
207
|
+
// is native JSON.stringify on both paths. It matters because these keys order
|
|
208
|
+
// `protectionSha256`'s sort and reach `branchSha256`'s missing-digest, so a format that
|
|
209
|
+
// merely round-trips would move digests that sealed sessions carry. Anything that is not
|
|
210
|
+
// three strings falls through to the serializer, which owns every remaining case.
|
|
211
|
+
// Measured 8.5x, 4.27us to 0.50us per call, over 2,000 refs. Gate 150 pins the identity.
|
|
197
212
|
export function objectRefKey(ref: EvidenceRef): string {
|
|
198
|
-
|
|
213
|
+
const sessionId = ref.sessionId;
|
|
214
|
+
const entryId = ref.entryId;
|
|
215
|
+
const role = ref.role;
|
|
216
|
+
if (typeof sessionId === "string" && typeof entryId === "string" && typeof role === "string") {
|
|
217
|
+
return `{"sessionId":${jsonStringify(sessionId)},"entryId":${jsonStringify(entryId)},` +
|
|
218
|
+
`"role":${jsonStringify(role)}}`;
|
|
219
|
+
}
|
|
220
|
+
return stableStringify({ sessionId, entryId, role });
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
export function sameObjectIdentity(left: EvidenceRef, right: EvidenceRef): boolean {
|
|
@@ -15,6 +15,40 @@ export function bytes(value: unknown): number {
|
|
|
15
15
|
return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Does `heldText`, the serialization of the first `held` elements, stand as a prefix of
|
|
20
|
+
* `bodyText`, the serialization of the whole array?
|
|
21
|
+
*
|
|
22
|
+
* ARRAY SERIALIZATION IS PREFIX-FREE BY CONSTRUCTION, which is the whole reason this can be
|
|
23
|
+
* answered from a string the caller already has instead of by serializing a slice. `safeJson`
|
|
24
|
+
* emits `[`, then the elements separated by `,`, then `]`: no whitespace, no trailing comma.
|
|
25
|
+
* So the serialization of the first `held` elements is exactly the head of the whole one with
|
|
26
|
+
* `]` in place of the `,` that follows element `held - 1`.
|
|
27
|
+
*
|
|
28
|
+
* The two ends break that rule and are branched on rather than argued away. At `held` 0 the
|
|
29
|
+
* prefix is `[]` and there is no comma to match, so testing `"[," ` would be nonsense. At
|
|
30
|
+
* `held === length` there is no following element and the two strings are simply equal.
|
|
31
|
+
*
|
|
32
|
+
* The comma is the reason this is not a bare `startsWith`, and it is worth being exact about
|
|
33
|
+
* when it earns its place. JSON's object and string forms SELF-TERMINATE, with `}` and a
|
|
34
|
+
* closing quote, so for the message arrays this has one caller for, a bare `startsWith` would
|
|
35
|
+
* happen to agree. Numbers do not self-terminate: `[1]` is a leading substring of `[12]` and
|
|
36
|
+
* a bare `startsWith` accepts it. The separator is what makes the predicate correct for any
|
|
37
|
+
* array rather than for today's caller, and a false positive in that caller splices a stale
|
|
38
|
+
* head in front of a live tail. Gate 154 drives the numeric case for exactly this reason.
|
|
39
|
+
*/
|
|
40
|
+
export function serializedPrefixHolds(
|
|
41
|
+
bodyText: string,
|
|
42
|
+
heldText: string | null | undefined,
|
|
43
|
+
held: number,
|
|
44
|
+
length: number,
|
|
45
|
+
): boolean {
|
|
46
|
+
if (heldText === null || heldText === undefined || length < held) return false;
|
|
47
|
+
if (held === 0) return heldText === "[]";
|
|
48
|
+
if (held === length) return bodyText === heldText;
|
|
49
|
+
return bodyText.startsWith(`${heldText.slice(0, -1)},`);
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
export function boundedUtf8(text: string, maximumBytes: number): string {
|
|
19
53
|
const buffer = Buffer.from(text, "utf8");
|
|
20
54
|
if (buffer.length <= maximumBytes) return text;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundReceiptText } from "./measurement.ts";
|
|
2
|
+
import { oneLine } from "./selection.ts";
|
|
2
3
|
import {
|
|
3
4
|
contextBrand,
|
|
4
5
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
* worth a turn's attention, and it is ephemeral, so it never accumulates.
|
|
23
24
|
*/
|
|
24
25
|
export function foldNoticeText(input: {
|
|
25
|
-
unbriefed: ReadonlyArray<{ id: string; kind: string; tokens: number }>;
|
|
26
|
+
unbriefed: ReadonlyArray<{ id: string; kind: string; tokens: number; brief?: string }>;
|
|
26
27
|
pending: number;
|
|
27
28
|
toolName: string;
|
|
28
29
|
brandNoun?: string;
|
|
@@ -49,8 +50,15 @@ export function foldNoticeText(input: {
|
|
|
49
50
|
].join("\n");
|
|
50
51
|
// WHAT IT COULD NOT NAME, IT COUNTS (gate 136's law, and gate 115's shape one level
|
|
51
52
|
// out): a list that stops early and says nothing reads as a complete list.
|
|
53
|
+
// EACH ROW IDENTIFIES ITS SPAN. The pending mark carries the deterministic brief the
|
|
54
|
+
// runtime cut it with, and a notice that withholds it makes the agent guess: the
|
|
55
|
+
// dogfooded 1M session answered "I need to brief three pending folds. What are they?
|
|
56
|
+
// Likely: ..." from memory, and a wrong guess writes an accurate-sounding brief onto
|
|
57
|
+
// the wrong fold. The head is the identification; the agent adds what it cannot know.
|
|
58
|
+
const NOTICE_ROW_CHARS = 160;
|
|
52
59
|
const entries = input.unbriefed
|
|
53
|
-
.map((fold) => `${fold.id} (${fold.kind}, ~${fold.tokens} tokens)`
|
|
60
|
+
.map((fold) => `${fold.id} (${fold.kind}, ~${fold.tokens} tokens)` +
|
|
61
|
+
(fold.brief ? `: ${oneLine(fold.brief, NOTICE_ROW_CHARS)}` : ""));
|
|
54
62
|
const overflowText = (seated: number) =>
|
|
55
63
|
`${input.toolName} {"action":"status"} lists the other ${input.unbriefed.length - seated}.`;
|
|
56
64
|
let seated = entries.length;
|
|
@@ -65,6 +73,7 @@ export function foldNoticeText(input: {
|
|
|
65
73
|
`[${brand} folds] Pending folds are waiting for briefs; the list is unavailable this pass.`);
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
|
|
68
77
|
export interface ContextReceipt {
|
|
69
78
|
kind: string;
|
|
70
79
|
ordinal: number;
|
|
@@ -76,8 +85,6 @@ export interface ContextReceipt {
|
|
|
76
85
|
occupancyAfter: number | null;
|
|
77
86
|
spansFolded: number;
|
|
78
87
|
toolResultsFolded: number;
|
|
79
|
-
splitFolds: number;
|
|
80
|
-
splitFromChars: number;
|
|
81
88
|
absorbedWedges: number;
|
|
82
89
|
recovered: boolean;
|
|
83
90
|
protectedBytes: number;
|
|
@@ -96,8 +103,6 @@ export function contextReceipt(input: Partial<ContextReceipt> & { kind: string;
|
|
|
96
103
|
occupancyAfter: input.occupancyAfter ?? null,
|
|
97
104
|
spansFolded: input.spansFolded ?? 0,
|
|
98
105
|
toolResultsFolded: input.toolResultsFolded ?? 0,
|
|
99
|
-
splitFolds: input.splitFolds ?? 0,
|
|
100
|
-
splitFromChars: input.splitFromChars ?? 0,
|
|
101
106
|
absorbedWedges: input.absorbedWedges ?? 0,
|
|
102
107
|
recovered: input.recovered ?? false,
|
|
103
108
|
protectedBytes: input.protectedBytes ?? 0,
|
|
@@ -128,9 +133,6 @@ export function receiptLine(receipt: ContextReceipt): string {
|
|
|
128
133
|
`about ${receipt.freedTokens} tokens freed.`,
|
|
129
134
|
occupancy,
|
|
130
135
|
folded,
|
|
131
|
-
receipt.splitFolds
|
|
132
|
-
? `A ${receipt.splitFromChars}-char span was split into ${receipt.splitFolds} bite-sized folds.`
|
|
133
|
-
: "",
|
|
134
136
|
receipt.absorbedWedges
|
|
135
137
|
? `${receipt.absorbedWedges} short stale span(s) wedged between folds were absorbed into their ` +
|
|
136
138
|
"later neighbour rather than left as crumbs."
|
|
@@ -176,3 +178,133 @@ export function receiptBlockText(input: {
|
|
|
176
178
|
}
|
|
177
179
|
return [header, ...kept, verbs].join("\n");
|
|
178
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
|
}
|