pi-fold 0.0.1 → 0.1.1

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.
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { types as utilTypes } from "node:util";
3
+
4
+ export interface EvidenceRef {
5
+ sessionId: string;
6
+ entryId: string;
7
+ role: string;
8
+ sha256: string;
9
+ }
10
+
11
+ // Capture trust-boundary intrinsics before mutable prototypes can influence validation.
12
+ const ArrayConstructor = Array;
13
+ const arrayIsArray = Array.isArray;
14
+ const arrayPrototype = Array.prototype;
15
+ const defineProperty = Object.defineProperty;
16
+ const freeze = Object.freeze;
17
+ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
18
+ const getPrototypeOf = Object.getPrototypeOf;
19
+ const jsonStringify = JSON.stringify;
20
+ const number = Number;
21
+ const numberIsFinite = Number.isFinite;
22
+ const numberIsSafeInteger = Number.isSafeInteger;
23
+ const objectIs = Object.is;
24
+ const objectPrototype = Object.prototype;
25
+ const ownKeys = Reflect.ownKeys;
26
+ const regexpTest = RegExp.prototype.test;
27
+ const reflectApply = Reflect.apply;
28
+ const string = String;
29
+ const isProxy = utilTypes.isProxy;
30
+ const sha256Pattern = /^[a-f0-9]{64}$/;
31
+
32
+ function appendArrayValue(values: unknown[], value: unknown): void {
33
+ defineProperty(values, string(values.length), {
34
+ value,
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ });
39
+ }
40
+
41
+ function safeJson(value: unknown, ancestors: unknown[]): string | undefined {
42
+ if (value === null) return "null";
43
+ const kind = typeof value;
44
+ if (kind === "string" || kind === "boolean") return jsonStringify(value);
45
+ if (kind === "number") return numberIsFinite(value) ? jsonStringify(value) : "null";
46
+ if (kind === "undefined" || kind === "function" || kind === "symbol") return undefined;
47
+ if (kind === "bigint") throw new Error("Value is not JSON-serializable");
48
+ if (!value || kind !== "object" || isProxy(value)) throw new Error("Value is not JSON-serializable");
49
+ for (let index = 0; index < ancestors.length; index += 1) {
50
+ if (ancestors[index] === value) throw new Error("Value contains a cycle");
51
+ }
52
+ const depth = ancestors.length;
53
+ appendArrayValue(ancestors, value);
54
+ try {
55
+ if (arrayIsArray(value)) {
56
+ if (getPrototypeOf(value) !== arrayPrototype) throw new Error("Value is not a plain JSON array");
57
+ const lengthDescriptor = getOwnPropertyDescriptor(value, "length");
58
+ if (!lengthDescriptor || !("value" in lengthDescriptor) ||
59
+ !numberIsSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) {
60
+ throw new Error("Value is not a dense JSON array");
61
+ }
62
+ const length = number(lengthDescriptor.value);
63
+ let output = "[";
64
+ for (let index = 0; index < length; index += 1) {
65
+ if (index) output += ",";
66
+ const descriptor = getOwnPropertyDescriptor(value, string(index));
67
+ if (!descriptor) {
68
+ output += "null";
69
+ continue;
70
+ }
71
+ if (!("value" in descriptor) || !descriptor.enumerable) throw new Error("Value contains an array accessor");
72
+ output += safeJson(descriptor.value, ancestors) ?? "null";
73
+ }
74
+ return `${output}]`;
75
+ }
76
+ if (!isPlainRecord(value)) throw new Error("Value is not a plain JSON object");
77
+ const keys = ownKeys(value);
78
+ let output = "{";
79
+ let emitted = 0;
80
+ for (let index = 0; index < keys.length; index += 1) {
81
+ const key = keys[index];
82
+ if (typeof key !== "string") throw new Error("Value contains a symbol property");
83
+ const descriptor = getOwnPropertyDescriptor(value, key);
84
+ if (!descriptor || !("value" in descriptor)) throw new Error("Value contains an object accessor");
85
+ if (!descriptor.enumerable) continue;
86
+ const serialized = safeJson(descriptor.value, ancestors);
87
+ if (serialized === undefined) continue;
88
+ if (emitted) output += ",";
89
+ output += `${jsonStringify(key)}:${serialized}`;
90
+ emitted += 1;
91
+ }
92
+ return `${output}}`;
93
+ } finally {
94
+ defineProperty(ancestors, "length", { value: depth, writable: true });
95
+ }
96
+ }
97
+
98
+ /** Preserve JSON property order without invoking getters, toJSON, coercion, iterators, or mutable prototypes. */
99
+ export function stableStringify(value: unknown): string {
100
+ const serialized = safeJson(value, new ArrayConstructor<unknown>());
101
+ if (typeof serialized !== "string") throw new Error("Value is not JSON-serializable");
102
+ return serialized;
103
+ }
104
+
105
+ export function sha256Text(value: string): string {
106
+ return createHash("sha256").update(value, "utf8").digest("hex");
107
+ }
108
+
109
+ export function sha256Value(value: unknown): string {
110
+ return sha256Text(stableStringify(value));
111
+ }
112
+
113
+ export function isPlainRecord(value: unknown): value is Record<string, unknown> {
114
+ if (!value || typeof value !== "object" || arrayIsArray(value) || isProxy(value)) return false;
115
+ const prototype = getPrototypeOf(value);
116
+ return prototype === objectPrototype || prototype === null;
117
+ }
118
+
119
+ export function ownDataProperty(value: unknown, key: PropertyKey): unknown {
120
+ if (!isPlainRecord(value)) return undefined;
121
+ const descriptor = getOwnPropertyDescriptor(value, key);
122
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
123
+ }
124
+
125
+ /** Copy a dense own-data array without invoking inherited accessors, iterators, or methods. */
126
+ export function denseOwnArrayValues(value: unknown): unknown[] | null {
127
+ if (!arrayIsArray(value) || isProxy(value) || getPrototypeOf(value) !== arrayPrototype) return null;
128
+ const lengthDescriptor = getOwnPropertyDescriptor(value, "length");
129
+ if (!lengthDescriptor || !("value" in lengthDescriptor) ||
130
+ !numberIsSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) return null;
131
+ const length = number(lengthDescriptor.value);
132
+ const values = new ArrayConstructor<unknown>(length);
133
+ for (let index = 0; index < length; index += 1) {
134
+ const descriptor = getOwnPropertyDescriptor(value, string(index));
135
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return null;
136
+ defineProperty(values, string(index), {
137
+ value: descriptor.value,
138
+ enumerable: true,
139
+ configurable: false,
140
+ writable: false,
141
+ });
142
+ }
143
+ const keys = ownKeys(value);
144
+ let indexes = 0;
145
+ for (let index = 0; index < keys.length; index += 1) {
146
+ const key = keys[index];
147
+ if (key === "length") continue;
148
+ if (key === "toJSON") {
149
+ const descriptor = getOwnPropertyDescriptor(value, key);
150
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== undefined || descriptor.enumerable ||
151
+ descriptor.configurable || descriptor.writable) return null;
152
+ continue;
153
+ }
154
+ if (typeof key !== "string") return null;
155
+ const numeric = number(key);
156
+ if (!numberIsSafeInteger(numeric) || numeric < 0 || numeric >= length || string(numeric) !== key) return null;
157
+ indexes += 1;
158
+ }
159
+ if (indexes !== length) return null;
160
+
161
+ const copiedLength = getOwnPropertyDescriptor(values, "length");
162
+ if (!copiedLength || !("value" in copiedLength) || copiedLength.value !== length) return null;
163
+ for (let index = 0; index < length; index += 1) {
164
+ const source = getOwnPropertyDescriptor(value, string(index));
165
+ const copied = getOwnPropertyDescriptor(values, string(index));
166
+ if (!source || !("value" in source) || !copied || !("value" in copied) ||
167
+ !objectIs(copied.value, source.value) || !copied.enumerable || copied.configurable || copied.writable) return null;
168
+ }
169
+ freeze(values);
170
+ return values;
171
+ }
172
+
173
+ export function isObjectRef(value: unknown): value is EvidenceRef {
174
+ if (!isPlainRecord(value)) return false;
175
+ const keys = ownKeys(value);
176
+ if (keys.length !== 4) return false;
177
+ let mask = 0;
178
+ for (let index = 0; index < keys.length; index += 1) {
179
+ const key = keys[index];
180
+ if (typeof key !== "string") return false;
181
+ if (key === "sessionId") mask |= 1;
182
+ else if (key === "entryId") mask |= 2;
183
+ else if (key === "role") mask |= 4;
184
+ else if (key === "sha256") mask |= 8;
185
+ else return false;
186
+ const descriptor = getOwnPropertyDescriptor(value, key);
187
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return false;
188
+ }
189
+ if (mask !== 15) return false;
190
+ const sessionId = ownDataProperty(value, "sessionId");
191
+ const entryId = ownDataProperty(value, "entryId");
192
+ const role = ownDataProperty(value, "role");
193
+ const sha256 = ownDataProperty(value, "sha256");
194
+ return typeof sessionId === "string" && sessionId.length > 0 &&
195
+ typeof entryId === "string" && entryId.length > 0 &&
196
+ typeof role === "string" && role.length > 0 &&
197
+ typeof sha256 === "string" && reflectApply(regexpTest, sha256Pattern, [sha256]);
198
+ }
199
+
200
+ export function objectRefKey(ref: EvidenceRef): string {
201
+ return stableStringify({ sessionId: ref.sessionId, entryId: ref.entryId, role: ref.role });
202
+ }
203
+
204
+ export function sameObjectIdentity(left: EvidenceRef, right: EvidenceRef): boolean {
205
+ return objectRefKey(left) === objectRefKey(right);
206
+ }
207
+
208
+ export function evidenceValue(message: unknown): unknown {
209
+ if (!isPlainRecord(message) || ownDataProperty(message, "role") !== "custom") return message;
210
+ return {
211
+ role: "custom",
212
+ customType: ownDataProperty(message, "customType"),
213
+ content: ownDataProperty(message, "content"),
214
+ display: ownDataProperty(message, "display"),
215
+ details: ownDataProperty(message, "details"),
216
+ };
217
+ }
218
+
219
+ export function evidenceSha256(message: unknown): string {
220
+ return sha256Value(evidenceValue(message));
221
+ }
222
+
223
+ export function evidenceRef(sessionId: string, entryId: string, message: unknown): EvidenceRef {
224
+ if (!sessionId || !entryId || !isPlainRecord(message)) throw new Error("Pi evidence lacks a stable session, entry, or message");
225
+ const role = ownDataProperty(message, "role");
226
+ if (typeof role !== "string" || !role) throw new Error(`Pi entry ${entryId} has no role discriminator`);
227
+ return { sessionId, entryId, role, sha256: evidenceSha256(message) };
228
+ }
@@ -0,0 +1,238 @@
1
+ import { sha256Value } from "../json.ts";
2
+ import { clone } from "./canonical.ts";
3
+ import {
4
+ boundReceiptText,
5
+ hardFenceRatio,
6
+ } from "./measurement.ts";
7
+ import { ADVISORY_BUDGETS, validAdvisoryState } from "./persistence.ts";
8
+ import type {
9
+ ActiveContextSnapshot,
10
+ ActiveContextState,
11
+ AdvisoryMilestone,
12
+ GuidanceProfile,
13
+ SurfacingSuggestion,
14
+ } from "./policy.ts";
15
+ import {
16
+ contextBrand,
17
+ DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
18
+ DEFAULT_GUIDANCE_PROFILE,
19
+ GUIDANCE_PROFILES,
20
+ } from "./policy.ts";
21
+
22
+ export { ADVISORY_BUDGETS };
23
+
24
+ export interface AdvisorySchedule {
25
+ key: string;
26
+ rungs: Array<{ milestone: AdvisoryMilestone; threshold: number; budget: number }>;
27
+ }
28
+
29
+ export function advisoryState(state: ActiveContextState): NonNullable<ActiveContextState["advisory"]> {
30
+ if (state.advisory === undefined) return { highWater: 0, delivered: {} };
31
+ if (!validAdvisoryState(state.advisory)) {
32
+ throw new Error("Corrupt in-memory advisory state (no silent fallback)");
33
+ }
34
+ return clone(state.advisory);
35
+ }
36
+
37
+ export function clearArmedAdvisory(state: ActiveContextState): ActiveContextState {
38
+ const current = advisoryState(state);
39
+ if (!current.armed) return state;
40
+ const advisory = { ...current };
41
+ delete advisory.armed;
42
+ return { ...state, advisory };
43
+ }
44
+
45
+ export function normalizeGuidanceProfile(guidance: unknown = DEFAULT_GUIDANCE_PROFILE): GuidanceProfile {
46
+ if (!GUIDANCE_PROFILES.includes(guidance as GuidanceProfile)) {
47
+ throw new Error('guidance must be "pressure", "curation", or "minimal"');
48
+ }
49
+ return guidance as GuidanceProfile;
50
+ }
51
+
52
+ function guidanceRungs(
53
+ snapshot: Pick<ActiveContextSnapshot, "policy" | "contextWindow">,
54
+ guidance: GuidanceProfile,
55
+ ): AdvisorySchedule["rungs"] {
56
+ const urgent = {
57
+ milestone: "urgent" as const,
58
+ threshold: hardFenceRatio(snapshot) - 0.03,
59
+ budget: ADVISORY_BUDGETS.urgent,
60
+ };
61
+ if (guidance === "minimal") return [urgent];
62
+ return [
63
+ ...(guidance === "curation"
64
+ ? [{ milestone: "orientation" as const, threshold: 0.25, budget: ADVISORY_BUDGETS.orientation }]
65
+ : []),
66
+ { milestone: "notice" as const, threshold: 0.50, budget: ADVISORY_BUDGETS.notice },
67
+ { milestone: "tools" as const, threshold: snapshot.policy.toolFoldRatio - 0.04,
68
+ budget: ADVISORY_BUDGETS.tools },
69
+ { milestone: "chapters" as const, threshold: snapshot.policy.prepareRatio - 0.05,
70
+ budget: ADVISORY_BUDGETS.chapters },
71
+ urgent,
72
+ ];
73
+ }
74
+
75
+ export function advisorySchedule(
76
+ snapshot: Pick<ActiveContextSnapshot, "policy" | "contextWindow">,
77
+ guidance: GuidanceProfile = DEFAULT_GUIDANCE_PROFILE,
78
+ ): AdvisorySchedule {
79
+ const rungs = guidanceRungs(snapshot, guidance);
80
+ for (let index = rungs.length - 2; index >= 0; index -= 1) {
81
+ rungs[index].threshold = Math.min(rungs[index].threshold, rungs[index + 1].threshold - 0.02);
82
+ }
83
+ for (const rung of rungs) rung.threshold = Math.max(0, Math.min(1, rung.threshold));
84
+ return {
85
+ key: sha256Value(rungs.map(({ milestone, threshold }) => ({ milestone, threshold }))),
86
+ rungs,
87
+ };
88
+ }
89
+
90
+ export function updateAdvisoryMilestone(
91
+ currentState: ActiveContextState,
92
+ ratio: number,
93
+ schedule: AdvisorySchedule,
94
+ scheduleChanged: boolean,
95
+ scheduleKey: string,
96
+ ): { state: ActiveContextState; milestone: AdvisoryMilestone | null } {
97
+ const current = advisoryState(currentState);
98
+ if (scheduleChanged) {
99
+ return {
100
+ state: { ...currentState, advisory: { ...current, highWater: Math.min(1, ratio) } },
101
+ milestone: null,
102
+ };
103
+ }
104
+ let highWater = current.highWater;
105
+ for (let index = schedule.rungs.length - 1; index >= 0; index -= 1) {
106
+ const rung = schedule.rungs[index];
107
+ if ((current.delivered[rung.milestone] ?? 0) > 0 && ratio < 0.85 * rung.threshold) {
108
+ highWater = Math.min(highWater, index > 0 ? schedule.rungs[index - 1].threshold : 0);
109
+ }
110
+ }
111
+ const crossed = schedule.rungs.filter((rung) =>
112
+ highWater < rung.threshold && ratio >= rung.threshold &&
113
+ (current.delivered[rung.milestone] ?? 0) < rung.budget);
114
+ const selected = crossed.at(-1) ?? null;
115
+ const delivered = { ...current.delivered };
116
+ if (selected) delivered[selected.milestone] = (delivered[selected.milestone] ?? 0) + 1;
117
+ const armed = selected
118
+ ? { milestone: selected.milestone, threshold: selected.threshold, scheduleKey }
119
+ : current.armed;
120
+ return {
121
+ state: {
122
+ ...currentState,
123
+ advisory: {
124
+ highWater: Math.min(1, Math.max(highWater, ratio)),
125
+ delivered,
126
+ ...(armed ? { armed } : {}),
127
+ },
128
+ },
129
+ milestone: selected?.milestone ?? null,
130
+ };
131
+ }
132
+
133
+ function curationText(milestone: AdvisoryMilestone, percent: number, toolName: string): string {
134
+ if (milestone === "orientation") {
135
+ return "Collapsed folds are a browsable index of the work behind you, and their briefs sit in the cached " +
136
+ `prefix of the window, so paging that index costs almost nothing. Page it with ${toolName} ` +
137
+ `{"action":"status"} and expand what the current task needs with ${toolName} ` +
138
+ '{"action":"expand","id":"<fold-id>"}.';
139
+ }
140
+ if (milestone === "notice") {
141
+ return `Context is ${percent}% full; curate it against the task you are on now. Fold the spans that task ` +
142
+ `no longer needs with ${toolName} ` +
143
+ '{"action":"fold","ids":["<start>","<end>"],"brief":"<factual brief>"}, and keep what must stay raw out ' +
144
+ `of every fold with ${toolName} {"action":"protect","ids":["<entry-id>"]}.`;
145
+ }
146
+ if (milestone === "tools") {
147
+ return "Completed read-only tool batches are the cheapest thing to fold, and their endpoint ids are in the " +
148
+ "live advisory. Fold the batches whose detail this task is finished with; each one expands back to the " +
149
+ `exact entries later with ${toolName} {"action":"expand","id":"<fold-id>"}.`;
150
+ }
151
+ return "Fold up: hand two or more adjacent folds of finished work to " +
152
+ `${toolName} {"action":"fold","ids":["<fold-id>","<fold-id>"],"brief":"<factual brief>"} so they nest in ` +
153
+ "one deeper fold, leaving the oldest material deepest and still exactly recoverable. A closed chapter " +
154
+ `folds first, from the eligibleChapter endpoints in ${toolName} {"action":"status"}.`;
155
+ }
156
+
157
+ export function milestoneText(
158
+ milestone: AdvisoryMilestone,
159
+ sessionId: string,
160
+ threshold: number,
161
+ toolName: string,
162
+ brandNoun = DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
163
+ guidance: GuidanceProfile = DEFAULT_GUIDANCE_PROFILE,
164
+ ): string {
165
+ const percent = Math.round(threshold * 100);
166
+ const prefix = `[${contextBrand(brandNoun)} milestone ${milestone}; session ${sessionId.slice(0, 16)}]`;
167
+ if (guidance === "curation" && milestone !== "urgent") {
168
+ return `${prefix} ${curationText(milestone, percent, toolName)}`;
169
+ }
170
+ if (milestone === "notice") {
171
+ return `${prefix} Context pressure has crossed ${percent}%. Automatic folding is available. ` +
172
+ `Inspect candidates exactly with ${toolName} {"action":"status"}.`;
173
+ }
174
+ if (milestone === "tools") {
175
+ return `${prefix} The read-only tool-fold rung begins at ${percent}%. ` +
176
+ "Eligible completed tool batches can be folded now; current endpoint ids are in the live advisory.";
177
+ }
178
+ if (milestone === "chapters") {
179
+ return `${prefix} The chapter preparation rung begins at ${percent}%. ` +
180
+ `Use eligibleChapter endpoints with ${toolName} ` +
181
+ '{"action":"fold","ids":["<start>","<end>"],"brief":"<factual brief>"}.';
182
+ }
183
+ return `${prefix} The hard context fence is near. The next automatic action is a committed chapter fold ` +
184
+ "or the provider request is aborted before transmission.";
185
+ }
186
+
187
+ /**
188
+ * The surfacing carrier. It rides the same ephemeral tail channel as the live
189
+ * advisory: appended after the stable prefix, never durable, never a mutation of
190
+ * the projection, and cleared whenever the next context event is built.
191
+ */
192
+ export function surfacingText(input: {
193
+ suggestions: readonly SurfacingSuggestion[];
194
+ brandNoun?: string;
195
+ }): string | null {
196
+ if (!input.suggestions.length) return null;
197
+ const brand = contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN);
198
+ const lines = input.suggestions.map((suggestion, index) => [
199
+ `${index + 1}. ${suggestion.id}${suggestion.depth ? ` (depth ${suggestion.depth})` : ""}: ${suggestion.text}`,
200
+ ...(suggestion.alternateRoute ? [` peek (ephemeral read): ${suggestion.alternateRoute}`] : []),
201
+ ` expand (durable restore): ${suggestion.route}`,
202
+ ].join("\n"));
203
+ return boundReceiptText(
204
+ [
205
+ `[${brand} suggestions] ${input.suggestions.length} collapsed span(s) look relevant to the current ` +
206
+ "task. Peek reads one back ephemerally at any depth; expand restores it in place, outermost first. " +
207
+ "Ignoring them is a valid answer.",
208
+ ...lines,
209
+ ].join("\n"),
210
+ 2_048,
211
+ `[${brand} suggestions] Relevant collapsed spans are unavailable this pass.`,
212
+ );
213
+ }
214
+
215
+ export function liveAdvisoryText(input: {
216
+ milestone: AdvisoryMilestone;
217
+ ratio: number;
218
+ toolEndpoints: string[];
219
+ chapterEndpoints: string[];
220
+ remediationCount: number;
221
+ brandNoun?: string;
222
+ }): string {
223
+ const tools = input.toolEndpoints.length
224
+ ? input.toolEndpoints.slice(0, 3).join(", ")
225
+ : "none";
226
+ const chapter = input.chapterEndpoints.length
227
+ ? `${input.chapterEndpoints[0]}..${input.chapterEndpoints.at(-1)}`
228
+ : "none";
229
+ return boundReceiptText(
230
+ `[${contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN)} advisory] ` +
231
+ `pressure ${Math.round(input.ratio * 100)}%; milestone ${input.milestone}; ` +
232
+ `eligible read-only batch endpoints: ${tools}; eligibleChapter endpoints: ${chapter}; ` +
233
+ `session milestone count: ${input.remediationCount}.`,
234
+ 2_048,
235
+ `[${contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN)} advisory] ` +
236
+ "Live pressure details are unavailable.",
237
+ );
238
+ }
@@ -0,0 +1,171 @@
1
+ import {
2
+ denseOwnArrayValues,
3
+ evidenceSha256,
4
+ isPlainRecord,
5
+ stableStringify,
6
+ } from "../json.ts";
7
+ import { ACTIVE_CONTEXT_POLICY, DEFAULT_ACTIVE_CONTEXT_TOOL_NAME } from "./policy.ts";
8
+ import type { ActiveContextState } from "./policy.ts";
9
+
10
+ export function clone<T>(value: T): T {
11
+ return structuredClone(value);
12
+ }
13
+
14
+ export function bytes(value: unknown): number {
15
+ return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
16
+ }
17
+
18
+ export function boundedUtf8(text: string, maximumBytes: number): string {
19
+ const buffer = Buffer.from(text, "utf8");
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
+ return buffer.subarray(0, maximumBytes).toString("utf8").replace(/\uFFFD$/, "");
24
+ }
25
+
26
+ export function ownValue(value: unknown, key: string): unknown {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
28
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
29
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
30
+ }
31
+
32
+ export function exactRecord(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
33
+ if (!isPlainRecord(value)) return false;
34
+ const expected = new Set(keys);
35
+ const ownKeys = Reflect.ownKeys(value);
36
+ if (ownKeys.length !== expected.size) return false;
37
+ for (let index = 0; index < ownKeys.length; index += 1) {
38
+ const key = ownKeys[index];
39
+ if (typeof key !== "string" || !expected.has(key)) return false;
40
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
41
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ export function messageRole(message: unknown): string | null {
47
+ const role = ownValue(message, "role");
48
+ return typeof role === "string" && role ? role : null;
49
+ }
50
+
51
+ export function contentText(message: unknown): string {
52
+ const content = ownValue(message, "content");
53
+ if (typeof content === "string") return content;
54
+ const parts = denseOwnArrayValues(content);
55
+ if (!parts) return "";
56
+ const text: string[] = [];
57
+ for (let index = 0; index < parts.length; index += 1) {
58
+ const part = parts[index];
59
+ if (part && typeof part === "object" && ownValue(part, "type") === "text") {
60
+ text.push(String(ownValue(part, "text") ?? ""));
61
+ }
62
+ }
63
+ return text.join("\n");
64
+ }
65
+
66
+ /** Pi's exact one-entry projection, kept local so this service has no second transcript owner. */
67
+ export function sessionEntryMessages(entry: Record<string, unknown>): unknown[] {
68
+ if (entry.type === "message" && entry.message && typeof entry.message === "object") {
69
+ const message = entry.message as Record<string, unknown>;
70
+ if ((message.role === "user" || message.role === "assistant" || message.role === "toolResult") &&
71
+ message.content == null) {
72
+ return [{ ...message, content: [] }];
73
+ }
74
+ return [message];
75
+ }
76
+ const timestamp = new Date(String(entry.timestamp)).getTime();
77
+ if (entry.type === "custom_message") {
78
+ return [{
79
+ role: "custom",
80
+ customType: entry.customType,
81
+ content: entry.content ?? [],
82
+ display: entry.display,
83
+ details: entry.details,
84
+ timestamp,
85
+ }];
86
+ }
87
+ if (entry.type === "branch_summary" && entry.summary) {
88
+ return [{ role: "branchSummary", summary: entry.summary, fromId: entry.fromId, timestamp }];
89
+ }
90
+ if (entry.type === "compaction") {
91
+ return [{ role: "compactionSummary", summary: entry.summary, tokensBefore: entry.tokensBefore, timestamp }];
92
+ }
93
+ return [];
94
+ }
95
+
96
+ export type UniqueMessageAnchor = {
97
+ index: number;
98
+ entryId: string;
99
+ message: unknown;
100
+ };
101
+
102
+ export function uniqueMessageDigestAnchor(
103
+ entries: readonly unknown[],
104
+ messageSha256: string,
105
+ ): UniqueMessageAnchor | null {
106
+ let anchor: UniqueMessageAnchor | null = null;
107
+ let digestMatches = 0;
108
+ for (let index = 0; index < entries.length; index += 1) {
109
+ const entry = entries[index];
110
+ if (!entry || typeof entry !== "object") continue;
111
+ const entryId = ownValue(entry, "id");
112
+ if (typeof entryId !== "string" || !entryId) continue;
113
+ const messages = sessionEntryMessages(entry as Record<string, unknown>);
114
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
115
+ let digest = "";
116
+ try { digest = evidenceSha256(messages[messageIndex]); } catch { continue; }
117
+ if (digest !== messageSha256) continue;
118
+ digestMatches += 1;
119
+ anchor = { index, entryId, message: messages[messageIndex] };
120
+ }
121
+ }
122
+ if (digestMatches !== 1 || !anchor) return null;
123
+ let identityMatches = 0;
124
+ for (let index = 0; index < entries.length; index += 1) {
125
+ if (ownValue(entries[index], "id") === anchor.entryId) identityMatches += 1;
126
+ }
127
+ return identityMatches === 1 ? anchor : null;
128
+ }
129
+
130
+ export function emptyActiveContextState(sessionId: string): ActiveContextState {
131
+ if (!sessionId) throw new Error("Active context requires a Pi session ID");
132
+ return {
133
+ version: 1,
134
+ sessionId,
135
+ revision: 0,
136
+ folds: [],
137
+ expanded: [],
138
+ protected: [],
139
+ tokensSinceToolFold: 0,
140
+ leases: {},
141
+ advisory: { highWater: 0, delivered: {} },
142
+ };
143
+ }
144
+
145
+ export function usefulBrief(
146
+ value: unknown,
147
+ maximum = ACTIVE_CONTEXT_POLICY.maxBriefChars,
148
+ toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
149
+ ): value is string {
150
+ if (typeof value !== "string") return false;
151
+ const brief = value.trim();
152
+ if (!brief || brief.length > maximum) return false;
153
+ const factualLines: string[] = [];
154
+ const lines = brief.split(/\r?\n/);
155
+ for (let index = 0; index < lines.length; index += 1) {
156
+ const text = lines[index].trim();
157
+ if (!text || text.toLowerCase().includes(toolName.toLowerCase()) ||
158
+ /^\[[^\]]*(?:fold|active-context)[^\]]*\]$/i.test(text)) continue;
159
+ if (/^(?:topology\b|expand\b|refold\b|list(?:\/page)?\b|page\b|action\b|fold(?:ed)?\s+(?:placeholder|uid|id)\b|(?:this\s+)?fold\b.*\b(?:expand|refold|placeholder|topology|uid|id)\b|(?:parent|children?|previous|next|navigation)\s*[:=])/i.test(text)) continue;
160
+ factualLines.push(text);
161
+ }
162
+ const factual = factualLines.join(" ").trim();
163
+ return /[A-Za-z0-9]{3,}/.test(factual);
164
+ }
165
+
166
+ export function structurallyValidBrief(value: unknown, maximum = ACTIVE_CONTEXT_POLICY.maxBriefChars): value is string {
167
+ if (typeof value !== "string") return false;
168
+ const brief = value.trim();
169
+ return Boolean(brief) && brief.length <= maximum &&
170
+ (brief.match(/[A-Za-z0-9]/g)?.length ?? 0) >= 3;
171
+ }