pi-fold 1.0.1 → 1.0.3
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 +35 -42
- package/extensions/active-context.ts +2375 -601
- package/extensions/evidence.js +15 -2
- package/extensions/index.js +69 -21
- package/extensions/json.ts +33 -1
- package/extensions/lib/curation.ts +141 -139
- package/extensions/lib/folding.ts +418 -74
- package/extensions/lib/instrumentation.ts +24 -4
- package/extensions/lib/measurement.ts +92 -16
- package/extensions/lib/persistence.ts +212 -42
- package/extensions/lib/policy.ts +511 -155
- package/extensions/lib/rollback.ts +216 -0
- package/extensions/lib/scheduling.ts +415 -107
- package/extensions/lib/selection.ts +528 -144
- package/extensions/lib/surfacing.ts +480 -225
- package/extensions/lib/tool-surface.ts +25 -5
- package/extensions/lib/transcript.ts +146 -62
- package/extensions/summarizer.js +206 -10
- package/package.json +1 -1
- package/extensions/lib/advisory.ts +0 -309
package/extensions/evidence.js
CHANGED
|
@@ -562,8 +562,21 @@ 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
|
+
function conventionalMcpTool(toolName) {
|
|
576
|
+
return mcpServerForTool(toolName, null) !== null;
|
|
577
|
+
}
|
|
578
|
+
|
|
565
579
|
export function registerEvidenceIngestion(pi, {
|
|
566
|
-
isMcpTool = () => false,
|
|
567
580
|
entryTypePrefix = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
|
|
568
581
|
} = {}) {
|
|
569
582
|
const evidenceNamespace = safeSegment(
|
|
@@ -623,7 +636,7 @@ export function registerEvidenceIngestion(pi, {
|
|
|
623
636
|
return projectEvidenceText(text, descriptor, "utf8-head", truncation ? { truncation } : undefined);
|
|
624
637
|
}
|
|
625
638
|
|
|
626
|
-
if (
|
|
639
|
+
if (conventionalMcpTool(toolName)) {
|
|
627
640
|
const envelope = eventEnvelope(event);
|
|
628
641
|
const serialized = `${stableStringify(envelope)}\n`;
|
|
629
642
|
if (utf8Bytes(serialized) <= TOOL_RESULT_PROJECTION_BYTES) return;
|
package/extensions/index.js
CHANGED
|
@@ -2,29 +2,77 @@ import { registerActiveContext } from "./active-context.ts";
|
|
|
2
2
|
import { registerEvidenceIngestion } from "./evidence.js";
|
|
3
3
|
import { createSummarizeContextSpan } from "./summarizer.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* THE public surface: five options, and every name outside them is refused.
|
|
7
|
+
*
|
|
8
|
+
* Unknown keys used to spread through silently, which made every deletion a no-op rather
|
|
9
|
+
* than an error and handed a deployment the opposite of what it asked for. A caller that
|
|
10
|
+
* passes a name believes it bought behavior, so a name this package does not sell is a
|
|
11
|
+
* throw with the replacement named in it.
|
|
12
|
+
*/
|
|
13
|
+
const PUBLIC_OPTIONS = Object.freeze([
|
|
14
|
+
"thresholds", "summarizer", "providerInputBudget", "blacklistAutoFoldTools", "guidance",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Both spellings of the auto-fold list were ALLOW-lists, and the list runs the other way
|
|
19
|
+
* now. Forwarding one silently would be the worst possible outcome of a rename: a set of
|
|
20
|
+
* tools a deployment named to make foldable would become the exact set it barred.
|
|
21
|
+
*/
|
|
22
|
+
const INVERTED_AUTO_FOLD_LIST = "renamed blacklistAutoFoldTools, and the sense is INVERTED: " +
|
|
23
|
+
"every completed tool batch folds unmarked, and the list names the exceptions whose " +
|
|
24
|
+
"results must stay raw. An allow-list moved across verbatim would bar exactly the tools " +
|
|
25
|
+
"it meant to permit";
|
|
26
|
+
|
|
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
|
+
const REFUSED_OPTIONS = Object.freeze({
|
|
34
|
+
toolName: "the deployment identity is hardwired to pi-fold: the tool is pi_fold_context",
|
|
35
|
+
toolLabel: "the deployment identity is hardwired to pi-fold",
|
|
36
|
+
brandNoun: "the deployment identity is hardwired to pi-fold",
|
|
37
|
+
entryTypePrefix: "the deployment identity is hardwired to pi-fold: durable state lives " +
|
|
38
|
+
"under pi-fold-active-context, and moving it would strand every fold already written",
|
|
39
|
+
commandPrefix: "the deployment identity is hardwired to pi-fold: the commands are " +
|
|
40
|
+
"/pi-fold-context and /fold-context",
|
|
41
|
+
commandNames: "the deployment identity is hardwired to pi-fold: the commands are " +
|
|
42
|
+
"/pi-fold-context and /fold-context",
|
|
43
|
+
isMcpTool: "MCP tools are recognized by the mcp__server__tool naming convention. The " +
|
|
44
|
+
"predicate defaulted to false, so conventionally named MCP tools were never classified " +
|
|
45
|
+
"unless a host supplied one, which is a documented feature that never ran",
|
|
46
|
+
readOnlyTools: INVERTED_AUTO_FOLD_LIST,
|
|
47
|
+
autoFoldableTools: INVERTED_AUTO_FOLD_LIST,
|
|
48
|
+
evidenceIngestion: "evidence ingestion is always on: the hook always registers, oversized " +
|
|
49
|
+
"tool results are pinned to read-only 0444 artifacts under the session directory, and " +
|
|
50
|
+
"the 512 MB session cap is a constant. Exact recovery is what the folds promise, and a " +
|
|
51
|
+
"switch that removed the anchors removed the promise",
|
|
52
|
+
providerTotalWindow: "renamed providerInputBudget, and it is ALREADY NET: pass the tokens " +
|
|
53
|
+
"the deployment may actually fill, not the total window the runtime then subtracts a " +
|
|
54
|
+
"guessed output reservation from",
|
|
55
|
+
summarizeContextSpan: "choose a brief generator with summarizer (\"session\" or " +
|
|
56
|
+
"{ provider, model, effort })",
|
|
57
|
+
});
|
|
58
|
+
|
|
5
59
|
export function registerPiFold(pi, options = {}, loadHostModule) {
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
throw new Error("summarizer and summarizeContextSpan cannot be configured together");
|
|
15
|
-
}
|
|
16
|
-
const configuredSummarizer = summarizeContextSpan ??
|
|
17
|
-
createSummarizeContextSpan(summarizer, loadHostModule);
|
|
18
|
-
if (evidenceIngestion) {
|
|
19
|
-
registerEvidenceIngestion(pi, {
|
|
20
|
-
isMcpTool,
|
|
21
|
-
entryTypePrefix: activeContextOptions.entryTypePrefix,
|
|
22
|
-
});
|
|
60
|
+
for (const name of Object.keys(options)) {
|
|
61
|
+
if (Object.hasOwn(REFUSED_OPTIONS, name)) {
|
|
62
|
+
throw new Error(`${name} is no longer an option: ${REFUSED_OPTIONS[name]}`);
|
|
63
|
+
}
|
|
64
|
+
if (!PUBLIC_OPTIONS.includes(name)) {
|
|
65
|
+
throw new Error(`${name} is not a pi-fold option: the surface is ` +
|
|
66
|
+
`${PUBLIC_OPTIONS.join(", ")}`);
|
|
67
|
+
}
|
|
23
68
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
registerEvidenceIngestion(pi);
|
|
75
|
+
return registerActiveContext(pi, { ...activeContextOptions, summarizeContextSpan });
|
|
28
76
|
}
|
|
29
77
|
|
|
30
78
|
export default function piFold(pi, options) {
|
package/extensions/json.ts
CHANGED
|
@@ -216,8 +216,40 @@ export function evidenceValue(message: unknown): unknown {
|
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
|
|
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
|
+
const EVIDENCE_DIGESTS = new WeakMap<object, string>();
|
|
245
|
+
|
|
219
246
|
export function evidenceSha256(message: unknown): string {
|
|
220
|
-
return sha256Value(evidenceValue(message));
|
|
247
|
+
if (!message || typeof message !== "object") return sha256Value(evidenceValue(message));
|
|
248
|
+
const cached = EVIDENCE_DIGESTS.get(message);
|
|
249
|
+
if (cached !== undefined) return cached;
|
|
250
|
+
const digest = sha256Value(evidenceValue(message));
|
|
251
|
+
EVIDENCE_DIGESTS.set(message, digest);
|
|
252
|
+
return digest;
|
|
221
253
|
}
|
|
222
254
|
|
|
223
255
|
export function evidenceRef(sessionId: string, entryId: string, message: unknown): EvidenceRef {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { objectRefKey } from "../json.ts";
|
|
2
2
|
import {
|
|
3
|
+
boundedUtf8,
|
|
3
4
|
bytes,
|
|
4
5
|
messageRole,
|
|
5
6
|
} from "./canonical.ts";
|
|
@@ -8,12 +9,11 @@ import {
|
|
|
8
9
|
contextBrand,
|
|
9
10
|
CONTEXT_MARK_RESPONSE_BYTES,
|
|
10
11
|
CONTEXT_RECEIPT_BLOCK_BYTES,
|
|
11
|
-
CURATION_GATE_MAX_ROUNDS,
|
|
12
|
-
CURATION_OCCUPANCY_SHARE,
|
|
13
|
-
CURATION_REMINDER_SHARES,
|
|
14
|
-
CURATION_STALE_TOOL_SHARE,
|
|
15
12
|
DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
16
13
|
MAX_CONTEXT_RECEIPTS,
|
|
14
|
+
MAX_LAST_CALL_TEXT_BYTES,
|
|
15
|
+
MAX_SURFACING_LINE_BYTES,
|
|
16
|
+
MAX_THRESHOLD_NOTICE_TEXT_BYTES,
|
|
17
17
|
} from "./policy.ts";
|
|
18
18
|
import type {
|
|
19
19
|
ActiveContextSnapshot,
|
|
@@ -67,6 +67,8 @@ export function staleToolMass(
|
|
|
67
67
|
export interface CurationSignals {
|
|
68
68
|
/** Measured provider tokens as a share of the truthful serving budget; null unmeasured. */
|
|
69
69
|
occupancy: number | null;
|
|
70
|
+
/** The trigger line this occupancy is read against: thresholds.maxTarget. */
|
|
71
|
+
maxTarget: number;
|
|
70
72
|
occupancyTokens: number | null;
|
|
71
73
|
/** THE serving budget: one resolved value, the same one the fence and estimator use. */
|
|
72
74
|
budgetTokens: number;
|
|
@@ -103,6 +105,7 @@ export function curationSignals(input: {
|
|
|
103
105
|
: 4;
|
|
104
106
|
const staleToolTokens = Math.ceil(mass.bytes / charsPerToken);
|
|
105
107
|
return {
|
|
108
|
+
maxTarget: input.snapshot.thresholds.maxTarget,
|
|
106
109
|
occupancy: input.usedTokens === null || input.budgetTokens <= 0
|
|
107
110
|
? null
|
|
108
111
|
: input.usedTokens / input.budgetTokens,
|
|
@@ -117,50 +120,20 @@ export function curationSignals(input: {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
123
|
+
* ONE signal. Occupancy of the serving budget reaches maxTarget, and a commit is due.
|
|
124
|
+
*
|
|
125
|
+
* The second signal is gone with the announcement it guarded. It existed because the
|
|
126
|
+
* trigger ANNOUNCED a commit before running one, and announcing a commit that has
|
|
127
|
+
* nothing stale to fold is a false statement in the window; the announcement was
|
|
128
|
+
* deleted when its cache cost was measured, and the AND-condition outlived its reason.
|
|
129
|
+
* A commit with nothing eligible now simply applies nothing and says so, which the
|
|
130
|
+
* reclaim floor and the eligibility accounting already report. Stale mass is still
|
|
131
|
+
* measured and still reported; it is no longer a condition on the trigger.
|
|
122
132
|
*/
|
|
123
133
|
export function curationTriggerFires(signals: CurationSignals): boolean {
|
|
124
134
|
return signals.occupancy !== null &&
|
|
125
135
|
Number.isFinite(signals.occupancy) &&
|
|
126
|
-
signals.occupancy >=
|
|
127
|
-
signals.staleToolShare >= CURATION_STALE_TOOL_SHARE;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The sparse reminders.
|
|
132
|
-
*
|
|
133
|
-
* Two per window cycle, one line each, tail-appended, and informatory. Below the
|
|
134
|
-
* curation threshold NOTHING folds automatically, so the only useful thing to say
|
|
135
|
-
* before the fold event is: mark as you go, and the folds will be neat when it fires.
|
|
136
|
-
* `fired` is the count already spent this cycle; a commit resets it to zero.
|
|
137
|
-
*/
|
|
138
|
-
export function dueReminderIndex(
|
|
139
|
-
occupancy: number | null,
|
|
140
|
-
fired: number,
|
|
141
|
-
shares: readonly number[] = CURATION_REMINDER_SHARES,
|
|
142
|
-
): number | null {
|
|
143
|
-
if (occupancy === null || !Number.isFinite(occupancy)) return null;
|
|
144
|
-
for (let index = Math.max(0, Math.trunc(fired)); index < shares.length; index += 1) {
|
|
145
|
-
if (occupancy >= shares[index]) return index;
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function curationReminderText(input: {
|
|
151
|
-
signals: CurationSignals;
|
|
152
|
-
toolName: string;
|
|
153
|
-
brandNoun?: string;
|
|
154
|
-
}): string {
|
|
155
|
-
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
156
|
-
const occupancy = input.signals.occupancy === null
|
|
157
|
-
? "unmeasured"
|
|
158
|
-
: `${Math.round(input.signals.occupancy * 100)}%`;
|
|
159
|
-
return `[${brand} curation] Occupancy ${occupancy} of the ${input.signals.budgetTokens}-token serving ` +
|
|
160
|
-
`budget; nothing folds until ${Math.round(CURATION_OCCUPANCY_SHARE * 100)}%. Mark SEVERAL finished ` +
|
|
161
|
-
`chapters in one call with ${input.toolName} {"action":"fold","marks":[{"ids":["<start>","<end>"],` +
|
|
162
|
-
"\"brief\":\"<factual brief>\"}]}: one call answers with everything held plus what is still " +
|
|
163
|
-
"unmarked, so the folds are neat when the fold event triggers.";
|
|
136
|
+
signals.occupancy >= signals.maxTarget;
|
|
164
137
|
}
|
|
165
138
|
|
|
166
139
|
/**
|
|
@@ -195,118 +168,141 @@ export function markAwarenessText(input: {
|
|
|
195
168
|
].join("\n"), CONTEXT_MARK_RESPONSE_BYTES, `[${brand} marks] Held; the remainder is unavailable this pass.`);
|
|
196
169
|
}
|
|
197
170
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
171
|
+
/**
|
|
172
|
+
* The rider: the one post-commit carrier, composed at a fold commit and delivered
|
|
173
|
+
* INSIDE the rewrite that commit already paid for, beside the receipt. Its wording
|
|
174
|
+
* follows the curation rule (report what is happening, name the actions), its numbers
|
|
175
|
+
* are decision inputs only: pending agent marks with what they free, up to three
|
|
176
|
+
* completed-unit anchors, and the pinned share against its cap. Raw occupancy,
|
|
177
|
+
* distance-to-commit and unmarked-share percentages stay OUT: they are pressure
|
|
178
|
+
* readouts, not decisions, and the pre-commit last call is where they are both
|
|
179
|
+
* accurate and free.
|
|
180
|
+
*/
|
|
181
|
+
export function contextRiderText(input: {
|
|
182
|
+
toolName: string;
|
|
183
|
+
brandNoun?: string;
|
|
184
|
+
pendingAgentMarks: number;
|
|
185
|
+
eligibleMarks: number;
|
|
186
|
+
freedTokens: number;
|
|
187
|
+
eligibleFreedTokens: number;
|
|
188
|
+
/** Up to three completed-unit entry ids the agent could mark next. */
|
|
189
|
+
anchors: string[];
|
|
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);
|
|
208
223
|
}
|
|
209
224
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
proceededBy: CurationProceedReason | null;
|
|
215
|
-
roundsUsed: number;
|
|
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)}`;
|
|
216
229
|
}
|
|
217
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
|
+
|
|
218
235
|
/**
|
|
219
|
-
*
|
|
236
|
+
* The pre-commit last-call.
|
|
220
237
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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.
|
|
226
244
|
*/
|
|
227
|
-
export function
|
|
228
|
-
gate: CurationGate | null;
|
|
229
|
-
ordinal: number;
|
|
245
|
+
export function lastCallText(input: {
|
|
230
246
|
signals: CurationSignals;
|
|
231
|
-
|
|
232
|
-
contextCalls: number;
|
|
247
|
+
unmarked: { spans: number; tokens: number };
|
|
233
248
|
pendingMarks: number;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
proceed: true,
|
|
267
|
-
proceededBy: engaged ? "round-cap" : "non-context-response",
|
|
268
|
-
roundsUsed,
|
|
269
|
-
};
|
|
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);
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* work as the stated default. A message shaped like a question inviting a chat-style
|
|
277
|
-
* reply can make a single-turn agent emit a final answer and end its own run, so this
|
|
278
|
-
* never asks anything and never implies a reply is expected.
|
|
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.
|
|
279
287
|
*/
|
|
280
|
-
export function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
export function thresholdNoticeText(input: {
|
|
289
|
+
share: number;
|
|
290
|
+
occupancyTokens: number;
|
|
291
|
+
budgetTokens: number;
|
|
292
|
+
maxTarget: number;
|
|
284
293
|
toolName: string;
|
|
285
294
|
brandNoun?: string;
|
|
286
295
|
}): string {
|
|
287
|
-
const maxRounds = input.maxRounds ?? CURATION_GATE_MAX_ROUNDS;
|
|
288
296
|
const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"Marking well is what makes this cheap: your briefs are what makes these spans findable later, and " +
|
|
299
|
-
"spans batched into one commit rewrite the prefix once instead of once per fold, so the cache survives " +
|
|
300
|
-
"and the next fold event arrives later.",
|
|
301
|
-
`Available now: ${input.toolName} ` +
|
|
302
|
-
"{\"action\":\"fold\",\"marks\":[{\"ids\":[\"<start>\",\"<end>\"],\"brief\":\"<factual brief>\"}]} " +
|
|
303
|
-
"marks SEVERAL spans in one call, and answers with everything held plus what is still unmarked; " +
|
|
304
|
-
"{\"action\":\"rebrief\",\"id\":\"<fold-id>\",\"brief\":\"<factual brief>\"} corrects an existing " +
|
|
305
|
-
"brief; {\"action\":\"reboundary\",\"id\":\"<fold-id>\"} returns a mis-cut fold to raw so you can " +
|
|
306
|
-
"re-fold it.",
|
|
307
|
-
"Continuing the task is the default: this commit proceeds on the next pass unless the next thing you do " +
|
|
308
|
-
`is a ${input.toolName} call, and it proceeds regardless after ${remaining} more curation round(s).`,
|
|
309
|
-
].join("\n"), 2_048, `[${brand} curation] A commit epoch is due; details are unavailable this pass.`);
|
|
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
|
+
);
|
|
310
306
|
}
|
|
311
307
|
|
|
312
308
|
/** One automatic context action, reported back as status rather than as advice. */
|
|
@@ -330,6 +326,8 @@ export interface ContextReceipt {
|
|
|
330
326
|
absorbedWedges: number;
|
|
331
327
|
/** Overflow recovery, when this action rebuilt a request the provider would reject. */
|
|
332
328
|
recovered: boolean;
|
|
329
|
+
/** Mass the agent's pins held out of this commit's reach, in bytes. */
|
|
330
|
+
protectedBytes: number;
|
|
333
331
|
note: string | null;
|
|
334
332
|
}
|
|
335
333
|
|
|
@@ -349,6 +347,7 @@ export function contextReceipt(input: Partial<ContextReceipt> & { kind: string;
|
|
|
349
347
|
splitFromChars: input.splitFromChars ?? 0,
|
|
350
348
|
absorbedWedges: input.absorbedWedges ?? 0,
|
|
351
349
|
recovered: input.recovered ?? false,
|
|
350
|
+
protectedBytes: input.protectedBytes ?? 0,
|
|
352
351
|
note: input.note ?? null,
|
|
353
352
|
};
|
|
354
353
|
}
|
|
@@ -387,6 +386,9 @@ export function receiptLine(receipt: ContextReceipt): string {
|
|
|
387
386
|
receipt.recovered
|
|
388
387
|
? "This ran as overflow recovery: the request would not have been transmissible, and it was rebuilt rather than dropped."
|
|
389
388
|
: "",
|
|
389
|
+
receipt.protectedBytes
|
|
390
|
+
? `${receipt.protectedBytes} byte(s) stay pinned under protect; unprotect releases them when the work moves on.`
|
|
391
|
+
: "",
|
|
390
392
|
receipt.note ?? "",
|
|
391
393
|
];
|
|
392
394
|
return parts.filter(Boolean).join(" ");
|