@platless/axom-cli 0.3.13 → 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/dist/axom-patch/cli/README.md +30 -8
- 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-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- 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 +33 -6
- 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/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 +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- 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/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 +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- 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 +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- 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/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/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- 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/{fileOps.js → files/text.js} +117 -38
- 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/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// One keeper-acp session (spec §5.3): the adapter process it hosts, its raw stream, its outbox stream, checkpoints, the
|
|
2
|
+
// `down`-cause index, envelope diagnostics and stderr handling. Pure mechanism — no ACP beyond sniffing `id`/`method`.
|
|
3
|
+
//
|
|
4
|
+
// The same class runs in keeper-acp (raw and outbox are kit stream logs under `data/sessions/<id>/`, the adapter leads its own
|
|
5
|
+
// process group) and in the daemon's fallback (raw in memory, outbox a kit stream log under `<configDir>/agents/local/<key>/`,
|
|
6
|
+
// the adapter a plain daemon child): both transports therefore produce identical record sequences for the same adapter traffic.
|
|
7
|
+
// Keeper-side module: node builtins and kit siblings only (the import guard enforces it).
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { rmSync } from 'node:fs';
|
|
11
|
+
import { KeeperMethodError } from '../kit/protocol.js';
|
|
12
|
+
import { processStartTime, signalGroup } from '../kit/orphanSweep.js';
|
|
13
|
+
import { EnvelopeTracker } from './envelope.js';
|
|
14
|
+
import { ACP_EXIT_DRAIN_MS, ACP_KILL_GRACE_MS, ACP_MAX_RECORD_BYTES, ACP_READ_MAX_BYTES, ACP_READ_MAX_RECORDS, ACP_TERMINATE_GRACE_MS, AcpErrorCode, } from './protocol.js';
|
|
15
|
+
import { StderrCapture, jsonEscaped } from './stderr.js';
|
|
16
|
+
const MAX_EVENT_RECORDS = 256;
|
|
17
|
+
const GROUP_POLL_MS = 50;
|
|
18
|
+
export class SpawnFailedError extends KeeperMethodError {
|
|
19
|
+
constructor(message) { super(AcpErrorCode.spawnFailed, message); this.name = 'SpawnFailedError'; }
|
|
20
|
+
}
|
|
21
|
+
const messageOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
22
|
+
const causeKeyOf = (cause) => `${cause.o}:${cause.i}`;
|
|
23
|
+
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms).unref(); });
|
|
24
|
+
export class AcpSession {
|
|
25
|
+
options;
|
|
26
|
+
meta;
|
|
27
|
+
raw;
|
|
28
|
+
outbox;
|
|
29
|
+
proc;
|
|
30
|
+
lastExit = null;
|
|
31
|
+
downs = new Map();
|
|
32
|
+
downsComplete;
|
|
33
|
+
envelope = new EnvelopeTracker();
|
|
34
|
+
stderr;
|
|
35
|
+
lastSeq = null;
|
|
36
|
+
stdoutHeld = false;
|
|
37
|
+
released = false;
|
|
38
|
+
spawning = false;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.meta = options.meta;
|
|
42
|
+
this.raw = options.raw;
|
|
43
|
+
this.outbox = options.outbox;
|
|
44
|
+
this.downsComplete = options.freshRaw;
|
|
45
|
+
this.lastExit = options.meta.lastExit ?? null;
|
|
46
|
+
this.stderr = new StderrCapture({ file: options.stderrFile, onLine: (line) => this.appendRaw({ k: 'err', l: line }), onError: (error) => this.report(error) });
|
|
47
|
+
const head = this.outbox.state().head;
|
|
48
|
+
if (head >= this.outbox.state().first) {
|
|
49
|
+
const last = this.outbox.readFrom(head, 1).records[0];
|
|
50
|
+
if (last && Number.isSafeInteger(last.seq))
|
|
51
|
+
this.lastSeq = last.seq;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
get sessionKey() { return this.meta.sessionKey; }
|
|
55
|
+
get running() { return !!this.proc && this.proc.state !== 'exited'; }
|
|
56
|
+
get pendingInbound() { return this.envelope.pendingInboundCount; }
|
|
57
|
+
report(error) { this.options.log(`session ${this.meta.sessionKey}: ${messageOf(error)}`); }
|
|
58
|
+
appendRaw(input) { return this.raw.append(input); }
|
|
59
|
+
// ---- spawn ---------------------------------------------------------------------------------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* Starts the adapter: `spawn(command, args, {cwd, env, detached})` with exactly the daemon's env (never this process's own).
|
|
62
|
+
* Either a `proc spawn` record is appended (and later exactly one `proc exit`), or the spawn fails with nothing appended.
|
|
63
|
+
* stdout/stderr are held until the spawn record is in raw, so it is always the process's first record.
|
|
64
|
+
*/
|
|
65
|
+
async spawn(spec) {
|
|
66
|
+
if (this.released)
|
|
67
|
+
throw new KeeperMethodError(AcpErrorCode.notFound, 'Session released');
|
|
68
|
+
if (this.spawning || (this.proc && this.proc.state !== 'exited'))
|
|
69
|
+
throw new KeeperMethodError(AcpErrorCode.busy, 'A process is already running in this session');
|
|
70
|
+
this.spawning = true;
|
|
71
|
+
try {
|
|
72
|
+
return await this.spawnNow(spec);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.spawning = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
get busy() { return this.spawning || this.running; }
|
|
79
|
+
async spawnNow(spec) {
|
|
80
|
+
const spawnId = randomUUID();
|
|
81
|
+
let child;
|
|
82
|
+
try {
|
|
83
|
+
child = (this.options.spawnProcess ?? spawn)(spec.command, spec.args, { cwd: spec.cwd, env: spec.env, detached: this.options.detached, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new SpawnFailedError(messageOf(error));
|
|
87
|
+
}
|
|
88
|
+
child.stdin.on('error', () => { }); // EPIPE from a dying adapter: its exit is reported by the exit record
|
|
89
|
+
if (child.pid === undefined) {
|
|
90
|
+
const error = await new Promise((resolve) => child.once('error', resolve));
|
|
91
|
+
throw new SpawnFailedError(messageOf(error));
|
|
92
|
+
}
|
|
93
|
+
child.on('error', (error) => this.report(error));
|
|
94
|
+
const pid = child.pid;
|
|
95
|
+
const pgid = this.options.detached ? pid : 0;
|
|
96
|
+
let markRecorded;
|
|
97
|
+
const proc = {
|
|
98
|
+
spawnId, child, pid, pgid, startTime: null, sinceMs: this.options.now(), state: 'starting',
|
|
99
|
+
recorded: new Promise((resolve) => { markRecorded = resolve; }),
|
|
100
|
+
stdout: { chunks: [], bytes: 0, dropping: 0 },
|
|
101
|
+
};
|
|
102
|
+
this.proc = proc;
|
|
103
|
+
this.envelope.reset();
|
|
104
|
+
this.stderr.restart();
|
|
105
|
+
this.stderr.addSecrets(spec.stderrSecrets ?? []);
|
|
106
|
+
this.meta.spawn = { spawnId, pid, pgid, startTime: null, atMs: proc.sinceMs };
|
|
107
|
+
this.meta.exited = false;
|
|
108
|
+
this.meta.removeOnExit = [...(spec.removeOnExit ?? [])];
|
|
109
|
+
this.saveMeta(); // the pid (and what to remove) is durable before anything else happens
|
|
110
|
+
// Listeners right away (Node resumes stdio on the leader's exit; data without a listener would be lost); chunks that arrive
|
|
111
|
+
// before the spawn record is written are held and the stream paused.
|
|
112
|
+
let held = [];
|
|
113
|
+
child.stdout.on('data', (chunk) => { if (held) {
|
|
114
|
+
held.push(['out', chunk]);
|
|
115
|
+
child.stdout.pause();
|
|
116
|
+
return;
|
|
117
|
+
} this.onStdout(proc, chunk); });
|
|
118
|
+
child.stderr.on('data', (chunk) => { if (held) {
|
|
119
|
+
held.push(['err', chunk]);
|
|
120
|
+
child.stderr.pause();
|
|
121
|
+
return;
|
|
122
|
+
} this.stderr.push(chunk); });
|
|
123
|
+
child.stdout.on('error', () => { });
|
|
124
|
+
child.stderr.on('error', () => { });
|
|
125
|
+
child.stdout.pause();
|
|
126
|
+
child.stderr.pause();
|
|
127
|
+
child.once('exit', (code, signal) => this.onLeaderExit(proc, code, signal));
|
|
128
|
+
child.once('close', () => { void this.finish(proc); });
|
|
129
|
+
let record;
|
|
130
|
+
try {
|
|
131
|
+
proc.startTime = await (this.options.startTime ?? ((p) => processStartTime(p)))(pid).catch(() => null);
|
|
132
|
+
this.meta.spawn.startTime = proc.startTime;
|
|
133
|
+
this.saveMeta();
|
|
134
|
+
record = this.appendRaw({
|
|
135
|
+
k: 'proc', kind: 'spawn', spawnId, pid, pgid, startTime: proc.startTime ?? '',
|
|
136
|
+
command: spec.command, args: spec.args, cwd: spec.cwd, envKeys: Object.keys(spec.env).sort(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// The spawn record could not be written: the process must not run unrecorded.
|
|
141
|
+
this.signal(proc, 'SIGKILL');
|
|
142
|
+
held = undefined;
|
|
143
|
+
proc.state = 'exited';
|
|
144
|
+
this.proc = undefined;
|
|
145
|
+
this.meta.spawn = null;
|
|
146
|
+
this.meta.exited = true;
|
|
147
|
+
this.saveMeta();
|
|
148
|
+
markRecorded();
|
|
149
|
+
throw new SpawnFailedError(`Could not record the spawn: ${messageOf(error)}`);
|
|
150
|
+
}
|
|
151
|
+
if (proc.state === 'starting')
|
|
152
|
+
proc.state = 'running';
|
|
153
|
+
markRecorded();
|
|
154
|
+
const pending = held;
|
|
155
|
+
held = undefined;
|
|
156
|
+
for (const [stream, chunk] of pending) {
|
|
157
|
+
if (stream === 'out')
|
|
158
|
+
this.onStdout(proc, chunk);
|
|
159
|
+
else
|
|
160
|
+
this.stderr.push(chunk);
|
|
161
|
+
}
|
|
162
|
+
if (!this.stdoutHeld)
|
|
163
|
+
child.stdout.resume();
|
|
164
|
+
child.stderr.resume();
|
|
165
|
+
return { spawnId, pid, pgid, o: record.o };
|
|
166
|
+
}
|
|
167
|
+
saveMeta() {
|
|
168
|
+
try {
|
|
169
|
+
this.options.saveMeta(this.meta);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.report(error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Persists the metadata now (an adopting keeper records itself as the owner). */
|
|
176
|
+
persist() { this.saveMeta(); }
|
|
177
|
+
/** stdout → one `up` record per line, verbatim (no newline). A line over the record cap is dropped and a `gap` says so. */
|
|
178
|
+
onStdout(proc, chunk) {
|
|
179
|
+
const out = proc.stdout;
|
|
180
|
+
let start = 0;
|
|
181
|
+
for (;;) {
|
|
182
|
+
const newline = chunk.indexOf(0x0a, start);
|
|
183
|
+
if (newline < 0)
|
|
184
|
+
break;
|
|
185
|
+
const piece = chunk.subarray(start, newline);
|
|
186
|
+
start = newline + 1;
|
|
187
|
+
if (out.dropping > 0) {
|
|
188
|
+
this.appendGap(out.dropping + piece.length);
|
|
189
|
+
out.dropping = 0;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const line = out.chunks.length === 0 ? piece : Buffer.concat([...out.chunks, piece], out.bytes + piece.length);
|
|
193
|
+
out.chunks = [];
|
|
194
|
+
out.bytes = 0;
|
|
195
|
+
this.upLine(line);
|
|
196
|
+
}
|
|
197
|
+
if (start >= chunk.length)
|
|
198
|
+
return;
|
|
199
|
+
const rest = chunk.subarray(start);
|
|
200
|
+
if (out.dropping > 0) {
|
|
201
|
+
out.dropping += rest.length;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
out.chunks.push(Buffer.from(rest));
|
|
205
|
+
out.bytes += rest.length;
|
|
206
|
+
if (out.bytes > ACP_MAX_RECORD_BYTES) {
|
|
207
|
+
out.dropping = out.bytes;
|
|
208
|
+
out.chunks = [];
|
|
209
|
+
out.bytes = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
upLine(bytes) {
|
|
213
|
+
const line = bytes.toString('utf8');
|
|
214
|
+
if (bytes.length > ACP_MAX_RECORD_BYTES / 2 && Buffer.byteLength(JSON.stringify(line)) > ACP_MAX_RECORD_BYTES - 1024) {
|
|
215
|
+
this.appendGap(bytes.length);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const record = this.appendRaw({ k: 'up', l: line });
|
|
220
|
+
this.envelope.up(line, record.t);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.report(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
appendGap(droppedBytes) {
|
|
227
|
+
try {
|
|
228
|
+
this.appendRaw({ k: 'gap', stream: 'stdout', droppedBytes });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.report(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** The keeper's disk guard (spec §3.4: below 1 GiB free, stop reading adapter stdout). stderr keeps flowing (bounded file). */
|
|
235
|
+
holdStdout(hold) {
|
|
236
|
+
if (this.stdoutHeld === hold)
|
|
237
|
+
return;
|
|
238
|
+
this.stdoutHeld = hold;
|
|
239
|
+
const proc = this.proc;
|
|
240
|
+
if (!proc || proc.state === 'starting' || proc.state === 'exited')
|
|
241
|
+
return;
|
|
242
|
+
if (hold)
|
|
243
|
+
proc.child.stdout.pause();
|
|
244
|
+
else
|
|
245
|
+
proc.child.stdout.resume();
|
|
246
|
+
}
|
|
247
|
+
// ---- exit ----------------------------------------------------------------------------------------------------------------
|
|
248
|
+
onLeaderExit(proc, code, signal) {
|
|
249
|
+
proc.leaderExit = { code, signal };
|
|
250
|
+
if (proc.state !== 'exited')
|
|
251
|
+
proc.state = 'exiting';
|
|
252
|
+
if (this.stdoutHeld)
|
|
253
|
+
proc.child.stdout.resume(); // nothing more can be produced; let it drain
|
|
254
|
+
if (proc.terminateReason)
|
|
255
|
+
return; // terminate() drives the rest
|
|
256
|
+
// The adapter ended by itself: whatever is left of its group (an MCP server, a CLI it started) goes too, and stdio is drained
|
|
257
|
+
// for a bounded time (a survivor may hold the pipes open).
|
|
258
|
+
if (this.options.detached && this.groupAlive(proc)) {
|
|
259
|
+
signalGroup(proc.pgid, 'SIGTERM');
|
|
260
|
+
proc.groupTimer = setTimeout(() => { if (this.groupAlive(proc))
|
|
261
|
+
signalGroup(proc.pgid, 'SIGKILL'); }, ACP_KILL_GRACE_MS);
|
|
262
|
+
proc.groupTimer.unref();
|
|
263
|
+
}
|
|
264
|
+
proc.drainTimer = setTimeout(() => { this.closePipes(proc); void this.finish(proc); }, ACP_EXIT_DRAIN_MS);
|
|
265
|
+
proc.drainTimer.unref();
|
|
266
|
+
}
|
|
267
|
+
/** Writes the exit record once: flushes partial lines, appends `proc exit`, runs `removeOnExit`. */
|
|
268
|
+
finish(proc) {
|
|
269
|
+
proc.finished ??= (async () => {
|
|
270
|
+
await proc.recorded;
|
|
271
|
+
clearTimeout(proc.drainTimer);
|
|
272
|
+
if (this.proc !== proc)
|
|
273
|
+
return this.lastExit; // the spawn record could not be written: nothing to close
|
|
274
|
+
if (proc.stdout.dropping > 0) {
|
|
275
|
+
this.appendGap(proc.stdout.dropping);
|
|
276
|
+
proc.stdout.dropping = 0;
|
|
277
|
+
}
|
|
278
|
+
if (proc.stdout.bytes > 0) {
|
|
279
|
+
const line = Buffer.concat(proc.stdout.chunks, proc.stdout.bytes);
|
|
280
|
+
proc.stdout.chunks = [];
|
|
281
|
+
proc.stdout.bytes = 0;
|
|
282
|
+
this.upLine(line);
|
|
283
|
+
}
|
|
284
|
+
this.stderr.end();
|
|
285
|
+
const code = proc.leaderExit?.code ?? proc.child.exitCode ?? null;
|
|
286
|
+
const signal = proc.leaderExit?.signal ?? proc.child.signalCode ?? null;
|
|
287
|
+
const stderrTail = this.stderr.tail;
|
|
288
|
+
let exit = null;
|
|
289
|
+
try {
|
|
290
|
+
const record = this.appendRaw({ k: 'proc', kind: 'exit', code, signal, stderrTail, ...(proc.terminateReason ? { reason: proc.terminateReason } : {}) });
|
|
291
|
+
exit = { code, signal, stderrTail, ...(proc.terminateReason ? { reason: proc.terminateReason } : {}), o: record.o, atMs: record.t };
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
this.report(error);
|
|
295
|
+
}
|
|
296
|
+
proc.state = 'exited';
|
|
297
|
+
this.lastExit = exit;
|
|
298
|
+
this.meta.exited = true;
|
|
299
|
+
this.meta.lastExit = exit;
|
|
300
|
+
this.saveMeta();
|
|
301
|
+
this.removeOnExit();
|
|
302
|
+
return exit;
|
|
303
|
+
})();
|
|
304
|
+
return proc.finished;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Removes `targets` (default: what the current spawn named) and drops them from the metadata. A caller cleaning up after an
|
|
308
|
+
* EARLIER spawn passes the list it captured then: a later spawn's own targets are never touched (final review minor 8).
|
|
309
|
+
*/
|
|
310
|
+
removeOnExit(targets = this.meta.removeOnExit) {
|
|
311
|
+
const removing = [...targets];
|
|
312
|
+
for (const target of removing) {
|
|
313
|
+
try {
|
|
314
|
+
rmSync(target, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
this.report(error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const remaining = this.meta.removeOnExit.filter((target) => !removing.includes(target));
|
|
321
|
+
if (remaining.length !== this.meta.removeOnExit.length) {
|
|
322
|
+
this.meta.removeOnExit = remaining;
|
|
323
|
+
this.saveMeta();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
groupAlive(proc) {
|
|
327
|
+
if (!this.options.detached)
|
|
328
|
+
return proc.child.exitCode === null && proc.child.signalCode === null;
|
|
329
|
+
try {
|
|
330
|
+
process.kill(-proc.pgid, 0);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return false;
|
|
335
|
+
} // ESRCH: no member left (EPERM: the id is no longer ours)
|
|
336
|
+
}
|
|
337
|
+
signal(proc, signal) {
|
|
338
|
+
if (this.options.detached) {
|
|
339
|
+
signalGroup(proc.pgid, signal);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
proc.child.kill(signal);
|
|
344
|
+
}
|
|
345
|
+
catch { /* gone */ }
|
|
346
|
+
}
|
|
347
|
+
closePipes(proc) {
|
|
348
|
+
proc.child.stdin.destroy();
|
|
349
|
+
proc.child.stdout.destroy();
|
|
350
|
+
proc.child.stderr.destroy();
|
|
351
|
+
}
|
|
352
|
+
/** Until the group (fallback: the child) is gone or `ms` passed. True when it is gone. */
|
|
353
|
+
async goneWithin(proc, ms) {
|
|
354
|
+
const end = Date.now() + ms;
|
|
355
|
+
for (;;) {
|
|
356
|
+
const leaderDone = proc.leaderExit !== undefined;
|
|
357
|
+
if (leaderDone && !this.groupAlive(proc))
|
|
358
|
+
return true;
|
|
359
|
+
if (!this.options.detached && leaderDone)
|
|
360
|
+
return true;
|
|
361
|
+
if (Date.now() >= end)
|
|
362
|
+
return false;
|
|
363
|
+
await sleep(Math.min(GROUP_POLL_MS, Math.max(1, end - Date.now())));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* SIGTERM to the process group → up to `graceMs` → SIGKILL → up to 2 s → close the pipes → `exit` record (spec §5.3). The group,
|
|
368
|
+
* not just the leader: an adapter's CLI and MCP servers must not survive it. Idempotent; an exited session answers its last exit.
|
|
369
|
+
*/
|
|
370
|
+
async terminate(graceMs = ACP_TERMINATE_GRACE_MS, reason = 'terminated') {
|
|
371
|
+
const proc = this.proc;
|
|
372
|
+
if (!proc)
|
|
373
|
+
return this.lastExit;
|
|
374
|
+
await proc.recorded;
|
|
375
|
+
if (this.proc !== proc || proc.state === 'exited')
|
|
376
|
+
return this.lastExit;
|
|
377
|
+
if (proc.terminating)
|
|
378
|
+
return proc.terminating;
|
|
379
|
+
proc.terminateReason ??= reason;
|
|
380
|
+
proc.terminating = (async () => {
|
|
381
|
+
clearTimeout(proc.drainTimer);
|
|
382
|
+
clearTimeout(proc.groupTimer);
|
|
383
|
+
if (graceMs > 0) {
|
|
384
|
+
this.signal(proc, 'SIGTERM');
|
|
385
|
+
if (await this.goneWithin(proc, graceMs)) {
|
|
386
|
+
await this.drainBriefly(proc);
|
|
387
|
+
this.closePipes(proc);
|
|
388
|
+
return this.finish(proc);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
this.signal(proc, 'SIGKILL');
|
|
392
|
+
await this.goneWithin(proc, ACP_KILL_GRACE_MS);
|
|
393
|
+
this.closePipes(proc);
|
|
394
|
+
return this.finish(proc);
|
|
395
|
+
})();
|
|
396
|
+
return proc.terminating;
|
|
397
|
+
}
|
|
398
|
+
/** After a clean exit, give stdio a moment to deliver what the adapter wrote last (it normally closes at once). */
|
|
399
|
+
async drainBriefly(proc) {
|
|
400
|
+
if (proc.finished)
|
|
401
|
+
return;
|
|
402
|
+
await Promise.race([new Promise((resolve) => proc.child.once('close', () => resolve())), sleep(250)]);
|
|
403
|
+
}
|
|
404
|
+
// ---- write / note / outbox / commit --------------------------------------------------------------------------------------
|
|
405
|
+
/**
|
|
406
|
+
* Atomic (one synchronous step, so no other request interleaves): refused unless `spawnId` is the process running now; a cause
|
|
407
|
+
* already in raw is a duplicate; otherwise secrets join the stderr redaction, the LOGGED line is appended as `down`, and the line
|
|
408
|
+
* with secrets substituted goes to stdin. Append before stdin: a line that reached the adapter always has its record.
|
|
409
|
+
*/
|
|
410
|
+
write(spawnId, line, annotation, redact) {
|
|
411
|
+
const proc = this.proc;
|
|
412
|
+
if (!proc || proc.state !== 'running' || proc.terminateReason)
|
|
413
|
+
return { status: 'refused', reason: 'not_running' };
|
|
414
|
+
if (proc.spawnId !== spawnId)
|
|
415
|
+
return { status: 'refused', reason: 'spawn_mismatch' };
|
|
416
|
+
const key = causeKeyOf(annotation.cause);
|
|
417
|
+
const existing = this.downs.get(key);
|
|
418
|
+
if (existing !== undefined)
|
|
419
|
+
return { status: 'duplicate', o: existing };
|
|
420
|
+
if (line.includes('\n'))
|
|
421
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, 'A down line may not contain a newline');
|
|
422
|
+
if (line.length > ACP_MAX_RECORD_BYTES / 4 && Buffer.byteLength(JSON.stringify(line)) > ACP_MAX_RECORD_BYTES - 64 * 1024)
|
|
423
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, 'Down line exceeds the record limit');
|
|
424
|
+
let stdinLine = line;
|
|
425
|
+
for (const { ref, value } of redact)
|
|
426
|
+
stdinLine = stdinLine.split(ref).join(jsonEscaped(value));
|
|
427
|
+
this.stderr.addSecrets(redact.map((r) => r.value));
|
|
428
|
+
const record = this.appendRaw({ k: 'down', l: line, a: annotation, ...(redact.length > 0 ? { redact: redact.map((r) => r.ref) } : {}) });
|
|
429
|
+
this.downs.set(key, record.o);
|
|
430
|
+
this.envelope.down(line, record.t);
|
|
431
|
+
proc.child.stdin.write(`${stdinLine}\n`);
|
|
432
|
+
return { status: 'written', o: record.o };
|
|
433
|
+
}
|
|
434
|
+
note(note) {
|
|
435
|
+
return { o: this.appendRaw({ k: 'note', n: note }).o };
|
|
436
|
+
}
|
|
437
|
+
outboxAppend(entries) {
|
|
438
|
+
let last = this.lastSeq;
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
if (last !== null && entry.seq <= last)
|
|
441
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, `Outbox seq ${entry.seq} does not follow ${last}`);
|
|
442
|
+
last = entry.seq;
|
|
443
|
+
}
|
|
444
|
+
let first = -1;
|
|
445
|
+
let lastO = -1;
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
const record = this.outbox.append({ k: 'frame', seq: entry.seq, src: entry.src, frame: entry.frame });
|
|
448
|
+
if (first < 0)
|
|
449
|
+
first = record.o;
|
|
450
|
+
lastO = record.o;
|
|
451
|
+
this.lastSeq = entry.seq;
|
|
452
|
+
}
|
|
453
|
+
return { first, last: lastO };
|
|
454
|
+
}
|
|
455
|
+
/** Checkpoint, then outbox truncation (with its watermark), then raw truncation — never past the latest checkpoint. */
|
|
456
|
+
commit(params) {
|
|
457
|
+
const rawState = this.raw.state();
|
|
458
|
+
if (params.checkpoint) {
|
|
459
|
+
if (params.checkpoint.offset > rawState.head)
|
|
460
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, 'Checkpoint offset is beyond the raw head');
|
|
461
|
+
this.raw.writeCheckpoint(params.checkpoint.offset, Buffer.from(params.checkpoint.blob, 'base64'));
|
|
462
|
+
}
|
|
463
|
+
if (params.outbox) {
|
|
464
|
+
if (params.outbox.through > this.outbox.state().head)
|
|
465
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, 'Outbox commit is beyond the outbox head');
|
|
466
|
+
this.outbox.commit(params.outbox.through, params.outbox.tail);
|
|
467
|
+
}
|
|
468
|
+
if (params.rawThrough !== undefined) {
|
|
469
|
+
const checkpointOffset = this.raw.latestCheckpoint()?.offset ?? -1;
|
|
470
|
+
if (params.rawThrough > checkpointOffset)
|
|
471
|
+
throw new KeeperMethodError(AcpErrorCode.badRequest, 'Raw may only be truncated through the latest checkpoint');
|
|
472
|
+
this.raw.commit(params.rawThrough);
|
|
473
|
+
for (const [key, o] of this.downs)
|
|
474
|
+
if (o <= params.rawThrough)
|
|
475
|
+
this.downs.delete(key);
|
|
476
|
+
}
|
|
477
|
+
return { raw: this.streamState('raw'), outbox: this.streamState('outbox'), checkpointOffset: this.raw.latestCheckpoint()?.offset ?? -1 };
|
|
478
|
+
}
|
|
479
|
+
// ---- read ------------------------------------------------------------------------------------------------------------------
|
|
480
|
+
read(stream, from, limit = ACP_READ_MAX_RECORDS) {
|
|
481
|
+
const log = stream === 'raw' ? this.raw : this.outbox;
|
|
482
|
+
const result = log.readFrom(from, Math.min(limit, ACP_READ_MAX_RECORDS), { maxBytes: ACP_READ_MAX_BYTES });
|
|
483
|
+
const state = log.state();
|
|
484
|
+
const next = result.records.length > 0 ? result.records[result.records.length - 1].o + 1 : Math.max(from, result.first);
|
|
485
|
+
return { records: result.records, first: result.first, head: state.head, next, truncated: result.truncated };
|
|
486
|
+
}
|
|
487
|
+
source(stream) {
|
|
488
|
+
const log = stream === 'raw' ? this.raw : this.outbox;
|
|
489
|
+
const inner = log.asEventSource({ maxBytes: ACP_READ_MAX_BYTES });
|
|
490
|
+
return { read: (from, limit, options) => inner.read(from, Math.min(limit, MAX_EVENT_RECORDS), options), watch: (notify) => inner.watch(notify) };
|
|
491
|
+
}
|
|
492
|
+
/** Is a `down` with this cause in raw (the in-process lookup of the daemon's fallback)? */
|
|
493
|
+
hasDown(cause) {
|
|
494
|
+
if (!this.downsComplete)
|
|
495
|
+
this.downsFrom(0);
|
|
496
|
+
return this.downs.has(causeKeyOf(cause));
|
|
497
|
+
}
|
|
498
|
+
/** Every retained `down` with a cause at or after `from` (scans raw once for a session this owner did not create). */
|
|
499
|
+
downsFrom(from) {
|
|
500
|
+
if (!this.downsComplete) {
|
|
501
|
+
let next = this.raw.state().first;
|
|
502
|
+
for (;;) {
|
|
503
|
+
const { records } = this.raw.readFrom(next, ACP_READ_MAX_RECORDS, { maxBytes: ACP_READ_MAX_BYTES });
|
|
504
|
+
if (records.length === 0)
|
|
505
|
+
break;
|
|
506
|
+
for (const record of records) {
|
|
507
|
+
const cause = record.k === 'down' ? record.a?.cause : undefined;
|
|
508
|
+
if (cause && Number.isSafeInteger(cause.o) && Number.isSafeInteger(cause.i))
|
|
509
|
+
this.downs.set(causeKeyOf(cause), record.o);
|
|
510
|
+
}
|
|
511
|
+
next = records[records.length - 1].o + 1;
|
|
512
|
+
}
|
|
513
|
+
this.downsComplete = true;
|
|
514
|
+
}
|
|
515
|
+
const downs = [...this.downs.entries()].filter(([, o]) => o >= from).sort((a, b) => a[1] - b[1])
|
|
516
|
+
.map(([key, o]) => { const [co, ci] = key.split(':').map(Number); return { o, cause: { o: co, i: ci } }; });
|
|
517
|
+
const state = this.raw.state();
|
|
518
|
+
return { downs, first: state.first, head: state.head };
|
|
519
|
+
}
|
|
520
|
+
// ---- state -----------------------------------------------------------------------------------------------------------------
|
|
521
|
+
streamState(stream) {
|
|
522
|
+
const s = (stream === 'raw' ? this.raw : this.outbox).state();
|
|
523
|
+
return { head: s.head, first: s.first, next: s.next, committed: s.committed, bytes: s.bytes, overQuota: s.overQuota };
|
|
524
|
+
}
|
|
525
|
+
get overQuota() { return this.raw.state().overQuota || this.outbox.state().overQuota; }
|
|
526
|
+
processInfo() {
|
|
527
|
+
const proc = this.proc;
|
|
528
|
+
if (proc && proc.state !== 'exited')
|
|
529
|
+
return { state: proc.state, spawnId: proc.spawnId, pid: proc.pid, pgid: proc.pgid, startTime: proc.startTime, sinceEpochMs: proc.sinceMs };
|
|
530
|
+
if (proc || this.meta.spawn)
|
|
531
|
+
return { state: 'exited', spawnId: proc?.spawnId ?? this.meta.spawn?.spawnId ?? null, exit: this.lastExit };
|
|
532
|
+
return { state: 'none' };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* A keeper loss (spec §5.3): the process this session's metadata names was not started by this keeper, so whatever it was is
|
|
536
|
+
* over for this session. `proc exit{reason:'keeper_lost'}` is appended with the stderr tail the file kept, and the session is exited.
|
|
537
|
+
*/
|
|
538
|
+
recordKeeperLost(stderrTail) {
|
|
539
|
+
let exit = null;
|
|
540
|
+
try {
|
|
541
|
+
const record = this.appendRaw({ k: 'proc', kind: 'exit', code: null, signal: null, stderrTail, reason: 'keeper_lost' });
|
|
542
|
+
exit = { code: null, signal: null, stderrTail, reason: 'keeper_lost', o: record.o, atMs: record.t };
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
this.report(error);
|
|
546
|
+
}
|
|
547
|
+
this.lastExit = exit;
|
|
548
|
+
this.meta.exited = true;
|
|
549
|
+
this.meta.lastExit = exit;
|
|
550
|
+
this.saveMeta();
|
|
551
|
+
return exit;
|
|
552
|
+
}
|
|
553
|
+
summary() {
|
|
554
|
+
const outbox = this.outbox.state();
|
|
555
|
+
return {
|
|
556
|
+
sessionKey: this.meta.sessionKey, label: this.meta.label, createdAtMs: this.meta.createdAtMs, process: this.processInfo(),
|
|
557
|
+
raw: this.streamState('raw'), outbox: { ...this.streamState('outbox'), committedTail: outbox.committedTail, lastSeq: this.lastSeq },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
attach() {
|
|
561
|
+
const summary = this.summary();
|
|
562
|
+
const raw = this.raw.state();
|
|
563
|
+
const checkpoint = this.raw.latestCheckpoint();
|
|
564
|
+
const cp = checkpoint ? { seq: checkpoint.seq, offset: checkpoint.offset, t: checkpoint.t, blob: checkpoint.blob.toString('base64') } : null;
|
|
565
|
+
return { ...summary, head: raw.head, committed: raw.committed, committedTail: this.outbox.state().committedTail, checkpoint: cp, envelope: this.envelope.snapshot() };
|
|
566
|
+
}
|
|
567
|
+
/** Closes the streams (the process must have exited). `remove` also deletes what the caller keeps on disk for it. */
|
|
568
|
+
close() {
|
|
569
|
+
this.released = true;
|
|
570
|
+
this.stderr.close();
|
|
571
|
+
try {
|
|
572
|
+
this.raw.close();
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
this.report(error);
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
this.outbox.close();
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
this.report(error);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|