@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,456 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Processes that outlive one tool call: servers, REPLs, long builds, prompts.
|
|
3
|
-
*
|
|
4
|
-
* `exec_command` starts one and waits up to a yield time; if it is still
|
|
5
|
-
* running, the model gets a session id and polls or types into it with
|
|
6
|
-
* `write_stdin`. The shape and names follow Codex's unified exec
|
|
7
|
-
* (codex-rs `unified_exec/`, Apache-2.0; design only), which is what GPT-family
|
|
8
|
-
* models are trained against, and Warp's Terminal-Bench write-up found that
|
|
9
|
-
* control over interactive programs was worth more than anything else.
|
|
10
|
-
*
|
|
11
|
-
* Every call still ends in one tool_result, so the log records each
|
|
12
|
-
* observation in order (log-as-truth). What the log cannot hold is the
|
|
13
|
-
* process itself: sessions belong to this OS process and die with it, and a
|
|
14
|
-
* restarted xeno recovers observations from the log without reattaching a PID.
|
|
15
|
-
*
|
|
16
|
-
* Output is spooled to a file as it arrives. Each call archives exactly the
|
|
17
|
-
* bytes since the previous call as an observation blob (D8), without keeping
|
|
18
|
-
* cumulative output strings. Capture errors are explicit partial observations.
|
|
19
|
-
*/
|
|
20
|
-
import { open, mkdir, rm, type FileHandle } from "node:fs/promises";
|
|
21
|
-
import { createReadStream } from "node:fs";
|
|
22
|
-
import { join } from "node:path";
|
|
23
|
-
import { randomUUID } from "node:crypto";
|
|
24
|
-
import { commandArgv, commandEnvironment, COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
|
|
25
|
-
import { archiveObservation, type ArchivedObservation } from "../store/observations.ts";
|
|
26
|
-
import { configDir } from "../store/config.ts";
|
|
27
|
-
import { xenoBinDir } from "../store/tool-binaries.ts";
|
|
28
|
-
import { ToolFault, NOT_STARTED } from "../core/tool-failure.ts";
|
|
29
|
-
import { processReceipt, retainProcessReceipts, type ProcessReceipt, type ProcessSource } from "../core/process-receipt.ts";
|
|
30
|
-
import type { CommandArtifactObserver } from "./command-artifacts.ts";
|
|
31
|
-
import type { CommandArtifactReceipt } from "../core/command-artifacts.ts";
|
|
32
|
-
|
|
33
|
-
/** Codex's default: long enough for most commands to finish in one call. */
|
|
34
|
-
export const DEFAULT_YIELD_MS = 10_000;
|
|
35
|
-
/** Bounds on a foreground poll or input send. Owned observation waits have no polling deadline. */
|
|
36
|
-
export const MIN_YIELD_MS = 100;
|
|
37
|
-
export const MAX_YIELD_MS = 30_000;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The wait actually taken, clamped like Codex's `clamp_yield_time` (S.6
|
|
41
|
-
* item 5): an out-of-range value still runs the call, and `note` tells the
|
|
42
|
-
* model what it waited instead of what it asked for.
|
|
43
|
-
*/
|
|
44
|
-
export function yieldWait(value: unknown): { ms: number; note: string } {
|
|
45
|
-
const requested = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_YIELD_MS;
|
|
46
|
-
const ms = Math.min(MAX_YIELD_MS, Math.max(MIN_YIELD_MS, requested));
|
|
47
|
-
return { ms, note: ms === requested ? "" : ` · yield_time_ms ${requested} clamped to ${ms}` };
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Bounds on one owned empty wait's window, copied from Codex's empty
|
|
51
|
-
* `write_stdin` (MIN_EMPTY_YIELD_TIME_MS, DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS),
|
|
52
|
-
* not measured. The floor is what stops a chatty process from waking the
|
|
53
|
-
* model every line: below it, a 100 ms request brings the per-line wakeups back.
|
|
54
|
-
*/
|
|
55
|
-
export const MIN_EMPTY_YIELD_MS = 5_000;
|
|
56
|
-
export const MAX_EMPTY_YIELD_MS = 300_000;
|
|
57
|
-
export function emptyYieldWait(value: unknown): { ms: number; note: string } {
|
|
58
|
-
const requested = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_YIELD_MS;
|
|
59
|
-
const ms = Math.min(MAX_EMPTY_YIELD_MS, Math.max(MIN_EMPTY_YIELD_MS, requested));
|
|
60
|
-
return { ms, note: ms === requested ? "" : ` · yield_time_ms ${requested} clamped to ${ms}` };
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Concurrent sessions. An assumption, not a measurement: comfortably above a
|
|
64
|
-
* server + watcher + REPL, low enough that a runaway loop of starts is refused
|
|
65
|
-
* well before it exhausts the machine.
|
|
66
|
-
*/
|
|
67
|
-
export const MAX_SESSIONS = 16;
|
|
68
|
-
/** Assumed drain bound for descendants which escaped the owned group; never claim EOF on expiry. */
|
|
69
|
-
const EXIT_DRAIN_MS = 1_000;
|
|
70
|
-
/** Disposable acceleration only. Original log receipts outlive these assumed memory bounds. */
|
|
71
|
-
export const CLOSED_RECEIPT_LIMIT = 64;
|
|
72
|
-
export const CLOSED_RECEIPT_AGE_MS = 10 * 60_000;
|
|
73
|
-
|
|
74
|
-
type TakenOutput = {
|
|
75
|
-
stored: ArchivedObservation; receipt: ProcessReceipt; wallMs: number;
|
|
76
|
-
exit: number | null; stopped?: "timeout" | "cancelled";
|
|
77
|
-
/**
|
|
78
|
-
* Input that arrived after the process exited and was not written:
|
|
79
|
-
* "control" for interrupt/EOF, whose intent the exit already satisfied;
|
|
80
|
-
* "refused" for real input, which the caller must report as not sent.
|
|
81
|
-
*/
|
|
82
|
-
unsent?: "control" | "refused";
|
|
83
|
-
};
|
|
84
|
-
type ClosedSession = { owner?: string; id: number; endedAt: number; receipt: ProcessReceipt };
|
|
85
|
-
|
|
86
|
-
interface Session {
|
|
87
|
-
owner?: string;
|
|
88
|
-
id: number;
|
|
89
|
-
command: string;
|
|
90
|
-
cwd: string;
|
|
91
|
-
tty: boolean;
|
|
92
|
-
source: ProcessSource;
|
|
93
|
-
artifactObserver?: CommandArtifactObserver;
|
|
94
|
-
artifacts?: Promise<CommandArtifactReceipt>;
|
|
95
|
-
started: number;
|
|
96
|
-
pid: number;
|
|
97
|
-
spool: FileHandle;
|
|
98
|
-
spoolPath: string;
|
|
99
|
-
queue: Promise<unknown>;
|
|
100
|
-
operations: Promise<unknown>;
|
|
101
|
-
final?: TakenOutput;
|
|
102
|
-
captureError?: string;
|
|
103
|
-
written: number;
|
|
104
|
-
read: number;
|
|
105
|
-
exit: number | null;
|
|
106
|
-
stopped?: "timeout" | "cancelled";
|
|
107
|
-
escalation?: ReturnType<typeof setTimeout>;
|
|
108
|
-
timeoutMs: number;
|
|
109
|
-
exited: Promise<void>;
|
|
110
|
-
drained: Promise<void>;
|
|
111
|
-
finishing?: Promise<void>;
|
|
112
|
-
closeCapture(): Promise<void>;
|
|
113
|
-
alive(): boolean;
|
|
114
|
-
write(chars: string): void;
|
|
115
|
-
kill(signal: NodeJS.Signals): void;
|
|
116
|
-
timer: ReturnType<typeof setTimeout>;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const sessions = new Map<number, Session>();
|
|
120
|
-
const closedSessions = new Map<number, ClosedSession>();
|
|
121
|
-
// O.14: a restarted process must not accidentally reuse an old poll capability.
|
|
122
|
-
let nextId = Number.parseInt(randomUUID().replaceAll("-", "").slice(0, 12), 16);
|
|
123
|
-
let hooked = false;
|
|
124
|
-
|
|
125
|
-
function killGroup(pid: number, signal: NodeJS.Signals): void {
|
|
126
|
-
try { process.kill(-pid, signal); } catch (error) {
|
|
127
|
-
if ((error as { code?: string }).code !== "ESRCH") {
|
|
128
|
-
try { process.kill(pid, signal); } catch { /* already gone */ }
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Every session dies with xeno: a server nobody can poll is a leak, not a feature. */
|
|
134
|
-
export function killAllExecSessions(): void {
|
|
135
|
-
for (const session of sessions.values()) {
|
|
136
|
-
clearTimeout(session.timer);
|
|
137
|
-
clearTimeout(session.escalation);
|
|
138
|
-
if (session.exit === null) session.kill("SIGKILL");
|
|
139
|
-
void session.spool.close().catch(() => {});
|
|
140
|
-
void rm(session.spoolPath, { force: true });
|
|
141
|
-
}
|
|
142
|
-
sessions.clear();
|
|
143
|
-
closedSessions.clear();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function runningSessions(owner?: string): { id: number; command: string; exit: number | null }[] {
|
|
147
|
-
return [...sessions.values()].filter(session => session.owner === owner).map(({ id, command, exit }) => ({ id, command, exit }));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function pruneClosed(now: number): void {
|
|
151
|
-
const retained = retainProcessReceipts([...closedSessions.values()], now, CLOSED_RECEIPT_AGE_MS, CLOSED_RECEIPT_LIMIT);
|
|
152
|
-
closedSessions.clear();
|
|
153
|
-
for (const item of retained) closedSessions.set(item.id, item);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function closedSession(id: number, owner?: string): ClosedSession | undefined {
|
|
157
|
-
pruneClosed(performance.now());
|
|
158
|
-
const item = closedSessions.get(id);
|
|
159
|
-
return item?.owner === owner ? item : undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function append(session: Session, chunk: Uint8Array): void {
|
|
163
|
-
if (chunk.length === 0) return;
|
|
164
|
-
const bytes = chunk.slice();
|
|
165
|
-
session.queue = session.queue.then(async () => {
|
|
166
|
-
let offset = 0;
|
|
167
|
-
while (offset < bytes.length) {
|
|
168
|
-
const { bytesWritten } = await session.spool.write(bytes, offset, bytes.length - offset, session.written);
|
|
169
|
-
if (bytesWritten < 1) throw new Error("process output spool made no write progress");
|
|
170
|
-
offset += bytesWritten; session.written += bytesWritten;
|
|
171
|
-
}
|
|
172
|
-
}).catch(error => {
|
|
173
|
-
session.captureError = `process output capture failed: ${String(error)}`;
|
|
174
|
-
stopSession(session, "cancelled");
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export async function startSession(input: {
|
|
179
|
-
command: string; cwd: string; tty: boolean; timeoutMs: number; spoolDir: string; source: ProcessSource; owner?: string;
|
|
180
|
-
artifactObserver?: CommandArtifactObserver;
|
|
181
|
-
/** Internal literal launch capability; model exec_command remains a Bash contract. */
|
|
182
|
-
argv?: readonly string[];
|
|
183
|
-
}): Promise<Session | { error: string }> {
|
|
184
|
-
if (sessions.size >= MAX_SESSIONS) {
|
|
185
|
-
const list = runningSessions(input.owner).map((s) => s.id).join(", ");
|
|
186
|
-
return { error: `refused: managed process capacity ${MAX_SESSIONS} reached; your handles: ${list || "none"}; finish or cancel an owned process before starting another` };
|
|
187
|
-
}
|
|
188
|
-
if (!hooked) { hooked = true; process.once("exit", killAllExecSessions); }
|
|
189
|
-
await mkdir(input.spoolDir, { recursive: true });
|
|
190
|
-
const spoolPath = join(input.spoolDir, `.${randomUUID()}.session`);
|
|
191
|
-
const spool = await open(spoolPath, "w+", 0o600);
|
|
192
|
-
const env = commandEnvironment(process.env, xenoBinDir(configDir()));
|
|
193
|
-
let resolveExit!: () => void;
|
|
194
|
-
const exited = new Promise<void>((resolve) => { resolveExit = resolve; });
|
|
195
|
-
const session = {
|
|
196
|
-
id: nextId++, ...(input.owner === undefined ? {} : { owner: input.owner }), command: input.command, cwd: input.cwd, tty: input.tty, source: input.source, started: performance.now(),
|
|
197
|
-
spool, spoolPath, queue: Promise.resolve(), operations: Promise.resolve(), written: 0, read: 0,
|
|
198
|
-
exit: null, timeoutMs: input.timeoutMs, exited, drained: Promise.resolve(),
|
|
199
|
-
...(input.artifactObserver ? { artifactObserver: input.artifactObserver } : {}),
|
|
200
|
-
} as Omit<Session, "pid" | "write" | "kill" | "timer"> as Session;
|
|
201
|
-
|
|
202
|
-
try { if (input.tty) {
|
|
203
|
-
// A PTY: the line discipline turns ^C into SIGINT for the foreground job,
|
|
204
|
-
// which is what an interactive program expects. Bun ships the PTY.
|
|
205
|
-
let drained!: () => void;
|
|
206
|
-
session.drained = new Promise<void>(resolve => { drained = resolve; });
|
|
207
|
-
const proc = Bun.spawn(input.argv ? [...input.argv] : commandArgv(input.command), {
|
|
208
|
-
cwd: input.cwd, env,
|
|
209
|
-
terminal: { cols: 120, rows: 40, data: (_terminal, data) => append(session, data),
|
|
210
|
-
// This callback closes the stream; only proc.exited supplies the command exit.
|
|
211
|
-
exit: (_terminal, code) => { if (code !== 0) session.captureError = "PTY output stream closed with a read error"; drained(); } },
|
|
212
|
-
});
|
|
213
|
-
session.pid = proc.pid;
|
|
214
|
-
session.alive = () => proc.exitCode === null;
|
|
215
|
-
session.write = (chars) => { proc.terminal?.write(chars); };
|
|
216
|
-
session.kill = (signal) => killGroup(proc.pid, signal);
|
|
217
|
-
session.closeCapture = async () => { proc.terminal?.close(); drained(); };
|
|
218
|
-
void proc.exited.then((code) => { session.exit = code; resolveExit(); void drainExitedSession(session); });
|
|
219
|
-
} else {
|
|
220
|
-
const proc = Bun.spawn(input.argv ? [...input.argv] : commandArgv(input.command), {
|
|
221
|
-
cwd: input.cwd, env, stdin: "pipe", stdout: "pipe", stderr: "pipe", detached: true,
|
|
222
|
-
});
|
|
223
|
-
session.pid = proc.pid;
|
|
224
|
-
session.alive = () => proc.exitCode === null;
|
|
225
|
-
// Without a terminal there is no line discipline, so the two control
|
|
226
|
-
// characters a model reaches for are mapped by hand: ^C interrupts the
|
|
227
|
-
// group, ^D closes stdin.
|
|
228
|
-
session.write = (chars) => {
|
|
229
|
-
for (const part of chars.split(/(\u0003|\u0004)/)) {
|
|
230
|
-
if (part === "\u0003") killGroup(proc.pid, "SIGINT");
|
|
231
|
-
else if (part === "\u0004") void proc.stdin.end();
|
|
232
|
-
else if (part.length > 0) { proc.stdin.write(part); void proc.stdin.flush(); }
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
session.kill = (signal) => killGroup(proc.pid, signal);
|
|
236
|
-
const readers: ReadableStreamDefaultReader<Uint8Array>[] = [];
|
|
237
|
-
const pump = async (stream: ReadableStream<Uint8Array>) => {
|
|
238
|
-
const reader = stream.getReader();
|
|
239
|
-
readers.push(reader);
|
|
240
|
-
try { for (;;) { const { done, value } = await reader.read(); if (done) return; append(session, value); await session.queue; } }
|
|
241
|
-
catch (error) { session.captureError = `process output stream failed: ${String(error)}`; }
|
|
242
|
-
finally { reader.releaseLock(); }
|
|
243
|
-
};
|
|
244
|
-
session.closeCapture = async () => { await Promise.allSettled(readers.map(reader => reader.cancel())); };
|
|
245
|
-
session.drained = Promise.all([pump(proc.stdout), pump(proc.stderr)]).then(() => {});
|
|
246
|
-
void proc.exited.then((code) => { session.exit = code; resolveExit(); void drainExitedSession(session); });
|
|
247
|
-
} } catch (error) {
|
|
248
|
-
await spool.close(); await rm(spoolPath, { force: true });
|
|
249
|
-
const os = (error as { code?: string }).code;
|
|
250
|
-
throw new ToolFault(os === "ENOENT" || os === "ENOTDIR" ? "environment_missing" : os === "EACCES" || os === "EPERM" ? "permission_denied" : "execution_failed", String(error), NOT_STARTED);
|
|
251
|
-
}
|
|
252
|
-
session.timer = setTimeout(() => stopSession(session, "timeout"), input.timeoutMs);
|
|
253
|
-
sessions.set(session.id, session);
|
|
254
|
-
return session;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function findSession(id: number, owner?: string): Session | undefined {
|
|
258
|
-
const session = sessions.get(id);
|
|
259
|
-
return session?.owner === owner ? session : undefined;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function stopSession(session: Session, reason: "timeout" | "cancelled"): void {
|
|
263
|
-
if (session.stopped !== undefined || session.exit !== null) return;
|
|
264
|
-
session.stopped = reason;
|
|
265
|
-
clearTimeout(session.timer);
|
|
266
|
-
session.kill("SIGTERM");
|
|
267
|
-
session.escalation = setTimeout(() => session.kill("SIGKILL"), COMMAND_TERMINATION_GRACE_MS);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** A managed group is owned for its foreground command lifetime, never silently detached. */
|
|
271
|
-
function drainExitedSession(session: Session): Promise<void> {
|
|
272
|
-
// Once only: a later artifact retry must not signal a potentially reused PID/group.
|
|
273
|
-
session.finishing ??= (async () => {
|
|
274
|
-
try { await finishCapture(session); } catch (error) { session.captureError = `output drain failed: ${String(error)}`; }
|
|
275
|
-
// The exit owns this observation even if no model polls until files change again.
|
|
276
|
-
if (session.artifactObserver && session.exit !== null) {
|
|
277
|
-
session.artifacts = session.artifactObserver.finish({ exit: session.exit,
|
|
278
|
-
...(session.stopped ? { stopped: session.stopped } : {}), capture: session.captureError ? "partial" : "complete" });
|
|
279
|
-
await session.artifacts;
|
|
280
|
-
}
|
|
281
|
-
})();
|
|
282
|
-
return session.finishing;
|
|
283
|
-
}
|
|
284
|
-
async function finishCapture(session: Session): Promise<void> {
|
|
285
|
-
session.kill("SIGKILL");
|
|
286
|
-
clearTimeout(session.escalation);
|
|
287
|
-
clearTimeout(session.timer);
|
|
288
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
289
|
-
const deadline = new Promise<false>(resolve => { timer = setTimeout(() => resolve(false), EXIT_DRAIN_MS); });
|
|
290
|
-
const complete = await Promise.race([session.drained.then(() => true), deadline]);
|
|
291
|
-
clearTimeout(timer);
|
|
292
|
-
if (!complete) {
|
|
293
|
-
session.captureError = "output drain deadline reached; a descendant may have escaped the owned group";
|
|
294
|
-
await session.closeCapture();
|
|
295
|
-
}
|
|
296
|
-
await session.queue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Wait for exit, the yield deadline or cancellation, whichever is first. */
|
|
300
|
-
async function waitSession(session: Session, yieldMs: number, signal?: AbortSignal): Promise<void> {
|
|
301
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
302
|
-
let onAbort: (() => void) | undefined;
|
|
303
|
-
const deadline = new Promise<void>((resolve) => { timer = setTimeout(resolve, yieldMs); });
|
|
304
|
-
const aborted = new Promise<void>((resolve) => {
|
|
305
|
-
if (signal === undefined) return;
|
|
306
|
-
onAbort = resolve;
|
|
307
|
-
if (signal.aborted) resolve();
|
|
308
|
-
else signal.addEventListener("abort", onAbort, { once: true });
|
|
309
|
-
});
|
|
310
|
-
try { await Promise.race([session.exited, deadline, aborted]); }
|
|
311
|
-
finally { clearTimeout(timer); if (onAbort) signal?.removeEventListener("abort", onAbort); }
|
|
312
|
-
if (signal?.aborted) stopSession(session, "cancelled");
|
|
313
|
-
if (session.stopped !== undefined) await session.exited;
|
|
314
|
-
if (session.exit !== null) await drainExitedSession(session);
|
|
315
|
-
await session.queue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export interface ObservationWait {
|
|
319
|
-
/** Steering releases this observation wait without cancelling the owned command. */
|
|
320
|
-
wake?: AbortSignal;
|
|
321
|
-
onWaiting?: (active: boolean) => void;
|
|
322
|
-
/** One window; output is delivered at a window's end, never as it arrives. */
|
|
323
|
-
windowMs: number;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* OMP's job owner pattern: no paid model turn merely to observe elapsed time.
|
|
328
|
-
* Exit, timeout, cancellation and steering end the wait at once. Output does
|
|
329
|
-
* not: it is collected until the window ends, and a window with nothing new
|
|
330
|
-
* rolls into the next. Waking per byte made a process that prints a warning
|
|
331
|
-
* every few seconds cost one model request per line (session mv4wyiyg-lebu,
|
|
332
|
-
* fix-owned-wait-coalesce-output); Codex collects to the deadline the same way.
|
|
333
|
-
*/
|
|
334
|
-
async function waitForObservation(session: Session, signal: AbortSignal | undefined, wait: ObservationWait): Promise<void> {
|
|
335
|
-
await session.queue;
|
|
336
|
-
let interrupt!: () => void;
|
|
337
|
-
const interrupted = new Promise<void>(resolve => { interrupt = resolve; });
|
|
338
|
-
signal?.addEventListener("abort", interrupt, { once: true });
|
|
339
|
-
wait.wake?.addEventListener("abort", interrupt, { once: true });
|
|
340
|
-
const ended = () => session.exit !== null || signal?.aborted === true || wait.wake?.aborted === true;
|
|
341
|
-
const pending = !ended();
|
|
342
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
343
|
-
try {
|
|
344
|
-
if (pending) {
|
|
345
|
-
wait.onWaiting?.(true);
|
|
346
|
-
while (!ended()) {
|
|
347
|
-
const deadline = new Promise<void>(resolve => { timer = setTimeout(resolve, wait.windowMs); });
|
|
348
|
-
await Promise.race([session.exited, interrupted, deadline]);
|
|
349
|
-
clearTimeout(timer);
|
|
350
|
-
await session.queue;
|
|
351
|
-
if (session.written > session.read) break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
} finally {
|
|
355
|
-
clearTimeout(timer);
|
|
356
|
-
signal?.removeEventListener("abort", interrupt);
|
|
357
|
-
wait.wake?.removeEventListener("abort", interrupt);
|
|
358
|
-
if (pending) wait.onWaiting?.(false);
|
|
359
|
-
}
|
|
360
|
-
if (signal?.aborted) stopSession(session, "cancelled");
|
|
361
|
-
if (session.stopped !== undefined) await session.exited;
|
|
362
|
-
if (session.exit !== null) await drainExitedSession(session);
|
|
363
|
-
await session.queue;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Bytes printed since the previous call, archived in full. A session whose
|
|
368
|
-
* exit has now been reported is closed.
|
|
369
|
-
*/
|
|
370
|
-
async function takeOutput(session: Session, outputStore: string, previewBytes: number): Promise<TakenOutput> {
|
|
371
|
-
if (session.exit !== null) await drainExitedSession(session);
|
|
372
|
-
await session.queue;
|
|
373
|
-
if (session.exit !== null) await drainExitedSession(session);
|
|
374
|
-
const from = session.read, to = session.written;
|
|
375
|
-
// Snapshot exit before async archival: a later exit belongs to the next observation.
|
|
376
|
-
const exit = session.exit;
|
|
377
|
-
const path = session.spoolPath;
|
|
378
|
-
async function* range(start: number): AsyncGenerator<Uint8Array> {
|
|
379
|
-
if (to <= start) return;
|
|
380
|
-
for await (const chunk of createReadStream(path, { start, end: to - 1 })) yield chunk as Uint8Array;
|
|
381
|
-
}
|
|
382
|
-
const stored = await archiveObservation(outputStore, range(from), previewBytes);
|
|
383
|
-
const full = exit === null ? undefined : from === 0 ? stored : await archiveObservation(outputStore, range(0), previewBytes);
|
|
384
|
-
const artifacts = exit === null ? session.artifactObserver?.pending : await session.artifacts;
|
|
385
|
-
// Neither the cursor nor the handle advances until every referenced artifact exists.
|
|
386
|
-
session.read = to;
|
|
387
|
-
const taken: TakenOutput = { stored, exit, ...(session.stopped ? { stopped: session.stopped } : {}), wallMs: performance.now() - session.started,
|
|
388
|
-
receipt: processReceipt({ session: session.id, source: session.source, exit, output: stored.ref, start: from, end: to,
|
|
389
|
-
...(session.stopped ? { stopped: session.stopped } : {}), ...(full ? { fullOutput: full.ref } : {}),
|
|
390
|
-
...(artifacts ? { artifacts } : {}),
|
|
391
|
-
...(session.captureError ? { capture: "partial" } : exit !== null ? { capture: "complete" } : {}) }) };
|
|
392
|
-
if (exit !== null) {
|
|
393
|
-
clearTimeout(session.timer);
|
|
394
|
-
clearTimeout(session.escalation);
|
|
395
|
-
sessions.delete(session.id);
|
|
396
|
-
session.final = taken;
|
|
397
|
-
closedSessions.set(session.id, { ...(session.owner === undefined ? {} : { owner: session.owner }), id: session.id,
|
|
398
|
-
endedAt: performance.now(), receipt: taken.receipt });
|
|
399
|
-
pruneClosed(performance.now());
|
|
400
|
-
await session.closeCapture();
|
|
401
|
-
await session.spool.close().catch(() => {});
|
|
402
|
-
await rm(path, { force: true });
|
|
403
|
-
}
|
|
404
|
-
return taken;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Input made only of Ctrl-C / Ctrl-D. Sent to a process that has already
|
|
409
|
-
* exited, it asks for something the exit already did (stop, end of input), so
|
|
410
|
-
* it is answered as a poll rather than refused -- otherwise the exit and the
|
|
411
|
-
* unread tail wait for a second call the model may never make (ticket session
|
|
412
|
-
* mv4ugd29, write-stdin-exited card).
|
|
413
|
-
*/
|
|
414
|
-
export function isControlInput(chars: string): boolean {
|
|
415
|
-
return chars.length > 0 && /^[\u0003\u0004]+$/.test(chars);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** One lease covers stdin, waiting, archival and cursor commit; cancellation can interrupt a queued wait. */
|
|
419
|
-
export async function pollSession(session: Session, outputStore: string, previewBytes: number, yieldMs: number,
|
|
420
|
-
chars = "", signal?: AbortSignal, wait?: ObservationWait): Promise<TakenOutput> {
|
|
421
|
-
const cancel = () => stopSession(session, "cancelled");
|
|
422
|
-
if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
|
|
423
|
-
const result = session.operations.then(async () => {
|
|
424
|
-
if (session.final) {
|
|
425
|
-
if (chars.length > 0 && !isControlInput(chars)) throw new ToolFault("not_found", "process is closed; input was not sent", NOT_STARTED);
|
|
426
|
-
return { ...session.final, receipt: { ...session.final.receipt, replayed: true } };
|
|
427
|
-
}
|
|
428
|
-
// An exit not yet reported is still delivered with any late input: the
|
|
429
|
-
// input is never written, but the exit and unread output are not withheld.
|
|
430
|
-
let unsent: TakenOutput["unsent"];
|
|
431
|
-
if (chars.length > 0) {
|
|
432
|
-
if (session.exit !== null) unsent = isControlInput(chars) ? "control" : "refused";
|
|
433
|
-
else session.write(chars);
|
|
434
|
-
}
|
|
435
|
-
if (chars.length === 0 && wait) await waitForObservation(session, signal, wait);
|
|
436
|
-
else await waitSession(session, yieldMs, signal);
|
|
437
|
-
const taken = await takeOutput(session, outputStore, previewBytes);
|
|
438
|
-
return unsent === undefined ? taken : { ...taken, unsent };
|
|
439
|
-
});
|
|
440
|
-
session.operations = result.catch(() => {});
|
|
441
|
-
try { return await result; }
|
|
442
|
-
finally { signal?.removeEventListener("abort", cancel); }
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Stop only this owner, drain its actual exit and preserve every unread byte (D8). */
|
|
446
|
-
export async function disposeExecSessions(owner: string, outputStore: string, previewBytes: number) {
|
|
447
|
-
const owned = [...sessions.values()].filter(session => session.owner === owner);
|
|
448
|
-
const results = await Promise.all(owned.map(async session => {
|
|
449
|
-
stopSession(session, "cancelled");
|
|
450
|
-
const output = await pollSession(session, outputStore, previewBytes, 0);
|
|
451
|
-
if (output.exit === null) throw new Error("disposed process has no exit observation");
|
|
452
|
-
if (output.receipt.replayed) return null;
|
|
453
|
-
return { session: session.id, command: session.command, exit: output.exit, output: output.stored.ref, receipt: output.receipt };
|
|
454
|
-
}));
|
|
455
|
-
return results.filter(item => item !== null);
|
|
456
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** O.1: the creator owns cancellation and waits for cleanup before admitting another run. */
|
|
2
|
-
export class ExecutionControl {
|
|
3
|
-
private active: { controller: AbortController; done: Promise<void>; finish: () => void } | null = null;
|
|
4
|
-
|
|
5
|
-
begin(parent?: AbortSignal): { signal: AbortSignal; finish: () => void } {
|
|
6
|
-
if (this.active !== null) throw new Error("this session already has an active execution");
|
|
7
|
-
const controller = new AbortController();
|
|
8
|
-
let finish!: () => void;
|
|
9
|
-
const done = new Promise<void>(resolve => { finish = resolve; });
|
|
10
|
-
const owned = { controller, done, finish };
|
|
11
|
-
this.active = owned;
|
|
12
|
-
const abort = () => controller.abort();
|
|
13
|
-
parent?.addEventListener("abort", abort, { once: true });
|
|
14
|
-
if (parent?.aborted) abort();
|
|
15
|
-
return {
|
|
16
|
-
signal: controller.signal,
|
|
17
|
-
finish: () => {
|
|
18
|
-
parent?.removeEventListener("abort", abort);
|
|
19
|
-
if (this.active === owned) this.active = null;
|
|
20
|
-
finish();
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
cancel(): void { this.active?.controller.abort(); }
|
|
26
|
-
whenIdle(): Promise<void> { return this.active?.done ?? Promise.resolve(); }
|
|
27
|
-
get running(): boolean { return this.active !== null; }
|
|
28
|
-
get signal(): AbortSignal | undefined { return this.active?.controller.signal; }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Only for admission waits: an unstarted action has no effect to drain. */
|
|
32
|
-
export async function waitForAdmission<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T | undefined> {
|
|
33
|
-
if (signal === undefined) return pending;
|
|
34
|
-
if (signal.aborted) return undefined;
|
|
35
|
-
let abort!: () => void;
|
|
36
|
-
const cancelled = new Promise<undefined>(resolve => { abort = () => resolve(undefined); });
|
|
37
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
38
|
-
try { return await Promise.race([pending, cancelled]); }
|
|
39
|
-
finally { signal.removeEventListener("abort", abort); }
|
|
40
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** Reload local dependency graphs without clearing unrelated process modules. */
|
|
2
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
|
|
7
|
-
export async function loadExtensionFactory(path: string, generation: number): Promise<unknown> {
|
|
8
|
-
// Pi uses a fresh jiti loader with moduleCache:false. Bun can rebuild the
|
|
9
|
-
// local graph itself, so this adds no production loader dependency (D13).
|
|
10
|
-
const result = await Bun.build({ entrypoints: [path], target: "bun", format: "esm",
|
|
11
|
-
plugins: [{ name: "extension-local-graph", setup(build) {
|
|
12
|
-
build.onResolve({ filter: /^[^./]/ }, args => {
|
|
13
|
-
if (isAbsolute(args.path) || args.path.startsWith("node:") || args.path.startsWith("bun:")) return undefined;
|
|
14
|
-
// A package belongs to the extension's installation. Never auto-install
|
|
15
|
-
// dependencies or resolve them against the current workspace.
|
|
16
|
-
return { path: Bun.resolveSync(args.path, dirname(args.importer || path)), external: true };
|
|
17
|
-
});
|
|
18
|
-
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async args => {
|
|
19
|
-
const url = pathToFileURL(args.path).href;
|
|
20
|
-
const transpiler = new Bun.Transpiler({ loader: /tsx$/.test(args.path) ? "tsx" : /jsx$/.test(args.path) ? "jsx" : "ts",
|
|
21
|
-
define: { "import.meta.url": JSON.stringify(url), "import.meta.dir": JSON.stringify(dirname(args.path)),
|
|
22
|
-
"import.meta.dirname": JSON.stringify(dirname(args.path)), "import.meta.path": JSON.stringify(args.path),
|
|
23
|
-
"import.meta.filename": JSON.stringify(args.path), "import.meta.main": "false" } });
|
|
24
|
-
return { contents: transpiler.transformSync(await Bun.file(args.path).text()), loader: "js" };
|
|
25
|
-
});
|
|
26
|
-
} }] });
|
|
27
|
-
if (!result.success || !result.outputs[0]) throw new Error(result.logs.map(log => log.message).join("\n") || "Extension build failed");
|
|
28
|
-
const code = await result.outputs[0].text() + `\n//# sourceURL=${pathToFileURL(path).href}?generation=${generation}\n`;
|
|
29
|
-
// A fresh file gives Bun an ordinary module identity without its data-URL
|
|
30
|
-
// path-length limit. The original import.meta locations were frozen above.
|
|
31
|
-
const staging = await mkdtemp(join(tmpdir(), "xeno-extension-module-"));
|
|
32
|
-
try {
|
|
33
|
-
const entry = join(staging, "extension.mjs"); await Bun.write(entry, code);
|
|
34
|
-
const module: { default?: unknown } = await import(pathToFileURL(entry).href);
|
|
35
|
-
return module.default;
|
|
36
|
-
} finally { await rm(staging, { recursive: true, force: true }); }
|
|
37
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What an extension may do to the interactive UI (D20). The runtime never
|
|
3
|
-
* constructs a terminal; the app supplies an implementation.
|
|
4
|
-
*
|
|
5
|
-
* Deliberately small. Extensions describe content (`render(width): string[]`
|
|
6
|
-
* ANSI lines) and ask for decisions (select / confirm / input); they do not
|
|
7
|
-
* own focus, overlays or the composer. That keeps a misbehaving extension
|
|
8
|
-
* from wedging input, and keeps the UI framework replaceable without
|
|
9
|
-
* breaking extensions again.
|
|
10
|
-
*/
|
|
11
|
-
import type { Theme } from "../core/theme.ts";
|
|
12
|
-
|
|
13
|
-
/** A block of ANSI lines that may depend on width. */
|
|
14
|
-
export interface ExtensionComponent { render(width: number): string[]; dispose?(): void }
|
|
15
|
-
|
|
16
|
-
export interface ExtensionTheme extends Theme {
|
|
17
|
-
fg(color: string, text: string): string; bg(color: string, text: string): string;
|
|
18
|
-
bold(text: string): string; italic(text: string): string; underline(text: string): string; dim(text: string): string; inverse(text: string): string; strikethrough(text: string): string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface DialogOptions { signal?: AbortSignal; timeout?: number }
|
|
22
|
-
|
|
23
|
-
export interface ExtensionUI {
|
|
24
|
-
select(title: string, options: string[], opts?: DialogOptions): Promise<string | undefined>;
|
|
25
|
-
confirm(title: string, message: string, opts?: DialogOptions): Promise<boolean>;
|
|
26
|
-
input(title: string, placeholder?: string, opts?: DialogOptions): Promise<string | undefined>;
|
|
27
|
-
/** Read-only block in a dialog; resolves when the user closes it. */
|
|
28
|
-
show(title: string, component: ExtensionComponent): Promise<void>;
|
|
29
|
-
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
30
|
-
setStatus(key: string, text: string | undefined): void;
|
|
31
|
-
/** Lines shown above the composer, keyed so an extension can replace or clear its own. */
|
|
32
|
-
setWidget(key: string, lines: string[] | undefined): void;
|
|
33
|
-
setWorkingMessage(message?: string): void;
|
|
34
|
-
setHiddenThinkingLabel(label?: string): void;
|
|
35
|
-
setTitle(title: string): void;
|
|
36
|
-
getEditorText(): string; setEditorText(text: string): void; pasteToEditor(text: string): void;
|
|
37
|
-
readonly theme: ExtensionTheme;
|
|
38
|
-
getToolsExpanded(): boolean; setToolsExpanded(expanded: boolean): void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Headless extensions can notify; interactive requests fail explicitly. */
|
|
42
|
-
export function headlessUI(notify: ExtensionUI["notify"]): ExtensionUI {
|
|
43
|
-
return new Proxy({ notify }, { get(target, key) {
|
|
44
|
-
if (key === "notify") return target.notify;
|
|
45
|
-
return () => { throw new Error("Extension UI requires an interactive terminal; check ctx.hasUI"); };
|
|
46
|
-
} }) as ExtensionUI;
|
|
47
|
-
}
|