@visual-z/xeno 0.7.2 → 0.8.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.
- package/CHANGELOG.md +60 -0
- package/README.md +36 -9
- package/docs/foundations.md +1 -1
- package/package.json +3 -1
- package/src/apps/cli.ts +4 -3
- package/src/apps/commands.ts +8 -0
- package/src/apps/goal-flow.ts +70 -0
- package/src/apps/render.ts +0 -2
- package/src/apps/session-hub.ts +6 -5
- package/src/apps/session-lifecycle.ts +64 -0
- package/src/apps/shell.ts +89 -34
- package/src/apps/startup.ts +32 -31
- package/src/apps/ui/app.ts +105 -13
- package/src/apps/ui/control-model.ts +18 -1
- package/src/apps/ui/surface.ts +2 -0
- package/src/apps/update-check.ts +1 -1
- package/src/core/acceptance.ts +41 -9
- package/src/core/blind-checks.ts +72 -0
- package/src/core/branch-receipt.ts +3 -1
- package/src/core/checkpoints.ts +21 -0
- package/src/core/churn.ts +2 -2
- package/src/core/completion-review.ts +18 -11
- package/src/core/continuation.ts +11 -10
- package/src/core/control-view.ts +5 -8
- package/src/core/deadline.ts +2 -1
- package/src/core/derive.ts +0 -7
- package/src/core/environment.ts +2 -2
- package/src/core/event-validation.ts +19 -6
- package/src/core/events.ts +41 -32
- package/src/core/evidence-ladder.ts +62 -0
- package/src/core/evidence-links.ts +0 -10
- package/src/core/evidence.ts +1 -2
- package/src/core/export.ts +6 -11
- package/src/core/goal-view.ts +64 -0
- package/src/core/goal.ts +200 -0
- package/src/core/history.ts +1 -11
- package/src/core/json-event.ts +0 -6
- package/src/core/outcome.ts +0 -1
- package/src/core/program-status.ts +11 -2
- package/src/core/progress.ts +14 -8
- package/src/core/project.ts +6 -16
- package/src/core/read-bounds.ts +11 -2
- package/src/core/read-observation.ts +24 -0
- package/src/core/request-budget.ts +13 -3
- package/src/core/request-state.ts +178 -19
- package/src/core/response-recovery.ts +15 -2
- package/src/core/session-entry.ts +8 -3
- package/src/core/session-exit.ts +19 -3
- package/src/core/session-format.ts +169 -0
- package/src/core/settings.ts +7 -27
- package/src/core/stall.ts +30 -7
- package/src/core/stop-report.ts +4 -3
- package/src/core/transcript.ts +22 -2
- package/src/runtime/blind-check.ts +36 -0
- package/src/runtime/counter-check.ts +74 -0
- package/src/runtime/exec-sessions.ts +11 -0
- package/src/runtime/foundation.ts +46 -8
- package/src/runtime/loop.ts +239 -430
- package/src/runtime/oauth.ts +1 -3
- package/src/runtime/openai.ts +2 -2
- package/src/runtime/pi-provider.ts +3 -3
- package/src/runtime/provider-request.ts +2 -1
- package/src/runtime/provider.ts +33 -3
- package/src/runtime/retry.ts +9 -2
- package/src/runtime/runtime-target.ts +2 -2
- package/src/runtime/session-control.ts +4 -4
- package/src/runtime/tools.ts +44 -42
- package/src/runtime/usage.ts +28 -0
- package/src/runtime/version.ts +12 -0
- package/src/runtime/workspace-lifecycle.ts +31 -0
- package/src/store/blind-checks.ts +32 -0
- package/src/store/checkpoint-history.ts +1 -12
- package/src/store/file-index.ts +88 -7
- package/src/store/log.ts +25 -27
- package/src/store/recovery.ts +1 -3
- package/src/store/replay.ts +2 -2
- package/src/store/restore.ts +10 -202
- package/src/store/search.ts +7 -5
- package/src/store/session-archive.ts +0 -1
- package/src/store/sessions.ts +31 -33
- package/src/store/workspace-clone.ts +42 -0
- package/src/store/workspace.ts +10 -157
- package/src/tui/ui/duration.ts +16 -0
- package/src/tui/ui/layout.ts +4 -4
- package/src/tui/ui/screen.ts +13 -7
- package/src/tui/ui/status.ts +99 -17
- package/src/core/benchmark.ts +0 -195
- package/src/core/evaluation-coverage.ts +0 -74
- package/src/core/experiment.ts +0 -173
- package/src/core/harness-comparison.ts +0 -90
- package/src/core/harness-diagnostics.ts +0 -206
- package/src/core/native-eval.ts +0 -25
- package/src/core/policy-evaluation.ts +0 -55
- package/src/core/query-eval-contract.ts +0 -14
- package/src/core/recall-analysis.ts +0 -90
- package/src/core/recall-trial.ts +0 -153
- package/src/core/recovery-corpus.ts +0 -17
- package/src/core/recovery-evaluation.ts +0 -60
- package/src/core/tool-mode-protocol.ts +0 -25
- package/src/core/trial-lifecycle.ts +0 -14
- package/src/core/window-trial.ts +0 -65
- package/src/index.ts +0 -10
- package/src/runtime/benchmark.ts +0 -187
- package/src/runtime/harness-comparison.ts +0 -376
- package/src/runtime/harness-eval.ts +0 -462
- package/src/runtime/native-eval-fixtures.ts +0 -33
- package/src/runtime/native-eval.ts +0 -146
- package/src/runtime/query-eval-verifier.ts +0 -28
- package/src/runtime/recall-trial.ts +0 -226
- package/src/runtime/recovery-coverage.ts +0 -71
- package/src/runtime/recovery-evidence.ts +0 -76
- package/src/runtime/recovery-fixtures.ts +0 -116
- package/src/runtime/tool-mode-eval.ts +0 -146
- package/src/runtime/trial-control.ts +0 -45
- package/src/runtime/window-trial.ts +0 -141
- package/src/store/git-branch.ts +0 -35
- package/src/tui/ui/testing.ts +0 -5
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/** Q.20: meter the production Responses adapter without translating its wire protocol. */
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { nativeMeasured, nativeReservation, nativeUsage } from "../core/native-eval.ts";
|
|
4
|
-
import type { BenchmarkPrice } from "../core/benchmark.ts";
|
|
5
|
-
import type { FrozenPrefix, Message } from "../core/project.ts";
|
|
6
|
-
import type { Provider, ProviderConfig } from "./provider.ts";
|
|
7
|
-
import { PiProvider } from "./pi-provider.ts";
|
|
8
|
-
import type { ComparisonLedger, GatewayRecord } from "./harness-comparison.ts";
|
|
9
|
-
|
|
10
|
-
const digest = (body: string) => createHash("sha256").update(body).digest("hex");
|
|
11
|
-
const bytes = (body: string) => new TextEncoder().encode(body);
|
|
12
|
-
const noEffects = { env: () => undefined, run: () => { throw Error("evaluation forbids credential commands"); } };
|
|
13
|
-
// Offline replay only. It has the SDK's required claim shape, no signature or authority.
|
|
14
|
-
export const NATIVE_EVAL_TEST_KEY = "offline." + btoa(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "offline-evaluation" } })) + ".unsigned";
|
|
15
|
-
|
|
16
|
-
export async function nativeWire(request: Request): Promise<Record<string, unknown>> {
|
|
17
|
-
const raw = new Uint8Array(await request.clone().arrayBuffer());
|
|
18
|
-
if (raw.length > 8 * 1024 * 1024) throw Error("native eval wire exceeds archive bound");
|
|
19
|
-
const body = request.headers.get("content-encoding") === "zstd" ? Bun.zstdDecompressSync(raw) : raw;
|
|
20
|
-
if (body.length > 8 * 1024 * 1024) throw Error("native eval wire exceeds archive bound");
|
|
21
|
-
return JSON.parse(new TextDecoder().decode(body)) as Record<string, unknown>;
|
|
22
|
-
}
|
|
23
|
-
/** Running the same serializer with a local fetch prevents a second, approximate wire builder. */
|
|
24
|
-
export async function nativePayload(config: ProviderConfig, frozen: FrozenPrefix, messages: Message[], sessionId?: string): Promise<Record<string, unknown>> {
|
|
25
|
-
let payload: Record<string, unknown> | undefined;
|
|
26
|
-
const capture = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
27
|
-
payload = await nativeWire(new Request(input, init));
|
|
28
|
-
return new Response(null, { status: 400 });
|
|
29
|
-
}) as typeof fetch;
|
|
30
|
-
const provider = new PiProvider(config, NATIVE_EVAL_TEST_KEY, { oauth: true, effects: noEffects, fetch: capture,
|
|
31
|
-
...(sessionId ? { sessionId } : {}), retry: { maxRetries: 0, maxRetryDelayMs: 0 } });
|
|
32
|
-
await provider.send(frozen, messages, 0).catch(() => {});
|
|
33
|
-
if (!payload) throw Error("native offline wire was not produced");
|
|
34
|
-
return payload;
|
|
35
|
-
}
|
|
36
|
-
export function nativeInput(payload: Record<string, unknown>): { bytes: Uint8Array; messages: unknown[] } {
|
|
37
|
-
if (!Array.isArray(payload["input"])) throw Error("native input is missing");
|
|
38
|
-
return { bytes: bytes(JSON.stringify([payload["instructions"], payload["tools"] ?? [], payload["input"]])), messages: payload["input"] };
|
|
39
|
-
}
|
|
40
|
-
export interface NativeResponse { model: unknown; usage: unknown; status: unknown; service_tier: unknown }
|
|
41
|
-
|
|
42
|
-
/** Forward unchanged chunks with backpressure; retain only a terminal receipt, never headers. */
|
|
43
|
-
export function nativeResponseTap(body: ReadableStream<Uint8Array>, terminal: (response: NativeResponse) => Promise<void>) {
|
|
44
|
-
const decoder = new TextDecoder(); let pending = "", observed = false;
|
|
45
|
-
const line = async (text: string) => {
|
|
46
|
-
if (!text.startsWith("data:") || text.slice(5).trim() === "[DONE]") return;
|
|
47
|
-
const event = JSON.parse(text.slice(5)) as Record<string, unknown>;
|
|
48
|
-
if (!["response.completed", "response.done", "response.incomplete"].includes(String(event["type"]))) return;
|
|
49
|
-
if (observed) throw Error("duplicate native terminal response");
|
|
50
|
-
observed = true;
|
|
51
|
-
const response = event["response"] as Record<string, unknown> | undefined;
|
|
52
|
-
await terminal({ model: response?.["model"] ?? null, usage: response?.["usage"] ?? null,
|
|
53
|
-
status: response?.["status"] ?? null, service_tier: response?.["service_tier"] ?? null });
|
|
54
|
-
};
|
|
55
|
-
return body.pipeThrough(new TransformStream<Uint8Array, Uint8Array>({
|
|
56
|
-
async transform(chunk, controller) {
|
|
57
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
58
|
-
// The completed event repeats output. Bound this framing buffer separately from the saved receipt.
|
|
59
|
-
if (pending.length > 8 * 1024 * 1024) throw Error("native SSE frame exceeds archive bound");
|
|
60
|
-
let at: number;
|
|
61
|
-
while ((at = pending.indexOf("\n")) >= 0) { await line(pending.slice(0, at).replace(/\r$/, "")); pending = pending.slice(at + 1); }
|
|
62
|
-
controller.enqueue(chunk);
|
|
63
|
-
},
|
|
64
|
-
async flush() { pending += decoder.decode(); if (pending.trim()) await line(pending.trim()); },
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function nativeEvalTransport(options: {
|
|
69
|
-
config: ProviderConfig; key: string; price: BenchmarkPrice; ledger: ComparisonLedger; maxRequests: number;
|
|
70
|
-
fetch?: typeof fetch;
|
|
71
|
-
sessionId?: string;
|
|
72
|
-
record: (row: GatewayRecord) => Promise<void>;
|
|
73
|
-
wire: (payload: Record<string, unknown>, request: number) => Promise<void>;
|
|
74
|
-
response: (receipt: NativeResponse, request: number) => Promise<void>;
|
|
75
|
-
}) {
|
|
76
|
-
const { config, ledger, price } = options;
|
|
77
|
-
if (config.spec.api !== "openai-codex-responses" || !config.spec.maxTokens || config.maxTokens !== config.spec.maxTokens)
|
|
78
|
-
throw Error("native eval must reserve the full subscription model output ceiling");
|
|
79
|
-
const controller = new AbortController();
|
|
80
|
-
let requests = 0, stopped: "budget" | "balance" | "error" | "limit" | null = null;
|
|
81
|
-
let previous: ReturnType<typeof nativeInput> | null = null;
|
|
82
|
-
let current: { row: GatewayRecord; started: number; terminal: NativeResponse | null; httpStatus: number | null } | undefined;
|
|
83
|
-
let active = false, idle = Promise.resolve();
|
|
84
|
-
const observedFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
85
|
-
if (!active || current || controller.signal.aborted || ledger.halted || stopped) throw Error("native eval refused an extra request");
|
|
86
|
-
if (requests >= options.maxRequests) { stopped = "limit"; throw Error("native request limit"); }
|
|
87
|
-
const request = new Request(input, init), payload = await nativeWire(request), wire = JSON.stringify(payload);
|
|
88
|
-
if (payload["model"] !== (config.spec.wireId ?? config.spec.id) || payload["store"] !== false || payload["previous_response_id"] !== undefined || payload["stream"] !== true)
|
|
89
|
-
throw Error("native wire differs from frozen stateless policy");
|
|
90
|
-
const reservation = nativeReservation(bytes(wire).length, config.spec.maxTokens!, price);
|
|
91
|
-
if (ledger.budget === null || ledger.charged + reservation > ledger.budget) { stopped = "budget"; throw Error("native eval budget exhausted"); }
|
|
92
|
-
const inputState = nativeInput(payload); let shared = 0;
|
|
93
|
-
if (previous) while (shared < Math.min(previous.bytes.length, inputState.bytes.length) && previous.bytes[shared] === inputState.bytes[shared]) shared++;
|
|
94
|
-
const unchanged = previous === null ? null : JSON.stringify(inputState.messages.slice(0, previous.messages.length)) === JSON.stringify(previous.messages);
|
|
95
|
-
previous = inputState;
|
|
96
|
-
ledger.charged += reservation;
|
|
97
|
-
const row: GatewayRecord = { kind: "reserved", request: ++requests, inputSha256: digest(wire), inputBytes: inputState.bytes.length,
|
|
98
|
-
sharedInputBytes: shared, prefixUnchanged: unchanged, hasTools: Array.isArray(payload["tools"]) && payload["tools"].length > 0,
|
|
99
|
-
reservation, charge: reservation, measured: null, ledgerCharged: ledger.charged, usage: null, failure: null, responseModel: null, wallMs: 0 };
|
|
100
|
-
const state = { row, started: performance.now(), terminal: null as NativeResponse | null, httpStatus: null as number | null };
|
|
101
|
-
current = state;
|
|
102
|
-
await options.record(row); await options.wire(payload, requests);
|
|
103
|
-
const response = await (options.fetch ?? fetch)(new Request(request, { redirect: "error", signal: AbortSignal.any([controller.signal, request.signal]) }));
|
|
104
|
-
state.httpStatus = response.status;
|
|
105
|
-
if (!response.ok || !response.body) { await response.body?.cancel(); throw Error("native upstream response failed"); }
|
|
106
|
-
return new Response(nativeResponseTap(response.body, async terminal => {
|
|
107
|
-
state.terminal = terminal; await options.response(terminal, row.request);
|
|
108
|
-
}), { status: response.status, headers: response.headers });
|
|
109
|
-
}) as typeof fetch;
|
|
110
|
-
const actual = new PiProvider(config, options.key, { oauth: true, effects: noEffects, fetch: observedFetch,
|
|
111
|
-
...(options.sessionId ? { sessionId: options.sessionId } : {}), retry: { maxRetries: 0, maxRetryDelayMs: 0 } });
|
|
112
|
-
const provider: Provider = { config, async send(frozen, messages, node, signal, onDelta) {
|
|
113
|
-
if (active) throw Error("native eval model requests must be sequential");
|
|
114
|
-
active = true; current = undefined;
|
|
115
|
-
let finish = () => {}; idle = new Promise<void>(resolve => { finish = resolve; });
|
|
116
|
-
let failed = false;
|
|
117
|
-
try {
|
|
118
|
-
const turn = await actual.send(frozen, messages, node, AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]), onDelta);
|
|
119
|
-
const state = current as { terminal: NativeResponse | null } | undefined;
|
|
120
|
-
const observed = nativeUsage(state?.terminal);
|
|
121
|
-
const same = observed && Object.entries(observed).every(([key, count]) => turn.usage[key as keyof typeof observed] === count);
|
|
122
|
-
const model = state?.terminal?.model;
|
|
123
|
-
if (!same || typeof model !== "string" || !(model === config.spec.id || new RegExp("^" + config.spec.id + "-\\d{4}-\\d{2}-\\d{2}$").test(model)))
|
|
124
|
-
throw Error("native model or usage differs from the production adapter");
|
|
125
|
-
return turn;
|
|
126
|
-
} catch {
|
|
127
|
-
failed = true; stopped ??= "error";
|
|
128
|
-
throw Error("native eval request failed; inspect status and retained usage");
|
|
129
|
-
} finally {
|
|
130
|
-
try {
|
|
131
|
-
const state = current as { row: GatewayRecord; started: number; terminal: NativeResponse | null; httpStatus: number | null } | undefined;
|
|
132
|
-
if (state) {
|
|
133
|
-
const usage = nativeUsage(state.terminal), measured = nativeMeasured(state.terminal, price), charge = measured ?? state.row.reservation;
|
|
134
|
-
ledger.charged += charge - state.row.reservation;
|
|
135
|
-
if (failed || measured === null || charge > state.row.reservation) { ledger.halted = true; stopped ??= "error"; }
|
|
136
|
-
await options.record({ ...state.row, kind: "settled", usage, measured, charge, ledgerCharged: ledger.charged,
|
|
137
|
-
responseModel: typeof state.terminal?.model === "string" ? state.terminal.model : null,
|
|
138
|
-
failure: failed ? { kind: [402, 429].includes(state.httpStatus ?? 0) ? "balance" : "provider", status: state.httpStatus } : null,
|
|
139
|
-
wallMs: performance.now() - state.started });
|
|
140
|
-
}
|
|
141
|
-
} catch { ledger.halted = true; throw Error("native eval ledger could not be persisted"); }
|
|
142
|
-
finally { active = false; finish(); }
|
|
143
|
-
}
|
|
144
|
-
} };
|
|
145
|
-
return { provider, get requests() { return requests; }, get stopped() { return stopped; }, async close() { controller.abort(); await idle; } };
|
|
146
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/** Q.21: emit the isolated Bun verifier; it owns file I/O and process exit. */
|
|
2
|
-
import type { queryEvalContract } from "../core/query-eval-contract.ts";
|
|
3
|
-
|
|
4
|
-
export function queryEvalVerifier(contract: ReturnType<typeof queryEvalContract>): string {
|
|
5
|
-
// The standalone fixture carries only shape expectations and a one-way oracle
|
|
6
|
-
// digest. Diagnostics never expose expected numeric values to the model.
|
|
7
|
-
return `import { createHash } from "node:crypto";
|
|
8
|
-
const paths = ${JSON.stringify(contract.paths)}, columns = ${JSON.stringify(contract.columns)}, rowCount = ${contract.rowCount};
|
|
9
|
-
const fail = (reason, detail = {}) => { console.error(JSON.stringify({ok:false, reason, ...detail})); process.exit(1); };
|
|
10
|
-
const body = await Bun.file("answer.txt").text().catch(() => fail("answer_missing"));
|
|
11
|
-
let value; try { value = JSON.parse(body); } catch { fail("invalid_json"); }
|
|
12
|
-
if (!Array.isArray(value) || value.length !== paths.length) fail("source_count", {expected:paths.length, actual:Array.isArray(value)?value.length:null});
|
|
13
|
-
for (let i = 0; i < paths.length; i++) {
|
|
14
|
-
const item = value[i];
|
|
15
|
-
if (!item || typeof item !== "object" || JSON.stringify(Object.keys(item)) !== '["path","rows"]') fail("item_columns", {index:i, expected:["path","rows"]});
|
|
16
|
-
if (item.path !== paths[i]) fail("source_order", {index:i, expected:paths[i]});
|
|
17
|
-
if (!Array.isArray(item.rows) || item.rows.length !== rowCount) fail("row_count", {path:paths[i], expected:rowCount, actual:Array.isArray(item.rows)?item.rows.length:null});
|
|
18
|
-
for (let row = 0; row < item.rows.length; row++) {
|
|
19
|
-
const record = item.rows[row];
|
|
20
|
-
if (!record || typeof record !== "object" || JSON.stringify(Object.keys(record)) !== JSON.stringify(columns)) fail("result_columns", {path:paths[i], row, expected:columns, actual:record&&typeof record==="object"?Object.keys(record):null});
|
|
21
|
-
if (columns.some(column => column === "team" ? typeof record[column] !== "string" : !Number.isFinite(record[column]))) fail("result_type", {path:paths[i], row});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
if (body !== JSON.stringify(value) + "\\n") fail("serialization", {expected:"compact JSON with one trailing newline"});
|
|
25
|
-
if (createHash("sha256").update(body).digest("hex") !== ${JSON.stringify(contract.answerSha256)}) fail("value_mismatch", {next:"Recheck the original task, source rows, filter and aggregation. The result shape is correct; at least one value or row order differs."});
|
|
26
|
-
console.log(JSON.stringify({ok:true}));
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F2 delayed recall across three harnesses behind one metered gateway.
|
|
3
|
-
*
|
|
4
|
-
* Each page is deleted when its stage ends, so a later question can only be
|
|
5
|
-
* answered from what the harness itself retained. The arms differ in exactly
|
|
6
|
-
* that: xeno keeps a lossless log with a `history` reader; the ablation keeps
|
|
7
|
-
* the same windowed projection without the reader; Pi keeps its own compaction
|
|
8
|
-
* summary. See docs/tasks/bench-recall-runner.md.
|
|
9
|
-
*/
|
|
10
|
-
import { appendFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
11
|
-
import { dirname, join, relative } from "node:path";
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
13
|
-
import { recallCodes, recallPrompt, verifyRecallAnswer, type RecallFixture, type RecallKind, type RecallVerdict } from "../core/recall-trial.ts";
|
|
14
|
-
import type { BenchmarkPrice, BenchmarkUsage } from "../core/benchmark.ts";
|
|
15
|
-
import { Session } from "./loop.ts";
|
|
16
|
-
import { OpenAICompletionsProvider } from "./openai.ts";
|
|
17
|
-
import type { ProviderConfig, ToolSchema } from "./provider.ts";
|
|
18
|
-
import { COMPARISON_SYSTEM, COMPARISON_TOOLS, comparisonGateway, runPiPrompt, type ComparisonBackend, type ComparisonLedger, type GatewayRecord, type PiIdentity } from "./harness-comparison.ts";
|
|
19
|
-
import { DEFAULT_TOOL_CONTEXT, TOOL_SCHEMAS, runTool } from "./tools.ts";
|
|
20
|
-
|
|
21
|
-
export const RECALL_ARMS = ["xeno", "xeno-nohistory", "pi"] as const;
|
|
22
|
-
export type RecallArm = typeof RECALL_ARMS[number];
|
|
23
|
-
|
|
24
|
-
/** Native memory tools are the independent variable; everything else is shared. */
|
|
25
|
-
export function recallToolNames(arm: RecallArm): readonly string[] {
|
|
26
|
-
return arm === "xeno" ? [...COMPARISON_TOOLS, "history", "note"] : COMPARISON_TOOLS;
|
|
27
|
-
}
|
|
28
|
-
export const recallTools = (arm: RecallArm): ToolSchema[] =>
|
|
29
|
-
recallToolNames(arm).map((name) => TOOL_SCHEMAS.find((tool) => tool.name === name)!);
|
|
30
|
-
|
|
31
|
-
export interface RecallStageResult {
|
|
32
|
-
stage: number; kind: RecallKind; source: number; distance: number;
|
|
33
|
-
/** A context boundary (xeno cut / Pi compaction) separated the source page from the answer. */
|
|
34
|
-
crossed: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* The expected code was in the first request of this stage. A crossed
|
|
37
|
-
* boundary does not imply loss (xeno retains recent exchanges, a Pi summary
|
|
38
|
-
* may copy a code); this measures loss on the wire, identically per arm.
|
|
39
|
-
*/
|
|
40
|
-
visibleAtAsk: boolean | null;
|
|
41
|
-
/** Visible in every task request of the stage; a mid-stage boundary can evict it after the ask. Null for recent. */
|
|
42
|
-
visibleThroughout: boolean | null;
|
|
43
|
-
status: "answered" | "stopped" | "error"; reason: string; verdict: RecallVerdict;
|
|
44
|
-
requests: number; pageReads: number; historyCalls: number; boundariesAfter: number;
|
|
45
|
-
}
|
|
46
|
-
export interface RecallTrialResult {
|
|
47
|
-
arm: RecallArm; planned: number; unrun: number; stages: RecallStageResult[];
|
|
48
|
-
toolNames: string[]; boundaries: number; requests: number;
|
|
49
|
-
knownCost: number; reservedUnknown: number; usage: BenchmarkUsage; peakPrompt: number;
|
|
50
|
-
/** Files other than answers the model created: its own notes, reported separately. */
|
|
51
|
-
modelFiles: string[];
|
|
52
|
-
/** Workspace copies of deleted pages; must be empty for the stage to count as observed-once. */
|
|
53
|
-
leakedPages: string[];
|
|
54
|
-
stopped: "budget" | "balance" | "error" | "limit" | null; wallMs: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const sha = (body: string) => createHash("sha256").update(body).digest("hex");
|
|
58
|
-
|
|
59
|
-
async function listFiles(root: string): Promise<string[]> {
|
|
60
|
-
const out: string[] = [];
|
|
61
|
-
for (const entry of await readdir(root, { withFileTypes: true, recursive: true })) {
|
|
62
|
-
if (entry.isFile()) out.push(relative(root, join(entry.parentPath, entry.name)));
|
|
63
|
-
}
|
|
64
|
-
return out.sort();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function runRecallTrial(options: {
|
|
68
|
-
directory: string; fixture: RecallFixture; arm: RecallArm;
|
|
69
|
-
config: ProviderConfig; price: BenchmarkPrice; ledger: ComparisonLedger; backend: ComparisonBackend;
|
|
70
|
-
maxTurnsPerStage: number; stageTimeoutMs: number;
|
|
71
|
-
pi: PiIdentity | null; extension: string;
|
|
72
|
-
/** Synthetic pressure only; live trials keep Pi's default compaction. */
|
|
73
|
-
piCompaction?: { reserveTokens: number; keepRecentTokens: number };
|
|
74
|
-
onProgress?: (event: Record<string, unknown>) => Promise<void>;
|
|
75
|
-
}): Promise<RecallTrialResult> {
|
|
76
|
-
const { fixture, arm } = options, started = performance.now();
|
|
77
|
-
const workspace = join(options.directory, "workspace"), outputStore = join(options.directory, "observations");
|
|
78
|
-
await mkdir(workspace, { recursive: true }); await mkdir(outputStore, { recursive: true });
|
|
79
|
-
const tools = recallTools(arm), allow = recallToolNames(arm);
|
|
80
|
-
const result: RecallTrialResult = { arm, planned: fixture.pages.length, unrun: fixture.pages.length, stages: [],
|
|
81
|
-
toolNames: [...allow], boundaries: 0, requests: 0, knownCost: 0, reservedUnknown: 0,
|
|
82
|
-
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 }, peakPrompt: 0,
|
|
83
|
-
modelFiles: [], leakedPages: [], stopped: null, wallMs: 0 };
|
|
84
|
-
let stage = 0, pageReads = 0, historyCalls = 0, stageTaskRequests = 0;
|
|
85
|
-
let seen: boolean[] = [];
|
|
86
|
-
const page = () => fixture.pages[stage]!;
|
|
87
|
-
const countCall = (name: string, input: unknown) => {
|
|
88
|
-
if (name === "history") historyCalls++;
|
|
89
|
-
if (name === "read" && input !== null && typeof input === "object" && (input as Record<string, unknown>)["path"] === page().path) pageReads++;
|
|
90
|
-
};
|
|
91
|
-
const toolContext = { ...DEFAULT_TOOL_CONTEXT, root: workspace, allow, outputStore };
|
|
92
|
-
const gateway = comparisonGateway({ config: options.config, price: options.price, ledger: options.ledger, tools,
|
|
93
|
-
// Pi's summary calls share this ceiling; the slack keeps them from starving task turns.
|
|
94
|
-
maxRequests: (options.maxTurnsPerStage + 2) * fixture.pages.length,
|
|
95
|
-
// The same per-stage cap for every arm: Session enforces maxTurns itself,
|
|
96
|
-
// Pi has no such flag, so the gateway refuses its excess task requests.
|
|
97
|
-
admit: (hasTools) => !hasTools || stageTaskRequests++ < options.maxTurnsPerStage,
|
|
98
|
-
backend: (payload, signal) => {
|
|
99
|
-
// Summary calls carry no tools; only a task turn shows what the model sees.
|
|
100
|
-
if (Array.isArray(payload["tools"])) seen.push(JSON.stringify(payload["messages"]).replaceAll("\\", "").includes(page().question.expected));
|
|
101
|
-
return options.backend(payload, signal);
|
|
102
|
-
},
|
|
103
|
-
executeTool: (name, input) => { countCall(name, input); return runTool(toolContext, name, input); },
|
|
104
|
-
record: async (record: GatewayRecord) => {
|
|
105
|
-
await appendFile(join(options.directory, "requests.jsonl"), JSON.stringify({ stage, ...record }) + "\n");
|
|
106
|
-
if (record.kind === "settled") {
|
|
107
|
-
result.requests++;
|
|
108
|
-
if (record.measured === null) result.reservedUnknown += record.charge; else result.knownCost += record.measured;
|
|
109
|
-
if (record.usage) {
|
|
110
|
-
for (const key of Object.keys(result.usage) as (keyof BenchmarkUsage)[]) result.usage[key] += record.usage[key];
|
|
111
|
-
result.peakPrompt = Math.max(result.peakPrompt, record.usage.input_tokens + record.usage.cache_read_input_tokens);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
await options.onProgress?.({ phase: record.kind, arm, stage, request: record.request, measured: record.measured, failure: record.failure });
|
|
115
|
-
} });
|
|
116
|
-
const config = { ...options.config, spec: { ...options.config.spec, baseUrl: gateway.url } };
|
|
117
|
-
const codes = recallCodes(fixture), boundaryAfter: number[] = [];
|
|
118
|
-
let session: Session | null = null;
|
|
119
|
-
try {
|
|
120
|
-
if (arm !== "pi") {
|
|
121
|
-
await mkdir(join(options.directory, "log"));
|
|
122
|
-
const provider = new OpenAICompletionsProvider(config, gateway.token, { retry: { maxRetries: 0, maxRetryDelayMs: 0 } });
|
|
123
|
-
session = new Session({ logDir: join(options.directory, "log"), frozen: { system: COMPARISON_SYSTEM, tools },
|
|
124
|
-
tools: toolContext, contextPolicy: "windowed", maxTurns: options.maxTurnsPerStage, harnessReviews: false }, provider);
|
|
125
|
-
await session.start();
|
|
126
|
-
} else if (!options.pi) throw new Error("Pi identity required");
|
|
127
|
-
for (const current of fixture.pages) {
|
|
128
|
-
if (options.ledger.halted || gateway.stopped) break;
|
|
129
|
-
stage = current.stage; pageReads = 0; historyCalls = 0; seen = []; stageTaskRequests = 0;
|
|
130
|
-
const file = join(workspace, current.path);
|
|
131
|
-
await mkdir(dirname(file), { recursive: true }); await mkdir(join(workspace, "answers"), { recursive: true });
|
|
132
|
-
await writeFile(file, current.body, { flag: "wx" });
|
|
133
|
-
const requestsBefore = result.requests, signal = AbortSignal.timeout(options.stageTimeoutMs);
|
|
134
|
-
let status: RecallStageResult["status"] = "stopped", reason = "no completion";
|
|
135
|
-
try {
|
|
136
|
-
if (session) {
|
|
137
|
-
for await (const event of session.send(recallPrompt(fixture, stage), signal)) {
|
|
138
|
-
if (event.kind === "cut") result.boundaries++;
|
|
139
|
-
if (event.kind === "tool_call" && event.tool) countCall(event.tool.name, event.tool.input);
|
|
140
|
-
if (event.kind === "done") reason = event.reason ?? "unknown completion";
|
|
141
|
-
}
|
|
142
|
-
if (["stop", "end_turn"].includes(reason)) status = "answered";
|
|
143
|
-
} else {
|
|
144
|
-
const run = await runPiPrompt({ directory: options.directory, workspace, prompt: recallPrompt(fixture, stage), config,
|
|
145
|
-
gateway, price: options.price, pi: options.pi!, extension: options.extension, signal, resume: stage > 0, tools,
|
|
146
|
-
...(options.piCompaction ? { piCompaction: options.piCompaction } : {}) });
|
|
147
|
-
result.boundaries += run.cuts; reason = run.reason; status = run.answered ? "answered" : "error";
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
status = "error"; reason = signal.aborted ? "stage deadline" : gateway.stopped ? `model gateway: ${gateway.stopped}` : "stage failed";
|
|
151
|
-
}
|
|
152
|
-
boundaryAfter[stage] = result.boundaries;
|
|
153
|
-
const answer = await readFile(join(workspace, current.answerPath), "utf8").catch(() => null);
|
|
154
|
-
const verdict = verifyRecallAnswer(answer, current.question, codes);
|
|
155
|
-
await rm(file);
|
|
156
|
-
const { question } = current;
|
|
157
|
-
const row: RecallStageResult = { stage, kind: question.kind, source: question.source, distance: stage - question.source,
|
|
158
|
-
crossed: question.source < stage && result.boundaries > boundaryAfter[question.source]!,
|
|
159
|
-
visibleAtAsk: question.source === stage || seen.length === 0 ? null : seen[0]!,
|
|
160
|
-
visibleThroughout: question.source === stage || seen.length === 0 ? null : seen.every(Boolean),
|
|
161
|
-
status, reason, verdict, requests: result.requests - requestsBefore, pageReads, historyCalls, boundariesAfter: result.boundaries };
|
|
162
|
-
result.stages.push(row); result.unrun = result.planned - result.stages.length;
|
|
163
|
-
await writeFile(join(options.directory, `stage-${String(stage).padStart(3, "0")}.json`),
|
|
164
|
-
JSON.stringify({ ...row, answer }, null, 2) + "\n", { flag: "wx" });
|
|
165
|
-
await options.onProgress?.({ phase: "stage", arm, stage, verdict: verdict.reason, crossed: row.crossed, boundaries: result.boundaries });
|
|
166
|
-
}
|
|
167
|
-
} finally { await gateway.close(); }
|
|
168
|
-
const files = await listFiles(workspace), bodies = new Set(fixture.pages.map((p) => sha(p.body)));
|
|
169
|
-
result.modelFiles = files.filter((path) => !path.startsWith("answers/") && !path.startsWith("incoming/"));
|
|
170
|
-
for (const path of files) if (bodies.has(sha(await readFile(join(workspace, path), "utf8")))) result.leakedPages.push(path);
|
|
171
|
-
result.stopped = gateway.stopped; result.wallMs = performance.now() - started;
|
|
172
|
-
await writeFile(join(options.directory, "result.json"), JSON.stringify(result, null, 2) + "\n", { flag: "wx" });
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Mechanical backend: answers only from what the request actually shows.
|
|
178
|
-
*
|
|
179
|
-
* It cannot remember across requests, so a code it finds proves the
|
|
180
|
-
* information was in that projection (or recovered by history). Its summary
|
|
181
|
-
* text for Pi is invented here, which is why nothing it produces is a score.
|
|
182
|
-
*/
|
|
183
|
-
export function recallMechanicalBackend(): ComparisonBackend {
|
|
184
|
-
let id = 0;
|
|
185
|
-
return async (payload) => {
|
|
186
|
-
const messages = payload["messages"] as Record<string, unknown>[], tools = payload["tools"] as { function: { name: string } }[] | undefined;
|
|
187
|
-
const reply = (content: string | null, call?: { name: string; input: unknown }) => ({
|
|
188
|
-
id: `recall-mock-${++id}`, object: "chat.completion", created: 1, model: "recall-mechanical",
|
|
189
|
-
choices: [{ index: 0, finish_reason: call ? "tool_calls" : "stop", message: { role: "assistant", content,
|
|
190
|
-
...(call ? { tool_calls: [{ id: `call-${id}`, type: "function", function: { name: call.name, arguments: JSON.stringify(call.input) } }] } : {}) } }],
|
|
191
|
-
usage: { prompt_tokens: Math.ceil(JSON.stringify([tools ?? [], messages]).length / 3.6), completion_tokens: 64, prompt_cache_hit_tokens: 0 },
|
|
192
|
-
});
|
|
193
|
-
if (!tools) return reply("Checkpoint: staged pages processed and answers written.");
|
|
194
|
-
const text = (m: Record<string, unknown>) => typeof m["content"] === "string" ? m["content"] : JSON.stringify(m["content"] ?? "");
|
|
195
|
-
const at = messages.findLastIndex((m) => m["role"] === "user" && /Stage \d+: read /.test(text(m)));
|
|
196
|
-
// After a cut the current request can sit inside a handoff that also quotes
|
|
197
|
-
// earlier stages; the latest quoted stage is the active one.
|
|
198
|
-
const asks = [...text(messages[at]!).matchAll(/read (\S+) in full\. Question: what is the current code of ticket (T-\d{5})\? Write only the code to (\S+),/g)];
|
|
199
|
-
const [, path, ticket, answerPath] = asks.at(-1)!;
|
|
200
|
-
const calls = messages.slice(at + 1).flatMap((m) => (m["tool_calls"] as { function: { name: string; arguments: string } }[] | undefined) ?? [])
|
|
201
|
-
.map((c) => ({ name: c.function.name, input: JSON.parse(c.function.arguments) as Record<string, unknown> }));
|
|
202
|
-
if (calls.some((c) => c.name === "write")) return reply("Stage done.");
|
|
203
|
-
if (!calls.some((c) => c.name === "read" && c.input["path"] === path)) return reply(null, { name: "read", input: { path } });
|
|
204
|
-
// Backslashes vanish so nested JSON (history results) matches the same way.
|
|
205
|
-
const visible = JSON.stringify(messages).replaceAll("\\", "");
|
|
206
|
-
const pattern = new RegExp(`"ticket":"${ticket}","code":"(K-[0-9a-f]{12})"`, "g");
|
|
207
|
-
// Search results are newest first, so the first hit there is the latest code;
|
|
208
|
-
// in the conversation itself the last occurrence is the latest.
|
|
209
|
-
const search = messages.slice(at + 1).findLast((m) => m["role"] === "tool" && text(m).includes('"matches"'));
|
|
210
|
-
const found = (search ? [...text(search).replaceAll("\\", "").matchAll(pattern)][0]?.[1] : undefined)
|
|
211
|
-
?? [...visible.matchAll(pattern)].at(-1)?.[1];
|
|
212
|
-
if (found) return reply(null, { name: "write", input: { path: answerPath, content: found } });
|
|
213
|
-
const names = new Set(tools.map((t) => t.function.name));
|
|
214
|
-
const history = calls.filter((c) => c.name === "history");
|
|
215
|
-
if (names.has("history") && history.length === 0) return reply(null, { name: "history", input: { query: ticket } });
|
|
216
|
-
if (names.has("history") && history.length === 1) {
|
|
217
|
-
// Pick an observation that read a page; the question prompt also matches the ticket.
|
|
218
|
-
const result = messages.slice(at + 1).findLast((m) => m["role"] === "tool" && text(m).includes('"matches"'));
|
|
219
|
-
let items: { event: string; tool?: string }[] = [];
|
|
220
|
-
try { items = (JSON.parse(text(result ?? {})) as { items: typeof items }).items; } catch { /* not a search page */ }
|
|
221
|
-
const event = items.find((item) => item.tool === "read")?.event;
|
|
222
|
-
if (event) return reply(null, { name: "history", input: { event } });
|
|
223
|
-
}
|
|
224
|
-
return reply(null, { name: "write", input: { path: answerPath, content: "UNKNOWN" } });
|
|
225
|
-
};
|
|
226
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/** R12c: collect all started trials, including crash prefixes, without provider access. */
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { requestAccounting, summarizeRecoveryCoverage, type TrialCoverage } from "../core/evaluation-coverage.ts";
|
|
4
|
-
import { deriveRecoveryMetrics } from "../core/recovery-evaluation.ts";
|
|
5
|
-
import type { EvalResult } from "../core/experiment.ts";
|
|
6
|
-
import { isTrialLifecycle } from "../core/trial-lifecycle.ts";
|
|
7
|
-
import { readCursor } from "../store/log.ts";
|
|
8
|
-
import { readNodeHistory } from "../store/restore.ts";
|
|
9
|
-
import { auditEvalJob, auditEvalRequests, evalDirectory, type EvalPlan, type EvalEnd } from "./harness-eval.ts";
|
|
10
|
-
import type { GatewayRecord } from "./harness-comparison.ts";
|
|
11
|
-
import { verifiedRecoveryRetrievals } from "./recovery-evidence.ts";
|
|
12
|
-
|
|
13
|
-
export function requestPrefix(body: string): { records: GatewayRecord[]; tail: "complete" | "truncated" } {
|
|
14
|
-
const chunks = body.split("\n"), records: GatewayRecord[] = [];
|
|
15
|
-
let tail: "complete" | "truncated" = "complete";
|
|
16
|
-
for (const [index, line] of chunks.entries()) {
|
|
17
|
-
if (!line && index === chunks.length - 1) continue;
|
|
18
|
-
try { records.push(JSON.parse(line) as GatewayRecord); }
|
|
19
|
-
catch {
|
|
20
|
-
if (index !== chunks.length - 1) throw Error("corrupt request ledger interior");
|
|
21
|
-
tail = "truncated";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return { records, tail };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function inspectRecoveryTrials(output: string, plan: EvalPlan, audit = false) {
|
|
28
|
-
if (![2, 5].includes(plan.version)) throw Error("complete coverage needs protocol v2 or v5");
|
|
29
|
-
const trials: TrialCoverage[] = [], results: EvalResult[] = [];
|
|
30
|
-
let charged = 0, halted = false;
|
|
31
|
-
for (const job of plan.jobs) {
|
|
32
|
-
const directory = evalDirectory(output, job), startPath = join(directory, "trial-start.json");
|
|
33
|
-
const requestsPath = join(directory, "requests.jsonl"), resultPath = join(directory, "result.json"), endPath = join(directory, "end.json");
|
|
34
|
-
if (!await Bun.file(startPath).exists()) {
|
|
35
|
-
if (await Bun.file(requestsPath).exists() || await Bun.file(resultPath).exists() || await Bun.file(endPath).exists()) throw Error("trial evidence lacks start identity");
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
const start = await Bun.file(startPath).json() as { job: string; ledgerBefore: number };
|
|
39
|
-
if (start.job !== job.id || start.ledgerBefore !== charged || halted) throw Error("global recovery ledger sequence changed");
|
|
40
|
-
const prefix = requestPrefix(await Bun.file(requestsPath).exists() ? await Bun.file(requestsPath).text() : "");
|
|
41
|
-
const end = await Bun.file(endPath).exists() ? await Bun.file(endPath).json() as EvalEnd : undefined;
|
|
42
|
-
if (end?.lifecycle && !isTrialLifecycle(end.lifecycle)) throw Error("invalid recovery lifecycle");
|
|
43
|
-
const accounting = requestAccounting(prefix.records, start.ledgerBefore);
|
|
44
|
-
if (end && (end.ledgerBefore !== start.ledgerBefore || end.ledgerAfter !== accounting.charged || prefix.tail !== "complete")) throw Error("recovery end differs from request ledger");
|
|
45
|
-
let result: EvalResult | undefined;
|
|
46
|
-
if (await Bun.file(resultPath).exists()) {
|
|
47
|
-
if (!end || prefix.tail !== "complete" || accounting.unsettledRequests) throw Error("final result lacks settled source");
|
|
48
|
-
result = audit ? await auditEvalJob(directory, plan, job) : await Bun.file(resultPath).json() as EvalResult;
|
|
49
|
-
if (result.id !== job.id) throw Error("result identity changed");
|
|
50
|
-
results.push(result);
|
|
51
|
-
} else if (audit) await auditEvalRequests(directory, plan, job, { records: prefix.records, ledgerBefore: start.ledgerBefore, allowPending: true });
|
|
52
|
-
let partialMetrics: EvalResult["recovery"];
|
|
53
|
-
// During request callbacks the cursor can be behind the active append; only
|
|
54
|
-
// an offline audit reads incomplete tool metrics and labels its outcome unknown.
|
|
55
|
-
if (audit && !result) {
|
|
56
|
-
const cursor = await readCursor(join(directory, "log"));
|
|
57
|
-
if (cursor) {
|
|
58
|
-
const history = await readNodeHistory(join(directory, "log"), cursor.node, undefined, "context");
|
|
59
|
-
if (!history.ok) throw Error(history.error);
|
|
60
|
-
const originals = await verifiedRecoveryRetrievals(directory, history.nodes, plan.limits.maxOutputBytes!);
|
|
61
|
-
partialMetrics = deriveRecoveryMetrics(history.nodes, new Set(originals.keys()), null, path => resolve(directory, "workspace", path));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
trials.push({ id: job.id, accounting, tail: prefix.tail, ...(result ? { result } : {}),
|
|
65
|
-
...(partialMetrics ? { partialMetrics } : {}), interruption: end?.lifecycle?.interruption?.source ?? null });
|
|
66
|
-
charged = accounting.charged;
|
|
67
|
-
halted = end === undefined || end.ledgerHalted === true || !accounting.fullyMeasured || prefix.tail === "truncated" ||
|
|
68
|
-
prefix.records.some(row => row.kind === "settled" && (row.failure !== null || row.measured !== null && row.measured > row.reservation));
|
|
69
|
-
}
|
|
70
|
-
return { trials, results, ledger: { budget: plan.budget, charged, halted }, summary: summarizeRecoveryCoverage(plan.jobs, trials, plan.mechanicalOnly) };
|
|
71
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** R12c: offline evidence readers verify source bytes and never rerun commands. */
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import type { LogEvent } from "../core/events.ts";
|
|
5
|
-
import { historyEntries } from "../core/history-query.ts";
|
|
6
|
-
import { readObservation } from "../store/observations.ts";
|
|
7
|
-
import { isRuntimeTargetReceipt } from "../core/runtime-target.ts";
|
|
8
|
-
|
|
9
|
-
export function recoveryWorkEvents(events: readonly LogEvent[], last: string | undefined): LogEvent[] {
|
|
10
|
-
const at = events.findIndex(event => event.id === last);
|
|
11
|
-
if (at < 0 || events.slice(at + 1).some(event => !["process_exit", "target_observation"].includes(event.t)))
|
|
12
|
-
throw Error("invalid recovery work boundary");
|
|
13
|
-
return events.slice(0, at + 1);
|
|
14
|
-
}
|
|
15
|
-
const equal = (left: unknown, right: unknown) => JSON.stringify(left) === JSON.stringify(right);
|
|
16
|
-
const originalFields = (entry: unknown) => {
|
|
17
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
18
|
-
// Consumption-time freshness is metadata around the immutable event, not
|
|
19
|
-
// part of the original observation whose bytes this metric verifies.
|
|
20
|
-
const { environment: _freshness, ...original } = entry as Record<string, unknown>;
|
|
21
|
-
return original;
|
|
22
|
-
};
|
|
23
|
-
export async function verifiedRecoveryRetrievals(directory: string, nodes: readonly { node: number; events: readonly LogEvent[] }[], maxOutputBytes: number) {
|
|
24
|
-
const result = new Map<string, { source: string; text: string }>();
|
|
25
|
-
const calls = new Map<string, { name: string; input: unknown }>();
|
|
26
|
-
const prefix: { node: number; events: LogEvent[] }[] = [];
|
|
27
|
-
for (const part of nodes) {
|
|
28
|
-
const seen: LogEvent[] = []; prefix.push({ node: part.node, events: seen });
|
|
29
|
-
for (const event of part.events) {
|
|
30
|
-
if (event.t === "assistant") for (const block of event.blocks) if (block.type === "tool_use") calls.set(`${part.node}:${block.call_id}`, block);
|
|
31
|
-
if (event.t === "tool_invocation") calls.set(`${part.node}:${event.call_id}`, { name: event.name, input: event.input ?? calls.get(`${part.node}:${event.call_id}`)?.input });
|
|
32
|
-
if (event.t === "tool_result" && event.ok) {
|
|
33
|
-
const call = calls.get(`${event.invocation_ref?.node ?? part.node}:${event.call_id}`);
|
|
34
|
-
const input = call?.input as Record<string, unknown> | undefined;
|
|
35
|
-
if (call?.name === "history" && input) {
|
|
36
|
-
if (typeof input["artifact"] === "string") {
|
|
37
|
-
const page = await readObservation(join(directory, "observations"), input["artifact"], Number(input["offset"] ?? 0), Math.max(1, Math.floor(maxOutputBytes / 3)));
|
|
38
|
-
// Only bytes actually delivered to the model count; an archived but
|
|
39
|
-
// clipped response still requires another retrieval by the agent.
|
|
40
|
-
let delivered: unknown;
|
|
41
|
-
try { delivered = JSON.parse(event.content); }
|
|
42
|
-
catch {
|
|
43
|
-
// JSON escaping can make even a bounded byte page exceed the
|
|
44
|
-
// preview. Verify its archive, but do not credit unseen bytes.
|
|
45
|
-
if (!event.content_ref) throw Error("unreadable recovery artifact receipt");
|
|
46
|
-
const stored = await Bun.file(event.content_ref.path).text();
|
|
47
|
-
if (createHash("sha256").update(stored).digest("hex") !== event.content_ref.sha256 || !equal(JSON.parse(stored), page))
|
|
48
|
-
throw Error("recovery artifact archive differs from original bytes");
|
|
49
|
-
}
|
|
50
|
-
if (delivered !== undefined && !equal(delivered, page)) throw Error("recovery artifact receipt differs from original bytes");
|
|
51
|
-
if (delivered !== undefined && page.base64) result.set(`${part.node}:${event.id}`, { source: page.sha256, text: page.text });
|
|
52
|
-
} else if (typeof input["event"] === "string" || Array.isArray(input["events"])) {
|
|
53
|
-
const entries = historyEntries(prefix), ids = typeof input["event"] === "string" ? [input["event"]] : input["events"] as unknown[];
|
|
54
|
-
const originals = ids.map(id => entries.find(entry => entry.event === id));
|
|
55
|
-
let delivered: unknown;
|
|
56
|
-
try { delivered = JSON.parse(event.content); } catch { /* a clipped index is not a complete original event */ }
|
|
57
|
-
const batch = delivered as { items?: unknown[]; missing?: unknown[] } | undefined;
|
|
58
|
-
const actual = typeof input["event"] === "string" ? [delivered] : Array.isArray(batch?.items) && equal(batch?.missing, []) ? batch.items : [];
|
|
59
|
-
if (originals.every(Boolean) && equal(actual.map(originalFields), originals)) result.set(`${part.node}:${event.id}`,
|
|
60
|
-
{ source: originals.map(entry => entry!.event).join(","), text: originals.map(entry => entry!.text).join("\n") });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (event.t === "target_observation" && event.observation.state === "ready") {
|
|
65
|
-
const receipt = event.observation;
|
|
66
|
-
if (!isRuntimeTargetReceipt(receipt) || !receipt.manifest) throw Error("invalid target receipt");
|
|
67
|
-
const body = await Bun.file(receipt.manifest.path).text();
|
|
68
|
-
if (createHash("sha256").update(body).digest("hex") !== receipt.manifest.sha256) throw Error("target observation source changed");
|
|
69
|
-
const archived = JSON.parse(body) as { observation: unknown }, { manifest: _manifest, ...original } = receipt;
|
|
70
|
-
if (!equal(archived.observation, original)) throw Error("target observation differs from archived capture");
|
|
71
|
-
}
|
|
72
|
-
seen.push(event);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return result;
|
|
76
|
-
}
|