@visual-z/xeno 0.2.1 → 0.3.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 +9 -0
- package/README.md +106 -26
- package/docs/install.md +51 -13
- package/docs/providers.md +11 -0
- package/package.json +5 -4
- package/src/apps/approve.ts +3 -3
- package/src/apps/commands.ts +38 -5
- package/src/apps/completions.ts +73 -19
- package/src/apps/image-draft.ts +46 -0
- 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 +298 -1526
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +128 -86
- package/src/apps/ui/app.ts +1383 -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 +84 -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-review.ts +21 -5
- 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/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 +80 -5
- package/src/core/events.ts +97 -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/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/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/project.ts +45 -18
- 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/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 +26 -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-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 +2 -2
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -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 +460 -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 +1021 -268
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +143 -0
- package/src/runtime/oauth.ts +1 -1
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- 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/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 +199 -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 +271 -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
package/src/core/project.ts
CHANGED
|
@@ -30,6 +30,10 @@ import type { LogEvent, AssistantBlock } from "./events.ts";
|
|
|
30
30
|
import { assertNever } from "./exhaustive.ts";
|
|
31
31
|
import type { ImageContent } from "./images.ts";
|
|
32
32
|
import { type Handoff, emitHandoff } from "./handoff.ts";
|
|
33
|
+
import { diagnosticContent } from "./tool-contract.ts";
|
|
34
|
+
|
|
35
|
+
/** O.8: persist the interpretation used to replay a frozen request. */
|
|
36
|
+
export const PROJECTION_VERSION = 1;
|
|
33
37
|
|
|
34
38
|
export type ContentBlock =
|
|
35
39
|
| ImageContent
|
|
@@ -61,15 +65,18 @@ export interface FrozenPrefix {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface ProjectInput {
|
|
68
|
+
/** Diagnostic only; trace never enters provider messages. */
|
|
69
|
+
trace?: boolean;
|
|
64
70
|
frozen: FrozenPrefix;
|
|
65
71
|
/** Previous segment's handoff, or null for segment 0. */
|
|
66
72
|
handoff: Handoff | null;
|
|
67
73
|
events: readonly LogEvent[];
|
|
68
|
-
/**
|
|
69
|
-
logHint?: { manifestPath: string; segmentPath: string };
|
|
74
|
+
/** Source locations and versioned recovery guidance; workspace search does not index the log. */
|
|
75
|
+
logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export interface ProjectResult {
|
|
79
|
+
provenance?: { version: number; messages: string[][]; omitted: string[] };
|
|
73
80
|
system: string;
|
|
74
81
|
tools: readonly unknown[];
|
|
75
82
|
messages: Message[];
|
|
@@ -110,9 +117,11 @@ export interface ProjectViolation {
|
|
|
110
117
|
* two things". It is a following message rather than a mutation of the user
|
|
111
118
|
* turn so appending the event only appends to the projection (P7).
|
|
112
119
|
*/
|
|
113
|
-
type ReplayEvent = Extract<LogEvent, { t: "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
|
|
120
|
+
type ReplayEvent = Extract<LogEvent, { t: "acceptance_contract" | "acceptance_decision" | "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
|
|
114
121
|
|
|
115
122
|
function replayable(e: LogEvent): e is ReplayEvent {
|
|
123
|
+
if (e.t === "tool_result" && (e.parent_call_id !== undefined || e.origin === "extension")) return false;
|
|
124
|
+
if (e.t === "acceptance_contract" || e.t === "acceptance_decision") return true;
|
|
116
125
|
if (e.t === "extension") return e.inContext;
|
|
117
126
|
if (e.t === "user_bash") return !e.excludeFromContext;
|
|
118
127
|
if (e.t === "rewind") return e.mode !== "both";
|
|
@@ -198,7 +207,7 @@ export function renderRewindMessage(e: Extract<LogEvent, { t: "rewind" }>): Mess
|
|
|
198
207
|
*/
|
|
199
208
|
export function renderHandoffMessage(
|
|
200
209
|
handoff: Handoff,
|
|
201
|
-
logHint?: { manifestPath: string; segmentPath: string },
|
|
210
|
+
logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true },
|
|
202
211
|
): Message {
|
|
203
212
|
const yaml = emitHandoff(handoff);
|
|
204
213
|
|
|
@@ -221,8 +230,10 @@ export function renderHandoffMessage(
|
|
|
221
230
|
"detail this state does not carry, read it rather than guessing:",
|
|
222
231
|
` - segment index: ${logHint.manifestPath}`,
|
|
223
232
|
` - previous segment: ${logHint.segmentPath}`,
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
...(logHint.scopedGuidance !== true ? [
|
|
234
|
+
"Each line is one JSON event. Search it with history; read a line range with read,",
|
|
235
|
+
"or filter with bash and jq. The grep tool skips the log directory.",
|
|
236
|
+
] : ["Each line is one JSON event. Recover originals using the declared tools' contracts; if evidence is inaccessible, state the gap instead of guessing."]),
|
|
226
237
|
);
|
|
227
238
|
}
|
|
228
239
|
|
|
@@ -251,9 +262,11 @@ export function renderHandoffMessage(
|
|
|
251
262
|
export function projectContext(input: ProjectInput): ProjectResult {
|
|
252
263
|
const violations: ProjectViolation[] = [];
|
|
253
264
|
const messages: Message[] = [];
|
|
265
|
+
const sources: string[][] = [];
|
|
266
|
+
const append = (message: Message, ids: string[]) => { messages.push(message); sources.push(ids); };
|
|
254
267
|
|
|
255
268
|
if (input.handoff) {
|
|
256
|
-
|
|
269
|
+
append(renderHandoffMessage(input.handoff, input.logHint), [`handoff:${input.handoff.predecessor?.node ?? "root"}`]);
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
const events = input.events.filter(replayable);
|
|
@@ -262,10 +275,12 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
262
275
|
// a SINGLE user message. Splitting them across messages teaches the model to stop
|
|
263
276
|
// issuing parallel calls, which is a silent, permanent capability regression.
|
|
264
277
|
let pending: ContentBlock[] = [];
|
|
278
|
+
let pendingSources: string[] = [];
|
|
265
279
|
const flushPending = () => {
|
|
266
280
|
if (pending.length > 0) {
|
|
267
|
-
|
|
281
|
+
append({ role: "user", content: pending }, pendingSources);
|
|
268
282
|
pending = [];
|
|
283
|
+
pendingSources = [];
|
|
269
284
|
}
|
|
270
285
|
};
|
|
271
286
|
|
|
@@ -273,29 +288,38 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
273
288
|
let awaiting = new Set<string>();
|
|
274
289
|
|
|
275
290
|
for (const e of events) {
|
|
291
|
+
if (e.t === "acceptance_contract" || e.t === "acceptance_decision") {
|
|
292
|
+
flushPending();
|
|
293
|
+
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `acceptance event ${e.id} follows unanswered tools` });
|
|
294
|
+
append({ role: "user", content: [{ type: "text", text: e.t === "acceptance_contract"
|
|
295
|
+
? `Declared acceptance contract (execution success alone does not satisfy it): ${JSON.stringify(e.contract)}`
|
|
296
|
+
: `User accepted criterion ${e.criterion} of contract ${e.contract_id} for request ${e.request}.` +
|
|
297
|
+
(e.target_binding ? ` Bound to runtime instance ${e.target_binding}, observation ${e.target_observation}. Later runtime changes invalidate this acceptance.` : "") }] }, [e.id]);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
276
300
|
if (e.t === "extension") {
|
|
277
301
|
flushPending();
|
|
278
302
|
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `custom message ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}` });
|
|
279
|
-
|
|
303
|
+
append({ role: "user", content: [{ type: "text", text: e.content }] }, [e.id]);
|
|
280
304
|
continue;
|
|
281
305
|
}
|
|
282
306
|
if (e.t === "user_bash") {
|
|
283
307
|
flushPending();
|
|
284
|
-
|
|
308
|
+
append({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] }, [e.id]);
|
|
285
309
|
continue;
|
|
286
310
|
}
|
|
287
311
|
if (e.t === "snapshot") {
|
|
288
312
|
flushPending();
|
|
289
|
-
|
|
313
|
+
append({ role: "user", content: [{ type: "text",
|
|
290
314
|
text: `Workspace observation: changes outside declared file edits: ${e.undeclared.join(", ")}. ${REREAD_INSTRUCTION}`,
|
|
291
|
-
}] });
|
|
315
|
+
}] }, [e.id]);
|
|
292
316
|
continue;
|
|
293
317
|
}
|
|
294
318
|
if (e.t === "rewind") {
|
|
295
319
|
// Flushed first for the same reason a user turn is: it opens a user
|
|
296
320
|
// message, and a pending tool_result batch has to close before it.
|
|
297
321
|
flushPending();
|
|
298
|
-
|
|
322
|
+
append(renderRewindMessage(e), [e.id]);
|
|
299
323
|
continue;
|
|
300
324
|
}
|
|
301
325
|
if (e.t === "user" || e.t === "guidance") {
|
|
@@ -307,8 +331,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
307
331
|
});
|
|
308
332
|
awaiting = new Set();
|
|
309
333
|
}
|
|
310
|
-
|
|
311
|
-
...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] });
|
|
334
|
+
append({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
|
|
335
|
+
...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] }, [e.id]);
|
|
312
336
|
continue;
|
|
313
337
|
}
|
|
314
338
|
if (e.t === "interrupt") {
|
|
@@ -316,7 +340,7 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
316
340
|
// look-ahead mutation of that turn would rewrite it once a later
|
|
317
341
|
// event (tool_use, another user) was already projected after it.
|
|
318
342
|
flushPending();
|
|
319
|
-
|
|
343
|
+
append({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] }, [e.id]);
|
|
320
344
|
continue;
|
|
321
345
|
}
|
|
322
346
|
|
|
@@ -328,7 +352,7 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
328
352
|
detail: `assistant turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
|
|
329
353
|
});
|
|
330
354
|
}
|
|
331
|
-
|
|
355
|
+
append({ role: "assistant", content: e.blocks.map(toContentBlock) }, [e.id]);
|
|
332
356
|
awaiting = new Set(
|
|
333
357
|
e.blocks.filter((b) => b.type === "tool_use").map((b) => (b as { call_id: string }).call_id),
|
|
334
358
|
);
|
|
@@ -344,10 +368,11 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
344
368
|
continue;
|
|
345
369
|
}
|
|
346
370
|
awaiting.delete(e.call_id);
|
|
371
|
+
pendingSources.push(e.id);
|
|
347
372
|
pending.push({
|
|
348
373
|
type: "tool_result",
|
|
349
374
|
tool_use_id: e.call_id,
|
|
350
|
-
content: e.content,
|
|
375
|
+
content: diagnosticContent(e.content, e.diagnostic) + (e.warnings?.length ? "\nObserver warnings (operation result retained): " + JSON.stringify(e.warnings) : ""),
|
|
351
376
|
...(e.ok ? {} : { is_error: true }),
|
|
352
377
|
});
|
|
353
378
|
// Images follow the paired text result, so APIs without image tool outputs receive a user image.
|
|
@@ -368,6 +393,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
368
393
|
}
|
|
369
394
|
|
|
370
395
|
return {
|
|
396
|
+
...(input.trace ? { provenance: { version: PROJECTION_VERSION, messages: sources,
|
|
397
|
+
omitted: input.events.filter(e => !sources.some(ids => ids.includes(e.id))).map(e => e.id) } } : {}),
|
|
371
398
|
system: input.frozen.system,
|
|
372
399
|
tools: input.frozen.tools,
|
|
373
400
|
messages,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** R10 / D6: stable scope and completion rules; execution details belong to tool contracts. */
|
|
2
|
+
export function taskGuidance(): string {
|
|
3
|
+
return [
|
|
4
|
+
"How to work:",
|
|
5
|
+
"- Keep going until the request is fully resolved. The user's request defines scope; a tool profile only selects capabilities. State reasonable assumptions; ask only for decisions the user must make.",
|
|
6
|
+
"- Implementation: understand the relevant code, reproduce bugs, finish every requested change and verify the final state with the project's tests, build or type check. Finish with what changed and the command that verified it, or the specific blocker. A research answer does not complete an implementation request.",
|
|
7
|
+
"- Research: answer the requested questions with original sources, distinguish evidence from inference, and report gaps. Do not change files merely to establish completion.",
|
|
8
|
+
"- Data analysis: calculate over the complete declared inputs; report sources, denominators, exclusions and uncertainty. Truncated previews cannot establish totals.",
|
|
9
|
+
"- Runtime or visual work: bind checks to the actual build and running target. Automated checks do not establish human acceptance; report any required acceptance as pending.",
|
|
10
|
+
"- Put independent reads and searches in one response; they run in parallel. After a command fails, read the end of its output first. If the same approach fails twice, change the approach instead of retrying it.",
|
|
11
|
+
"- Use only declared capabilities and obey admission decisions. If required evidence is inaccessible, state the specific blocker without inventing success, bypassing permission or replaying an operation with uncertain effects.",
|
|
12
|
+
].join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ToolGuidance {
|
|
16
|
+
/** Only builtin companions with these exact contracts can satisfy the dependency. */
|
|
17
|
+
requires: readonly string[];
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Capability projection is deterministic; excluding a companion cannot add advice. */
|
|
22
|
+
export function toolDescription(description: string, guidance: readonly ToolGuidance[], tools: readonly string[]): string {
|
|
23
|
+
return [description, ...guidance.filter(item => item.requires.every(name => tools.includes(name))).map(item => item.text)].join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function recoveryInstructions(tools: readonly string[]): string {
|
|
27
|
+
return (tools.includes("history")
|
|
28
|
+
? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. history(work_id=ID) recovers work. "
|
|
29
|
+
: "Original user messages and evidence remain at the source references. The original-source recovery capability is unavailable; state any resulting evidence gap instead of guessing. ") +
|
|
30
|
+
"Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting.";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Preserve the source locator even when the interface for retrieving it is unavailable. */
|
|
34
|
+
export function artifactRecovery(sha256: string, offset: number, tools: readonly string[] = ["history"]): string {
|
|
35
|
+
const source = `artifact=${sha256} offset=${offset}`;
|
|
36
|
+
return tools.includes("history") ? `history ${source}` : `${source}; recovery capability unavailable`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Q.21: task-specific shape advice cannot reveal the independent numeric oracle. */
|
|
2
|
+
import type { ToolModeTask } from "./tool-mode-protocol.ts";
|
|
3
|
+
|
|
4
|
+
export function queryEvalContract(task: ToolModeTask, paths: readonly string[], answerSha256: string) {
|
|
5
|
+
const columns = task === "tool-small" ? ["amount"] : task === "tool-batch" ? ["total"] : ["team", "total"];
|
|
6
|
+
const rowCount = task === "tool-aggregate" ? 4 : 1;
|
|
7
|
+
const operation = task === "tool-small" ? "select amount; return one row with only amount" : task === "tool-batch"
|
|
8
|
+
? "sum amount across all records as total; return one row with only total per source"
|
|
9
|
+
: "keep enabled=true, group by team, sum amount as total, and sort by team ascending; return four rows with columns team,total in that order";
|
|
10
|
+
const instruction = "Use query on each of " + paths.join(", ") + " to " + operation +
|
|
11
|
+
'. Write compact JSON with a trailing newline to answer.txt: [{"path":"data-0.jsonl","rows":[...]}], one item per source in the listed order. ' +
|
|
12
|
+
"Preserve all fixture inputs and run bun run gate.";
|
|
13
|
+
return { instruction, columns, rowCount, paths: [...paths], answerSha256 };
|
|
14
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** R2 / D8: source bytes, an unclipped observation, and the visible projection have distinct identities. */
|
|
2
|
+
import type { ArtifactRef } from "./events.ts";
|
|
3
|
+
import { artifactRecovery } from "./prompt-policy.ts";
|
|
4
|
+
import { headTailSelection, isObservationArtifact as ref, isBytePreview, type BytePreview } from "./observation-view.ts";
|
|
5
|
+
|
|
6
|
+
export interface ObservationReceipt {
|
|
7
|
+
version: 1;
|
|
8
|
+
source: { host: string; workspace: string; resource: string; sha256: string; raw: ArtifactRef };
|
|
9
|
+
artifact: ArtifactRef;
|
|
10
|
+
/** R4: content-addressed version premise, separate from this formatted observation. */
|
|
11
|
+
descriptor?: ArtifactRef;
|
|
12
|
+
selection: { start: number; end: number; total_lines: number };
|
|
13
|
+
preview: BytePreview;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
const size = (text: string) => encoder.encode(text).length;
|
|
18
|
+
/** Existing assumed preview cap; archival removes data loss without changing the display policy. */
|
|
19
|
+
const READ_MAX_LINE_CHARS = 2000;
|
|
20
|
+
|
|
21
|
+
export function readObservationText(header: string, lines: readonly string[], start: number): string {
|
|
22
|
+
return header + lines.map((line, i) => `${start + i}\t${line}`).join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Map two successive cuts (per line and total bytes) back to the same immutable observation. */
|
|
26
|
+
export function previewReadObservation(header: string, lines: readonly string[], start: number, artifact: string,
|
|
27
|
+
maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): { text: string; preview: ObservationReceipt["preview"]; semantic: string } {
|
|
28
|
+
const spans: { view: number; source: number; bytes: number }[] = [];
|
|
29
|
+
let text = "", source = 0, view = 0;
|
|
30
|
+
const mapped = (value: string, offset: number) => {
|
|
31
|
+
const bytes = size(value);
|
|
32
|
+
if (bytes) spans.push({ view, source: offset, bytes });
|
|
33
|
+
text += value; view += bytes;
|
|
34
|
+
};
|
|
35
|
+
const synthetic = (value: string) => { text += value; view += size(value); };
|
|
36
|
+
mapped(header, 0); source += size(header);
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i]!, prefix = `${start + i}\t`;
|
|
39
|
+
let end = Math.min(line.length, maxLineChars);
|
|
40
|
+
// JavaScript string indices are UTF-16; a character can occupy a surrogate pair.
|
|
41
|
+
if (end > 0 && end < line.length && /[\uD800-\uDBFF]/.test(line[end - 1]!)) end--;
|
|
42
|
+
const kept = prefix + line.slice(0, end);
|
|
43
|
+
mapped(kept, source);
|
|
44
|
+
if (end < line.length) synthetic(` [line truncated: ${line.length} chars; ${artifactRecovery(artifact, source + size(kept), tools)}]`);
|
|
45
|
+
source += size(prefix + line);
|
|
46
|
+
if (i < lines.length - 1) { mapped("\n", source); source++; }
|
|
47
|
+
}
|
|
48
|
+
const bytes = encoder.encode(text);
|
|
49
|
+
const selected = headTailSelection(bytes, bytes, bytes.length, maxBytes,
|
|
50
|
+
omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
|
|
51
|
+
const visible: [number, number][] = [];
|
|
52
|
+
for (const [lo, hi] of selected.visible) for (const span of spans) {
|
|
53
|
+
const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
|
|
54
|
+
if (end <= begin) continue;
|
|
55
|
+
const a = span.source + begin - span.view, b = span.source + end - span.view;
|
|
56
|
+
const last = visible.at(-1);
|
|
57
|
+
if (last?.[1] === a) last[1] = b;
|
|
58
|
+
else visible.push([a, b]);
|
|
59
|
+
}
|
|
60
|
+
let next = 0;
|
|
61
|
+
for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
|
|
62
|
+
const full = encoder.encode(readObservationText(header, lines, start));
|
|
63
|
+
const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(Math.max(lo, size(header)), Math.max(hi, size(header))))).join("");
|
|
64
|
+
return { text: selected.text, semantic, preview: { unit: "utf8_bytes", complete: next === source,
|
|
65
|
+
visible, next_offset: next === source ? null : next } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
69
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
70
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
71
|
+
|
|
72
|
+
/** Optional on old logs, strict when present. A cursor cannot certify bytes outside its artifact. */
|
|
73
|
+
export function isObservationReceipt(value: unknown): value is ObservationReceipt {
|
|
74
|
+
if (!record(value) || value["version"] !== 1 || !ref(value["artifact"])) return false;
|
|
75
|
+
if (value["descriptor"] !== undefined && !ref(value["descriptor"])) return false;
|
|
76
|
+
const source = value["source"], selection = value["selection"], preview = value["preview"];
|
|
77
|
+
if (!record(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") ||
|
|
78
|
+
!digest(source["sha256"]) || !ref(source["raw"]) || source["sha256"] !== source["raw"].sha256) return false;
|
|
79
|
+
if (!record(selection) || !natural(selection["start"]) || selection["start"] < 1 || !natural(selection["end"]) ||
|
|
80
|
+
!natural(selection["total_lines"]) || selection["end"] < selection["start"] || selection["end"] > selection["total_lines"]) return false;
|
|
81
|
+
return isBytePreview(preview, value["artifact"].bytes);
|
|
82
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** R12c: curated mechanisms, never copied private transcript contents or population estimates. */
|
|
2
|
+
export const RECOVERY_SOURCE_REPORT = { path: "docs/research/local-agent-tool-audit-2026-10-06.md",
|
|
3
|
+
sha256: "a4c83ec9e5c52ea6500434673f0e90c171e55aa247d293dc3f09c893ec604832" } as const;
|
|
4
|
+
export const RECOVERY_CASES = [
|
|
5
|
+
{id:"recovery-missing-path",family:"parameters",partition:"development",source:"E2",basis:"observed",scenario:"An edit omits its required file identity."},
|
|
6
|
+
{id:"recovery-stale-version",family:"version",partition:"development",source:"section-2.2-replacement-mismatch",basis:"derived-scenario",scenario:"An obsolete file digest must not authorize an edit over newer content; the source mismatch alone did not establish a stale-version cause."},
|
|
7
|
+
{id:"recovery-long-output",family:"output",partition:"development",source:"X2",basis:"observed",scenario:"A clipped long line hides the needed value; recover immutable original bytes."},
|
|
8
|
+
{id:"recovery-api-mismatch",family:"environment",partition:"holdout",source:"E4",basis:"portable-analogue",scenario:"An older local API rejects a named option; inspect its actual contract before retrying."},
|
|
9
|
+
{id:"recovery-closed-process",family:"lifecycle",partition:"development",source:"E5",basis:"derived-scenario",scenario:"A finished process cannot accept more input; the original unknown ID did not establish why ownership was lost."},
|
|
10
|
+
{id:"recovery-unchanged-retry",family:"recovery",partition:"development",source:"E3",basis:"derived-scenario",scenario:"Changing replacement wording does not repair an unchanged missing anchor; this extends the source's identical retry mechanism."},
|
|
11
|
+
{id:"recovery-runtime-acceptance",family:"acceptance",partition:"holdout",source:"E6",basis:"scenario-from-user-question",scenario:"A build is not a running current instance or author acceptance; bind the preview to its build."},
|
|
12
|
+
{id:"recovery-eof",family:"parameters",partition:"holdout",source:"section-2.2-read-range",basis:"observed",scenario:"A line request exceeds the real file boundary; recover from the reported range."},
|
|
13
|
+
] as const;
|
|
14
|
+
export type RecoveryTask = typeof RECOVERY_CASES[number]["id"];
|
|
15
|
+
export const RECOVERY_TASKS = RECOVERY_CASES.map(item=>item.id);
|
|
16
|
+
export const recoveryCase = (task:string) => RECOVERY_CASES.find(item=>item.id===task);
|
|
17
|
+
export type RecoveryPartition = "all" | "development" | "holdout";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** R12c: metrics are projections of original calls, not generated tool-result prose. */
|
|
2
|
+
import type {LogEvent} from "./events.ts";
|
|
3
|
+
import {advanceRecovery,emptyRecoveryState} from "./failure-recovery.ts";
|
|
4
|
+
import {resolveEvidence} from "./evidence.ts";
|
|
5
|
+
|
|
6
|
+
export interface RecoveryMetrics {
|
|
7
|
+
toolCalls:number; recordedInvocations:number; outerCalls:number; nestedCalls:number; unresolvedCalls:number; toolFailures:number;
|
|
8
|
+
firstToolSuccess:boolean|null; firstPassTaskSuccess:boolean|null; finalTaskSuccess:boolean|null;
|
|
9
|
+
recoveryCalls:number; unchangedFailureRetries:number; classifiedFailures:number; unclassifiedFailures:number;
|
|
10
|
+
originalEvidenceRetrievals:number; unsupportedClaims:number;
|
|
11
|
+
}
|
|
12
|
+
export function deriveRecoveryMetrics(nodes:readonly {node:number;events:readonly LogEvent[]}[],verifiedRetrievals:ReadonlySet<string>,
|
|
13
|
+
finalTaskSuccess:boolean|null,identify:(path:string)=>string):RecoveryMetrics{
|
|
14
|
+
type Call={name:string;ordinal:number;recorded:boolean;nested:boolean;ok:boolean|null};
|
|
15
|
+
const calls=new Map<string,Call>();let ordinal=0,firstFailure=Infinity,repeated=0,classified=0,unclassified=0,retrieved=0,unsupported=0;
|
|
16
|
+
let state=emptyRecoveryState();
|
|
17
|
+
for(const part of nodes)for(const [index,event] of part.events.entries()){
|
|
18
|
+
ordinal++;
|
|
19
|
+
if(event.t==="assistant")for(const block of event.blocks)if(block.type==="tool_use"){
|
|
20
|
+
const key=`${part.node}:${block.call_id}`;
|
|
21
|
+
if(calls.has(key))throw Error("duplicate recovery call identity");
|
|
22
|
+
calls.set(key,{name:block.name,ordinal,recorded:false,nested:false,ok:null});
|
|
23
|
+
}
|
|
24
|
+
if(event.t==="tool_invocation"){
|
|
25
|
+
const key=`${part.node}:${event.call_id}`,prior=calls.get(key);
|
|
26
|
+
if(prior?.recorded)throw Error("duplicate recovery invocation");
|
|
27
|
+
calls.set(key,{name:event.name,ordinal:prior?.ordinal??ordinal,recorded:true,nested:event.parent_call_id!==undefined,ok:prior?.ok??null});
|
|
28
|
+
}
|
|
29
|
+
if(event.t==="tool_result"){
|
|
30
|
+
const key=`${event.invocation_ref?.node??part.node}:${event.call_id}`,call=calls.get(key);
|
|
31
|
+
if(!call||call.ok!==null)throw Error("unpaired recovery result");
|
|
32
|
+
call.ok=event.ok;
|
|
33
|
+
if(!event.ok){firstFailure=Math.min(firstFailure,ordinal);if(event.diagnostic)classified++;else unclassified++;}
|
|
34
|
+
if(call.name==="history"&&event.ok&&verifiedRetrievals.has(`${part.node}:${event.id}`))retrieved++;
|
|
35
|
+
}
|
|
36
|
+
if(event.t==="claim"&&!resolveEvidence(part.events.slice(0,index),event.evidence).ok)unsupported++;
|
|
37
|
+
state=advanceRecovery(state,[event],identify);
|
|
38
|
+
if(event.t==="tool_result"&&!event.ok&&[...state.failures.values()].some(run=>run.count>1&&run.sources.at(-1)===event.id))repeated++;
|
|
39
|
+
}
|
|
40
|
+
const rows=[...calls.values()],failed=rows.filter(call=>call.ok===false).length,unresolved=rows.filter(call=>call.ok===null).length;
|
|
41
|
+
// An invocation record proves the attempt was logged, not that schema or
|
|
42
|
+
// permission checks admitted its effect. Rejected requests have receipts too.
|
|
43
|
+
return {toolCalls:rows.length,recordedInvocations:rows.filter(call=>call.recorded).length,outerCalls:rows.filter(call=>!call.nested).length,
|
|
44
|
+
nestedCalls:rows.filter(call=>call.nested).length,unresolvedCalls:unresolved,toolFailures:failed,firstToolSuccess:rows[0]?.ok??null,
|
|
45
|
+
firstPassTaskSuccess:finalTaskSuccess===null?null:finalTaskSuccess&&failed===0&&unresolved===0,
|
|
46
|
+
finalTaskSuccess,recoveryCalls:rows.filter(call=>call.ordinal>firstFailure).length,unchangedFailureRetries:repeated,
|
|
47
|
+
classifiedFailures:classified,unclassifiedFailures:unclassified,originalEvidenceRetrievals:retrieved,unsupportedClaims:unsupported};
|
|
48
|
+
}
|
|
49
|
+
export function isRecoveryMetrics(value:unknown):value is RecoveryMetrics{
|
|
50
|
+
if(!value||typeof value!=="object")return false;
|
|
51
|
+
const v=value as Record<string,unknown>;
|
|
52
|
+
const numbers=["toolCalls","recordedInvocations","outerCalls","nestedCalls","unresolvedCalls","toolFailures","recoveryCalls","unchangedFailureRetries","classifiedFailures","unclassifiedFailures","originalEvidenceRetrievals","unsupportedClaims"];
|
|
53
|
+
if(numbers.some(key=>!Number.isSafeInteger(v[key])||Number(v[key])<0))return false;
|
|
54
|
+
if(["firstToolSuccess","firstPassTaskSuccess","finalTaskSuccess"].some(key=>v[key]!==null&&typeof v[key]!=="boolean"))return false;
|
|
55
|
+
const n=(key:string)=>Number(v[key]);
|
|
56
|
+
return n("outerCalls")+n("nestedCalls")===n("toolCalls")&&n("recordedInvocations")<=n("toolCalls")
|
|
57
|
+
&&n("unresolvedCalls")+n("toolFailures")<=n("toolCalls")&&n("classifiedFailures")+n("unclassifiedFailures")===n("toolFailures")
|
|
58
|
+
&&n("recoveryCalls")<=n("toolCalls")&&n("unchangedFailureRetries")<=n("toolFailures")&&n("originalEvidenceRetrievals")<=n("toolCalls")-n("toolFailures")-n("unresolvedCalls")
|
|
59
|
+
&&(v["firstPassTaskSuccess"]!==true||v["finalTaskSuccess"]===true&&n("toolFailures")===0&&n("unresolvedCalls")===0);
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** R9 / D8: navigation into original ancestry, never a semantic summary or permission grant. */
|
|
2
|
+
import type { LogEvent } from "./events.ts";
|
|
3
|
+
import { historyEntries, type HistoryEntry } from "./history-query.ts";
|
|
4
|
+
import { isEnvironmentReceipt } from "./environment.ts";
|
|
5
|
+
import { resourceKey } from "./evidence-links.ts";
|
|
6
|
+
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
7
|
+
|
|
8
|
+
export const RECOVERY_SECTIONS = ["users", "failures", "environment", "next_actions"] as const;
|
|
9
|
+
export type RecoverySection = typeof RECOVERY_SECTIONS[number];
|
|
10
|
+
export const RECOVERY_KINDS = { users: "user", failures: "failure", environment: "environment", next_actions: "next_action" } as const;
|
|
11
|
+
export type RecoveryNodes = readonly { node: number; events: readonly LogEvent[] }[];
|
|
12
|
+
export type RecoveryReference = {
|
|
13
|
+
node: number; event: string; kind: "user" | "failure" | "environment" | "next_action";
|
|
14
|
+
/** This is the recorded role, not proof of current authorization or human authorship. */
|
|
15
|
+
provenance: "user_message" | "observation" | "model_annotation";
|
|
16
|
+
abandoned?: true; tool?: string; work?: string;
|
|
17
|
+
resources?: readonly string[];
|
|
18
|
+
/** A failed request can name a resource without having observed it. */
|
|
19
|
+
requested_resource?: string;
|
|
20
|
+
diagnostic?: Pick<ToolDiagnostic, "code" | "execution" | "effects">;
|
|
21
|
+
host?: string; workspace?: string; cwd?: string; program?: string;
|
|
22
|
+
};
|
|
23
|
+
export type RecoverySources = Record<RecoverySection, RecoveryReference[]>;
|
|
24
|
+
export type RecoveryRanges = Record<RecoverySection, { total: number; omitted: number }>;
|
|
25
|
+
|
|
26
|
+
/** Newest first within each role; no keywords decide which user sentence is a constraint. */
|
|
27
|
+
export function recoverySources(nodes: RecoveryNodes, entries: readonly HistoryEntry[] = historyEntries(nodes)): RecoverySources {
|
|
28
|
+
const abandoned = new Set(nodes.flatMap(part => part.events.flatMap(event => event.t === "interrupt" ? [event.user_id] : [])));
|
|
29
|
+
const result: RecoverySources = { users: [], failures: [], environment: [], next_actions: [] };
|
|
30
|
+
for (const entry of entries.toReversed()) {
|
|
31
|
+
const source = { node: entry.node, event: entry.event };
|
|
32
|
+
if (entry.kind === "user") result.users.push({ ...source, kind: "user", provenance: "user_message",
|
|
33
|
+
...(abandoned.has(entry.event) ? { abandoned: true } : {}) });
|
|
34
|
+
if (entry.kind === "next_action") result.next_actions.push({ ...source, kind: "next_action", provenance: "model_annotation",
|
|
35
|
+
...(entry.work_ids?.[0] ? { work: entry.work_ids[0] } : {}) });
|
|
36
|
+
if (entry.kind !== "tool_result") continue;
|
|
37
|
+
if (entry.ok === false) {
|
|
38
|
+
const input = entry.input !== null && typeof entry.input === "object" ? entry.input as Record<string, unknown> : {};
|
|
39
|
+
const requested = typeof input["path"] === "string" ? input["path"] : typeof input["cwd"] === "string" ? input["cwd"] : undefined;
|
|
40
|
+
const diagnostic = entry.diagnostic;
|
|
41
|
+
result.failures.push({ ...source, kind: "failure", provenance: "observation", ...(entry.tool ? { tool: entry.tool } : {}),
|
|
42
|
+
...(entry.resources?.length ? { resources: entry.resources } : {}),
|
|
43
|
+
...(requested ? { requested_resource: resourceKey(requested) } : {}),
|
|
44
|
+
...(diagnostic ? { diagnostic: { code: diagnostic.code,
|
|
45
|
+
...(diagnostic.execution ? { execution: diagnostic.execution } : {}), ...(diagnostic.effects ? { effects: diagnostic.effects } : {}) } } : {}) });
|
|
46
|
+
}
|
|
47
|
+
if (entry.tool === "environment") {
|
|
48
|
+
const receipt = entry.data;
|
|
49
|
+
// Only a validated actual receipt contributes identity. Text that looks
|
|
50
|
+
// like a host or a successful probe does not become a machine fact (D11).
|
|
51
|
+
const identity = isEnvironmentReceipt(receipt) && entry.effect?.kind === "command" && entry.effect.exit === receipt.evidence.exit
|
|
52
|
+
? { host: receipt.source.host, workspace: receipt.source.workspace, cwd: receipt.source.cwd, program: receipt.query.program } : {};
|
|
53
|
+
result.environment.push({ ...source, kind: "environment", provenance: "observation", ...identity });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Source labels are stable even when the runtime attaches a fresh validity view. */
|
|
60
|
+
export function recoveryPage(sources: RecoverySources, entries: readonly HistoryEntry[], section: RecoverySection, offset: number, limit: number): {
|
|
61
|
+
total: number; offset: number; next: number | null; items: { source: RecoveryReference; original: HistoryEntry }[];
|
|
62
|
+
} {
|
|
63
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
64
|
+
throw new RangeError("recovery offset must be nonnegative and limit must be 1..100");
|
|
65
|
+
}
|
|
66
|
+
const indexed = new Map(entries.map(entry => [entry.event, entry]));
|
|
67
|
+
const references = sources[section], end = Math.min(references.length, offset + limit);
|
|
68
|
+
return { total: references.length, offset, next: end < references.length ? end : null,
|
|
69
|
+
items: references.slice(offset, end).map(source => {
|
|
70
|
+
const original = indexed.get(source.event);
|
|
71
|
+
if (!original || original.node !== source.node) throw new Error("recovery source is absent from the selected ancestry");
|
|
72
|
+
return { source, original };
|
|
73
|
+
}) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** R8: declarations select a target; only an owned runtime observation supplies its identity. */
|
|
2
|
+
import { parseCommandArtifactPlan, validArtifactPath, type CommandArtifactPlan } from "./command-artifacts.ts";
|
|
3
|
+
import type { ArtifactRef } from "./events.ts";
|
|
4
|
+
import { isObservationArtifact } from "./observation-view.ts";
|
|
5
|
+
import type { ProcessSource } from "./process-receipt.ts";
|
|
6
|
+
|
|
7
|
+
export type RuntimeTarget = { id: string; build: string; artifacts: CommandArtifactPlan; entry: string;
|
|
8
|
+
interpreter?: string; args?: string[]; workdir?: string; port?: number };
|
|
9
|
+
export type TargetBuild = { node: number; event: string; manifest: ArtifactRef };
|
|
10
|
+
export type RuntimeInstance = { id: string; session: number; pid: number; source: ProcessSource;
|
|
11
|
+
entry: { path: string; sha256: string }; program: { path: string; sha256: string; device: string; inode: string } };
|
|
12
|
+
export type RuntimeTargetReceipt = {
|
|
13
|
+
kind: "runtime_target"; version: 1; target: string; request: string; contract_event: string;
|
|
14
|
+
state: "ready" | "not_ready" | "stale" | "unknown"; reason: string; observed_at: string;
|
|
15
|
+
source: ProcessSource; build?: TargetBuild; instance?: RuntimeInstance; port?: number;
|
|
16
|
+
checks: { build: boolean; inputs: boolean; artifacts: boolean; process: boolean; image: boolean; port: boolean | null };
|
|
17
|
+
manifest?: ArtifactRef;
|
|
18
|
+
};
|
|
19
|
+
export type TargetAcceptance = { id: string; build: string; status: "not_run" | RuntimeTargetReceipt["state"];
|
|
20
|
+
observation?: string; binding?: string; session?: number; pid?: number; entry?: string; port?: number; build_event?: string; reason?: string };
|
|
21
|
+
|
|
22
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
const id = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value);
|
|
24
|
+
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
25
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
26
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
27
|
+
const source = (value: unknown): value is ProcessSource => record(value) && ["host", "workspace", "cwd", "runtime"].every(key => text(value[key]));
|
|
28
|
+
|
|
29
|
+
export function parseRuntimeTarget(value: unknown): RuntimeTarget {
|
|
30
|
+
if (!record(value) || Object.keys(value).some(key => !["id", "build", "artifacts", "entry", "interpreter", "args", "workdir", "port"].includes(key)) ||
|
|
31
|
+
!id(value["id"]) || !id(value["build"]) || !validArtifactPath(value["entry"])) throw Error("invalid runtime target declaration");
|
|
32
|
+
const artifacts = parseCommandArtifactPlan(value["artifacts"]);
|
|
33
|
+
if (!artifacts.paths.includes(value["entry"])) throw Error("target entry must be an observed build artifact");
|
|
34
|
+
const interpreter = value["interpreter"], args = value["args"], workdir = value["workdir"], port = value["port"];
|
|
35
|
+
if (interpreter !== undefined && (typeof interpreter !== "string" || !interpreter.startsWith("/") || /[\x00-\x1f\x7f]/.test(interpreter) || interpreter.length > 4096)) throw Error("target interpreter needs an explicit absolute path");
|
|
36
|
+
// Assumed argv budget prevents declarations from becoming an unbounded prompt or exec payload.
|
|
37
|
+
if (args !== undefined && (!Array.isArray(args) || args.length > 64 || !args.every(arg => typeof arg === "string" && !arg.includes("\0")) || JSON.stringify(args).length > 8192)) throw Error("target args must be a bounded literal string array");
|
|
38
|
+
if (workdir !== undefined && workdir !== "." && !validArtifactPath(workdir)) throw Error("target workdir must stay inside the workspace");
|
|
39
|
+
if (port !== undefined && (!natural(port) || port < 1 || port > 65535)) throw Error("target port must be 1..65535 on IPv4 loopback");
|
|
40
|
+
return { id: value["id"], build: value["build"], entry: value["entry"], artifacts,
|
|
41
|
+
...(interpreter !== undefined ? { interpreter: interpreter as string } : {}), ...(args !== undefined ? { args: [...args as string[]] } : {}),
|
|
42
|
+
...(workdir !== undefined ? { workdir: workdir as string } : {}), ...(port !== undefined ? { port: port as number } : {}) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function targetIsReady(receipt: Pick<RuntimeTargetReceipt, "build" | "instance" | "checks">): boolean {
|
|
46
|
+
return receipt.build !== undefined && receipt.instance !== undefined && receipt.checks.build && receipt.checks.inputs && receipt.checks.artifacts &&
|
|
47
|
+
receipt.checks.process && receipt.checks.image && receipt.checks.port !== false;
|
|
48
|
+
}
|
|
49
|
+
export function isRuntimeTargetReceipt(value: unknown): value is RuntimeTargetReceipt {
|
|
50
|
+
if (!record(value) || value["kind"] !== "runtime_target" || value["version"] !== 1 || !id(value["target"]) || !text(value["request"]) ||
|
|
51
|
+
!text(value["contract_event"]) || !text(value["observed_at"]) || !text(value["reason"]) || !source(value["source"]) ||
|
|
52
|
+
!["ready", "not_ready", "stale", "unknown"].includes(String(value["state"]))) return false;
|
|
53
|
+
const build = value["build"], instance = value["instance"], checks = value["checks"], port = value["port"];
|
|
54
|
+
if (build !== undefined && (!record(build) || !natural(build["node"]) || !text(build["event"]) || !isObservationArtifact(build["manifest"]))) return false;
|
|
55
|
+
if (instance !== undefined && (!record(instance) || !text(instance["id"]) || !natural(instance["session"]) || instance["session"] < 1 ||
|
|
56
|
+
!natural(instance["pid"]) || instance["pid"] < 1 || !source(instance["source"]) ||
|
|
57
|
+
!["host", "workspace", "cwd", "runtime"].every(key => (instance["source"] as Record<string, string>)[key] === (value["source"] as Record<string, string>)[key]) ||
|
|
58
|
+
!record(instance["entry"]) || !text(instance["entry"]["path"]) || !digest(instance["entry"]["sha256"]) ||
|
|
59
|
+
!record(instance["program"]) || !text(instance["program"]["path"]) || !digest(instance["program"]["sha256"]) ||
|
|
60
|
+
!text(instance["program"]["device"]) || !text(instance["program"]["inode"]))) return false;
|
|
61
|
+
if (!record(checks) || !["build", "inputs", "artifacts", "process", "image"].every(key => typeof checks[key] === "boolean") ||
|
|
62
|
+
!(checks["port"] === null || typeof checks["port"] === "boolean") ||
|
|
63
|
+
(port === undefined ? checks["port"] !== null : !natural(port) || port < 1 || port > 65535 || checks["port"] === null)) return false;
|
|
64
|
+
if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
|
|
65
|
+
return value["state"] !== "ready" || targetIsReady(value as RuntimeTargetReceipt) && isObservationArtifact(value["manifest"]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** lsof -F0 is a field protocol; matching a process name or display substring proves nothing. */
|
|
69
|
+
export function lsofFiles(output: string, pid: number): Record<string, string>[] {
|
|
70
|
+
let process = -1, file: Record<string, string> | undefined;
|
|
71
|
+
const files: Record<string, string>[] = [];
|
|
72
|
+
for (const chunk of output.split("\0")) {
|
|
73
|
+
const field = chunk.replace(/^\n/, "");
|
|
74
|
+
if (!field) continue;
|
|
75
|
+
const key = field[0]!, value = field.slice(1);
|
|
76
|
+
if (key === "p") { process = Number(value); file = undefined; }
|
|
77
|
+
else if (key === "f") { file = { f: value }; if (process === pid) files.push(file); }
|
|
78
|
+
else if (file && process === pid) {
|
|
79
|
+
if (key === "T") { const split = value.indexOf("="); if (split > 0) file[`T${value.slice(0, split)}`] = value.slice(split + 1); }
|
|
80
|
+
else file[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
export function lsofOwnsImage(output: string, pid: number, device: string, inode: string): boolean {
|
|
86
|
+
return lsofFiles(output, pid).some(file => {
|
|
87
|
+
if (file["f"] !== "txt" || file["i"] !== inode || !/^0x[0-9a-f]+$/i.test(file["D"] ?? "")) return false;
|
|
88
|
+
try { return BigInt(file["D"]!) === BigInt(device); } catch { return false; }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function lsofOwnsPort(output: string, pid: number, port: number): boolean {
|
|
92
|
+
return lsofFiles(output, pid).some(file => file["t"] === "IPv4" && file["P"] === "TCP" && file["TST"] === "LISTEN" &&
|
|
93
|
+
["*", "127.0.0.1"].some(host => file["n"] === `${host}:${port}`));
|
|
94
|
+
}
|