@platless/axom-cli 0.1.0 → 0.5.9
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/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { privateDirectory } from '../tools/client.js';
|
|
7
|
+
import { cancelledFilePayload, fileErrorFrame } from './adapters/computerFileFrames.js';
|
|
8
|
+
import { FILES_MUTATING_METHODS } from './adapters/toolRequests.js';
|
|
9
|
+
import { FILES_HOST_MAX_IN_FLIGHT } from './hostProtocol.js';
|
|
10
|
+
/*
|
|
11
|
+
* Daemon side of the files host (files/host.ts): spawns it, multiplexes requests over its IPC channel, cancels, samples
|
|
12
|
+
* its activity, and keeps it alive. A crash (or the hung-IO watchdog's kill) fails every request already sent to it —
|
|
13
|
+
* mutations as outcome-unknown, everything else as offline — and the host is respawned with 1 s -> 30 s exponential
|
|
14
|
+
* backoff; the new host's start runs uploads recover(). Requests that arrive while no host is ready wait for one,
|
|
15
|
+
* bounded. A configuration change restarts the host once it is idle.
|
|
16
|
+
*/
|
|
17
|
+
export const TOOL_DEADLINE_MS = 7_500;
|
|
18
|
+
export const DEFAULT_FILES_HOST_TIMINGS = {
|
|
19
|
+
backoffInitialMs: 1_000, backoffMaxMs: 30_000, backoffResetAfterMs: 30_000, hungPinMs: 60_000, readyWaitMs: 10_000,
|
|
20
|
+
cancelGraceMs: 2_000, stopBudgetMs: 6_000, restartUploadGraceMs: 60_000, readyDeadlineMs: 60_000, spawnPrepareMs: 10_000,
|
|
21
|
+
killExitGraceMs: 5_000, watchdogIntervalMs: 1_000,
|
|
22
|
+
};
|
|
23
|
+
function bounded(work, ms, message) {
|
|
24
|
+
let timer;
|
|
25
|
+
const timeout = new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
|
|
26
|
+
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
|
|
27
|
+
}
|
|
28
|
+
const defaultScript = () => fileURLToPath(new URL('./host.js', import.meta.url));
|
|
29
|
+
export class FilesHostClient {
|
|
30
|
+
options;
|
|
31
|
+
hostDir;
|
|
32
|
+
timings;
|
|
33
|
+
child;
|
|
34
|
+
/** Requests may be sent to the current host. */
|
|
35
|
+
ready = false;
|
|
36
|
+
/** The current host reported ready (stays true while a pending restart drains it). */
|
|
37
|
+
hostUp = false;
|
|
38
|
+
readyAt = 0;
|
|
39
|
+
optionsReadAt = 0;
|
|
40
|
+
spawning = false;
|
|
41
|
+
stopped = false;
|
|
42
|
+
failures = 0;
|
|
43
|
+
respawnTimer;
|
|
44
|
+
watchdog;
|
|
45
|
+
lastInfo;
|
|
46
|
+
lastStatus;
|
|
47
|
+
lastStatusAt = 0;
|
|
48
|
+
lastActivity = 0;
|
|
49
|
+
/** Requests sent to the current host and not yet answered. */
|
|
50
|
+
sent = new Map();
|
|
51
|
+
/** Requests waiting for a ready host (not yet sent). */
|
|
52
|
+
waiting = 0;
|
|
53
|
+
readyWaiters = new Set();
|
|
54
|
+
restartRequestedAt = null;
|
|
55
|
+
plannedExit = false;
|
|
56
|
+
/** The current host is being shut down or killed: its status and ready messages no longer describe a live host. */
|
|
57
|
+
retiring = false;
|
|
58
|
+
spawnedAt = 0;
|
|
59
|
+
exited;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.hostDir = path.join(path.resolve(options.configDir), 'files-host');
|
|
63
|
+
this.timings = { ...DEFAULT_FILES_HOST_TIMINGS, ...options.timings };
|
|
64
|
+
this.watchdog = setInterval(() => this.tick(), this.timings.watchdogIntervalMs);
|
|
65
|
+
this.watchdog.unref();
|
|
66
|
+
}
|
|
67
|
+
/** Starts the host (idempotent). Failures are retried with backoff; requests wait for readiness, bounded. */
|
|
68
|
+
start() {
|
|
69
|
+
if (this.stopped || this.child || this.spawning || this.respawnTimer)
|
|
70
|
+
return;
|
|
71
|
+
void this.spawn();
|
|
72
|
+
}
|
|
73
|
+
/** The `tools.info` file fields of the last host that became ready (undefined before the first one). */
|
|
74
|
+
info() { return this.lastInfo; }
|
|
75
|
+
/** Live sample for TaskRegistry: in-progress uploads + running media encodes of the current host. */
|
|
76
|
+
activity() {
|
|
77
|
+
if (!this.child || !this.hostUp || !this.lastStatus)
|
|
78
|
+
return 0;
|
|
79
|
+
return this.lastStatus.uploads + this.lastStatus.encodes;
|
|
80
|
+
}
|
|
81
|
+
/** Process id of the current host (diagnostics and tests). */
|
|
82
|
+
get pid() { return this.child?.pid; }
|
|
83
|
+
get isReady() { return this.ready; }
|
|
84
|
+
/** The configured roots changed: restart the host once it is idle (or drained, after a grace). */
|
|
85
|
+
reconfigure() {
|
|
86
|
+
if (this.stopped)
|
|
87
|
+
return;
|
|
88
|
+
this.restartRequestedAt ??= Date.now();
|
|
89
|
+
this.tick();
|
|
90
|
+
}
|
|
91
|
+
/** One `file_*_request`; never rejects — every failure is a result frame with one of the wire codes. */
|
|
92
|
+
async runFrame(request, signal) {
|
|
93
|
+
const mutation = request.t === 'file_write_request' || request.t === 'file_mkdir_request';
|
|
94
|
+
const outcome = await this.call({ op: 'frame', params: request, deadlineAt: null }, mutation, null, signal);
|
|
95
|
+
if (outcome.ok)
|
|
96
|
+
return outcome.result;
|
|
97
|
+
if (outcome.reason === 'cancelled')
|
|
98
|
+
return fileErrorFrame(request, cancelledFilePayload(request.t));
|
|
99
|
+
if (outcome.reason === 'lost' && mutation)
|
|
100
|
+
return fileErrorFrame(request, { code: 'outcome_unknown', message: outcome.error });
|
|
101
|
+
return fileErrorFrame(request, { code: 'workstation_offline', message: outcome.error });
|
|
102
|
+
}
|
|
103
|
+
/** One `files.*` tool request; rejects with the `tool_result.error` text. */
|
|
104
|
+
async tool(method, params) {
|
|
105
|
+
const deadlineAt = Date.now() + TOOL_DEADLINE_MS;
|
|
106
|
+
const outcome = await this.call({ op: 'tool', params: { method, params }, deadlineAt }, FILES_MUTATING_METHODS.has(method), deadlineAt);
|
|
107
|
+
if (outcome.ok)
|
|
108
|
+
return outcome.result;
|
|
109
|
+
throw new Error(outcome.error);
|
|
110
|
+
}
|
|
111
|
+
/** `terminal.create`'s cwd (absent = home) resolved in the configured-roots scope, as the legacy keeper did; rejects with its error text. */
|
|
112
|
+
async terminalCwd(cwd) {
|
|
113
|
+
const deadlineAt = Date.now() + TOOL_DEADLINE_MS;
|
|
114
|
+
const outcome = await this.call({ op: 'terminalCwd', params: { cwd }, deadlineAt }, false, deadlineAt);
|
|
115
|
+
if (outcome.ok)
|
|
116
|
+
return outcome.result.path;
|
|
117
|
+
throw new Error(outcome.error);
|
|
118
|
+
}
|
|
119
|
+
/** Cleans orphaned upload temporaries recorded in a retired process's manifest directory (`recoverUploads` in the host). */
|
|
120
|
+
async recoverUploads(dir) {
|
|
121
|
+
const deadlineAt = Date.now() + 60_000;
|
|
122
|
+
const outcome = await this.call({ op: 'recoverUploads', params: { dir }, deadlineAt }, true, deadlineAt);
|
|
123
|
+
if (!outcome.ok)
|
|
124
|
+
throw new Error(outcome.error);
|
|
125
|
+
}
|
|
126
|
+
/** Stops the host for good: graceful shutdown within the stop budget, then SIGKILL. Bounded. */
|
|
127
|
+
async stop() {
|
|
128
|
+
this.stopped = true;
|
|
129
|
+
clearInterval(this.watchdog);
|
|
130
|
+
if (this.respawnTimer) {
|
|
131
|
+
clearTimeout(this.respawnTimer);
|
|
132
|
+
this.respawnTimer = undefined;
|
|
133
|
+
}
|
|
134
|
+
for (const wake of [...this.readyWaiters])
|
|
135
|
+
wake();
|
|
136
|
+
await this.shutdownChild();
|
|
137
|
+
}
|
|
138
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
139
|
+
call(body, mutation, deadlineAt, signal) {
|
|
140
|
+
return new Promise(resolve => {
|
|
141
|
+
let done = false;
|
|
142
|
+
let grace;
|
|
143
|
+
let deadline;
|
|
144
|
+
const id = randomUUID();
|
|
145
|
+
const message = { ...body, id };
|
|
146
|
+
const pending = { message, mutation, settle: outcome => {
|
|
147
|
+
if (done)
|
|
148
|
+
return;
|
|
149
|
+
done = true;
|
|
150
|
+
if (grace)
|
|
151
|
+
clearTimeout(grace);
|
|
152
|
+
if (deadline)
|
|
153
|
+
clearTimeout(deadline);
|
|
154
|
+
signal?.removeEventListener('abort', onAbort);
|
|
155
|
+
if (this.sent.get(id) === pending)
|
|
156
|
+
this.sent.delete(id);
|
|
157
|
+
resolve(outcome);
|
|
158
|
+
} };
|
|
159
|
+
const onAbort = () => {
|
|
160
|
+
if (!this.sent.has(id)) {
|
|
161
|
+
pending.settle({ ok: false, error: 'File operation was cancelled', reason: 'cancelled' });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.post({ cancel: id });
|
|
165
|
+
// The host answers a cancelled frame itself at its next checkpoint; a hung one does not, so answer for it.
|
|
166
|
+
grace ??= setTimeout(() => pending.settle({ ok: false, error: 'File operation was cancelled', reason: 'cancelled' }), this.timings.cancelGraceMs);
|
|
167
|
+
};
|
|
168
|
+
if (signal?.aborted) {
|
|
169
|
+
pending.settle({ ok: false, error: 'File operation was cancelled', reason: 'cancelled' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
173
|
+
if (deadlineAt !== null) {
|
|
174
|
+
deadline = setTimeout(() => {
|
|
175
|
+
if (this.sent.has(id))
|
|
176
|
+
this.post({ cancel: id });
|
|
177
|
+
pending.settle({ ok: false, error: 'Workstation tool timed out; mutation outcome may be unknown', reason: 'timeout' });
|
|
178
|
+
}, Math.max(0, deadlineAt - Date.now()));
|
|
179
|
+
}
|
|
180
|
+
if (this.sent.size + this.waiting >= FILES_HOST_MAX_IN_FLIGHT) {
|
|
181
|
+
pending.settle({ ok: false, error: 'Workstation file service is busy; try again shortly', reason: 'unavailable' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
void (async () => {
|
|
185
|
+
this.waiting++;
|
|
186
|
+
let ready;
|
|
187
|
+
try {
|
|
188
|
+
ready = await this.whenReady(deadlineAt === null ? this.timings.readyWaitMs : Math.min(this.timings.readyWaitMs, deadlineAt - Date.now()), () => done);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
this.waiting--;
|
|
192
|
+
}
|
|
193
|
+
if (done)
|
|
194
|
+
return;
|
|
195
|
+
if (!ready || !this.child) {
|
|
196
|
+
pending.settle({ ok: false, error: 'Workstation file service is unavailable; try again shortly', reason: 'unavailable' });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.sent.set(id, pending);
|
|
200
|
+
if (!this.post(message)) {
|
|
201
|
+
// Nothing reached the host: not run, so offline even for a mutation.
|
|
202
|
+
this.sent.delete(id);
|
|
203
|
+
pending.settle({ ok: false, error: 'Workstation file service is unavailable; try again shortly', reason: 'unavailable' });
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
post(message) {
|
|
209
|
+
const child = this.child;
|
|
210
|
+
if (!child?.connected)
|
|
211
|
+
return false;
|
|
212
|
+
try {
|
|
213
|
+
child.send(message);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
whenReady(timeoutMs, abandoned) {
|
|
221
|
+
if (this.ready && this.child)
|
|
222
|
+
return Promise.resolve(true);
|
|
223
|
+
if (this.stopped || timeoutMs <= 0)
|
|
224
|
+
return Promise.resolve(false);
|
|
225
|
+
this.start();
|
|
226
|
+
return new Promise(resolve => {
|
|
227
|
+
const finish = () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
clearInterval(poll);
|
|
230
|
+
this.readyWaiters.delete(wake);
|
|
231
|
+
resolve(this.ready && !!this.child && !this.stopped);
|
|
232
|
+
};
|
|
233
|
+
const wake = () => finish();
|
|
234
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
235
|
+
// A caller that was settled meanwhile (cancel, deadline) stops waiting.
|
|
236
|
+
const poll = setInterval(() => { if (abandoned())
|
|
237
|
+
finish(); }, 50);
|
|
238
|
+
this.readyWaiters.add(wake);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async spawn() {
|
|
242
|
+
if (this.stopped || this.child || this.spawning)
|
|
243
|
+
return;
|
|
244
|
+
this.spawning = true;
|
|
245
|
+
try {
|
|
246
|
+
const readAt = Date.now();
|
|
247
|
+
const base = await bounded((async () => { await privateDirectory(this.hostDir); return this.options.options(); })(), this.timings.spawnPrepareMs, 'preparing the files host timed out');
|
|
248
|
+
this.optionsReadAt = readAt;
|
|
249
|
+
const init = { ...base, uploadStateDir: path.join(this.hostDir, 'uploads') };
|
|
250
|
+
if (this.stopped)
|
|
251
|
+
return;
|
|
252
|
+
const child = fork(this.options.script ?? defaultScript(), [], {
|
|
253
|
+
cwd: this.hostDir, stdio: ['ignore', 'ignore', 'pipe', 'ipc'], execArgv: [], serialization: 'json',
|
|
254
|
+
});
|
|
255
|
+
this.child = child;
|
|
256
|
+
this.ready = false;
|
|
257
|
+
this.hostUp = false;
|
|
258
|
+
this.lastStatus = undefined;
|
|
259
|
+
this.lastStatusAt = Date.now();
|
|
260
|
+
this.plannedExit = false;
|
|
261
|
+
this.retiring = false;
|
|
262
|
+
this.spawnedAt = Date.now();
|
|
263
|
+
let resolveExited;
|
|
264
|
+
this.exited = new Promise(resolve => { resolveExited = resolve; });
|
|
265
|
+
if (child.stderr) {
|
|
266
|
+
const lines = createInterface({ input: child.stderr });
|
|
267
|
+
lines.on('line', line => this.log(`[files-host] ${line.slice(0, 2048)}`));
|
|
268
|
+
}
|
|
269
|
+
child.on('message', (message) => { if (this.child === child)
|
|
270
|
+
this.onMessage(message); });
|
|
271
|
+
let gone = false;
|
|
272
|
+
const onGone = (detail) => {
|
|
273
|
+
if (gone)
|
|
274
|
+
return;
|
|
275
|
+
gone = true;
|
|
276
|
+
resolveExited();
|
|
277
|
+
if (this.child === child)
|
|
278
|
+
this.onExit(detail);
|
|
279
|
+
};
|
|
280
|
+
child.on('exit', (code, signal) => onGone(signal ? `signal ${signal}` : `code ${code}`));
|
|
281
|
+
child.on('error', error => { this.log(`[files-host] ${error.message}`); if (child.exitCode === null && child.signalCode === null && !child.pid)
|
|
282
|
+
onGone('spawn failed'); });
|
|
283
|
+
child.send({ t: 'init', options: init });
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
this.log(`[files-host] start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
this.scheduleRespawn();
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this.spawning = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
onMessage(message) {
|
|
294
|
+
if (!message || typeof message !== 'object')
|
|
295
|
+
return;
|
|
296
|
+
if ('id' in message) {
|
|
297
|
+
const pending = this.sent.get(message.id);
|
|
298
|
+
if (!pending)
|
|
299
|
+
return;
|
|
300
|
+
pending.settle(message.ok ? { ok: true, result: message.result } : { ok: false, error: message.error, reason: 'host' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Only replies are still meaningful from a host being retired (in-flight requests may finish during a graceful stop).
|
|
304
|
+
if (this.retiring)
|
|
305
|
+
return;
|
|
306
|
+
if (message.t === 'ready') {
|
|
307
|
+
this.lastInfo = message.info;
|
|
308
|
+
this.ready = true;
|
|
309
|
+
this.hostUp = true;
|
|
310
|
+
this.readyAt = Date.now();
|
|
311
|
+
this.lastStatusAt = this.readyAt;
|
|
312
|
+
for (const wake of [...this.readyWaiters])
|
|
313
|
+
wake();
|
|
314
|
+
}
|
|
315
|
+
else if (message.t === 'init_failed') {
|
|
316
|
+
this.log(`[files-host] init failed: ${message.error}`);
|
|
317
|
+
}
|
|
318
|
+
else if (message.t === 'status') {
|
|
319
|
+
this.lastStatus = message;
|
|
320
|
+
this.lastStatusAt = Date.now();
|
|
321
|
+
this.activityMayHaveChanged();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
onExit(detail) {
|
|
325
|
+
const planned = this.plannedExit;
|
|
326
|
+
const upFor = this.readyAt ? Date.now() - this.readyAt : 0;
|
|
327
|
+
this.child = undefined;
|
|
328
|
+
this.ready = false;
|
|
329
|
+
this.hostUp = false;
|
|
330
|
+
this.readyAt = 0;
|
|
331
|
+
this.lastStatus = undefined;
|
|
332
|
+
this.retiring = false;
|
|
333
|
+
for (const pending of [...this.sent.values()]) {
|
|
334
|
+
pending.settle({
|
|
335
|
+
ok: false, reason: 'lost',
|
|
336
|
+
error: pending.mutation
|
|
337
|
+
? (pending.message.op === 'frame' ? 'Workstation file service restarted; the outcome is unknown' : 'Workstation file service restarted; mutation outcome may be unknown')
|
|
338
|
+
: 'Workstation file service restarted; try again',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
this.sent.clear();
|
|
342
|
+
this.activityMayHaveChanged();
|
|
343
|
+
if (this.stopped)
|
|
344
|
+
return;
|
|
345
|
+
if (planned) {
|
|
346
|
+
this.failures = 0;
|
|
347
|
+
void this.spawn();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
this.log(`[files-host] exited (${detail}); restarting`);
|
|
351
|
+
if (upFor >= this.timings.backoffResetAfterMs)
|
|
352
|
+
this.failures = 0;
|
|
353
|
+
this.scheduleRespawn();
|
|
354
|
+
}
|
|
355
|
+
scheduleRespawn() {
|
|
356
|
+
if (this.stopped || this.respawnTimer)
|
|
357
|
+
return;
|
|
358
|
+
const delay = Math.min(this.timings.backoffMaxMs, this.timings.backoffInitialMs * 2 ** this.failures);
|
|
359
|
+
this.failures++;
|
|
360
|
+
this.respawnTimer = setTimeout(() => { this.respawnTimer = undefined; void this.spawn(); }, delay);
|
|
361
|
+
this.respawnTimer.unref();
|
|
362
|
+
}
|
|
363
|
+
tick() {
|
|
364
|
+
const child = this.child;
|
|
365
|
+
if (!child || this.stopped || this.retiring)
|
|
366
|
+
return;
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
if (!this.hostUp) {
|
|
369
|
+
if (now - this.spawnedAt > this.timings.readyDeadlineMs) {
|
|
370
|
+
this.log(`[files-host] not ready ${now - this.spawnedAt} ms after start; killing pid ${child.pid}`);
|
|
371
|
+
this.kill(child);
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const since = now - this.lastStatusAt;
|
|
376
|
+
const pinned = this.lastStatus?.pinnedMs ? this.lastStatus.pinnedMs + since : 0;
|
|
377
|
+
if (pinned > this.timings.hungPinMs || since > this.timings.hungPinMs) {
|
|
378
|
+
// Last resort for a hung IO (NFS): nothing else releases the shared pinned() mutex. In-flight requests fail as
|
|
379
|
+
// lost (mutations outcome-unknown) when the exit arrives; the respawn runs uploads recover().
|
|
380
|
+
this.log(`[files-host] ${pinned > this.timings.hungPinMs ? `pinned section held ${pinned} ms` : `no status for ${since} ms`}; killing pid ${child.pid}`);
|
|
381
|
+
this.kill(child);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (this.restartRequestedAt === null || this.plannedExit)
|
|
385
|
+
return;
|
|
386
|
+
// The current host read its options after the change was requested: it is already current.
|
|
387
|
+
if (this.restartRequestedAt <= this.optionsReadAt) {
|
|
388
|
+
this.restartRequestedAt = null;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const overdue = now - this.restartRequestedAt > this.timings.restartUploadGraceMs;
|
|
392
|
+
// Past the grace, stop feeding it: new requests wait for the restarted host while the ones in flight finish.
|
|
393
|
+
if (overdue)
|
|
394
|
+
this.ready = false;
|
|
395
|
+
if (this.sent.size === 0 && (overdue || (this.lastStatus?.uploads ?? 0) === 0)) {
|
|
396
|
+
this.restartRequestedAt = null;
|
|
397
|
+
this.log('[files-host] workspaces changed; restarting');
|
|
398
|
+
void this.shutdownChild(true);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Graceful shutdown of the current host (bounded by SIGKILL). `planned` respawns right after it exits. */
|
|
402
|
+
async shutdownChild(planned = false) {
|
|
403
|
+
const child = this.child;
|
|
404
|
+
if (!child)
|
|
405
|
+
return;
|
|
406
|
+
this.ready = false;
|
|
407
|
+
this.plannedExit = planned;
|
|
408
|
+
this.retiring = true;
|
|
409
|
+
this.lastStatus = undefined;
|
|
410
|
+
this.activityMayHaveChanged();
|
|
411
|
+
const exited = this.exited ?? Promise.resolve();
|
|
412
|
+
if (!this.post({ t: 'shutdown' }))
|
|
413
|
+
this.kill(child);
|
|
414
|
+
let timer;
|
|
415
|
+
const budget = new Promise(resolve => { timer = setTimeout(() => resolve('timeout'), this.timings.stopBudgetMs); });
|
|
416
|
+
if (await Promise.race([exited.then(() => 'exited'), budget]) === 'timeout') {
|
|
417
|
+
this.kill(child);
|
|
418
|
+
await Promise.race([exited, new Promise(resolve => setTimeout(resolve, this.timings.killExitGraceMs + 100))]);
|
|
419
|
+
}
|
|
420
|
+
if (timer)
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* SIGKILL the host. Its 'exit' normally follows and fails what was sent to it; if it does not arrive within the grace
|
|
425
|
+
* (a zombie the OS will not reap, an exit event lost), the child is treated as gone anyway so requests settle and a
|
|
426
|
+
* successor starts.
|
|
427
|
+
*/
|
|
428
|
+
kill(child) {
|
|
429
|
+
if (this.child !== child)
|
|
430
|
+
return;
|
|
431
|
+
this.ready = false;
|
|
432
|
+
this.hostUp = false;
|
|
433
|
+
this.retiring = true;
|
|
434
|
+
this.lastStatus = undefined;
|
|
435
|
+
this.activityMayHaveChanged();
|
|
436
|
+
try {
|
|
437
|
+
child.kill('SIGKILL');
|
|
438
|
+
}
|
|
439
|
+
catch { /* already gone */ }
|
|
440
|
+
const timer = setTimeout(() => { if (this.child === child)
|
|
441
|
+
this.onExit('killed; no exit event'); }, this.timings.killExitGraceMs);
|
|
442
|
+
timer.unref();
|
|
443
|
+
}
|
|
444
|
+
activityMayHaveChanged() {
|
|
445
|
+
const now = this.activity();
|
|
446
|
+
if (now === this.lastActivity)
|
|
447
|
+
return;
|
|
448
|
+
this.lastActivity = now;
|
|
449
|
+
try {
|
|
450
|
+
this.options.onActivityChanged?.();
|
|
451
|
+
}
|
|
452
|
+
catch { /* a listener failure must not break the host client */ }
|
|
453
|
+
}
|
|
454
|
+
log(line) { (this.options.log ?? console.error)(line); }
|
|
455
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class FileError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'FileError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Maps an fs errno (or anything else) to a coded error; a FileError passes through unchanged. */
|
|
10
|
+
export function toFileError(error) {
|
|
11
|
+
if (error instanceof FileError)
|
|
12
|
+
return error;
|
|
13
|
+
const code = error?.code;
|
|
14
|
+
if (code === 'ENOENT')
|
|
15
|
+
return new FileError('not_found', 'File or parent directory not found');
|
|
16
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
17
|
+
return new FileError('permission_denied', 'File access denied');
|
|
18
|
+
if (code === 'EEXIST')
|
|
19
|
+
return new FileError('conflict', 'Target already exists');
|
|
20
|
+
if (code === 'ELOOP')
|
|
21
|
+
return new FileError('invalid_path', 'Symbolic links are not allowed');
|
|
22
|
+
return new FileError('io_error', 'File operation failed');
|
|
23
|
+
}
|
|
24
|
+
export function cancelledError() { return new FileError('workstation_offline', 'File operation was cancelled'); }
|
|
25
|
+
export function throwIfAborted(signal) {
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
throw cancelledError();
|
|
28
|
+
}
|