pi-fold 1.0.4 → 3.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 +75 -28
- package/extensions/active-context.ts +1213 -2756
- package/extensions/evidence.js +0 -10
- package/extensions/index.js +25 -32
- package/extensions/json.ts +3 -29
- package/extensions/lib/canonical.ts +0 -8
- package/extensions/lib/curation.ts +52 -313
- package/extensions/lib/editor-ui.ts +739 -0
- package/extensions/lib/folding.ts +57 -446
- package/extensions/lib/instrumentation.ts +13 -127
- package/extensions/lib/measurement.ts +6 -83
- package/extensions/lib/persistence.ts +272 -229
- package/extensions/lib/policy.ts +165 -612
- package/extensions/lib/rollback.ts +0 -53
- package/extensions/lib/scheduling.ts +352 -450
- package/extensions/lib/selection.ts +212 -390
- package/extensions/lib/tool-surface.ts +29 -46
- package/extensions/lib/transcript.ts +47 -166
- package/extensions/settings.ts +481 -0
- package/package.json +4 -1
- package/extensions/lib/surfacing.ts +0 -604
- package/extensions/summarizer.js +0 -291
package/extensions/evidence.js
CHANGED
|
@@ -562,16 +562,6 @@ function mcpServerForTool(toolName, fallback) {
|
|
|
562
562
|
: fallback;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
/**
|
|
566
|
-
* MCP tools are recognized by their name, and by nothing else.
|
|
567
|
-
*
|
|
568
|
-
* This was a host-supplied predicate whose default returned false for everything, so a
|
|
569
|
-
* conventionally named MCP tool was never classified unless a host wrote the predicate:
|
|
570
|
-
* a documented feature whose shipped state guaranteed it never ran. `mcp__server__tool`
|
|
571
|
-
* is the convention every MCP host follows, so it is the rule. A name outside the
|
|
572
|
-
* convention is deliberately not classified as MCP, and its oversized results fall to
|
|
573
|
-
* the ordinary tool-result path.
|
|
574
|
-
*/
|
|
575
565
|
function conventionalMcpTool(toolName) {
|
|
576
566
|
return mcpServerForTool(toolName, null) !== null;
|
|
577
567
|
}
|
package/extensions/index.js
CHANGED
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
import { registerActiveContext } from "./active-context.ts";
|
|
2
2
|
import { registerEvidenceIngestion } from "./evidence.js";
|
|
3
|
-
import { createSummarizeContextSpan } from "./summarizer.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
export {
|
|
5
|
+
applyFoldSettingsEdit,
|
|
6
|
+
loadFoldSettingsFile,
|
|
7
|
+
readFoldSettingsFile,
|
|
8
|
+
registerFoldSettings,
|
|
9
|
+
saveFoldSettingsFile,
|
|
10
|
+
} from "./settings.ts";
|
|
11
|
+
|
|
13
12
|
const PUBLIC_OPTIONS = Object.freeze([
|
|
14
|
-
"thresholds", "
|
|
13
|
+
"thresholds", "providerInputBudget", "blacklistAutoFoldTools",
|
|
15
14
|
]);
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
const GENERATOR_DELETED = "the model brief generator is deleted (2026-08-14): across 15 sealed " +
|
|
17
|
+
"generator runs, 75 percent of its 1,186 upgraded briefs were never consulted or never became " +
|
|
18
|
+
"visible, and the no-generator build scored the same for less. Every fold carries its " +
|
|
19
|
+
"deterministic brief; sessions already holding model briefs keep them on load";
|
|
20
|
+
|
|
22
21
|
const INVERTED_AUTO_FOLD_LIST = "renamed blacklistAutoFoldTools, and the sense is INVERTED: " +
|
|
23
22
|
"every completed tool batch folds unmarked, and the list names the exceptions whose " +
|
|
24
23
|
"results must stay raw. An allow-list moved across verbatim would bar exactly the tools " +
|
|
25
24
|
"it meant to permit";
|
|
26
25
|
|
|
27
|
-
/**
|
|
28
|
-
* Options that left the surface, each answered by name. Deployment identity is hardwired:
|
|
29
|
-
* this is a branded package with one deployment, not a framework a host rebrands, and a
|
|
30
|
-
* consumer that moved `entryTypePrefix` stranded its own durable folds under a namespace
|
|
31
|
-
* nothing would read back.
|
|
32
|
-
*/
|
|
33
26
|
const REFUSED_OPTIONS = Object.freeze({
|
|
27
|
+
guidance: "deleted 2026-08-23: it switched the copy that taught the agent to " +
|
|
28
|
+
"CHOOSE SPANS, and the agent does not choose spans. The runtime cuts at the frontier and " +
|
|
29
|
+
"the agent annotates what it cut, so the guidance it switched is replaced wholesale by " +
|
|
30
|
+
"the post-fold notice, which is not optional: a fold the agent is never told about is one " +
|
|
31
|
+
"it can never brief",
|
|
34
32
|
toolName: "the deployment identity is hardwired to pi-fold: the tool is pi_fold_context",
|
|
35
33
|
toolLabel: "the deployment identity is hardwired to pi-fold",
|
|
36
34
|
brandNoun: "the deployment identity is hardwired to pi-fold",
|
|
37
35
|
entryTypePrefix: "the deployment identity is hardwired to pi-fold: durable state lives " +
|
|
38
36
|
"under pi-fold-active-context, and moving it would strand every fold already written",
|
|
39
37
|
commandPrefix: "the deployment identity is hardwired to pi-fold: the commands are " +
|
|
40
|
-
"/
|
|
38
|
+
"/fold, /fold-editor, /fold-status and /fold-settings",
|
|
41
39
|
commandNames: "the deployment identity is hardwired to pi-fold: the commands are " +
|
|
42
|
-
"/
|
|
40
|
+
"/fold, /fold-editor, /fold-status and /fold-settings",
|
|
43
41
|
isMcpTool: "MCP tools are recognized by the mcp__server__tool naming convention. The " +
|
|
44
42
|
"predicate defaulted to false, so conventionally named MCP tools were never classified " +
|
|
45
43
|
"unless a host supplied one, which is a documented feature that never ran",
|
|
@@ -52,11 +50,11 @@ const REFUSED_OPTIONS = Object.freeze({
|
|
|
52
50
|
providerTotalWindow: "renamed providerInputBudget, and it is ALREADY NET: pass the tokens " +
|
|
53
51
|
"the deployment may actually fill, not the total window the runtime then subtracts a " +
|
|
54
52
|
"guessed output reservation from",
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
summarizer: GENERATOR_DELETED,
|
|
54
|
+
summarizeContextSpan: GENERATOR_DELETED,
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
export function registerPiFold(pi, options = {}
|
|
57
|
+
export function registerPiFold(pi, options = {}) {
|
|
60
58
|
for (const name of Object.keys(options)) {
|
|
61
59
|
if (Object.hasOwn(REFUSED_OPTIONS, name)) {
|
|
62
60
|
throw new Error(`${name} is no longer an option: ${REFUSED_OPTIONS[name]}`);
|
|
@@ -66,13 +64,8 @@ export function registerPiFold(pi, options = {}, loadHostModule) {
|
|
|
66
64
|
`${PUBLIC_OPTIONS.join(", ")}`);
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
|
-
const { summarizer = "session", ...activeContextOptions } = options;
|
|
70
|
-
const summarizeContextSpan = createSummarizeContextSpan(summarizer, loadHostModule);
|
|
71
|
-
// Evidence first, then the runtime. Ingestion is unconditional: the artifacts are the
|
|
72
|
-
// exact-recovery anchors an oversized tool result folds against, so a deployment that
|
|
73
|
-
// turned them off kept the folds and lost what makes them lossless.
|
|
74
67
|
registerEvidenceIngestion(pi);
|
|
75
|
-
return registerActiveContext(pi,
|
|
68
|
+
return registerActiveContext(pi, options);
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
export default function piFold(pi, options) {
|
package/extensions/json.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface EvidenceRef {
|
|
|
8
8
|
sha256: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// Capture trust-boundary intrinsics before mutable prototypes can influence validation.
|
|
12
11
|
const ArrayConstructor = Array;
|
|
13
12
|
const arrayIsArray = Array.isArray;
|
|
14
13
|
const arrayPrototype = Array.prototype;
|
|
@@ -95,7 +94,6 @@ function safeJson(value: unknown, ancestors: unknown[]): string | undefined {
|
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
/** Preserve JSON property order without invoking getters, toJSON, coercion, iterators, or mutable prototypes. */
|
|
99
97
|
export function stableStringify(value: unknown): string {
|
|
100
98
|
const serialized = safeJson(value, new ArrayConstructor<unknown>());
|
|
101
99
|
if (typeof serialized !== "string") throw new Error("Value is not JSON-serializable");
|
|
@@ -122,7 +120,6 @@ export function ownDataProperty(value: unknown, key: PropertyKey): unknown {
|
|
|
122
120
|
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
/** Copy a dense own-data array without invoking inherited accessors, iterators, or methods. */
|
|
126
123
|
export function denseOwnArrayValues(value: unknown): unknown[] | null {
|
|
127
124
|
if (!arrayIsArray(value) || isProxy(value) || getPrototypeOf(value) !== arrayPrototype) return null;
|
|
128
125
|
const lengthDescriptor = getOwnPropertyDescriptor(value, "length");
|
|
@@ -202,7 +199,9 @@ export function objectRefKey(ref: EvidenceRef): string {
|
|
|
202
199
|
}
|
|
203
200
|
|
|
204
201
|
export function sameObjectIdentity(left: EvidenceRef, right: EvidenceRef): boolean {
|
|
205
|
-
return
|
|
202
|
+
return left.sessionId === right.sessionId &&
|
|
203
|
+
left.entryId === right.entryId &&
|
|
204
|
+
left.role === right.role;
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
export function evidenceValue(message: unknown): unknown {
|
|
@@ -216,31 +215,6 @@ export function evidenceValue(message: unknown): unknown {
|
|
|
216
215
|
};
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
/**
|
|
220
|
-
* Evidence digests, kept against the message OBJECT.
|
|
221
|
-
*
|
|
222
|
-
* A message is canonicalized and hashed whole to produce its digest, and the messages
|
|
223
|
-
* this runs on are transcript entries: tool results in the tens of kilobytes. Nothing
|
|
224
|
-
* cached the result, so every reading that wanted to confirm a ref rehashed the entire
|
|
225
|
-
* message from scratch. `exactMapped` does exactly that on every lookup, and the
|
|
226
|
-
* selection, surfacing, ordering and accounting passes call it once per ref, per fold,
|
|
227
|
-
* several times a turn. Replayed against the sealed sol-20260812 rep-2 session at its
|
|
228
|
-
* 85 percent point, that single recomputation was 76 percent of all CPU, and it grows
|
|
229
|
-
* with the transcript because the transcript is what it rehashes.
|
|
230
|
-
*
|
|
231
|
-
* A WeakMap on the object is the same shape `MAPPED_BY_KEY` and `BRANCH_POSITIONS`
|
|
232
|
-
* already use one line over, and the same rule gate 113 settled for entry evidence: the
|
|
233
|
-
* session is append-only, so keep the derivation against the object and let a REPLACED
|
|
234
|
-
* object miss rather than serve a stale answer. Entries are never edited in place; they
|
|
235
|
-
* are appended, and a changed message is a different object, which misses and rehashes.
|
|
236
|
-
*
|
|
237
|
-
* What this no longer detects, stated plainly: a message mutated IN PLACE after it was
|
|
238
|
-
* first hashed now keeps its original digest. That case was already half invisible,
|
|
239
|
-
* because the ref a snapshot carries was itself computed from that same object at
|
|
240
|
-
* mapping time, so a mutation that beat the digest would have to be caught by this
|
|
241
|
-
* recomputation alone. No path in this package edits a persisted message, and the
|
|
242
|
-
* runtime clones before it stores.
|
|
243
|
-
*/
|
|
244
218
|
const EVIDENCE_DIGESTS = new WeakMap<object, string>();
|
|
245
219
|
|
|
246
220
|
export function evidenceSha256(message: unknown): string {
|
|
@@ -18,16 +18,9 @@ export function bytes(value: unknown): number {
|
|
|
18
18
|
export function boundedUtf8(text: string, maximumBytes: number): string {
|
|
19
19
|
const buffer = Buffer.from(text, "utf8");
|
|
20
20
|
if (buffer.length <= maximumBytes) return text;
|
|
21
|
-
// A multi-byte sequence cut by the bound decodes to one trailing replacement
|
|
22
|
-
// character; dropping it keeps the slice an exact prefix of the source.
|
|
23
21
|
return buffer.subarray(0, maximumBytes).toString("utf8").replace(/\uFFFD$/, "");
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
/**
|
|
27
|
-
* The suffix of `text` beginning at a byte offset. A multi-byte sequence cut by the
|
|
28
|
-
* offset decodes to one leading replacement character; dropping it keeps the slice an
|
|
29
|
-
* exact suffix of the source, which is what makes a paged peek losslessly rejoinable.
|
|
30
|
-
*/
|
|
31
24
|
export function utf8Slice(text: string, offsetBytes: number): string {
|
|
32
25
|
if (offsetBytes <= 0) return text;
|
|
33
26
|
const buffer = Buffer.from(text, "utf8");
|
|
@@ -75,7 +68,6 @@ export function contentText(message: unknown): string {
|
|
|
75
68
|
return text.join("\n");
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
/** Pi's exact one-entry projection, kept local so this service has no second transcript owner. */
|
|
79
71
|
export function sessionEntryMessages(entry: Record<string, unknown>): unknown[] {
|
|
80
72
|
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
81
73
|
const message = entry.message as Record<string, unknown>;
|
|
@@ -1,311 +1,70 @@
|
|
|
1
|
-
import { objectRefKey } from "../json.ts";
|
|
2
|
-
import {
|
|
3
|
-
boundedUtf8,
|
|
4
|
-
bytes,
|
|
5
|
-
messageRole,
|
|
6
|
-
} from "./canonical.ts";
|
|
7
1
|
import { boundReceiptText } from "./measurement.ts";
|
|
8
2
|
import {
|
|
9
3
|
contextBrand,
|
|
10
|
-
CONTEXT_MARK_RESPONSE_BYTES,
|
|
11
4
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
5
|
+
FOLD_NOTICE_BYTES,
|
|
12
6
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
13
7
|
MAX_CONTEXT_RECEIPTS,
|
|
14
|
-
MAX_LAST_CALL_TEXT_BYTES,
|
|
15
|
-
MAX_SURFACING_LINE_BYTES,
|
|
16
|
-
MAX_THRESHOLD_NOTICE_TEXT_BYTES,
|
|
17
|
-
} from "./policy.ts";
|
|
18
|
-
import type {
|
|
19
|
-
ActiveContextSnapshot,
|
|
20
|
-
ActiveContextState,
|
|
21
8
|
} from "./policy.ts";
|
|
22
9
|
|
|
23
10
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* The ladder's commit triggers are correct and silent. Silent is the problem: the
|
|
27
|
-
* agent learns that its evidence became placeholders by reading a placeholder, which
|
|
28
|
-
* is the one moment it can no longer do anything about it. So a commit is ANNOUNCED
|
|
29
|
-
* before it happens, early enough that reacting is still cheap, and the announcement
|
|
30
|
-
* carries what is about to happen, what it costs, and the actions that change it.
|
|
31
|
-
*
|
|
32
|
-
* Everything here is deterministic: transcript ordinals, measured tokens and byte
|
|
33
|
-
* counts only. No wall clock and no randomness.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
export interface StaleToolMass {
|
|
37
|
-
/** Serialized bytes of tool results outside the fresh tail and outside every fold. */
|
|
38
|
-
bytes: number;
|
|
39
|
-
/** How many tool results that mass is spread over. */
|
|
40
|
-
results: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Tool-result mass a commit could actually reach: outside the fresh tail, outside any
|
|
45
|
-
* protection, and not already owned by a fold. Occupancy alone would announce commits
|
|
46
|
-
* that have nothing to fold; this is the second signal that says the announcement is
|
|
47
|
-
* about to be worth something.
|
|
48
|
-
*/
|
|
49
|
-
export function staleToolMass(
|
|
50
|
-
snapshot: ActiveContextSnapshot,
|
|
51
|
-
state: ActiveContextState,
|
|
52
|
-
): StaleToolMass {
|
|
53
|
-
const owned = new Set(state.folds.flatMap((fold) =>
|
|
54
|
-
fold.parts.flatMap((part) => part.kind === "raw" ? [objectRefKey(part.ref)] : [])));
|
|
55
|
-
let total = 0;
|
|
56
|
-
let results = 0;
|
|
57
|
-
for (const item of snapshot.mapped) {
|
|
58
|
-
if (!item.ref || messageRole(item.message) !== "toolResult") continue;
|
|
59
|
-
if (snapshot.toolProtectedIndices.has(item.index)) continue;
|
|
60
|
-
if (owned.has(objectRefKey(item.ref))) continue;
|
|
61
|
-
total += bytes(item.message);
|
|
62
|
-
results += 1;
|
|
63
|
-
}
|
|
64
|
-
return { bytes: total, results };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface CurationSignals {
|
|
68
|
-
/** Measured provider tokens as a share of the truthful serving budget; null unmeasured. */
|
|
69
|
-
occupancy: number | null;
|
|
70
|
-
/** The trigger line this occupancy is read against: thresholds.maxTarget. */
|
|
71
|
-
maxTarget: number;
|
|
72
|
-
occupancyTokens: number | null;
|
|
73
|
-
/** THE serving budget: one resolved value, the same one the fence and estimator use. */
|
|
74
|
-
budgetTokens: number;
|
|
75
|
-
window: number;
|
|
76
|
-
/** Stale tool mass as a share of the serving budget, in the session's own tokens. */
|
|
77
|
-
staleToolShare: number;
|
|
78
|
-
staleToolTokens: number;
|
|
79
|
-
staleToolResults: number;
|
|
80
|
-
/** Marks a commit could apply right now. */
|
|
81
|
-
eligibleFolds: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Both signals are measured against ONE resolved serving budget, handed in by the
|
|
86
|
-
* caller that owns the capacity accounting. Nothing here re-derives a window from a
|
|
87
|
-
* descriptor: rep 15's trigger and gate ran the whole run against a 272,000-token
|
|
88
|
-
* per-request descriptor while the fence used the truthful 383,616, because the budget
|
|
89
|
-
* was computed twice from two different places.
|
|
90
|
-
*/
|
|
91
|
-
export function curationSignals(input: {
|
|
92
|
-
snapshot: ActiveContextSnapshot;
|
|
93
|
-
state: ActiveContextState;
|
|
94
|
-
usedTokens: number | null;
|
|
95
|
-
budgetTokens: number;
|
|
96
|
-
/** The resolved serving WINDOW the budget was taken from, reported for audit. */
|
|
97
|
-
window: number;
|
|
98
|
-
/** The session's own measured serialized chars per token; never a fixed constant. */
|
|
99
|
-
charsPerToken: number;
|
|
100
|
-
eligibleFolds: number;
|
|
101
|
-
}): CurationSignals {
|
|
102
|
-
const mass = staleToolMass(input.snapshot, input.state);
|
|
103
|
-
const charsPerToken = Number.isFinite(input.charsPerToken) && input.charsPerToken > 0
|
|
104
|
-
? input.charsPerToken
|
|
105
|
-
: 4;
|
|
106
|
-
const staleToolTokens = Math.ceil(mass.bytes / charsPerToken);
|
|
107
|
-
return {
|
|
108
|
-
maxTarget: input.snapshot.thresholds.maxTarget,
|
|
109
|
-
occupancy: input.usedTokens === null || input.budgetTokens <= 0
|
|
110
|
-
? null
|
|
111
|
-
: input.usedTokens / input.budgetTokens,
|
|
112
|
-
occupancyTokens: input.usedTokens,
|
|
113
|
-
budgetTokens: input.budgetTokens,
|
|
114
|
-
window: input.window,
|
|
115
|
-
staleToolShare: input.budgetTokens > 0 ? staleToolTokens / input.budgetTokens : 0,
|
|
116
|
-
staleToolTokens,
|
|
117
|
-
staleToolResults: mass.results,
|
|
118
|
-
eligibleFolds: input.eligibleFolds,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* ONE signal. Occupancy of the serving budget reaches maxTarget, and a commit is due.
|
|
11
|
+
* THE POST-FOLD NOTICE: THE ONE MOMENT THE AGENT KNOWS SOMETHING THE RUNTIME DOES NOT.
|
|
124
12
|
*
|
|
125
|
-
* The
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* measured and still reported; it is no longer a condition on the trigger.
|
|
132
|
-
*/
|
|
133
|
-
export function curationTriggerFires(signals: CurationSignals): boolean {
|
|
134
|
-
return signals.occupancy !== null &&
|
|
135
|
-
Number.isFinite(signals.occupancy) &&
|
|
136
|
-
signals.occupancy >= signals.maxTarget;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The awareness block a mark call answers with.
|
|
13
|
+
* The runtime has just cut folds behind the agent, out of material the agent is still
|
|
14
|
+
* looking at. It knows the spans, the sizes and the staleness; it does not know why any
|
|
15
|
+
* of it mattered. So the notice states what was cut and asks for exactly one thing, a
|
|
16
|
+
* brief, and it states the price of answering, which is nothing: the folds it names are
|
|
17
|
+
* PENDING, so a brief written now is stored outside the projection and reaches the window
|
|
18
|
+
* only when the commit writes that placeholder for the first time.
|
|
141
19
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* head of the largest names, and the one percentage worth steering by. It is hard
|
|
146
|
-
* bounded on the receipt block's principle: awareness that becomes bloat is bloat.
|
|
20
|
+
* It is batched rather than continuous. One notice per cut is noise, and noise is how
|
|
21
|
+
* guidance gets ignored; this one appears when enough folds are standing unbriefed to be
|
|
22
|
+
* worth a turn's attention, and it is ephemeral, so it never accumulates.
|
|
147
23
|
*/
|
|
148
|
-
export function
|
|
149
|
-
|
|
150
|
-
|
|
24
|
+
export function foldNoticeText(input: {
|
|
25
|
+
unbriefed: ReadonlyArray<{ id: string; kind: string; tokens: number }>;
|
|
26
|
+
pending: number;
|
|
151
27
|
toolName: string;
|
|
152
28
|
brandNoun?: string;
|
|
153
29
|
}): string {
|
|
154
30
|
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`[${brand}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
`
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
pinnedShare: number;
|
|
191
|
-
maxPinnedShare: number;
|
|
192
|
-
/** The surfacing slate, already bounded and already logged; null on a silent pass. */
|
|
193
|
-
suggestion?: string | null;
|
|
194
|
-
}): string {
|
|
195
|
-
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
196
|
-
const anchors = input.anchors.length
|
|
197
|
-
? input.anchors.slice(0, 3).join(", ")
|
|
198
|
-
: "none";
|
|
199
|
-
const pinnedPercent = Math.round(input.pinnedShare * 100);
|
|
200
|
-
const capPercent = Math.round(input.maxPinnedShare * 100);
|
|
201
|
-
// The slate carries its OWN bound and is joined after this text is bounded, so the
|
|
202
|
-
// carrier's overhead is its bound plus the line's, and neither can eat the other: a
|
|
203
|
-
// long rider must never truncate the one sentence a suggestion consists of.
|
|
204
|
-
return joinSurfacing(boundReceiptText(
|
|
205
|
-
[
|
|
206
|
-
`[${brand} notice] A fold commit just landed; the next one will batch every pending mark ` +
|
|
207
|
-
"into one rewrite, and marks are free until then.",
|
|
208
|
-
"Mark FINISHED units at their clean boundaries; batches beat singles: " +
|
|
209
|
-
`${input.toolName} {"action":"fold","marks":[{"ids":["<start>","<end>"],"brief":"<factual brief>"}]} ` +
|
|
210
|
-
"carries several decisions into that single rewrite, each keeping your brief instead of a " +
|
|
211
|
-
`generated one. ${input.pendingAgentMarks} of your mark(s) pending; ` +
|
|
212
|
-
`${input.eligibleMarks} mark(s) eligible now, freeing about ${input.eligibleFreedTokens} ` +
|
|
213
|
-
`of the ${input.freedTokens} marked token(s).`,
|
|
214
|
-
`Completed units ready to mark, largest first: ${anchors}.`,
|
|
215
|
-
`Pinning: ${input.toolName} {"action":"protect","ids":["<entry-id>"]} holds entries raw through ` +
|
|
216
|
-
`every fold, and {"action":"unprotect"} releases them. Pinned context is ${pinnedPercent}% of ` +
|
|
217
|
-
`the working window against a ${capPercent}% cap; at the cap, protect refuses until ` +
|
|
218
|
-
"something is released.",
|
|
219
|
-
].join("\n"),
|
|
220
|
-
2_048,
|
|
221
|
-
`[${brand} notice] Post-commit curation details are unavailable this pass.`,
|
|
222
|
-
), input.suggestion);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** One carrier, one optional slate line, appended last and bounded on its own. */
|
|
226
|
-
export function joinSurfacing(text: string, suggestion?: string | null): string {
|
|
227
|
-
if (!suggestion) return text;
|
|
228
|
-
return `${text}\n${boundedUtf8(suggestion, MAX_SURFACING_LINE_BYTES)}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** The ruled last-call sentence (Shane, 2026-08-09 13:23), verbatim. */
|
|
232
|
-
export const LAST_CALL_WORDING =
|
|
233
|
-
"Fold commit triggered, please add or edit marks, pin or unpin context based on foreseeable relevance.";
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* The pre-commit last-call.
|
|
237
|
-
*
|
|
238
|
-
* It rides the commit boundary: the band-top trigger has fired, the rewrite is one
|
|
239
|
-
* round away, so this is the single moment telemetry is both accurate and free. It
|
|
240
|
-
* carries the ruled wording, the three numbers the ruling names (occupancy against the
|
|
241
|
-
* commit line, unmarked foldable mass the ladder can reach, pending marks), the verbs,
|
|
242
|
-
* and the one-round law. Never a question: continuing the task is the stated default,
|
|
243
|
-
* and the commit proceeds regardless.
|
|
244
|
-
*/
|
|
245
|
-
export function lastCallText(input: {
|
|
246
|
-
signals: CurationSignals;
|
|
247
|
-
unmarked: { spans: number; tokens: number };
|
|
248
|
-
pendingMarks: number;
|
|
249
|
-
/** Peek copies this exposure marked for reclaim, so the pin that vetoes one is timely. */
|
|
250
|
-
peekReclaims?: number;
|
|
251
|
-
/** The surfacing slate, already bounded and already logged; null on a silent pass. */
|
|
252
|
-
suggestion?: string | null;
|
|
253
|
-
toolName: string;
|
|
254
|
-
brandNoun?: string;
|
|
255
|
-
}): string {
|
|
256
|
-
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
257
|
-
const { signals } = input;
|
|
258
|
-
const occupancy = signals.occupancy === null ? "unmeasured" : `${Math.round(signals.occupancy * 100)}%`;
|
|
259
|
-
const peekReclaims = input.peekReclaims ?? 0;
|
|
260
|
-
return joinSurfacing(boundReceiptText([
|
|
261
|
-
`[${brand} last call] ${LAST_CALL_WORDING}`,
|
|
262
|
-
`Occupancy is ${occupancy} of the ${signals.budgetTokens}-token serving budget, at or past the ` +
|
|
263
|
-
`${Math.round(signals.maxTarget * 100)}% commit line. Unmarked foldable mass: ` +
|
|
264
|
-
`${input.unmarked.spans} span(s), about ${input.unmarked.tokens} tokens. ` +
|
|
265
|
-
`Pending marks: ${input.pendingMarks}.`,
|
|
266
|
-
// Stated only when there is one to veto: a standing sentence about zero peek copies
|
|
267
|
-
// is bytes every exposure pays for and no round ever acts on.
|
|
268
|
-
peekReclaims > 0
|
|
269
|
-
? `Peek copies reclaimed by this commit: ${peekReclaims}. Each duplicates a fold you can peek ` +
|
|
270
|
-
"again at any time, and its placeholder names that fold, so nothing becomes unreachable. " +
|
|
271
|
-
"Pin one to keep the copy raw."
|
|
272
|
-
: "",
|
|
273
|
-
`Marks: ${input.toolName} ` +
|
|
274
|
-
"{\"action\":\"fold\",\"marks\":[{\"ids\":[\"<start>\",\"<end>\"],\"brief\":\"<factual brief>\"}]} " +
|
|
275
|
-
"adds or widens several in one call. Pins: {\"action\":\"protect\",\"ids\":[\"<entry-id>\"]} holds " +
|
|
276
|
-
"entries raw through every fold, and {\"action\":\"unprotect\"} releases them.",
|
|
277
|
-
"This is one round: the commit proceeds on the pass after your next response with whatever marks " +
|
|
278
|
-
"exist. Continuing the task is the default; nothing here needs a reply.",
|
|
279
|
-
].filter(Boolean).join("\n"), MAX_LAST_CALL_TEXT_BYTES,
|
|
280
|
-
`[${brand} last call] ${LAST_CALL_WORDING}`), input.suggestion);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* One occupancy waypoint, stated once. Informatory and quiet: below the commit line
|
|
285
|
-
* nothing folds automatically, so the only useful sentence is where the window stands
|
|
286
|
-
* and that marks and pins made now shape the eventual commit.
|
|
287
|
-
*/
|
|
288
|
-
export function thresholdNoticeText(input: {
|
|
289
|
-
share: number;
|
|
290
|
-
occupancyTokens: number;
|
|
291
|
-
budgetTokens: number;
|
|
292
|
-
maxTarget: number;
|
|
293
|
-
toolName: string;
|
|
294
|
-
brandNoun?: string;
|
|
295
|
-
}): string {
|
|
296
|
-
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
297
|
-
return boundReceiptText(
|
|
298
|
-
`[${brand} notice] Context occupancy crossed ${Math.round(input.share * 100)}% of the ` +
|
|
299
|
-
`${input.budgetTokens}-token serving budget (${input.occupancyTokens} tokens). Nothing folds before ` +
|
|
300
|
-
`the fold commit at ${Math.round(input.maxTarget * 100)}%; marks and pins made now shape it: ` +
|
|
301
|
-
`${input.toolName} {"action":"fold","marks":[{"ids":["<start>","<end>"],"brief":"<factual brief>"}]} ` +
|
|
302
|
-
'or {"action":"protect","ids":["<entry-id>"]}.',
|
|
303
|
-
MAX_THRESHOLD_NOTICE_TEXT_BYTES,
|
|
304
|
-
`[${brand} notice] Context occupancy crossed ${Math.round(input.share * 100)}% of the serving budget.`,
|
|
305
|
-
);
|
|
31
|
+
// THE INSTRUCTION IS SEATED BEFORE THE LIST, and the list is what gives when the
|
|
32
|
+
// notice runs out of room. The first cut of this built one string with the ids in the
|
|
33
|
+
// opening line and handed the whole thing to the bound, so a seven-fold batch spent its
|
|
34
|
+
// budget on ids and lost "here is how to answer" off the end: the agent was told which
|
|
35
|
+
// folds were unbriefed and not told that briefing them was free, which is the only fact
|
|
36
|
+
// in here it cannot work out for itself. Fold count grows without limit and the
|
|
37
|
+
// instruction does not, so the instruction is fixed and the ids are the variable part.
|
|
38
|
+
const head = [
|
|
39
|
+
`[${brand} folds] ${input.unbriefed.length} of your ${input.pending} pending fold(s) carry ` +
|
|
40
|
+
"no brief yet. These spans are cut but NOT folded: every byte they cover is still in " +
|
|
41
|
+
"front of you exactly as it was, and they enter your window only when a commit " +
|
|
42
|
+
"applies them.",
|
|
43
|
+
`Give each one a sentence: ${input.toolName} {"action":"brief","id":"<fold-id>","brief":"..."}. ` +
|
|
44
|
+
"You know why the span mattered and what you will want back from it; the automatic " +
|
|
45
|
+
"brief reads the span alone and does not. Writing it now costs your window nothing, " +
|
|
46
|
+
"because the fold is not in your window yet.",
|
|
47
|
+
`Wrong boundary? ${input.toolName} {"action":"reboundary","ids":[...]} re-cuts it. ` +
|
|
48
|
+
`Should not be taken at all? ${input.toolName} {"action":"unmark","ids":["<fold-id>"]}.`,
|
|
49
|
+
].join("\n");
|
|
50
|
+
// WHAT IT COULD NOT NAME, IT COUNTS (gate 136's law, and gate 115's shape one level
|
|
51
|
+
// out): a list that stops early and says nothing reads as a complete list.
|
|
52
|
+
const entries = input.unbriefed
|
|
53
|
+
.map((fold) => `${fold.id} (${fold.kind}, ~${fold.tokens} tokens)`);
|
|
54
|
+
const overflowText = (seated: number) =>
|
|
55
|
+
`${input.toolName} {"action":"status"} lists the other ${input.unbriefed.length - seated}.`;
|
|
56
|
+
let seated = entries.length;
|
|
57
|
+
let list = `Unbriefed: ${entries.join("; ")}.`;
|
|
58
|
+
while (seated > 0 && Buffer.byteLength(`${head}\n${list}`, "utf8") > FOLD_NOTICE_BYTES) {
|
|
59
|
+
seated -= 1;
|
|
60
|
+
list = seated === 0
|
|
61
|
+
? overflowText(0)
|
|
62
|
+
: `Unbriefed: ${entries.slice(0, seated).join("; ")}. ${overflowText(seated)}`;
|
|
63
|
+
}
|
|
64
|
+
return boundReceiptText(`${head}\n${list}`, FOLD_NOTICE_BYTES,
|
|
65
|
+
`[${brand} folds] Pending folds are waiting for briefs; the list is unavailable this pass.`);
|
|
306
66
|
}
|
|
307
67
|
|
|
308
|
-
/** One automatic context action, reported back as status rather than as advice. */
|
|
309
68
|
export interface ContextReceipt {
|
|
310
69
|
kind: string;
|
|
311
70
|
ordinal: number;
|
|
@@ -313,20 +72,14 @@ export interface ContextReceipt {
|
|
|
313
72
|
foldsCommitted: number;
|
|
314
73
|
foldsCreated: number;
|
|
315
74
|
freedTokens: number;
|
|
316
|
-
/** Measured occupancy either side of the action, in tokens. The impact, concretely. */
|
|
317
75
|
occupancyBefore: number | null;
|
|
318
76
|
occupancyAfter: number | null;
|
|
319
|
-
/** What the action actually folded, split the way an agent thinks about it. */
|
|
320
77
|
spansFolded: number;
|
|
321
78
|
toolResultsFolded: number;
|
|
322
|
-
/** Bite-sized splitting, when an oversized span became sequential folds. */
|
|
323
79
|
splitFolds: number;
|
|
324
80
|
splitFromChars: number;
|
|
325
|
-
/** Short stale spans a commit absorbed into their later neighbour fold. */
|
|
326
81
|
absorbedWedges: number;
|
|
327
|
-
/** Overflow recovery, when this action rebuilt a request the provider would reject. */
|
|
328
82
|
recovered: boolean;
|
|
329
|
-
/** Mass the agent's pins held out of this commit's reach, in bytes. */
|
|
330
83
|
protectedBytes: number;
|
|
331
84
|
note: string | null;
|
|
332
85
|
}
|
|
@@ -352,7 +105,6 @@ export function contextReceipt(input: Partial<ContextReceipt> & { kind: string;
|
|
|
352
105
|
};
|
|
353
106
|
}
|
|
354
107
|
|
|
355
|
-
/** Newest last, oldest evicted: a receipt block that can never grow into its own problem. */
|
|
356
108
|
export function withReceipt(
|
|
357
109
|
receipts: readonly ContextReceipt[],
|
|
358
110
|
receipt: ContextReceipt,
|
|
@@ -387,18 +139,13 @@ export function receiptLine(receipt: ContextReceipt): string {
|
|
|
387
139
|
? "This ran as overflow recovery: the request would not have been transmissible, and it was rebuilt rather than dropped."
|
|
388
140
|
: "",
|
|
389
141
|
receipt.protectedBytes
|
|
390
|
-
? `${receipt.protectedBytes} byte(s) stay pinned
|
|
142
|
+
? `${receipt.protectedBytes} byte(s) stay pinned; unpin releases them when the work moves on.`
|
|
391
143
|
: "",
|
|
392
144
|
receipt.note ?? "",
|
|
393
145
|
];
|
|
394
146
|
return parts.filter(Boolean).join(" ");
|
|
395
147
|
}
|
|
396
148
|
|
|
397
|
-
/**
|
|
398
|
-
* The receipt block. Informatory, never exhortative: it reports what the runtime did,
|
|
399
|
-
* what it cost, and the verbs that correct it. Hard-bounded, because a report about
|
|
400
|
-
* bloat that becomes bloat has argued against itself.
|
|
401
|
-
*/
|
|
402
149
|
export function receiptBlockText(input: {
|
|
403
150
|
receipts: readonly ContextReceipt[];
|
|
404
151
|
toolName: string;
|
|
@@ -410,14 +157,8 @@ export function receiptBlockText(input: {
|
|
|
410
157
|
`[${brand} actions] Recent automatic context actions, newest last. Exact source remains expandable.`;
|
|
411
158
|
const verbs = `Correct any of it: ${input.toolName} ` +
|
|
412
159
|
"{\"action\":\"expand\",\"id\":\"<fold-id>\"} restores a span in place; " +
|
|
413
|
-
"{\"action\":\"
|
|
414
|
-
"
|
|
415
|
-
"mis-cut fold to raw so you can fold the span you meant.";
|
|
416
|
-
// The verbs are the only part of this block an agent can act on, and they sit last, so
|
|
417
|
-
// a plain tail truncation drops them precisely when the report runs long -- which is
|
|
418
|
-
// when a commit did the most and correcting it matters most. Reserve them, and spend
|
|
419
|
-
// what remains on receipt lines newest-first: an older action that no longer fits is
|
|
420
|
-
// the right thing to lose, and its structured record is still in the ring.
|
|
160
|
+
"{\"action\":\"reboundary\",\"id\":\"<fold-id>\"} returns a mis-cut fold to raw so you can " +
|
|
161
|
+
"fold the span you meant.";
|
|
421
162
|
const fixed = Buffer.byteLength(header, "utf8") + Buffer.byteLength(verbs, "utf8") + 1;
|
|
422
163
|
if (fixed > CONTEXT_RECEIPT_BLOCK_BYTES) {
|
|
423
164
|
return `[${brand} actions] Recent automatic context actions are unavailable this pass.`;
|
|
@@ -427,8 +168,6 @@ export function receiptBlockText(input: {
|
|
|
427
168
|
for (let index = input.receipts.length - 1; index >= 0; index -= 1) {
|
|
428
169
|
const line = receiptLine(input.receipts[index]);
|
|
429
170
|
if (Buffer.byteLength(line, "utf8") + 1 > remaining) {
|
|
430
|
-
// Not even the newest line fits whole: carry as much of it as the reservation
|
|
431
|
-
// leaves rather than reporting an action with no detail at all.
|
|
432
171
|
if (!kept.length && remaining > 1) kept.push(boundReceiptText(line, remaining - 1, ""));
|
|
433
172
|
break;
|
|
434
173
|
}
|