@visual-z/xeno 0.8.4 → 0.9.1
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 +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/** Typed local control client; requests are never retried after a delivery error. */
|
|
2
|
-
import { CONTROL_SCHEMA, parseControlRequest } from "../core/control-protocol.ts";
|
|
3
|
-
import type { ContextLens, LensFocus } from "../core/control-view.ts";
|
|
4
|
-
import type { LogEvent } from "../core/events.ts";
|
|
5
|
-
import type { QuestionReply } from "../core/question.ts";
|
|
6
|
-
import type { SessionList } from "../store/sessions.ts";
|
|
7
|
-
import type { ControlNotice, ControlSnapshot } from "./session-control.ts";
|
|
8
|
-
|
|
9
|
-
export interface ControlApi {
|
|
10
|
-
"thread/read": { params: { threadId: string }; result: ControlSnapshot };
|
|
11
|
-
"thread/list": { params: Record<string, never>; result: SessionList & { live: string[] } };
|
|
12
|
-
"thread/new": { params: { name?: string }; result: ControlSnapshot };
|
|
13
|
-
"thread/resume": { params: { session: string }; result: ControlSnapshot };
|
|
14
|
-
"thread/fork": { params: { threadId: string; name?: string }; result: ControlSnapshot };
|
|
15
|
-
"thread/close": { params: { threadId: string }; result: { closed: boolean } };
|
|
16
|
-
"thread/mode": { params: { threadId: string; mode: "build" | "plan" }; result: ControlSnapshot };
|
|
17
|
-
"turn/start": { params: { threadId: string; input: string }; result: { turnId: string } };
|
|
18
|
-
"turn/steer": { params: { threadId: string; turnId: string; input: string }; result: { queued: boolean } };
|
|
19
|
-
"turn/interrupt": { params: { threadId: string; turnId: string }; result: { interruptRequested: boolean } };
|
|
20
|
-
"approval/respond": { params: { threadId: string; turnId: string; approvalId: string; choice: "once" | "command" | "tool" | "deny" }; result: { accepted: boolean } };
|
|
21
|
-
"question/respond": { params: { threadId: string; turnId: string; requestId: string; reply: string }; result: { accepted: boolean } };
|
|
22
|
-
"acceptance/accept": { params: { threadId: string; requestId: string; criterionId: string }; result: { accepted: boolean } };
|
|
23
|
-
"context/read": { params: { threadId: string; focus: LensFocus; offset?: number; limit?: number }; result: {
|
|
24
|
-
node: number; streamId: string; sequence: number; focus: LensFocus; total: number; entries: readonly unknown[]; nextOffset: number | null;
|
|
25
|
-
components: ContextLens["components"]; used: ContextLens["used"]; countKind: ContextLens["countKind"] } };
|
|
26
|
-
"item/list": { params: { threadId: string; after?: { node: number; event: string }; limit?: number }; result: {
|
|
27
|
-
items: { node: number; id: string; type: LogEvent["t"] }[]; nextCursor: { node: number; event: string } | null } };
|
|
28
|
-
"item/read": { params: { threadId: string; node: number; event: string }; result: { node: number; event: LogEvent } };
|
|
29
|
-
"thread/subscribe": { params: { threadId: string; after?: { streamId: string; sequence: number } }; result: { snapshot: ControlSnapshot; resyncRequired: boolean } };
|
|
30
|
-
}
|
|
31
|
-
function record(value: unknown): value is Record<string, unknown> { return !!value && typeof value === "object" && !Array.isArray(value); }
|
|
32
|
-
function identity(value: unknown): value is string { return typeof value === "string" && value.length > 0 && value.length <= 128; }
|
|
33
|
-
function count(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; }
|
|
34
|
-
function snapshot(value: unknown): boolean {
|
|
35
|
-
return record(value) && value["version"] === CONTROL_SCHEMA.protocol && identity(value["threadId"]) && identity(value["streamId"]) && count(value["sequence"])
|
|
36
|
-
&& count(value["node"]) && (value["activeTurnId"] === null || identity(value["activeTurnId"])) && (value["goal"] === null || record(value["goal"]))
|
|
37
|
-
&& [null, "permission", "question", "auth"].includes(value["waiting"] as null) && Array.isArray(value["approvals"]) && Array.isArray(value["questions"]) && record(value["context"]);
|
|
38
|
-
}
|
|
39
|
-
function result(method: keyof ControlApi, value: unknown): boolean {
|
|
40
|
-
if (["thread/read", "thread/new", "thread/resume", "thread/fork", "thread/mode"].includes(method)) return snapshot(value);
|
|
41
|
-
if (!record(value)) return false;
|
|
42
|
-
switch (method) {
|
|
43
|
-
case "thread/list": return Array.isArray(value["sessions"]) && count(value["truncated"]) && Array.isArray(value["live"]) && value["live"].every(identity);
|
|
44
|
-
case "thread/close": return typeof value["closed"] === "boolean";
|
|
45
|
-
case "turn/start": return identity(value["turnId"]);
|
|
46
|
-
case "turn/steer": return typeof value["queued"] === "boolean";
|
|
47
|
-
case "turn/interrupt": return typeof value["interruptRequested"] === "boolean";
|
|
48
|
-
case "approval/respond": case "question/respond": case "acceptance/accept": return typeof value["accepted"] === "boolean";
|
|
49
|
-
case "thread/subscribe": return snapshot(value["snapshot"]) && typeof value["resyncRequired"] === "boolean";
|
|
50
|
-
case "context/read": return count(value["node"]) && identity(value["streamId"]) && count(value["sequence"]) && ["messages", "omitted", "history"].includes(String(value["focus"]))
|
|
51
|
-
&& count(value["total"]) && Array.isArray(value["entries"]) && (value["nextOffset"] === null || count(value["nextOffset"])) && record(value["components"])
|
|
52
|
-
&& typeof value["used"] === "number" && typeof value["countKind"] === "string";
|
|
53
|
-
case "item/list": return Array.isArray(value["items"]) && value["items"].every(item => record(item) && count(item["node"]) && identity(item["id"]) && typeof item["type"] === "string")
|
|
54
|
-
&& (value["nextCursor"] === null || record(value["nextCursor"]) && count(value["nextCursor"]["node"]) && identity(value["nextCursor"]["event"]));
|
|
55
|
-
case "item/read": return count(value["node"]) && record(value["event"]) && identity(value["event"]["id"]) && typeof value["event"]["t"] === "string";
|
|
56
|
-
default: return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export class ControlClient {
|
|
60
|
-
private readonly write: (line: string) => void;
|
|
61
|
-
private readonly closeTransport: (() => void) | undefined;
|
|
62
|
-
private next = 1;
|
|
63
|
-
private buffer = "";
|
|
64
|
-
private closed = false;
|
|
65
|
-
private ready = false;
|
|
66
|
-
private initializing = false;
|
|
67
|
-
private readonly pending = new Map<number, { resolve(value: unknown): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout> }>();
|
|
68
|
-
private readonly listeners = new Set<(notice: ControlNotice) => void>();
|
|
69
|
-
constructor(write: (line: string) => void, options: { close?: () => void } = {}) { this.write = write; this.closeTransport = options.close; }
|
|
70
|
-
onNotice(listener: (notice: ControlNotice) => void, threadId?: string): () => void {
|
|
71
|
-
const actual = threadId ? (notice: ControlNotice) => { if (notice.params.threadId === threadId) listener(notice); } : listener;
|
|
72
|
-
this.listeners.add(actual); return () => { this.listeners.delete(actual); };
|
|
73
|
-
}
|
|
74
|
-
async initialize(name: string, version: string): Promise<{ threadId: string; streamId: string }> {
|
|
75
|
-
if (this.ready || this.initializing) throw Error("control client already initialized or initializing");
|
|
76
|
-
this.initializing = true;
|
|
77
|
-
try {
|
|
78
|
-
const value = await this.raw("initialize", { clientInfo: { name, version } });
|
|
79
|
-
if (!record(value) || value["protocol"] !== CONTROL_SCHEMA.protocol || !identity(value["threadId"]) || !identity(value["streamId"])) {
|
|
80
|
-
this.close("unsupported or invalid control initialize result"); throw Error("unsupported or invalid control initialize result");
|
|
81
|
-
}
|
|
82
|
-
try { this.write(JSON.stringify({ method: "initialized", params: {} }) + "\n"); }
|
|
83
|
-
catch { this.close("control handshake delivery failed"); throw Error("control handshake delivery failed"); }
|
|
84
|
-
this.ready = true; return { threadId: value["threadId"], streamId: value["streamId"] };
|
|
85
|
-
} finally { this.initializing = false; }
|
|
86
|
-
}
|
|
87
|
-
request<M extends keyof ControlApi>(method: M, params: ControlApi[M]["params"]): Promise<ControlApi[M]["result"]> {
|
|
88
|
-
if (!this.ready) return Promise.reject(Error("initialize the control client before making requests"));
|
|
89
|
-
return this.raw(method, params).then(value => {
|
|
90
|
-
if (!result(method, value)) { this.close("invalid typed control result"); throw Error("invalid typed control result"); }
|
|
91
|
-
return value as ControlApi[M]["result"];
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
respondQuestion(threadId: string, turnId: string, requestId: string, reply: QuestionReply): Promise<{ accepted: boolean }> {
|
|
95
|
-
return this.request("question/respond", { threadId, turnId, requestId, reply: JSON.stringify(reply) });
|
|
96
|
-
}
|
|
97
|
-
raw(method: string, params: Record<string, unknown>, timeoutMs = 30_000): Promise<unknown> {
|
|
98
|
-
if (this.closed) return Promise.reject(Error("control client is closed"));
|
|
99
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) return Promise.reject(Error("control timeout must be 1..300000 ms"));
|
|
100
|
-
if (this.pending.size >= CONTROL_SCHEMA.limits.queuedRequests) return Promise.reject(Error("client request limit reached"));
|
|
101
|
-
const id = this.next++, line = JSON.stringify({ id, method, params }) + "\n";
|
|
102
|
-
try { parseControlRequest({ id, method, params }); } catch { return Promise.reject(Error("invalid control request parameters")); }
|
|
103
|
-
if (Buffer.byteLength(line) > CONTROL_SCHEMA.limits.frameBytes) return Promise.reject(Error("request exceeds frame limit"));
|
|
104
|
-
return new Promise((resolve, reject) => {
|
|
105
|
-
const timer = setTimeout(() => { this.pending.delete(id); reject(Error("request timed out; effect is unknown, inspect before retrying")); }, timeoutMs);
|
|
106
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
107
|
-
try { this.write(line); } catch { clearTimeout(timer); this.pending.delete(id); reject(Error("control delivery failed; effects are unknown, inspect before retrying")); }
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
feed(text: string): void {
|
|
111
|
-
if (this.closed) return;
|
|
112
|
-
this.buffer += text;
|
|
113
|
-
for (;;) {
|
|
114
|
-
const at = this.buffer.indexOf("\n");
|
|
115
|
-
if (at < 0) { if (Buffer.byteLength(this.buffer) > CONTROL_SCHEMA.limits.frameBytes) this.close("oversized control frame"); return; }
|
|
116
|
-
const line = this.buffer.slice(0, at); this.buffer = this.buffer.slice(at + 1);
|
|
117
|
-
if (Buffer.byteLength(line) > CONTROL_SCHEMA.limits.frameBytes) { this.close("oversized control frame"); return; }
|
|
118
|
-
if (!line.trim()) continue;
|
|
119
|
-
let data: unknown;
|
|
120
|
-
try { data = JSON.parse(line); } catch { this.close("invalid control JSON"); return; }
|
|
121
|
-
if (!record(data)) { this.close("invalid control envelope"); return; }
|
|
122
|
-
if (Object.hasOwn(data, "id")) {
|
|
123
|
-
const id = data["id"], error = data["error"];
|
|
124
|
-
if (!(id === null || typeof id === "number" && Number.isSafeInteger(id) && id >= 1) || Object.keys(data).some(key => !["id", "result", "error"].includes(key))
|
|
125
|
-
|| Object.hasOwn(data, "result") === Object.hasOwn(data, "error")
|
|
126
|
-
|| Object.hasOwn(data, "error") && (!record(error) || !Number.isSafeInteger(error["code"]) || typeof error["message"] !== "string" || Object.keys(error).some(key => !["code", "message"].includes(key)))) {
|
|
127
|
-
this.close("invalid control response envelope"); return;
|
|
128
|
-
}
|
|
129
|
-
if (id === null) { this.close("control server rejected the connection frame"); return; }
|
|
130
|
-
const pending = this.pending.get(id); if (!pending) continue;
|
|
131
|
-
this.pending.delete(id); clearTimeout(pending.timer);
|
|
132
|
-
if (record(error)) pending.reject(Error(`Control request rejected (${String(error["code"])}): ${String(error["message"]).slice(0, 512)}`)); else pending.resolve(data["result"]);
|
|
133
|
-
} else {
|
|
134
|
-
const params = data["params"], method = data["method"];
|
|
135
|
-
if (Object.keys(data).some(key => !["method", "params"].includes(key)) || typeof method !== "string" || !CONTROL_SCHEMA.notifications.some(name => name === method)
|
|
136
|
-
|| !record(params) || !identity(params["threadId"]) || !identity(params["streamId"]) || !count(params["sequence"]) || params["sequence"] === 0
|
|
137
|
-
|| params["turnId"] !== undefined && !identity(params["turnId"])) { this.close("invalid control notification envelope"); return; }
|
|
138
|
-
for (const listener of this.listeners) try { listener(structuredClone(data) as ControlNotice); } catch { /* observers do not own transport */ }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
close(reason = "control transport closed; unresolved effects are unknown"): void {
|
|
143
|
-
if (this.closed) return;
|
|
144
|
-
this.closed = true; this.ready = false; this.buffer = "";
|
|
145
|
-
for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(Error(reason)); }
|
|
146
|
-
this.pending.clear(); this.listeners.clear();
|
|
147
|
-
try { this.closeTransport?.(); } catch { /* transport failures cannot reopen this client or replay requests */ }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
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
|
-
import type { SessionHub } from "./session-hub.ts";
|
|
8
|
-
|
|
9
|
-
export type RpcOutput = { id: RpcId | null; result?: unknown; error?: { code: number; message: string } } | { method: string; params: unknown };
|
|
10
|
-
|
|
11
|
-
export class ControlConnection {
|
|
12
|
-
private readonly control: () => SessionControl;
|
|
13
|
-
private readonly write: (output: RpcOutput) => void;
|
|
14
|
-
private initialized = false;
|
|
15
|
-
private ready = false;
|
|
16
|
-
private subscribed = false;
|
|
17
|
-
private closed = false;
|
|
18
|
-
private attached: SessionControl | undefined;
|
|
19
|
-
private stop: (() => void) | undefined;
|
|
20
|
-
private readonly denyOnClose: boolean;
|
|
21
|
-
private readonly hub: SessionHub | undefined;
|
|
22
|
-
private readonly subscriptions = new Map<SessionControl, () => void>();
|
|
23
|
-
private readonly questionClients = new Map<SessionControl, () => void>();
|
|
24
|
-
|
|
25
|
-
constructor(control: () => SessionControl, write: (output: RpcOutput) => void, options: { denyOnClose?: boolean; hub?: SessionHub } = {}) {
|
|
26
|
-
this.control = control; this.write = write; this.denyOnClose = options.denyOnClose ?? false;
|
|
27
|
-
this.hub = options.hub;
|
|
28
|
-
}
|
|
29
|
-
private attach(): SessionControl {
|
|
30
|
-
const control = this.control();
|
|
31
|
-
if (this.attached !== control) {
|
|
32
|
-
this.stop?.(); this.attached = control;
|
|
33
|
-
this.stop = control.subscribe(notice => { if (!this.closed && this.ready && this.subscribed) this.write(notice); });
|
|
34
|
-
}
|
|
35
|
-
return control;
|
|
36
|
-
}
|
|
37
|
-
async receive(value: unknown): Promise<void> {
|
|
38
|
-
if (this.closed) return;
|
|
39
|
-
let id: RpcId | null = null;
|
|
40
|
-
try {
|
|
41
|
-
if (typeof value === "object" && value !== null && "id" in value &&
|
|
42
|
-
(typeof value.id === "string" && value.id.length <= 128 || typeof value.id === "number" && Number.isSafeInteger(value.id))) id = value.id;
|
|
43
|
-
const request = parseControlRequest(value), params = request.params;
|
|
44
|
-
let control = this.attach();
|
|
45
|
-
if (request.method === "initialize") {
|
|
46
|
-
if (this.initialized) throw new ControlProtocolError(-32000, "already initialized");
|
|
47
|
-
this.initialized = true;
|
|
48
|
-
this.write({ id, result: { protocol: CONTROL_SCHEMA.protocol, experimental: true, threadId: control.threadId, streamId: control.streamId,
|
|
49
|
-
capabilities: { turns: true, steering: true, approvals: true, questions: true, contextLens: true, snapshotResync: true, durableEventReplay: "item/list", adaptiveExecution: false } } });
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (!this.initialized) throw new ControlProtocolError(-32000, "not initialized");
|
|
53
|
-
if (request.method === "initialized") { if (this.ready) throw new ControlProtocolError(-32000, "already ready"); this.ready = true; return; }
|
|
54
|
-
if (!this.ready) throw new ControlProtocolError(-32000, "send initialized before using the session");
|
|
55
|
-
if (request.method === "thread/list") {
|
|
56
|
-
if (!this.hub) throw Error("multi-session methods require app-server");
|
|
57
|
-
this.write({ id, result: await this.hub.list() }); return;
|
|
58
|
-
}
|
|
59
|
-
if (request.method === "thread/new" || request.method === "thread/resume") {
|
|
60
|
-
if (!this.hub) throw Error("multi-session methods require app-server");
|
|
61
|
-
const opened = await this.hub.open(request.method === "thread/new" ? "new" : "resume", params);
|
|
62
|
-
this.write({ id, result: await opened.snapshot() }); return;
|
|
63
|
-
}
|
|
64
|
-
if (this.hub && typeof params["threadId"] === "string") control = this.hub.get(params["threadId"]);
|
|
65
|
-
if (params["threadId"] !== control.threadId) throw new ControlProtocolError(-32000, "stale or unknown threadId; initialize a new connection after changing sessions");
|
|
66
|
-
let result: unknown;
|
|
67
|
-
switch (request.method) {
|
|
68
|
-
case "thread/fork": {
|
|
69
|
-
if (!this.hub) throw Error("multi-session methods require app-server");
|
|
70
|
-
result = await (await this.hub.open("fork", { ...params, session: control.session.config.logDir })).snapshot(); break;
|
|
71
|
-
}
|
|
72
|
-
case "thread/close": {
|
|
73
|
-
if (!this.hub) throw Error("multi-session methods require app-server");
|
|
74
|
-
await this.hub.close(control.threadId); this.subscriptions.get(control)?.(); this.subscriptions.delete(control);
|
|
75
|
-
this.questionClients.get(control)?.(); this.questionClients.delete(control);
|
|
76
|
-
result = { closed: true }; break;
|
|
77
|
-
}
|
|
78
|
-
case "thread/mode": {
|
|
79
|
-
if (!control.configureWorkflow) throw Error("workflow changes are unavailable for this session");
|
|
80
|
-
await control.configureWorkflow(params["mode"] as "build" | "plan"); result = await control.snapshot(); break;
|
|
81
|
-
}
|
|
82
|
-
case "thread/read": result = await control.snapshot(); break;
|
|
83
|
-
case "thread/subscribe": {
|
|
84
|
-
if (!this.questionClients.has(control)) this.questionClients.set(control, control.attachQuestionClient());
|
|
85
|
-
if (this.hub) {
|
|
86
|
-
if (!this.subscriptions.has(control)) this.subscriptions.set(control, control.subscribe(notice => { if (!this.closed) this.write(notice); }));
|
|
87
|
-
} else this.subscribed = true;
|
|
88
|
-
const snapshot = await control.snapshot();
|
|
89
|
-
const after = params["after"] as { streamId: string; sequence: number } | undefined;
|
|
90
|
-
result = { snapshot, resyncRequired: after !== undefined && (after.streamId !== snapshot.streamId || after.sequence !== snapshot.sequence) };
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
case "turn/start": result = { turnId: control.start(String(params["input"])) }; break;
|
|
94
|
-
case "turn/steer": control.steer(String(params["turnId"]), String(params["input"])); result = { queued: true }; break;
|
|
95
|
-
case "turn/interrupt": control.interrupt(String(params["turnId"])); result = { interruptRequested: true }; break;
|
|
96
|
-
case "approval/respond": control.respond(String(params["turnId"]), String(params["approvalId"]), params["choice"] as ApprovalChoice); result = { accepted: true }; break;
|
|
97
|
-
case "question/respond": control.respondQuestion(String(params["turnId"]), String(params["requestId"]), JSON.parse(String(params["reply"]))); result = { accepted: true }; break;
|
|
98
|
-
case "acceptance/accept": await control.accept(String(params["requestId"]), String(params["criterionId"])); result = { accepted: true }; break;
|
|
99
|
-
case "context/read": {
|
|
100
|
-
const snapshot = await control.snapshot(false), all = selectLens(snapshot.context, params["focus"] as LensFocus);
|
|
101
|
-
const offset = Number(params["offset"] ?? 0), limit = Number(params["limit"] ?? 50);
|
|
102
|
-
result = { node: snapshot.node, streamId: snapshot.streamId, sequence: snapshot.sequence, focus: params["focus"], total: all.length,
|
|
103
|
-
entries: all.slice(offset, offset + limit), nextOffset: offset + limit < all.length ? offset + limit : null,
|
|
104
|
-
components: snapshot.context.components, used: snapshot.context.used, countKind: snapshot.context.countKind };
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case "item/list": {
|
|
108
|
-
const all = await control.items(), after = params["after"] as { node: number; event: string } | undefined;
|
|
109
|
-
const index = after ? all.findIndex(item => item.node === after.node && item.event.id === after.event) : -1;
|
|
110
|
-
if (after && index < 0) throw Error("cursor is not on the current path; refresh the snapshot");
|
|
111
|
-
const entries = all.slice(index + 1, index + 1 + Number(params["limit"] ?? 50));
|
|
112
|
-
const last = entries.at(-1);
|
|
113
|
-
result = { items: entries.map(item => ({ node: item.node, id: item.event.id, type: item.event.t })),
|
|
114
|
-
nextCursor: last && index + 1 + entries.length < all.length ? { node: last.node, event: last.event.id } : null };
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case "item/read": {
|
|
118
|
-
const item = (await control.items()).find(item => item.node === params["node"] && item.event.id === params["event"]);
|
|
119
|
-
if (!item) throw Error("item is not on the current path");
|
|
120
|
-
result = item; break;
|
|
121
|
-
}
|
|
122
|
-
default: assertNever(request.method, "control request");
|
|
123
|
-
}
|
|
124
|
-
if (!this.closed) this.write({ id, result });
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (!this.closed) this.write({ id, error: { code: error instanceof ControlProtocolError ? error.code : -32000,
|
|
127
|
-
message: error instanceof Error ? error.message.slice(0, 512) : "control request failed" } });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
close(): void {
|
|
131
|
-
this.closed = true; this.stop?.();
|
|
132
|
-
for (const stop of this.subscriptions.values()) stop(); this.subscriptions.clear();
|
|
133
|
-
for (const stop of this.questionClients.values()) stop(); this.questionClients.clear();
|
|
134
|
-
if (this.denyOnClose && this.attached) {
|
|
135
|
-
this.attached.denyApprovals();
|
|
136
|
-
if (this.attached.activeTurnId) this.attached.interrupt(this.attached.activeTurnId);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Counter-checks: the discriminating rung of the evidence ladder (S.1).
|
|
3
|
-
*
|
|
4
|
-
* A check that passed is run once more against the workspace with this
|
|
5
|
-
* request's changes undone. If it still passes, it did not test the change --
|
|
6
|
-
* unless it is a guard, which must still pass there (S.7). That is the harness
|
|
7
|
-
* applying to the agent the rule verify-red applies to every card: "a test
|
|
8
|
-
* that cannot fail proves nothing". This module only runs the checks and
|
|
9
|
-
* reports their exit codes; what an exit code means for a criterion of a
|
|
10
|
-
* given role is decided in core (`counterVerdict`).
|
|
11
|
-
*
|
|
12
|
-
* Three rules keep the verdict honest:
|
|
13
|
-
* - Only commands that already ran and passed in this request are re-run --
|
|
14
|
-
* checks the user accepted -- and only in a disposable clone.
|
|
15
|
-
* - The clone must be reverted exactly: after undoing the request's changes
|
|
16
|
-
* its source fingerprint must equal the baseline recorded when the request
|
|
17
|
-
* began. The undo comes from a snapshot taken at the start when there is
|
|
18
|
-
* one, else from the journal; a change neither recorded (a shell command
|
|
19
|
-
* writing files without a checkpoint, with no snapshot) cannot be undone,
|
|
20
|
-
* so nothing runs and the result is unavailable rather than a guess.
|
|
21
|
-
* - The real workspace is never written.
|
|
22
|
-
*/
|
|
23
|
-
import { join, relative } from "node:path";
|
|
24
|
-
import { changedPaths, planFileTransition, type CheckpointEvent, type FileChanges } from "../core/checkpoints.ts";
|
|
25
|
-
import type { LogEvent } from "../core/events.ts";
|
|
26
|
-
import { applyFileChanges } from "../store/file-checkpoints.ts";
|
|
27
|
-
import { observeCommand } from "../store/command.ts";
|
|
28
|
-
import { observeWorkspace } from "../store/workspace-observation.ts";
|
|
29
|
-
import { cloneWorkspace } from "../store/workspace-clone.ts";
|
|
30
|
-
|
|
31
|
-
export type CounterCheckInput = {
|
|
32
|
-
root: string; blobs: string; ownedPaths: readonly string[];
|
|
33
|
-
/** The request's events, in order. */
|
|
34
|
-
events: readonly LogEvent[];
|
|
35
|
-
/**
|
|
36
|
-
* The exact transition back to the request's starting state, when the
|
|
37
|
-
* caller took a snapshot at the start (fix-counter-check-own-baseline). It
|
|
38
|
-
* covers changes a shell command made. Absent: the request's journaled
|
|
39
|
-
* file changes are undone instead.
|
|
40
|
-
*/
|
|
41
|
-
revert?: FileChanges;
|
|
42
|
-
checks: readonly { id: string; command: string; guard?: true }[];
|
|
43
|
-
timeoutMs: number; maxOutputBytes: number; outputStore: string; signal?: AbortSignal;
|
|
44
|
-
};
|
|
45
|
-
export type CounterCheckResult = { id: string; command: string; guard?: true } & ({ exit: number } | { reason: string });
|
|
46
|
-
|
|
47
|
-
export async function counterCheck(input: CounterCheckInput): Promise<CounterCheckResult[]> {
|
|
48
|
-
const unavailable = (reason: string): CounterCheckResult[] => input.checks.map(check => ({ ...check, reason }));
|
|
49
|
-
if (input.checks.length === 0) return [];
|
|
50
|
-
const baseline = input.events.findLast(event => event.t === "workspace_observation" && event.baseline === true);
|
|
51
|
-
if (baseline?.t !== "workspace_observation" || baseline.fingerprint === undefined) return unavailable("no workspace baseline was recorded when this request began");
|
|
52
|
-
const applied = input.events.filter((event): event is CheckpointEvent => event.t === "checkpoint" && event.phase === "applied");
|
|
53
|
-
// Undoing every applied change of this request and replaying none is the journal's revert.
|
|
54
|
-
const revert = input.revert ?? planFileTransition(applied, []);
|
|
55
|
-
if (changedPaths(revert).length === 0) return unavailable("this request recorded no file changes to undo");
|
|
56
|
-
const clone = await cloneWorkspace(input.root, input.signal);
|
|
57
|
-
if (!clone.ok) return unavailable(clone.reason);
|
|
58
|
-
try {
|
|
59
|
-
try { await applyFileChanges(clone.path, input.blobs, revert, input.signal); }
|
|
60
|
-
catch (error) { return unavailable(`the recorded changes could not be undone: ${error instanceof Error ? error.message : String(error)}`); }
|
|
61
|
-
const owned = input.ownedPaths.map(path => { const rel = relative(input.root, path); return rel.startsWith("..") ? path : join(clone.path, rel); });
|
|
62
|
-
const observed = await observeWorkspace(clone.path, input.signal, owned);
|
|
63
|
-
if (!observed.ok) return unavailable(`the reverted copy could not be observed: ${observed.reason}`);
|
|
64
|
-
if (observed.fingerprint !== baseline.fingerprint) return unavailable(input.revert ? "the request's starting state could not be rebuilt exactly"
|
|
65
|
-
: "some changes were made outside tracked edits, so the request's starting state could not be rebuilt");
|
|
66
|
-
const results: CounterCheckResult[] = [];
|
|
67
|
-
for (const check of input.checks) {
|
|
68
|
-
const captured = await observeCommand({ command: check.command, cwd: clone.path, outputStore: input.outputStore,
|
|
69
|
-
maxOutputBytes: input.maxOutputBytes, timeoutMs: input.timeoutMs, ...(input.signal ? { signal: input.signal } : {}) });
|
|
70
|
-
results.push(captured.stopped ? { ...check, reason: `the check did not finish in the reverted copy (${captured.stopped})` } : { ...check, exit: captured.exit });
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
} finally { await clone.dispose(); }
|
|
74
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/** O.6: resolve model associations against original observations in the active ancestry. */
|
|
2
|
-
import type { EvidenceLinkEvent } from "../core/events.ts";
|
|
3
|
-
import type { Handoff } from "../core/handoff.ts";
|
|
4
|
-
import { historyEntries } from "../core/history-query.ts";
|
|
5
|
-
import { observedVersion, resourceKey } from "../core/evidence-links.ts";
|
|
6
|
-
import { scopedWorkId } from "../core/work-state.ts";
|
|
7
|
-
import { readNodeHistory } from "../store/restore.ts";
|
|
8
|
-
import { failureDiagnostic, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
9
|
-
import type { ToolResult } from "./tools.ts";
|
|
10
|
-
|
|
11
|
-
export async function resolveEvidenceNote(logDir: string, node: number, namespace: string, current: Handoff, event: EvidenceLinkEvent): Promise<{ event?: EvidenceLinkEvent; result: ToolResult }> {
|
|
12
|
-
const work = scopedWorkId(namespace, event.work, current.state.work_items ?? []);
|
|
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) } });
|
|
15
|
-
if (!current.state.work_items?.some(item => item.id === work)) return fail("pin requires an existing work item");
|
|
16
|
-
const history = await readNodeHistory(logDir, node);
|
|
17
|
-
if (!history.ok) return fail(history.error, true);
|
|
18
|
-
const entries = historyEntries(history.nodes);
|
|
19
|
-
const source = entries.find(entry => entry.event === event.source && entry.kind === "tool_result");
|
|
20
|
-
if (!source) return fail("pin source must be an original tool observation in the active ancestry");
|
|
21
|
-
const resource = event.resource === undefined ? source.resources?.[0] : resourceKey(event.resource);
|
|
22
|
-
if (resource !== undefined && !source.resources?.includes(resource)) return fail("pin resource must match the original observation");
|
|
23
|
-
if (event.supersedes && (event.supersedes === event.source || !current.evidence_links?.some(link =>
|
|
24
|
-
link.work === work && link.event === event.supersedes && link.superseded_by === undefined))) return fail("supersedes must name an active pinned source for this work item");
|
|
25
|
-
const events = history.nodes.flatMap(part => [...part.events]);
|
|
26
|
-
const observed = events.find(item => item.id === event.source);
|
|
27
|
-
const effect = observed?.t === "tool_result" ? observed.effect : undefined;
|
|
28
|
-
const version = effect && "path" in effect ? effect.kind === "read" ? effect.sha256 : effect.sha256_after : undefined;
|
|
29
|
-
const latest = resource === undefined ? undefined : observedVersion(events, resource);
|
|
30
|
-
const resolved: EvidenceLinkEvent = { ...event, work,
|
|
31
|
-
...(resource !== undefined ? { resource } : {}), ...(version !== undefined ? { version } : {}),
|
|
32
|
-
...(latest !== undefined ? { observed_version: latest } : {}) };
|
|
33
|
-
return { event: resolved, result: { ok: true, content: JSON.stringify({ recorded: true, work_id: work, source_event: source.event, source_node: source.node,
|
|
34
|
-
resource: resource ?? null, instruction: "This links observed evidence; completion still requires verification." }) } };
|
|
35
|
-
}
|