@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/runtime/clipboard.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Pi's files → image → text precedence (MIT, 02eed88) over the platform's own
|
|
3
|
+
* clipboard commands. The optional native helper went with the vendored TUI
|
|
4
|
+
* (D20); a caller may still inject one.
|
|
5
|
+
*/
|
|
2
6
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
export interface NativeClipboard {
|
|
9
|
+
setText?(text: string): Promise<void>;
|
|
10
|
+
getText?(): Promise<string | null | undefined>;
|
|
11
|
+
getImage?(): Promise<Uint8Array | null | undefined>;
|
|
12
|
+
getFilePaths?(): Promise<string[] | null | undefined>;
|
|
13
|
+
}
|
|
14
|
+
function getNativeClipboard(): NativeClipboard | null { return null; }
|
|
4
15
|
import { runClipboardCommand } from "../vendor/pi-rich/clipboard-command.ts";
|
|
5
16
|
|
|
6
17
|
export type ClipboardContent = { files: string[] } | { image: Uint8Array } | { text: string } | null;
|
|
@@ -104,9 +115,9 @@ export async function readClipboard(options: ClipboardOptions = {}): Promise<Cli
|
|
|
104
115
|
return bytes.length ? { text: bytes.toString("utf8") } : null;
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
|
-
const image = await native?.getImage();
|
|
118
|
+
const image = await native?.getImage?.();
|
|
108
119
|
if (image?.length) return { image };
|
|
109
|
-
const text = await native?.getText();
|
|
120
|
+
const text = await native?.getText?.();
|
|
110
121
|
if (text !== undefined) return text ? { text } : null;
|
|
111
122
|
const fallback = env["TERMUX_VERSION"] ? await run("termux-clipboard-get", []) : platform === "darwin" ? await run("pbpaste", []) : undefined;
|
|
112
123
|
if (fallback === undefined) throw new Error("Clipboard unavailable for this terminal session");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** R8: one owner observes an admitted command once, including after a managed call yields. */
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { artifactChange, commandArtifactState, parseCommandArtifactPlan, COMMAND_ARTIFACT_SCOPE, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
|
|
5
|
+
import { observeArtifactInputs, observeArtifactFiles, artifactExclusions, archiveCommandArtifacts } from "../store/command-artifacts.ts";
|
|
6
|
+
import type { ToolContext } from "./tools.ts";
|
|
7
|
+
|
|
8
|
+
export type ArtifactCommandEnd = { exit: number; stopped?: "cancelled" | "timeout"; capture?: "complete" | "partial"; cleanup?: unknown };
|
|
9
|
+
export type CommandArtifactObserver = {
|
|
10
|
+
pending: CommandArtifactReceipt;
|
|
11
|
+
finish(end: ArtifactCommandEnd): Promise<CommandArtifactReceipt>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Syntactic validation precedes admission; no preflight disk probing is smuggled through a denied tool. */
|
|
15
|
+
export function prepareArtifactArguments(input: unknown): unknown {
|
|
16
|
+
if (input === null || typeof input !== "object" || Array.isArray(input) || !("artifacts" in input)) return input;
|
|
17
|
+
return { ...input, artifacts: parseCommandArtifactPlan(input.artifacts) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function beginCommandArtifacts(ctx: Pick<ToolContext, "root" | "hostId" | "ownedStatePaths" | "outputStore" | "signal">,
|
|
21
|
+
command: string, cwd: string, requested: unknown): Promise<CommandArtifactObserver | undefined> {
|
|
22
|
+
if (requested === undefined) return undefined;
|
|
23
|
+
const plan = parseCommandArtifactPlan(requested), exclusions = artifactExclusions(ctx.root, plan,
|
|
24
|
+
[...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
|
|
25
|
+
const beforeInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
|
|
26
|
+
const before = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
|
|
27
|
+
const pending: CommandArtifactReceipt = {
|
|
28
|
+
kind: "command_artifacts", version: 1, state: "pending", plan,
|
|
29
|
+
source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd), runtime: `bun/${Bun.version}` },
|
|
30
|
+
command, exit: null, started_at: new Date().toISOString(), observed_at: null,
|
|
31
|
+
inputs: { scope: COMMAND_ARTIFACT_SCOPE, exclusions, before: beforeInputs.sha256, after: null },
|
|
32
|
+
outputs: [], reasons: beforeInputs.reason ? [beforeInputs.reason] : [],
|
|
33
|
+
};
|
|
34
|
+
let completed: Promise<CommandArtifactReceipt> | undefined;
|
|
35
|
+
return { pending, finish(end) {
|
|
36
|
+
completed ??= (async () => {
|
|
37
|
+
// Observation failures retain the actual exit. Never retry a post-exit read against later files.
|
|
38
|
+
const reasons = [...pending.reasons];
|
|
39
|
+
if (end.exit !== 0) reasons.push("command exited unsuccessfully");
|
|
40
|
+
if (end.stopped) reasons.push(`command ${end.stopped}`);
|
|
41
|
+
if (end.capture === "partial") reasons.push("command output capture is partial");
|
|
42
|
+
if (end.cleanup) reasons.push("command cleanup is unconfirmed");
|
|
43
|
+
const after = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
|
|
44
|
+
const afterInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
|
|
45
|
+
if (afterInputs.reason) reasons.push(afterInputs.reason);
|
|
46
|
+
const receipt: CommandArtifactReceipt = { ...pending, exit: end.exit, observed_at: new Date().toISOString(),
|
|
47
|
+
inputs: { ...pending.inputs, after: afterInputs.sha256 },
|
|
48
|
+
outputs: plan.paths.map((path, i) => ({ path, before: before[i]!, after: after[i]!, change: artifactChange(before[i]!, after[i]!) })), reasons };
|
|
49
|
+
receipt.state = commandArtifactState(receipt);
|
|
50
|
+
try { receipt.manifest = await archiveCommandArtifacts(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), receipt); }
|
|
51
|
+
catch { receipt.reasons.push("artifact observation archive unavailable"); receipt.state = commandArtifactState(receipt); }
|
|
52
|
+
return receipt;
|
|
53
|
+
})();
|
|
54
|
+
return completed;
|
|
55
|
+
} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function commandArtifactSummary(receipt: CommandArtifactReceipt | undefined): string {
|
|
59
|
+
if (!receipt) return "";
|
|
60
|
+
const changes = ["created", "modified", "unchanged", "unknown"].map(change => `${change}=${receipt.outputs.filter(output => output.change === change).length}`).join(", ");
|
|
61
|
+
return `\n[artifacts ${receipt.state}; ${changes}; reasons=${receipt.reasons.length}; ` +
|
|
62
|
+
(receipt.manifest ? `history artifact=${receipt.manifest.sha256}; ` : "") +
|
|
63
|
+
"command association only, not build causality or runtime acceptance]";
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Connection lifecycle is transient; durable state belongs exclusively to Session's log (Q24). */
|
|
2
|
+
import { CONTROL_SCHEMA, parseControlRequest, ControlProtocolError, type RpcId } from "../core/control-protocol.ts";
|
|
3
|
+
import { selectLens, type LensFocus } from "../core/control-view.ts";
|
|
4
|
+
import { assertNever } from "../core/exhaustive.ts";
|
|
5
|
+
import type { SessionControl } from "./session-control.ts";
|
|
6
|
+
import type { ApprovalChoice } from "./tools.ts";
|
|
7
|
+
|
|
8
|
+
export type RpcOutput = { id: RpcId | null; result?: unknown; error?: { code: number; message: string } } | { method: string; params: unknown };
|
|
9
|
+
|
|
10
|
+
export class ControlConnection {
|
|
11
|
+
private readonly control: () => SessionControl;
|
|
12
|
+
private readonly write: (output: RpcOutput) => void;
|
|
13
|
+
private initialized = false;
|
|
14
|
+
private ready = false;
|
|
15
|
+
private subscribed = false;
|
|
16
|
+
private closed = false;
|
|
17
|
+
private attached: SessionControl | undefined;
|
|
18
|
+
private stop: (() => void) | undefined;
|
|
19
|
+
private readonly denyOnClose: boolean;
|
|
20
|
+
|
|
21
|
+
constructor(control: () => SessionControl, write: (output: RpcOutput) => void, options: { denyOnClose?: boolean } = {}) {
|
|
22
|
+
this.control = control; this.write = write; this.denyOnClose = options.denyOnClose ?? false;
|
|
23
|
+
}
|
|
24
|
+
private attach(): SessionControl {
|
|
25
|
+
const control = this.control();
|
|
26
|
+
if (this.attached !== control) {
|
|
27
|
+
this.stop?.(); this.attached = control;
|
|
28
|
+
this.stop = control.subscribe(notice => { if (!this.closed && this.ready && this.subscribed) this.write(notice); });
|
|
29
|
+
}
|
|
30
|
+
return control;
|
|
31
|
+
}
|
|
32
|
+
async receive(value: unknown): Promise<void> {
|
|
33
|
+
if (this.closed) return;
|
|
34
|
+
let id: RpcId | null = null;
|
|
35
|
+
try {
|
|
36
|
+
if (typeof value === "object" && value !== null && "id" in value &&
|
|
37
|
+
(typeof value.id === "string" && value.id.length <= 128 || typeof value.id === "number" && Number.isSafeInteger(value.id))) id = value.id;
|
|
38
|
+
const request = parseControlRequest(value), params = request.params;
|
|
39
|
+
const control = this.attach();
|
|
40
|
+
if (request.method === "initialize") {
|
|
41
|
+
if (this.initialized) throw new ControlProtocolError(-32000, "already initialized");
|
|
42
|
+
this.initialized = true;
|
|
43
|
+
this.write({ id, result: { protocol: CONTROL_SCHEMA.protocol, experimental: true, threadId: control.threadId, streamId: control.streamId,
|
|
44
|
+
capabilities: { turns: true, steering: true, approvals: true, contextLens: true, snapshotResync: true, durableEventReplay: "item/list", adaptiveExecution: false } } });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!this.initialized) throw new ControlProtocolError(-32000, "not initialized");
|
|
48
|
+
if (request.method === "initialized") { if (this.ready) throw new ControlProtocolError(-32000, "already ready"); this.ready = true; return; }
|
|
49
|
+
if (!this.ready) throw new ControlProtocolError(-32000, "send initialized before using the session");
|
|
50
|
+
if (params["threadId"] !== control.threadId) throw new ControlProtocolError(-32000, "stale or unknown threadId; initialize a new connection after changing sessions");
|
|
51
|
+
let result: unknown;
|
|
52
|
+
switch (request.method) {
|
|
53
|
+
case "thread/read": result = await control.snapshot(); break;
|
|
54
|
+
case "thread/subscribe": {
|
|
55
|
+
this.subscribed = true;
|
|
56
|
+
const snapshot = await control.snapshot();
|
|
57
|
+
const after = params["after"] as { streamId: string; sequence: number } | undefined;
|
|
58
|
+
result = { snapshot, resyncRequired: after !== undefined && (after.streamId !== snapshot.streamId || after.sequence !== snapshot.sequence) };
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "turn/start": result = { turnId: control.start(String(params["input"])) }; break;
|
|
62
|
+
case "turn/steer": control.steer(String(params["turnId"]), String(params["input"])); result = { queued: true }; break;
|
|
63
|
+
case "turn/interrupt": control.interrupt(String(params["turnId"])); result = { interruptRequested: true }; break;
|
|
64
|
+
case "approval/respond": control.respond(String(params["turnId"]), String(params["approvalId"]), params["choice"] as ApprovalChoice); result = { accepted: true }; break;
|
|
65
|
+
case "acceptance/accept": await control.accept(String(params["requestId"]), String(params["criterionId"])); result = { accepted: true }; break;
|
|
66
|
+
case "context/read": {
|
|
67
|
+
const snapshot = await control.snapshot(false), all = selectLens(snapshot.context, params["focus"] as LensFocus);
|
|
68
|
+
const offset = Number(params["offset"] ?? 0), limit = Number(params["limit"] ?? 50);
|
|
69
|
+
result = { node: snapshot.node, streamId: snapshot.streamId, sequence: snapshot.sequence, focus: params["focus"], total: all.length,
|
|
70
|
+
entries: all.slice(offset, offset + limit), nextOffset: offset + limit < all.length ? offset + limit : null,
|
|
71
|
+
components: snapshot.context.components, used: snapshot.context.used, countKind: snapshot.context.countKind };
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "item/list": {
|
|
75
|
+
const all = await control.items(), after = params["after"] as { node: number; event: string } | undefined;
|
|
76
|
+
const index = after ? all.findIndex(item => item.node === after.node && item.event.id === after.event) : -1;
|
|
77
|
+
if (after && index < 0) throw Error("cursor is not on the current path; refresh the snapshot");
|
|
78
|
+
const entries = all.slice(index + 1, index + 1 + Number(params["limit"] ?? 50));
|
|
79
|
+
const last = entries.at(-1);
|
|
80
|
+
result = { items: entries.map(item => ({ node: item.node, id: item.event.id, type: item.event.t })),
|
|
81
|
+
nextCursor: last && index + 1 + entries.length < all.length ? { node: last.node, event: last.event.id } : null };
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "item/read": {
|
|
85
|
+
const item = (await control.items()).find(item => item.node === params["node"] && item.event.id === params["event"]);
|
|
86
|
+
if (!item) throw Error("item is not on the current path");
|
|
87
|
+
result = item; break;
|
|
88
|
+
}
|
|
89
|
+
default: assertNever(request.method, "control request");
|
|
90
|
+
}
|
|
91
|
+
if (!this.closed) this.write({ id, result });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!this.closed) this.write({ id, error: { code: error instanceof ControlProtocolError ? error.code : -32000,
|
|
94
|
+
message: error instanceof Error ? error.message.slice(0, 512) : "control request failed" } });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
close(): void {
|
|
98
|
+
this.closed = true; this.stop?.();
|
|
99
|
+
if (this.denyOnClose && this.attached) {
|
|
100
|
+
this.attached.denyApprovals();
|
|
101
|
+
if (this.attached.activeTurnId) this.attached.interrupt(this.attached.activeTurnId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** R6: observe through the same shell and process owner; never infer versions from the host PATH. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { ENVIRONMENT_OUTPUT_BYTES, ENVIRONMENT_PARSE_BYTES, ENVIRONMENT_TIMEOUT_MS, ENVIRONMENT_TTL_MS,
|
|
6
|
+
environmentCapability, parseEnvironmentFrame, type EnvironmentProbe, type EnvironmentReceipt, type EnvironmentSource } from "../core/environment.ts";
|
|
7
|
+
import { executionOutcome } from "../core/outcome.ts";
|
|
8
|
+
import { canonicalValue } from "../core/continuation.ts";
|
|
9
|
+
import { failureDiagnostic } from "../core/tool-failure.ts";
|
|
10
|
+
import { observeCommand } from "../store/command.ts";
|
|
11
|
+
import type { ToolContext, ToolResult } from "./tools.ts";
|
|
12
|
+
|
|
13
|
+
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
|
|
14
|
+
/** Hash only resolution-related inherited inputs; never expose the full environment or its credentials. */
|
|
15
|
+
export function environmentSource(ctx: Pick<ToolContext, "root" | "hostId">, cwd: string): EnvironmentSource {
|
|
16
|
+
const keys = ["PATH", "HOME", "SHELL", "LANG", "BASH_ENV", "PYTHONHOME", "PYTHONPATH", "PYENV_VERSION", "VIRTUAL_ENV", "CONDA_PREFIX", "NVM_BIN"];
|
|
17
|
+
return { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd),
|
|
18
|
+
runtime: `bun/${Bun.version};${process.platform}/${process.arch};${process.execPath}`,
|
|
19
|
+
inherited_sha256: hash(canonicalValue(Object.fromEntries(keys.map(key => [key, process.env[key] ?? null])))) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function observeEnvironment(ctx: ToolContext, cwd: string, program: string, probe: EnvironmentProbe, command: string): Promise<ToolResult> {
|
|
23
|
+
const budget = new AbortController();
|
|
24
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, budget.signal]) : budget.signal;
|
|
25
|
+
let outputBytes = 0;
|
|
26
|
+
const source = environmentSource(ctx, cwd);
|
|
27
|
+
const captured = await observeCommand({ command, cwd, cleanupOnExit: true, outputLimitBytes: ENVIRONMENT_OUTPUT_BYTES,
|
|
28
|
+
outputStore: ctx.outputStore ?? join(ctx.root, ".xeno", "observations"),
|
|
29
|
+
maxOutputBytes: ENVIRONMENT_PARSE_BYTES, timeoutMs: Math.min(ENVIRONMENT_TIMEOUT_MS, ctx.timeoutMs, ctx.maxTimeoutMs ?? Infinity), signal,
|
|
30
|
+
onOutput: chunk => {
|
|
31
|
+
outputBytes += chunk.byteLength;
|
|
32
|
+
if (outputBytes > ENVIRONMENT_OUTPUT_BYTES) budget.abort();
|
|
33
|
+
ctx.onOutput?.(chunk);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const outputLimited = budget.signal.aborted;
|
|
37
|
+
const parseLimited = captured.ref.bytes > ENVIRONMENT_PARSE_BYTES;
|
|
38
|
+
const frame = parseEnvironmentFrame(new TextDecoder().decode(captured.head), program, probe);
|
|
39
|
+
const capability = captured.cleanup ? { state: "unknown" as const, reported_version: null, reason: captured.cleanup.message } : environmentCapability(frame, captured.exit, { complete: captured.capture === "complete",
|
|
40
|
+
...(captured.stopped ? { stopped: captured.stopped } : {}),
|
|
41
|
+
...(outputLimited ? { limited: "output" } : parseLimited ? { limited: "parse" } : {}) });
|
|
42
|
+
const observedAt = Date.now();
|
|
43
|
+
const receipt: EnvironmentReceipt = { kind: "environment", version: 1, query: { program, probe }, source,
|
|
44
|
+
observed_at: observedAt, expires_at: observedAt + ENVIRONMENT_TTL_MS, ...capability,
|
|
45
|
+
shell: frame ? { executable: frame.shell, version: frame.shell_version, cwd: frame.cwd, path_sha256: hash(frame.path) } : null,
|
|
46
|
+
resolution: frame ? { kind: frame.kind, executable: frame.executable } : null,
|
|
47
|
+
evidence: { output: captured.ref, exit: captured.exit, complete: captured.capture === "complete" } };
|
|
48
|
+
const status = outputLimited ? "tool_failed" : captured.stopped ?? (capability.state === "observed" ? "tool_completed" : "tool_failed");
|
|
49
|
+
const code = outputLimited ? "execution_failed" : captured.stopped ?? (capability.state === "missing" ? "environment_missing" : "execution_failed");
|
|
50
|
+
const text = JSON.stringify(receipt);
|
|
51
|
+
const bytes = new TextEncoder().encode(text);
|
|
52
|
+
// Raw output is the primary artifact. Structured data is separately preserved by the Session broker.
|
|
53
|
+
const content = bytes.length <= ctx.maxOutputBytes ? text :
|
|
54
|
+
`environment ${receipt.state}; exit ${captured.exit}; full receipt in data; original output: ${captured.ref.sha256}`;
|
|
55
|
+
return { ok: capability.state === "observed", content, content_ref: captured.ref, data: receipt,
|
|
56
|
+
...(captured.cleanup ? { warnings: [captured.cleanup] } : {}),
|
|
57
|
+
observation_sha256: hash(canonicalValue({ query: receipt.query, source, ...capability, shell: receipt.shell, resolution: receipt.resolution, evidence: receipt.evidence })),
|
|
58
|
+
effect: { kind: "command", cmd: command, exit: captured.exit }, outcome: executionOutcome(status),
|
|
59
|
+
...(capability.state === "observed" ? {} : { diagnostic: failureDiagnostic(code, capability.reason ?? "capability unconfirmed", { execution: "started", effects: "possible" }) }) };
|
|
60
|
+
}
|
|
@@ -5,14 +5,16 @@ import { historyEntries } from "../core/history-query.ts";
|
|
|
5
5
|
import { observedVersion, resourceKey } from "../core/evidence-links.ts";
|
|
6
6
|
import { scopedWorkId } from "../core/work-state.ts";
|
|
7
7
|
import { readNodeHistory } from "../store/restore.ts";
|
|
8
|
+
import { failureDiagnostic, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
8
9
|
import type { ToolResult } from "./tools.ts";
|
|
9
10
|
|
|
10
11
|
export async function resolveEvidenceNote(logDir: string, node: number, namespace: string, current: Handoff, event: EvidenceLinkEvent): Promise<{ event?: EvidenceLinkEvent; result: ToolResult }> {
|
|
11
12
|
const work = scopedWorkId(namespace, event.work, current.state.work_items ?? []);
|
|
12
|
-
const fail = (message: string) => ({ result: { ok: false, content: message
|
|
13
|
+
const fail = (message: string, attempted = false) => ({ result: { ok: false, content: message, data: { recorded: false },
|
|
14
|
+
diagnostic: failureDiagnostic(attempted ? "execution_failed" : "invalid_input", message, attempted ? READ_ATTEMPTED : NOT_STARTED) } });
|
|
13
15
|
if (!current.state.work_items?.some(item => item.id === work)) return fail("pin requires an existing work item");
|
|
14
16
|
const history = await readNodeHistory(logDir, node);
|
|
15
|
-
if (!history.ok) return fail(history.error);
|
|
17
|
+
if (!history.ok) return fail(history.error, true);
|
|
16
18
|
const entries = historyEntries(history.nodes);
|
|
17
19
|
const source = entries.find(entry => entry.event === event.source && entry.kind === "tool_result");
|
|
18
20
|
if (!source) return fail("pin source must be an original tool observation in the active ancestry");
|