pi-fold 0.0.1 → 0.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/LICENSE +21 -0
- package/README.md +76 -11
- package/extensions/active-context.ts +1990 -0
- package/extensions/evidence.js +673 -0
- package/extensions/index.js +32 -0
- package/extensions/json.ts +228 -0
- package/extensions/lib/advisory.ts +152 -0
- package/extensions/lib/canonical.ts +163 -0
- package/extensions/lib/folding.ts +1012 -0
- package/extensions/lib/measurement.ts +429 -0
- package/extensions/lib/persistence.ts +773 -0
- package/extensions/lib/policy.ts +233 -0
- package/extensions/lib/selection.ts +501 -0
- package/extensions/lib/tool-surface.ts +66 -0
- package/extensions/lib/transcript.ts +479 -0
- package/extensions/summarizer.js +95 -0
- package/package.json +48 -11
- package/index.js +0 -4
|
@@ -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,152 @@
|
|
|
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
|
+
} from "./policy.ts";
|
|
13
|
+
import { contextBrand, DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN } from "./policy.ts";
|
|
14
|
+
|
|
15
|
+
export { ADVISORY_BUDGETS };
|
|
16
|
+
|
|
17
|
+
export interface AdvisorySchedule {
|
|
18
|
+
key: string;
|
|
19
|
+
rungs: Array<{ milestone: AdvisoryMilestone; threshold: number; budget: number }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function advisoryState(state: ActiveContextState): NonNullable<ActiveContextState["advisory"]> {
|
|
23
|
+
if (state.advisory === undefined) return { highWater: 0, delivered: {} };
|
|
24
|
+
if (!validAdvisoryState(state.advisory)) {
|
|
25
|
+
throw new Error("Corrupt in-memory advisory state (no silent fallback)");
|
|
26
|
+
}
|
|
27
|
+
return clone(state.advisory);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearArmedAdvisory(state: ActiveContextState): ActiveContextState {
|
|
31
|
+
const current = advisoryState(state);
|
|
32
|
+
if (!current.armed) return state;
|
|
33
|
+
const advisory = { ...current };
|
|
34
|
+
delete advisory.armed;
|
|
35
|
+
return { ...state, advisory };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function advisorySchedule(
|
|
39
|
+
snapshot: Pick<ActiveContextSnapshot, "policy" | "contextWindow">,
|
|
40
|
+
): AdvisorySchedule {
|
|
41
|
+
const rungs = [
|
|
42
|
+
{ milestone: "notice" as const, threshold: 0.50, budget: ADVISORY_BUDGETS.notice },
|
|
43
|
+
{ milestone: "tools" as const, threshold: snapshot.policy.toolFoldRatio - 0.04,
|
|
44
|
+
budget: ADVISORY_BUDGETS.tools },
|
|
45
|
+
{ milestone: "chapters" as const, threshold: snapshot.policy.prepareRatio - 0.05,
|
|
46
|
+
budget: ADVISORY_BUDGETS.chapters },
|
|
47
|
+
{ milestone: "urgent" as const, threshold: hardFenceRatio(snapshot) - 0.03,
|
|
48
|
+
budget: ADVISORY_BUDGETS.urgent },
|
|
49
|
+
];
|
|
50
|
+
for (let index = rungs.length - 2; index >= 0; index -= 1) {
|
|
51
|
+
rungs[index].threshold = Math.min(rungs[index].threshold, rungs[index + 1].threshold - 0.02);
|
|
52
|
+
}
|
|
53
|
+
for (const rung of rungs) rung.threshold = Math.max(0, Math.min(1, rung.threshold));
|
|
54
|
+
return {
|
|
55
|
+
key: sha256Value(rungs.map(({ milestone, threshold }) => ({ milestone, threshold }))),
|
|
56
|
+
rungs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function updateAdvisoryMilestone(
|
|
61
|
+
currentState: ActiveContextState,
|
|
62
|
+
ratio: number,
|
|
63
|
+
schedule: AdvisorySchedule,
|
|
64
|
+
scheduleChanged: boolean,
|
|
65
|
+
scheduleKey: string,
|
|
66
|
+
): { state: ActiveContextState; milestone: AdvisoryMilestone | null } {
|
|
67
|
+
const current = advisoryState(currentState);
|
|
68
|
+
if (scheduleChanged) {
|
|
69
|
+
return {
|
|
70
|
+
state: { ...currentState, advisory: { ...current, highWater: Math.min(1, ratio) } },
|
|
71
|
+
milestone: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let highWater = current.highWater;
|
|
75
|
+
for (let index = schedule.rungs.length - 1; index >= 0; index -= 1) {
|
|
76
|
+
const rung = schedule.rungs[index];
|
|
77
|
+
if ((current.delivered[rung.milestone] ?? 0) > 0 && ratio < 0.85 * rung.threshold) {
|
|
78
|
+
highWater = Math.min(highWater, index > 0 ? schedule.rungs[index - 1].threshold : 0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const crossed = schedule.rungs.filter((rung) =>
|
|
82
|
+
highWater < rung.threshold && ratio >= rung.threshold &&
|
|
83
|
+
(current.delivered[rung.milestone] ?? 0) < rung.budget);
|
|
84
|
+
const selected = crossed.at(-1) ?? null;
|
|
85
|
+
const delivered = { ...current.delivered };
|
|
86
|
+
if (selected) delivered[selected.milestone] = (delivered[selected.milestone] ?? 0) + 1;
|
|
87
|
+
const armed = selected
|
|
88
|
+
? { milestone: selected.milestone, threshold: selected.threshold, scheduleKey }
|
|
89
|
+
: current.armed;
|
|
90
|
+
return {
|
|
91
|
+
state: {
|
|
92
|
+
...currentState,
|
|
93
|
+
advisory: {
|
|
94
|
+
highWater: Math.min(1, Math.max(highWater, ratio)),
|
|
95
|
+
delivered,
|
|
96
|
+
...(armed ? { armed } : {}),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
milestone: selected?.milestone ?? null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function milestoneText(
|
|
104
|
+
milestone: AdvisoryMilestone,
|
|
105
|
+
sessionId: string,
|
|
106
|
+
threshold: number,
|
|
107
|
+
toolName: string,
|
|
108
|
+
brandNoun = DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
|
|
109
|
+
): string {
|
|
110
|
+
const percent = Math.round(threshold * 100);
|
|
111
|
+
const prefix = `[${contextBrand(brandNoun)} milestone ${milestone}; session ${sessionId.slice(0, 16)}]`;
|
|
112
|
+
if (milestone === "notice") {
|
|
113
|
+
return `${prefix} Context pressure has crossed ${percent}%. Automatic folding is available. ` +
|
|
114
|
+
`Inspect candidates exactly with ${toolName} {"action":"status"}.`;
|
|
115
|
+
}
|
|
116
|
+
if (milestone === "tools") {
|
|
117
|
+
return `${prefix} The read-only tool-fold rung begins at ${percent}%. ` +
|
|
118
|
+
"Eligible completed tool batches can be folded now; current endpoint ids are in the live advisory.";
|
|
119
|
+
}
|
|
120
|
+
if (milestone === "chapters") {
|
|
121
|
+
return `${prefix} The chapter preparation rung begins at ${percent}%. ` +
|
|
122
|
+
`Use eligibleChapter endpoints with ${toolName} ` +
|
|
123
|
+
'{"action":"fold","ids":["<start>","<end>"],"brief":"<factual brief>"}.';
|
|
124
|
+
}
|
|
125
|
+
return `${prefix} The hard context fence is near. The next automatic action is a committed chapter fold ` +
|
|
126
|
+
"or the provider request is aborted before transmission.";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function liveAdvisoryText(input: {
|
|
130
|
+
milestone: AdvisoryMilestone;
|
|
131
|
+
ratio: number;
|
|
132
|
+
toolEndpoints: string[];
|
|
133
|
+
chapterEndpoints: string[];
|
|
134
|
+
remediationCount: number;
|
|
135
|
+
brandNoun?: string;
|
|
136
|
+
}): string {
|
|
137
|
+
const tools = input.toolEndpoints.length
|
|
138
|
+
? input.toolEndpoints.slice(0, 3).join(", ")
|
|
139
|
+
: "none";
|
|
140
|
+
const chapter = input.chapterEndpoints.length
|
|
141
|
+
? `${input.chapterEndpoints[0]}..${input.chapterEndpoints.at(-1)}`
|
|
142
|
+
: "none";
|
|
143
|
+
return boundReceiptText(
|
|
144
|
+
`[${contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN)} advisory] ` +
|
|
145
|
+
`pressure ${Math.round(input.ratio * 100)}%; milestone ${input.milestone}; ` +
|
|
146
|
+
`eligible read-only batch endpoints: ${tools}; eligibleChapter endpoints: ${chapter}; ` +
|
|
147
|
+
`session milestone count: ${input.remediationCount}.`,
|
|
148
|
+
2_048,
|
|
149
|
+
`[${contextBrand(input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN)} advisory] ` +
|
|
150
|
+
"Live pressure details are unavailable.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
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 ownValue(value: unknown, key: string): unknown {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
20
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
21
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function exactRecord(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
25
|
+
if (!isPlainRecord(value)) return false;
|
|
26
|
+
const expected = new Set(keys);
|
|
27
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
28
|
+
if (ownKeys.length !== expected.size) return false;
|
|
29
|
+
for (let index = 0; index < ownKeys.length; index += 1) {
|
|
30
|
+
const key = ownKeys[index];
|
|
31
|
+
if (typeof key !== "string" || !expected.has(key)) return false;
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
33
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function messageRole(message: unknown): string | null {
|
|
39
|
+
const role = ownValue(message, "role");
|
|
40
|
+
return typeof role === "string" && role ? role : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function contentText(message: unknown): string {
|
|
44
|
+
const content = ownValue(message, "content");
|
|
45
|
+
if (typeof content === "string") return content;
|
|
46
|
+
const parts = denseOwnArrayValues(content);
|
|
47
|
+
if (!parts) return "";
|
|
48
|
+
const text: string[] = [];
|
|
49
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
50
|
+
const part = parts[index];
|
|
51
|
+
if (part && typeof part === "object" && ownValue(part, "type") === "text") {
|
|
52
|
+
text.push(String(ownValue(part, "text") ?? ""));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return text.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Pi's exact one-entry projection, kept local so this service has no second transcript owner. */
|
|
59
|
+
export function sessionEntryMessages(entry: Record<string, unknown>): unknown[] {
|
|
60
|
+
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
61
|
+
const message = entry.message as Record<string, unknown>;
|
|
62
|
+
if ((message.role === "user" || message.role === "assistant" || message.role === "toolResult") &&
|
|
63
|
+
message.content == null) {
|
|
64
|
+
return [{ ...message, content: [] }];
|
|
65
|
+
}
|
|
66
|
+
return [message];
|
|
67
|
+
}
|
|
68
|
+
const timestamp = new Date(String(entry.timestamp)).getTime();
|
|
69
|
+
if (entry.type === "custom_message") {
|
|
70
|
+
return [{
|
|
71
|
+
role: "custom",
|
|
72
|
+
customType: entry.customType,
|
|
73
|
+
content: entry.content ?? [],
|
|
74
|
+
display: entry.display,
|
|
75
|
+
details: entry.details,
|
|
76
|
+
timestamp,
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
if (entry.type === "branch_summary" && entry.summary) {
|
|
80
|
+
return [{ role: "branchSummary", summary: entry.summary, fromId: entry.fromId, timestamp }];
|
|
81
|
+
}
|
|
82
|
+
if (entry.type === "compaction") {
|
|
83
|
+
return [{ role: "compactionSummary", summary: entry.summary, tokensBefore: entry.tokensBefore, timestamp }];
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type UniqueMessageAnchor = {
|
|
89
|
+
index: number;
|
|
90
|
+
entryId: string;
|
|
91
|
+
message: unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function uniqueMessageDigestAnchor(
|
|
95
|
+
entries: readonly unknown[],
|
|
96
|
+
messageSha256: string,
|
|
97
|
+
): UniqueMessageAnchor | null {
|
|
98
|
+
let anchor: UniqueMessageAnchor | null = null;
|
|
99
|
+
let digestMatches = 0;
|
|
100
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
101
|
+
const entry = entries[index];
|
|
102
|
+
if (!entry || typeof entry !== "object") continue;
|
|
103
|
+
const entryId = ownValue(entry, "id");
|
|
104
|
+
if (typeof entryId !== "string" || !entryId) continue;
|
|
105
|
+
const messages = sessionEntryMessages(entry as Record<string, unknown>);
|
|
106
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
|
|
107
|
+
let digest = "";
|
|
108
|
+
try { digest = evidenceSha256(messages[messageIndex]); } catch { continue; }
|
|
109
|
+
if (digest !== messageSha256) continue;
|
|
110
|
+
digestMatches += 1;
|
|
111
|
+
anchor = { index, entryId, message: messages[messageIndex] };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (digestMatches !== 1 || !anchor) return null;
|
|
115
|
+
let identityMatches = 0;
|
|
116
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
117
|
+
if (ownValue(entries[index], "id") === anchor.entryId) identityMatches += 1;
|
|
118
|
+
}
|
|
119
|
+
return identityMatches === 1 ? anchor : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function emptyActiveContextState(sessionId: string): ActiveContextState {
|
|
123
|
+
if (!sessionId) throw new Error("Active context requires a Pi session ID");
|
|
124
|
+
return {
|
|
125
|
+
version: 1,
|
|
126
|
+
sessionId,
|
|
127
|
+
revision: 0,
|
|
128
|
+
folds: [],
|
|
129
|
+
expanded: [],
|
|
130
|
+
protected: [],
|
|
131
|
+
tokensSinceToolFold: 0,
|
|
132
|
+
leases: {},
|
|
133
|
+
advisory: { highWater: 0, delivered: {} },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function usefulBrief(
|
|
138
|
+
value: unknown,
|
|
139
|
+
maximum = ACTIVE_CONTEXT_POLICY.maxBriefChars,
|
|
140
|
+
toolName = DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
|
|
141
|
+
): value is string {
|
|
142
|
+
if (typeof value !== "string") return false;
|
|
143
|
+
const brief = value.trim();
|
|
144
|
+
if (!brief || brief.length > maximum) return false;
|
|
145
|
+
const factualLines: string[] = [];
|
|
146
|
+
const lines = brief.split(/\r?\n/);
|
|
147
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
148
|
+
const text = lines[index].trim();
|
|
149
|
+
if (!text || text.toLowerCase().includes(toolName.toLowerCase()) ||
|
|
150
|
+
/^\[[^\]]*(?:fold|active-context)[^\]]*\]$/i.test(text)) continue;
|
|
151
|
+
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;
|
|
152
|
+
factualLines.push(text);
|
|
153
|
+
}
|
|
154
|
+
const factual = factualLines.join(" ").trim();
|
|
155
|
+
return /[A-Za-z0-9]{3,}/.test(factual);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function structurallyValidBrief(value: unknown, maximum = ACTIVE_CONTEXT_POLICY.maxBriefChars): value is string {
|
|
159
|
+
if (typeof value !== "string") return false;
|
|
160
|
+
const brief = value.trim();
|
|
161
|
+
return Boolean(brief) && brief.length <= maximum &&
|
|
162
|
+
(brief.match(/[A-Za-z0-9]/g)?.length ?? 0) >= 3;
|
|
163
|
+
}
|