@visual-z/xeno 0.2.1 → 0.4.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/CHANGELOG.md +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** R12c: sanitized, executable analogues of the audit's failure mechanisms. */
|
|
2
|
+
import {createHash} from "node:crypto";
|
|
3
|
+
import {seededRandom} from "../core/experiment.ts";
|
|
4
|
+
import {recoveryCase,RECOVERY_TASKS,RECOVERY_SOURCE_REPORT,type RecoveryTask} from "../core/recovery-corpus.ts";
|
|
5
|
+
import type {AcceptanceContract} from "../core/acceptance.ts";
|
|
6
|
+
import type {EvalFixture,EvalPlan} from "./harness-eval.ts";
|
|
7
|
+
import type {ComparisonBackend} from "./harness-comparison.ts";
|
|
8
|
+
|
|
9
|
+
const digest=(text:string)=>createHash("sha256").update(text).digest("hex");
|
|
10
|
+
export const RECOVERY_SYSTEM="Solve the supplied recovery task using actual file and tool evidence. Preserve the fixture and verifier inputs. " +
|
|
11
|
+
"Inspect failed-call premises before retrying. A clipped preview is not the complete source. Recover closed process evidence without replaying its effects. " +
|
|
12
|
+
"Verify the requested artifacts with bun run gate. Building, observing a running target, and the user's acceptance are separate facts. " +
|
|
13
|
+
"Report unavailable capabilities or pending human review honestly. Work within this session without delegation.";
|
|
14
|
+
export function recoveryFixture(seed:number,task:RecoveryTask,repeat:number):EvalFixture{
|
|
15
|
+
const specification=recoveryCase(task)!,random=seededRandom(seed+RECOVERY_TASKS.indexOf(task)*1000+repeat);
|
|
16
|
+
const answer=Array.from({length:4},()=>Math.floor(random()*0x100000000).toString(16).padStart(8,"0")).join("")+"\n";
|
|
17
|
+
const files:Record<string,string>={"source.txt":`CODE=${answer}`,"package.json":JSON.stringify({scripts:{gate:"bun verify.ts",build:"bun build.ts"}})+"\n"};
|
|
18
|
+
const goals:Record<string,string>={"answer.txt":digest(answer)},mutable=["answer.txt"];
|
|
19
|
+
let scenario="Copy the CODE value from source.txt to answer.txt with one trailing newline. ";
|
|
20
|
+
if(task==="recovery-missing-path"){
|
|
21
|
+
files["answer.txt"]="pending\n";scenario+="Replace pending in answer.txt; an earlier editor request omitted path. Use the actual file identity. ";
|
|
22
|
+
}else if(task==="recovery-stale-version"){
|
|
23
|
+
files["draft.txt"]="before\nexternal change\n";mutable.push("draft.txt");goals["draft.txt"]=digest("after\nexternal change\n");
|
|
24
|
+
scenario+=`Change before to after in draft.txt while retaining its other line. An old observation had sha256 ${digest("before\n")}; it may be stale. `;
|
|
25
|
+
}else if(task==="recovery-long-output"){
|
|
26
|
+
files["source.txt"]="x".repeat(5000)+`CODE=${answer.trim()}`+"y".repeat(10000)+"\n";
|
|
27
|
+
scenario="Recover the 32 hexadecimal characters after CODE= in source.txt and put them in answer.txt with one trailing newline. The line exceeds a preview; use immutable original evidence when needed. ";
|
|
28
|
+
}else if(task==="recovery-api-mismatch"){
|
|
29
|
+
files["compat.ts"]='// Portable legacy API fixture; the optional filter flag is unsupported.\n'+
|
|
30
|
+
'if(Bun.argv.some(arg=>arg.startsWith("--filter")))throw new TypeError("extract() does not support the filter option");\n'+
|
|
31
|
+
'const source=await Bun.file("source.txt").text();await Bun.write("answer.txt",source.slice(5));\n';
|
|
32
|
+
scenario+="The local compat.ts implements a legacy API; a suggested invocation was bun compat.ts --filter=safe. Inspect what this installed implementation supports before using it. ";
|
|
33
|
+
}else if(task==="recovery-closed-process"){
|
|
34
|
+
files["producer.ts"]='const count=Number(await Bun.file("counter.txt").text().catch(()=>"0"));await Bun.write("counter.txt",String(count+1));console.log((await Bun.file("source.txt").text()).trim());\n';
|
|
35
|
+
goals["counter.txt"]=digest("1");mutable.push("counter.txt");
|
|
36
|
+
scenario="Run bun producer.ts exactly once to obtain CODE. It increments a counter and exits. Recover its terminal receipt if further process input fails; do not launch it again. Put CODE in answer.txt with one newline. ";
|
|
37
|
+
}else if(task==="recovery-unchanged-retry"){
|
|
38
|
+
files["draft.txt"]="present\n";mutable.push("draft.txt");goals["draft.txt"]=digest("fixed\n");
|
|
39
|
+
scenario+="Change the actual draft.txt anchor to fixed. A proposed old_string was absent; changing only new_string would not repair a missing anchor. ";
|
|
40
|
+
}else if(task==="recovery-runtime-acceptance"){
|
|
41
|
+
files["app-source.js"]='console.log("fixture preview ready");setInterval(()=>{},1000);\n';
|
|
42
|
+
files["build.ts"]='import {mkdir,copyFile} from "node:fs/promises";await mkdir("dist",{recursive:true});await copyFile("app-source.js","dist/app.js");\n';
|
|
43
|
+
goals["dist/app.js"]=digest(files["app-source.js"]);
|
|
44
|
+
scenario+="Build with bun run build, recording artifacts {paths:[\"dist/app.js\"],output_roots:[\"dist\"]}, and start the declared preview with target. A previous build or a missing target does not prove this instance is ready. Leave its human review pending. The harness closes the temporary preview after this trial. ";
|
|
45
|
+
}else if(task==="recovery-eof")scenario+="A suggested read starts at line 999; recover from the real file boundary rather than assuming that line exists. ";
|
|
46
|
+
files["verify.ts"]=`import {createHash} from "node:crypto";const goals=${JSON.stringify(goals)};for(const [path,sha] of Object.entries(goals)){const text=await Bun.file(path).text().catch(()=>"");if(createHash("sha256").update(text).digest("hex")!==sha)process.exit(1);}\n`;
|
|
47
|
+
return {task,repeat,files,digests:Object.fromEntries(Object.entries(files).filter(([path])=>!mutable.includes(path)).map(([path,text])=>[path,digest(text)])),
|
|
48
|
+
answerSha256:digest(answer),prompts:[scenario+"Preserve verifier and fixture inputs, run bun run gate, and report the actual acceptance state."],
|
|
49
|
+
recovery:{sourceReport:RECOVERY_SOURCE_REPORT,specification,answer,goalDigests:goals,mutableFiles:mutable}};
|
|
50
|
+
}
|
|
51
|
+
export function recoveryContract(plan:EvalPlan):AcceptanceContract{
|
|
52
|
+
if(!plan.runtime.bunExecutable)throw Error("recovery runtime identity missing");
|
|
53
|
+
return {version:1,id:"recovery-preview",criteria:[
|
|
54
|
+
{id:"gate",kind:"command",command:"bun run gate",description:"The requested outputs pass the frozen verifier"},
|
|
55
|
+
{id:"build",kind:"command",command:"bun run build",description:"The current preview is built"},
|
|
56
|
+
{id:"review",kind:"human",target:"preview",description:"The author reviews this exact running preview"}],
|
|
57
|
+
targets:[{id:"preview",build:"build",artifacts:{paths:["dist/app.js"],output_roots:["dist"]},entry:"dist/app.js",interpreter:plan.runtime.bunExecutable}]};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A deterministic driver tests the protocol and grader, not model competence. */
|
|
61
|
+
export function recoveryFixtureBackend(input:EvalFixture):ComparisonBackend{
|
|
62
|
+
if(!input.recovery)throw Error("recovery fixture metadata missing");
|
|
63
|
+
const answer=input.recovery.answer;let step=0;
|
|
64
|
+
return async payload=>{
|
|
65
|
+
const messages=payload["messages"] as {role:string;content?:unknown}[];
|
|
66
|
+
const text=messages.filter(message=>message.role==="tool").map(message=>typeof message.content==="string"?message.content:JSON.stringify(message.content)).join("\n");
|
|
67
|
+
const required=(pattern:RegExp)=>{const match=pattern.exec(text);if(!match)throw Error("fixture driver could not find the original tool receipt");return match[1]!;};
|
|
68
|
+
const calls:{name:string;input:unknown}[]=[],index=step++;
|
|
69
|
+
const call=(name:string,args:unknown)=>calls.push({name,input:args});
|
|
70
|
+
const read=()=>call("read",{path:"source.txt"}),write=()=>call("write",{path:"answer.txt",content:answer}),gate=()=>call("exec_command",{cmd:"bun run gate",yield_time_ms:1000});
|
|
71
|
+
switch(input.task){
|
|
72
|
+
case "recovery-missing-path":
|
|
73
|
+
if(index===0)call("edit",{old_string:"pending",new_string:answer.trim()});
|
|
74
|
+
if(index===1){read();call("read",{path:"answer.txt"});}
|
|
75
|
+
if(index===2)call("edit",{path:"answer.txt",old_string:"pending",new_string:answer.trim()});
|
|
76
|
+
if(index===3)gate();break;
|
|
77
|
+
case "recovery-stale-version":
|
|
78
|
+
if(index===0)call("edit",{path:"draft.txt",expected_sha256:digest("before\n"),old_string:"before",new_string:"after"});
|
|
79
|
+
if(index===1)call("read",{path:"draft.txt"});
|
|
80
|
+
if(index===2)call("edit",{path:"draft.txt",old_string:"before",new_string:"after"});
|
|
81
|
+
if(index===3)read();if(index===4)write();if(index===5)gate();break;
|
|
82
|
+
case "recovery-long-output":
|
|
83
|
+
if(index===0)read();
|
|
84
|
+
if(index===1)call("history",{artifact:required(/sha256: ([a-f0-9]{64})/),offset:4500});
|
|
85
|
+
if(index===2)write();if(index===3)gate();break;
|
|
86
|
+
case "recovery-api-mismatch":
|
|
87
|
+
if(index===0)call("exec_command",{cmd:"bun compat.ts --filter=safe",yield_time_ms:1000});
|
|
88
|
+
if(index===1)call("read",{path:"compat.ts"});
|
|
89
|
+
if(index===2)call("exec_command",{cmd:"bun compat.ts",yield_time_ms:1000});
|
|
90
|
+
if(index===3)gate();break;
|
|
91
|
+
case "recovery-closed-process":
|
|
92
|
+
if(index===0)call("exec_command",{cmd:"bun producer.ts",yield_time_ms:5000});
|
|
93
|
+
if(index===1)call("history",{recent:true,tool:"exec_command"});
|
|
94
|
+
if(index===2||index===3)call("write_stdin",{session_id:Number(required(/"session_id"\s*:\s*(\d+)/)),chars:index===2?"repeat\n":"",yield_time_ms:100});
|
|
95
|
+
if(index===4)write();if(index===5)gate();break;
|
|
96
|
+
case "recovery-unchanged-retry":
|
|
97
|
+
if(index===0||index===3)call("read",{path:"draft.txt"});
|
|
98
|
+
if(index===1||index===2)call("edit",{path:"draft.txt",old_string:"absent",new_string:index===1?"first":"second"});
|
|
99
|
+
if(index===4)call("edit",{path:"draft.txt",old_string:"present",new_string:"fixed"});
|
|
100
|
+
if(index===5)read();if(index===6)write();if(index===7)gate();break;
|
|
101
|
+
case "recovery-runtime-acceptance":
|
|
102
|
+
if(index===0||index===4)call("target",{action:"start",target:"preview",yield_time_ms:100});
|
|
103
|
+
if(index===1)read();if(index===2)write();
|
|
104
|
+
if(index===3)call("exec_command",{cmd:"bun run build",artifacts:{paths:["dist/app.js"],output_roots:["dist"]},yield_time_ms:1000});
|
|
105
|
+
if(index===5)gate();break;
|
|
106
|
+
case "recovery-eof":
|
|
107
|
+
if(index===0)call("read",{path:"source.txt",start:999});
|
|
108
|
+
if(index===1)read();if(index===2)write();if(index===3)gate();break;
|
|
109
|
+
default:throw Error("not a recovery fixture");
|
|
110
|
+
}
|
|
111
|
+
return {id:`recovery-${index}`,model:"eval-mechanical",choices:[{index:0,message:{role:"assistant",
|
|
112
|
+
content:calls.length?null:input.task==="recovery-runtime-acceptance"?"Preview preparation ended; the author's acceptance is pending.":"Recovery fixture finished.",
|
|
113
|
+
...(calls.length?{tool_calls:calls.map((item,i)=>({id:`recovery-${index}-${i}`,type:"function",function:{name:item.name,arguments:JSON.stringify(item.input)}}))}:{})},
|
|
114
|
+
finish_reason:calls.length?"tool_calls":"stop"}],usage:{prompt_tokens:200,completion_tokens:32,prompt_cache_hit_tokens:0}};
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** R8: live capabilities stay with their Session; only observations cross a log boundary. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import type { LogEvent, ArtifactRef } from "../core/events.ts";
|
|
6
|
+
import { deriveAcceptance, type AcceptanceState } from "../core/acceptance.ts";
|
|
7
|
+
import { canonicalValue } from "../core/continuation.ts";
|
|
8
|
+
import { isCommandArtifactReceipt, sameArtifactIdentity, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
|
|
9
|
+
import { targetIsReady, type RuntimeTarget, type RuntimeTargetReceipt, type RuntimeInstance } from "../core/runtime-target.ts";
|
|
10
|
+
import { failureDiagnostic, ToolFault, NOT_STARTED, UNKNOWN_EFFECT } from "../core/tool-failure.ts";
|
|
11
|
+
import { commandTimeout } from "../core/command-contract.ts";
|
|
12
|
+
import { executionOutcome } from "../core/outcome.ts";
|
|
13
|
+
import { archiveCommandArtifacts, artifactExclusions, observeArtifactFiles, observeArtifactInputs } from "../store/command-artifacts.ts";
|
|
14
|
+
import { readObservation } from "../store/observations.ts";
|
|
15
|
+
import { targetDirectory, targetProgram, observeRuntimeIdentity } from "../store/runtime-target.ts";
|
|
16
|
+
import { findSession, startSession, pollSession } from "./exec-sessions.ts";
|
|
17
|
+
import { sessionResult, type ToolContext, type ToolResult } from "./tools.ts";
|
|
18
|
+
|
|
19
|
+
type History = readonly { node: number; events: readonly LogEvent[] }[];
|
|
20
|
+
type LiveTarget = { request: string; contract: string; build: string; instance: RuntimeInstance };
|
|
21
|
+
type Request = { action: "start" | "inspect"; target: string; session_id?: number; yield_time_ms?: number; timeout?: number };
|
|
22
|
+
// Assumed bound for the small build manifest; binaries are observed separately, never read from log JSON.
|
|
23
|
+
const MANIFEST_BYTES = 256 * 1024;
|
|
24
|
+
|
|
25
|
+
export class RuntimeTargets {
|
|
26
|
+
private readonly live = new Map<string, LiveTarget>();
|
|
27
|
+
|
|
28
|
+
async perform(ctx: ToolContext, history: History, input: Request): Promise<{ result: ToolResult; observation: RuntimeTargetReceipt }> {
|
|
29
|
+
const events = history.flatMap(part => [...part.events]);
|
|
30
|
+
const state = deriveAcceptance(undefined, events);
|
|
31
|
+
const contract = events.find(event => event.id === state?.contract_event);
|
|
32
|
+
const target = contract?.t === "acceptance_contract" ? contract.contract.targets?.find(item => item.id === input.target) : undefined;
|
|
33
|
+
if (!state || !target) throw new ToolFault("invalid_input", "target must name a runtime in the current trusted acceptance contract", NOT_STARTED);
|
|
34
|
+
const archive = ctx.outputStore ?? join(ctx.root, ".xeno", "observations");
|
|
35
|
+
const receipt: RuntimeTargetReceipt = { kind: "runtime_target", version: 1, target: target.id,
|
|
36
|
+
request: state.request, contract_event: state.contract_event, state: "unknown", reason: "runtime has not been observed",
|
|
37
|
+
observed_at: new Date().toISOString(), source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(ctx.root, target.workdir ?? "."), runtime: `bun/${Bun.version}` },
|
|
38
|
+
...(target.port === undefined ? {} : { port: target.port }),
|
|
39
|
+
checks: { build: false, inputs: false, artifacts: false, process: false, image: false, port: target.port === undefined ? null : false } };
|
|
40
|
+
const raw: ArtifactRef[] = [];
|
|
41
|
+
let processResult: ToolResult | undefined;
|
|
42
|
+
let launched = false;
|
|
43
|
+
try {
|
|
44
|
+
const build = await this.build(ctx, history, state, target, receipt);
|
|
45
|
+
const existing = this.live.get(target.id);
|
|
46
|
+
if (input.action === "start") {
|
|
47
|
+
if (input.session_id !== undefined) throw Error("start does not accept a session_id; inspect an existing handle instead");
|
|
48
|
+
if (existing && findSession(existing.instance.session, ctx.execOwner)?.alive()) throw Error(`target already owns session ${existing.instance.session}; inspect or stop that handle before starting another`);
|
|
49
|
+
if (!build) throw Error(receipt.reason);
|
|
50
|
+
const cwd = await targetDirectory(ctx.root, target.workdir ?? ".");
|
|
51
|
+
const entryPath = join(await targetDirectory(ctx.root, "."), target.entry);
|
|
52
|
+
const program = await targetProgram(target.interpreter ?? entryPath, ctx.signal);
|
|
53
|
+
if (ctx.signal?.aborted) throw Error("target start cancelled before execution");
|
|
54
|
+
const argv = [program.path, ...(target.interpreter ? [entryPath] : []), ...(target.args ?? [])];
|
|
55
|
+
const command = argv.map(arg => `'${arg.replaceAll("'", "'\\''")}'`).join(" ");
|
|
56
|
+
const source = { ...receipt.source, cwd };
|
|
57
|
+
const session = await startSession({ argv, command, cwd, tty: false, spoolDir: archive, source,
|
|
58
|
+
timeoutMs: commandTimeout(input.timeout, ctx.timeoutMs, ctx.maxTimeoutMs), ...(ctx.execOwner ? { owner: ctx.execOwner } : {}) });
|
|
59
|
+
if ("error" in session) throw Error(session.error);
|
|
60
|
+
launched = true;
|
|
61
|
+
const entry = build.outputs.find(output => output.path === target.entry)!.after;
|
|
62
|
+
if (entry.state !== "observed") throw Error("target entry observation missing");
|
|
63
|
+
const live: LiveTarget = { request: state.request, contract: state.contract_event, build: receipt.build!.event,
|
|
64
|
+
instance: { id: randomUUID(), session: session.id, pid: session.pid, source, entry: { path: entryPath, sha256: entry.sha256 }, program } };
|
|
65
|
+
this.live.set(target.id, live);
|
|
66
|
+
const taken = await pollSession(session, archive, ctx.maxOutputBytes, input.yield_time_ms ?? 1000, "", ctx.signal);
|
|
67
|
+
raw.push(taken.stored.ref);
|
|
68
|
+
processResult = await sessionResult(ctx, session.command, session.timeoutMs, taken);
|
|
69
|
+
}
|
|
70
|
+
const live = this.live.get(target.id);
|
|
71
|
+
if (!live || live.request !== state.request || live.contract !== state.contract_event) throw Error("no live target capability in this Session; historical PID observations are not reattached");
|
|
72
|
+
receipt.instance = live.instance; receipt.source = live.instance.source;
|
|
73
|
+
if (input.session_id !== undefined && live.instance.session !== input.session_id) throw Error("requested handle is not this target instance");
|
|
74
|
+
if (!build || live.build !== receipt.build?.event) { receipt.state = "stale"; throw Error("the running instance is not bound to the current successful build"); }
|
|
75
|
+
const session = findSession(live.instance.session, ctx.execOwner);
|
|
76
|
+
if (!session || !session.alive() || session.exit !== null) { receipt.state = "stale"; throw Error("owned runtime exited or its live capability was lost"); }
|
|
77
|
+
const actual = await observeRuntimeIdentity(session.pid, live.instance.program, target.port, archive, ctx.signal);
|
|
78
|
+
raw.push(...actual.raw);
|
|
79
|
+
// Recheck after OS inspection: neither an exec nor a file replacement during a probe can inherit readiness.
|
|
80
|
+
const current = await targetProgram(target.interpreter ?? live.instance.entry.path, ctx.signal);
|
|
81
|
+
const currentBuild = await this.build(ctx, history, state, target, receipt);
|
|
82
|
+
receipt.checks.process = findSession(session.id, ctx.execOwner) === session && session.alive() && session.exit === null;
|
|
83
|
+
receipt.checks.image = actual.image && canonicalValue(current) === canonicalValue(live.instance.program);
|
|
84
|
+
receipt.checks.port = actual.port;
|
|
85
|
+
receipt.reason = !currentBuild ? receipt.reason : !receipt.checks.process ? "owned runtime exited during observation" :
|
|
86
|
+
!receipt.checks.image ? "runtime executable identity changed or could not be established" : actual.reason;
|
|
87
|
+
receipt.state = targetIsReady(receipt) ? "ready" : !actual.known ? "unknown" : !currentBuild || !receipt.checks.process ? "stale" : "not_ready";
|
|
88
|
+
} catch (error) { receipt.reason = error instanceof Error ? error.message : "target observation failed"; }
|
|
89
|
+
// Cancellation can arrive during a bounded file/OS probe, after the first
|
|
90
|
+
// poll released its listener. Finish through the same owned process path.
|
|
91
|
+
if (ctx.signal?.aborted) {
|
|
92
|
+
const live = this.live.get(target.id);
|
|
93
|
+
const session = live && live.request === state.request && live.contract === state.contract_event ? findSession(live.instance.session, ctx.execOwner) : undefined;
|
|
94
|
+
if (session) {
|
|
95
|
+
const taken = await pollSession(session, archive, ctx.maxOutputBytes, 0, "", ctx.signal);
|
|
96
|
+
raw.push(taken.stored.ref); processResult = await sessionResult(ctx, session.command, session.timeoutMs, taken);
|
|
97
|
+
}
|
|
98
|
+
receipt.state = "unknown"; receipt.checks.process = false; receipt.reason = "target observation cancelled";
|
|
99
|
+
}
|
|
100
|
+
receipt.observed_at = new Date().toISOString();
|
|
101
|
+
try { receipt.manifest = await archiveCommandArtifacts(archive, { observation: receipt, raw }); }
|
|
102
|
+
catch { receipt.state = "unknown"; receipt.reason = "runtime observation archive unavailable"; }
|
|
103
|
+
const content = `target ${target.id}: ${receipt.state}; ${receipt.reason}\n` +
|
|
104
|
+
`build ${receipt.build?.event ?? "none"}; entry ${receipt.instance?.entry.path ?? target.entry}; ` +
|
|
105
|
+
`session ${receipt.instance?.session ?? "none"}; pid ${receipt.instance?.pid ?? "unknown"}; port ${target.port ?? "not declared"}` +
|
|
106
|
+
(receipt.manifest ? `\nObservation: history artifact=${receipt.manifest.sha256}` : "") +
|
|
107
|
+
(receipt.state === "ready" ? "\nAwaiting explicit user acceptance of this instance." : "");
|
|
108
|
+
const result: ToolResult = { ...processResult, ok: receipt.state === "ready", content: content + (processResult ? `\n${processResult.content}` : ""),
|
|
109
|
+
// Process exit and target readiness are different operations. Keep the
|
|
110
|
+
// actual exit in effect/data; only the target owns this tool's outcome.
|
|
111
|
+
outcome: processResult?.outcome?.reason === "cancelled" || processResult?.outcome?.reason === "timeout" ? processResult.outcome :
|
|
112
|
+
executionOutcome(ctx.signal?.aborted ? "cancelled" : receipt.state === "ready" ? "tool_completed" : "tool_failed"),
|
|
113
|
+
...(processResult?.data ? {} : { data: receipt }),
|
|
114
|
+
...(receipt.state === "ready" ? {} : { diagnostic: failureDiagnostic(ctx.signal?.aborted ? "cancelled" : "unknown_effect", receipt.reason, launched ? UNKNOWN_EFFECT : NOT_STARTED) }) };
|
|
115
|
+
// The original stream reference must still match the process receipt. The
|
|
116
|
+
// separate target event/manifest preserves the richer runtime observation.
|
|
117
|
+
return { result, observation: receipt };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async build(ctx: ToolContext, history: History, state: AcceptanceState, target: RuntimeTarget, receipt: RuntimeTargetReceipt): Promise<CommandArtifactReceipt | undefined> {
|
|
121
|
+
const criterion = state.criteria.find(item => item.id === target.build);
|
|
122
|
+
const part = history.find(part => part.events.some(event => event.id === criterion?.evidence?.event_id));
|
|
123
|
+
const event = part?.events.find(event => event.id === criterion?.evidence?.event_id);
|
|
124
|
+
const build = event?.t === "tool_result" && event.ok && event.effect?.kind === "command" && event.effect.exit === 0 ? event.effect.artifacts : undefined;
|
|
125
|
+
if (criterion?.status !== "pass" || !isCommandArtifactReceipt(build) || build.state !== "observed" || !build.manifest || !part || !event ||
|
|
126
|
+
criterion.kind !== "command" || build.command !== criterion.command || build.source.host !== receipt.source.host ||
|
|
127
|
+
build.source.workspace !== resolve(ctx.root) || canonicalValue(build.plan) !== canonicalValue(target.artifacts)) {
|
|
128
|
+
receipt.reason = `run the exact build condition ${target.build} with artifacts=${JSON.stringify(target.artifacts)} on this host and workspace`; return undefined;
|
|
129
|
+
}
|
|
130
|
+
receipt.build = { node: part.node, event: event.id, manifest: build.manifest };
|
|
131
|
+
const exclusions = artifactExclusions(ctx.root, target.artifacts, [...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
|
|
132
|
+
const { manifest, ...original } = build;
|
|
133
|
+
if (manifest.bytes > MANIFEST_BYTES) throw Error("build manifest exceeds runtime verification budget");
|
|
134
|
+
const page = await readObservation(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), manifest.sha256, 0, MANIFEST_BYTES);
|
|
135
|
+
if (page.next !== null || page.bytes !== manifest.bytes || canonicalValue(JSON.parse(page.text)) !== canonicalValue(original) ||
|
|
136
|
+
canonicalValue(exclusions) !== canonicalValue(build.inputs.exclusions)) throw Error("build observation scope or archived manifest does not match its original receipt");
|
|
137
|
+
receipt.checks.build = true;
|
|
138
|
+
const inputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
|
|
139
|
+
const files = await observeArtifactFiles(ctx.root, target.artifacts.paths, ctx.signal);
|
|
140
|
+
receipt.checks.inputs = inputs.sha256 !== null && inputs.sha256 === build.inputs.after;
|
|
141
|
+
receipt.checks.artifacts = build.outputs.every((output, i) => {
|
|
142
|
+
const file = files[i];
|
|
143
|
+
return file?.state === "observed" && output.after.state === "observed" && file.sha256 === output.after.sha256 &&
|
|
144
|
+
file.bytes === output.after.bytes && sameArtifactIdentity(file.identity, output.after.identity);
|
|
145
|
+
});
|
|
146
|
+
if (!receipt.checks.inputs || !receipt.checks.artifacts) { receipt.state = "stale"; receipt.reason = "build inputs or artifact bytes/identity changed; rebuild before launching or accepting"; return undefined; }
|
|
147
|
+
return build;
|
|
148
|
+
}
|
|
149
|
+
}
|