@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/export.ts
CHANGED
|
@@ -91,6 +91,9 @@ function escapeHtml(s: string): string {
|
|
|
91
91
|
|
|
92
92
|
function eventHtml(e: LogEvent): string {
|
|
93
93
|
switch (e.t) {
|
|
94
|
+
case "tool_invocation": return wrap("event", "tool invocation", e.at, `${e.name} · ${e.call_id}${e.parent_call_id ? ` · parent ${e.parent_call_id}` : ""} · input ${e.input_ref.sha256} · schema ${e.schema_ref?.sha256 ?? "unavailable"}`);
|
|
95
|
+
case "checkpoint": return wrap("event", `file checkpoint ${e.phase}`, e.at, e.reason ?? Object.keys(e.changes).join("\n"));
|
|
96
|
+
case "workspace_observation": return wrap("event", "verification observation", e.at, e.fingerprint ?? e.reason ?? "unavailable");
|
|
94
97
|
case "extension": return e.display ? wrap("event", e.customType, e.at, e.content || JSON.stringify(e.data)) : "";
|
|
95
98
|
case "user":
|
|
96
99
|
return wrap("user", "user", e.at, [e.text, ...(e.images ?? []).map(imageDescription)].join("\n"));
|
|
@@ -143,6 +146,12 @@ function eventHtml(e: LogEvent): string {
|
|
|
143
146
|
return wrap("event", `work ${e.operation}`, e.at, `${e.work_id}: ${e.what}`);
|
|
144
147
|
case "evidence_link":
|
|
145
148
|
return wrap("event", "evidence link", e.at, `${e.work}: ${e.what} → ${e.source}`);
|
|
149
|
+
case "work_owner": case "work_dependencies": return wrap("event", e.t, e.at, JSON.stringify(e));
|
|
150
|
+
case "skill_loaded": return wrap("event", "skill loaded", e.at, `${e.name} · ${e.path} · ${e.sha256} · ${e.mode}`);
|
|
151
|
+
case "process_exit": return wrap("event", "process exit", e.at, `${e.session} · exit ${e.exit} · output ${e.output.sha256}`);
|
|
152
|
+
case "acceptance_contract": return wrap("event", "acceptance contract", e.at, JSON.stringify(e.contract));
|
|
153
|
+
case "target_observation": return wrap("event", "runtime target", e.at, JSON.stringify(e.observation));
|
|
154
|
+
case "acceptance_decision": return wrap("event", "human acceptance", e.at, `${e.contract_id}: ${e.criterion}`);
|
|
146
155
|
default:
|
|
147
156
|
return assertNever(e, "eventHtml");
|
|
148
157
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/** R5 / D8: replayable failure premises; advice never authorizes or automatically retries a tool. */
|
|
2
|
+
import type { GuidanceEvent, LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
+
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
4
|
+
import { canonicalValue } from "./continuation.ts";
|
|
5
|
+
import { repairToolInput } from "./tool-input.ts";
|
|
6
|
+
import { failureDiagnostic, scopeFailureGuidance } from "./tool-failure.ts";
|
|
7
|
+
|
|
8
|
+
/** Reuses the existing three-attempt diagnostic hypothesis, not a measured optimum or a stop threshold. */
|
|
9
|
+
export const RECOVERY_FAILURES = 3;
|
|
10
|
+
type Call = { name: string; input: unknown };
|
|
11
|
+
export interface FailureIdentity {
|
|
12
|
+
family: string;
|
|
13
|
+
resource: string;
|
|
14
|
+
version: string;
|
|
15
|
+
code: ToolDiagnostic["code"];
|
|
16
|
+
execution: NonNullable<ToolDiagnostic["execution"]>;
|
|
17
|
+
effects: NonNullable<ToolDiagnostic["effects"]>;
|
|
18
|
+
premise: string;
|
|
19
|
+
}
|
|
20
|
+
interface FailureRun {
|
|
21
|
+
identity: FailureIdentity;
|
|
22
|
+
key: string;
|
|
23
|
+
count: number;
|
|
24
|
+
sources: readonly string[];
|
|
25
|
+
notified: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface RecoveryState {
|
|
28
|
+
calls: ReadonlyMap<string, Call>;
|
|
29
|
+
versions: ReadonlyMap<string, string>;
|
|
30
|
+
failures: ReadonlyMap<string, FailureRun>;
|
|
31
|
+
change: number;
|
|
32
|
+
}
|
|
33
|
+
export function emptyRecoveryState(): RecoveryState {
|
|
34
|
+
return { calls: new Map(), versions: new Map(), failures: new Map(), change: 0 };
|
|
35
|
+
}
|
|
36
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
38
|
+
const fileTools = ["read", "write", "edit", "grep", "find", "ls"];
|
|
39
|
+
const family = (name: string): string => name === "bash" || name === "exec_command" ? "command"
|
|
40
|
+
: name === "edit" || name === "write" ? "file_change" : name;
|
|
41
|
+
|
|
42
|
+
function target(call: Call, identify: (path: string) => string): { family: string; resource: string; input: Record<string, unknown> } {
|
|
43
|
+
const repaired = repairToolInput(call.name, call.input), input = record(repaired) ? repaired : { value: repaired };
|
|
44
|
+
const kind = family(call.name);
|
|
45
|
+
return { family: kind, input, resource: fileTools.includes(call.name) && typeof input["path"] === "string" ? identify(input["path"])
|
|
46
|
+
: kind === "command" ? identify(typeof input["workdir"] === "string" ? input["workdir"] : ".")
|
|
47
|
+
: call.name === "write_stdin" ? `session:${String(input["session_id"])}` : call.name };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function groupKey(call: Call, result: ToolResultEvent, identify: (path: string) => string): string {
|
|
51
|
+
const context = target(call, identify);
|
|
52
|
+
return canonicalValue([context.family, context.resource, context.family === "command"
|
|
53
|
+
? result.effect?.kind === "command" ? result.effect.cmd.trim() : String(context.input["command"] ?? context.input["cmd"] ?? "").trim() : null]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Only recorded versions and structured failures are facts; stderr words never identify an error. */
|
|
57
|
+
export function failureIdentity(call: Call, result: ToolResultEvent, state: RecoveryState,
|
|
58
|
+
identify: (path: string) => string): FailureIdentity | null {
|
|
59
|
+
const diagnostic = result.diagnostic;
|
|
60
|
+
if (result.ok || !diagnostic || diagnostic.execution === undefined || diagnostic.effects === undefined) return null;
|
|
61
|
+
const context = target(call, identify), data = record(result.data) ? result.data : {};
|
|
62
|
+
const current = data["sha256"];
|
|
63
|
+
const effect = result.effect;
|
|
64
|
+
const known = state.versions.get(context.resource);
|
|
65
|
+
const version = diagnostic.code === "permission_denied" ? "unknown" : current === null ? "absent" : digest(current) ? current
|
|
66
|
+
: effect?.kind === "command" && effect.workspace_observed !== false && digest(effect.workspace_sha256) ? effect.workspace_sha256
|
|
67
|
+
: digest(known) ? known : "unknown";
|
|
68
|
+
const input = context.input;
|
|
69
|
+
let premise: unknown;
|
|
70
|
+
if (diagnostic.code === "version_mismatch") {
|
|
71
|
+
const precondition = record(data["precondition"]) ? data["precondition"] : {};
|
|
72
|
+
premise = { expected: precondition["expected"] === null ? "absent"
|
|
73
|
+
: precondition["expected"] ?? input["expected_sha256"] ?? input["read_handle"] ?? "unknown" };
|
|
74
|
+
} else if (diagnostic.code === "permission_denied") {
|
|
75
|
+
// Replacement wording and requested shell timeouts cannot change a denied capability.
|
|
76
|
+
premise = { operation: context.family === "command" ? effect?.kind === "command" ? effect.cmd.trim()
|
|
77
|
+
: String(input["command"] ?? input["cmd"] ?? "").trim() : context.family };
|
|
78
|
+
} else if (context.family === "file_change" && ["not_found", "ambiguous_edit"].includes(diagnostic.code)) {
|
|
79
|
+
const edits = Array.isArray(input["edits"]) ? input["edits"] : [input];
|
|
80
|
+
premise = edits.map(edit => record(edit) ? { old_string: edit["old_string"], replace_all: edit["replace_all"] === true } : edit);
|
|
81
|
+
} else if (context.family === "command") {
|
|
82
|
+
// Shared launch contract equates bash/exec_command; arbitrary shell strings are not parsed as equivalent.
|
|
83
|
+
premise = { command: effect?.kind === "command" ? effect.cmd.trim() : String(input["command"] ?? input["cmd"] ?? "").trim(),
|
|
84
|
+
exit: effect?.kind === "command" ? effect.exit : null,
|
|
85
|
+
timeout: input["timeout"] ?? null, tty: input["tty"] === true, change: state.change };
|
|
86
|
+
} else {
|
|
87
|
+
const normalized = { ...input };
|
|
88
|
+
if (fileTools.includes(call.name) && typeof normalized["path"] === "string") normalized["path"] = context.resource;
|
|
89
|
+
premise = normalized;
|
|
90
|
+
}
|
|
91
|
+
return { family: context.family, resource: context.resource, version, code: diagnostic.code,
|
|
92
|
+
execution: diagnostic.execution, effects: diagnostic.effects, premise: canonicalValue(premise) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function changedPremises(state: RecoveryState, changed: boolean, resource?: string, version?: string): RecoveryState {
|
|
96
|
+
return { ...state, change: state.change + Number(changed), failures: new Map([...state.failures].filter(([, run]) =>
|
|
97
|
+
run.identity.code === "permission_denied" || !(changed && run.identity.family === "command" ||
|
|
98
|
+
resource !== undefined && version !== undefined && run.identity.resource === resource && run.identity.version !== version))) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Incremental disposable projection; folding the original ancestry yields exactly the same state. */
|
|
102
|
+
export function advanceRecovery(state: RecoveryState, events: readonly LogEvent[], identify: (path: string) => string): RecoveryState {
|
|
103
|
+
let current = state;
|
|
104
|
+
for (const event of events) {
|
|
105
|
+
if (event.t === "user") { current = { ...current, failures: new Map() }; continue; }
|
|
106
|
+
if (event.t === "assistant") {
|
|
107
|
+
const calls = new Map(current.calls);
|
|
108
|
+
for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
|
|
109
|
+
current = { ...current, calls }; continue;
|
|
110
|
+
}
|
|
111
|
+
if (event.t === "tool_invocation") {
|
|
112
|
+
if (event.input !== undefined) current = { ...current, calls: new Map(current.calls).set(event.call_id, { name: event.name, input: event.input }) };
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (event.t === "guidance" && event.reason === "churn") {
|
|
116
|
+
current = { ...current, failures: new Map([...current.failures].map(([group, run]) =>
|
|
117
|
+
[group, event.sources.includes(run.sources[0]!) ? { ...run, notified: true } : run])) }; continue;
|
|
118
|
+
}
|
|
119
|
+
if (event.t === "workspace_observation" && event.changed) {
|
|
120
|
+
current = changedPremises(current, true); continue;
|
|
121
|
+
}
|
|
122
|
+
if (event.t === "checkpoint" && event.phase === "applied") {
|
|
123
|
+
for (const [path, change] of Object.entries(event.changes))
|
|
124
|
+
current = changedPremises(current, change.before?.sha256 !== change.after?.sha256 || change.before?.mode !== change.after?.mode,
|
|
125
|
+
identify(path), change.after === null ? "absent" : change.after.sha256);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (event.t !== "tool_result") continue;
|
|
129
|
+
const call = current.calls.get(event.call_id), calls = new Map(current.calls); calls.delete(event.call_id);
|
|
130
|
+
current = { ...current, calls };
|
|
131
|
+
const effect = event.effect;
|
|
132
|
+
if (effect?.kind === "edit" || effect?.kind === "read") {
|
|
133
|
+
const resource = identify(effect.path), previous = current.versions.get(resource);
|
|
134
|
+
const version = effect.kind === "edit" ? effect.sha256_after : effect.sha256;
|
|
135
|
+
const changed = effect.kind === "edit" ? effect.sha256_before !== effect.sha256_after : digest(previous) && previous !== version;
|
|
136
|
+
current = changedPremises({ ...current, versions: new Map(current.versions).set(resource, version) }, changed,
|
|
137
|
+
resource, digest(version) ? version : undefined);
|
|
138
|
+
}
|
|
139
|
+
if (!call) continue;
|
|
140
|
+
const group = groupKey(call, event, identify);
|
|
141
|
+
if (event.ok) {
|
|
142
|
+
const failures = new Map(current.failures); failures.delete(group);
|
|
143
|
+
current = { ...current, failures }; continue;
|
|
144
|
+
}
|
|
145
|
+
const identity = failureIdentity(call, event, current, identify);
|
|
146
|
+
if (!identity) continue;
|
|
147
|
+
const key = canonicalValue(identity), previous = current.failures.get(group);
|
|
148
|
+
const run: FailureRun = previous?.key === key ? { ...previous, count: previous.count + 1,
|
|
149
|
+
sources: [...previous.sources.slice(0, 2), event.id] }
|
|
150
|
+
: { identity, key, count: 1, sources: [event.id], notified: false };
|
|
151
|
+
current = { ...current, failures: new Map(current.failures).set(group, run) };
|
|
152
|
+
}
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function recoveryReview(state: RecoveryState, tools?: readonly string[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
|
|
157
|
+
for (const run of state.failures.values()) {
|
|
158
|
+
if (run.count < RECOVERY_FAILURES || run.notified) continue;
|
|
159
|
+
const identity = run.identity;
|
|
160
|
+
// Recompute trusted instructions; never promote arbitrary tool error text to harness instructions.
|
|
161
|
+
const diagnostic = failureDiagnostic(identity.code, "", { execution: identity.execution, effects: identity.effects });
|
|
162
|
+
const policy = tools === undefined ? diagnostic : scopeFailureGuidance(diagnostic, tools);
|
|
163
|
+
return { reason: "churn", sources: run.sources,
|
|
164
|
+
text: `Harness: ${run.count} attempts share an unchanged failure premise in the recorded facts. ` +
|
|
165
|
+
`Tool family ${JSON.stringify(identity.family)}; resource label (data) ${JSON.stringify(identity.resource.slice(0, 160))}; ` +
|
|
166
|
+
`code ${identity.code}; version ${identity.version}; execution ${identity.execution}; effects ${identity.effects}. ` +
|
|
167
|
+
`${policy.next} ` + (policy.retry === "after_inspection" ? "Inspect actual state for partial effects before any replay. " : "") +
|
|
168
|
+
"Identify which relevant premise changed before another attempt; changing replacement wording is not a new premise. " +
|
|
169
|
+
(identity.version === "unknown" ? "The resource version is unknown; this notice does not prove external state is unchanged. " : "") +
|
|
170
|
+
"This is recovery advice, not a task-failure verdict or authorization to replay. Original result references: " + run.sources.join(", ") + "." };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** R4 / D8: a read descriptor names immutable version premises; it grants no authority. */
|
|
2
|
+
import type { ObservationReceipt } from "./read-observation.ts";
|
|
3
|
+
import { isObservationArtifact } from "./observation-view.ts";
|
|
4
|
+
import type { LogEvent } from "./events.ts";
|
|
5
|
+
|
|
6
|
+
export interface FileIdentity { host: string; workspace: string; resource: string }
|
|
7
|
+
export interface FileReadDescriptor {
|
|
8
|
+
kind: "file_read";
|
|
9
|
+
version: 1;
|
|
10
|
+
source: ObservationReceipt["source"];
|
|
11
|
+
selection: ObservationReceipt["selection"];
|
|
12
|
+
}
|
|
13
|
+
export interface FilePrecondition {
|
|
14
|
+
source: "read_handle" | "expected_sha256" | "session" | "create_only" | "unobserved";
|
|
15
|
+
expected: string | null;
|
|
16
|
+
read_handle?: string;
|
|
17
|
+
}
|
|
18
|
+
export type FilePreconditionDecision = { ok: true; precondition: FilePrecondition } |
|
|
19
|
+
{ ok: false; code: "invalid_input" | "version_mismatch"; message: string; precondition?: FilePrecondition };
|
|
20
|
+
|
|
21
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
23
|
+
export const isFileDigest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
24
|
+
|
|
25
|
+
/** Preserve chronology across path aliases; handoff anchors intentionally keep their legacy spelling. */
|
|
26
|
+
export function latestObservedFileVersion(events: readonly LogEvent[], resource: string, identify: (path: string) => string): string | undefined {
|
|
27
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
28
|
+
const event = events[index]!;
|
|
29
|
+
if (event.t !== "tool_result") continue;
|
|
30
|
+
const effect = event.effect;
|
|
31
|
+
if ((effect?.kind === "read" || effect?.kind === "edit") && identify(effect.path) === resource)
|
|
32
|
+
return effect.kind === "read" ? effect.sha256 : effect.sha256_after;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isFileReadDescriptor(value: unknown): value is FileReadDescriptor {
|
|
38
|
+
if (!record(value) || value["kind"] !== "file_read" || value["version"] !== 1) return false;
|
|
39
|
+
const source = value["source"], selection = value["selection"];
|
|
40
|
+
return record(source) && ["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") &&
|
|
41
|
+
isFileDigest(source["sha256"]) && isObservationArtifact(source["raw"]) && source["raw"].sha256 === source["sha256"] &&
|
|
42
|
+
record(selection) && natural(selection["start"]) && selection["start"] >= 1 && natural(selection["end"]) &&
|
|
43
|
+
natural(selection["total_lines"]) && selection["start"] <= selection["end"] && selection["end"] <= selection["total_lines"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Explicit premises take precedence; an omitted premise may use the complete log projection. */
|
|
47
|
+
export function checkFilePrecondition(identity: FileIdentity, current: string | null, input: {
|
|
48
|
+
descriptor?: FileReadDescriptor; read_handle?: string; expected_sha256?: string; observed?: string; create_only?: boolean;
|
|
49
|
+
}): FilePreconditionDecision {
|
|
50
|
+
const invalid = (message: string): FilePreconditionDecision => ({ ok: false, code: "invalid_input", message });
|
|
51
|
+
if (input.expected_sha256 !== undefined && !isFileDigest(input.expected_sha256)) return invalid("expected_sha256 must be a lowercase SHA-256 digest");
|
|
52
|
+
if (input.observed !== undefined && !isFileDigest(input.observed)) return invalid("the observed file version is invalid; inspect the original log");
|
|
53
|
+
if (input.create_only && (input.read_handle !== undefined || input.expected_sha256 !== undefined))
|
|
54
|
+
return invalid("create_only cannot be combined with read_handle or expected_sha256");
|
|
55
|
+
if ((input.read_handle === undefined) !== (input.descriptor === undefined)) return invalid("read_handle requires its verified descriptor");
|
|
56
|
+
if (input.descriptor) {
|
|
57
|
+
if (!isFileReadDescriptor(input.descriptor) || !isFileDigest(input.read_handle)) return invalid("invalid read_handle descriptor");
|
|
58
|
+
for (const field of ["host", "workspace", "resource"] as const)
|
|
59
|
+
if (input.descriptor.source[field] !== identity[field]) return invalid(`read_handle names another ${field}; read the intended file here`);
|
|
60
|
+
if (input.expected_sha256 !== undefined && input.expected_sha256 !== input.descriptor.source.sha256)
|
|
61
|
+
return invalid("read_handle and expected_sha256 name different versions");
|
|
62
|
+
}
|
|
63
|
+
const precondition: FilePrecondition = input.create_only ? { source: "create_only", expected: null }
|
|
64
|
+
: input.descriptor ? { source: "read_handle", expected: input.descriptor.source.sha256, read_handle: input.read_handle! }
|
|
65
|
+
: input.expected_sha256 !== undefined ? { source: "expected_sha256", expected: input.expected_sha256 }
|
|
66
|
+
: input.observed !== undefined ? { source: "session", expected: input.observed }
|
|
67
|
+
: { source: "unobserved", expected: null };
|
|
68
|
+
if (precondition.source !== "unobserved" && precondition.expected !== current)
|
|
69
|
+
return { ok: false, code: "version_mismatch", precondition,
|
|
70
|
+
message: `file version changed; expected ${precondition.expected ?? "absent"}, current sha256: ${current ?? "absent"}. Read the current file before retrying.` };
|
|
71
|
+
return { ok: true, precondition };
|
|
72
|
+
}
|
package/src/core/handoff.ts
CHANGED
|
@@ -19,6 +19,11 @@ import type { ProgressEvidence } from "./progress.ts";
|
|
|
19
19
|
import { isExecutionOutcome, type ExecutionOutcome } from "./outcome.ts";
|
|
20
20
|
import type { WorkItem } from "./work-state.ts";
|
|
21
21
|
import type { EvidenceLink } from "./evidence-links.ts";
|
|
22
|
+
import type { WorkingSet } from "./working-set.ts";
|
|
23
|
+
import { RECOVERY_SECTIONS, RECOVERY_KINDS } from "./recovery-sources.ts";
|
|
24
|
+
import type { AcceptanceState } from "./acceptance.ts";
|
|
25
|
+
import type { LoadedSkill } from "./skills.ts";
|
|
26
|
+
import type { ProcessObservation } from "./process-state.ts";
|
|
22
27
|
import { emitYaml } from "./yaml.ts";
|
|
23
28
|
|
|
24
29
|
export const HANDOFF_VERSION = 1 as const;
|
|
@@ -34,6 +39,8 @@ export type Evidence = {
|
|
|
34
39
|
event_id: string;
|
|
35
40
|
/** Exact observed workspace after the command; absence means legacy/unknown. */
|
|
36
41
|
workspace_sha256?: string;
|
|
42
|
+
/** P.3: omitted on historical full-workspace observations. */
|
|
43
|
+
workspace_scope?: "source-v1";
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export type CompletedItem = {
|
|
@@ -79,12 +86,16 @@ export type BlockedItem = {
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export type DecisionItem = {
|
|
89
|
+
work_id?: string;
|
|
90
|
+
source?: { node: number; event_id: string };
|
|
82
91
|
chose: string;
|
|
83
92
|
because: string;
|
|
84
93
|
rejected?: readonly string[];
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
export type PitfallItem = {
|
|
97
|
+
work_id?: string;
|
|
98
|
+
source?: { node: number; event_id: string };
|
|
88
99
|
tried: string;
|
|
89
100
|
failed_because: string;
|
|
90
101
|
}
|
|
@@ -110,11 +121,13 @@ export type BranchRecord = {
|
|
|
110
121
|
/** Event id in this node where the branch was spawned. */
|
|
111
122
|
spawned_at: EventId;
|
|
112
123
|
purpose: string;
|
|
124
|
+
background?: true;
|
|
113
125
|
/** null means unresolved, which D16 requires be visible rather than quiet. */
|
|
114
126
|
resolution: "merged" | "discarded" | null;
|
|
115
127
|
/** Present once the branch joined; a path, never the handoff's contents. */
|
|
116
128
|
handoff_ref?: string;
|
|
117
129
|
summary?: string;
|
|
130
|
+
receipt_ref?: string;
|
|
118
131
|
outcome?: ExecutionOutcome;
|
|
119
132
|
};
|
|
120
133
|
|
|
@@ -126,6 +139,7 @@ export type VerificationItem = {
|
|
|
126
139
|
at?: IsoTime;
|
|
127
140
|
exit?: number;
|
|
128
141
|
workspace_sha256?: string;
|
|
142
|
+
workspace_scope?: "source-v1";
|
|
129
143
|
stale?: true;
|
|
130
144
|
}
|
|
131
145
|
|
|
@@ -142,6 +156,11 @@ export type UserTurnRecord = {
|
|
|
142
156
|
|
|
143
157
|
export type Handoff = {
|
|
144
158
|
handoff_version: typeof HANDOFF_VERSION | 2;
|
|
159
|
+
working_set?: WorkingSet;
|
|
160
|
+
acceptance?: AcceptanceState;
|
|
161
|
+
loaded_skills?: readonly LoadedSkill[];
|
|
162
|
+
/** O.14: last observations only; live capabilities cannot survive a restart. */
|
|
163
|
+
processes?: readonly ProcessObservation[];
|
|
145
164
|
/** V2 arrays are carried suffixes; the immutable source rebuilds every page. */
|
|
146
165
|
history?: {
|
|
147
166
|
source: NodeRef;
|
|
@@ -219,8 +238,8 @@ export type Handoff = {
|
|
|
219
238
|
const ORDER: Record<string, readonly string[]> = {
|
|
220
239
|
"": [
|
|
221
240
|
"handoff_version", "predecessor", "history",
|
|
222
|
-
"objective", "user_turns", "user_turns_truncated",
|
|
223
|
-
"state", "decisions", "pitfalls", "file_anchors",
|
|
241
|
+
"objective", "working_set", "user_turns", "user_turns_truncated",
|
|
242
|
+
"state", "acceptance", "loaded_skills", "processes", "decisions", "pitfalls", "file_anchors",
|
|
224
243
|
"verification", "branches", "open_questions", "progress_evidence", "evidence_links",
|
|
225
244
|
"continuation_valid", "budget",
|
|
226
245
|
],
|
|
@@ -239,7 +258,9 @@ const ORDER: Record<string, readonly string[]> = {
|
|
|
239
258
|
pitfalls: ["tried", "failed_because"],
|
|
240
259
|
file_anchors: ["path", "sha256", "read_ranges", "touched"],
|
|
241
260
|
verification: ["gate", "status", "at", "exit"],
|
|
242
|
-
branches: ["node", "spawned_at", "purpose", "resolution", "handoff_ref", "summary", "outcome"],
|
|
261
|
+
branches: ["node", "spawned_at", "purpose", "resolution", "handoff_ref", "summary", "outcome", "receipt_ref", "background"],
|
|
262
|
+
processes: ["session", "node", "event", "observed", "command", "exit", "output"],
|
|
263
|
+
"processes.output": ["sha256", "path", "bytes"],
|
|
243
264
|
"branches.outcome": ["status", "reason"],
|
|
244
265
|
evidence_links: ["id", "work", "what", "event", "resource", "version", "freshness", "superseded_by"],
|
|
245
266
|
budget: ["nodes_on_path", "nodes_without_progress"],
|
|
@@ -279,6 +300,10 @@ export type Violation = {
|
|
|
279
300
|
| "INVALID_EXECUTION_OUTCOME"
|
|
280
301
|
| "INVALID_WORK_STATE"
|
|
281
302
|
| "INVALID_EVIDENCE_LINK"
|
|
303
|
+
| "INVALID_WORKING_SET"
|
|
304
|
+
| "INVALID_ACCEPTANCE"
|
|
305
|
+
| "INVALID_SKILL_SOURCE"
|
|
306
|
+
| "INVALID_PROCESS_STATE"
|
|
282
307
|
| "BAD_VERSION";
|
|
283
308
|
detail: string;
|
|
284
309
|
}
|
|
@@ -297,6 +322,10 @@ export function validateHandoff(h: Handoff): Violation[] {
|
|
|
297
322
|
}
|
|
298
323
|
const workIds = new Set<string>();
|
|
299
324
|
for (const item of h.state.work_items ?? []) {
|
|
325
|
+
if (item.depends_on && (new Set(item.depends_on).size !== item.depends_on.length || item.depends_on.includes(item.id) ||
|
|
326
|
+
item.depends_on.some(dep => !dep.trim())) || item.owner !== undefined && (!item.owner.trim() || !item.owner_event)) {
|
|
327
|
+
v.push({ code: "INVALID_WORK_STATE", detail: `invalid dependencies or ownership for work ${item.id}` });
|
|
328
|
+
}
|
|
300
329
|
const proven = item.verification === "verified" || item.verification === "stale";
|
|
301
330
|
if (!item.id.trim() || workIds.has(item.id) || !item.what.trim() || !item.opened_at || !item.updated_at ||
|
|
302
331
|
!["active", "blocked", "completed", "cancelled"].includes(item.status) ||
|
|
@@ -318,13 +347,29 @@ export function validateHandoff(h: Handoff): Violation[] {
|
|
|
318
347
|
if (h.handoff_version !== HANDOFF_VERSION && h.handoff_version !== 2) {
|
|
319
348
|
v.push({ code: "BAD_VERSION", detail: `expected ${HANDOFF_VERSION}, got ${h.handoff_version}` });
|
|
320
349
|
}
|
|
350
|
+
if (h.working_set) {
|
|
351
|
+
const view = h.working_set;
|
|
352
|
+
if (![1, 2].includes(view.version) || view.source.node !== h.predecessor?.node || view.source.sha256 !== h.predecessor?.sha256 ||
|
|
353
|
+
!Number.isSafeInteger(view.omitted.work) || view.omitted.work < 0 || !Number.isSafeInteger(view.omitted.references) || view.omitted.references < 0 ||
|
|
354
|
+
new Set(view.work.map(item => item.id)).size !== view.work.length || view.work.some(item => !["active", "blocked"].includes(item.status)) ||
|
|
355
|
+
view.references.some(reference => !reference.event) ||
|
|
356
|
+
view.version === 2 && (!view.recovery_sections || RECOVERY_SECTIONS.some(section => {
|
|
357
|
+
const range = view.recovery_sections![section], references = view.references.filter(ref => ref.kind === RECOVERY_KINDS[section]);
|
|
358
|
+
return !range || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.omitted) || range.omitted < 0 || range.omitted > range.total ||
|
|
359
|
+
range.total - range.omitted !== references.length || new Set(references.map(ref => ref.event)).size !== references.length ||
|
|
360
|
+
references.some(ref => !Number.isSafeInteger(ref.node) || Number(ref.node) < 0 || ref.provenance !==
|
|
361
|
+
(section === "users" ? "user_message" : section === "next_actions" ? "model_annotation" : "observation"));
|
|
362
|
+
}))) {
|
|
363
|
+
v.push({ code: "INVALID_WORKING_SET", detail: "working set must reference its source and explicit active state" });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
321
366
|
if (h.handoff_version === 2) {
|
|
322
367
|
const history = h.history;
|
|
323
368
|
if (!history || !h.predecessor || history.source.node !== h.predecessor.node ||
|
|
324
369
|
history.source.sha256 !== h.predecessor.sha256) {
|
|
325
370
|
v.push({ code: "INVALID_HISTORY", detail: "paged handoff must identify its immutable predecessor" });
|
|
326
371
|
} else for (const [section, range] of Object.entries(history.sections)) {
|
|
327
|
-
const values = section.startsWith("state.") ? h.state[section.slice(6) as keyof Handoff["state"]]
|
|
372
|
+
const values = section === "acceptance.criteria" ? h.acceptance?.criteria : section === "acceptance.targets" ? h.acceptance?.targets : section.startsWith("state.") ? h.state[section.slice(6) as keyof Handoff["state"]]
|
|
328
373
|
: h[section as keyof Handoff];
|
|
329
374
|
if (!Array.isArray(values) || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.offset) ||
|
|
330
375
|
range.offset < 0 || range.total < range.offset || range.total - range.offset !== values.length) {
|
|
@@ -333,6 +378,22 @@ export function validateHandoff(h: Handoff): Violation[] {
|
|
|
333
378
|
}
|
|
334
379
|
}
|
|
335
380
|
|
|
381
|
+
if (h.acceptance?.criteria.some(item => item.status === "pass" && (!item.evidence || item.evidence.exit !== 0) ||
|
|
382
|
+
item.status === "accepted" && (item.kind !== "human" || !item.accepted_event || item.target && !item.accepted_binding))) {
|
|
383
|
+
v.push({ code: "INVALID_ACCEPTANCE", detail: "criteria need actual command or user decision evidence" });
|
|
384
|
+
}
|
|
385
|
+
if (h.acceptance?.targets?.some(target => target.status === "ready" && (!target.observation || !target.binding || !target.session || !target.pid || !target.entry || !target.build_event))) {
|
|
386
|
+
v.push({ code: "INVALID_ACCEPTANCE", detail: "ready targets need original build and owned instance observations" });
|
|
387
|
+
}
|
|
388
|
+
if (h.loaded_skills?.some(item => !/^[a-f0-9]{64}$/.test(item.sha256) || item.sha256 !== item.artifact ||
|
|
389
|
+
!item.path || !item.event || !Number.isSafeInteger(item.node) || item.node < 0)) {
|
|
390
|
+
v.push({ code: "INVALID_SKILL_SOURCE", detail: "loaded skill must reference original instruction bytes and event" });
|
|
391
|
+
}
|
|
392
|
+
if (h.processes?.some(item => !Number.isSafeInteger(item.session) || item.session < 1 || !Number.isSafeInteger(item.node) || item.node < 0 ||
|
|
393
|
+
!item.event || !["running", "exited"].includes(item.observed) || item.observed === "exited" && !Number.isInteger(item.exit) ||
|
|
394
|
+
item.output && (!/^[a-f0-9]{64}$/.test(item.output.sha256) || !Number.isSafeInteger(item.output.bytes) || item.output.bytes < 0 || !item.output.path))) {
|
|
395
|
+
v.push({ code: "INVALID_PROCESS_STATE", detail: "process state requires original observation and actual exit evidence" });
|
|
396
|
+
}
|
|
336
397
|
for (const c of h.state.completed) {
|
|
337
398
|
if (c.evidence.exit !== 0) {
|
|
338
399
|
v.push({
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
3
|
import { canonicalValue } from "./continuation.ts";
|
|
4
4
|
import { deriveEvidenceLinks, observationResources, resourceKey, type EvidenceLink } from "./evidence-links.ts";
|
|
5
|
+
import type { EnvironmentValidity } from "./environment.ts";
|
|
5
6
|
|
|
6
|
-
export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome" | "data" | "diagnostic"> {
|
|
7
|
+
export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome" | "data" | "data_ref" | "diagnostic" | "observation" | "warnings"> {
|
|
7
8
|
node: number;
|
|
8
9
|
event: string;
|
|
9
10
|
kind: string;
|
|
@@ -16,27 +17,62 @@ export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome"
|
|
|
16
17
|
work_ids?: readonly string[];
|
|
17
18
|
evidence?: readonly EvidenceLink[];
|
|
18
19
|
source_available?: boolean;
|
|
20
|
+
/** Runtime consumption view only; never changes the original capability receipt. */
|
|
21
|
+
environment?: EnvironmentValidity;
|
|
22
|
+
invocation?: { event: string; input: string; schema: string | null; parent_call_id?: string; origin?: "extension" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** O.15: bump when historyEntries or freshness semantics change. */
|
|
26
|
+
export const HISTORY_INDEX_VERSION = 6;
|
|
27
|
+
export function historyKeyInput(nodes: readonly { node: number; events: readonly LogEvent[] }[], version = HISTORY_INDEX_VERSION): string {
|
|
28
|
+
const calls = new Map<string, string>();
|
|
29
|
+
const self = (name: string | undefined) => name === "history" || name === "note";
|
|
30
|
+
return canonicalValue({ version, nodes: nodes.map(part => ({ node: part.node, events: part.events.flatMap<LogEvent>(event => {
|
|
31
|
+
// The index deliberately excludes its own reads. Effects still affect
|
|
32
|
+
// freshness, and annotations have their own durable events (O.6).
|
|
33
|
+
if (event.t === "tool_invocation") { calls.set(event.call_id, event.name); return self(event.name) ? [] : [event]; }
|
|
34
|
+
if (event.t === "tool_result" && self(calls.get(event.call_id)) && event.effect === undefined) return [];
|
|
35
|
+
if (event.t !== "assistant") return [event];
|
|
36
|
+
for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
|
|
37
|
+
const blocks = event.blocks.filter(block => block.type === "text" || block.type === "tool_use" && !self(block.name));
|
|
38
|
+
return blocks.length ? [{ ...event, blocks }] : [];
|
|
39
|
+
}) })) });
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
export function historyEntries(nodes: readonly { node: number; events: readonly LogEvent[] }[]): HistoryEntry[] {
|
|
22
43
|
const entries: HistoryEntry[] = [];
|
|
23
44
|
const calls = new Map<string, { name: string; input: unknown }>();
|
|
45
|
+
const invocations = new Map<string, NonNullable<HistoryEntry["invocation"]>>();
|
|
24
46
|
for (const { node, events } of nodes) for (const e of events) {
|
|
25
47
|
if (e.t === "assistant") {
|
|
26
48
|
for (const block of e.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
|
|
27
49
|
const text = e.blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
28
50
|
if (text) entries.push({ node, event: e.id, kind: e.t, text });
|
|
51
|
+
} else if (e.t === "tool_invocation") {
|
|
52
|
+
calls.set(e.call_id, { name: e.name, input: e.input ?? calls.get(e.call_id)?.input ?? { artifact: e.input_ref.sha256 } });
|
|
53
|
+
invocations.set(e.call_id, { event: e.id, input: e.input_ref.sha256, schema: e.schema_ref?.sha256 ?? null,
|
|
54
|
+
...(e.origin ? { origin: e.origin } : {}),
|
|
55
|
+
...(e.parent_call_id ? { parent_call_id: e.parent_call_id } : {}) });
|
|
29
56
|
} else if (e.t === "tool_result") {
|
|
30
57
|
const call = calls.get(e.call_id);
|
|
31
58
|
// Indexing query results recursively would make the index amplify itself.
|
|
32
59
|
if (call?.name === "history" || call?.name === "note") continue;
|
|
33
60
|
entries.push({ node, event: e.id, kind: e.t, text: e.content, ok: e.ok,
|
|
34
61
|
...(call ? { tool: call.name, input: call.input } : {}),
|
|
62
|
+
...(invocations.has(e.call_id) ? { invocation: invocations.get(e.call_id)! } : {}),
|
|
35
63
|
resources: observationResources(e, call?.input),
|
|
36
64
|
...(e.effect ? { effect: e.effect } : {}), ...(e.outcome ? { outcome: e.outcome } : {}),
|
|
37
65
|
...(e.data !== undefined ? { data: e.data } : {}), ...(e.diagnostic ? { diagnostic: e.diagnostic } : {}),
|
|
66
|
+
...(e.data_ref ? { data_ref: e.data_ref } : {}),
|
|
67
|
+
...(e.observation ? { observation: e.observation } : {}),
|
|
68
|
+
...(e.warnings ? { warnings: e.warnings } : {}),
|
|
38
69
|
...(e.content_ref ? { artifact: e.content_ref.sha256 } : {}) });
|
|
39
|
-
} else if (
|
|
70
|
+
} else if (e.t === "target_observation") {
|
|
71
|
+
entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e.observation),
|
|
72
|
+
...(e.observation.manifest ? { artifact: e.observation.manifest.sha256 } : {}) });
|
|
73
|
+
} else if (e.t === "process_exit") {
|
|
74
|
+
entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e), artifact: e.output.sha256 });
|
|
75
|
+
} else if (["work_owner", "work_dependencies", "skill_loaded", "acceptance_contract", "acceptance_decision", "user", "next_action", "decision", "pitfall", "blocked", "question", "claim", "work_status", "evidence_link"].includes(e.t)) {
|
|
40
76
|
entries.push({ node, event: e.id, kind: e.t, text: e.t === "user" ? e.text : canonicalValue(e),
|
|
41
77
|
...("work_id" in e && e.work_id ? { work_ids: [e.work_id] } : {}),
|
|
42
78
|
...(e.t === "evidence_link" ? { work_ids: [e.work], ...(e.resource ? { resources: [resourceKey(e.resource)] } : {}) } : {}) });
|
package/src/core/history.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** D20: bounded carried state, with deterministic pages rebuilt from the log. */
|
|
2
2
|
import { emitHandoff, type Handoff } from "./handoff.ts";
|
|
3
3
|
import { estimateTokens } from "./tokens.ts";
|
|
4
|
+
import { workingSet, recoveryWorkingSet } from "./working-set.ts";
|
|
5
|
+
import type { RecoveryNodes } from "./recovery-sources.ts";
|
|
4
6
|
|
|
5
7
|
// A carried handoff must leave room for another segment's work. This budget is
|
|
6
8
|
// the existing 5k work floor, not a new segment threshold or an accuracy claim.
|
|
@@ -11,6 +13,10 @@ export const HISTORY_SECTIONS = [
|
|
|
11
13
|
"branches", "open_questions", "progress_evidence",
|
|
12
14
|
"state.work_items",
|
|
13
15
|
"evidence_links",
|
|
16
|
+
"acceptance.criteria",
|
|
17
|
+
"acceptance.targets",
|
|
18
|
+
"loaded_skills",
|
|
19
|
+
"processes",
|
|
14
20
|
] as const;
|
|
15
21
|
export type HistorySection = (typeof HISTORY_SECTIONS)[number];
|
|
16
22
|
|
|
@@ -22,6 +28,10 @@ export function historyItems(h: Handoff, section: HistorySection): readonly unkn
|
|
|
22
28
|
case "state.blocked": return h.state.blocked;
|
|
23
29
|
case "state.work_items": return h.state.work_items ?? [];
|
|
24
30
|
case "evidence_links": return h.evidence_links ?? [];
|
|
31
|
+
case "acceptance.criteria": return h.acceptance?.criteria ?? [];
|
|
32
|
+
case "acceptance.targets": return h.acceptance?.targets ?? [];
|
|
33
|
+
case "loaded_skills": return h.loaded_skills ?? [];
|
|
34
|
+
case "processes": return h.processes ?? [];
|
|
25
35
|
case "progress_evidence": return h.progress_evidence ?? [];
|
|
26
36
|
default: return h[section];
|
|
27
37
|
}
|
|
@@ -47,6 +57,10 @@ function suffixes(h: Handoff, offsets: Record<HistorySection, number>): Handoff
|
|
|
47
57
|
open_questions: h.open_questions.slice(offsets.open_questions),
|
|
48
58
|
progress_evidence: (h.progress_evidence ?? []).slice(offsets.progress_evidence),
|
|
49
59
|
...(h.evidence_links !== undefined ? { evidence_links: h.evidence_links.slice(offsets.evidence_links ?? 0) } : {}),
|
|
60
|
+
...(h.acceptance ? { acceptance: { ...h.acceptance, criteria: h.acceptance.criteria.slice(offsets["acceptance.criteria"] ?? 0),
|
|
61
|
+
...(h.acceptance.targets ? { targets: h.acceptance.targets.slice(offsets["acceptance.targets"] ?? 0) } : {}) } } : {}),
|
|
62
|
+
...(h.loaded_skills ? { loaded_skills: h.loaded_skills.slice(offsets.loaded_skills ?? 0) } : {}),
|
|
63
|
+
...(h.processes ? { processes: h.processes.slice(offsets.processes ?? 0) } : {}),
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
|
|
@@ -57,7 +71,11 @@ export function boundHandoff(full: Handoff, budget = DEFAULT_HANDOFF_TOKENS): Ha
|
|
|
57
71
|
if (full.predecessor === null) throw new Error("history requires an immutable source");
|
|
58
72
|
// An absent new section must not change legacy projections or frozen replay.
|
|
59
73
|
const sections = HISTORY_SECTIONS.filter(section => (section !== "state.work_items" || full.state.work_items !== undefined) &&
|
|
60
|
-
(section !== "evidence_links" || full.evidence_links !== undefined)
|
|
74
|
+
(section !== "evidence_links" || full.evidence_links !== undefined) &&
|
|
75
|
+
(section !== "acceptance.criteria" || full.acceptance !== undefined) &&
|
|
76
|
+
(section !== "acceptance.targets" || full.acceptance?.targets !== undefined) &&
|
|
77
|
+
(section !== "loaded_skills" || full.loaded_skills !== undefined) &&
|
|
78
|
+
(section !== "processes" || full.processes !== undefined));
|
|
61
79
|
const offsets = Object.fromEntries(sections.map((s) => [s, historyItems(full, s).length])) as Record<HistorySection, number>;
|
|
62
80
|
const build = (): Handoff => ({
|
|
63
81
|
...suffixes(full, offsets),
|
|
@@ -98,3 +116,22 @@ export function historyPage(full: Handoff, section: HistorySection, offset: numb
|
|
|
98
116
|
const end = Math.min(items.length, offset + limit);
|
|
99
117
|
return { total: items.length, offset, next: end < items.length ? end : null, items: items.slice(offset, end) };
|
|
100
118
|
}
|
|
119
|
+
|
|
120
|
+
/** O.10: the optional priority view shares the original total budget. */
|
|
121
|
+
export function boundWorkingHandoff(full: Handoff, budget = DEFAULT_HANDOFF_TOKENS): Handoff {
|
|
122
|
+
const legacy = boundHandoff(full, budget);
|
|
123
|
+
// 40% is a declared allocation hypothesis, not a measured context threshold.
|
|
124
|
+
const view = workingSet(full, budget * .4);
|
|
125
|
+
if (!view) return legacy;
|
|
126
|
+
try { return boundHandoff({ ...full, working_set: view }, budget); }
|
|
127
|
+
catch (error) { if (error instanceof RangeError) return legacy; throw error; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** R9 uses the same budget; originals are supplied by the ancestry owner. */
|
|
131
|
+
export function boundRecoveryHandoff(full: Handoff, nodes: RecoveryNodes, budget = DEFAULT_HANDOFF_TOKENS, tools?: readonly string[]): Handoff {
|
|
132
|
+
const legacy = boundHandoff(full, budget);
|
|
133
|
+
const view = recoveryWorkingSet(full, nodes, budget * .4, tools);
|
|
134
|
+
if (!view) return legacy;
|
|
135
|
+
try { return boundHandoff({ ...full, working_set: view }, budget); }
|
|
136
|
+
catch (error) { if (error instanceof RangeError) return legacy; throw error; }
|
|
137
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Q.13: observed connectivity is a cache, never authorization or replay evidence. */
|
|
2
|
+
export type IntegrationFailureCode = "offline" | "rate_limited" | "auth_required" | "protocol_error" | "timeout" | "unknown";
|
|
3
|
+
export type IntegrationTime = { at: number; tick: number };
|
|
4
|
+
export type IntegrationHealth = {
|
|
5
|
+
state: "not_checked" | "ready" | "backoff" | "auth_required" | "protocol_error";
|
|
6
|
+
observed: IntegrationTime | null;
|
|
7
|
+
failures: number;
|
|
8
|
+
retryAt: number | null;
|
|
9
|
+
error: IntegrationFailureCode | null;
|
|
10
|
+
};
|
|
11
|
+
// Assumed bounds, not measured optima: avoid tight failure loops and unbounded server delays.
|
|
12
|
+
export const INTEGRATION_BACKOFF_MS = 1_000;
|
|
13
|
+
export const MAX_INTEGRATION_BACKOFF_MS = 60_000;
|
|
14
|
+
export const INTEGRATION_HEALTH_TTL_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
export function initialIntegrationHealth(): IntegrationHealth {
|
|
17
|
+
return { state: "not_checked", observed: null, failures: 0, retryAt: null, error: null };
|
|
18
|
+
}
|
|
19
|
+
export function readyIntegrationHealth(now: IntegrationTime): IntegrationHealth {
|
|
20
|
+
return { ...initialIntegrationHealth(), state: "ready", observed: { ...now } };
|
|
21
|
+
}
|
|
22
|
+
export function failedIntegrationHealth(previous: IntegrationHealth, error: IntegrationFailureCode, now: IntegrationTime, retryAfterMs?: number): IntegrationHealth {
|
|
23
|
+
const failures = Math.min(16, previous.failures + 1);
|
|
24
|
+
const delay = Math.min(MAX_INTEGRATION_BACKOFF_MS, Math.max(INTEGRATION_BACKOFF_MS * 2 ** (failures - 1),
|
|
25
|
+
Number.isFinite(retryAfterMs) ? Math.max(0, retryAfterMs!) : 0));
|
|
26
|
+
const state = error === "auth_required" || error === "protocol_error" ? error : "backoff";
|
|
27
|
+
return { state, observed: { ...now }, failures, retryAt: state === "backoff" ? now.tick + delay : null, error };
|
|
28
|
+
}
|
|
29
|
+
export function integrationHealthView(health: IntegrationHealth, now: IntegrationTime) {
|
|
30
|
+
const age = health.observed === null ? null : Math.max(0, now.tick - health.observed.tick);
|
|
31
|
+
return { state: health.state, observed_at: health.observed?.at ?? null, age_ms: age,
|
|
32
|
+
stale: age === null || age >= INTEGRATION_HEALTH_TTL_MS, error: health.error,
|
|
33
|
+
failures: health.failures, retry_after_ms: health.retryAt === null ? null : Math.max(0, health.retryAt - now.tick) };
|
|
34
|
+
}
|