@xioflow/kernel 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -15
- package/dist/capability/check.d.ts +32 -0
- package/dist/capability/check.d.ts.map +1 -0
- package/dist/capability/check.js +136 -0
- package/dist/capability/check.js.map +1 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.d.ts.map +1 -0
- package/dist/capability/index.js +3 -0
- package/dist/capability/index.js.map +1 -0
- package/dist/capability/path-utils.d.ts +11 -0
- package/dist/capability/path-utils.d.ts.map +1 -0
- package/dist/capability/path-utils.js +46 -0
- package/dist/capability/path-utils.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +94 -0
- package/dist/cli.js.map +1 -0
- package/dist/confinement/bubblewrap.d.ts +8 -0
- package/dist/confinement/bubblewrap.d.ts.map +1 -0
- package/dist/confinement/bubblewrap.js +45 -0
- package/dist/confinement/bubblewrap.js.map +1 -0
- package/dist/confinement/detector.d.ts +4 -0
- package/dist/confinement/detector.d.ts.map +1 -0
- package/dist/confinement/detector.js +35 -0
- package/dist/confinement/detector.js.map +1 -0
- package/dist/confinement/index.d.ts +5 -0
- package/dist/confinement/index.d.ts.map +1 -0
- package/dist/confinement/index.js +5 -0
- package/dist/confinement/index.js.map +1 -0
- package/dist/confinement/sandbox-exec.d.ts +8 -0
- package/dist/confinement/sandbox-exec.d.ts.map +1 -0
- package/dist/confinement/sandbox-exec.js +30 -0
- package/dist/confinement/sandbox-exec.js.map +1 -0
- package/dist/confinement/srt.d.ts +8 -0
- package/dist/confinement/srt.d.ts.map +1 -0
- package/dist/confinement/srt.js +41 -0
- package/dist/confinement/srt.js.map +1 -0
- package/dist/domain.d.ts +26 -2
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +192 -18
- package/dist/domain.js.map +1 -1
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +42 -61
- package/dist/driver/node-driver.js.map +1 -1
- package/dist/driver/reaper-driver.d.ts +62 -0
- package/dist/driver/reaper-driver.d.ts.map +1 -0
- package/dist/driver/reaper-driver.js +371 -0
- package/dist/driver/reaper-driver.js.map +1 -0
- package/dist/driver/spawn-support.d.ts +4 -0
- package/dist/driver/spawn-support.d.ts.map +1 -0
- package/dist/driver/spawn-support.js +31 -0
- package/dist/driver/spawn-support.js.map +1 -0
- package/dist/driver/types.d.ts +19 -2
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/fault/crashpoint.d.ts +8 -0
- package/dist/fault/crashpoint.d.ts.map +1 -0
- package/dist/fault/crashpoint.js +37 -0
- package/dist/fault/crashpoint.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +233 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +32 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +255 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/native/darwin-arm64/xioflow-reaper +0 -0
- package/dist/native/darwin-x64/xioflow-reaper +0 -0
- package/dist/native/linux-arm64/xioflow-reaper +0 -0
- package/dist/native/linux-x64/xioflow-reaper +0 -0
- package/dist/otel/otlp.d.ts +37 -0
- package/dist/otel/otlp.d.ts.map +1 -0
- package/dist/otel/otlp.js +224 -0
- package/dist/otel/otlp.js.map +1 -0
- package/dist/quick-run.d.ts.map +1 -1
- package/dist/quick-run.js +5 -12
- package/dist/quick-run.js.map +1 -1
- package/dist/recovery/engine.d.ts +7 -0
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +176 -4
- package/dist/recovery/engine.js.map +1 -1
- package/dist/snapshot/git-shadow.d.ts +57 -0
- package/dist/snapshot/git-shadow.d.ts.map +1 -0
- package/dist/snapshot/git-shadow.js +364 -0
- package/dist/snapshot/git-shadow.js.map +1 -0
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +31 -0
- package/dist/store/schema.js.map +1 -1
- package/dist/store/sqlite.d.ts +14 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +150 -58
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/admission.d.ts +26 -0
- package/dist/supervisor/admission.d.ts.map +1 -0
- package/dist/supervisor/admission.js +98 -0
- package/dist/supervisor/admission.js.map +1 -0
- package/dist/supervisor/drainer.d.ts +17 -0
- package/dist/supervisor/drainer.d.ts.map +1 -0
- package/dist/supervisor/drainer.js +200 -0
- package/dist/supervisor/drainer.js.map +1 -0
- package/dist/supervisor/finalize.d.ts +21 -0
- package/dist/supervisor/finalize.d.ts.map +1 -0
- package/dist/supervisor/finalize.js +66 -0
- package/dist/supervisor/finalize.js.map +1 -0
- package/dist/supervisor/fingerprint.d.ts +8 -0
- package/dist/supervisor/fingerprint.d.ts.map +1 -0
- package/dist/supervisor/fingerprint.js +54 -0
- package/dist/supervisor/fingerprint.js.map +1 -0
- package/dist/supervisor/process-monitor.d.ts +9 -0
- package/dist/supervisor/process-monitor.d.ts.map +1 -0
- package/dist/supervisor/process-monitor.js +230 -0
- package/dist/supervisor/process-monitor.js.map +1 -0
- package/dist/supervisor/process-output.d.ts +27 -0
- package/dist/supervisor/process-output.d.ts.map +1 -0
- package/dist/supervisor/process-output.js +102 -0
- package/dist/supervisor/process-output.js.map +1 -0
- package/dist/supervisor/process-run.d.ts +30 -0
- package/dist/supervisor/process-run.d.ts.map +1 -0
- package/dist/supervisor/process-run.js +191 -0
- package/dist/supervisor/process-run.js.map +1 -0
- package/dist/supervisor/replay.d.ts +14 -0
- package/dist/supervisor/replay.d.ts.map +1 -0
- package/dist/supervisor/replay.js +70 -0
- package/dist/supervisor/replay.js.map +1 -0
- package/dist/supervisor/rollback.d.ts +8 -0
- package/dist/supervisor/rollback.d.ts.map +1 -0
- package/dist/supervisor/rollback.js +153 -0
- package/dist/supervisor/rollback.js.map +1 -0
- package/dist/supervisor/service.d.ts +24 -0
- package/dist/supervisor/service.d.ts.map +1 -0
- package/dist/supervisor/service.js +588 -0
- package/dist/supervisor/service.js.map +1 -0
- package/dist/supervisor/snapshot-ops.d.ts +36 -0
- package/dist/supervisor/snapshot-ops.d.ts.map +1 -0
- package/dist/supervisor/snapshot-ops.js +243 -0
- package/dist/supervisor/snapshot-ops.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +62 -30
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +164 -842
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/supervisor/types.d.ts +64 -0
- package/dist/supervisor/types.d.ts.map +1 -0
- package/dist/supervisor/types.js +2 -0
- package/dist/supervisor/types.js.map +1 -0
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +619 -12
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +161 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/workspace/read-tracking.d.ts +23 -0
- package/dist/workspace/read-tracking.d.ts.map +1 -0
- package/dist/workspace/read-tracking.js +66 -0
- package/dist/workspace/read-tracking.js.map +1 -0
- package/dist/workspace/transactions.d.ts +89 -0
- package/dist/workspace/transactions.d.ts.map +1 -0
- package/dist/workspace/transactions.js +277 -0
- package/dist/workspace/transactions.js.map +1 -0
- package/package.json +8 -2
|
@@ -1,76 +1,34 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (cmd.envWhiteList) {
|
|
17
|
-
const keys = Object.keys(cmd.envWhiteList).sort();
|
|
18
|
-
envSorted = keys.map((k) => [k, cmd.envWhiteList[k]]);
|
|
19
|
-
}
|
|
20
|
-
const canonical = {
|
|
21
|
-
args: cmd.args || [],
|
|
22
|
-
cwd: cmd.cwd || '',
|
|
23
|
-
envWhiteList: envSorted,
|
|
24
|
-
execPath: cmd.execPath || '',
|
|
25
|
-
inheritEnv: cmd.inheritEnv ?? null,
|
|
26
|
-
requiredResources: [...(options.requiredResources || [])].sort(),
|
|
27
|
-
resourceBudget: options.resourceBudget
|
|
28
|
-
? {
|
|
29
|
-
enforcement: options.resourceBudget.enforcement ?? null,
|
|
30
|
-
maxCpuTimeMs: options.resourceBudget.maxCpuTimeMs ?? null,
|
|
31
|
-
maxMemoryBytes: options.resourceBudget.maxMemoryBytes ?? null,
|
|
32
|
-
maxOutputBytes: options.resourceBudget.maxOutputBytes ?? null,
|
|
33
|
-
maxPids: options.resourceBudget.maxPids ?? null,
|
|
34
|
-
}
|
|
35
|
-
: null,
|
|
36
|
-
stdinHash,
|
|
37
|
-
timeoutMs: options.timeoutMs ?? null,
|
|
38
|
-
};
|
|
39
|
-
const canonicalJson = JSON.stringify(canonical);
|
|
40
|
-
return crypto.createHash('sha256').update(canonicalJson).digest('hex');
|
|
41
|
-
}
|
|
42
|
-
function trimToValidUtf8(buf) {
|
|
43
|
-
const len = buf.length;
|
|
44
|
-
for (let i = 1; i <= Math.min(3, len); i++) {
|
|
45
|
-
const b = buf[len - i];
|
|
46
|
-
if ((b & 0xc0) === 0xc0) {
|
|
47
|
-
const needed = (b & 0xe0) === 0xc0 ? 2 : (b & 0xf0) === 0xe0 ? 3 : (b & 0xf8) === 0xf0 ? 4 : 1;
|
|
48
|
-
if (i < needed) {
|
|
49
|
-
return Buffer.from(buf.subarray(0, len - i));
|
|
50
|
-
}
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
else if ((b & 0x80) === 0) {
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return buf;
|
|
58
|
-
}
|
|
59
|
-
function trimStartToValidUtf8(buf) {
|
|
60
|
-
let start = 0;
|
|
61
|
-
while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
|
|
62
|
-
start++;
|
|
63
|
-
}
|
|
64
|
-
return Buffer.from(buf.subarray(start));
|
|
65
|
-
}
|
|
3
|
+
import { OperationNotActiveError, } from '../types.js';
|
|
4
|
+
import { NodePlatformDriver } from '../driver/node-driver.js';
|
|
5
|
+
import { GitShadowSnapshotDriver } from '../snapshot/git-shadow.js';
|
|
6
|
+
import { ServiceSupervisor } from './service.js';
|
|
7
|
+
import { admitProcessScope, assertHardBudgetSupported, assertRunAcceptsOperations, resolveConfinementDriver, } from './admission.js';
|
|
8
|
+
import { computeInputFingerprint } from './fingerprint.js';
|
|
9
|
+
import { finalizeOperation, indeterminateResult } from './finalize.js';
|
|
10
|
+
import { joinInFlight, replayRecorded } from './replay.js';
|
|
11
|
+
import { runProcess } from './process-run.js';
|
|
12
|
+
import * as snapshotOps from './snapshot-ops.js';
|
|
13
|
+
import { rollback } from './rollback.js';
|
|
14
|
+
import { WorkspaceTransactions, } from '../workspace/transactions.js';
|
|
15
|
+
export { normalizeResourceName, computeInputFingerprint } from './fingerprint.js';
|
|
66
16
|
export class ProcessSupervisor {
|
|
67
17
|
domain;
|
|
68
18
|
driver;
|
|
69
19
|
activeOperations = new Map();
|
|
70
|
-
|
|
20
|
+
serviceSupervisor;
|
|
21
|
+
snapshotDriver;
|
|
22
|
+
confinementDriver;
|
|
23
|
+
transactions;
|
|
24
|
+
constructor(domain, driver = domain.getDriver?.() || new NodePlatformDriver(), snapshotDriver, confinementDriver) {
|
|
71
25
|
this.domain = domain;
|
|
72
26
|
this.driver = driver;
|
|
73
27
|
this.domain.setDriver?.(driver);
|
|
28
|
+
this.serviceSupervisor = new ServiceSupervisor(domain, driver, this);
|
|
29
|
+
this.snapshotDriver = snapshotDriver || new GitShadowSnapshotDriver();
|
|
30
|
+
this.confinementDriver = confinementDriver;
|
|
31
|
+
this.domain.setSnapshotDriver?.(this.snapshotDriver);
|
|
74
32
|
}
|
|
75
33
|
getDomain() {
|
|
76
34
|
return this.domain;
|
|
@@ -78,116 +36,120 @@ export class ProcessSupervisor {
|
|
|
78
36
|
getDriver() {
|
|
79
37
|
return this.driver;
|
|
80
38
|
}
|
|
39
|
+
getSnapshotDriver() {
|
|
40
|
+
return this.snapshotDriver;
|
|
41
|
+
}
|
|
42
|
+
setConfinementDriver(driver) {
|
|
43
|
+
this.confinementDriver = driver;
|
|
44
|
+
}
|
|
45
|
+
getConfinementDriver() {
|
|
46
|
+
return this.confinementDriver;
|
|
47
|
+
}
|
|
48
|
+
async startService(spec) {
|
|
49
|
+
return this.serviceSupervisor.startService(spec);
|
|
50
|
+
}
|
|
51
|
+
get snapshotContext() {
|
|
52
|
+
return { domain: this.domain, snapshotDriver: this.snapshotDriver };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 快照捕获协议 (ARCHITECTURE §3.5 / 契约 #32, #53)
|
|
56
|
+
*/
|
|
57
|
+
async captureSnapshot(options) {
|
|
58
|
+
return snapshotOps.captureSnapshot(this.snapshotContext, options);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 回滚协议 (ARCHITECTURE §3.5 / 契约 #28–#30, #56)
|
|
62
|
+
*/
|
|
63
|
+
async rollback(options) {
|
|
64
|
+
return rollback(this.snapshotContext, options);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 分叉工作区 Materialize (ARCHITECTURE §3.5 / 契约 #54)
|
|
68
|
+
*/
|
|
69
|
+
async materialize(snapshotId, newRoot) {
|
|
70
|
+
return snapshotOps.materialize(this.snapshotContext, snapshotId, newRoot);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 回收分叉工作区 Dematerialize (ARCHITECTURE §3.5 / 契约 #54)
|
|
74
|
+
*/
|
|
75
|
+
async dematerialize(newRoot, options) {
|
|
76
|
+
return snapshotOps.dematerialize(this.snapshotContext, newRoot, options);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 回收快照:删除驱动侧的私有 ref 与 store 记录,并写 SNAPSHOT_PRUNED。
|
|
80
|
+
* 宿主按自己的保留策略调用(例如每轮只保留会话基线与当前轮检查点),否则快照 ref 会无限累积。
|
|
81
|
+
* 未知 id 视为已回收(幂等);驱动删除失败直接抛出,store 记录保留,便于重试。
|
|
82
|
+
*/
|
|
83
|
+
async pruneSnapshots(snapshotIds, options) {
|
|
84
|
+
return snapshotOps.pruneSnapshots(this.snapshotContext, snapshotIds, options);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 工作区事务(ARCHITECTURE §3.9):在主工作区的基线快照上 materialize 出独立 fork,
|
|
88
|
+
* agent 以 `forkRoot` 为 cwd 工作;commit 时按读写集做乐观并发校验,无冲突才把写集应用回主工作区。
|
|
89
|
+
*/
|
|
90
|
+
async beginWorkspaceTransaction(options) {
|
|
91
|
+
return this.workspaceTransactions().begin(options);
|
|
92
|
+
}
|
|
93
|
+
async inspectWorkspaceTransaction(txId) {
|
|
94
|
+
return this.workspaceTransactions().inspect(txId);
|
|
95
|
+
}
|
|
96
|
+
async commitWorkspaceTransaction(txId) {
|
|
97
|
+
return this.workspaceTransactions().commit(txId);
|
|
98
|
+
}
|
|
99
|
+
async abortWorkspaceTransaction(txId, reason) {
|
|
100
|
+
return this.workspaceTransactions().abort(txId, reason);
|
|
101
|
+
}
|
|
102
|
+
workspaceTransactions() {
|
|
103
|
+
if (this.transactions)
|
|
104
|
+
return this.transactions;
|
|
105
|
+
const snapshotDriver = this.snapshotDriver;
|
|
106
|
+
if (!(snapshotDriver instanceof GitShadowSnapshotDriver)) {
|
|
107
|
+
throw new Error(`Workspace transactions need the git-shadow snapshot driver (got ${snapshotDriver.name})`);
|
|
108
|
+
}
|
|
109
|
+
this.transactions = new WorkspaceTransactions({
|
|
110
|
+
domain: this.domain,
|
|
111
|
+
snapshotDriver,
|
|
112
|
+
captureSnapshot: async (runId, opId, root) => {
|
|
113
|
+
const res = await this.captureSnapshot({ runId, opId, roots: [root] });
|
|
114
|
+
if (res.status !== 'succeeded' || !res.snapshot) {
|
|
115
|
+
throw new Error(`Base snapshot for ${opId} failed: ${res.errorMessage ?? res.status}`);
|
|
116
|
+
}
|
|
117
|
+
return res.snapshot;
|
|
118
|
+
},
|
|
119
|
+
materialize: async (snapshotId, forkPath) => {
|
|
120
|
+
await this.materialize(snapshotId, forkPath);
|
|
121
|
+
// 租约移交:materialize 以独占租约保留 fork(§3.5);事务接管 fork 后释放它,
|
|
122
|
+
// fork 内 agent 操作之间的互斥由各自声明的 workspace:write:<fork> 保证。
|
|
123
|
+
for (const res of new Set([`workspace:write:${path.resolve(forkPath)}`, `workspace:write:${fs.realpathSync(forkPath)}`])) {
|
|
124
|
+
const owner = this.domain.getResourceOwner(res);
|
|
125
|
+
if (owner?.startsWith('mat-'))
|
|
126
|
+
this.domain.internalReleaseResources(owner, [res]);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
dematerialize: (forkPath) => this.dematerialize(forkPath, { force: true }),
|
|
130
|
+
});
|
|
131
|
+
return this.transactions;
|
|
132
|
+
}
|
|
81
133
|
/**
|
|
82
134
|
* 启动协议:准入检查 -> 先持久化意图 -> 请求驱动启动 -> 登记身份并转 active -> 监督运行 -> 记录结果
|
|
83
135
|
*/
|
|
84
136
|
async executeProcess(options) {
|
|
137
|
+
// 0. 准入:capability 范围、写入限制、hard 预算能力、调用 Run 状态
|
|
138
|
+
const { mutationRoots, capAdmission } = admitProcessScope(this.domain, options);
|
|
139
|
+
const confinementDriver = resolveConfinementDriver(options, this.confinementDriver);
|
|
140
|
+
const command = confinementDriver ? confinementDriver.wrap(options.command, mutationRoots || []) : options.command;
|
|
85
141
|
const startTime = Date.now();
|
|
86
|
-
|
|
87
|
-
const drainTimeoutMs = options.drainTimeoutMs ?? 2000; // 默认 2000ms
|
|
88
|
-
let stdoutDrainer;
|
|
89
|
-
let stderrDrainer;
|
|
90
|
-
// 0. [准入期强校验 Pre-admission Capability Check]
|
|
91
|
-
if (options.resourceBudget?.enforcement === 'hard') {
|
|
92
|
-
if (options.resourceBudget.maxMemoryBytes && !this.driver.capabilities.memoryHardLimit) {
|
|
93
|
-
throw new UnsupportedCapabilityError('memoryHardLimit', this.driver.name, 'hard');
|
|
94
|
-
}
|
|
95
|
-
if (options.resourceBudget.maxPids && !this.driver.capabilities.pidsLimit) {
|
|
96
|
-
throw new UnsupportedCapabilityError('pidsLimit', this.driver.name, 'hard');
|
|
97
|
-
}
|
|
98
|
-
if (options.resourceBudget.maxCpuTimeMs && !this.driver.capabilities.cpuLimit) {
|
|
99
|
-
throw new UnsupportedCapabilityError('cpuLimit', this.driver.name, 'hard');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
142
|
+
assertHardBudgetSupported(this.driver, options.resourceBudget);
|
|
102
143
|
const inputFingerprint = options.inputFingerprint || computeInputFingerprint(options);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (callingRun.status === 'succeeded' ||
|
|
110
|
-
callingRun.status === 'failed' ||
|
|
111
|
-
callingRun.status === 'cancelled' ||
|
|
112
|
-
callingRun.status === 'indeterminate') {
|
|
113
|
-
throw new Error(`Cannot register operation "${options.opId}" for Run "${options.runId}" because the Run is already finalized with status "${callingRun.status}".`);
|
|
144
|
+
assertRunAcceptsOperations(this.domain, options.runId, options.opId);
|
|
145
|
+
// 1–2. 幂等重放(ARCHITECTURE §3.7):在飞加入,已持久化原样返回
|
|
146
|
+
const inFlight = this.activeOperations.get(options.opId);
|
|
147
|
+
if (inFlight) {
|
|
148
|
+
return joinInFlight(this.domain, inFlight, options, inputFingerprint);
|
|
114
149
|
}
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
if (inFlight.inputFingerprint !== inputFingerprint) {
|
|
119
|
-
throw new OperationIdConflictError(options.opId, inFlight.inputFingerprint, inputFingerprint, inFlight.phase, inFlight.runId);
|
|
120
|
-
}
|
|
121
|
-
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'joined', options.runId);
|
|
122
|
-
if (options.onStreamChunk) {
|
|
123
|
-
inFlight.streamSubscribers.add(options.onStreamChunk);
|
|
124
|
-
}
|
|
125
|
-
if (options.abortSignal) {
|
|
126
|
-
const signal = options.abortSignal;
|
|
127
|
-
if (signal.aborted) {
|
|
128
|
-
if (options.onStreamChunk) {
|
|
129
|
-
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
130
|
-
}
|
|
131
|
-
throw signal.reason || new Error('Operation wait aborted');
|
|
132
|
-
}
|
|
133
|
-
return await new Promise((resolve, reject) => {
|
|
134
|
-
const onAbort = () => {
|
|
135
|
-
if (options.onStreamChunk) {
|
|
136
|
-
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
137
|
-
}
|
|
138
|
-
reject(signal.reason || new Error('Operation wait aborted'));
|
|
139
|
-
};
|
|
140
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
141
|
-
inFlight.resultPromise.then((res) => {
|
|
142
|
-
signal.removeEventListener('abort', onAbort);
|
|
143
|
-
if (options.onStreamChunk) {
|
|
144
|
-
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
145
|
-
}
|
|
146
|
-
resolve({ ...res, replayed: true });
|
|
147
|
-
}, (err) => {
|
|
148
|
-
signal.removeEventListener('abort', onAbort);
|
|
149
|
-
if (options.onStreamChunk) {
|
|
150
|
-
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
151
|
-
}
|
|
152
|
-
reject(err);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
const res = await inFlight.resultPromise;
|
|
157
|
-
if (options.onStreamChunk) {
|
|
158
|
-
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
...res,
|
|
162
|
-
replayed: true,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// 2. 已持久化操作重放判定(ARCHITECTURE §3.7 / 契约 #45, #47, #48, #49)
|
|
166
|
-
const existingRecorded = this.domain.getStore().getOperation(options.opId);
|
|
167
|
-
if (existingRecorded) {
|
|
168
|
-
if (existingRecorded.inputFingerprint !== inputFingerprint) {
|
|
169
|
-
throw new OperationIdConflictError(options.opId, existingRecorded.inputFingerprint, inputFingerprint, existingRecorded.status, existingRecorded.runId);
|
|
170
|
-
}
|
|
171
|
-
// 未终结状态且不在内存中 → 必须先执行恢复(崩溃残留现场)
|
|
172
|
-
if (existingRecorded.status !== 'done') {
|
|
173
|
-
throw new RecoveryRequiredError(options.opId, existingRecorded.status, existingRecorded.runId);
|
|
174
|
-
}
|
|
175
|
-
// indeterminate 状态:绝不重跑,原样返回,保留租约
|
|
176
|
-
if (existingRecorded.result?.status === 'indeterminate') {
|
|
177
|
-
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'indeterminate', options.runId);
|
|
178
|
-
return {
|
|
179
|
-
...existingRecorded.result,
|
|
180
|
-
replayed: true,
|
|
181
|
-
runId: existingRecorded.runId,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
// 已结清终态 (succeeded / failed / cancelled):返回已持久化结果
|
|
185
|
-
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'recorded', options.runId);
|
|
186
|
-
return {
|
|
187
|
-
...existingRecorded.result,
|
|
188
|
-
replayed: true,
|
|
189
|
-
runId: existingRecorded.runId,
|
|
190
|
-
};
|
|
150
|
+
const replayed = replayRecorded(this.domain, options, inputFingerprint);
|
|
151
|
+
if (replayed) {
|
|
152
|
+
return replayed;
|
|
191
153
|
}
|
|
192
154
|
const op = {
|
|
193
155
|
id: options.opId,
|
|
@@ -195,11 +157,38 @@ export class ProcessSupervisor {
|
|
|
195
157
|
kind: 'process',
|
|
196
158
|
name: options.name,
|
|
197
159
|
inputFingerprint,
|
|
198
|
-
requiredResources: options.requiredResources,
|
|
160
|
+
requiredResources: options.requiredResources || [],
|
|
161
|
+
mutationRoots,
|
|
162
|
+
capabilityId: options.capabilityId,
|
|
199
163
|
timeoutMs: options.timeoutMs,
|
|
200
164
|
resourceBudget: options.resourceBudget,
|
|
201
165
|
status: 'pending',
|
|
202
166
|
};
|
|
167
|
+
const opState = this.trackOperation(options, inputFingerprint, startTime);
|
|
168
|
+
const executionPromise = runProcess(this.runContext, {
|
|
169
|
+
options,
|
|
170
|
+
op,
|
|
171
|
+
opState,
|
|
172
|
+
command,
|
|
173
|
+
confined: confinementDriver !== undefined,
|
|
174
|
+
confinementDriverName: confinementDriver?.name,
|
|
175
|
+
capAdmission,
|
|
176
|
+
});
|
|
177
|
+
opState.resultPromise = executionPromise;
|
|
178
|
+
return await executionPromise;
|
|
179
|
+
}
|
|
180
|
+
get runContext() {
|
|
181
|
+
return {
|
|
182
|
+
domain: this.domain,
|
|
183
|
+
driver: this.driver,
|
|
184
|
+
stop: (opId, reason, graceMs) => this.handleStopPipeline(opId, reason, graceMs),
|
|
185
|
+
forget: (opId) => {
|
|
186
|
+
this.activeOperations.delete(opId);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** 登记在飞状态;abortSignal 触发即走取消流水线(已中止则在启动前取消)。 */
|
|
191
|
+
trackOperation(options, inputFingerprint, startTime) {
|
|
203
192
|
const streamSubscribers = new Set();
|
|
204
193
|
if (options.onStreamChunk) {
|
|
205
194
|
streamSubscribers.add(options.onStreamChunk);
|
|
@@ -226,416 +215,7 @@ export class ProcessSupervisor {
|
|
|
226
215
|
options.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
227
216
|
}
|
|
228
217
|
}
|
|
229
|
-
|
|
230
|
-
let timeoutTimer = null;
|
|
231
|
-
let samplingInterval = null;
|
|
232
|
-
let stdoutDrainer;
|
|
233
|
-
let stderrDrainer;
|
|
234
|
-
try {
|
|
235
|
-
// 1. [资源分配与排队等待]
|
|
236
|
-
await this.domain.allocateResourcesWithWait(options.opId, options.requiredResources || [], options.waitTimeoutMs ?? 0, options.resourceBudget, () => opState.cancelRequested === true);
|
|
237
|
-
if (opState.cancelRequested) {
|
|
238
|
-
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
239
|
-
}
|
|
240
|
-
// 2. [启动协议步骤 1] 写入 SQLite (status: intent_registered)
|
|
241
|
-
try {
|
|
242
|
-
this.domain.getStore().registerOperationIntent(op, this.domain.domainId);
|
|
243
|
-
opState.phase = 'intent_registered';
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
this.domain.internalReleaseResources(options.opId, options.requiredResources);
|
|
247
|
-
throw err;
|
|
248
|
-
}
|
|
249
|
-
if (opState.cancelRequested) {
|
|
250
|
-
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
251
|
-
}
|
|
252
|
-
let handle;
|
|
253
|
-
opState.phase = 'spawning';
|
|
254
|
-
try {
|
|
255
|
-
// 3. [启动协议步骤 2] 请求平台驱动启动
|
|
256
|
-
handle = await this.driver.spawn(options.command);
|
|
257
|
-
opState.handle = handle;
|
|
258
|
-
opState.command = options.command;
|
|
259
|
-
}
|
|
260
|
-
catch (spawnError) {
|
|
261
|
-
opState.phase = 'done';
|
|
262
|
-
opState.stopResolve?.({ stopped: 'confirmed_stopped', scope: 'direct_child', errorDetails: spawnError?.message });
|
|
263
|
-
opState.stopResolve = undefined;
|
|
264
|
-
// 启动失败(如可执行文件不存在),不产生假运行状态,直接失败收尾并释放资源
|
|
265
|
-
const failResult = {
|
|
266
|
-
kind: 'process',
|
|
267
|
-
status: 'failed',
|
|
268
|
-
exitCode: 127,
|
|
269
|
-
signal: null,
|
|
270
|
-
stdout: '',
|
|
271
|
-
stderr: spawnError?.message || String(spawnError),
|
|
272
|
-
spawnFailure: spawnError?.message || String(spawnError),
|
|
273
|
-
isTruncated: false,
|
|
274
|
-
identityVerification: 'not_original_process',
|
|
275
|
-
durationMs: Date.now() - startTime,
|
|
276
|
-
completedAt: new Date().toISOString(),
|
|
277
|
-
};
|
|
278
|
-
return this.finalizeOperation(options.opId, failResult, options.requiredResources, true);
|
|
279
|
-
}
|
|
280
|
-
// 4. [启动协议步骤 3] 登记执行身份,状态推进为 active
|
|
281
|
-
try {
|
|
282
|
-
this.domain.getStore().updateOperationStatus(options.opId, 'active', handle.identity);
|
|
283
|
-
opState.phase = 'active';
|
|
284
|
-
handle.releaseGate?.();
|
|
285
|
-
}
|
|
286
|
-
catch (statusError) {
|
|
287
|
-
handle.destroyGate?.();
|
|
288
|
-
// N4: spawn 成功但登记 active 失败,防止产生孤儿进程
|
|
289
|
-
opState.phase = 'stopping';
|
|
290
|
-
const stopRes = await this.driver.terminate(handle.identity, 2000);
|
|
291
|
-
// 有界等待退出与关闭流,防止管道悬挂
|
|
292
|
-
await Promise.race([
|
|
293
|
-
(handle.onRootExit ?? handle.onExit).catch(() => { }),
|
|
294
|
-
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
295
|
-
]);
|
|
296
|
-
try {
|
|
297
|
-
handle.stdout.destroy?.();
|
|
298
|
-
handle.stderr.destroy?.();
|
|
299
|
-
}
|
|
300
|
-
catch { }
|
|
301
|
-
opState.phase = 'done';
|
|
302
|
-
if (opState.stopResolve) {
|
|
303
|
-
const resolveFn = opState.stopResolve;
|
|
304
|
-
opState.stopResolve = undefined;
|
|
305
|
-
resolveFn(stopRes);
|
|
306
|
-
}
|
|
307
|
-
if (stopRes.stopped === 'confirmed_stopped') {
|
|
308
|
-
const failResult = {
|
|
309
|
-
kind: 'process',
|
|
310
|
-
status: 'failed',
|
|
311
|
-
exitCode: null,
|
|
312
|
-
signal: null,
|
|
313
|
-
stdout: '',
|
|
314
|
-
stderr: `Failed to register active status: ${statusError?.message || String(statusError)}`,
|
|
315
|
-
spawnFailure: statusError?.message || String(statusError),
|
|
316
|
-
isTruncated: false,
|
|
317
|
-
identityVerification: 'not_original_process',
|
|
318
|
-
durationMs: Date.now() - startTime,
|
|
319
|
-
completedAt: new Date().toISOString(),
|
|
320
|
-
};
|
|
321
|
-
this.finalizeOperation(options.opId, failResult, options.requiredResources, true);
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
const indetResult = {
|
|
325
|
-
kind: 'indeterminate',
|
|
326
|
-
status: 'indeterminate',
|
|
327
|
-
reason: `Failed to register active status: ${statusError?.message || String(statusError)}, and process ${handle.identity.pid} could not be confirmed stopped: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
328
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
329
|
-
durationMs: Date.now() - startTime,
|
|
330
|
-
completedAt: new Date().toISOString(),
|
|
331
|
-
};
|
|
332
|
-
this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
333
|
-
}
|
|
334
|
-
throw statusError;
|
|
335
|
-
}
|
|
336
|
-
// 准备 artifacts 溢出转储目录
|
|
337
|
-
const artifactsDir = options.artifactsDir || path.join(this.domain.domainPath, 'artifacts');
|
|
338
|
-
let artifactsDirError = undefined;
|
|
339
|
-
if (!fs.existsSync(artifactsDir)) {
|
|
340
|
-
try {
|
|
341
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
342
|
-
}
|
|
343
|
-
catch (dirErr) {
|
|
344
|
-
artifactsDirError = dirErr.message || String(dirErr);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const stdoutSpillPath = path.join(artifactsDir, `${options.opId}-stdout.log`);
|
|
348
|
-
const stderrSpillPath = path.join(artifactsDir, `${options.opId}-stderr.log`);
|
|
349
|
-
// 5. [正常监督、有界排空与流式转储 (Spill to Artifacts)]
|
|
350
|
-
let totalOutputBytes = 0;
|
|
351
|
-
const onChunk = (bytes) => {
|
|
352
|
-
totalOutputBytes += bytes;
|
|
353
|
-
if (options.resourceBudget?.enforcement === 'soft' &&
|
|
354
|
-
options.resourceBudget.maxOutputBytes &&
|
|
355
|
-
totalOutputBytes > options.resourceBudget.maxOutputBytes &&
|
|
356
|
-
!opState.terminationReason) {
|
|
357
|
-
opState.terminationReason = 'output_exceeded';
|
|
358
|
-
this.handleStopPipeline(options.opId, 'output_exceeded', 1000).catch(() => { });
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
let streamCallbackError;
|
|
362
|
-
const forwardChunk = (stream) => (chunk) => {
|
|
363
|
-
for (const subscriber of opState.streamSubscribers) {
|
|
364
|
-
try {
|
|
365
|
-
subscriber(stream, chunk);
|
|
366
|
-
}
|
|
367
|
-
catch (err) {
|
|
368
|
-
streamCallbackError ??= err instanceof Error ? err.message : String(err);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
stdoutDrainer = this.setupStreamDrainer(handle.stdout, maxBytes, stdoutSpillPath, onChunk, forwardChunk('stdout'), artifactsDirError);
|
|
373
|
-
stderrDrainer = this.setupStreamDrainer(handle.stderr, maxBytes, stderrSpillPath, onChunk, forwardChunk('stderr'), artifactsDirError);
|
|
374
|
-
// 检查在 spawn 途中是否已被请求取消:若已请求取消,立即触发停止流水线向底层发送终止信号
|
|
375
|
-
if (opState.cancelRequested) {
|
|
376
|
-
this.handleStopPipeline(options.opId, 'user_cancelled', opState.cancelGraceMs ?? 2000).catch(() => { });
|
|
377
|
-
}
|
|
378
|
-
const timeoutPromise = options.timeoutMs && options.timeoutMs > 0
|
|
379
|
-
? new Promise((resolve) => {
|
|
380
|
-
timeoutTimer = setTimeout(() => resolve('timeout'), options.timeoutMs);
|
|
381
|
-
})
|
|
382
|
-
: new Promise(() => { });
|
|
383
|
-
// Soft / Observe 模式资源采样治理
|
|
384
|
-
let peakMemoryBytes = 0;
|
|
385
|
-
let peakCpuTimeMs = 0;
|
|
386
|
-
if (this.driver.sampleMetrics &&
|
|
387
|
-
options.resourceBudget &&
|
|
388
|
-
(options.resourceBudget.enforcement === 'soft' || options.resourceBudget.enforcement === 'observe')) {
|
|
389
|
-
samplingInterval = setInterval(async () => {
|
|
390
|
-
try {
|
|
391
|
-
const metrics = await this.driver.sampleMetrics(handle.identity);
|
|
392
|
-
peakMemoryBytes = Math.max(peakMemoryBytes, metrics.rssBytes);
|
|
393
|
-
peakCpuTimeMs = Math.max(peakCpuTimeMs, metrics.cpuTimeMs);
|
|
394
|
-
if (options.resourceBudget?.enforcement === 'soft') {
|
|
395
|
-
if (options.resourceBudget.maxMemoryBytes && metrics.rssBytes > options.resourceBudget.maxMemoryBytes) {
|
|
396
|
-
opState.terminationReason = 'memory_exceeded';
|
|
397
|
-
if (samplingInterval)
|
|
398
|
-
clearInterval(samplingInterval);
|
|
399
|
-
await this.handleStopPipeline(options.opId, 'memory_exceeded', 1000);
|
|
400
|
-
}
|
|
401
|
-
else if (options.resourceBudget.maxCpuTimeMs && metrics.cpuTimeMs > options.resourceBudget.maxCpuTimeMs) {
|
|
402
|
-
opState.terminationReason = 'cpu_exceeded';
|
|
403
|
-
if (samplingInterval)
|
|
404
|
-
clearInterval(samplingInterval);
|
|
405
|
-
await this.handleStopPipeline(options.opId, 'cpu_exceeded', 1000);
|
|
406
|
-
}
|
|
407
|
-
else if (options.resourceBudget.maxPids && metrics.pidsCount > options.resourceBudget.maxPids) {
|
|
408
|
-
opState.terminationReason = 'pids_exceeded';
|
|
409
|
-
if (samplingInterval)
|
|
410
|
-
clearInterval(samplingInterval);
|
|
411
|
-
await this.handleStopPipeline(options.opId, 'pids_exceeded', 1000);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
catch { }
|
|
416
|
-
}, 100);
|
|
417
|
-
}
|
|
418
|
-
let notifyStop;
|
|
419
|
-
const stopTriggerPromise = new Promise((resolve) => {
|
|
420
|
-
notifyStop = resolve;
|
|
421
|
-
});
|
|
422
|
-
const previousResolve = opState.stopResolve;
|
|
423
|
-
opState.stopResolve = (res) => {
|
|
424
|
-
previousResolve?.(res);
|
|
425
|
-
notifyStop?.(res);
|
|
426
|
-
};
|
|
427
|
-
// 等待根进程退出、超时触发、或外部停止完成
|
|
428
|
-
const rootExitPromise = handle.onRootExit ?? handle.onExit;
|
|
429
|
-
const stopCandidate = opState.stopPromise ? opState.stopPromise : stopTriggerPromise;
|
|
430
|
-
const exitOrTimeout = await Promise.race([
|
|
431
|
-
rootExitPromise.then((res) => ({ type: 'exit', res, stopRes: null })),
|
|
432
|
-
timeoutPromise.then((type) => ({ type, res: null, stopRes: null })),
|
|
433
|
-
stopCandidate.then((stopRes) => ({ type: 'stop', res: null, stopRes })),
|
|
434
|
-
]);
|
|
435
|
-
if (timeoutTimer)
|
|
436
|
-
clearTimeout(timeoutTimer);
|
|
437
|
-
if (samplingInterval)
|
|
438
|
-
clearInterval(samplingInterval);
|
|
439
|
-
let exitResult;
|
|
440
|
-
if (exitOrTimeout.type === 'stop') {
|
|
441
|
-
const stopRes = exitOrTimeout.stopRes;
|
|
442
|
-
if (stopRes.stopped !== 'confirmed_stopped') {
|
|
443
|
-
if (handle.rawProcess && typeof handle.rawProcess.kill === 'function') {
|
|
444
|
-
try {
|
|
445
|
-
handle.rawProcess.kill('SIGKILL');
|
|
446
|
-
}
|
|
447
|
-
catch { }
|
|
448
|
-
}
|
|
449
|
-
try {
|
|
450
|
-
handle.stdout.destroy?.();
|
|
451
|
-
handle.stderr.destroy?.();
|
|
452
|
-
}
|
|
453
|
-
catch { }
|
|
454
|
-
const stored = this.domain.getStore().getOperation(options.opId);
|
|
455
|
-
if (stored && stored.status === 'done') {
|
|
456
|
-
return (stored.result ?? null);
|
|
457
|
-
}
|
|
458
|
-
const indetResult = {
|
|
459
|
-
kind: 'indeterminate',
|
|
460
|
-
status: 'indeterminate',
|
|
461
|
-
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
462
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
463
|
-
durationMs: Date.now() - startTime,
|
|
464
|
-
completedAt: new Date().toISOString(),
|
|
465
|
-
};
|
|
466
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
467
|
-
}
|
|
468
|
-
const rootExitWaitMs = 1000;
|
|
469
|
-
const rootExitOrTimeout = await Promise.race([
|
|
470
|
-
rootExitPromise.then((res) => ({ exited: true, res })),
|
|
471
|
-
new Promise((resolve) => setTimeout(() => resolve({ exited: false }), rootExitWaitMs)),
|
|
472
|
-
]);
|
|
473
|
-
exitResult = rootExitOrTimeout.exited ? rootExitOrTimeout.res : { exitCode: null, signal: 'SIGKILL' };
|
|
474
|
-
}
|
|
475
|
-
else if (exitOrTimeout.type === 'timeout') {
|
|
476
|
-
opState.timedOut = true;
|
|
477
|
-
opState.terminationReason = 'timed_out';
|
|
478
|
-
const stopRes = await this.handleStopPipeline(options.opId, 'timed_out', 1500);
|
|
479
|
-
// 超时路径:根进程已由停止流水线终止,等待根进程退出事实(避免被持有管道的逃逸后代挂死)
|
|
480
|
-
// 关键防护(问题 4):给等待根进程退出增加有界上限(1000ms),
|
|
481
|
-
// 若驱动停止未确认(stopped !== 'confirmed_stopped')或根进程超出上限仍未退出,转入 indeterminate
|
|
482
|
-
const rootExitWaitMs = 1000;
|
|
483
|
-
const rootExitOrTimeout = await Promise.race([
|
|
484
|
-
(handle.onRootExit ?? handle.onExit).then((res) => ({ exited: true, res })),
|
|
485
|
-
new Promise((resolve) => setTimeout(() => resolve({ exited: false }), rootExitWaitMs)),
|
|
486
|
-
]);
|
|
487
|
-
if (stopRes.stopped !== 'confirmed_stopped' || !rootExitOrTimeout.exited) {
|
|
488
|
-
const stored = this.domain.getStore().getOperation(options.opId);
|
|
489
|
-
if (stored && stored.status === 'done' && stored.result?.status === 'indeterminate') {
|
|
490
|
-
return stored.result;
|
|
491
|
-
}
|
|
492
|
-
const indetResult = {
|
|
493
|
-
kind: 'indeterminate',
|
|
494
|
-
status: 'indeterminate',
|
|
495
|
-
reason: `Process ${handle.identity.pid} timed out and root process could not be confirmed exited within ${rootExitWaitMs}ms bound: ${stopRes.errorDetails || 'root process still alive'}`,
|
|
496
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
497
|
-
durationMs: Date.now() - startTime,
|
|
498
|
-
completedAt: new Date().toISOString(),
|
|
499
|
-
};
|
|
500
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
501
|
-
}
|
|
502
|
-
exitResult = rootExitOrTimeout.res;
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
exitResult = exitOrTimeout.res;
|
|
506
|
-
}
|
|
507
|
-
// 有界等待流排空
|
|
508
|
-
const drainPromise = Promise.all([stdoutDrainer.finishPromise, stderrDrainer.finishPromise]);
|
|
509
|
-
const drainTimeoutPromise = new Promise((resolve) => setTimeout(resolve, drainTimeoutMs));
|
|
510
|
-
let drained = await Promise.race([
|
|
511
|
-
drainPromise.then(() => true),
|
|
512
|
-
drainTimeoutPromise.then(() => false),
|
|
513
|
-
]);
|
|
514
|
-
// 排空超时说明根进程已退出但仍有后代持有管道:回收整个进程组,
|
|
515
|
-
// 否则操作会一直挂到超时,且 root 的真实退出事实会被超时掩盖。
|
|
516
|
-
let residualProcessesReaped = false;
|
|
517
|
-
if (!drained && exitOrTimeout.type === 'exit') {
|
|
518
|
-
const reapResult = await this.driver.terminate(handle.identity, 1500);
|
|
519
|
-
if (reapResult.stopped !== 'confirmed_stopped') {
|
|
520
|
-
const indetResult = {
|
|
521
|
-
kind: 'indeterminate',
|
|
522
|
-
status: 'indeterminate',
|
|
523
|
-
reason: `Process ${handle.identity.pid} exited but residual descendants could not be confirmed stopped: ${reapResult.errorDetails}`,
|
|
524
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
525
|
-
durationMs: Date.now() - startTime,
|
|
526
|
-
completedAt: new Date().toISOString(),
|
|
527
|
-
};
|
|
528
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
529
|
-
}
|
|
530
|
-
residualProcessesReaped = true;
|
|
531
|
-
// 被杀死的后代释放管道后,给排空最后一次机会
|
|
532
|
-
drained = await Promise.race([
|
|
533
|
-
drainPromise.then(() => true),
|
|
534
|
-
new Promise((resolve) => setTimeout(() => resolve(false), 250)),
|
|
535
|
-
]);
|
|
536
|
-
}
|
|
537
|
-
if (!drained) {
|
|
538
|
-
// 排空超时路径强制调用 fsyncSync + closeSync 结清刷盘
|
|
539
|
-
stdoutDrainer.forceFinalize();
|
|
540
|
-
stderrDrainer.forceFinalize();
|
|
541
|
-
}
|
|
542
|
-
const stdoutData = stdoutDrainer.getResult();
|
|
543
|
-
const stderrData = stderrDrainer.getResult();
|
|
544
|
-
const isTruncated = stdoutData.isTruncated || stderrData.isTruncated;
|
|
545
|
-
const spillError = stdoutData.spillError || stderrData.spillError || artifactsDirError;
|
|
546
|
-
const outputRef = (!spillError && (stdoutData.outputRef || stderrData.outputRef)) || undefined;
|
|
547
|
-
const outputHash = (!spillError && (stdoutData.outputHash || stderrData.outputHash)) || undefined;
|
|
548
|
-
// 校验进程身份
|
|
549
|
-
const idVerify = await this.driver.verifyIdentity(handle.identity);
|
|
550
|
-
const isResourceOrTimeoutStopped = opState.timedOut ||
|
|
551
|
-
opState.terminationReason === 'memory_exceeded' ||
|
|
552
|
-
opState.terminationReason === 'cpu_exceeded' ||
|
|
553
|
-
opState.terminationReason === 'pids_exceeded' ||
|
|
554
|
-
opState.terminationReason === 'output_exceeded';
|
|
555
|
-
const isSucceeded = exitResult.exitCode === 0 &&
|
|
556
|
-
!isResourceOrTimeoutStopped &&
|
|
557
|
-
!opState.cancelRequested;
|
|
558
|
-
const finalStatus = isResourceOrTimeoutStopped
|
|
559
|
-
? 'failed'
|
|
560
|
-
: opState.cancelRequested
|
|
561
|
-
? 'cancelled'
|
|
562
|
-
: isSucceeded
|
|
563
|
-
? 'succeeded'
|
|
564
|
-
: 'failed';
|
|
565
|
-
const terminationReason = opState.terminationReason || (finalStatus === 'succeeded' ? 'completed' : undefined);
|
|
566
|
-
const result = {
|
|
567
|
-
kind: 'process',
|
|
568
|
-
status: finalStatus,
|
|
569
|
-
exitCode: exitResult.exitCode,
|
|
570
|
-
signal: exitResult.signal,
|
|
571
|
-
stdout: stdoutData.content,
|
|
572
|
-
stderr: stderrData.content,
|
|
573
|
-
isTruncated,
|
|
574
|
-
stdoutTruncated: stdoutData.isTruncated,
|
|
575
|
-
stderrTruncated: stderrData.isTruncated,
|
|
576
|
-
stdoutRef: !stdoutData.spillError ? stdoutData.outputRef : undefined,
|
|
577
|
-
stderrRef: !stderrData.spillError ? stderrData.outputRef : undefined,
|
|
578
|
-
stdoutBytes: stdoutData.bytesSeen,
|
|
579
|
-
stderrBytes: stderrData.bytesSeen,
|
|
580
|
-
stdoutHash: !stdoutData.spillError ? stdoutData.outputHash : undefined,
|
|
581
|
-
stderrHash: !stderrData.spillError ? stderrData.outputHash : undefined,
|
|
582
|
-
...(residualProcessesReaped ? { residualProcessesReaped: true } : {}),
|
|
583
|
-
...(streamCallbackError ? { streamCallbackError } : {}),
|
|
584
|
-
...(spillError ? { spillError } : {}),
|
|
585
|
-
...(stdoutData.spillError ? { stdoutSpillError: stdoutData.spillError } : {}),
|
|
586
|
-
...(stderrData.spillError ? { stderrSpillError: stderrData.spillError } : {}),
|
|
587
|
-
outputRef,
|
|
588
|
-
outputHash,
|
|
589
|
-
terminationReason,
|
|
590
|
-
peakMemoryBytes,
|
|
591
|
-
cpuTimeMs: peakCpuTimeMs,
|
|
592
|
-
identityVerification: idVerify,
|
|
593
|
-
durationMs: Date.now() - startTime,
|
|
594
|
-
completedAt: new Date().toISOString(),
|
|
595
|
-
};
|
|
596
|
-
if (opState.stopPromise) {
|
|
597
|
-
const stopRes = await opState.stopPromise;
|
|
598
|
-
if (stopRes.stopped !== 'confirmed_stopped') {
|
|
599
|
-
const stored = this.domain.getStore().getOperation(options.opId);
|
|
600
|
-
if (stored && stored.status === 'done') {
|
|
601
|
-
return (stored.result ?? null);
|
|
602
|
-
}
|
|
603
|
-
const indetResult = {
|
|
604
|
-
kind: 'indeterminate',
|
|
605
|
-
status: 'indeterminate',
|
|
606
|
-
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
607
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
608
|
-
durationMs: Date.now() - startTime,
|
|
609
|
-
completedAt: new Date().toISOString(),
|
|
610
|
-
};
|
|
611
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
// 6. [事务提交结果与释放资源 - Single Writer]
|
|
615
|
-
// 统一由 finalizeOperation 写入事实与处理资源
|
|
616
|
-
return this.finalizeOperation(options.opId, result, options.requiredResources, true, artifactsDir);
|
|
617
|
-
}
|
|
618
|
-
finally {
|
|
619
|
-
stdoutDrainer?.forceFinalize();
|
|
620
|
-
stderrDrainer?.forceFinalize();
|
|
621
|
-
if (timeoutTimer)
|
|
622
|
-
clearTimeout(timeoutTimer);
|
|
623
|
-
if (samplingInterval)
|
|
624
|
-
clearInterval(samplingInterval);
|
|
625
|
-
if (opState.stopResolve) {
|
|
626
|
-
const resolveFn = opState.stopResolve;
|
|
627
|
-
opState.stopResolve = undefined;
|
|
628
|
-
resolveFn({
|
|
629
|
-
stopped: 'confirmed_stopped',
|
|
630
|
-
scope: 'direct_child',
|
|
631
|
-
errorDetails: opState.phase === 'done' ? undefined : 'Operation terminated before process activation',
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
this.activeOperations.delete(options.opId);
|
|
635
|
-
}
|
|
636
|
-
})();
|
|
637
|
-
opState.resultPromise = executionPromise;
|
|
638
|
-
return await executionPromise;
|
|
218
|
+
return opState;
|
|
639
219
|
}
|
|
640
220
|
/**
|
|
641
221
|
* 停止确认流水线 (Stopping Pipeline)
|
|
@@ -685,15 +265,11 @@ export class ProcessSupervisor {
|
|
|
685
265
|
if (stopResult.stopped !== 'confirmed_stopped') {
|
|
686
266
|
const op = this.domain.getStore().getOperation(opId);
|
|
687
267
|
if (op && op.status !== 'done') {
|
|
688
|
-
const
|
|
689
|
-
kind: 'indeterminate',
|
|
690
|
-
status: 'indeterminate',
|
|
268
|
+
const indet = indeterminateResult({
|
|
691
269
|
reason: `Process ${active.handle.identity.pid} could not be confirmed stopped: ${stopResult.errorDetails}`,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
};
|
|
696
|
-
this.finalizeOperation(opId, indetResult, undefined, false);
|
|
270
|
+
startTime: active.startTime,
|
|
271
|
+
});
|
|
272
|
+
finalizeOperation(this.domain, opId, indet, { releaseResourceLock: false });
|
|
697
273
|
}
|
|
698
274
|
}
|
|
699
275
|
existingResolve?.(stopResult);
|
|
@@ -719,266 +295,12 @@ export class ProcessSupervisor {
|
|
|
719
295
|
}
|
|
720
296
|
return active;
|
|
721
297
|
}
|
|
722
|
-
handlePreSpawnCancellation(options, opState, startTime) {
|
|
723
|
-
opState.phase = 'done';
|
|
724
|
-
const stopRes = { stopped: 'confirmed_stopped', scope: 'direct_child' };
|
|
725
|
-
opState.stopResolve?.(stopRes);
|
|
726
|
-
opState.stopResolve = undefined;
|
|
727
|
-
const cancelResult = {
|
|
728
|
-
kind: 'process',
|
|
729
|
-
status: 'cancelled',
|
|
730
|
-
exitCode: null,
|
|
731
|
-
signal: null,
|
|
732
|
-
stdout: '',
|
|
733
|
-
stderr: '',
|
|
734
|
-
isTruncated: false,
|
|
735
|
-
terminationReason: 'user_cancelled',
|
|
736
|
-
evidence: 'unobserved',
|
|
737
|
-
identityVerification: 'not_original_process',
|
|
738
|
-
durationMs: Date.now() - startTime,
|
|
739
|
-
completedAt: new Date().toISOString(),
|
|
740
|
-
};
|
|
741
|
-
const stored = this.domain.getStore().getOperation(options.opId);
|
|
742
|
-
if (!stored) {
|
|
743
|
-
// 此时操作是在 waiting_resources 阶段就被取消,尚未获取资源租约,亦未正常注册意图。
|
|
744
|
-
// 为保证 store 中有该操作记录以便写入 cancelled 结果,直接记录预意图取消操作,
|
|
745
|
-
// 绝不向 resource_leases 写入虚假租约,亦不产生虚假的 OPERATION_INTENT_REGISTERED 事件。
|
|
746
|
-
const op = {
|
|
747
|
-
id: options.opId,
|
|
748
|
-
runId: options.runId,
|
|
749
|
-
kind: 'process',
|
|
750
|
-
name: options.name,
|
|
751
|
-
inputFingerprint: options.inputFingerprint || computeInputFingerprint(options),
|
|
752
|
-
requiredResources: options.requiredResources || [],
|
|
753
|
-
status: 'done',
|
|
754
|
-
};
|
|
755
|
-
this.domain.getStore().recordPreIntentCancelledOperation(op, this.domain.domainId, cancelResult);
|
|
756
|
-
this.domain.internalReleaseResources(options.opId, options.requiredResources);
|
|
757
|
-
return cancelResult;
|
|
758
|
-
}
|
|
759
|
-
return this.finalizeOperation(options.opId, cancelResult, options.requiredResources, true);
|
|
760
|
-
}
|
|
761
298
|
pruneArtifacts(filter) {
|
|
762
299
|
return this.domain.pruneArtifacts(filter);
|
|
763
300
|
}
|
|
301
|
+
/** 保留原签名:共享契约与单写者测试直接调用它验证「重复终结必须抛错」。 */
|
|
764
302
|
finalizeOperation(opId, result, requiredResources, releaseResourceLock = true, artifactsDir) {
|
|
765
|
-
|
|
766
|
-
if (existing && existing.status === 'done') {
|
|
767
|
-
throw new Error(`Cannot finalize operation "${opId}": operation is already finalized with status "${existing.status}"`);
|
|
768
|
-
}
|
|
769
|
-
if (existing) {
|
|
770
|
-
result.runId = existing.runId;
|
|
771
|
-
}
|
|
772
|
-
if (result.kind === 'process') {
|
|
773
|
-
const procRes = result;
|
|
774
|
-
const targetDir = artifactsDir || path.join(this.domain.domainPath, 'artifacts');
|
|
775
|
-
const stdoutSpillPath = path.join(targetDir, `${opId}-stdout.log`);
|
|
776
|
-
const stderrSpillPath = path.join(targetDir, `${opId}-stderr.log`);
|
|
777
|
-
if (!procRes.stdoutRef && fs.existsSync(stdoutSpillPath)) {
|
|
778
|
-
try {
|
|
779
|
-
fs.unlinkSync(stdoutSpillPath);
|
|
780
|
-
}
|
|
781
|
-
catch (unlinkErr) {
|
|
782
|
-
procRes.spillError = procRes.spillError || unlinkErr.message || String(unlinkErr);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (!procRes.stderrRef && fs.existsSync(stderrSpillPath)) {
|
|
786
|
-
try {
|
|
787
|
-
fs.unlinkSync(stderrSpillPath);
|
|
788
|
-
}
|
|
789
|
-
catch (unlinkErr) {
|
|
790
|
-
procRes.spillError = procRes.spillError || unlinkErr.message || String(unlinkErr);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (result.status === 'indeterminate') {
|
|
795
|
-
this.domain.getStore().recordOperationResult(opId, result, false);
|
|
796
|
-
const op = this.domain.getStore().getOperation(opId);
|
|
797
|
-
if (op) {
|
|
798
|
-
const termReason = result.terminationReason;
|
|
799
|
-
this.domain.getStore().updateRunStatus(op.runId, 'indeterminate', termReason);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
else {
|
|
803
|
-
this.domain.getStore().recordOperationResult(opId, result, true);
|
|
804
|
-
if (releaseResourceLock) {
|
|
805
|
-
this.domain.internalReleaseResources(opId, requiredResources);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
return result;
|
|
809
|
-
}
|
|
810
|
-
setupStreamDrainer(stream, maxBytes, spillFilePath, onChunk, onData, spillInitError) {
|
|
811
|
-
const overhead = maxBytes >= 80 ? Math.min(64, Math.floor(maxBytes / 4)) : 0;
|
|
812
|
-
const effectiveMax = maxBytes - overhead;
|
|
813
|
-
const headMaxBytes = overhead > 0 ? Math.floor(effectiveMax * 0.75) : maxBytes;
|
|
814
|
-
const tailMaxBytes = overhead > 0 ? Math.max(0, effectiveMax - headMaxBytes) : 0;
|
|
815
|
-
const headChunks = [];
|
|
816
|
-
let headBytes = 0;
|
|
817
|
-
let bytesSeen = 0;
|
|
818
|
-
let isTruncated = false;
|
|
819
|
-
let spillFd = null;
|
|
820
|
-
let spillError = spillInitError;
|
|
821
|
-
const hash = crypto.createHash('sha256');
|
|
822
|
-
let isFinalized = false;
|
|
823
|
-
let outputHash = undefined;
|
|
824
|
-
const tailRing = tailMaxBytes > 0 ? Buffer.alloc(tailMaxBytes) : null;
|
|
825
|
-
let tailHead = 0;
|
|
826
|
-
let tailCount = 0;
|
|
827
|
-
if (spillFilePath && !spillError) {
|
|
828
|
-
try {
|
|
829
|
-
spillFd = fs.openSync(spillFilePath, 'w');
|
|
830
|
-
}
|
|
831
|
-
catch (openErr) {
|
|
832
|
-
spillError = openErr.message || String(openErr);
|
|
833
|
-
spillFd = null;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
const doFinalize = () => {
|
|
837
|
-
if (isFinalized)
|
|
838
|
-
return;
|
|
839
|
-
isFinalized = true;
|
|
840
|
-
if (spillFd !== null) {
|
|
841
|
-
try {
|
|
842
|
-
fs.fsyncSync(spillFd);
|
|
843
|
-
fs.closeSync(spillFd);
|
|
844
|
-
}
|
|
845
|
-
catch (syncErr) {
|
|
846
|
-
spillError = spillError || syncErr.message || String(syncErr);
|
|
847
|
-
}
|
|
848
|
-
spillFd = null;
|
|
849
|
-
}
|
|
850
|
-
if (!spillError) {
|
|
851
|
-
try {
|
|
852
|
-
outputHash = hash.digest('hex');
|
|
853
|
-
}
|
|
854
|
-
catch { }
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
outputHash = undefined;
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
const finishPromise = new Promise((resolve) => {
|
|
861
|
-
const finalize = () => {
|
|
862
|
-
doFinalize();
|
|
863
|
-
resolve();
|
|
864
|
-
};
|
|
865
|
-
stream.on('data', (chunk) => {
|
|
866
|
-
if (isFinalized) {
|
|
867
|
-
// finalize 之后,流继续读走数据避免堵塞或 SIGPIPE,但跳过 hash、落盘和转发
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
871
|
-
bytesSeen += buf.length;
|
|
872
|
-
if (onChunk) {
|
|
873
|
-
try {
|
|
874
|
-
onChunk(buf.length);
|
|
875
|
-
}
|
|
876
|
-
catch { }
|
|
877
|
-
}
|
|
878
|
-
// 持续落盘转储全部流与写盘同链哈希 (P0-9)
|
|
879
|
-
if (spillFd !== null) {
|
|
880
|
-
try {
|
|
881
|
-
fs.writeSync(spillFd, buf);
|
|
882
|
-
hash.update(buf);
|
|
883
|
-
}
|
|
884
|
-
catch (writeErr) {
|
|
885
|
-
spillError = spillError || writeErr.message || String(writeErr);
|
|
886
|
-
try {
|
|
887
|
-
fs.closeSync(spillFd);
|
|
888
|
-
}
|
|
889
|
-
catch { }
|
|
890
|
-
spillFd = null;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
else if (!spillFilePath && !spillError) {
|
|
894
|
-
try {
|
|
895
|
-
hash.update(buf);
|
|
896
|
-
}
|
|
897
|
-
catch { }
|
|
898
|
-
}
|
|
899
|
-
if (onData) {
|
|
900
|
-
onData(buf);
|
|
901
|
-
}
|
|
902
|
-
// Head 内存有界保留
|
|
903
|
-
if (headBytes < headMaxBytes) {
|
|
904
|
-
if (headBytes + buf.length <= headMaxBytes) {
|
|
905
|
-
headChunks.push(buf);
|
|
906
|
-
headBytes += buf.length;
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
const remaining = headMaxBytes - headBytes;
|
|
910
|
-
if (remaining > 0) {
|
|
911
|
-
headChunks.push(buf.subarray(0, remaining));
|
|
912
|
-
headBytes += remaining;
|
|
913
|
-
}
|
|
914
|
-
isTruncated = true;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
isTruncated = true;
|
|
919
|
-
}
|
|
920
|
-
if (bytesSeen > maxBytes) {
|
|
921
|
-
isTruncated = true;
|
|
922
|
-
}
|
|
923
|
-
// Tail 环形缓冲保留
|
|
924
|
-
if (tailRing !== null && tailMaxBytes > 0) {
|
|
925
|
-
if (buf.length >= tailMaxBytes) {
|
|
926
|
-
buf.copy(tailRing, 0, buf.length - tailMaxBytes);
|
|
927
|
-
tailHead = 0;
|
|
928
|
-
tailCount = tailMaxBytes;
|
|
929
|
-
}
|
|
930
|
-
else {
|
|
931
|
-
for (let i = 0; i < buf.length; i++) {
|
|
932
|
-
tailRing[tailHead] = buf[i];
|
|
933
|
-
tailHead = (tailHead + 1) % tailMaxBytes;
|
|
934
|
-
}
|
|
935
|
-
tailCount = Math.min(tailMaxBytes, tailCount + buf.length);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
stream.on('end', finalize);
|
|
940
|
-
stream.on('close', finalize);
|
|
941
|
-
stream.on('error', finalize);
|
|
942
|
-
});
|
|
943
|
-
return {
|
|
944
|
-
getResult: () => {
|
|
945
|
-
doFinalize();
|
|
946
|
-
let content;
|
|
947
|
-
if (bytesSeen <= maxBytes) {
|
|
948
|
-
content = Buffer.concat(headChunks).toString('utf8');
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
const rawHead = trimToValidUtf8(Buffer.concat(headChunks));
|
|
952
|
-
let cleanTail = Buffer.alloc(0);
|
|
953
|
-
if (tailRing !== null && tailCount > 0) {
|
|
954
|
-
const rawTail = Buffer.alloc(tailCount);
|
|
955
|
-
if (tailCount < tailMaxBytes) {
|
|
956
|
-
tailRing.copy(rawTail, 0, 0, tailCount);
|
|
957
|
-
}
|
|
958
|
-
else {
|
|
959
|
-
const part1 = tailRing.subarray(tailHead, tailMaxBytes);
|
|
960
|
-
const part2 = tailRing.subarray(0, tailHead);
|
|
961
|
-
part1.copy(rawTail, 0);
|
|
962
|
-
part2.copy(rawTail, part1.length);
|
|
963
|
-
}
|
|
964
|
-
cleanTail = trimStartToValidUtf8(rawTail);
|
|
965
|
-
}
|
|
966
|
-
const truncatedBytes = Math.max(0, bytesSeen - rawHead.length - cleanTail.length);
|
|
967
|
-
const marker = `\n[... truncated ${truncatedBytes} bytes ...]\n`;
|
|
968
|
-
content = rawHead.toString('utf8') + marker + cleanTail.toString('utf8');
|
|
969
|
-
}
|
|
970
|
-
return {
|
|
971
|
-
content,
|
|
972
|
-
isTruncated,
|
|
973
|
-
outputRef: isTruncated && spillFilePath && !spillError ? spillFilePath : undefined,
|
|
974
|
-
outputHash: !spillError ? outputHash : undefined,
|
|
975
|
-
bytesSeen,
|
|
976
|
-
spillError,
|
|
977
|
-
};
|
|
978
|
-
},
|
|
979
|
-
finishPromise,
|
|
980
|
-
forceFinalize: doFinalize,
|
|
981
|
-
};
|
|
303
|
+
return finalizeOperation(this.domain, opId, result, { requiredResources, releaseResourceLock, artifactsDir });
|
|
982
304
|
}
|
|
983
305
|
}
|
|
984
306
|
//# sourceMappingURL=supervisor.js.map
|