@xioflow/kernel 0.1.5 → 0.2.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.
Files changed (38) hide show
  1. package/README.md +14 -3
  2. package/dist/domain.d.ts +40 -6
  3. package/dist/domain.d.ts.map +1 -1
  4. package/dist/domain.js +388 -24
  5. package/dist/domain.js.map +1 -1
  6. package/dist/driver/node-driver.d.ts +10 -24
  7. package/dist/driver/node-driver.d.ts.map +1 -1
  8. package/dist/driver/node-driver.js +201 -280
  9. package/dist/driver/node-driver.js.map +1 -1
  10. package/dist/driver/process-facts.d.ts +25 -0
  11. package/dist/driver/process-facts.d.ts.map +1 -0
  12. package/dist/driver/process-facts.js +164 -0
  13. package/dist/driver/process-facts.js.map +1 -0
  14. package/dist/driver/sampler.d.ts +51 -0
  15. package/dist/driver/sampler.d.ts.map +1 -0
  16. package/dist/driver/sampler.js +224 -0
  17. package/dist/driver/sampler.js.map +1 -0
  18. package/dist/driver/types.d.ts +21 -1
  19. package/dist/driver/types.d.ts.map +1 -1
  20. package/dist/recovery/engine.d.ts.map +1 -1
  21. package/dist/recovery/engine.js +103 -19
  22. package/dist/recovery/engine.js.map +1 -1
  23. package/dist/store/sqlite.d.ts +12 -0
  24. package/dist/store/sqlite.d.ts.map +1 -1
  25. package/dist/store/sqlite.js +162 -12
  26. package/dist/store/sqlite.js.map +1 -1
  27. package/dist/supervisor/supervisor.d.ts +15 -0
  28. package/dist/supervisor/supervisor.d.ts.map +1 -1
  29. package/dist/supervisor/supervisor.js +619 -214
  30. package/dist/supervisor/supervisor.js.map +1 -1
  31. package/dist/testing/contract-suite.d.ts.map +1 -1
  32. package/dist/testing/contract-suite.js +301 -11
  33. package/dist/testing/contract-suite.js.map +1 -1
  34. package/dist/types.d.ts +34 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/types.js +29 -0
  37. package/dist/types.js.map +1 -1
  38. package/package.json +2 -2
package/README.md CHANGED
@@ -42,7 +42,7 @@ npm install @xioflow/kernel
42
42
  import { ExecutionDomain, NodePlatformDriver, ProcessSupervisor } from '@xioflow/kernel';
43
43
 
44
44
  // The domain directory will hold domain.db, domain.lock and artifacts/.
45
- const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow', 'my-domain');
45
+ const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow/kernel', 'my-domain');
46
46
  const supervisor = new ProcessSupervisor(domain, new NodePlatformDriver());
47
47
 
48
48
  // Every operation belongs to a Run, and every Run to a Task. Register them first:
@@ -96,7 +96,7 @@ After a restart, reacquire the domain and run the recovery engine:
96
96
  ```js
97
97
  import { ExecutionDomain, NodePlatformDriver, RecoveryEngine } from '@xioflow/kernel';
98
98
 
99
- const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow', 'my-domain');
99
+ const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow/kernel', 'my-domain');
100
100
  const report = await new RecoveryEngine(domain, new NodePlatformDriver()).recover();
101
101
  ```
102
102
 
@@ -150,10 +150,21 @@ Releases are tag-driven. A tag always produces a GitHub Release; the npm upload
150
150
 
151
151
  1. Bump `version` in `package.json`, commit, and push to `main`.
152
152
  2. Push the matching tag, e.g. `git tag v0.1.5 && git push origin v0.1.5`.
153
- 3. `.github/workflows/release.yml` re-runs typecheck, tests and the pack smoke test, refuses a tag that does not match `package.json`, creates the GitHub Release, and — only when the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED` is `true` — publishes with `npm publish --provenance` and verifies the attestation.
153
+ 3. `.github/workflows/release.yml` re-runs typecheck, tests and the pack smoke test, refuses a tag that does not match `package.json`, creates the GitHub Release with the packed tarball, `SHA256SUMS` and a build provenance attestation attached, and — only when the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED` is `true` — publishes with `npm publish --provenance` and verifies the attestation.
154
154
 
155
155
  One-time setup, in this order: (1) npmjs.com → package settings → Trusted Publisher → GitHub Actions: organization `Xio-Shark`, repository `xioflow`, workflow filename `release.yml`, environment left empty; (2) set the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED=true`. Until both exist the publish job is skipped and the reason is printed in the `verify` job's log. npm's registry index can lag several minutes behind an upload, so the verification step polls and may need a job re-run.
156
156
 
157
+ To attach assets to an existing tag without publishing to npm, run the workflow manually: `gh workflow run release.yml -f tag=v0.1.5`. An asset that is already attached is kept if identical and fails the run if it differs; published assets are never overwritten.
158
+
159
+ ### Verifying a release
160
+
161
+ ```bash
162
+ gh release download v0.1.5 --repo Xio-Shark/xioflow
163
+ shasum -a 256 -c SHA256SUMS
164
+ gh attestation verify xioflow-kernel-0.1.5.tgz --repo Xio-Shark/xioflow
165
+ npm audit signatures # inside a project that installed @xioflow/kernel from npm
166
+ ```
167
+
157
168
  ## License
158
169
 
159
170
  MIT
package/dist/domain.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SqliteStore } from './store/sqlite.js';
2
- import { Operation, OwnerRecord, ResourceBudget, DomainBudget, DomainLockedError, ResourceConflictError, EpochFencedError } from './types.js';
3
- export { DomainLockedError, ResourceConflictError, EpochFencedError };
2
+ import { PlatformDriver } from './driver/types.js';
3
+ import { Operation, OwnerRecord, ResourceBudget, DomainBudget, DomainLockedError, ResourceConflictError, EpochFencedError, TerminationReason, AdjudicationRecord } from './types.js';
4
+ export { DomainLockedError, ResourceConflictError, EpochFencedError, AdjudicationRecord };
4
5
  export declare class ExecutionDomain {
5
6
  readonly domainPath: string;
6
7
  readonly domainId: string;
@@ -9,8 +10,11 @@ export declare class ExecutionDomain {
9
10
  private lockFd;
10
11
  private ownerRecord;
11
12
  private domainBudget?;
13
+ private driver?;
12
14
  private inMemoryLockedResources;
15
+ private inMemoryAllocatedOps;
13
16
  private inMemoryBudgets;
17
+ private waitQueue;
14
18
  private heartbeatInterval;
15
19
  private isFenced;
16
20
  private constructor();
@@ -34,22 +38,52 @@ export declare class ExecutionDomain {
34
38
  * 刷新租约心跳,若失败则触发代际栅栏与写凭证销毁
35
39
  */
36
40
  renewHeartbeat(): void;
41
+ private getActiveOperationIds;
42
+ private diagnoseConflict;
37
43
  /**
38
44
  * 申请占用资源(支持隔离检查与配额校验)
39
45
  */
40
46
  allocateResources(operationId: string, resources: string[], waitedDurationMs?: number, budget?: ResourceBudget): void;
41
47
  /**
42
- * 具备排队等待与诊断的异步资源分配
48
+ * 具备 FIFO 排队等待与诊断的异步资源分配 (P0-5, P0-13)
43
49
  */
44
- allocateResourcesWithWait(operationId: string, resources: string[], maxWaitMs?: number, budget?: ResourceBudget): Promise<void>;
50
+ allocateResourcesWithWait(operationId: string, resources: string[], maxWaitMs?: number, budget?: ResourceBudget, isCancelled?: () => boolean): Promise<void>;
45
51
  /**
46
- * 释放资源占用
52
+ * 取消在等待队列中的操作 (立即唤醒以完成取消协议)
47
53
  */
48
- releaseResources(operationId: string, resources?: string[]): void;
54
+ cancelWait(operationId: string): boolean;
55
+ private processWaitQueue;
56
+ setDriver(driver: PlatformDriver): void;
57
+ /**
58
+ * 内部私有资源释放(走代际栅栏并记录 RESOURCES_RELEASED 事件,§0.2 裁决 1 / N6)
59
+ * @internal 仅供 supervisor、recovery engine 与 adjudicate 等内核内部组件调用
60
+ */
61
+ internalReleaseResources(operationId: string, resources?: string[]): void;
62
+ /**
63
+ * 人工裁决协议:对 indeterminate 状态进行有审计记录的唯一收口 (ARCHITECTURE §3.6 / P0-7)
64
+ */
65
+ adjudicate(opId: string, verdict: 'confirmed_stopped' | 'abandon_with_residuals', actor: string, note?: string): Promise<AdjudicationRecord>;
66
+ /**
67
+ * Run 完成与取消协议 (ARCHITECTURE §3.3)
68
+ */
69
+ reportRunCancelled(runId: string, reason?: TerminationReason): void;
70
+ reportRunSucceeded(runId: string): void;
71
+ reportRunFailed(runId: string, reason?: TerminationReason): void;
49
72
  /**
50
73
  * 意图登记协议:先持久化意图与占用,再允许驱动调用
51
74
  */
52
75
  registerOperationIntent(op: Operation): void;
76
+ /**
77
+ * 产物回收防线 (ARCHITECTURE §4.3 / N7)
78
+ * 只回收已终态且未被引用的产物,严格拒绝回收未终结或 indeterminate 状态的产物。
79
+ */
80
+ pruneArtifacts(filter?: {
81
+ olderThanMs?: number;
82
+ prefix?: string;
83
+ }): {
84
+ deleted: string[];
85
+ retained: string[];
86
+ };
53
87
  isClosed(): boolean;
54
88
  close(): void;
55
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAEL,SAAS,EAET,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;AAEtE,qBAAa,eAAe;IAC1B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,YAAY,CAAC,CAAe;IAEpC,OAAO,CAAC,uBAAuB,CAAkC;IAEjE,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,QAAQ,CAAkB;IAElC,OAAO,eAqCN;IAED,OAAc,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAkB,GAAG,eAAe,CAqDvF;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IASzB,cAAc,IAAI,WAAW,CAEnC;IAEM,QAAQ,IAAI,MAAM,CAExB;IAEM,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAEjD;IAEM,eAAe,IAAI,YAAY,GAAG,SAAS,CAEjD;IAED;;OAEG;IACI,yBAAyB,IAAI,IAAI,CAuBvC;IAEM,QAAQ,IAAI,WAAW,CAE7B;IAEM,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEnD;IAEM,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE9D;IAEM,qBAAqB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAElD;IAEM,cAAc,IAAI,OAAO,CAE/B;IAED,OAAO,CAAC,eAAe;IAWvB;;OAEG;IACI,cAAc,IAAI,IAAI,CAqB5B;IAED;;OAEG;IACI,iBAAiB,CACtB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,gBAAgB,GAAE,MAAU,EAC5B,MAAM,CAAC,EAAE,cAAc,GACtB,IAAI,CA6BN;IAED;;OAEG;IACU,yBAAyB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,GAAE,MAAU,EACrB,MAAM,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAkBf;IAED;;OAEG;IACI,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAiBvE;IAED;;OAEG;IACI,uBAAuB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAalD;IAEM,QAAQ,IAAI,OAAO,CAEzB;IAEM,KAAK,IAAI,IAAI,CAmCnB;CACF"}
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"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"}
package/dist/domain.js CHANGED
@@ -12,10 +12,15 @@ export class ExecutionDomain {
12
12
  lockFd = null;
13
13
  ownerRecord;
14
14
  domainBudget;
15
+ driver;
15
16
  // 内存隔离表:resourceId -> operationId
16
17
  inMemoryLockedResources = new Map();
18
+ // 内存活跃操作(独立于资源租约,用于 domain:max_concurrent_ops)
19
+ inMemoryAllocatedOps = new Set();
17
20
  // 内存活跃操作预算
18
21
  inMemoryBudgets = new Map();
22
+ // FIFO 等待队列
23
+ waitQueue = [];
19
24
  heartbeatInterval = null;
20
25
  isFenced = false;
21
26
  constructor(domainPath, domainId, lockFilePath, lockFd) {
@@ -42,7 +47,7 @@ export class ExecutionDomain {
42
47
  this.heartbeatInterval = null;
43
48
  }
44
49
  try {
45
- this.store.unsafeSetCurrentEpochForTesting(-1);
50
+ this.store.fence();
46
51
  }
47
52
  catch { }
48
53
  }
@@ -56,6 +61,7 @@ export class ExecutionDomain {
56
61
  fs.mkdirSync(domainPath, { recursive: true });
57
62
  }
58
63
  const lockFilePath = path.join(domainPath, 'domain.lock');
64
+ const dbPath = path.join(domainPath, 'domain.db');
59
65
  let fd = null;
60
66
  try {
61
67
  // 尝试独占创建锁文件
@@ -74,11 +80,24 @@ export class ExecutionDomain {
74
80
  }
75
81
  if (lockMeta) {
76
82
  const isAlive = ExecutionDomain.checkProcessAlive(lockMeta.ownerPid);
77
- if (isAlive) {
83
+ let leaseExpired = false;
84
+ // 结合 owners 表判定租约是否过期 (P0-11)
85
+ if (fs.existsSync(dbPath)) {
86
+ try {
87
+ const tempStore = new SqliteStore(dbPath);
88
+ const owner = tempStore.getOwner(domainId);
89
+ if (owner && new Date(owner.expiresAt).getTime() < Date.now()) {
90
+ leaseExpired = true;
91
+ }
92
+ tempStore.close();
93
+ }
94
+ catch { }
95
+ }
96
+ if (isAlive && !leaseExpired) {
78
97
  throw new DomainLockedError(lockMeta.domainId, lockMeta.ownerPid, lockMeta.acquiredAt);
79
98
  }
80
99
  }
81
- // 旧进程已死亡,安全覆盖锁
100
+ // 旧进程已死亡或租约已过期,安全覆盖锁
82
101
  try {
83
102
  fs.unlinkSync(lockFilePath);
84
103
  }
@@ -96,7 +115,22 @@ export class ExecutionDomain {
96
115
  hostname: os.hostname(),
97
116
  };
98
117
  fs.writeSync(fd, JSON.stringify(metadata, null, 2));
99
- return new ExecutionDomain(domainPath, domainId, lockFilePath, fd);
118
+ try {
119
+ return new ExecutionDomain(domainPath, domainId, lockFilePath, fd);
120
+ }
121
+ catch (ctorErr) {
122
+ if (fd !== null) {
123
+ try {
124
+ fs.closeSync(fd);
125
+ }
126
+ catch { }
127
+ try {
128
+ fs.unlinkSync(lockFilePath);
129
+ }
130
+ catch { }
131
+ }
132
+ throw ctorErr;
133
+ }
100
134
  }
101
135
  static checkProcessAlive(pid) {
102
136
  try {
@@ -125,6 +159,7 @@ export class ExecutionDomain {
125
159
  rebuildIsolationFromStore() {
126
160
  this.inMemoryLockedResources.clear();
127
161
  this.inMemoryBudgets.clear();
162
+ this.inMemoryAllocatedOps.clear();
128
163
  // 1. 读取持久化未释放的 resource_leases
129
164
  const leases = this.store.getPersistedResourceLeases(this.domainId);
130
165
  for (const lease of leases) {
@@ -133,9 +168,10 @@ export class ExecutionDomain {
133
168
  this.inMemoryBudgets.set(lease.operationId, lease.budget);
134
169
  }
135
170
  }
136
- // 2. 核对未终结操作声明的所有资源
171
+ // 2. 核对未终结操作声明的所有资源与活跃操作集合 (独立于 resource_leases)
137
172
  const unfinishedOps = this.store.getUnfinishedOperations(this.domainId);
138
173
  for (const op of unfinishedOps) {
174
+ this.inMemoryAllocatedOps.add(op.id);
139
175
  for (const res of op.requiredResources) {
140
176
  this.inMemoryLockedResources.set(res, op.id);
141
177
  }
@@ -181,12 +217,36 @@ export class ExecutionDomain {
181
217
  this.heartbeatInterval = null;
182
218
  }
183
219
  try {
184
- this.store.unsafeSetCurrentEpochForTesting(-1);
220
+ this.store.fence();
185
221
  }
186
222
  catch { }
187
223
  throw err;
188
224
  }
189
225
  }
226
+ getActiveOperationIds() {
227
+ const set = new Set(this.inMemoryAllocatedOps);
228
+ const unfinishedOps = this.store.getUnfinishedOperations(this.domainId);
229
+ for (const op of unfinishedOps) {
230
+ set.add(op.id);
231
+ }
232
+ return set;
233
+ }
234
+ diagnoseConflict(operationId, resources, waitedDurationMs = 0, budget) {
235
+ for (const res of resources) {
236
+ const existingOwner = this.inMemoryLockedResources.get(res);
237
+ if (existingOwner && existingOwner !== operationId) {
238
+ return new ResourceConflictError(res, existingOwner, operationId, waitedDurationMs);
239
+ }
240
+ }
241
+ if (this.domainBudget && this.domainBudget.maxConcurrentOps) {
242
+ const activeOps = this.getActiveOperationIds();
243
+ if (activeOps.size >= this.domainBudget.maxConcurrentOps && !activeOps.has(operationId)) {
244
+ const holder = Array.from(activeOps)[0] || 'domain-concurrency-cap';
245
+ return new ResourceConflictError('domain:max_concurrent_ops', holder, operationId, waitedDurationMs);
246
+ }
247
+ }
248
+ return new ResourceConflictError(resources[0] || 'domain:resource', 'unknown', operationId, waitedDurationMs);
249
+ }
190
250
  /**
191
251
  * 申请占用资源(支持隔离检查与配额校验)
192
252
  */
@@ -195,20 +255,22 @@ export class ExecutionDomain {
195
255
  // 1. 检查资源冲突
196
256
  for (const res of resources) {
197
257
  const existingOwner = this.inMemoryLockedResources.get(res);
258
+ // 注(N8):现有持有人等于当前 opId 仅当同一调用内部阶段重入(如子操作或多阶段申请)时合法;
259
+ // 在飞 opId 的重复提交已由 supervisor 入口守卫拦截,绝不可能在此被当作合法持有人放行。
198
260
  if (existingOwner && existingOwner !== operationId) {
199
261
  throw new ResourceConflictError(res, existingOwner, operationId, waitedDurationMs);
200
262
  }
201
263
  }
202
- // 2. 检查域级配额(并发数)
203
- if (this.domainBudget) {
204
- const activeOps = new Set(this.inMemoryLockedResources.values());
205
- if (this.domainBudget.maxConcurrentOps &&
206
- activeOps.size >= this.domainBudget.maxConcurrentOps &&
264
+ // 2. 检查域级配额(并发数独立于 resource_leases)
265
+ if (this.domainBudget && this.domainBudget.maxConcurrentOps) {
266
+ const activeOps = this.getActiveOperationIds();
267
+ if (activeOps.size >= this.domainBudget.maxConcurrentOps &&
207
268
  !activeOps.has(operationId)) {
208
269
  const holder = Array.from(activeOps)[0] || 'domain-concurrency-cap';
209
270
  throw new ResourceConflictError('domain:max_concurrent_ops', holder, operationId, waitedDurationMs);
210
271
  }
211
272
  }
273
+ this.inMemoryAllocatedOps.add(operationId);
212
274
  for (const res of resources) {
213
275
  this.inMemoryLockedResources.set(res, operationId);
214
276
  }
@@ -217,39 +279,157 @@ export class ExecutionDomain {
217
279
  }
218
280
  }
219
281
  /**
220
- * 具备排队等待与诊断的异步资源分配
282
+ * 具备 FIFO 排队等待与诊断的异步资源分配 (P0-5, P0-13)
221
283
  */
222
- async allocateResourcesWithWait(operationId, resources, maxWaitMs = 0, budget) {
284
+ async allocateResourcesWithWait(operationId, resources, maxWaitMs = 0, budget, isCancelled) {
285
+ this.assertNotFenced();
286
+ if (isCancelled && isCancelled()) {
287
+ return;
288
+ }
223
289
  const startTime = Date.now();
224
- while (true) {
290
+ // 如果队列为空,尝试直接分配
291
+ if (this.waitQueue.length === 0) {
225
292
  try {
226
- this.allocateResources(operationId, resources, Date.now() - startTime, budget);
293
+ this.allocateResources(operationId, resources, 0, budget);
227
294
  return;
228
295
  }
296
+ catch (err) {
297
+ if (!(err instanceof ResourceConflictError)) {
298
+ throw err;
299
+ }
300
+ if (maxWaitMs <= 0) {
301
+ throw err;
302
+ }
303
+ }
304
+ }
305
+ else {
306
+ // 队列中已有排队者:若不愿等待则直接抛出诊断
307
+ if (maxWaitMs <= 0) {
308
+ throw this.diagnoseConflict(operationId, resources, 0, budget);
309
+ }
310
+ }
311
+ // 必须入队等待 (严格 FIFO)
312
+ return new Promise((resolve, reject) => {
313
+ let timer;
314
+ const item = {
315
+ operationId,
316
+ resources,
317
+ budget,
318
+ resolve: () => {
319
+ if (timer)
320
+ clearTimeout(timer);
321
+ resolve();
322
+ },
323
+ reject: (err) => {
324
+ if (timer)
325
+ clearTimeout(timer);
326
+ reject(err);
327
+ },
328
+ isCancelled,
329
+ startTime,
330
+ maxWaitMs,
331
+ timer: undefined,
332
+ };
333
+ timer = setTimeout(() => {
334
+ const idx = this.waitQueue.indexOf(item);
335
+ if (idx !== -1) {
336
+ this.waitQueue.splice(idx, 1);
337
+ }
338
+ if (item.isCancelled && item.isCancelled()) {
339
+ item.resolve();
340
+ return;
341
+ }
342
+ const elapsed = Date.now() - item.startTime;
343
+ const err = this.diagnoseConflict(item.operationId, item.resources, elapsed, item.budget);
344
+ item.reject(err);
345
+ }, maxWaitMs);
346
+ item.timer = timer;
347
+ this.waitQueue.push(item);
348
+ });
349
+ }
350
+ /**
351
+ * 取消在等待队列中的操作 (立即唤醒以完成取消协议)
352
+ */
353
+ cancelWait(operationId) {
354
+ const idx = this.waitQueue.findIndex((item) => item.operationId === operationId);
355
+ if (idx !== -1) {
356
+ const item = this.waitQueue[idx];
357
+ this.waitQueue.splice(idx, 1);
358
+ if (item.timer)
359
+ clearTimeout(item.timer);
360
+ item.resolve();
361
+ return true;
362
+ }
363
+ return false;
364
+ }
365
+ processWaitQueue() {
366
+ if (this.waitQueue.length === 0 || this.isFenced) {
367
+ return;
368
+ }
369
+ const reservedResources = new Set();
370
+ let concurrencyBlocked = false;
371
+ let i = 0;
372
+ while (i < this.waitQueue.length) {
373
+ const item = this.waitQueue[i];
374
+ // 1. 检查取消
375
+ if (item.isCancelled && item.isCancelled()) {
376
+ this.waitQueue.splice(i, 1);
377
+ item.resolve();
378
+ continue;
379
+ }
380
+ // 2. 检查是否与更早等待者的资源发生争抢
381
+ const hasConflictWithEarlierWaiters = item.resources.some((r) => reservedResources.has(r));
382
+ if (hasConflictWithEarlierWaiters || concurrencyBlocked) {
383
+ for (const res of item.resources) {
384
+ reservedResources.add(res);
385
+ }
386
+ i++;
387
+ continue;
388
+ }
389
+ // 3. 尝试分配
390
+ try {
391
+ const elapsed = Date.now() - item.startTime;
392
+ this.allocateResources(item.operationId, item.resources, elapsed, item.budget);
393
+ // 分配成功:移出等待队列并唤醒
394
+ this.waitQueue.splice(i, 1);
395
+ item.resolve();
396
+ }
229
397
  catch (err) {
230
398
  if (err instanceof ResourceConflictError) {
231
- const elapsed = Date.now() - startTime;
232
- if (elapsed >= maxWaitMs) {
233
- throw new ResourceConflictError(err.resourceId, err.existingOwnerOpId, operationId, elapsed);
399
+ for (const res of item.resources) {
400
+ reservedResources.add(res);
401
+ }
402
+ if (err.resourceId === 'domain:max_concurrent_ops') {
403
+ concurrencyBlocked = true;
234
404
  }
235
- await new Promise((r) => setTimeout(r, 50));
405
+ i++;
236
406
  }
237
407
  else {
238
- throw err;
408
+ this.waitQueue.splice(i, 1);
409
+ item.reject(err);
239
410
  }
240
411
  }
241
412
  }
242
413
  }
414
+ setDriver(driver) {
415
+ this.driver = driver;
416
+ }
243
417
  /**
244
- * 释放资源占用
418
+ * 内部私有资源释放(走代际栅栏并记录 RESOURCES_RELEASED 事件,§0.2 裁决 1 / N6)
419
+ * @internal 仅供 supervisor、recovery engine 与 adjudicate 等内核内部组件调用
245
420
  */
246
- releaseResources(operationId, resources) {
421
+ internalReleaseResources(operationId, resources) {
422
+ this.assertNotFenced();
423
+ this.store.verifyEpochFencing(this.domainId);
424
+ const releasedList = [];
425
+ this.inMemoryAllocatedOps.delete(operationId);
247
426
  this.inMemoryBudgets.delete(operationId);
248
- if (resources) {
427
+ if (resources && resources.length > 0) {
249
428
  for (const res of resources) {
250
429
  if (this.inMemoryLockedResources.get(res) === operationId) {
251
430
  this.inMemoryLockedResources.delete(res);
252
431
  this.store.releaseResourceLease(operationId, res);
432
+ releasedList.push(res);
253
433
  }
254
434
  }
255
435
  }
@@ -257,10 +437,115 @@ export class ExecutionDomain {
257
437
  for (const [res, owner] of Array.from(this.inMemoryLockedResources.entries())) {
258
438
  if (owner === operationId) {
259
439
  this.inMemoryLockedResources.delete(res);
440
+ releasedList.push(res);
260
441
  }
261
442
  }
262
443
  this.store.releaseResourceLease(operationId);
263
444
  }
445
+ this.store.recordEventAndTransitionState({
446
+ domainId: this.domainId,
447
+ operationId,
448
+ type: 'RESOURCES_RELEASED',
449
+ payload: {
450
+ operationId,
451
+ resources: releasedList,
452
+ },
453
+ timestamp: new Date().toISOString(),
454
+ });
455
+ this.processWaitQueue();
456
+ }
457
+ /**
458
+ * 人工裁决协议:对 indeterminate 状态进行有审计记录的唯一收口 (ARCHITECTURE §3.6 / P0-7)
459
+ */
460
+ async adjudicate(opId, verdict, actor, note) {
461
+ this.assertNotFenced();
462
+ this.store.verifyEpochFencing(this.domainId);
463
+ const op = this.store.getOperation(opId);
464
+ if (!op) {
465
+ throw new Error(`Operation "${opId}" not found`);
466
+ }
467
+ if (op.status !== 'done' || op.result?.status !== 'indeterminate') {
468
+ throw new Error(`Cannot adjudicate operation "${opId}": status must be 'indeterminate' (current status: '${op.status}', result: '${op.result?.status}')`);
469
+ }
470
+ // 2. 事实补全:驱动再做一次身份核验与残留扫描
471
+ const residualPids = [];
472
+ const pid = op.processIdentity?.pid;
473
+ const pgid = op.processIdentity?.pgid;
474
+ if (pid) {
475
+ try {
476
+ process.kill(pid, 0);
477
+ residualPids.push(pid);
478
+ }
479
+ catch { }
480
+ }
481
+ if (pgid !== undefined) {
482
+ if (this.driver && this.driver.getGroupEvidence) {
483
+ try {
484
+ const members = await this.driver.getGroupEvidence(pgid);
485
+ for (const m of members) {
486
+ if (!residualPids.includes(m.pid)) {
487
+ residualPids.push(m.pid);
488
+ }
489
+ }
490
+ }
491
+ catch { }
492
+ }
493
+ else {
494
+ try {
495
+ process.kill(-pgid, 0);
496
+ if (pid && !residualPids.includes(pid)) {
497
+ residualPids.push(pid);
498
+ }
499
+ }
500
+ catch { }
501
+ }
502
+ }
503
+ if (verdict === 'confirmed_stopped' && residualPids.length > 0) {
504
+ throw new Error(`Cannot adjudicate operation "${opId}" as 'confirmed_stopped': residual processes are still alive: [${residualPids.join(', ')}]. Use 'abandon_with_residuals' or ensure processes are terminated first.`);
505
+ }
506
+ const record = {
507
+ operationId: opId,
508
+ verdict,
509
+ actor,
510
+ note,
511
+ residualPids: residualPids.length > 0 ? residualPids : undefined,
512
+ decidedAt: new Date().toISOString(),
513
+ };
514
+ // 3. 事务提交:写入 AdjudicationRecord 与 journal 事件 OPERATION_ADJUDICATED
515
+ this.store.recordEventAndTransitionState({
516
+ domainId: this.domainId,
517
+ runId: op.runId,
518
+ operationId: opId,
519
+ type: 'OPERATION_ADJUDICATED',
520
+ payload: {
521
+ ...record,
522
+ processIdentity: op.processIdentity,
523
+ },
524
+ timestamp: record.decidedAt,
525
+ });
526
+ const updatedResult = {
527
+ ...op.result,
528
+ adjudication: record,
529
+ };
530
+ this.store.updateOperationResult(opId, updatedResult);
531
+ // 释放租约 (走内部私有释放 API,带 epoch 栅栏与 RESOURCES_RELEASED 事件)
532
+ this.internalReleaseResources(opId, op.requiredResources);
533
+ return record;
534
+ }
535
+ /**
536
+ * Run 完成与取消协议 (ARCHITECTURE §3.3)
537
+ */
538
+ reportRunCancelled(runId, reason = 'user_cancelled') {
539
+ this.assertNotFenced();
540
+ this.store.reportRunCancelled(runId, reason);
541
+ }
542
+ reportRunSucceeded(runId) {
543
+ this.assertNotFenced();
544
+ this.store.reportRunSucceeded(runId);
545
+ }
546
+ reportRunFailed(runId, reason = 'completed') {
547
+ this.assertNotFenced();
548
+ this.store.reportRunFailed(runId, reason);
264
549
  }
265
550
  /**
266
551
  * 意图登记协议:先持久化意图与占用,再允许驱动调用
@@ -275,14 +560,93 @@ export class ExecutionDomain {
275
560
  }
276
561
  catch (err) {
277
562
  // 写入失败回滚内存状态
278
- this.releaseResources(op.id, op.requiredResources);
563
+ this.internalReleaseResources(op.id, op.requiredResources);
279
564
  throw err;
280
565
  }
281
566
  }
567
+ /**
568
+ * 产物回收防线 (ARCHITECTURE §4.3 / N7)
569
+ * 只回收已终态且未被引用的产物,严格拒绝回收未终结或 indeterminate 状态的产物。
570
+ */
571
+ pruneArtifacts(filter) {
572
+ this.assertNotFenced();
573
+ const artifactsDir = path.join(this.domainPath, 'artifacts');
574
+ if (!fs.existsSync(artifactsDir)) {
575
+ return { deleted: [], retained: [] };
576
+ }
577
+ const protectedFiles = new Set();
578
+ const allOps = this.store.getAllOperations(this.domainId);
579
+ for (const op of allOps) {
580
+ const isIndet = op.result?.status === 'indeterminate' || op.result?.kind === 'indeterminate';
581
+ const isDone = op.status === 'done';
582
+ if (!isDone || isIndet) {
583
+ // 未终结或 indeterminate:保护所有可能关联的产物
584
+ protectedFiles.add(`${op.id}-stdout.log`);
585
+ protectedFiles.add(`${op.id}-stderr.log`);
586
+ if (op.outputRef)
587
+ protectedFiles.add(path.basename(op.outputRef));
588
+ if (op.result?.stdoutRef)
589
+ protectedFiles.add(path.basename(op.result.stdoutRef));
590
+ if (op.result?.stderrRef)
591
+ protectedFiles.add(path.basename(op.result.stderrRef));
592
+ }
593
+ else {
594
+ // 已终态:仅保护仍被引用的产物
595
+ if (op.outputRef)
596
+ protectedFiles.add(path.basename(op.outputRef));
597
+ if (op.result?.stdoutRef)
598
+ protectedFiles.add(path.basename(op.result.stdoutRef));
599
+ if (op.result?.stderrRef)
600
+ protectedFiles.add(path.basename(op.result.stderrRef));
601
+ }
602
+ }
603
+ const files = fs.readdirSync(artifactsDir);
604
+ const deleted = [];
605
+ const retained = [];
606
+ const now = Date.now();
607
+ for (const file of files) {
608
+ const filePath = path.join(artifactsDir, file);
609
+ if (protectedFiles.has(file)) {
610
+ retained.push(filePath);
611
+ continue;
612
+ }
613
+ if (filter?.prefix && !file.startsWith(filter.prefix)) {
614
+ retained.push(filePath);
615
+ continue;
616
+ }
617
+ if (filter?.olderThanMs !== undefined) {
618
+ try {
619
+ const stat = fs.statSync(filePath);
620
+ if (now - stat.mtimeMs < filter.olderThanMs) {
621
+ retained.push(filePath);
622
+ continue;
623
+ }
624
+ }
625
+ catch {
626
+ retained.push(filePath);
627
+ continue;
628
+ }
629
+ }
630
+ try {
631
+ fs.unlinkSync(filePath);
632
+ deleted.push(filePath);
633
+ }
634
+ catch {
635
+ retained.push(filePath);
636
+ }
637
+ }
638
+ return { deleted, retained };
639
+ }
282
640
  isClosed() {
283
641
  return this.lockFd === null;
284
642
  }
285
643
  close() {
644
+ for (const item of this.waitQueue) {
645
+ if (item.timer)
646
+ clearTimeout(item.timer);
647
+ item.reject(new Error(`Domain ${this.domainId} closed while waiting for resources`));
648
+ }
649
+ this.waitQueue = [];
286
650
  if (this.heartbeatInterval) {
287
651
  clearInterval(this.heartbeatInterval);
288
652
  this.heartbeatInterval = null;