@actiondock/core 2.5.1 → 2.6.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 (134) 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 +2 -0
  25. package/dist/errors.js +2 -0
  26. package/dist/execution/service.js +14 -9
  27. package/dist/host/host.d.ts +33 -0
  28. package/dist/host/host.js +222 -362
  29. package/dist/host/routing.d.ts +92 -0
  30. package/dist/host/routing.js +238 -0
  31. package/dist/host/types.d.ts +8 -0
  32. package/dist/ipc/host.js +38 -0
  33. package/dist/platform/types.d.ts +7 -0
  34. package/dist/process/control-arbiter.d.ts +107 -0
  35. package/dist/process/control-arbiter.js +251 -0
  36. package/dist/process/diagnostics-sink.d.ts +26 -0
  37. package/dist/process/diagnostics-sink.js +35 -0
  38. package/dist/process/index.d.ts +7 -0
  39. package/dist/process/index.js +7 -0
  40. package/dist/process/input-dispatcher.d.ts +88 -0
  41. package/dist/process/input-dispatcher.js +240 -0
  42. package/dist/process/managed-record.d.ts +78 -0
  43. package/dist/process/managed-record.js +1 -0
  44. package/dist/process/process-manager.d.ts +15 -56
  45. package/dist/process/process-manager.js +119 -707
  46. package/dist/process/reservation-table.d.ts +39 -0
  47. package/dist/process/reservation-table.js +80 -0
  48. package/dist/process/run-executor.d.ts +16 -0
  49. package/dist/process/run-executor.js +155 -0
  50. package/dist/process/terminal-output-cache.d.ts +66 -0
  51. package/dist/process/terminal-output-cache.js +132 -0
  52. package/dist/profile/client-actions.d.ts +56 -0
  53. package/dist/profile/client-actions.js +215 -0
  54. package/dist/profile/client-config.d.ts +20 -0
  55. package/dist/profile/client-config.js +51 -0
  56. package/dist/profile/client-health.d.ts +14 -0
  57. package/dist/profile/client-health.js +68 -0
  58. package/dist/profile/client-playbooks.d.ts +19 -0
  59. package/dist/profile/client-playbooks.js +36 -0
  60. package/dist/profile/client-query.d.ts +30 -0
  61. package/dist/profile/client-query.js +70 -0
  62. package/dist/profile/client-runs.d.ts +42 -0
  63. package/dist/profile/client-runs.js +58 -0
  64. package/dist/profile/client-state.d.ts +56 -0
  65. package/dist/profile/client-state.js +83 -0
  66. package/dist/profile/client-transport.d.ts +61 -0
  67. package/dist/profile/client-transport.js +155 -0
  68. package/dist/profile/client.d.ts +23 -142
  69. package/dist/profile/client.js +28 -588
  70. package/dist/profile/manager.js +1 -1
  71. package/dist/project/closure.d.ts +4 -4
  72. package/dist/project/closure.js +11 -5
  73. package/dist/project/init.js +2 -2
  74. package/dist/project/loader.d.ts +0 -25
  75. package/dist/project/loader.js +1 -154
  76. package/dist/project/transactions.d.ts +12 -14
  77. package/dist/project/transactions.js +60 -836
  78. package/dist/registry/registry.js +6 -4
  79. package/dist/registry/resolve.js +1 -1
  80. package/dist/runtime/action-registry.d.ts +45 -0
  81. package/dist/runtime/action-registry.js +108 -0
  82. package/dist/runtime/clock.d.ts +5 -17
  83. package/dist/runtime/clock.js +5 -15
  84. package/dist/runtime/context.d.ts +3 -0
  85. package/dist/runtime/context.js +17 -56
  86. package/dist/runtime/index.d.ts +3 -0
  87. package/dist/runtime/index.js +3 -0
  88. package/dist/runtime/package-runner-factory.d.ts +82 -0
  89. package/dist/runtime/package-runner-factory.js +186 -0
  90. package/dist/runtime/run-persistence.d.ts +85 -0
  91. package/dist/runtime/run-persistence.js +111 -0
  92. package/dist/runtime/runner.d.ts +23 -33
  93. package/dist/runtime/runner.js +92 -286
  94. package/dist/runtime/standalone.d.ts +11 -1
  95. package/dist/runtime/standalone.js +137 -25
  96. package/dist/server/dispatcher.d.ts +8 -3
  97. package/dist/server/dispatcher.js +16 -40
  98. package/dist/server/routes/runs.js +6 -4
  99. package/dist/server/security.d.ts +1 -7
  100. package/dist/server/security.js +2 -13
  101. package/dist/storage/clock.d.ts +25 -0
  102. package/dist/storage/clock.js +23 -0
  103. package/dist/storage/data-dir-lock.d.ts +3 -36
  104. package/dist/storage/data-dir-lock.js +48 -881
  105. package/dist/storage/index.d.ts +8 -1
  106. package/dist/storage/index.js +8 -2
  107. package/dist/storage/lazy.js +1 -0
  108. package/dist/storage/lock-core.d.ts +162 -0
  109. package/dist/storage/lock-core.js +885 -0
  110. package/dist/storage/sqlite.d.ts +19 -6
  111. package/dist/storage/sqlite.js +81 -89
  112. package/dist/storage/types.d.ts +27 -5
  113. package/dist/storage/types.js +21 -0
  114. package/dist/storage/utils.d.ts +22 -0
  115. package/dist/storage/utils.js +34 -0
  116. package/dist/target/local.d.ts +1 -0
  117. package/dist/target/local.js +23 -17
  118. package/dist/target/remote-errors.d.ts +21 -0
  119. package/dist/target/remote-errors.js +51 -0
  120. package/dist/target/remote-polling.d.ts +35 -0
  121. package/dist/target/remote-polling.js +104 -0
  122. package/dist/target/remote.d.ts +4 -0
  123. package/dist/target/remote.js +108 -301
  124. package/dist/target/sse-parser.d.ts +35 -0
  125. package/dist/target/sse-parser.js +163 -0
  126. package/dist/target/target.js +2 -0
  127. package/dist/target/types.d.ts +6 -0
  128. package/dist/utils/index.d.ts +1 -0
  129. package/dist/utils/index.js +1 -0
  130. package/dist/utils/net.d.ts +10 -0
  131. package/dist/utils/net.js +16 -0
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/package.json +2 -2
@@ -0,0 +1,251 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ACCESS_DENIED, CONTROL_BUSY, CONTROL_EXPIRED, CONTROL_REVOKED, PROCESS_QUARANTINED, QUOTA_EXCEEDED, ProcessError, } from "../errors.js";
3
+ /**
4
+ * 控制权仲裁器。
5
+ *
6
+ * 单一职责:独占控制权状态机的全部推进路径——
7
+ * - 直接授予与 FIFO 等待队列(唯一唤醒)
8
+ * - TTL 定时器生命周期(设置、续租换新、清理)
9
+ * - 到期复核(复合控制态与凭据双重校验,转隔离而非释放)
10
+ * - 操作授权校验(write/control 提交前与调度复核共用)
11
+ * - 终态等待者单一拒绝入口
12
+ */
13
+ export class ControlArbiter {
14
+ host;
15
+ constructor(host) {
16
+ this.host = host;
17
+ }
18
+ /**
19
+ * 尝试立即授予控制权:控制权空闲且无等待者时直接授予,
20
+ * 否则按 FIFO 进入等待队列并返回等待 Promise。
21
+ *
22
+ * 调用方(原 acquire 请求)在排队路径 resolve 后负责预约结算;
23
+ * 唤醒方负责凭据落盘,保持两阶段时序不变。
24
+ */
25
+ tryAcquire(proc, input, call, runId) {
26
+ // 若控制权空闲且无等待者,直接授予
27
+ if (proc.info.control === "free" && proc.acquireWaiters.length === 0) {
28
+ const grant = this.grantControl(proc, input.requestId, input.ttlMs, runId);
29
+ return { granted: true, waiter: Promise.resolve(grant) };
30
+ }
31
+ // 控制权已被持有,检查等待队列配额并进入 FIFO 排队
32
+ if (proc.acquireWaiters.length >= this.host.maxWaitersPerProcess) {
33
+ throw new ProcessError(QUOTA_EXCEEDED, "Process acquire waiter quota exceeded", {
34
+ limit: this.host.maxWaitersPerProcess,
35
+ });
36
+ }
37
+ const totalHostWaiters = this.host.countHostAcquireWaiters();
38
+ if (totalHostWaiters >= this.host.maxWaitersPerHost) {
39
+ throw new ProcessError(QUOTA_EXCEEDED, "Host acquire waiter quota exceeded", {
40
+ limit: this.host.maxWaitersPerHost,
41
+ });
42
+ }
43
+ // 排队路径:由唤醒方在授予时落盘凭据;原请求 resolve 后负责预约 commit
44
+ const waiter = this.enqueueWaiter(proc, input, call, runId);
45
+ return { granted: false, waiter };
46
+ }
47
+ /**
48
+ * FIFO 入队单个等待者:注册超时定时器、中止监听与自清理钩子。
49
+ */
50
+ enqueueWaiter(proc, input, call, runId) {
51
+ return new Promise((resolve, reject) => {
52
+ let timer;
53
+ const waiter = {
54
+ requestId: input.requestId,
55
+ waitMs: input.waitMs,
56
+ ttlMs: input.ttlMs,
57
+ runId,
58
+ resolve: (granted) => {
59
+ cleanup();
60
+ resolve(granted);
61
+ },
62
+ reject: (err) => {
63
+ cleanup();
64
+ reject(err);
65
+ },
66
+ };
67
+ const cleanup = () => {
68
+ if (timer)
69
+ clearTimeout(timer);
70
+ const idx = proc.acquireWaiters.indexOf(waiter);
71
+ if (idx !== -1) {
72
+ proc.acquireWaiters.splice(idx, 1);
73
+ }
74
+ if (call?.signal && waiter.onAbort) {
75
+ call.signal.removeEventListener("abort", waiter.onAbort);
76
+ }
77
+ };
78
+ if (call?.signal) {
79
+ if (call.signal.aborted) {
80
+ reject(call.signal.reason ?? new ProcessError(CONTROL_BUSY, "Acquire cancelled"));
81
+ return;
82
+ }
83
+ waiter.onAbort = () => {
84
+ cleanup();
85
+ reject(call.signal?.reason ?? new ProcessError(CONTROL_BUSY, "Acquire cancelled"));
86
+ };
87
+ call.signal.addEventListener("abort", waiter.onAbort, { once: true });
88
+ }
89
+ timer = setTimeout(() => {
90
+ cleanup();
91
+ reject(new ProcessError(CONTROL_BUSY, "Timed out waiting for process control", {
92
+ waitMs: input.waitMs,
93
+ }));
94
+ }, input.waitMs);
95
+ if (typeof timer?.unref === "function") {
96
+ timer.unref();
97
+ }
98
+ proc.acquireWaiters.push(waiter);
99
+ });
100
+ }
101
+ /**
102
+ * 授予指定受管进程控制令牌:递增控制纪元、登记凭据并设置 TTL 定时器。
103
+ */
104
+ grantControl(proc, requestId, ttlMs, runId) {
105
+ proc.controlEpoch += 1;
106
+ const token = `tok_${proc.info.id}_${proc.controlEpoch}_${randomUUID().replace(/-/g, "")}`;
107
+ const expiresAt = new Date(Date.now() + ttlMs).toISOString();
108
+ proc.info.control = "held";
109
+ proc.currentGrant = {
110
+ token,
111
+ epoch: proc.controlEpoch,
112
+ ttlMs,
113
+ expiresAt,
114
+ runId,
115
+ requestId,
116
+ };
117
+ proc.ttlTimer = setTimeout(() => {
118
+ this.handleGrantTtlExpired(proc);
119
+ }, ttlMs);
120
+ if (typeof proc.ttlTimer.unref === "function") {
121
+ proc.ttlTimer.unref();
122
+ }
123
+ void this.host.persistState(proc.info.id, {
124
+ control: "held",
125
+ controlState: "held",
126
+ });
127
+ return {
128
+ token,
129
+ expiresAt,
130
+ };
131
+ }
132
+ /**
133
+ * 续租当前有效控制令牌:先清理旧 TTL 定时器再设置新定时器,
134
+ * 刷新到期时刻并联动空闲定时器。
135
+ */
136
+ renewGrant(proc, ttlMs) {
137
+ if (proc.ttlTimer) {
138
+ clearTimeout(proc.ttlTimer);
139
+ }
140
+ const expiresAt = new Date(Date.now() + ttlMs).toISOString();
141
+ proc.currentGrant.ttlMs = ttlMs;
142
+ proc.currentGrant.expiresAt = expiresAt;
143
+ proc.ttlTimer = setTimeout(() => {
144
+ this.handleGrantTtlExpired(proc);
145
+ }, ttlMs);
146
+ if (typeof proc.ttlTimer.unref === "function") {
147
+ proc.ttlTimer.unref();
148
+ }
149
+ // 延长控制权刷新 idleTimeout
150
+ this.host.refreshIdleTimer(proc);
151
+ return {
152
+ token: proc.currentGrant.token,
153
+ expiresAt,
154
+ };
155
+ }
156
+ /**
157
+ * 显式释放控制令牌:清理 TTL 定时器、凭据置空并唤醒下一个等待者。
158
+ */
159
+ releaseGrant(proc) {
160
+ if (proc.ttlTimer) {
161
+ clearTimeout(proc.ttlTimer);
162
+ proc.ttlTimer = undefined;
163
+ }
164
+ proc.currentGrant = undefined;
165
+ proc.info.control = "free";
166
+ void this.host.persistState(proc.info.id, {
167
+ control: "free",
168
+ controlState: "free",
169
+ });
170
+ // 正常 release 唤醒下一个等待者
171
+ this.wakeNextAcquireWaiter(proc);
172
+ }
173
+ /**
174
+ * 唤醒排队等待控制权的下一个调用者(唯一唤醒入口):
175
+ * 授予令牌、由唤醒方落盘排队凭据并 resolve 原等待 Promise。
176
+ */
177
+ wakeNextAcquireWaiter(proc) {
178
+ if (proc.acquireWaiters.length === 0) {
179
+ return;
180
+ }
181
+ const next = proc.acquireWaiters.shift();
182
+ const grant = this.grantControl(proc, next.requestId, next.ttlMs, next.runId);
183
+ void this.host.persistGrantReceipt(grant, next, proc);
184
+ next.resolve(grant);
185
+ }
186
+ /**
187
+ * 控制权到期未释放时的复核:仍处于 held 且凭据有效时转隔离(隔离而非释放)。
188
+ */
189
+ handleGrantTtlExpired(proc) {
190
+ if (proc.info.control !== "held" || !proc.currentGrant) {
191
+ return;
192
+ }
193
+ void this.host.quarantineProcess(proc.owner, proc.info.id, proc.currentGrant.token, "Control token TTL expired");
194
+ }
195
+ /**
196
+ * 操作提交前校验持有者令牌与授权(write/control 入队前与调度链路共用)。
197
+ */
198
+ validateOperation(proc, token) {
199
+ if (proc.info.control === "quarantined") {
200
+ throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
201
+ }
202
+ if (proc.info.control !== "held" || !proc.currentGrant) {
203
+ throw new ProcessError(ACCESS_DENIED, "Process control is not currently held");
204
+ }
205
+ if (proc.currentGrant.token !== token) {
206
+ throw new ProcessError(ACCESS_DENIED, "Invalid control token");
207
+ }
208
+ if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
209
+ throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
210
+ }
211
+ }
212
+ /**
213
+ * 校验当前持有凭据有效性(token 匹配且未过期),供 renew/release 提交前判定。
214
+ */
215
+ validateHeldGrant(proc, token) {
216
+ if (proc.info.control === "quarantined") {
217
+ throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
218
+ }
219
+ if (proc.info.control !== "held" || !proc.currentGrant) {
220
+ throw new ProcessError(CONTROL_REVOKED, "Process control is not currently held");
221
+ }
222
+ if (proc.currentGrant.token !== token) {
223
+ throw new ProcessError(ACCESS_DENIED, "Invalid control token");
224
+ }
225
+ if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
226
+ throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
227
+ }
228
+ }
229
+ /**
230
+ * 终态等待者单一拒绝入口:按 FIFO 拒绝全部排队等待者。
231
+ *
232
+ * 终态转移(exit/error/stop/quarantine)必须经由此方法清空等待队列,
233
+ * 禁止直接操作 acquireWaiters 数组。
234
+ */
235
+ revokeAllWaiters(proc, reason) {
236
+ while (proc.acquireWaiters.length > 0) {
237
+ const waiter = proc.acquireWaiters.shift();
238
+ waiter.reject(new ProcessError(reason.code, reason.message));
239
+ }
240
+ }
241
+ /**
242
+ * 清理进程全部控制权相关定时器与等待队列(stop/退出/故障路径统一入口)。
243
+ */
244
+ disposeProcess(proc) {
245
+ if (proc.ttlTimer) {
246
+ clearTimeout(proc.ttlTimer);
247
+ proc.ttlTimer = undefined;
248
+ }
249
+ proc.currentGrant = undefined;
250
+ }
251
+ }
@@ -0,0 +1,26 @@
1
+ import type { Logger } from "@actiondock/sdk";
2
+ /**
3
+ * 内部诊断日志汇聚器。
4
+ *
5
+ * 持有最近的持久化失败与驱动终止失败等诊断信息:优先写入注入的 logger,
6
+ * 缺失时保留在内存环形缓冲区,避免静默吞没异常。
7
+ */
8
+ export declare class DiagnosticsSink {
9
+ /** 注入的 logger 缺失时保留最近诊断条目的环形缓冲容量,默认 100 */
10
+ static readonly DEFAULT_BUFFER_LIMIT = 100;
11
+ private readonly logger?;
12
+ private readonly bufferLimit;
13
+ private readonly diagnostics;
14
+ constructor(options?: {
15
+ logger?: Logger;
16
+ bufferLimit?: number;
17
+ });
18
+ /**
19
+ * 记录内部诊断信息:优先写入注入的 logger,缺失时保留在内存环形缓冲区。
20
+ */
21
+ record(message: string, err?: unknown): void;
22
+ /**
23
+ * 获取最近的内部诊断日志快照(最近条目在前)。
24
+ */
25
+ recent(): string[];
26
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 内部诊断日志汇聚器。
3
+ *
4
+ * 持有最近的持久化失败与驱动终止失败等诊断信息:优先写入注入的 logger,
5
+ * 缺失时保留在内存环形缓冲区,避免静默吞没异常。
6
+ */
7
+ export class DiagnosticsSink {
8
+ /** 注入的 logger 缺失时保留最近诊断条目的环形缓冲容量,默认 100 */
9
+ static DEFAULT_BUFFER_LIMIT = 100;
10
+ logger;
11
+ bufferLimit;
12
+ diagnostics = [];
13
+ constructor(options) {
14
+ this.logger = options?.logger;
15
+ this.bufferLimit = options?.bufferLimit ?? DiagnosticsSink.DEFAULT_BUFFER_LIMIT;
16
+ }
17
+ /**
18
+ * 记录内部诊断信息:优先写入注入的 logger,缺失时保留在内存环形缓冲区。
19
+ */
20
+ record(message, err) {
21
+ const detail = err instanceof Error ? `${err.name}: ${err.message}` : err !== undefined ? String(err) : "";
22
+ const line = detail ? `${message} (${detail})` : message;
23
+ this.diagnostics.push(`${new Date().toISOString()} ${line}`);
24
+ if (this.diagnostics.length > this.bufferLimit) {
25
+ this.diagnostics.shift();
26
+ }
27
+ this.logger?.warn("[ProcessManager] " + line);
28
+ }
29
+ /**
30
+ * 获取最近的内部诊断日志快照(最近条目在前)。
31
+ */
32
+ recent() {
33
+ return [...this.diagnostics].reverse();
34
+ }
35
+ }
@@ -2,5 +2,12 @@ export * from "./cursor.js";
2
2
  export * from "./output-log.js";
3
3
  export * from "./metadata-store.js";
4
4
  export * from "./driver.js";
5
+ export * from "./diagnostics-sink.js";
6
+ export * from "./reservation-table.js";
7
+ export * from "./terminal-output-cache.js";
8
+ export * from "./run-executor.js";
9
+ export * from "./managed-record.js";
10
+ export * from "./control-arbiter.js";
11
+ export * from "./input-dispatcher.js";
5
12
  export * from "./process-manager.js";
6
13
  export * from "./context-process.js";
@@ -2,5 +2,12 @@ export * from "./cursor.js";
2
2
  export * from "./output-log.js";
3
3
  export * from "./metadata-store.js";
4
4
  export * from "./driver.js";
5
+ export * from "./diagnostics-sink.js";
6
+ export * from "./reservation-table.js";
7
+ export * from "./terminal-output-cache.js";
8
+ export * from "./run-executor.js";
9
+ export * from "./managed-record.js";
10
+ export * from "./control-arbiter.js";
11
+ export * from "./input-dispatcher.js";
5
12
  export * from "./process-manager.js";
6
13
  export * from "./context-process.js";
@@ -0,0 +1,88 @@
1
+ import type { OperationReceipt } from "@actiondock/sdk";
2
+ import type { ManagedProcessRecord, ProcessOwner, QueuedOperation } from "./managed-record.js";
3
+ import type { ControlArbiter } from "./control-arbiter.js";
4
+ import type { ProcessRequestKey, StoredProcessRecord } from "./metadata-store.js";
5
+ /**
6
+ * 输入调度器宿主协作回调集合。
7
+ *
8
+ * 持有者始终为 ProcessManager:调度器仅经回调透传副作用,不反向依赖管理器。
9
+ */
10
+ export interface InputDispatcherHost {
11
+ /** 收据与状态落盘(异步尽力而为,失败仅记录诊断) */
12
+ persistReceipt(key: ProcessRequestKey, receipt: OperationReceipt, payloadHash?: string): Promise<void>;
13
+ persistState(processId: string, patch: Partial<StoredProcessRecord>): Promise<void>;
14
+ /** 写入不确定失败后的隔离处置 */
15
+ quarantineProcess(owner: ProcessOwner, processId: string, token?: string, reason?: string): Promise<void> | unknown;
16
+ /** 成功输入后的空闲定时器刷新 */
17
+ refreshIdleTimer(proc: ManagedProcessRecord): void;
18
+ /** 内部诊断记录 */
19
+ recordDiagnostic(message: string, err?: unknown): void;
20
+ /** 宿主级输入队列待写字节统计 */
21
+ countHostPendingInputBytes(): number;
22
+ /** 输入队列容量配额 */
23
+ readonly maxPendingQueueBytesPerProcess: number;
24
+ readonly maxPendingQueueBytesPerHost: number;
25
+ }
26
+ /**
27
+ * write 入队请求参数。
28
+ */
29
+ export interface EnqueueWriteInput {
30
+ requestId: string;
31
+ token: string;
32
+ data: Uint8Array;
33
+ key: ProcessRequestKey;
34
+ payloadHash: string;
35
+ }
36
+ /**
37
+ * control 入队请求参数。
38
+ */
39
+ export interface EnqueueControlInput {
40
+ requestId: string;
41
+ token: string;
42
+ action: QueuedOperation["action"];
43
+ key: ProcessRequestKey;
44
+ payloadHash: string;
45
+ }
46
+ /**
47
+ * 输入调度器。
48
+ *
49
+ * 单一职责:待写入输入队列的入队、串行调度与接管——
50
+ * - 入队:同步阶段完成容量检查与字节预扣(同一同步块,杜绝 await 窗口配额穿透),异步阶段落盘并推送队列
51
+ * - 调度:inputQueue 串行循环,全部 await 返回点保留取消纪元与队首身份双重校验
52
+ * - 接管:takeoverQueue 同步原子原语(纪元递增、标记失败、落盘回调、清空、字节归零)
53
+ *
54
+ * 调度循环对控制权的复核经控制权仲裁器委托执行。
55
+ */
56
+ export declare class InputDispatcher {
57
+ private readonly host;
58
+ private readonly arbiter;
59
+ constructor(host: InputDispatcherHost, arbiter: ControlArbiter);
60
+ /**
61
+ * 入队写入操作:同步阶段完成授权校验、输入通道校验、队列容量检查与字节预扣;
62
+ * 异步阶段落盘排队收据并推送队列,随后触发串行调度。
63
+ *
64
+ * 返回的 Promise 在收据成功入队后 resolve(state 为 queued);
65
+ * 落盘或推送失败时回滚字节预扣并原样抛出。
66
+ */
67
+ enqueueWrite(proc: ManagedProcessRecord, input: EnqueueWriteInput): Promise<OperationReceipt>;
68
+ /**
69
+ * 入队控制指令:异步阶段落盘排队收据并推送队列,随后触发串行调度。
70
+ */
71
+ enqueueControl(proc: ManagedProcessRecord, input: EnqueueControlInput): Promise<OperationReceipt>;
72
+ /**
73
+ * 输入队列接管原语:递增取消纪元、标记全部待调度操作失败并清空队列。
74
+ *
75
+ * 同步原子序列(无 await 窗口):纪元递增 -> 逐条标记 failed ->
76
+ * 逐条登记落盘回调(fire-and-forget,落盘函数内部自捕获异常)->
77
+ * 清空队列 -> 待写字节归零。
78
+ * stop 与 quarantine 接管输入队列必须经由此单一入口。
79
+ */
80
+ takeoverQueue(proc: ManagedProcessRecord, errorCode: string): void;
81
+ /**
82
+ * 串行调度执行输入队列操作。
83
+ *
84
+ * 全部 await 返回点保留双重校验:取消纪元一致(未被 stop/quarantine 接管)
85
+ * 且队首身份未变(未被并发结算改写)。
86
+ */
87
+ private dispatchNext;
88
+ }
@@ -0,0 +1,240 @@
1
+ import { CONTROL_REVOKED, INPUT_CLOSED, INPUT_OUTCOME_UNKNOWN, PROCESS_QUARANTINED, QUEUE_FULL, ProcessError, } from "../errors.js";
2
+ /**
3
+ * 输入调度器。
4
+ *
5
+ * 单一职责:待写入输入队列的入队、串行调度与接管——
6
+ * - 入队:同步阶段完成容量检查与字节预扣(同一同步块,杜绝 await 窗口配额穿透),异步阶段落盘并推送队列
7
+ * - 调度:inputQueue 串行循环,全部 await 返回点保留取消纪元与队首身份双重校验
8
+ * - 接管:takeoverQueue 同步原子原语(纪元递增、标记失败、落盘回调、清空、字节归零)
9
+ *
10
+ * 调度循环对控制权的复核经控制权仲裁器委托执行。
11
+ */
12
+ export class InputDispatcher {
13
+ host;
14
+ arbiter;
15
+ constructor(host, arbiter) {
16
+ this.host = host;
17
+ this.arbiter = arbiter;
18
+ }
19
+ /**
20
+ * 入队写入操作:同步阶段完成授权校验、输入通道校验、队列容量检查与字节预扣;
21
+ * 异步阶段落盘排队收据并推送队列,随后触发串行调度。
22
+ *
23
+ * 返回的 Promise 在收据成功入队后 resolve(state 为 queued);
24
+ * 落盘或推送失败时回滚字节预扣并原样抛出。
25
+ */
26
+ async enqueueWrite(proc, input) {
27
+ // 入队前校验授权与控制状态
28
+ this.arbiter.validateOperation(proc, input.token);
29
+ if (proc.inputClosed) {
30
+ throw new ProcessError(INPUT_CLOSED, "Input stream is closed");
31
+ }
32
+ const rawBytes = input.data;
33
+ const dataSize = rawBytes.byteLength;
34
+ // 待写入队列容量检查:在任何 await 前同步原子校验并预占配额
35
+ if (proc.pendingInputBytes + dataSize > this.host.maxPendingQueueBytesPerProcess) {
36
+ throw new ProcessError(QUEUE_FULL, "Process pending input queue limit exceeded", {
37
+ limit: this.host.maxPendingQueueBytesPerProcess,
38
+ });
39
+ }
40
+ const totalHostPending = this.host.countHostPendingInputBytes();
41
+ if (totalHostPending + dataSize > this.host.maxPendingQueueBytesPerHost) {
42
+ throw new ProcessError(QUEUE_FULL, "Host pending input queue limit exceeded", {
43
+ limit: this.host.maxPendingQueueBytesPerHost,
44
+ });
45
+ }
46
+ // 同步占位预扣队列配额,杜绝并发 await 窗口穿透
47
+ proc.pendingInputBytes += dataSize;
48
+ let pendingBytesCommitted = false;
49
+ try {
50
+ const receipt = {
51
+ requestId: input.requestId,
52
+ state: "queued",
53
+ };
54
+ await this.host.persistReceipt(input.key, receipt, input.payloadHash);
55
+ proc.inputQueue.push({
56
+ type: "write",
57
+ requestId: input.requestId,
58
+ token: input.token,
59
+ bytes: rawBytes,
60
+ receipt,
61
+ key: input.key,
62
+ payloadHash: input.payloadHash,
63
+ cancelEpoch: proc.cancelEpoch,
64
+ });
65
+ pendingBytesCommitted = true;
66
+ // 异步调度推进
67
+ queueMicrotask(() => void this.dispatchNext(proc));
68
+ return { ...receipt };
69
+ }
70
+ finally {
71
+ if (!pendingBytesCommitted) {
72
+ proc.pendingInputBytes = Math.max(0, proc.pendingInputBytes - dataSize);
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * 入队控制指令:异步阶段落盘排队收据并推送队列,随后触发串行调度。
78
+ */
79
+ async enqueueControl(proc, input) {
80
+ // 入队前校验授权与控制状态
81
+ this.arbiter.validateOperation(proc, input.token);
82
+ const receipt = {
83
+ requestId: input.requestId,
84
+ state: "queued",
85
+ };
86
+ await this.host.persistReceipt(input.key, receipt, input.payloadHash);
87
+ proc.inputQueue.push({
88
+ type: "control",
89
+ requestId: input.requestId,
90
+ token: input.token,
91
+ action: input.action,
92
+ receipt,
93
+ key: input.key,
94
+ payloadHash: input.payloadHash,
95
+ cancelEpoch: proc.cancelEpoch,
96
+ });
97
+ queueMicrotask(() => void this.dispatchNext(proc));
98
+ return { ...receipt };
99
+ }
100
+ /**
101
+ * 输入队列接管原语:递增取消纪元、标记全部待调度操作失败并清空队列。
102
+ *
103
+ * 同步原子序列(无 await 窗口):纪元递增 -> 逐条标记 failed ->
104
+ * 逐条登记落盘回调(fire-and-forget,落盘函数内部自捕获异常)->
105
+ * 清空队列 -> 待写字节归零。
106
+ * stop 与 quarantine 接管输入队列必须经由此单一入口。
107
+ */
108
+ takeoverQueue(proc, errorCode) {
109
+ // 递增取消纪元:接管输入队列,使挂起中的 dispatch 放弃过期结算
110
+ proc.cancelEpoch = (proc.cancelEpoch ?? 0) + 1;
111
+ for (const op of proc.inputQueue) {
112
+ op.receipt.state = "failed";
113
+ op.receipt.errorCode = errorCode;
114
+ void this.host.persistReceipt(op.key, op.receipt, op.payloadHash);
115
+ }
116
+ proc.inputQueue = [];
117
+ proc.pendingInputBytes = 0;
118
+ }
119
+ /**
120
+ * 串行调度执行输入队列操作。
121
+ *
122
+ * 全部 await 返回点保留双重校验:取消纪元一致(未被 stop/quarantine 接管)
123
+ * 且队首身份未变(未被并发结算改写)。
124
+ */
125
+ async dispatchNext(proc) {
126
+ if (proc.isDispatching || proc.inputQueue.length === 0) {
127
+ return;
128
+ }
129
+ proc.isDispatching = true;
130
+ try {
131
+ while (proc.inputQueue.length > 0) {
132
+ const op = proc.inputQueue[0];
133
+ // dispatch 前校验 token、epoch 与授权(经控制权仲裁器委托复核)
134
+ if (proc.info.control !== "held" ||
135
+ !proc.currentGrant ||
136
+ proc.currentGrant.token !== op.token ||
137
+ new Date(proc.currentGrant.expiresAt).getTime() <= Date.now() ||
138
+ op.cancelEpoch !== proc.cancelEpoch) {
139
+ if (op.cancelEpoch === proc.cancelEpoch) {
140
+ op.receipt.state = "failed";
141
+ op.receipt.errorCode =
142
+ proc.info.control === "quarantined" ? PROCESS_QUARANTINED : CONTROL_REVOKED;
143
+ await this.host.persistReceipt(op.key, op.receipt, op.payloadHash);
144
+ }
145
+ proc.inputQueue.shift();
146
+ if (op.bytes && op.cancelEpoch === proc.cancelEpoch) {
147
+ proc.pendingInputBytes -= op.bytes.byteLength;
148
+ }
149
+ continue;
150
+ }
151
+ op.receipt.state = "dispatching";
152
+ await this.host.persistReceipt(op.key, op.receipt, op.payloadHash);
153
+ if (op.type === "write" && op.bytes) {
154
+ try {
155
+ if (!proc.handle) {
156
+ throw new Error("Missing driver handle");
157
+ }
158
+ await proc.handle.write(op.bytes);
159
+ // 写入返回后校验取消纪元与队首位置:已被 stop 或 quarantine 接管则放弃过期结算
160
+ if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
161
+ continue;
162
+ }
163
+ op.receipt.state = "completed";
164
+ op.receipt.acceptedBytes = op.bytes.byteLength;
165
+ this.host.refreshIdleTimer(proc);
166
+ }
167
+ catch (err) {
168
+ // 写入返回异常时同样校验取消纪元:被接管则放弃过期结算
169
+ if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
170
+ continue;
171
+ }
172
+ // 若写入出现不确定失败,结果标记为 unknown,进程转入 quarantined
173
+ op.receipt.state = "unknown";
174
+ op.receipt.errorCode = INPUT_OUTCOME_UNKNOWN;
175
+ await this.host.persistReceipt(op.key, op.receipt, op.payloadHash);
176
+ proc.inputQueue.shift();
177
+ proc.pendingInputBytes -= op.bytes.byteLength;
178
+ await this.host.quarantineProcess(proc.owner, proc.info.id, op.token, "Write failed with uncertain outcome");
179
+ break;
180
+ }
181
+ }
182
+ else if (op.type === "control" && op.action) {
183
+ try {
184
+ if (!proc.handle) {
185
+ throw new Error("Missing driver handle");
186
+ }
187
+ if (op.action.type === "input-eof") {
188
+ if (proc.handle.sendInputEOF) {
189
+ await proc.handle.sendInputEOF();
190
+ }
191
+ }
192
+ else if (op.action.type === "interrupt-foreground") {
193
+ if (proc.handle.interruptForeground) {
194
+ await proc.handle.interruptForeground();
195
+ }
196
+ }
197
+ else if (op.action.type === "resize") {
198
+ if (proc.handle.resize) {
199
+ await proc.handle.resize(op.action.cols, op.action.rows);
200
+ }
201
+ }
202
+ // 控制指令返回后校验取消纪元与队首位置:已被 stop 或 quarantine 接管则放弃过期结算
203
+ if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
204
+ continue;
205
+ }
206
+ if (op.action.type === "input-eof") {
207
+ proc.inputClosed = true;
208
+ await this.host.persistState(proc.info.id, { inputClosed: true });
209
+ }
210
+ op.receipt.state = "completed";
211
+ this.host.refreshIdleTimer(proc);
212
+ }
213
+ catch (err) {
214
+ if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
215
+ continue;
216
+ }
217
+ op.receipt.state = "failed";
218
+ op.receipt.errorCode = err instanceof ProcessError ? err.code : "CONTROL_FAILED";
219
+ if (err instanceof Error && err.message) {
220
+ op.receipt.errorMessage = err.message;
221
+ }
222
+ this.host.recordDiagnostic(`Control action '${op.action.type}' failed for request '${op.requestId}'`, err);
223
+ }
224
+ }
225
+ // 结算前再次校验:若结算窗口内被 stop 或 quarantine 接管则放弃改写
226
+ if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
227
+ continue;
228
+ }
229
+ await this.host.persistReceipt(op.key, op.receipt, op.payloadHash);
230
+ proc.inputQueue.shift();
231
+ if (op.bytes) {
232
+ proc.pendingInputBytes -= op.bytes.byteLength;
233
+ }
234
+ }
235
+ }
236
+ finally {
237
+ proc.isDispatching = false;
238
+ }
239
+ }
240
+ }