@actiondock/core 2.5.1 → 2.7.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 (175) hide show
  1. package/dist/app/app.d.ts +14 -1
  2. package/dist/app/app.js +88 -222
  3. package/dist/app/static-index.d.ts +39 -0
  4. package/dist/app/static-index.js +164 -0
  5. package/dist/app/types.d.ts +5 -0
  6. package/dist/catalog/action-index.js +6 -4
  7. package/dist/catalog/package-catalog.js +2 -1
  8. package/dist/doctor/checks/index.d.ts +5 -0
  9. package/dist/doctor/checks/index.js +5 -0
  10. package/dist/doctor/checks/project-locate.d.ts +6 -0
  11. package/dist/doctor/checks/project-locate.js +36 -0
  12. package/dist/doctor/checks/project.d.ts +6 -0
  13. package/dist/doctor/checks/project.js +383 -0
  14. package/dist/doctor/checks/registry.d.ts +13 -0
  15. package/dist/doctor/checks/registry.js +207 -0
  16. package/dist/doctor/checks/runtime.d.ts +13 -0
  17. package/dist/doctor/checks/runtime.js +90 -0
  18. package/dist/doctor/checks/storage.d.ts +5 -0
  19. package/dist/doctor/checks/storage.js +34 -0
  20. package/dist/doctor/context.d.ts +35 -0
  21. package/dist/doctor/context.js +11 -0
  22. package/dist/doctor/doctor.d.ts +5 -0
  23. package/dist/doctor/doctor.js +33 -602
  24. package/dist/errors.d.ts +22 -0
  25. package/dist/errors.js +22 -0
  26. package/dist/execution/service.js +14 -9
  27. package/dist/export/templates.js +42 -30
  28. package/dist/host/host.d.ts +33 -0
  29. package/dist/host/host.js +222 -362
  30. package/dist/host/routing.d.ts +92 -0
  31. package/dist/host/routing.js +238 -0
  32. package/dist/host/types.d.ts +8 -0
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.js +3 -1
  35. package/dist/input/advice.d.ts +100 -0
  36. package/dist/input/advice.js +767 -0
  37. package/dist/input/describe.d.ts +106 -0
  38. package/dist/input/describe.js +176 -0
  39. package/dist/input/file-input.d.ts +54 -0
  40. package/dist/input/file-input.js +124 -0
  41. package/dist/input/flat-decode.d.ts +11 -0
  42. package/dist/input/flat-decode.js +13 -0
  43. package/dist/input/flat-errors.d.ts +72 -0
  44. package/dist/input/flat-errors.js +87 -0
  45. package/dist/input/flat-materializer.d.ts +29 -0
  46. package/dist/input/flat-materializer.js +179 -0
  47. package/dist/input/flat-parser.d.ts +50 -0
  48. package/dist/input/flat-parser.js +169 -0
  49. package/dist/input/flat-predicates.d.ts +34 -0
  50. package/dist/input/flat-predicates.js +50 -0
  51. package/dist/input/index.d.ts +14 -0
  52. package/dist/input/index.js +14 -0
  53. package/dist/input/input-resolver.d.ts +63 -0
  54. package/dist/input/input-resolver.js +254 -0
  55. package/dist/input/json-depth-scanner.d.ts +17 -0
  56. package/dist/input/json-depth-scanner.js +49 -0
  57. package/dist/input/metadata.d.ts +99 -0
  58. package/dist/input/metadata.js +99 -0
  59. package/dist/input/stdin-input.d.ts +26 -0
  60. package/dist/input/stdin-input.js +125 -0
  61. package/dist/input/utf8.d.ts +19 -0
  62. package/dist/input/utf8.js +36 -0
  63. package/dist/input/validation-mapper.d.ts +27 -0
  64. package/dist/input/validation-mapper.js +144 -0
  65. package/dist/ipc/host.js +38 -0
  66. package/dist/json/index.d.ts +1 -0
  67. package/dist/json/index.js +1 -0
  68. package/dist/json/value-validator.d.ts +88 -0
  69. package/dist/json/value-validator.js +330 -0
  70. package/dist/platform/types.d.ts +7 -0
  71. package/dist/process/control-arbiter.d.ts +107 -0
  72. package/dist/process/control-arbiter.js +251 -0
  73. package/dist/process/diagnostics-sink.d.ts +26 -0
  74. package/dist/process/diagnostics-sink.js +35 -0
  75. package/dist/process/index.d.ts +7 -0
  76. package/dist/process/index.js +7 -0
  77. package/dist/process/input-dispatcher.d.ts +88 -0
  78. package/dist/process/input-dispatcher.js +240 -0
  79. package/dist/process/managed-record.d.ts +78 -0
  80. package/dist/process/managed-record.js +1 -0
  81. package/dist/process/process-manager.d.ts +15 -56
  82. package/dist/process/process-manager.js +119 -707
  83. package/dist/process/reservation-table.d.ts +39 -0
  84. package/dist/process/reservation-table.js +80 -0
  85. package/dist/process/run-executor.d.ts +16 -0
  86. package/dist/process/run-executor.js +155 -0
  87. package/dist/process/terminal-output-cache.d.ts +66 -0
  88. package/dist/process/terminal-output-cache.js +132 -0
  89. package/dist/profile/client-actions.d.ts +56 -0
  90. package/dist/profile/client-actions.js +215 -0
  91. package/dist/profile/client-config.d.ts +20 -0
  92. package/dist/profile/client-config.js +51 -0
  93. package/dist/profile/client-health.d.ts +14 -0
  94. package/dist/profile/client-health.js +69 -0
  95. package/dist/profile/client-playbooks.d.ts +19 -0
  96. package/dist/profile/client-playbooks.js +36 -0
  97. package/dist/profile/client-query.d.ts +30 -0
  98. package/dist/profile/client-query.js +70 -0
  99. package/dist/profile/client-runs.d.ts +42 -0
  100. package/dist/profile/client-runs.js +58 -0
  101. package/dist/profile/client-state.d.ts +56 -0
  102. package/dist/profile/client-state.js +83 -0
  103. package/dist/profile/client-transport.d.ts +61 -0
  104. package/dist/profile/client-transport.js +156 -0
  105. package/dist/profile/client.d.ts +23 -142
  106. package/dist/profile/client.js +28 -588
  107. package/dist/profile/manager.js +1 -1
  108. package/dist/project/closure.d.ts +4 -4
  109. package/dist/project/closure.js +11 -5
  110. package/dist/project/init.js +2 -2
  111. package/dist/project/loader.d.ts +0 -25
  112. package/dist/project/loader.js +1 -154
  113. package/dist/project/transactions.d.ts +12 -14
  114. package/dist/project/transactions.js +60 -836
  115. package/dist/registry/registry.js +6 -4
  116. package/dist/registry/resolve.js +1 -1
  117. package/dist/runtime/action-registry.d.ts +45 -0
  118. package/dist/runtime/action-registry.js +108 -0
  119. package/dist/runtime/clock.d.ts +5 -17
  120. package/dist/runtime/clock.js +5 -15
  121. package/dist/runtime/context.d.ts +3 -0
  122. package/dist/runtime/context.js +17 -56
  123. package/dist/runtime/index.d.ts +3 -0
  124. package/dist/runtime/index.js +3 -0
  125. package/dist/runtime/package-runner-factory.d.ts +82 -0
  126. package/dist/runtime/package-runner-factory.js +186 -0
  127. package/dist/runtime/run-persistence.d.ts +85 -0
  128. package/dist/runtime/run-persistence.js +111 -0
  129. package/dist/runtime/runner.d.ts +25 -45
  130. package/dist/runtime/runner.js +130 -354
  131. package/dist/runtime/standalone.d.ts +25 -2
  132. package/dist/runtime/standalone.js +305 -87
  133. package/dist/schema/index.d.ts +1 -0
  134. package/dist/schema/index.js +1 -0
  135. package/dist/schema/validator.d.ts +10 -0
  136. package/dist/schema/validator.js +48 -54
  137. package/dist/server/dispatcher.d.ts +8 -3
  138. package/dist/server/dispatcher.js +16 -40
  139. package/dist/server/routes/runs.js +6 -4
  140. package/dist/server/security.d.ts +1 -7
  141. package/dist/server/security.js +2 -13
  142. package/dist/storage/clock.d.ts +25 -0
  143. package/dist/storage/clock.js +23 -0
  144. package/dist/storage/data-dir-lock.d.ts +3 -36
  145. package/dist/storage/data-dir-lock.js +48 -881
  146. package/dist/storage/index.d.ts +8 -1
  147. package/dist/storage/index.js +8 -2
  148. package/dist/storage/lazy.js +1 -0
  149. package/dist/storage/lock-core.d.ts +162 -0
  150. package/dist/storage/lock-core.js +885 -0
  151. package/dist/storage/sqlite.d.ts +19 -6
  152. package/dist/storage/sqlite.js +81 -89
  153. package/dist/storage/types.d.ts +27 -5
  154. package/dist/storage/types.js +21 -0
  155. package/dist/storage/utils.d.ts +22 -0
  156. package/dist/storage/utils.js +34 -0
  157. package/dist/target/local.d.ts +1 -0
  158. package/dist/target/local.js +23 -17
  159. package/dist/target/remote-errors.d.ts +21 -0
  160. package/dist/target/remote-errors.js +51 -0
  161. package/dist/target/remote-polling.d.ts +35 -0
  162. package/dist/target/remote-polling.js +104 -0
  163. package/dist/target/remote.d.ts +4 -0
  164. package/dist/target/remote.js +108 -301
  165. package/dist/target/sse-parser.d.ts +35 -0
  166. package/dist/target/sse-parser.js +163 -0
  167. package/dist/target/target.js +2 -0
  168. package/dist/target/types.d.ts +6 -0
  169. package/dist/utils/index.d.ts +1 -0
  170. package/dist/utils/index.js +1 -0
  171. package/dist/utils/net.d.ts +10 -0
  172. package/dist/utils/net.js +16 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
@@ -1,10 +1,16 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { decodeBytes, encodeBytes, } from "@actiondock/sdk";
3
- import { ACCESS_DENIED, CONTROL_BUSY, CONTROL_EXPIRED, CONTROL_REVOKED, INPUT_CLOSED, INPUT_OUTCOME_UNKNOWN, INPUT_VALIDATION_FAILED, NOT_FOUND, PROCESS_CANCELLED, PROCESS_LOST, PROCESS_QUARANTINED, PROCESS_SPAWN_ERROR, PROCESS_TIMEOUT, QUEUE_FULL, QUOTA_EXCEEDED, REQUEST_CONFLICT, SERVER_ERROR, UNSUPPORTED_CAPABILITY, INVALID_CURSOR, OUTPUT_UNAVAILABLE, ProcessError, } from "../errors.js";
3
+ import { ACCESS_DENIED, CONTROL_BUSY, CONTROL_REVOKED, INPUT_VALIDATION_FAILED, NOT_FOUND, PROCESS_CANCELLED, PROCESS_LOST, PROCESS_QUARANTINED, PROCESS_SPAWN_ERROR, QUOTA_EXCEEDED, REQUEST_CONFLICT, SERVER_ERROR, UNSUPPORTED_CAPABILITY, INVALID_CURSOR, OUTPUT_UNAVAILABLE, ProcessError, } from "../errors.js";
4
4
  import { parseCursor, compareCursorPos } from "./cursor.js";
5
5
  import { ContextProcessAPI } from "./context-process.js";
6
+ import { ControlArbiter } from "./control-arbiter.js";
7
+ import { InputDispatcher } from "./input-dispatcher.js";
8
+ import { DiagnosticsSink } from "./diagnostics-sink.js";
6
9
  import { MemoryProcessMetadataStore, } from "./metadata-store.js";
7
10
  import { ProcessOutputLog } from "./output-log.js";
11
+ import { ReservationTable, } from "./reservation-table.js";
12
+ import { RunExecutor } from "./run-executor.js";
13
+ import { TerminalOutputCache } from "./terminal-output-cache.js";
8
14
  /**
9
15
  * 格式化生成作用域唯一隔离字符串。
10
16
  */
@@ -95,12 +101,6 @@ function toStoredProcessRecord(owner, info, startRequestId) {
95
101
  startRequestId,
96
102
  };
97
103
  }
98
- /**
99
- * 格式化同步幂等预占键。
100
- */
101
- function formatReservationKey(key, operation) {
102
- return `${key.hostEpoch}:${key.scope}:${key.processId ?? ""}:${operation}:${key.requestId}`;
103
- }
104
104
  /**
105
105
  * 校验正整数毫秒时长参数,非法时抛出参数错误。
106
106
  */
@@ -129,18 +129,21 @@ export class ProcessManager {
129
129
  defaultLimits;
130
130
  drainDeadlineMs;
131
131
  terminalLogRetentionMs;
132
- logger;
133
132
  processes = new Map();
134
- /** 已驱逐进程的输出日志缓存:附带时间戳支撑 TTL 过期回收与 LRU 淘汰 */
135
- evictedOutputLogs = new Map();
136
- /** 已淘汰输出日志的墓碑缓存:防止因日志淘汰静默丢失输出数据,支撑缺口明确告知与不可用异常 */
137
- evictedOutputTombstones = new Map();
133
+ /** 内部诊断日志汇聚器:未注入 logger 时保留最近的持久化与驱动错误,避免静默吞没异常 */
134
+ diagnosticsSink;
138
135
  /** 同步幂等预占表:以复合键在异步落盘窗口内锁定并发重复请求 */
139
- requestReservations = new Map();
136
+ reservationTable;
137
+ /** 终态输出日志保留缓存:附带 TTL 过期回收与 LRU 淘汰,及墓碑登记 */
138
+ terminalOutputCache;
139
+ /** 一次性 run 执行器:仅依赖驱动、错误映射与输入校验,不触碰进程注册表 */
140
+ runExecutor;
141
+ /** 控制权仲裁器:独占控制权状态机全部推进路径的唯一持有者 */
142
+ controlArbiter;
143
+ /** 输入调度器:输入队列入队、串行调度与接管原语的唯一持有者 */
144
+ inputDispatcher;
140
145
  initPromise;
141
146
  isShutdown = false;
142
- /** 内部诊断日志:未注入 logger 时保留最近的持久化与驱动错误,避免静默吞没异常 */
143
- diagnostics = [];
144
147
  constructor(options) {
145
148
  this.hostEpoch = options.hostEpoch ?? `epoch-${Date.now()}-${randomUUID().slice(0, 8)}`;
146
149
  this.driver = options.driver;
@@ -162,7 +165,37 @@ export class ProcessManager {
162
165
  };
163
166
  this.drainDeadlineMs = options.drainDeadlineMs ?? 5000;
164
167
  this.terminalLogRetentionMs = options.terminalLogRetentionMs ?? 10 * 60 * 1000;
165
- this.logger = options.logger;
168
+ this.diagnosticsSink = new DiagnosticsSink({ logger: options.logger });
169
+ this.reservationTable = new ReservationTable();
170
+ this.terminalOutputCache = new TerminalOutputCache({
171
+ retentionMs: this.terminalLogRetentionMs,
172
+ maxHostBufferBytes: this.quotas.maxOutputBufferBytesPerHost,
173
+ });
174
+ this.runExecutor = new RunExecutor(this.driver);
175
+ this.controlArbiter = new ControlArbiter({
176
+ persistState: (processId, patch) => this.persistState(processId, patch),
177
+ persistGrantReceipt: (grant, wait, proc) => this.persistReceipt({
178
+ hostEpoch: this.hostEpoch,
179
+ scope: proc.scope,
180
+ processId: proc.info.id,
181
+ requestId: wait.requestId,
182
+ }, grant, hashRequestPayload({ waitMs: wait.waitMs, ttlMs: wait.ttlMs })),
183
+ refreshIdleTimer: (proc) => this.refreshIdleTimer(proc),
184
+ quarantineProcess: (owner, processId, token, reason) => this.quarantineProcess(owner, processId, token, reason),
185
+ countHostAcquireWaiters: () => this.countHostAcquireWaiters(),
186
+ maxWaitersPerProcess: this.quotas.maxWaitersPerProcess,
187
+ maxWaitersPerHost: this.quotas.maxWaitersPerHost,
188
+ });
189
+ this.inputDispatcher = new InputDispatcher({
190
+ persistReceipt: (key, receipt, payloadHash) => this.persistReceipt(key, receipt, payloadHash),
191
+ persistState: (processId, patch) => this.persistState(processId, patch),
192
+ quarantineProcess: (owner, processId, token, reason) => this.quarantineProcess(owner, processId, token, reason),
193
+ refreshIdleTimer: (proc) => this.refreshIdleTimer(proc),
194
+ recordDiagnostic: (message, err) => this.recordDiagnostic(message, err),
195
+ countHostPendingInputBytes: () => this.countHostPendingInputBytes(),
196
+ maxPendingQueueBytesPerProcess: this.quotas.maxPendingQueueBytesPerProcess,
197
+ maxPendingQueueBytesPerHost: this.quotas.maxPendingQueueBytesPerHost,
198
+ }, this.controlArbiter);
166
199
  }
167
200
  /**
168
201
  * 初始化宿主环境,原子性收敛旧宿主遗留的非终态进程。
@@ -192,19 +225,13 @@ export class ProcessManager {
192
225
  * 记录内部诊断信息:优先写入注入的 logger,缺失时保留在内存环形缓冲区。
193
226
  */
194
227
  recordDiagnostic(message, err) {
195
- const detail = err instanceof Error ? `${err.name}: ${err.message}` : err !== undefined ? String(err) : "";
196
- const line = detail ? `${message} (${detail})` : message;
197
- this.diagnostics.push(`${new Date().toISOString()} ${line}`);
198
- if (this.diagnostics.length > 100) {
199
- this.diagnostics.shift();
200
- }
201
- this.logger?.warn("[ProcessManager] " + line);
228
+ this.diagnosticsSink.record(message, err);
202
229
  }
203
230
  /**
204
231
  * 获取最近的内部诊断日志快照(最近条目在前)。
205
232
  */
206
233
  get recentDiagnostics() {
207
- return [...this.diagnostics].reverse();
234
+ return this.diagnosticsSink.recent();
208
235
  }
209
236
  /**
210
237
  * 异步落盘进程状态:捕获并记录持久化失败,杜绝静默吞没异常。
@@ -230,52 +257,16 @@ export class ProcessManager {
230
257
  }
231
258
  /**
232
259
  * 同步预占幂等请求:跨 await 的检查与落盘窗口内锁定同复合键并发调用。
233
- * 返回 undefined 表示预占成功,调用方继续异步路径并在完成时调用 commit/cancel
234
- * 返回 RequestReservation 表示已有同请求进行中,调用方可等待其结果。
235
- * 若已有请求负载或操作类型不同,立即抛出 REQUEST_CONFLICT 杜绝混用结果。
260
+ * 实现委托给独立持有的预占表,语义详见 ReservationTable.reserve
236
261
  */
237
262
  reserveRequest(key, payloadHash, operation) {
238
- const reservationKey = formatReservationKey(key, operation);
239
- const existing = this.requestReservations.get(reservationKey);
240
- if (existing) {
241
- if (existing.payloadHash !== payloadHash || existing.operation !== operation) {
242
- throw new ProcessError(REQUEST_CONFLICT, "Request conflict: identical requestId with different payload", { requestId: key.requestId });
243
- }
244
- return existing;
245
- }
246
- let resolveFn = () => { };
247
- let rejectFn = () => { };
248
- const promise = new Promise((res, rej) => {
249
- resolveFn = res;
250
- rejectFn = rej;
251
- });
252
- // 预占 promise 可能永远无人等待:预先挂接空捕获,避免拒绝时触发 unhandledRejection
253
- promise.catch(() => { });
254
- const reservation = {
255
- payloadHash,
256
- operation,
257
- promise,
258
- resolve: resolveFn,
259
- reject: rejectFn,
260
- };
261
- this.requestReservations.set(reservationKey, reservation);
262
- return undefined;
263
+ return this.reservationTable.reserve(key, payloadHash, operation);
263
264
  }
264
265
  commitReservation(key, operation, value) {
265
- const reservationKey = formatReservationKey(key, operation);
266
- const reservation = this.requestReservations.get(reservationKey);
267
- if (reservation) {
268
- this.requestReservations.delete(reservationKey);
269
- reservation.resolve(value);
270
- }
266
+ this.reservationTable.commit(key, operation, value);
271
267
  }
272
268
  rejectReservation(key, operation, err) {
273
- const reservationKey = formatReservationKey(key, operation);
274
- const reservation = this.requestReservations.get(reservationKey);
275
- if (reservation) {
276
- this.requestReservations.delete(reservationKey);
277
- reservation.reject(err);
278
- }
269
+ this.reservationTable.reject(key, operation, err);
279
270
  }
280
271
  /**
281
272
  * 停止全部受管进程、清理全部定时器并置为关闭态,供宿主优雅退出使用。
@@ -319,10 +310,7 @@ export class ProcessManager {
319
310
  proc.drainTimer = undefined;
320
311
  }
321
312
  }
322
- for (const [reservationKey, res] of Array.from(this.requestReservations.entries())) {
323
- this.requestReservations.delete(reservationKey);
324
- res.reject(new ProcessError(PROCESS_CANCELLED, "Process manager has been shut down"));
325
- }
313
+ this.reservationTable.rejectAll(new ProcessError(PROCESS_CANCELLED, "Process manager has been shut down"));
326
314
  }
327
315
  assertNotShutdown() {
328
316
  if (this.isShutdown) {
@@ -602,75 +590,13 @@ export class ProcessManager {
602
590
  this.commitReservation(key, "acquire", cached);
603
591
  return cached;
604
592
  }
605
- // 若控制权空闲且无等待者,直接授予
606
- if (proc.info.control === "free" && proc.acquireWaiters.length === 0) {
607
- const grant = this.grantControl(proc, input.requestId, input.ttlMs, runId);
593
+ // 控制权仲裁:空闲且无等待者时直接授予,否则 FIFO 排队等待
594
+ // 排队路径凭据由唤醒方落盘;原请求 resolve 后在此处结算预约,保持两阶段时序不变
595
+ const outcome = this.controlArbiter.tryAcquire(proc, { requestId: input.requestId, waitMs: input.waitMs, ttlMs: input.ttlMs }, call, runId);
596
+ const grant = await outcome.waiter;
597
+ if (outcome.granted) {
608
598
  await this.metadataStore.recordRequest(key, grant, payloadHash);
609
- this.commitReservation(key, "acquire", grant);
610
- return grant;
611
599
  }
612
- // 控制权已被持有,检查等待队列配额并进入 FIFO 排队
613
- if (proc.acquireWaiters.length >= this.quotas.maxWaitersPerProcess) {
614
- throw new ProcessError(QUOTA_EXCEEDED, "Process acquire waiter quota exceeded", {
615
- limit: this.quotas.maxWaitersPerProcess,
616
- });
617
- }
618
- const totalHostWaiters = this.countHostAcquireWaiters();
619
- if (totalHostWaiters >= this.quotas.maxWaitersPerHost) {
620
- throw new ProcessError(QUOTA_EXCEEDED, "Host acquire waiter quota exceeded", {
621
- limit: this.quotas.maxWaitersPerHost,
622
- });
623
- }
624
- // 排队路径:由 wakeNextAcquireWaiter 在授予时落盘凭据并结算预占
625
- const grant = await new Promise((resolve, reject) => {
626
- let timer;
627
- const waiter = {
628
- requestId: input.requestId,
629
- waitMs: input.waitMs,
630
- ttlMs: input.ttlMs,
631
- runId,
632
- resolve: (granted) => {
633
- cleanup();
634
- resolve(granted);
635
- },
636
- reject: (err) => {
637
- cleanup();
638
- reject(err);
639
- },
640
- };
641
- const cleanup = () => {
642
- if (timer)
643
- clearTimeout(timer);
644
- const idx = proc.acquireWaiters.indexOf(waiter);
645
- if (idx !== -1) {
646
- proc.acquireWaiters.splice(idx, 1);
647
- }
648
- if (call?.signal && waiter.onAbort) {
649
- call.signal.removeEventListener("abort", waiter.onAbort);
650
- }
651
- };
652
- if (call?.signal) {
653
- if (call.signal.aborted) {
654
- reject(call.signal.reason ?? new ProcessError(PROCESS_CANCELLED, "Acquire cancelled"));
655
- return;
656
- }
657
- waiter.onAbort = () => {
658
- cleanup();
659
- reject(call.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Acquire cancelled"));
660
- };
661
- call.signal.addEventListener("abort", waiter.onAbort, { once: true });
662
- }
663
- timer = setTimeout(() => {
664
- cleanup();
665
- reject(new ProcessError(CONTROL_BUSY, "Timed out waiting for process control", {
666
- waitMs: input.waitMs,
667
- }));
668
- }, input.waitMs);
669
- if (typeof timer?.unref === "function") {
670
- timer.unref();
671
- }
672
- proc.acquireWaiters.push(waiter);
673
- });
674
600
  this.commitReservation(key, "acquire", grant);
675
601
  return grant;
676
602
  }
@@ -686,36 +612,9 @@ export class ProcessManager {
686
612
  checkPositiveDurationMs(ttlMs, "ttlMs");
687
613
  await this.ensureInitialized();
688
614
  const proc = await this.getOrLoadProcess(owner, id);
689
- if (proc.info.control === "quarantined") {
690
- throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
691
- }
692
- if (proc.info.control !== "held" || !proc.currentGrant) {
693
- throw new ProcessError(CONTROL_REVOKED, "Process control is not currently held");
694
- }
695
- if (proc.currentGrant.token !== token) {
696
- throw new ProcessError(ACCESS_DENIED, "Invalid control token");
697
- }
698
- if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
699
- throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
700
- }
701
- if (proc.ttlTimer) {
702
- clearTimeout(proc.ttlTimer);
703
- }
704
- const expiresAt = new Date(Date.now() + ttlMs).toISOString();
705
- proc.currentGrant.ttlMs = ttlMs;
706
- proc.currentGrant.expiresAt = expiresAt;
707
- proc.ttlTimer = setTimeout(() => {
708
- this.handleGrantTtlExpired(proc);
709
- }, ttlMs);
710
- if (typeof proc.ttlTimer.unref === "function") {
711
- proc.ttlTimer.unref();
712
- }
713
- // 延长控制权刷新 idleTimeout
714
- this.refreshIdleTimer(proc);
715
- return {
716
- token: proc.currentGrant.token,
717
- expiresAt,
718
- };
615
+ // 提交前校验持有凭据有效性,随后由仲裁器清理旧定时器并设置新定时器
616
+ this.controlArbiter.validateHeldGrant(proc, token);
617
+ return this.controlArbiter.renewGrant(proc, ttlMs);
719
618
  }
720
619
  /**
721
620
  * 显式释放控制令牌,允许后续控制者申请。
@@ -723,6 +622,7 @@ export class ProcessManager {
723
622
  async release(owner, id, token, call) {
724
623
  await this.ensureInitialized();
725
624
  const proc = await this.getOrLoadProcess(owner, id);
625
+ // 提交前校验持有凭据有效性
726
626
  if (proc.info.control === "quarantined") {
727
627
  throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
728
628
  }
@@ -736,18 +636,8 @@ export class ProcessManager {
736
636
  if (proc.inputQueue.length > 0) {
737
637
  throw new ProcessError(CONTROL_BUSY, "Cannot release control while operations are pending in queue");
738
638
  }
739
- if (proc.ttlTimer) {
740
- clearTimeout(proc.ttlTimer);
741
- proc.ttlTimer = undefined;
742
- }
743
- proc.currentGrant = undefined;
744
- proc.info.control = "free";
745
- await this.persistState(id, {
746
- control: "free",
747
- controlState: "free",
748
- });
749
- // 正常 release 唤醒下一个等待者
750
- this.wakeNextAcquireWaiter(proc);
639
+ // 仲裁器完成清理定时器、凭据置空与唯一唤醒下一个等待者
640
+ this.controlArbiter.releaseGrant(proc);
751
641
  }
752
642
  /**
753
643
  * 向受管进程输入流写入原始字节数据。
@@ -780,56 +670,17 @@ export class ProcessManager {
780
670
  this.commitReservation(key, "write", cached);
781
671
  return cached;
782
672
  }
783
- // 入队前校验授权与控制状态
784
- this.checkOperationAuth(proc, input.token);
785
- if (proc.inputClosed) {
786
- throw new ProcessError(INPUT_CLOSED, "Input stream is closed");
787
- }
673
+ // 输入调度器同步阶段完成授权校验与队列容量预扣,异步阶段落盘入队
788
674
  const rawBytes = decodeBytes(input.data);
789
- const dataSize = rawBytes.byteLength;
790
- // 待写入队列容量检查:在任何 await 前同步原子校验并预占配额
791
- if (proc.pendingInputBytes + dataSize > this.quotas.maxPendingQueueBytesPerProcess) {
792
- throw new ProcessError(QUEUE_FULL, "Process pending input queue limit exceeded", {
793
- limit: this.quotas.maxPendingQueueBytesPerProcess,
794
- });
795
- }
796
- const totalHostPending = this.countHostPendingInputBytes();
797
- if (totalHostPending + dataSize > this.quotas.maxPendingQueueBytesPerHost) {
798
- throw new ProcessError(QUEUE_FULL, "Host pending input queue limit exceeded", {
799
- limit: this.quotas.maxPendingQueueBytesPerHost,
800
- });
801
- }
802
- // 同步占位预扣队列配额,杜绝并发 await 窗口穿透
803
- proc.pendingInputBytes += dataSize;
804
- let pendingBytesCommitted = false;
805
- try {
806
- const receipt = {
807
- requestId: input.requestId,
808
- state: "queued",
809
- };
810
- await this.metadataStore.recordRequest(key, receipt, payloadHash);
811
- proc.inputQueue.push({
812
- type: "write",
813
- requestId: input.requestId,
814
- token: input.token,
815
- bytes: rawBytes,
816
- receipt,
817
- key,
818
- payloadHash,
819
- cancelEpoch: proc.cancelEpoch,
820
- });
821
- pendingBytesCommitted = true;
822
- // 异步调度推进
823
- queueMicrotask(() => void this.dispatchNext(proc));
824
- const queued = { ...receipt };
825
- this.commitReservation(key, "write", queued);
826
- return queued;
827
- }
828
- finally {
829
- if (!pendingBytesCommitted) {
830
- proc.pendingInputBytes = Math.max(0, proc.pendingInputBytes - dataSize);
831
- }
832
- }
675
+ const queued = await this.inputDispatcher.enqueueWrite(proc, {
676
+ requestId: input.requestId,
677
+ token: input.token,
678
+ data: rawBytes,
679
+ key,
680
+ payloadHash,
681
+ });
682
+ this.commitReservation(key, "write", queued);
683
+ return queued;
833
684
  }
834
685
  finally {
835
686
  // 成功路径已在 commit 中移除预占;此处仅对异常路径释放并唤醒等待方
@@ -867,9 +718,7 @@ export class ProcessManager {
867
718
  this.commitReservation(key, "control", cached);
868
719
  return cached;
869
720
  }
870
- // 入队前校验授权与控制状态
871
- this.checkOperationAuth(proc, input.token);
872
- // 校验能力支持
721
+ // 校验能力支持(能力归属管理器,在调度器入队前完成)
873
722
  if (input.action.type === "input-eof" && !proc.info.capabilities.inputEOF) {
874
723
  throw new ProcessError(UNSUPPORTED_CAPABILITY, "Driver does not support input-eof action");
875
724
  }
@@ -881,23 +730,14 @@ export class ProcessManager {
881
730
  (!proc.info.capabilities.resize || proc.info.io.mode !== "pty")) {
882
731
  throw new ProcessError(UNSUPPORTED_CAPABILITY, "Driver does not support resize or IO mode is not pty");
883
732
  }
884
- const receipt = {
885
- requestId: input.requestId,
886
- state: "queued",
887
- };
888
- await this.metadataStore.recordRequest(key, receipt, payloadHash);
889
- proc.inputQueue.push({
890
- type: "control",
733
+ // 输入调度器完成授权校验、落盘与入队
734
+ const queued = await this.inputDispatcher.enqueueControl(proc, {
891
735
  requestId: input.requestId,
892
736
  token: input.token,
893
737
  action: input.action,
894
- receipt,
895
738
  key,
896
739
  payloadHash,
897
- cancelEpoch: proc.cancelEpoch,
898
740
  });
899
- queueMicrotask(() => void this.dispatchNext(proc));
900
- const queued = { ...receipt };
901
741
  this.commitReservation(key, "control", queued);
902
742
  return queued;
903
743
  }
@@ -932,7 +772,7 @@ export class ProcessManager {
932
772
  const proc = await this.getOrLoadProcess(owner, id);
933
773
  if (proc.outputUnavailable) {
934
774
  const requestedPos = parseCursor(input.cursor, proc.info.hostEpoch, id);
935
- const tombstone = proc.outputTombstone ?? this.evictedOutputTombstones.get(id);
775
+ const tombstone = proc.outputTombstone ?? this.terminalOutputCache.getTombstone(id);
936
776
  if (tombstone) {
937
777
  const tailPos = parseCursor(tombstone.tailCursor, proc.info.hostEpoch, id);
938
778
  const cmp = compareCursorPos(requestedPos, tailPos);
@@ -1002,11 +842,7 @@ export class ProcessManager {
1002
842
  return { ...proc.info };
1003
843
  }
1004
844
  // 撤销控制权与定时器
1005
- if (proc.ttlTimer) {
1006
- clearTimeout(proc.ttlTimer);
1007
- proc.ttlTimer = undefined;
1008
- }
1009
- proc.currentGrant = undefined;
845
+ this.controlArbiter.disposeProcess(proc);
1010
846
  if (proc.idleTimer) {
1011
847
  clearTimeout(proc.idleTimer);
1012
848
  proc.idleTimer = undefined;
@@ -1019,21 +855,13 @@ export class ProcessManager {
1019
855
  clearTimeout(proc.drainTimer);
1020
856
  proc.drainTimer = undefined;
1021
857
  }
1022
- // 递增取消纪元:接管输入队列,使挂起中的 dispatch 放弃过期结算
1023
- proc.cancelEpoch = (proc.cancelEpoch ?? 0) + 1;
1024
- // 取消待 dispatch 输入
1025
- for (const op of proc.inputQueue) {
1026
- op.receipt.state = "failed";
1027
- op.receipt.errorCode = PROCESS_CANCELLED;
1028
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1029
- }
1030
- proc.inputQueue = [];
1031
- proc.pendingInputBytes = 0;
1032
- // 拒绝排队等待者
1033
- while (proc.acquireWaiters.length > 0) {
1034
- const waiter = proc.acquireWaiters.shift();
1035
- waiter.reject(new ProcessError(CONTROL_REVOKED, "Process was stopped"));
1036
- }
858
+ // 调度器同步原子接管输入队列:纪元递增、标记失败、登记落盘、清空与字节归零
859
+ this.inputDispatcher.takeoverQueue(proc, PROCESS_CANCELLED);
860
+ // 终态拒绝全部排队等待者
861
+ this.controlArbiter.revokeAllWaiters(proc, {
862
+ code: CONTROL_REVOKED,
863
+ message: "Process was stopped",
864
+ });
1037
865
  proc.info.endReason = proc.info.endReason ?? "requested";
1038
866
  proc.info.state = "stopping";
1039
867
  proc.info.control = "closed";
@@ -1078,21 +906,13 @@ export class ProcessManager {
1078
906
  proc.ttlTimer = undefined;
1079
907
  }
1080
908
  proc.currentGrant = undefined;
1081
- // 递增取消纪元:接管输入队列,使挂起中的 dispatch 放弃过期结算
1082
- proc.cancelEpoch = (proc.cancelEpoch ?? 0) + 1;
1083
- // 取消队列中所有待 dispatch 操作
1084
- for (const op of proc.inputQueue) {
1085
- op.receipt.state = "failed";
1086
- op.receipt.errorCode = CONTROL_REVOKED;
1087
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1088
- }
1089
- proc.inputQueue = [];
1090
- proc.pendingInputBytes = 0;
1091
- // 拒绝排队等待者
1092
- while (proc.acquireWaiters.length > 0) {
1093
- const waiter = proc.acquireWaiters.shift();
1094
- waiter.reject(new ProcessError(PROCESS_QUARANTINED, reason || "Process entered quarantined state"));
1095
- }
909
+ // 调度器同步原子接管输入队列:纪元递增、标记失败、登记落盘、清空与字节归零
910
+ this.inputDispatcher.takeoverQueue(proc, CONTROL_REVOKED);
911
+ // 终态拒绝全部排队等待者
912
+ this.controlArbiter.revokeAllWaiters(proc, {
913
+ code: PROCESS_QUARANTINED,
914
+ message: reason || "Process entered quarantined state",
915
+ });
1096
916
  await this.persistState(processId, {
1097
917
  control: "quarantined",
1098
918
  controlState: "quarantined",
@@ -1100,340 +920,13 @@ export class ProcessManager {
1100
920
  }
1101
921
  /**
1102
922
  * 一次性运行外部命令,收集有限输出并支持协作式取消与超时回收。
923
+ * 实现委托给独立持有的一次性 run 执行器,不触碰受管进程注册表。
1103
924
  */
1104
925
  async run(owner, input, call) {
1105
926
  checkOwnerAuthorized(owner, owner);
1106
927
  await this.ensureInitialized();
1107
928
  this.assertNotShutdown();
1108
- const ioMode = input.spec.io?.mode ?? "pipe";
1109
- if (ioMode !== "pipe") {
1110
- throw new ProcessError(UNSUPPORTED_CAPABILITY, "Run requires IO mode to be 'pipe'");
1111
- }
1112
- const effectiveSpec = {
1113
- ...input.spec,
1114
- io: {
1115
- ...input.spec.io,
1116
- mode: "pipe",
1117
- },
1118
- };
1119
- const runProcessId = `run-${randomUUID()}`;
1120
- const chunks = [];
1121
- let totalBytes = 0;
1122
- let truncated = false;
1123
- let timedOut = false;
1124
- let cancelled = false;
1125
- let resolveExit;
1126
- let rejectError;
1127
- const exitPromise = new Promise((resolve, reject) => {
1128
- resolveExit = resolve;
1129
- rejectError = reject;
1130
- });
1131
- let driverHandle;
1132
- const callbacks = {
1133
- onOutput: (stream, data) => {
1134
- if (truncated)
1135
- return;
1136
- if (stream !== "stdout" && stream !== "stderr")
1137
- return;
1138
- const remaining = input.maxOutputBytes - totalBytes;
1139
- if (remaining <= 0) {
1140
- truncated = true;
1141
- if (driverHandle) {
1142
- void driverHandle.terminate(1000);
1143
- }
1144
- return;
1145
- }
1146
- if (data.byteLength > remaining) {
1147
- const slice = data.subarray(0, remaining);
1148
- chunks.push({
1149
- stream,
1150
- data: encodeBytes(slice),
1151
- });
1152
- totalBytes += slice.byteLength;
1153
- truncated = true;
1154
- if (driverHandle) {
1155
- void driverHandle.terminate(1000);
1156
- }
1157
- }
1158
- else {
1159
- chunks.push({
1160
- stream,
1161
- data: encodeBytes(data),
1162
- });
1163
- totalBytes += data.byteLength;
1164
- }
1165
- },
1166
- onExit: (exit) => {
1167
- resolveExit(exit);
1168
- },
1169
- onError: (err) => {
1170
- rejectError(err);
1171
- },
1172
- };
1173
- // 对齐 start 防御顺序:派生前检查取消信号,已中止直接抛出取消错误不再派生进程
1174
- if (call?.signal?.aborted) {
1175
- throw call.signal.reason instanceof ProcessError
1176
- ? call.signal.reason
1177
- : new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
1178
- }
1179
- driverHandle = await this.driver.spawn(runProcessId, effectiveSpec, callbacks);
1180
- let timeoutTimer;
1181
- if (input.timeoutMs > 0) {
1182
- timeoutTimer = setTimeout(() => {
1183
- timedOut = true;
1184
- if (driverHandle) {
1185
- void driverHandle.terminate(1000);
1186
- }
1187
- }, input.timeoutMs);
1188
- if (typeof timeoutTimer?.unref === "function") {
1189
- timeoutTimer.unref();
1190
- }
1191
- }
1192
- let onAbort;
1193
- if (call?.signal) {
1194
- if (call.signal.aborted) {
1195
- if (timeoutTimer)
1196
- clearTimeout(timeoutTimer);
1197
- if (driverHandle)
1198
- void driverHandle.terminate(1000);
1199
- throw call.signal.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
1200
- }
1201
- onAbort = () => {
1202
- cancelled = true;
1203
- if (timeoutTimer)
1204
- clearTimeout(timeoutTimer);
1205
- if (driverHandle)
1206
- void driverHandle.terminate(1000);
1207
- };
1208
- call.signal.addEventListener("abort", onAbort, { once: true });
1209
- }
1210
- try {
1211
- const exit = await exitPromise;
1212
- if (timeoutTimer)
1213
- clearTimeout(timeoutTimer);
1214
- if (call?.signal && onAbort) {
1215
- call.signal.removeEventListener("abort", onAbort);
1216
- }
1217
- if (timedOut) {
1218
- throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
1219
- }
1220
- if (cancelled || call?.signal?.aborted) {
1221
- throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
1222
- }
1223
- return {
1224
- exit,
1225
- chunks,
1226
- truncated,
1227
- };
1228
- }
1229
- catch (err) {
1230
- if (timeoutTimer)
1231
- clearTimeout(timeoutTimer);
1232
- if (call?.signal && onAbort) {
1233
- call.signal.removeEventListener("abort", onAbort);
1234
- }
1235
- if (timedOut) {
1236
- throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
1237
- }
1238
- if (cancelled || call?.signal?.aborted) {
1239
- throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
1240
- }
1241
- throw err;
1242
- }
1243
- }
1244
- /**
1245
- * 授予指定受管进程控制令牌。
1246
- */
1247
- grantControl(proc, requestId, ttlMs, runId) {
1248
- proc.controlEpoch += 1;
1249
- const token = `tok_${proc.info.id}_${proc.controlEpoch}_${randomUUID().replace(/-/g, "")}`;
1250
- const expiresAt = new Date(Date.now() + ttlMs).toISOString();
1251
- proc.info.control = "held";
1252
- proc.currentGrant = {
1253
- token,
1254
- epoch: proc.controlEpoch,
1255
- ttlMs,
1256
- expiresAt,
1257
- runId,
1258
- requestId,
1259
- };
1260
- proc.ttlTimer = setTimeout(() => {
1261
- this.handleGrantTtlExpired(proc);
1262
- }, ttlMs);
1263
- if (typeof proc.ttlTimer.unref === "function") {
1264
- proc.ttlTimer.unref();
1265
- }
1266
- void this.persistState(proc.info.id, {
1267
- control: "held",
1268
- controlState: "held",
1269
- });
1270
- return {
1271
- token,
1272
- expiresAt,
1273
- };
1274
- }
1275
- /**
1276
- * 控制权到期未释放时自动转为隔离状态。
1277
- */
1278
- handleGrantTtlExpired(proc) {
1279
- if (proc.info.control !== "held" || !proc.currentGrant) {
1280
- return;
1281
- }
1282
- void this.quarantineProcess(proc.owner, proc.info.id, proc.currentGrant.token, "Control token TTL expired");
1283
- }
1284
- /**
1285
- * 唤醒排队等待控制权的下一个调用者。
1286
- */
1287
- wakeNextAcquireWaiter(proc) {
1288
- if (proc.acquireWaiters.length === 0) {
1289
- return;
1290
- }
1291
- const next = proc.acquireWaiters.shift();
1292
- const grant = this.grantControl(proc, next.requestId, next.ttlMs, next.runId);
1293
- const key = {
1294
- hostEpoch: this.hostEpoch,
1295
- scope: proc.scope,
1296
- processId: proc.info.id,
1297
- requestId: next.requestId,
1298
- };
1299
- const payloadHash = hashRequestPayload({ waitMs: next.waitMs, ttlMs: next.ttlMs });
1300
- void this.persistReceipt(key, grant, payloadHash);
1301
- next.resolve(grant);
1302
- }
1303
- /**
1304
- * 串行调度执行输入队列操作。
1305
- */
1306
- async dispatchNext(proc) {
1307
- if (proc.isDispatching || proc.inputQueue.length === 0) {
1308
- return;
1309
- }
1310
- proc.isDispatching = true;
1311
- try {
1312
- while (proc.inputQueue.length > 0) {
1313
- const op = proc.inputQueue[0];
1314
- // dispatch 前校验 token、epoch 与授权
1315
- if (proc.info.control !== "held" ||
1316
- !proc.currentGrant ||
1317
- proc.currentGrant.token !== op.token ||
1318
- new Date(proc.currentGrant.expiresAt).getTime() <= Date.now() ||
1319
- op.cancelEpoch !== proc.cancelEpoch) {
1320
- if (op.cancelEpoch === proc.cancelEpoch) {
1321
- op.receipt.state = "failed";
1322
- op.receipt.errorCode =
1323
- proc.info.control === "quarantined" ? PROCESS_QUARANTINED : CONTROL_REVOKED;
1324
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1325
- }
1326
- proc.inputQueue.shift();
1327
- if (op.bytes && op.cancelEpoch === proc.cancelEpoch) {
1328
- proc.pendingInputBytes -= op.bytes.byteLength;
1329
- }
1330
- continue;
1331
- }
1332
- op.receipt.state = "dispatching";
1333
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1334
- if (op.type === "write" && op.bytes) {
1335
- try {
1336
- if (!proc.handle) {
1337
- throw new Error("Missing driver handle");
1338
- }
1339
- await proc.handle.write(op.bytes);
1340
- // 写入返回后校验取消纪元与队首位置:已被 stop 或 quarantine 接管则放弃过期结算
1341
- if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
1342
- continue;
1343
- }
1344
- op.receipt.state = "completed";
1345
- op.receipt.acceptedBytes = op.bytes.byteLength;
1346
- this.refreshIdleTimer(proc);
1347
- }
1348
- catch (err) {
1349
- // 写入返回异常时同样校验取消纪元:被接管则放弃过期结算
1350
- if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
1351
- continue;
1352
- }
1353
- // 若写入出现不确定失败,结果标记为 unknown,进程转入 quarantined
1354
- op.receipt.state = "unknown";
1355
- op.receipt.errorCode = INPUT_OUTCOME_UNKNOWN;
1356
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1357
- proc.inputQueue.shift();
1358
- proc.pendingInputBytes -= op.bytes.byteLength;
1359
- await this.quarantineProcess(proc.owner, proc.info.id, op.token, "Write failed with uncertain outcome");
1360
- break;
1361
- }
1362
- }
1363
- else if (op.type === "control" && op.action) {
1364
- try {
1365
- if (!proc.handle) {
1366
- throw new Error("Missing driver handle");
1367
- }
1368
- if (op.action.type === "input-eof") {
1369
- if (proc.handle.sendInputEOF) {
1370
- await proc.handle.sendInputEOF();
1371
- }
1372
- }
1373
- else if (op.action.type === "interrupt-foreground") {
1374
- if (proc.handle.interruptForeground) {
1375
- await proc.handle.interruptForeground();
1376
- }
1377
- }
1378
- else if (op.action.type === "resize") {
1379
- if (proc.handle.resize) {
1380
- await proc.handle.resize(op.action.cols, op.action.rows);
1381
- }
1382
- }
1383
- // 控制指令返回后校验取消纪元与队首位置:已被接管则放弃过期结算
1384
- if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
1385
- continue;
1386
- }
1387
- if (op.action.type === "input-eof") {
1388
- proc.inputClosed = true;
1389
- await this.persistState(proc.info.id, { inputClosed: true });
1390
- }
1391
- op.receipt.state = "completed";
1392
- this.refreshIdleTimer(proc);
1393
- }
1394
- catch (err) {
1395
- if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
1396
- continue;
1397
- }
1398
- op.receipt.state = "failed";
1399
- op.receipt.errorCode = err instanceof ProcessError ? err.code : "CONTROL_FAILED";
1400
- if (err instanceof Error && err.message) {
1401
- op.receipt.errorMessage = err.message;
1402
- }
1403
- this.recordDiagnostic(`Control action '${op.action.type}' failed for request '${op.requestId}'`, err);
1404
- }
1405
- }
1406
- // 结算前再次校验:若结算窗口内被 stop 或 quarantine 接管则放弃改写
1407
- if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
1408
- continue;
1409
- }
1410
- await this.persistReceipt(op.key, op.receipt, op.payloadHash);
1411
- proc.inputQueue.shift();
1412
- if (op.bytes) {
1413
- proc.pendingInputBytes -= op.bytes.byteLength;
1414
- }
1415
- }
1416
- }
1417
- finally {
1418
- proc.isDispatching = false;
1419
- }
1420
- }
1421
- /**
1422
- * 操作提交前校验持有者令牌与授权。
1423
- */
1424
- checkOperationAuth(proc, token) {
1425
- if (proc.info.control === "quarantined") {
1426
- throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
1427
- }
1428
- if (proc.info.control !== "held" || !proc.currentGrant) {
1429
- throw new ProcessError(ACCESS_DENIED, "Process control is not currently held");
1430
- }
1431
- if (proc.currentGrant.token !== token) {
1432
- throw new ProcessError(ACCESS_DENIED, "Invalid control token");
1433
- }
1434
- if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
1435
- throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
1436
- }
929
+ return this.runExecutor.execute(input, call);
1437
930
  }
1438
931
  /**
1439
932
  * 启动空闲超时定时器。
@@ -1533,11 +1026,11 @@ export class ProcessManager {
1533
1026
  proc.info.endReason = "natural";
1534
1027
  }
1535
1028
  proc.currentGrant = undefined;
1536
- // 拒绝排队等待者
1537
- while (proc.acquireWaiters.length > 0) {
1538
- const waiter = proc.acquireWaiters.shift();
1539
- waiter.reject(new ProcessError(CONTROL_REVOKED, "Process has exited"));
1540
- }
1029
+ // 终态拒绝全部排队等待者(单一入口)
1030
+ this.controlArbiter.revokeAllWaiters(proc, {
1031
+ code: CONTROL_REVOKED,
1032
+ message: "Process has exited",
1033
+ });
1541
1034
  void this.persistState(proc.info.id, {
1542
1035
  state: "exited",
1543
1036
  control: "closed",
@@ -1602,10 +1095,11 @@ export class ProcessManager {
1602
1095
  proc.info.endReason = "natural";
1603
1096
  proc.outputLog.closeOutput("natural");
1604
1097
  proc.info.outputClosed = true;
1605
- while (proc.acquireWaiters.length > 0) {
1606
- const waiter = proc.acquireWaiters.shift();
1607
- waiter.reject(new ProcessError(CONTROL_REVOKED, `Process error: ${err.message}`));
1608
- }
1098
+ // 终态拒绝全部排队等待者(单一入口)
1099
+ this.controlArbiter.revokeAllWaiters(proc, {
1100
+ code: CONTROL_REVOKED,
1101
+ message: `Process error: ${err.message}`,
1102
+ });
1609
1103
  void this.persistState(proc.info.id, {
1610
1104
  state: "failed",
1611
1105
  control: "closed",
@@ -1646,8 +1140,8 @@ export class ProcessManager {
1646
1140
  throw new ProcessError(PROCESS_LOST, `Process '${processId}' belonged to a previous host epoch '${record.hostEpoch}' and has been marked lost; current epoch is '${this.hostEpoch}'`, { processId, hostEpoch: record.hostEpoch, currentHostEpoch: this.hostEpoch });
1647
1141
  }
1648
1142
  const info = toSdkProcessInfo(record);
1649
- const retainedLog = this.getRetainedOutputLog(processId);
1650
- const tombstone = this.evictedOutputTombstones.get(processId);
1143
+ const retainedLog = this.terminalOutputCache.get(processId);
1144
+ const tombstone = this.terminalOutputCache.getTombstone(processId);
1651
1145
  const isTerminalLoaded = info.state === "exited" || info.state === "failed" || info.state === "lost";
1652
1146
  const isOutputUnavailable = !retainedLog && (Boolean(tombstone) || (isTerminalLoaded && Boolean(record.outputClosed)));
1653
1147
  const outputLog = retainedLog ??
@@ -1686,84 +1180,11 @@ export class ProcessManager {
1686
1180
  this.processes.set(processId, loaded);
1687
1181
  return loaded;
1688
1182
  }
1689
- /**
1690
- * 记录已淘汰输出日志的墓碑信息(保留最近 2048 条,防止内存无限积压)。
1691
- */
1692
- recordTombstone(processId, tombstone) {
1693
- if (this.evictedOutputTombstones.size >= 2048) {
1694
- const oldestKey = this.evictedOutputTombstones.keys().next().value;
1695
- if (oldestKey) {
1696
- this.evictedOutputTombstones.delete(oldestKey);
1697
- }
1698
- }
1699
- this.evictedOutputTombstones.set(processId, tombstone);
1700
- }
1701
- /**
1702
- * 清理已过期的终态输出日志条目。
1703
- */
1704
- cleanExpiredTerminalLogs() {
1705
- const now = Date.now();
1706
- for (const [id, entry] of this.evictedOutputLogs.entries()) {
1707
- if (now - entry.evictedAt > this.terminalLogRetentionMs) {
1708
- this.recordTombstone(id, {
1709
- tailCursor: entry.log.tailCursor,
1710
- earliestCursor: entry.log.earliestCursor,
1711
- evictedAt: now,
1712
- });
1713
- this.evictedOutputLogs.delete(id);
1714
- }
1715
- }
1716
- }
1717
1183
  /**
1718
1184
  * 统计终态保留日志当前在内存中实际占用的输出缓冲字节总数。
1719
1185
  */
1720
1186
  countRetainedOutputBufferBytes() {
1721
- this.cleanExpiredTerminalLogs();
1722
- let bytes = 0;
1723
- for (const entry of this.evictedOutputLogs.values()) {
1724
- bytes += entry.log.currentBytes;
1725
- }
1726
- return bytes;
1727
- }
1728
- /**
1729
- * 将终态输出日志存入保留缓存,并根据宿主配额执行 LRU 与 TTL 淘汰。
1730
- */
1731
- retainTerminalOutputLog(processId, log) {
1732
- this.cleanExpiredTerminalLogs();
1733
- // 若新加入条目会导致总输出配额超限或数量超限,按 LRU 顺序淘汰最旧条目
1734
- while ((this.countRetainedOutputBufferBytes() + log.currentBytes > this.quotas.maxOutputBufferBytesPerHost ||
1735
- this.evictedOutputLogs.size >= 512) &&
1736
- this.evictedOutputLogs.size > 0) {
1737
- const oldestKey = this.evictedOutputLogs.keys().next().value;
1738
- if (!oldestKey)
1739
- break;
1740
- const oldestEntry = this.evictedOutputLogs.get(oldestKey);
1741
- if (oldestEntry) {
1742
- this.recordTombstone(oldestKey, {
1743
- tailCursor: oldestEntry.log.tailCursor,
1744
- earliestCursor: oldestEntry.log.earliestCursor,
1745
- evictedAt: Date.now(),
1746
- });
1747
- }
1748
- this.evictedOutputLogs.delete(oldestKey);
1749
- }
1750
- this.evictedOutputLogs.set(processId, {
1751
- log,
1752
- evictedAt: Date.now(),
1753
- });
1754
- }
1755
- /**
1756
- * 获取终态保留日志(附带过期清理与 LRU 触达更新)。
1757
- */
1758
- getRetainedOutputLog(processId) {
1759
- this.cleanExpiredTerminalLogs();
1760
- const entry = this.evictedOutputLogs.get(processId);
1761
- if (!entry)
1762
- return undefined;
1763
- // 触达刷新 LRU 顺序
1764
- this.evictedOutputLogs.delete(processId);
1765
- this.evictedOutputLogs.set(processId, entry);
1766
- return entry.log;
1187
+ return this.terminalOutputCache.retainedBytes();
1767
1188
  }
1768
1189
  /**
1769
1190
  * 终态驱逐与驱动释放:进程到达终态且输出已关闭后移除内存记录,
@@ -1780,7 +1201,7 @@ export class ProcessManager {
1780
1201
  }
1781
1202
  this.processes.delete(proc.info.id);
1782
1203
  // 保留输出日志支撑后续游标读取(内存输出无法从持久层恢复)
1783
- this.retainTerminalOutputLog(proc.info.id, proc.outputLog);
1204
+ this.terminalOutputCache.retain(proc.info.id, proc.outputLog);
1784
1205
  if (proc.handle && typeof this.driver.dispose === "function") {
1785
1206
  try {
1786
1207
  const disposed = this.driver.dispose(proc.handle);
@@ -1832,23 +1253,14 @@ export class ProcessManager {
1832
1253
  });
1833
1254
  }
1834
1255
  // 终态保留输出日志实际占用字节数纳入宿主预算
1835
- totalBuffer += this.countRetainedOutputBufferBytes();
1256
+ const retainedBefore = this.countRetainedOutputBufferBytes();
1257
+ totalBuffer += retainedBefore;
1836
1258
  // 若新进程申请的缓冲使总预算超限,优先淘汰已有的终态保留日志(LRU 策略)
1837
- while (totalBuffer + perProcBuf > this.quotas.maxOutputBufferBytesPerHost &&
1838
- this.evictedOutputLogs.size > 0) {
1839
- const oldestKey = this.evictedOutputLogs.keys().next().value;
1840
- if (!oldestKey)
1841
- break;
1842
- const oldest = this.evictedOutputLogs.get(oldestKey);
1843
- if (oldest) {
1844
- this.recordTombstone(oldestKey, {
1845
- tailCursor: oldest.log.tailCursor,
1846
- earliestCursor: oldest.log.earliestCursor,
1847
- evictedAt: Date.now(),
1848
- });
1849
- totalBuffer -= oldest.log.currentBytes;
1850
- }
1851
- this.evictedOutputLogs.delete(oldestKey);
1259
+ // 淘汰门槛为宿主预算扣除活跃进程已占字节与新申请字节,淘汰释放的字节数同步回扣宿主预算
1260
+ if (totalBuffer + perProcBuf > this.quotas.maxOutputBufferBytesPerHost) {
1261
+ const activeBufferBytes = totalBuffer - retainedBefore;
1262
+ const evictedBytes = this.terminalOutputCache.evictLRUUntil(this.quotas.maxOutputBufferBytesPerHost - perProcBuf - activeBufferBytes);
1263
+ totalBuffer -= evictedBytes;
1852
1264
  }
1853
1265
  if (totalBuffer + perProcBuf > this.quotas.maxOutputBufferBytesPerHost) {
1854
1266
  throw new ProcessError(QUOTA_EXCEEDED, "Host output buffer quota exceeded", {