@visual-z/xeno 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
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({
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** D11 / O.3: classify durable execution facts, never error words in tool output. */
|
|
2
|
+
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
+
import type { BenchmarkUsage } from "./benchmark.ts";
|
|
4
|
+
import { canonicalValue } from "./continuation.ts";
|
|
5
|
+
|
|
6
|
+
export type ResultClass = "success" | "invalid_input" | "cancelled" | "timeout" | "command_nonzero" |
|
|
7
|
+
"execution_failed" | "rejected" | "unclassified_failure";
|
|
8
|
+
|
|
9
|
+
export function classifyToolResult(event: ToolResultEvent): ResultClass {
|
|
10
|
+
const code = event.diagnostic?.code;
|
|
11
|
+
if (code === "invalid_input") return "invalid_input";
|
|
12
|
+
if (code === "cancelled" || event.outcome?.reason === "cancelled") return "cancelled";
|
|
13
|
+
if (code === "timeout" || event.outcome?.reason === "timeout") return "timeout";
|
|
14
|
+
if (event.effect?.kind === "command" && event.effect.exit !== 0) return "command_nonzero";
|
|
15
|
+
if (event.ok) return "success";
|
|
16
|
+
if (event.diagnostic?.execution === "not_started" ||
|
|
17
|
+
["version_mismatch", "not_found", "ambiguous_edit", "permission_denied", "environment_missing"].includes(code ?? "")) return "rejected";
|
|
18
|
+
if (code === "execution_failed" || code === "unknown_effect" || event.outcome?.reason === "execution_error") return "execution_failed";
|
|
19
|
+
return "unclassified_failure";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DiagnosticRequest {
|
|
23
|
+
request: number;
|
|
24
|
+
node: number | null;
|
|
25
|
+
usage: BenchmarkUsage | null;
|
|
26
|
+
responseModel: string | null;
|
|
27
|
+
failure: string | null;
|
|
28
|
+
firstDeltaMs: number | null;
|
|
29
|
+
wallMs: number | null;
|
|
30
|
+
prefixUnchanged: boolean | null;
|
|
31
|
+
sharedInputBytes: number | null;
|
|
32
|
+
frozenSha256: string | null;
|
|
33
|
+
/** Raw terminal counts can remain known when a cache breakdown is absent. */
|
|
34
|
+
reportedPromptTokens?: number | null;
|
|
35
|
+
reportedOutputTokens?: number | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ToolCounters {
|
|
38
|
+
calls: number; providerCalls: number; invocations: number; nestedInvocations: number;
|
|
39
|
+
results: number; pairedCalls: number; orphanCalls: number; orphanResults: number;
|
|
40
|
+
duplicateProviderCalls: number; duplicateInvocations: number; duplicateResults: number;
|
|
41
|
+
repeatedProviderInputs: number; repeatedInvocationInputs: number;
|
|
42
|
+
classes: Record<ResultClass, number>;
|
|
43
|
+
diagnosticCodes: Record<string, number>;
|
|
44
|
+
commandExits: Record<string, number>;
|
|
45
|
+
expectedNonzeroCommands: number;
|
|
46
|
+
undeclaredNonzeroCommands: number;
|
|
47
|
+
}
|
|
48
|
+
const classes = (): Record<ResultClass, number> => ({ success: 0, invalid_input: 0, cancelled: 0, timeout: 0,
|
|
49
|
+
command_nonzero: 0, execution_failed: 0, rejected: 0, unclassified_failure: 0 });
|
|
50
|
+
const counters = (): ToolCounters => ({ calls: 0, providerCalls: 0, invocations: 0, nestedInvocations: 0,
|
|
51
|
+
results: 0, pairedCalls: 0, orphanCalls: 0, orphanResults: 0, duplicateProviderCalls: 0,
|
|
52
|
+
duplicateInvocations: 0, duplicateResults: 0, repeatedProviderInputs: 0, repeatedInvocationInputs: 0,
|
|
53
|
+
classes: classes(), diagnosticCodes: {}, commandExits: {}, expectedNonzeroCommands: 0, undeclaredNonzeroCommands: 0 });
|
|
54
|
+
function add(record: Record<string, number>, key: string): void { record[key] = (record[key] ?? 0) + 1; }
|
|
55
|
+
const validCount = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
|
|
56
|
+
const reportedCount = (value: number | null | undefined): number | null => typeof value === "number" && validCount(value) ? value : null;
|
|
57
|
+
const validTime = (value: number | null): number | null => value !== null && Number.isFinite(value) && value >= 0 ? value : null;
|
|
58
|
+
function observedUsage(usage: BenchmarkUsage | null): BenchmarkUsage | null {
|
|
59
|
+
return usage && [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens,
|
|
60
|
+
usage.cache_creation_input_tokens].every(validCount) ? usage : null;
|
|
61
|
+
}
|
|
62
|
+
function quantile(values: readonly number[], q: number): number | null {
|
|
63
|
+
return values.length ? values.toSorted((a, b) => a - b)[Math.max(0, Math.ceil(values.length * q) - 1)]! : null;
|
|
64
|
+
}
|
|
65
|
+
interface Call {
|
|
66
|
+
name: string; provider: boolean; invocation: boolean; nested: boolean;
|
|
67
|
+
providerInput: string | null; invocationInput: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inputs and outputs remain private. Equal-input counts use canonical original
|
|
72
|
+
* model arguments and immutable admitted-input hashes separately (O.3 / Pi A).
|
|
73
|
+
* Expected nonzero exits need an evaluator declaration; an exit alone cannot
|
|
74
|
+
* prove a command was accidental, and model prose cannot establish intent.
|
|
75
|
+
*/
|
|
76
|
+
export function summarizeHarnessDiagnostics(input: {
|
|
77
|
+
events: readonly LogEvent[]; requests: readonly DiagnosticRequest[];
|
|
78
|
+
expectedNonzeroCallIds?: readonly string[];
|
|
79
|
+
}) {
|
|
80
|
+
const tools: Record<string, ToolCounters> = {};
|
|
81
|
+
const tool = (name: string): ToolCounters => tools[name] ??= counters();
|
|
82
|
+
const calls = new Map<string, Call>(), results = new Map<string, ToolResultEvent[]>(), seenEvents = new Set<string>();
|
|
83
|
+
const guidance: Record<string, number> = {}, cuts: Record<string, number> = {}, cleanupProcessExits: Record<string, number> = {};
|
|
84
|
+
let duplicateEvents = 0, interruptions = 0;
|
|
85
|
+
for (const event of input.events) {
|
|
86
|
+
if (seenEvents.has(event.id)) { duplicateEvents++; continue; }
|
|
87
|
+
seenEvents.add(event.id);
|
|
88
|
+
if (event.t === "guidance") add(guidance, event.reason);
|
|
89
|
+
if (event.t === "cut") add(cuts, event.reason);
|
|
90
|
+
if (event.t === "interrupt") interruptions++;
|
|
91
|
+
if (event.t === "process_exit") add(cleanupProcessExits, String(event.exit));
|
|
92
|
+
if (event.t === "assistant") for (const block of event.blocks) {
|
|
93
|
+
if (block.type !== "tool_use") continue;
|
|
94
|
+
const existing = calls.get(block.call_id), stats = tool(block.name);
|
|
95
|
+
if (existing?.provider) stats.duplicateProviderCalls++;
|
|
96
|
+
calls.set(block.call_id, { name: existing?.name ?? block.name, provider: true,
|
|
97
|
+
invocation: existing?.invocation ?? false, nested: existing?.nested ?? false,
|
|
98
|
+
providerInput: canonicalValue(block.input), invocationInput: existing?.invocationInput ?? null });
|
|
99
|
+
}
|
|
100
|
+
if (event.t === "tool_invocation") {
|
|
101
|
+
const existing = calls.get(event.call_id), stats = tool(event.name);
|
|
102
|
+
if (existing?.invocation) stats.duplicateInvocations++;
|
|
103
|
+
calls.set(event.call_id, { name: existing?.name ?? event.name, provider: existing?.provider ?? false,
|
|
104
|
+
invocation: true, nested: event.parent_call_id !== undefined,
|
|
105
|
+
providerInput: existing?.providerInput ?? null, invocationInput: event.input_ref.sha256 });
|
|
106
|
+
}
|
|
107
|
+
if (event.t === "tool_result") {
|
|
108
|
+
const found = results.get(event.call_id) ?? [];
|
|
109
|
+
found.push(event); results.set(event.call_id, found);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const providerInputs = new Map<string, Set<string>>(), invocationInputs = new Map<string, Set<string>>();
|
|
113
|
+
const repeat = (groups: Map<string, Set<string>>, name: string, key: string | null): boolean => {
|
|
114
|
+
if (key === null) return false;
|
|
115
|
+
const seen = groups.get(name) ?? new Set<string>(), repeated = seen.has(key);
|
|
116
|
+
seen.add(key); groups.set(name, seen); return repeated;
|
|
117
|
+
};
|
|
118
|
+
for (const [id, call] of calls) {
|
|
119
|
+
const stats = tool(call.name); stats.calls++;
|
|
120
|
+
if (call.provider) stats.providerCalls++;
|
|
121
|
+
if (call.invocation) stats.invocations++;
|
|
122
|
+
if (call.nested) stats.nestedInvocations++;
|
|
123
|
+
if (repeat(providerInputs, call.name, call.providerInput)) stats.repeatedProviderInputs++;
|
|
124
|
+
if (repeat(invocationInputs, call.name, call.invocationInput)) stats.repeatedInvocationInputs++;
|
|
125
|
+
if (results.has(id)) stats.pairedCalls++; else stats.orphanCalls++;
|
|
126
|
+
}
|
|
127
|
+
const expected = new Set(input.expectedNonzeroCallIds ?? []);
|
|
128
|
+
for (const [id, found] of results) {
|
|
129
|
+
const call = calls.get(id), stats = tool(call?.name ?? "<orphan>");
|
|
130
|
+
stats.duplicateResults += Math.max(0, found.length - 1);
|
|
131
|
+
for (const event of found) {
|
|
132
|
+
stats.results++;
|
|
133
|
+
if (!call) stats.orphanResults++;
|
|
134
|
+
stats.classes[classifyToolResult(event)]++;
|
|
135
|
+
if (event.diagnostic) add(stats.diagnosticCodes, event.diagnostic.code);
|
|
136
|
+
if (event.effect?.kind === "command") {
|
|
137
|
+
add(stats.commandExits, String(event.effect.exit));
|
|
138
|
+
if (event.effect.exit !== 0) {
|
|
139
|
+
if (expected.has(id)) stats.expectedNonzeroCommands++; else stats.undeclaredNonzeroCommands++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const requestIds = new Set<number>(), requests: {
|
|
145
|
+
request: number; node: number | null; usageKnown: boolean; promptTokens: number | null;
|
|
146
|
+
cachedTokens: number | null; cacheWriteTokens: number | null; outputTokens: number | null;
|
|
147
|
+
cacheHitRate: number | null; responseModel: string | null; failure: string | null;
|
|
148
|
+
firstDeltaMs: number | null; wallMs: number | null; prefixUnchanged: boolean | null; sharedInputBytes: number | null;
|
|
149
|
+
}[] = [];
|
|
150
|
+
const models: Record<string, number> = {};
|
|
151
|
+
const usage = { knownRequests: 0, unknownRequests: 0, requestsWithCacheReads: 0, promptTokens: 0, cachedTokens: 0,
|
|
152
|
+
cacheWriteTokens: 0, outputTokens: 0, promptKnownRequests: 0, promptUnknownRequests: 0,
|
|
153
|
+
cacheCoveredPromptTokens: 0, cacheCoverageRate: null as number | null,
|
|
154
|
+
observedCacheHitRate: null as number | null, cacheHitRate: null as number | null };
|
|
155
|
+
const prefix = { sameNodeComparisons: 0, sameNodeMutations: 0, boundaryComparisons: 0, boundaryMutations: 0,
|
|
156
|
+
unknownComparisons: 0, sameNodeFrozenMutations: 0, boundaryFrozenMutations: 0 };
|
|
157
|
+
let previous: DiagnosticRequest | undefined, duplicateRequests = 0;
|
|
158
|
+
for (const request of input.requests.toSorted((a, b) => a.request - b.request)) {
|
|
159
|
+
if (requestIds.has(request.request)) { duplicateRequests++; continue; }
|
|
160
|
+
requestIds.add(request.request);
|
|
161
|
+
const observed = observedUsage(request.usage);
|
|
162
|
+
const prompt = reportedCount(request.reportedPromptTokens) ?? (observed ?
|
|
163
|
+
observed.input_tokens + observed.cache_read_input_tokens + observed.cache_creation_input_tokens : null);
|
|
164
|
+
const output = reportedCount(request.reportedOutputTokens) ?? observed?.output_tokens ?? null;
|
|
165
|
+
if (prompt !== null) { usage.promptKnownRequests++; usage.promptTokens += prompt; } else usage.promptUnknownRequests++;
|
|
166
|
+
if (output !== null) usage.outputTokens += output;
|
|
167
|
+
if (observed) {
|
|
168
|
+
usage.knownRequests++; usage.cacheCoveredPromptTokens += prompt!; usage.cachedTokens += observed.cache_read_input_tokens;
|
|
169
|
+
usage.cacheWriteTokens += observed.cache_creation_input_tokens;
|
|
170
|
+
if (observed.cache_read_input_tokens > 0) usage.requestsWithCacheReads++;
|
|
171
|
+
} else usage.unknownRequests++;
|
|
172
|
+
if (request.responseModel !== null) add(models, request.responseModel);
|
|
173
|
+
if (previous) {
|
|
174
|
+
if (previous.node === null || request.node === null || request.prefixUnchanged === null) prefix.unknownComparisons++;
|
|
175
|
+
else if (previous.node === request.node) { prefix.sameNodeComparisons++; if (!request.prefixUnchanged) prefix.sameNodeMutations++; }
|
|
176
|
+
else { prefix.boundaryComparisons++; if (!request.prefixUnchanged) prefix.boundaryMutations++; }
|
|
177
|
+
if (previous.frozenSha256 !== null && request.frozenSha256 !== null && previous.frozenSha256 !== request.frozenSha256) {
|
|
178
|
+
if (previous.node === request.node && request.node !== null) prefix.sameNodeFrozenMutations++;
|
|
179
|
+
else if (previous.node !== null && request.node !== null) prefix.boundaryFrozenMutations++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
previous = request;
|
|
183
|
+
requests.push({ request: request.request, node: request.node, usageKnown: observed !== null, promptTokens: prompt,
|
|
184
|
+
cachedTokens: observed?.cache_read_input_tokens ?? null, cacheWriteTokens: observed?.cache_creation_input_tokens ?? null,
|
|
185
|
+
outputTokens: output, cacheHitRate: observed && prompt ? observed.cache_read_input_tokens / prompt : null,
|
|
186
|
+
responseModel: request.responseModel, failure: request.failure, firstDeltaMs: validTime(request.firstDeltaMs),
|
|
187
|
+
wallMs: validTime(request.wallMs), prefixUnchanged: request.prefixUnchanged, sharedInputBytes: request.sharedInputBytes });
|
|
188
|
+
}
|
|
189
|
+
usage.observedCacheHitRate = usage.cacheCoveredPromptTokens > 0 ? usage.cachedTokens / usage.cacheCoveredPromptTokens : null;
|
|
190
|
+
usage.cacheCoverageRate = usage.promptUnknownRequests === 0 && usage.promptTokens > 0 ? usage.cacheCoveredPromptTokens / usage.promptTokens : null;
|
|
191
|
+
usage.cacheHitRate = usage.unknownRequests === 0 && usage.promptTokens > 0 ? usage.cachedTokens / usage.promptTokens : null;
|
|
192
|
+
const delta = requests.flatMap(r => r.firstDeltaMs === null ? [] : [r.firstDeltaMs]);
|
|
193
|
+
const totals = counters();
|
|
194
|
+
for (const stats of Object.values(tools)) {
|
|
195
|
+
for (const key of Object.keys(totals) as (keyof ToolCounters)[]) {
|
|
196
|
+
const value = stats[key];
|
|
197
|
+
if (typeof value === "number") (totals[key] as number) += value;
|
|
198
|
+
}
|
|
199
|
+
for (const key of Object.keys(stats.classes) as ResultClass[]) totals.classes[key] += stats.classes[key];
|
|
200
|
+
for (const key of Object.keys(stats.commandExits)) totals.commandExits[key] = (totals.commandExits[key] ?? 0) + stats.commandExits[key]!;
|
|
201
|
+
for (const key of Object.keys(stats.diagnosticCodes)) totals.diagnosticCodes[key] = (totals.diagnosticCodes[key] ?? 0) + stats.diagnosticCodes[key]!;
|
|
202
|
+
}
|
|
203
|
+
return { eventCount: seenEvents.size, duplicateEvents, tools, totals, guidance, cuts, interruptions, cleanupProcessExits,
|
|
204
|
+
requestCount: requests.length, duplicateRequests, models, usage, prefix, requests,
|
|
205
|
+
firstDeltaMs: { samples: delta.length, missing: requests.length - delta.length, p50: quantile(delta, 0.5), p95: quantile(delta, 0.95) } };
|
|
206
|
+
}
|
|
@@ -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
|
+
}
|
package/src/core/keymap.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* parses, reports conflicts by name, and lets the last definition win.
|
|
4
4
|
*
|
|
5
5
|
* Silent drop of one side of a conflict is how a shortcut "just stops
|
|
6
|
-
* working" with no trail (feat-theming design).
|
|
7
|
-
*
|
|
6
|
+
* working" with no trail (feat-theming design). The action list is
|
|
7
|
+
* xeno's (core/action-defaults.ts, D20).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ACTION_DEFAULTS, ACTION_ALIASES } from "./action-defaults.ts";
|
|
@@ -88,26 +88,14 @@ export function resolveAction(map: Keymap, spec: string): ActionId | undefined {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function defaultKeymap(platform: "posix" | "win32" | "wsl" = "posix"): Keymap {
|
|
91
|
-
//
|
|
92
|
-
return parseKeymap({ ...ACTION_DEFAULTS, ...(platform === "
|
|
93
|
-
"app.suspend": platform === "win32" ? [] : ["ctrl+z"],
|
|
94
|
-
"editor.undo": platform === "win32" ? ["ctrl+z"] : ["alt+z"],
|
|
95
|
-
"app.cycleModelReverse": ["alt+p"], "app.message.followUp": ["ctrl+q"],
|
|
96
|
-
"app.message.dequeue": ["alt+q"], "app.clipboard.pasteImage": ["alt+v"],
|
|
97
|
-
"tui.altScreen.previousPrompt": ["ctrl+up"], "tui.altScreen.nextPrompt": ["ctrl+down"],
|
|
98
|
-
"tui.altScreen.search": ["ctrl+f"],
|
|
99
|
-
}) });
|
|
91
|
+
// Windows has no job control; Ctrl+Z stays a plain key there.
|
|
92
|
+
return parseKeymap({ ...ACTION_DEFAULTS, ...(platform === "win32" ? { "app.suspend": [] } : {}) });
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
/**
|
|
95
|
+
/** Scopes share chords on purpose: arrows mean history in the composer and movement in a list. */
|
|
103
96
|
export function actionScope(action: string): string {
|
|
104
|
-
if (action
|
|
105
|
-
if (action
|
|
106
|
-
if (action.startsWith("tui.select.")) return "select";
|
|
107
|
-
if (action.startsWith("tui.altScreen.")) return "viewport";
|
|
108
|
-
if (action.startsWith("app.tree.")) return "tree";
|
|
109
|
-
if (action.startsWith("app.models.")) return "models";
|
|
110
|
-
if (action.startsWith("app.session.") && !["app.session.new", "app.session.tree", "app.session.fork", "app.session.resume"].includes(action)) return "session";
|
|
97
|
+
if (action.startsWith("editor.")) return "editor";
|
|
98
|
+
if (action.startsWith("list.")) return "list";
|
|
111
99
|
return "global";
|
|
112
100
|
}
|
|
113
101
|
export function parseKeymap(json: unknown, base?: Keymap): Keymap {
|
|
@@ -135,7 +123,7 @@ export function parseKeymap(json: unknown, base?: Keymap): Keymap {
|
|
|
135
123
|
}
|
|
136
124
|
}
|
|
137
125
|
const byKey: Record<string,ActionId> = {};
|
|
138
|
-
for (const [action,keys] of Object.entries(byAction)) if (
|
|
126
|
+
for (const [action,keys] of Object.entries(byAction)) if (actionScope(action) === "global") for (const key of keys) byKey[key] = action as ActionId;
|
|
139
127
|
return { byKey, byAction, diagnostics };
|
|
140
128
|
}
|
|
141
129
|
|