pi-fold 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/extensions/active-context.ts +510 -133
- package/extensions/index.js +7 -2
- package/extensions/lib/brief-text.ts +116 -0
- package/extensions/lib/canonical.ts +3 -18
- package/extensions/lib/curation.ts +78 -39
- package/extensions/lib/editor-ui.ts +468 -47
- package/extensions/lib/folding.ts +20 -2
- package/extensions/lib/instrumentation.ts +3 -1
- package/extensions/lib/live-settings.ts +73 -0
- package/extensions/lib/persistence.ts +37 -3
- package/extensions/lib/policy.ts +55 -12
- package/extensions/lib/selection.ts +9 -44
- package/extensions/lib/tool-surface.ts +0 -1
- package/extensions/lib/transcript.ts +43 -0
- package/extensions/settings.ts +104 -30
- package/package.json +5 -5
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
refsProtected,
|
|
32
32
|
} from "./measurement.ts";
|
|
33
33
|
import {
|
|
34
|
+
assertFoldForestValid,
|
|
34
35
|
childFoldIds,
|
|
35
36
|
clearPrepared,
|
|
36
37
|
flattenFoldRefs,
|
|
@@ -908,7 +909,7 @@ export function projectActiveContext(
|
|
|
908
909
|
snapshot: ActiveContextSnapshot,
|
|
909
910
|
state: ActiveContextState,
|
|
910
911
|
): unknown[] {
|
|
911
|
-
|
|
912
|
+
assertFoldForestValid(state.folds);
|
|
912
913
|
const replacements = new Map<number, { end: number; messages: unknown[] }>();
|
|
913
914
|
for (const root of orderedRoots(state, snapshot)) {
|
|
914
915
|
const rendered = renderFold(root.fold, state, snapshot);
|
|
@@ -921,7 +922,24 @@ export function projectActiveContext(
|
|
|
921
922
|
output.push(...replacement.messages);
|
|
922
923
|
index = replacement.end + 1;
|
|
923
924
|
} else {
|
|
924
|
-
|
|
925
|
+
// A PASS-THROUGH MESSAGE IS SHARED, NOT COPIED (gate 158, 2026-08-29). This used to
|
|
926
|
+
// push `clone(...)`, which was in the first commit of the runtime and never load
|
|
927
|
+
// bearing: `snapshot.messages` is ALREADY `clone(input.eventMessages)`
|
|
928
|
+
// (transcript.ts:531), against an array Pi itself structuredClones before it calls
|
|
929
|
+
// any handler, so the copy was a third generation guarding objects the host never
|
|
930
|
+
// sees. It cost 54.3ms of a 298.5ms projection, 18 percent, on a sealed 3,364
|
|
931
|
+
// message session, and it is the only remaining per-message cost of that size.
|
|
932
|
+
//
|
|
933
|
+
// What makes sharing indistinguishable from copying: NOTHING writes a field into a
|
|
934
|
+
// projected message at any depth. Every rewrite builds a fresh object and replaces
|
|
935
|
+
// an array SLOT, here in `applyToolClips` and in `withdrawConsumedEphemeralPeeks`
|
|
936
|
+
// and `carryWithdrawalIntoFreeze` one layer up, so a shared element is never the
|
|
937
|
+
// thing that gets edited. Gate 158 pins that by deep-freezing `snapshot.messages`
|
|
938
|
+
// and driving the readers against it, so a future in-place write throws in the
|
|
939
|
+
// suite instead of silently corrupting every later projection through the frozen
|
|
940
|
+
// prefix. Folded spans keep their own copies: line 796 and `renderFoldParts` still
|
|
941
|
+
// clone, because those objects are built for the projection rather than read from it.
|
|
942
|
+
output.push(snapshot.messages[index]);
|
|
925
943
|
index += 1;
|
|
926
944
|
}
|
|
927
945
|
}
|
|
@@ -94,10 +94,12 @@ export type ContextEventKind =
|
|
|
94
94
|
| "context.absorb"
|
|
95
95
|
| "context.recovery"
|
|
96
96
|
| "context.suspend"
|
|
97
|
+
| "context.settings"
|
|
97
98
|
| "context.rollback"
|
|
98
99
|
| "context.projection"
|
|
99
100
|
| "context.usage"
|
|
100
|
-
| "context.prefix"
|
|
101
|
+
| "context.prefix"
|
|
102
|
+
| "context.anchor";
|
|
101
103
|
|
|
102
104
|
export function prefixDivergence(
|
|
103
105
|
previous: string | null,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// THE CHANNEL FROM /fold-settings TO THE RUNNING RUNTIME.
|
|
2
|
+
//
|
|
3
|
+
// THE NEED, from a live session (2026-08-29). /fold-settings wrote the file and nothing
|
|
4
|
+
// else: `registerPiFold` resolves thresholds, toolFoldThreshold and the notice settings
|
|
5
|
+
// ONCE at registration, so a session kept the settings pi booted with while the screen showed
|
|
6
|
+
// what the person had just chosen. Shane's terminal held both at once, "Start folding at
|
|
7
|
+
// 40%" on the screen over "commit at 90%" on the status line, and neither surface said
|
|
8
|
+
// why. The file was the durable truth and the running session was not reading it.
|
|
9
|
+
//
|
|
10
|
+
// The two halves are registered independently, `registerFoldSettings(pi)` and
|
|
11
|
+
// `registerPiFold(pi, settings)`, in either order, and neither returns a handle the other
|
|
12
|
+
// can hold. What they do share is the HOST OBJECT, so that is the channel: the runtime
|
|
13
|
+
// hangs an applier off `pi` under a registry symbol, and the settings screen looks for it
|
|
14
|
+
// when a person saves an edit, which is long after both registrations have run. Late
|
|
15
|
+
// binding is what makes the order irrelevant.
|
|
16
|
+
//
|
|
17
|
+
// A MODULE-LEVEL SLOT WAS THE OTHER CANDIDATE AND IT IS WORSE: the applier would then be
|
|
18
|
+
// process-global, every gate that registers a runtime would leave one standing, and a
|
|
19
|
+
// screen in one host could push settings into a runtime in another. Keeping it on the
|
|
20
|
+
// host means an unregistered host simply has no applier, which is a state the screen can
|
|
21
|
+
// read and state honestly rather than a state it has to guess at.
|
|
22
|
+
//
|
|
23
|
+
// Symbol.for, not a fresh Symbol: the global registry keys by string, so a package loaded
|
|
24
|
+
// twice through different module caches (jiti in the suite, pi's own loader in the
|
|
25
|
+
// deployment) still agrees on the property.
|
|
26
|
+
|
|
27
|
+
import type { ActiveContextThresholds } from "./policy.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What the settings screen hands the runtime: exactly the settings FILE's shape, where an
|
|
31
|
+
* absent field means the package default rather than "leave it alone". The runtime
|
|
32
|
+
* resolves it the same way it resolves its registration options, through one code path,
|
|
33
|
+
* so a value cannot mean one thing at boot and another at an edit.
|
|
34
|
+
*/
|
|
35
|
+
export interface LiveFoldSettings {
|
|
36
|
+
thresholds?: ActiveContextThresholds;
|
|
37
|
+
toolFoldThreshold?: number;
|
|
38
|
+
preCommitNotice?: boolean;
|
|
39
|
+
noticeLeadShare?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Refuses by throwing, exactly as registration does, and leaves the runtime untouched. */
|
|
43
|
+
export type LiveSettingsApplier = (settings: LiveFoldSettings, ctx?: unknown) => void;
|
|
44
|
+
|
|
45
|
+
const LIVE_SETTINGS = Symbol.for("pi-fold.live-settings");
|
|
46
|
+
|
|
47
|
+
export function publishLiveSettings(host: any, apply: LiveSettingsApplier): void {
|
|
48
|
+
// A HOST THAT REFUSES THE PROPERTY IS NOT A FAILURE. Nothing about folding depends on
|
|
49
|
+
// this channel: without it the file is still written and the next start still reads
|
|
50
|
+
// it, which is the behaviour every build before this one had.
|
|
51
|
+
try { host[LIVE_SETTINGS] = apply; } catch { }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether an edit saved now would reach a running runtime. The settings screen states
|
|
55
|
+
* which of the two it is instead of promising the one it cannot deliver. */
|
|
56
|
+
export function liveSettingsReachable(host: any): boolean {
|
|
57
|
+
try { return typeof host?.[LIVE_SETTINGS] === "function"; }
|
|
58
|
+
catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Push settings into the runtime registered on this host. Returns false when there is
|
|
63
|
+
* none, and lets the applier's own refusal THROW: the caller decides what a refusal
|
|
64
|
+
* means, and swallowing it here would leave a screen believing it had applied a value
|
|
65
|
+
* the runtime rejected.
|
|
66
|
+
*/
|
|
67
|
+
export function applyLiveSettings(host: any, settings: LiveFoldSettings, ctx?: unknown): boolean {
|
|
68
|
+
let apply: LiveSettingsApplier | undefined;
|
|
69
|
+
try { apply = host?.[LIVE_SETTINGS]; } catch { return false; }
|
|
70
|
+
if (typeof apply !== "function") return false;
|
|
71
|
+
apply(settings, ctx);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
@@ -22,9 +22,6 @@ import {
|
|
|
22
22
|
EXPAND_LEASE_GENERATIONS,
|
|
23
23
|
MAX_EXPAND_LEASES,
|
|
24
24
|
MAX_PENDING_MARKS,
|
|
25
|
-
MEMORY_BODY_CHARS,
|
|
26
|
-
MEMORY_KEY_CHARS,
|
|
27
|
-
MEMORY_KEYS_MAX,
|
|
28
25
|
} from "./policy.ts";
|
|
29
26
|
import type {
|
|
30
27
|
ActiveContextCheckpointV2,
|
|
@@ -314,6 +311,43 @@ export function validateFoldForest(folds: ActiveFold[]): ActiveFold[] {
|
|
|
314
311
|
return values;
|
|
315
312
|
}
|
|
316
313
|
|
|
314
|
+
// A FOREST THAT VALIDATED ONCE IS NOT VALIDATED AGAIN (gate 160, 2026-08-29). Profiling
|
|
315
|
+
// twelve projections of a sealed 681-fold session put 51.2 percent of ALL self time on one
|
|
316
|
+
// call path, and caller attribution found every serialization sample on it:
|
|
317
|
+
//
|
|
318
|
+
// projectActiveContext -> validateFoldForest -> sha256Value -> stableStringify -> safeJson
|
|
319
|
+
//
|
|
320
|
+
// The cost is this function's closing digest-drift loop, which flattens every fold's refs
|
|
321
|
+
// (for a consolidation parent, the union of all descendants) and hashes the array. The
|
|
322
|
+
// projection opens with `validateFoldForest(state.folds)` on EVERY context event and throws
|
|
323
|
+
// the result away: it wants the throw, not the normalized array. So it paid a full forest
|
|
324
|
+
// rehash per event to re-prove something it had already proven.
|
|
325
|
+
//
|
|
326
|
+
// THIS WRAPPER IS THE ONLY THING THAT CHANGES. `validateFoldForest` is untouched, still
|
|
327
|
+
// clones, still normalizes, still returns; the three callers that CONSUME its return value
|
|
328
|
+
// (parseActiveContextState, stateFromFoldRefs, commitPreparedFold) still call it directly
|
|
329
|
+
// and are unaffected. Each of those hands it a freshly built array anyway, so a memo could
|
|
330
|
+
// never have hit there; the projection is the one caller that reads the same array again
|
|
331
|
+
// and again, which is exactly why it is the one that pays.
|
|
332
|
+
//
|
|
333
|
+
// KEYED ON THE INPUT ARRAY, and sound because a state is rebuilt immutably: a changed
|
|
334
|
+
// forest is always a new array, and nothing anywhere mutates a folds array in place (no
|
|
335
|
+
// push, splice, sort, reverse or slot assignment on any `.folds` in extensions/). Nothing
|
|
336
|
+
// is shared out, because nothing is returned, so this carries none of gate 158's aliasing
|
|
337
|
+
// question.
|
|
338
|
+
//
|
|
339
|
+
// THE ONE COST, stated rather than hidden: a folds array mutated IN PLACE keeps its first
|
|
340
|
+
// verdict and a corruption introduced that way would go unreported. Gate 160 asserts that
|
|
341
|
+
// cost directly rather than pretending it away, and still requires a corrupt NEW forest to
|
|
342
|
+
// raise on first sight.
|
|
343
|
+
const VALIDATED_FORESTS = new WeakSet<readonly ActiveFold[]>();
|
|
344
|
+
|
|
345
|
+
export function assertFoldForestValid(folds: ActiveFold[]): void {
|
|
346
|
+
if (VALIDATED_FORESTS.has(folds)) return;
|
|
347
|
+
validateFoldForest(folds);
|
|
348
|
+
VALIDATED_FORESTS.add(folds);
|
|
349
|
+
}
|
|
350
|
+
|
|
317
351
|
export function parseActiveContextState(
|
|
318
352
|
value: unknown,
|
|
319
353
|
sessionId: string,
|
package/extensions/lib/policy.ts
CHANGED
|
@@ -34,14 +34,37 @@ export const ACTIVE_CONTEXT_STATE_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_P
|
|
|
34
34
|
export const ACTIVE_CONTEXT_FOLD_RECORD_ENTRY = `${DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX}-fold-record`;
|
|
35
35
|
export const PROVIDER_CONTEXT_MEASUREMENT_ENTRY = `${DEFAULT_ENTRY_NAMESPACE}-provider-context-measurement`;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* THE NOTICE'S DEFAULT LEAD, as a share of the serving budget below the commit trigger
|
|
38
|
+
* (Shane, 2026-08-22: "around 10% from the commit threshold"; built 2026-08-30).
|
|
38
39
|
*
|
|
39
|
-
* A
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* A DISTANCE, NOT A POINT. Expressed as how far BELOW maxTarget the notice speaks, so it
|
|
41
|
+
* tracks a band that moves: a deployment that drops its trigger to 0.40 gets its notice
|
|
42
|
+
* at 0.30 without touching this, where an absolute 0.70 would have put the warning after
|
|
43
|
+
* the thing it warns about. The fire point is `maxTarget - noticeLeadShare`, floored at
|
|
44
|
+
* zero, so a lead wider than the trigger means "speak from the first measurement".
|
|
45
|
+
*
|
|
46
|
+
* IT REPLACED A COUNT. Until 2026-08-30 the notice fired at UNBRIEFED_FOLDS_BEFORE_NOTICE
|
|
47
|
+
* = 3 unbriefed pending folds, which is a batching size and not an occasion: the frontier
|
|
48
|
+
* cuts continuously and ungated by occupancy, so three folds stand within a few turns of
|
|
49
|
+
* a session's start and the notice spoke a long way from any commit. The share is the
|
|
50
|
+
* occasion, and the edge latch is what batches it: one notice per approach.
|
|
43
51
|
*/
|
|
44
|
-
export const
|
|
52
|
+
export const DEFAULT_NOTICE_LEAD_SHARE = 0.10;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The lead is a share of the serving budget, so it is bounded the way every other share
|
|
56
|
+
* on this surface is: strictly inside 0 and 1. Zero would mean the notice fires exactly
|
|
57
|
+
* at the trigger, which is the same pass the commit fires on and therefore never a
|
|
58
|
+
* warning; 1 and above is not a share.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveNoticeLeadShare(value: unknown): number {
|
|
61
|
+
if (value === undefined) return DEFAULT_NOTICE_LEAD_SHARE;
|
|
62
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value >= 1) {
|
|
63
|
+
throw new Error("noticeLeadShare must be a share above 0 and below 1: how far below the " +
|
|
64
|
+
"commit trigger, as a fraction of the serving budget, the notice speaks");
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
45
68
|
|
|
46
69
|
/**
|
|
47
70
|
* How many spans the frontier cuts on one projection pass.
|
|
@@ -287,15 +310,35 @@ export const MAX_CONTEXT_RECEIPTS = 3;
|
|
|
287
310
|
export const CONTEXT_RECEIPT_BLOCK_BYTES = 900;
|
|
288
311
|
|
|
289
312
|
/**
|
|
290
|
-
* The
|
|
313
|
+
* The pre-commit notice's bound, and its own rather than the receipt block's.
|
|
291
314
|
*
|
|
292
|
-
* It is larger because it carries a variable-length list the receipt does not:
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
315
|
+
* It is larger because it carries a variable-length list the receipt does not: the fixed
|
|
316
|
+
* status and verb block plus one entry per staged fold, and the pending set grows past
|
|
317
|
+
* MAX_FRONTIER_CUTS_PER_PASS whenever the frontier stages faster than a commit lands.
|
|
318
|
+
* 1,200 seated the old instruction and roughly eight folds; past the bound the list gives
|
|
319
|
+
* way and states how many it could not name.
|
|
297
320
|
*/
|
|
298
321
|
export const FOLD_NOTICE_BYTES = 2_400;
|
|
322
|
+
/**
|
|
323
|
+
* WHAT ONE /fold-editor ROW SUMMARIZES, and what one page of its detail serves
|
|
324
|
+
* (2026-08-30).
|
|
325
|
+
*
|
|
326
|
+
* The editor's whole reading of a message was 300 characters of its first text block,
|
|
327
|
+
* with a detail row that sliced to 480 and therefore could never bind. Two numbers replace
|
|
328
|
+
* it because they answer two different questions. EDITOR_ROW_PREVIEW_CHARS is a SUMMARY
|
|
329
|
+
* on a line that also carries a role, an id and a badge, so it is generous enough that a
|
|
330
|
+
* reader can tell two rows apart and no more. EDITOR_DETAIL_PAGE_CHARS is a READ, served
|
|
331
|
+
* a page at a time by an injected accessor with `total` and `hasMore` travelling beside
|
|
332
|
+
* it, so depth is bounded by the reader's patience rather than by a constant: there is no
|
|
333
|
+
* ceiling on what the editor can reach, only on what it hands over at once.
|
|
334
|
+
*
|
|
335
|
+
* THIS IS A READER'S BOUND, NOT A WINDOW'S. Nothing here reaches the projection: paging
|
|
336
|
+
* an entry to its end in the editor moves no bytes and the model never learns it
|
|
337
|
+
* happened. `maxSourceChars` 200,000 is the bound on a read that DOES reach the agent.
|
|
338
|
+
*/
|
|
339
|
+
export const EDITOR_ROW_PREVIEW_CHARS = 240;
|
|
340
|
+
export const EDITOR_DETAIL_PAGE_CHARS = 4_000;
|
|
341
|
+
|
|
299
342
|
export const CONTEXT_STATUS_RESPONSE_BYTES = 24_000;
|
|
300
343
|
export const MAX_UNMARKED_CANDIDATES = 3;
|
|
301
344
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { EvidenceRef } from "../json.ts";
|
|
2
|
+
import { boundedSubject, oneLine, seatSubjects, toolClipHead } from "./brief-text.ts";
|
|
3
|
+
export { oneLine, seatSubjects, toolClipHead };
|
|
2
4
|
import {
|
|
3
5
|
denseOwnArrayValues,
|
|
4
6
|
objectRefKey,
|
|
@@ -397,13 +399,6 @@ function assistantNoteText(message: unknown): string {
|
|
|
397
399
|
// "content continues in the exact source", and a reader never mistakes a cut
|
|
398
400
|
// value for a complete one. oneLine collapses whitespace then delegates;
|
|
399
401
|
// factualValue sanitizes then delegates through oneLine. No other site slices.
|
|
400
|
-
function boundedSubject(text: string, budget: number): string {
|
|
401
|
-
if (text.length <= budget) return text;
|
|
402
|
-
let kept = text.slice(0, Math.max(0, budget - 3)).trimEnd();
|
|
403
|
-
const finalCode = kept.charCodeAt(kept.length - 1);
|
|
404
|
-
if (finalCode >= 0xd800 && finalCode <= 0xdbff) kept = kept.slice(0, -1);
|
|
405
|
-
return `${kept}...`;
|
|
406
|
-
}
|
|
407
402
|
|
|
408
403
|
export function deterministicConsolidationBrief(
|
|
409
404
|
candidate: FoldCandidate,
|
|
@@ -467,29 +462,16 @@ export function deterministicConsolidationBrief(
|
|
|
467
462
|
const noteCount = notes.length;
|
|
468
463
|
const subjects = [...childSubjects, ...notes];
|
|
469
464
|
if (!subjects.length) return "Grouped completed context covering no readable folds.";
|
|
470
|
-
const separator = " | ";
|
|
471
465
|
const lead = noteCount
|
|
472
466
|
? `Grouped completed context covering ${foldCount} folds and ${noteCount} agent notes: `
|
|
473
467
|
: `Grouped completed context covering ${subjects.length} folds: `;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const budget = room - tail.length - separator.length * (named - 1);
|
|
482
|
-
const order = subjects.slice(0, named).map((text, index) => ({ text, index })).sort((a, b) =>
|
|
483
|
-
a.text.length - b.text.length || a.index - b.index);
|
|
484
|
-
const bounded = new Array<string>(named);
|
|
485
|
-
let left = budget;
|
|
486
|
-
for (let taken = 0; taken < order.length; taken += 1) {
|
|
487
|
-
const owed = Math.max(1, Math.floor(left / (order.length - taken)));
|
|
488
|
-
const kept = boundedSubject(order[taken].text, owed);
|
|
489
|
-
bounded[order[taken].index] = kept;
|
|
490
|
-
left -= kept.length;
|
|
491
|
-
}
|
|
492
|
-
return `${lead}${bounded.join(separator)}${tail}.`.replace(/\s+/g, " ").trim();
|
|
468
|
+
// With notes in the division, what the count-slice drops may be a note rather
|
|
469
|
+
// than a fold, so the tail only claims folds when folds are all it holds.
|
|
470
|
+
return seatSubjects(subjects, lead, {
|
|
471
|
+
total: ACTIVE_CONTEXT_POLICY.maxBriefChars,
|
|
472
|
+
minSubjectChars: MIN_SUBJECT_CHARS,
|
|
473
|
+
omittedNoun: noteCount ? "in this group" : "folds in this group",
|
|
474
|
+
});
|
|
493
475
|
}
|
|
494
476
|
|
|
495
477
|
export const ALL_FOLD_KINDS: ReadonlySet<FoldKind> =
|
|
@@ -1106,23 +1088,6 @@ export function manualFoldCandidate(
|
|
|
1106
1088
|
// blank line is kept whole up to the cap; a result that is bulk from line one keeps its
|
|
1107
1089
|
// first cap's worth. The caller states the cut, per gate 136: the marker rides beside
|
|
1108
1090
|
// the count of what it hides, never silently.
|
|
1109
|
-
export function toolClipHead(text: string, cap: number): string {
|
|
1110
|
-
const lines = String(text ?? "").split(/\r?\n/);
|
|
1111
|
-
const kept: string[] = [];
|
|
1112
|
-
for (const line of lines) {
|
|
1113
|
-
if (line.trim()) kept.push(line);
|
|
1114
|
-
else if (kept.length) break;
|
|
1115
|
-
}
|
|
1116
|
-
const paragraph = kept.join("\n");
|
|
1117
|
-
const head = paragraph.length > 0 && paragraph.length <= cap
|
|
1118
|
-
? paragraph
|
|
1119
|
-
: String(text ?? "").slice(0, cap);
|
|
1120
|
-
return head;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
export function oneLine(value: string, maximum: number): string {
|
|
1124
|
-
return boundedSubject(value.replace(/\s+/g, " ").trim(), maximum);
|
|
1125
|
-
}
|
|
1126
1091
|
|
|
1127
1092
|
export function deterministicChapterBrief(
|
|
1128
1093
|
refs: EvidenceRef[],
|
|
@@ -31,6 +31,49 @@ import type {
|
|
|
31
31
|
MappedMessage,
|
|
32
32
|
} from "./policy.ts";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* ONE MESSAGE, AS THE TEXT A PERSON WOULD READ (2026-08-30).
|
|
36
|
+
*
|
|
37
|
+
* WHAT THIS REPLACED. `/fold-editor` built its entry previews inline, and the reading was
|
|
38
|
+
* `content.find((part) => part.type === "text")` sliced to 300 characters. So a message
|
|
39
|
+
* whose first block is not text previewed as the empty string, a tool result carrying its
|
|
40
|
+
* body in a later block previewed as its first block alone, and everything past 300
|
|
41
|
+
* characters was unreachable from the editor at any depth: the detail row sliced to 480,
|
|
42
|
+
* a bound the 300-character preview could never reach, so the deeper number was dead.
|
|
43
|
+
* Shane, 2026-08-30: "the amount of detail we can expand out to is a bit limited".
|
|
44
|
+
*
|
|
45
|
+
* EVERY BLOCK IS ACCOUNTED FOR, and the ones that are not text are NAMED rather than
|
|
46
|
+
* dropped. A window a person is auditing may not show them an empty row where an image or
|
|
47
|
+
* a tool call stands; naming it is how the row stays honest about what it could not
|
|
48
|
+
* render. The result is UNBOUNDED here on purpose. Bounding belongs to the caller, which
|
|
49
|
+
* knows whether it is drawing a one-line row or serving a paged read, and a cut applied
|
|
50
|
+
* here would be a cut nobody could state.
|
|
51
|
+
*/
|
|
52
|
+
export function entryText(message: unknown): string {
|
|
53
|
+
const content = ownValue(message, "content");
|
|
54
|
+
if (typeof content === "string") return content;
|
|
55
|
+
const parts = denseOwnArrayValues(content);
|
|
56
|
+
if (!parts) return "";
|
|
57
|
+
const out: string[] = [];
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
if (typeof part === "string") { out.push(part); continue; }
|
|
60
|
+
const type = ownValue(part, "type");
|
|
61
|
+
const text = ownValue(part, "text");
|
|
62
|
+
if (typeof text === "string" && text) { out.push(text); continue; }
|
|
63
|
+
if (type === "toolCall") {
|
|
64
|
+
const name = ownValue(part, "name");
|
|
65
|
+
// The ARGUMENTS are the content of a tool call, and they are what a person auditing
|
|
66
|
+
// a window needs to see: the call is why the result below it exists.
|
|
67
|
+
const args = ownValue(part, "arguments");
|
|
68
|
+
out.push(`[calls ${typeof name === "string" ? name : "a tool"}` +
|
|
69
|
+
`${args === undefined ? "" : ` ${stableStringify(args)}`}]`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
out.push(`[${typeof type === "string" && type ? type : "content"}]`);
|
|
73
|
+
}
|
|
74
|
+
return out.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
34
77
|
export function terminalAssistant(message: unknown): boolean {
|
|
35
78
|
if (messageRole(message) !== "assistant") return false;
|
|
36
79
|
const stop = ownValue(message, "stopReason");
|