@visual-z/xeno 0.1.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.
Files changed (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Facts about the machine, fixed at session start.
3
+ *
4
+ * Without them the model spends its first requests discovering what one
5
+ * `uname` / `pwd` / `git status` would have told it (Meta-Harness and
6
+ * LangChain's LocalContext middleware inject the same snapshot for that
7
+ * reason). They go into the frozen prefix, which a resumed session reads
8
+ * back from its saved context rather than rebuilding, so a date that has
9
+ * moved on by the next day does not change the cached prefix.
10
+ */
11
+ export interface SessionEnvironment {
12
+ platform: string;
13
+ shell: string;
14
+ cwd: string;
15
+ /** Local calendar date, YYYY-MM-DD. */
16
+ date: string;
17
+ git: boolean;
18
+ }
19
+
20
+ export function environmentSection(env: SessionEnvironment): string {
21
+ return [
22
+ "Environment (recorded at session start):",
23
+ `- Platform: ${env.platform}`,
24
+ `- Shell: ${env.shell}`,
25
+ `- Working directory: ${env.cwd}`,
26
+ `- Date: ${env.date}`,
27
+ `- Git repository: ${env.git ? "yes" : "no"}`,
28
+ ].join("\n");
29
+ }
@@ -0,0 +1,99 @@
1
+ /** D8/D11: malformed durable data is corruption, never a valid empty event. */
2
+ import type { LogEvent, NodeRef } from "./events.ts";
3
+ import { isExecutionOutcome } from "./outcome.ts";
4
+ import { isJsonValue, isToolDiagnostic } from "./tool-contract.ts";
5
+
6
+ export function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ const strings = (value: unknown): boolean => Array.isArray(value) && value.every((v) => typeof v === "string");
10
+ const stringMap = (value: unknown): boolean => isRecord(value) && Object.values(value).every((v) => typeof v === "string");
11
+ const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
12
+ const natural = (value: unknown): boolean => Number.isSafeInteger(value) && Number(value) >= 0;
13
+ export function validNodeRef(value: unknown): value is NodeRef {
14
+ return isRecord(value) && Number.isSafeInteger(value["node"]) && Number(value["node"]) >= 0 &&
15
+ typeof value["log_ref"] === "string" && typeof value["sha256"] === "string" &&
16
+ (value["at_event"] === undefined || typeof value["at_event"] === "string");
17
+ }
18
+
19
+ export function isLogEvent(value: unknown): value is LogEvent {
20
+ if (!isRecord(value) || typeof value["id"] !== "string" || value["id"].length === 0 || typeof value["at"] !== "string") return false;
21
+ if (value["outcome"] !== undefined && !isExecutionOutcome(value["outcome"])) return false;
22
+ if (value["work_id"] !== undefined && !(typeof value["work_id"] === "string" && value["work_id"].trim().length > 0)) return false;
23
+ const carried = value["carried_from"];
24
+ if (carried !== undefined && !(isRecord(carried) && natural(carried["node"]) && typeof carried["event"] === "string")) return false;
25
+ const has = (...keys: string[]): boolean => keys.every((key) => typeof value[key] === "string");
26
+ switch (value["t"]) {
27
+ case "user": return has("text");
28
+ case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline"].includes(String(value["reason"])) && strings(value["sources"]);
29
+ case "interrupt": return has("user_id");
30
+ case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) && Array.isArray(value["blocks"]) && value["blocks"].every((b: unknown) => {
31
+ if (!isRecord(b)) return false;
32
+ if (b["type"] === "text") return typeof b["text"] === "string";
33
+ if (b["type"] === "thinking") return typeof b["text"] === "string" && "raw" in b;
34
+ return b["type"] === "tool_use" && typeof b["call_id"] === "string" && typeof b["name"] === "string" && "input" in b;
35
+ });
36
+ case "tool_result": {
37
+ if (value["data"] !== undefined && !isJsonValue(value["data"])) return false;
38
+ if (value["diagnostic"] !== undefined && !isToolDiagnostic(value["diagnostic"])) return false;
39
+ if (!has("call_id", "content") || typeof value["ok"] !== "boolean") return false;
40
+ if (!optionalString(value["content_sha256"])) return false;
41
+ if (!optionalString(value["observation_sha256"])) return false;
42
+ const artifact = value["content_ref"];
43
+ if (artifact !== undefined && !(isRecord(artifact) && typeof artifact["sha256"] === "string" &&
44
+ typeof artifact["path"] === "string" && natural(artifact["bytes"]))) return false;
45
+ const effect = value["effect"];
46
+ if (effect === undefined) return true;
47
+ if (!isRecord(effect)) return false;
48
+ if (effect["kind"] === "command") return typeof effect["cmd"] === "string" && Number.isInteger(effect["exit"]) &&
49
+ optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"]);
50
+ if (effect["kind"] === "read") return typeof effect["path"] === "string" && typeof effect["sha256"] === "string" &&
51
+ (effect["ranges"] === undefined || (Array.isArray(effect["ranges"]) && effect["ranges"].every((r: unknown) =>
52
+ Array.isArray(r) && r.length === 2 && r.every((n) => Number.isSafeInteger(n)) && r[0] >= 1 && r[1] >= r[0])));
53
+ return effect["kind"] === "edit" && typeof effect["path"] === "string" && typeof effect["sha256_after"] === "string" &&
54
+ (effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
55
+ }
56
+ case "cut": return has("handoff_ref") && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
57
+ optionalString(value["retained_from"]) &&
58
+ (value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
59
+ (value["handoff_tokens"] === undefined || (typeof value["handoff_tokens"] === "number" && Number.isFinite(value["handoff_tokens"]) && value["handoff_tokens"] > 0)) &&
60
+ (value["predecessor"] === null || validNodeRef(value["predecessor"]));
61
+ case "claim": {
62
+ const q = value["evidence"];
63
+ return has("what") && isRecord(q) && (typeof q["gate"] === "string" || typeof q["command"] === "string");
64
+ }
65
+ case "decision": return has("chose", "because") && (value["rejected"] === undefined || strings(value["rejected"]));
66
+ case "pitfall": return has("tried", "failed_because");
67
+ case "blocked": return has("what", "reason");
68
+ case "question": return has("text");
69
+ case "next_action": return has("what", "next_action");
70
+ case "evidence_link": return has("work", "what", "source") && optionalString(value["resource"]) && optionalString(value["version"]) &&
71
+ optionalString(value["supersedes"]) && (value["observed_version"] === null || optionalString(value["observed_version"]));
72
+ case "work_status": return has("work_id", "what") && ["unblock", "reopen", "cancel"].includes(String(value["operation"])) &&
73
+ optionalString(value["reason"]) && (value["operation"] === "cancel" ||
74
+ (typeof value["next_action"] === "string" && value["next_action"].trim().length > 0));
75
+ case "snapshot": return stringMap(value["added"]) && stringMap(value["changed"]) && strings(value["removed"]) &&
76
+ strings(value["undeclared"]) && strings(value["declared_but_unchanged"]) && optionalString(value["workspace_sha256"]) &&
77
+ (value["phase"] === undefined || value["phase"] === "turn" || value["phase"] === "idle");
78
+ case "rewind": return ["both", "files", "context"].includes(String(value["mode"])) && validNodeRef(value["from"]) &&
79
+ has("prompt") && strings(value["restored"]) && strings(value["removed"]) && strings(value["unrecoverable"]) && strings(value["divergence"]);
80
+ case "spawn": {
81
+ const input = value["input"];
82
+ if (input !== undefined) {
83
+ if (!isRecord(input) || typeof input["source_event"] !== "string" || !strings(input["tools"]) ||
84
+ !Number.isSafeInteger(input["max_turns"]) || Number(input["max_turns"]) < 1 || !isRecord(input["workspace"])) return false;
85
+ const workspace = input["workspace"];
86
+ if (!(workspace["kind"] === "shared_readonly" || (workspace["kind"] === "git_tree" &&
87
+ typeof workspace["head"] === "string" && typeof workspace["tree"] === "string"))) return false;
88
+ }
89
+ return validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
90
+ (isRecord(value["workspace"]) && typeof value["workspace"]["path"] === "string" && typeof value["workspace"]["branch"] === "string"));
91
+ }
92
+ case "joined": return validNodeRef(value["branch"]) && has("handoff_ref", "summary") && optionalString(value["model"]) &&
93
+ (value["tip"] === undefined || validNodeRef(value["tip"]));
94
+ case "adopt": return validNodeRef(value["branch"]) && (value["next_node"] === undefined || natural(value["next_node"])) &&
95
+ (value["applied"] === undefined || value["applied"] === true);
96
+ case "discard": return validNodeRef(value["branch"]) && has("reason");
97
+ default: return false;
98
+ }
99
+ }
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Append-only event log types.
3
+ *
4
+ * Two hard rules, both load-bearing:
5
+ *
6
+ * 1. APPEND REFERENCES, NEVER COPIES (decision D8). A `cut` event stores the id of
7
+ * the handoff it produced and a ref to its predecessor segment -- never the
8
+ * history it replaces. Codex CLI re-embeds the full `replacement_history` into
9
+ * its rollout JSONL on every compaction, which is O(n^2) on disk and has been
10
+ * reported to reach 42 GB (openai/codex#41806). The rule exists to make that
11
+ * shape unrepresentable.
12
+ *
13
+ * 2. HARNESS-DERIVED VS MODEL-ANNOTATED. Harness events are produced from things
14
+ * that cannot be wrong: the user typed this, the tool exited with this code.
15
+ * Model-annotated events are optional, may be wrong, and always have a
16
+ * deterministic fallback. Nothing load-bearing depends on the model's honesty --
17
+ * in particular, verification comes from exit codes, never from the model
18
+ * saying the tests passed.
19
+ */
20
+
21
+ import type { ExecutionOutcome } from "./outcome.ts";
22
+ import type { JsonValue, ToolDiagnostic } from "./tool-contract.ts";
23
+
24
+ export type EventId = string;
25
+ export type Sha256 = string;
26
+ export type IsoTime = string;
27
+
28
+ /** Inclusive 1-indexed line range, matching how editors and tools report them. */
29
+ export type LineRange = readonly [start: number, end: number];
30
+
31
+ /**
32
+ * A pointer to one node of the session tree.
33
+ *
34
+ * A single parent pointer is what makes the log a tree rather than a list --
35
+ * the linear case is the degenerate tree where every node has exactly one
36
+ * child. `node` is the ALLOCATION number, not a position in the tree:
37
+ * structure lives only in parent pointers, the way a commit hash is an
38
+ * identity and the parent link is the shape.
39
+ */
40
+ export type NodeRef = {
41
+ node: number;
42
+ log_ref: string;
43
+ sha256: Sha256;
44
+ /**
45
+ * Where in the parent this node branched off. Omitted means the parent's
46
+ * current end.
47
+ *
48
+ * An event id rather than a turn index. An index is only stable while
49
+ * nothing upstream can be inserted, edited or compacted, and while that
50
+ * holds inside a node today it is a property of the storage rather than of
51
+ * the pointer. The id already exists, is already unique, and gives one more
52
+ * thing for free: a pointer at an event that is not there can be detected,
53
+ * where an out-of-range index can only be range-checked.
54
+ */
55
+ at_event?: EventId;
56
+ };
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Effects: what a tool call actually did, as classified by the tool registry.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * `gate` is attached by the harness from configured gate patterns, not guessed
64
+ * from the command text. A command with no configured gate is still recorded --
65
+ * it just counts as weaker evidence than a named gate.
66
+ */
67
+ export type Effect =
68
+ | { kind: "read"; path: string; sha256: Sha256; ranges?: readonly LineRange[] }
69
+ | { kind: "edit"; path: string; sha256_before: Sha256 | null; sha256_after: Sha256 }
70
+ | { kind: "command"; cmd: string; exit: number; gate?: string; workspace_sha256?: Sha256 };
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Harness-derived events
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export interface UserEvent {
77
+ t: "user";
78
+ id: EventId;
79
+ at: IsoTime;
80
+ text: string;
81
+ }
82
+
83
+ /** A factual harness notice, appended once rather than rewriting the prompt (N.1). */
84
+ export interface GuidanceEvent {
85
+ t: "guidance";
86
+ id: EventId;
87
+ at: IsoTime;
88
+ reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline";
89
+ text: string;
90
+ sources: readonly EventId[];
91
+ }
92
+
93
+ /**
94
+ * The user cancelled the turn that `user_id` opened.
95
+ *
96
+ * A later record, not a flag on that user event: the cancel happened after
97
+ * the prompt was written, and rewriting a committed event would violate
98
+ * append-only (D8). Only harness-observable facts are stored -- which prompt
99
+ * and when -- never the partial assistant text, which was never delivered.
100
+ * A cancelled generation is not a conversation that happened (same rule as
101
+ * AGENT_PLAYBOOK "凭证与错误文本": a failed request is not a conversation).
102
+ */
103
+ export interface InterruptEvent {
104
+ t: "interrupt";
105
+ id: EventId;
106
+ at: IsoTime;
107
+ /** The user event whose turn was cancelled. */
108
+ user_id: EventId;
109
+ }
110
+
111
+ /**
112
+ * Only the shapes the projection needs to replay a turn.
113
+ *
114
+ * A thinking block carries the provider's opaque fields alongside its text.
115
+ * They must be replayed byte-for-byte: the API rejects blocks whose content
116
+ * was modified, and a block whose text is empty still has to go back verbatim.
117
+ * Keeping only the text would drop the signature and fail the next request --
118
+ * so the raw block is kept under `raw` and replayed untouched, while `text` is
119
+ * what a UI renders.
120
+ */
121
+ export type AssistantBlock =
122
+ | { type: "text"; text: string }
123
+ | { type: "thinking"; text: string; raw: unknown }
124
+ /**
125
+ * `wire` is the provider's own per-call data beyond id, name and arguments
126
+ * (Gemini's thought signature), replayed verbatim like a thinking block's raw.
127
+ */
128
+ | { type: "tool_use"; call_id: string; name: string; input: unknown; wire?: unknown };
129
+
130
+ export interface AssistantEvent {
131
+ t: "assistant";
132
+ id: EventId;
133
+ at: IsoTime;
134
+ blocks: readonly AssistantBlock[];
135
+ /** N.12: provider-observed termination, never a claim of task completion. Legacy logs omit it. */
136
+ stop_reason?: string | null;
137
+ }
138
+
139
+ export interface ToolResultEvent {
140
+ t: "tool_result";
141
+ id: EventId;
142
+ at: IsoTime;
143
+ call_id: string;
144
+ ok: boolean;
145
+ /** What the model sees. Large payloads belong on disk, referenced by effect. */
146
+ content: string;
147
+ /** Digest of the complete observation, even when it fits inline in the log. */
148
+ content_sha256?: Sha256;
149
+ /** O.3: semantic observation digest excludes presentation metadata. */
150
+ observation_sha256?: Sha256;
151
+ content_ref?: ArtifactRef;
152
+ effect?: Effect;
153
+ /** O.1: actual termination, independent of effect/verification; legacy logs omit it. */
154
+ outcome?: ExecutionOutcome;
155
+ /** O.3: machine-readable observations and recovery hints, not additional proof. */
156
+ data?: JsonValue;
157
+ diagnostic?: ToolDiagnostic;
158
+ }
159
+
160
+ export type ArtifactRef = { sha256: Sha256; path: string; bytes: number };
161
+
162
+ /** Terminates the segment it appears in. Carries refs only -- see rule 1 above. */
163
+ export interface CutEvent {
164
+ t: "cut";
165
+ id: EventId;
166
+ at: IsoTime;
167
+ handoff_ref: string;
168
+ predecessor: NodeRef | null;
169
+ reason: CutReason;
170
+ /** Durable transition intent; recovery opens this exact successor. */
171
+ next_node?: number;
172
+ next_model?: string;
173
+ /** V2 carried-state budget, fixed in the log for identical replay. */
174
+ handoff_tokens?: number;
175
+ /** N.8: first retained event on the effective ancestry; no copied transcript. */
176
+ retained_from?: EventId;
177
+ }
178
+
179
+ export type CutReason = "hard_limit" | "trigger" | "model_checkpoint";
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Model-annotated events (optional; every consumer has a fallback)
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /**
186
+ * How a completion claim points at its proof. Resolved against the log by
187
+ * deriveHandoff(); a claim whose evidence does not resolve to a zero exit is
188
+ * surfaced as unverified rather than accepted or silently dropped.
189
+ */
190
+ export type EvidenceQuery =
191
+ | { gate: string }
192
+ | { command: string };
193
+
194
+ export interface ClaimEvent {
195
+ t: "claim";
196
+ id: EventId;
197
+ at: IsoTime;
198
+ what: string;
199
+ work_id?: string;
200
+ evidence: EvidenceQuery;
201
+ /** Set when an adopt moved this event from another node. */
202
+ carried_from?: CarriedFrom;
203
+ }
204
+
205
+ export interface DecisionEvent {
206
+ t: "decision";
207
+ id: EventId;
208
+ at: IsoTime;
209
+ chose: string;
210
+ because: string;
211
+ rejected?: readonly string[];
212
+ /** Set when an adopt moved this event from another node. */
213
+ carried_from?: CarriedFrom;
214
+ }
215
+
216
+ export interface PitfallEvent {
217
+ t: "pitfall";
218
+ id: EventId;
219
+ at: IsoTime;
220
+ tried: string;
221
+ failed_because: string;
222
+ /** Set when an adopt moved this event from another node. */
223
+ carried_from?: CarriedFrom;
224
+ }
225
+
226
+ export interface BlockedEvent {
227
+ t: "blocked";
228
+ id: EventId;
229
+ at: IsoTime;
230
+ what: string;
231
+ work_id?: string;
232
+ reason: string;
233
+ /** Set when an adopt moved this event from another node. */
234
+ carried_from?: CarriedFrom;
235
+ }
236
+
237
+ export interface QuestionEvent {
238
+ t: "question";
239
+ id: EventId;
240
+ at: IsoTime;
241
+ text: string;
242
+ /** Set when an adopt moved this event from another node. */
243
+ carried_from?: CarriedFrom;
244
+ }
245
+
246
+ /**
247
+ * What changed in the workspace during one turn, as a delta.
248
+ *
249
+ * A delta rather than a full listing: repeating the whole workspace every
250
+ * turn is the same quadratic growth D8 exists to prevent. The full set at
251
+ * any node is the deltas along its ancestry replayed in order.
252
+ *
253
+ * Recorded from a scan rather than from what the tools reported, because a
254
+ * shell command can write anywhere and its effect carries only an exit code.
255
+ * The two are compared, and a change nobody declared is surfaced.
256
+ */
257
+ export interface SnapshotEvent {
258
+ t: "snapshot";
259
+ id: EventId;
260
+ at: IsoTime;
261
+ added: Record<string, Sha256>;
262
+ changed: Record<string, Sha256>;
263
+ removed: readonly string[];
264
+ /** Paths that changed with no tool claiming them. */
265
+ undeclared: readonly string[];
266
+ /** Paths a tool claimed to edit that did not change. */
267
+ declared_but_unchanged: readonly string[];
268
+ workspace_sha256?: Sha256;
269
+ /** Idle observations must not redefine a completed user exchange. */
270
+ phase?: "turn" | "idle";
271
+ }
272
+
273
+ /**
274
+ * Which halves of a rewind moved.
275
+ *
276
+ * both the default. The conversation and the workspace go back
277
+ * together, so they still agree afterwards.
278
+ * files the workspace goes back, the conversation does not. The model's
279
+ * context describes edits that are no longer on disk.
280
+ * context the conversation goes back, the workspace does not. Files exist
281
+ * that the model has no memory of writing.
282
+ */
283
+ export type RewindMode = "both" | "files" | "context";
284
+
285
+ /**
286
+ * The workspace and the conversation were moved relative to each other.
287
+ *
288
+ * Recorded because the workspace is a projection of the log: a restore is a
289
+ * change no tool claimed, so without this event the next turn-boundary scan
290
+ * reports the whole thing as undeclared damage and the handoff inherits it.
291
+ *
292
+ * WHETHER IT REPLAYS DEPENDS ON `mode`, and the rule is the same one the
293
+ * whole log follows: tell the model exactly when, and only when, its picture
294
+ * would otherwise be wrong.
295
+ *
296
+ * "both" does NOT replay. The context and the disk agree afterwards, and
297
+ * saying "you just rewound" would carry the undone work back in another
298
+ * form -- the exact thing being undone.
299
+ *
300
+ * "files" and "context" DO replay, naming paths. They leave the two
301
+ * disagreeing, and a model planning against the wrong picture is the
302
+ * documented failure: with a context-only restore it "starts from bad code
303
+ * with no memory of how it got there", which is worse than either state.
304
+ */
305
+ export interface RewindEvent {
306
+ t: "rewind";
307
+ id: EventId;
308
+ at: IsoTime;
309
+ mode: RewindMode;
310
+ /** Where things were moved back to. */
311
+ from: NodeRef;
312
+ /** The prompt that opened the exchange moved back to, for a human reader. */
313
+ prompt: string;
314
+ restored: readonly string[];
315
+ removed: readonly string[];
316
+ /** Target files whose content is no longer in the blob store. */
317
+ unrecoverable: readonly string[];
318
+ /**
319
+ * Paths where the workspace and this conversation no longer agree, with
320
+ * which side is ahead. Computed from the plan, never described vaguely:
321
+ * "some files may differ" is not something a model can act on.
322
+ */
323
+ divergence: readonly string[];
324
+ }
325
+
326
+ // --- branch lifecycle ------------------------------------------------------
327
+
328
+ /**
329
+ * The parent opened a branch. Carries a reference, never the branch itself.
330
+ */
331
+ export interface SpawnEvent {
332
+ t: "spawn";
333
+ id: EventId;
334
+ at: IsoTime;
335
+ branch: NodeRef;
336
+ /** What the branch is for, in one line. */
337
+ purpose: string;
338
+ /** The isolated checkout belongs to this branch, including after restart. */
339
+ workspace?: { path: string; branch: string };
340
+ /** O.4: a task input envelope references the original request, never copies history. */
341
+ input?: {
342
+ source_event: string;
343
+ tools: readonly string[];
344
+ max_turns: number;
345
+ workspace: { kind: "shared_readonly" } | { kind: "git_tree"; head: string; tree: string };
346
+ };
347
+ }
348
+
349
+ /**
350
+ * A branch finished and handed its state back.
351
+ *
352
+ * `handoff_ref` is a path, and `summary` is a line the branch wrote -- the
353
+ * handoff's contents are never inlined here (D8). The parent reads the file
354
+ * when it needs the detail.
355
+ */
356
+ export interface JoinedEvent {
357
+ t: "joined";
358
+ id: EventId;
359
+ at: IsoTime;
360
+ branch: NodeRef;
361
+ handoff_ref: string;
362
+ summary: string;
363
+ /** A branch can cut before joining; its last node need not be its root. */
364
+ tip?: NodeRef;
365
+ model?: string;
366
+ /** Absence in a legacy join means unknown, never inferred success. */
367
+ outcome?: ExecutionOutcome;
368
+ }
369
+
370
+ /** The parent took this branch as the mainline. */
371
+ export interface AdoptEvent {
372
+ t: "adopt";
373
+ id: EventId;
374
+ at: IsoTime;
375
+ branch: NodeRef;
376
+ /** Commit intent after all parent tool results; recovery reuses this node. */
377
+ next_node?: number;
378
+ applied?: true;
379
+ }
380
+
381
+ /** The parent rejected this branch. Resolution, not silence (D16). */
382
+ export interface DiscardEvent {
383
+ t: "discard";
384
+ id: EventId;
385
+ at: IsoTime;
386
+ branch: NodeRef;
387
+ reason: string;
388
+ }
389
+
390
+ /**
391
+ * Where an event was carried from, when an adopt moved it into a new node.
392
+ *
393
+ * Adopting a branch would otherwise lose the parent's post-fork annotations --
394
+ * including the decision that chose the branch, which is the least disposable
395
+ * record there is. Carrying them is free in context terms because model
396
+ * annotations never replay as messages; only the derivation sees them.
397
+ */
398
+ export interface CarriedFrom {
399
+ node: number;
400
+ event: EventId;
401
+ }
402
+
403
+ /**
404
+ * The one genuinely non-derivable field. "What was I doing" can be reconstructed
405
+ * from the log; "what should happen next" cannot. When absent, the handoff says
406
+ * so via continuation_valid rather than inventing one.
407
+ */
408
+ export interface NextActionEvent {
409
+ t: "next_action";
410
+ id: EventId;
411
+ at: IsoTime;
412
+ what: string;
413
+ work_id?: string;
414
+ next_action: string;
415
+ /** Set when an adopt moved this event from another node. */
416
+ carried_from?: CarriedFrom;
417
+ }
418
+
419
+ /** O.2: lifecycle changes append facts; an old title is not a task identity. */
420
+ export interface WorkStatusEvent {
421
+ t: "work_status";
422
+ id: EventId;
423
+ at: IsoTime;
424
+ work_id: string;
425
+ what: string;
426
+ operation: "unblock" | "reopen" | "cancel";
427
+ next_action?: string;
428
+ reason?: string;
429
+ carried_from?: CarriedFrom;
430
+ }
431
+
432
+ /** O.6: a model associates an original observation with work; the runtime resolves its source. */
433
+ export interface EvidenceLinkEvent {
434
+ t: "evidence_link"; id: EventId; at: IsoTime;
435
+ work: string; what: string; source: EventId;
436
+ resource?: string; version?: string; supersedes?: EventId;
437
+ /** Last version observed by the harness when this association was recorded. */
438
+ observed_version?: string | null;
439
+ carried_from?: CarriedFrom;
440
+ }
441
+
442
+ export type LogEvent =
443
+ | UserEvent
444
+ | GuidanceEvent
445
+ | InterruptEvent
446
+ | AssistantEvent
447
+ | ToolResultEvent
448
+ | EvidenceLinkEvent
449
+ | CutEvent
450
+ | SnapshotEvent
451
+ | RewindEvent
452
+ | SpawnEvent
453
+ | JoinedEvent
454
+ | AdoptEvent
455
+ | DiscardEvent
456
+ | ClaimEvent
457
+ | DecisionEvent
458
+ | PitfallEvent
459
+ | BlockedEvent
460
+ | QuestionEvent
461
+ | NextActionEvent
462
+ | WorkStatusEvent;
463
+
464
+ export const HARNESS_EVENTS = [
465
+ "user", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind",
466
+ ] as const;
467
+ export const MODEL_EVENTS = [
468
+ "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
469
+ "spawn", "joined", "adopt", "discard",
470
+ ] as const;
471
+
472
+ /**
473
+ * Events an adopt carries into the node it opens.
474
+ *
475
+ * Only annotations: harness events are either already in the branch's
476
+ * ancestry or are parent actions the branch should not inherit.
477
+ */
478
+ export const CARRYABLE_EVENTS = [
479
+ "claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
480
+ ] as const;
481
+
482
+ export type CarryableEvent = Extract<LogEvent, { t: (typeof CARRYABLE_EVENTS)[number] }>;
483
+
484
+ export function isHarnessEvent(e: LogEvent): boolean {
485
+ return (HARNESS_EVENTS as readonly string[]).includes(e.t);
486
+ }