@actiondock/core 2.9.1 → 2.11.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 (256) hide show
  1. package/README.md +1 -2
  2. package/dist/catalog/action-catalog.js +3 -33
  3. package/dist/catalog/discovery.d.ts +0 -3
  4. package/dist/catalog/discovery.js +4 -4
  5. package/dist/catalog/graph.d.ts +0 -4
  6. package/dist/catalog/graph.js +12 -15
  7. package/dist/{profile/client-actions.d.ts → client/actions.d.ts} +1 -1
  8. package/dist/{profile/client-actions.js → client/actions.js} +2 -8
  9. package/dist/{profile/client-config.d.ts → client/config.d.ts} +1 -1
  10. package/dist/{profile/client-config.js → client/config.js} +1 -1
  11. package/dist/client/dispatcher.d.ts +4 -0
  12. package/dist/client/dispatcher.js +21 -0
  13. package/dist/client/health.d.ts +38 -0
  14. package/dist/{profile/client-health.js → client/health.js} +9 -6
  15. package/dist/client/index.d.ts +16 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/{profile/client-playbooks.d.ts → client/playbooks.d.ts} +1 -1
  18. package/dist/{profile/client-playbooks.js → client/playbooks.js} +1 -1
  19. package/dist/{profile/client-runs.d.ts → client/runs.d.ts} +4 -2
  20. package/dist/{profile/client-runs.js → client/runs.js} +7 -1
  21. package/dist/{profile/client-state.d.ts → client/state.d.ts} +1 -1
  22. package/dist/{profile/client-state.js → client/state.js} +1 -1
  23. package/dist/{profile/client-transport.d.ts → client/transport.d.ts} +29 -10
  24. package/dist/{profile/client-transport.js → client/transport.js} +125 -35
  25. package/dist/doctor/checks/index.d.ts +1 -1
  26. package/dist/doctor/checks/index.js +1 -1
  27. package/dist/doctor/checks/project.js +24 -92
  28. package/dist/doctor/checks/registry.js +1 -1
  29. package/dist/doctor/checks/runtime.d.ts +0 -4
  30. package/dist/doctor/checks/runtime.js +0 -28
  31. package/dist/doctor/doctor.js +1 -2
  32. package/dist/errors.d.ts +85 -4
  33. package/dist/errors.js +114 -4
  34. package/dist/execution/service.d.ts +55 -37
  35. package/dist/execution/service.js +601 -212
  36. package/dist/execution/types.d.ts +24 -5
  37. package/dist/host/host.d.ts +42 -22
  38. package/dist/host/host.js +565 -150
  39. package/dist/host/routing.js +6 -6
  40. package/dist/host/types.d.ts +25 -13
  41. package/dist/index.d.ts +9 -4
  42. package/dist/index.js +5 -2
  43. package/dist/input/advice.d.ts +0 -44
  44. package/dist/input/advice.js +7 -141
  45. package/dist/input/describe.d.ts +6 -0
  46. package/dist/input/describe.js +26 -1
  47. package/dist/input/flat-materializer.js +1 -1
  48. package/dist/input/flat-parser.js +1 -1
  49. package/dist/input/index.d.ts +0 -1
  50. package/dist/input/index.js +0 -1
  51. package/dist/input/input-resolver.js +2 -2
  52. package/dist/input/validation-mapper.d.ts +1 -1
  53. package/dist/invocation/policy.js +2 -0
  54. package/dist/ipc/diagnostic.d.ts +8 -21
  55. package/dist/ipc/diagnostic.js +24 -57
  56. package/dist/ipc/host.js +21 -7
  57. package/dist/ipc/service.d.ts +14 -0
  58. package/dist/ipc/service.js +132 -26
  59. package/dist/ipc/types.d.ts +0 -4
  60. package/dist/package/index.d.ts +4 -5
  61. package/dist/package/index.js +1 -2
  62. package/dist/package/runtime.d.ts +94 -23
  63. package/dist/package/runtime.js +628 -274
  64. package/dist/package/static-index.d.ts +3 -6
  65. package/dist/package/static-index.js +11 -40
  66. package/dist/package/types.d.ts +19 -4
  67. package/dist/platform/index.d.ts +2 -1
  68. package/dist/platform/index.js +2 -1
  69. package/dist/platform/loader-hook.d.ts +32 -0
  70. package/dist/platform/loader-hook.js +90 -0
  71. package/dist/{node → platform}/module-loader.d.ts +13 -0
  72. package/dist/{node → platform}/module-loader.js +51 -4
  73. package/dist/platform/node.d.ts +19 -8
  74. package/dist/platform/node.js +50 -20
  75. package/dist/platform/types.d.ts +7 -47
  76. package/dist/process/context-process.d.ts +3 -18
  77. package/dist/process/context-process.js +3 -44
  78. package/dist/process/control.d.ts +21 -0
  79. package/dist/process/control.js +34 -0
  80. package/dist/process/converters.d.ts +23 -0
  81. package/dist/process/converters.js +92 -0
  82. package/dist/process/cursor.js +2 -22
  83. package/dist/process/{diagnostics-sink.d.ts → diagnostics.d.ts} +0 -7
  84. package/dist/process/{diagnostics-sink.js → diagnostics.js} +6 -8
  85. package/dist/process/index.d.ts +1 -3
  86. package/dist/process/index.js +1 -3
  87. package/dist/process/input-dispatcher.d.ts +4 -13
  88. package/dist/process/input-dispatcher.js +48 -69
  89. package/dist/process/managed-record.d.ts +3 -31
  90. package/dist/process/metadata-store.d.ts +0 -41
  91. package/dist/process/metadata-store.js +2 -341
  92. package/dist/process/process-driver.js +2 -2
  93. package/dist/process/process-manager.d.ts +8 -50
  94. package/dist/process/process-manager.js +28 -417
  95. package/dist/process/quotas.d.ts +14 -0
  96. package/dist/process/quotas.js +29 -0
  97. package/dist/process/run-executor.js +0 -1
  98. package/dist/process/terminal-output-cache.d.ts +20 -34
  99. package/dist/process/terminal-output-cache.js +43 -64
  100. package/dist/profile/index.d.ts +1 -2
  101. package/dist/profile/index.js +1 -2
  102. package/dist/profile/manager.d.ts +2 -4
  103. package/dist/profile/manager.js +10 -40
  104. package/dist/profile/types.d.ts +1 -17
  105. package/dist/project/digest.d.ts +5 -1
  106. package/dist/project/digest.js +81 -219
  107. package/dist/project/index.d.ts +4 -5
  108. package/dist/project/index.js +4 -5
  109. package/dist/project/init.js +2 -2
  110. package/dist/project/loader.d.ts +3 -3
  111. package/dist/project/loader.js +72 -50
  112. package/dist/project/lockfile.js +6 -5
  113. package/dist/project/manifest.js +5 -4
  114. package/dist/project/transactions.d.ts +4 -42
  115. package/dist/project/transactions.js +83 -138
  116. package/dist/project/types-generator.js +2 -1
  117. package/dist/project/types.d.ts +2 -0
  118. package/dist/registry/index.d.ts +0 -1
  119. package/dist/registry/index.js +0 -1
  120. package/dist/registry/lock.d.ts +2 -11
  121. package/dist/registry/lock.js +14 -183
  122. package/dist/registry/registry.d.ts +1 -6
  123. package/dist/registry/registry.js +5 -17
  124. package/dist/runtime/action-registry.d.ts +1 -5
  125. package/dist/runtime/action-registry.js +0 -12
  126. package/dist/runtime/context.js +2 -2
  127. package/dist/runtime/events.d.ts +11 -50
  128. package/dist/runtime/events.js +88 -368
  129. package/dist/runtime/index.d.ts +0 -4
  130. package/dist/runtime/index.js +0 -4
  131. package/dist/runtime/run-persistence.d.ts +2 -0
  132. package/dist/runtime/run-persistence.js +2 -1
  133. package/dist/runtime/standalone-commands.d.ts +18 -0
  134. package/dist/runtime/standalone-commands.js +466 -0
  135. package/dist/runtime/standalone-help.d.ts +20 -0
  136. package/dist/runtime/standalone-help.js +72 -0
  137. package/dist/runtime/standalone.d.ts +1 -44
  138. package/dist/runtime/standalone.js +31 -526
  139. package/dist/schema/validator.d.ts +2 -2
  140. package/dist/schema/validator.js +2 -9
  141. package/dist/server/index.d.ts +0 -2
  142. package/dist/server/index.js +0 -2
  143. package/dist/server/routes/actions.js +1 -1
  144. package/dist/server/routes/info.js +1 -1
  145. package/dist/server/routes/playbooks.js +1 -1
  146. package/dist/server/routes/runs.js +123 -16
  147. package/dist/server/routes/state.js +43 -18
  148. package/dist/server/security.d.ts +1 -1
  149. package/dist/server/security.js +2 -2
  150. package/dist/server/server.js +3 -7
  151. package/dist/service/factory.js +4 -3
  152. package/dist/service/index.d.ts +1 -1
  153. package/dist/service/index.js +1 -1
  154. package/dist/service/remote-errors.d.ts +1 -1
  155. package/dist/service/remote-errors.js +6 -6
  156. package/dist/service/remote-polling.d.ts +3 -0
  157. package/dist/service/remote-polling.js +10 -5
  158. package/dist/service/remote.d.ts +5 -1
  159. package/dist/service/remote.js +108 -17
  160. package/dist/service/sse-stream.js +2 -2
  161. package/dist/service/types.d.ts +22 -19
  162. package/dist/service/types.js +1 -24
  163. package/dist/storage/clock.d.ts +1 -1
  164. package/dist/storage/clock.js +1 -1
  165. package/dist/storage/data-dir-lock.d.ts +10 -22
  166. package/dist/storage/data-dir-lock.js +28 -56
  167. package/dist/storage/file-lock.d.ts +64 -0
  168. package/dist/storage/file-lock.js +447 -0
  169. package/dist/storage/index.d.ts +2 -2
  170. package/dist/storage/index.js +5 -4
  171. package/dist/storage/run-liveness.d.ts +48 -0
  172. package/dist/storage/run-liveness.js +44 -0
  173. package/dist/storage/sqlite-config.d.ts +16 -0
  174. package/dist/storage/sqlite-config.js +59 -0
  175. package/dist/storage/sqlite-driver.d.ts +3 -1
  176. package/dist/storage/sqlite-driver.js +14 -4
  177. package/dist/storage/sqlite-events.d.ts +13 -0
  178. package/dist/storage/sqlite-events.js +48 -0
  179. package/dist/storage/sqlite-runs.d.ts +107 -0
  180. package/dist/storage/sqlite-runs.js +622 -0
  181. package/dist/storage/sqlite-schema.d.ts +5 -0
  182. package/dist/storage/sqlite-schema.js +143 -0
  183. package/dist/storage/sqlite-state.d.ts +33 -0
  184. package/dist/storage/sqlite-state.js +270 -0
  185. package/dist/storage/sqlite.d.ts +30 -57
  186. package/dist/storage/sqlite.js +84 -741
  187. package/dist/storage/types.d.ts +35 -2
  188. package/dist/utils/index.d.ts +18 -14
  189. package/dist/utils/index.js +37 -29
  190. package/dist/utils/semver.d.ts +18 -0
  191. package/dist/utils/semver.js +19 -0
  192. package/dist/utils/warning.d.ts +1 -0
  193. package/dist/utils/warning.js +52 -0
  194. package/dist/{json/value-validator.d.ts → value-validator.d.ts} +9 -0
  195. package/dist/value-validator.js +330 -0
  196. package/dist/version.d.ts +1 -1
  197. package/dist/version.js +1 -1
  198. package/package.json +10 -12
  199. package/dist/execution/index.d.ts +0 -2
  200. package/dist/execution/index.js +0 -2
  201. package/dist/filter/index.d.ts +0 -1
  202. package/dist/filter/index.js +0 -1
  203. package/dist/graph/index.d.ts +0 -4
  204. package/dist/graph/index.js +0 -4
  205. package/dist/input/metadata.d.ts +0 -99
  206. package/dist/input/metadata.js +0 -99
  207. package/dist/invocation/index.d.ts +0 -2
  208. package/dist/invocation/index.js +0 -2
  209. package/dist/ipc/index.d.ts +0 -4
  210. package/dist/ipc/index.js +0 -4
  211. package/dist/json/index.d.ts +0 -1
  212. package/dist/json/index.js +0 -1
  213. package/dist/json/value-validator.js +0 -330
  214. package/dist/node/index.d.ts +0 -1
  215. package/dist/node/index.js +0 -1
  216. package/dist/platform/node-fs.d.ts +0 -37
  217. package/dist/platform/node-fs.js +0 -81
  218. package/dist/process/control-arbiter.d.ts +0 -108
  219. package/dist/process/control-arbiter.js +0 -250
  220. package/dist/process/process-executor.d.ts +0 -47
  221. package/dist/process/process-executor.js +0 -296
  222. package/dist/profile/client-health.d.ts +0 -14
  223. package/dist/profile/client-query.d.ts +0 -30
  224. package/dist/profile/client-query.js +0 -69
  225. package/dist/profile/client.d.ts +0 -26
  226. package/dist/profile/client.js +0 -31
  227. package/dist/project/closure.d.ts +0 -25
  228. package/dist/project/closure.js +0 -74
  229. package/dist/registry/store.d.ts +0 -42
  230. package/dist/registry/store.js +0 -43
  231. package/dist/runtime/clock.d.ts +0 -7
  232. package/dist/runtime/clock.js +0 -7
  233. package/dist/runtime/module-loader.d.ts +0 -2
  234. package/dist/runtime/module-loader.js +0 -1
  235. package/dist/runtime/process.d.ts +0 -8
  236. package/dist/runtime/process.js +0 -1
  237. package/dist/runtime/runner.d.ts +0 -234
  238. package/dist/runtime/runner.js +0 -609
  239. package/dist/schema/index.d.ts +0 -1
  240. package/dist/schema/index.js +0 -1
  241. package/dist/server/dispatcher.d.ts +0 -25
  242. package/dist/server/dispatcher.js +0 -54
  243. package/dist/service/local.d.ts +0 -27
  244. package/dist/service/local.js +0 -141
  245. package/dist/storage/driver.d.ts +0 -10
  246. package/dist/storage/driver.js +0 -10
  247. package/dist/storage/lazy.d.ts +0 -8
  248. package/dist/storage/lazy.js +0 -45
  249. package/dist/storage/lock-core.d.ts +0 -158
  250. package/dist/storage/lock-core.js +0 -872
  251. package/dist/utils/net.d.ts +0 -10
  252. package/dist/utils/net.js +0 -16
  253. package/dist/utils/process.d.ts +0 -10
  254. package/dist/utils/process.js +0 -21
  255. /package/dist/{filter → utils}/intent.d.ts +0 -0
  256. /package/dist/{filter → utils}/intent.js +0 -0
@@ -1,128 +1,61 @@
1
- import { ActionDockError, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED } from "../errors.js";
2
1
  /**
3
- * 订阅者通道:封装实时事件队列、监听器与终止状态。
4
- * 职责:
5
- * - 接收实时推送事件并入队(超出背压上限时推送终止事件并切断通道)。
6
- * - 维护 done 终止标记与唤醒回调,供生成器编排消费。
7
- * - 暴露 inactive 注销钩子,供外部在通道切断时注销监听器。
2
+ * 订阅者通道:管理单个订阅者的事件缓冲队列与唤醒通知。
3
+ * 队列满时丢弃最旧事件并累计计数,不阻塞主流程,不切断通道。
8
4
  */
9
5
  class SubscriberChannel {
10
- /** 实时事件队列(含背压终止事件) */
11
6
  liveQueue = [];
12
- /** 是否已收到终止信号(finish、淘汰、中断或唤醒) */
7
+ droppedCount = 0;
13
8
  done = false;
14
- /** 实时监听器(入队与背压终止逻辑入口) */
15
- listener;
16
- /** 通道被切断或订阅退出时的注销回调(由订阅方注入) */
17
- onInactive = null;
18
- /** 等待挂起期间的唤醒回调(由等待方注入) */
19
9
  notify = null;
20
- runId;
21
10
  maxQueueSize;
22
- cursor;
23
- allocateEventId;
24
- backpressureTerminated = false;
25
- constructor(options) {
26
- this.runId = options.runId;
27
- this.maxQueueSize = options.maxQueueSize;
28
- this.cursor = options.cursor;
29
- this.allocateEventId = options.allocateEventId;
30
- this.listener = (evt) => this.receive(evt);
11
+ constructor(maxQueueSize) {
12
+ this.maxQueueSize = maxQueueSize;
31
13
  }
32
- /**
33
- * 接收单条实时事件:队列满时推送背压终止事件并切断通道。
34
- */
35
14
  receive(evt) {
36
- if (this.backpressureTerminated || this.done) {
15
+ if (this.done)
37
16
  return;
38
- }
39
17
  if (this.liveQueue.length >= this.maxQueueSize) {
40
- // 慢订阅者导致队列溢出:向该慢订阅者推送终止事件并切断通道,主运行不受反向阻塞
41
- this.backpressureTerminated = true;
42
- this.onInactive?.();
43
- const termEvt = {
44
- runId: this.runId,
45
- rootRunId: evt.rootRunId || this.runId,
46
- sequence: evt.sequence + 1,
47
- eventId: this.allocateEventId(),
48
- timestamp: new Date().toISOString(),
49
- type: "error",
50
- error: {
51
- code: EVENT_BACKPRESSURE_LIMIT,
52
- message: `Event subscription queue exceeded limit of ${this.maxQueueSize} items due to slow subscriber`,
53
- details: {
54
- lastConfirmedCursor: this.cursor.lastConfirmedEventId ??
55
- (this.cursor.lastConfirmedSequence >= 0 ? String(this.cursor.lastConfirmedSequence) : "0"),
56
- lastConfirmedEventId: this.cursor.lastConfirmedEventId,
57
- lastConfirmedSequence: this.cursor.lastConfirmedSequence,
58
- },
59
- },
60
- };
61
- this.liveQueue.length = 0;
62
- this.liveQueue.push(termEvt);
63
- if (this.notify) {
64
- this.notify();
65
- this.notify = null;
66
- }
67
- return;
18
+ this.liveQueue.shift();
19
+ this.droppedCount++;
68
20
  }
69
21
  this.liveQueue.push(evt);
70
22
  if (this.notify) {
71
- this.notify();
23
+ const cb = this.notify;
72
24
  this.notify = null;
73
- }
74
- if (evt.type === "finish") {
75
- this.done = true;
25
+ cb();
76
26
  }
77
27
  }
78
- /**
79
- * 标记通道终止并唤醒挂起的等待方。
80
- */
81
28
  terminate() {
82
29
  this.done = true;
83
30
  if (this.notify) {
84
- this.notify();
31
+ const cb = this.notify;
85
32
  this.notify = null;
33
+ cb();
86
34
  }
87
35
  }
88
36
  }
89
37
  /**
90
- * 计算单个事件的 UTF-8 JSON 字节长度。
91
- */
92
- function estimateEventBytes(event) {
93
- try {
94
- return Buffer.byteLength(JSON.stringify(event), "utf8");
95
- }
96
- catch {
97
- return 256;
98
- }
99
- }
100
- /**
101
- * 进程内有界事件缓冲区实现。
38
+ * 进程内轻量发布订阅事件广播器。
102
39
  * 遵循设计规范:
103
- * - 单运行双配额保护:上限 1024 条事件或 1MiB 字节数,优先淘汰日志与进度,保护终态。
40
+ * - 轻量发布订阅:保留订阅、退订、按 runId 过滤与事件分发。
41
+ * - 纯净广播:事件原样广播,绝不进行字节估算,绝不篡改用户执行结果。
42
+ * - 有界缓冲队列:简单条数上限(如 1000 条),队列满时丢弃最旧并计数,不切断通道。
104
43
  * - 全局有界回收:限制最大运行数(默认 500),溢出时优先淘汰已完成运行。
105
- * - 原子订阅衔接:先注册实时队列再回放历史快照,去重并杜绝并发丢事件窗口。
106
- * - 严格生命周期清理:迭代退出或中断时彻底注销监听器与 AbortSignal 事件。
107
- * - 慢订阅者背压保护:队列超出上限时推送终止事件并切断通道,主运行不受阻。
108
- * - 游标断点续传:支持全局持久化 eventId 与序列号,过期游标返回 EVENT_CURSOR_EXPIRED。
44
+ * - 错误隔离保护:单个订阅者监听器异常不扩散,保障系统整体稳定性。
109
45
  */
110
46
  export class InMemoryEventSink {
111
47
  runs = new Map();
112
48
  listeners = new Map();
113
- wakeups = new Map();
114
49
  evictions = new Map();
115
50
  evictedRuns = new Set();
116
51
  globalEventSeq = 0;
117
52
  maxEventsPerRun;
118
- maxBytesPerRun;
119
53
  maxRuns;
120
54
  maxSubscriberQueueSize;
121
55
  constructor(options = {}) {
122
- this.maxEventsPerRun = options.maxEventsPerRun ?? 1024;
123
- this.maxBytesPerRun = options.maxBytesPerRun ?? 1024 * 1024; // 1 MiB
56
+ this.maxEventsPerRun = options.maxEventsPerRun ?? 1000;
124
57
  this.maxRuns = options.maxRuns ?? 500;
125
- this.maxSubscriberQueueSize = options.maxSubscriberQueueSize ?? 1024;
58
+ this.maxSubscriberQueueSize = options.maxSubscriberQueueSize ?? 1000;
126
59
  }
127
60
  emit(event) {
128
61
  const runId = event.runId;
@@ -136,114 +69,33 @@ export class InMemoryEventSink {
136
69
  };
137
70
  }
138
71
  if (!run) {
139
- // 全局有界淘汰
140
72
  if (this.runs.size >= this.maxRuns) {
141
73
  this.evictOldestRun();
142
74
  }
143
75
  run = {
144
76
  entries: [],
145
- totalBytes: 0,
146
77
  lastAccessedAt: now,
147
78
  isTerminal: false,
148
- earliestRetainedSequence: processedEvent.sequence,
149
- earliestRetainedEventId: processedEvent.eventId,
79
+ droppedCount: 0,
150
80
  };
151
81
  this.runs.set(runId, run);
152
82
  this.evictedRuns.delete(runId);
153
83
  }
154
84
  else {
155
85
  run.lastAccessedAt = now;
156
- if (run.earliestRetainedSequence === undefined) {
157
- run.earliestRetainedSequence = processedEvent.sequence;
158
- run.earliestRetainedEventId = processedEvent.eventId;
159
- }
160
86
  }
161
- let eventBytes = estimateEventBytes(processedEvent);
162
- // 如果单条事件本身超过单运行最大字节限制,尝试截断 log / finish 数据
163
- if (eventBytes > this.maxBytesPerRun) {
164
- if (processedEvent.type === "log" && typeof processedEvent.message === "string") {
165
- const keepChars = Math.max(0, Math.floor(this.maxBytesPerRun / 2));
166
- processedEvent = {
167
- ...processedEvent,
168
- message: `${processedEvent.message.slice(0, keepChars)}... [TRUNCATED]`,
169
- data: undefined,
170
- };
171
- eventBytes = estimateEventBytes(processedEvent);
172
- }
173
- else if (processedEvent.type === "finish" && processedEvent.result) {
174
- if (processedEvent.result.ok) {
175
- processedEvent = {
176
- ...processedEvent,
177
- result: {
178
- ok: true,
179
- runId: processedEvent.result.runId,
180
- data: { _truncated: true, message: "Output exceeded maximum quota" },
181
- },
182
- };
183
- }
184
- else {
185
- const err = processedEvent.result.error;
186
- const keepChars = Math.max(0, Math.min(200, Math.floor(this.maxBytesPerRun / 2)));
187
- const truncatedMsg = typeof err?.message === "string"
188
- ? `${err.message.slice(0, keepChars)}... [TRUNCATED]`
189
- : "Error exceeded maximum quota";
190
- processedEvent = {
191
- ...processedEvent,
192
- result: {
193
- ok: false,
194
- runId: processedEvent.result.runId,
195
- error: {
196
- code: err?.code || "EXECUTION_ERROR",
197
- message: truncatedMsg,
198
- },
199
- },
200
- };
201
- }
202
- eventBytes = estimateEventBytes(processedEvent);
203
- }
204
- }
205
- // 严格限制单运行缓存字节配额,超标事件不可入队,杜绝突破 maxBytesPerRun
206
- if (eventBytes <= this.maxBytesPerRun) {
207
- // 检查是否超出单运行条数或字节配额,按优先级淘汰
208
- while (run.entries.length > 0 &&
209
- (run.entries.length >= this.maxEventsPerRun ||
210
- run.totalBytes + eventBytes > this.maxBytesPerRun)) {
211
- // 优先淘汰非核心事件:log 或 progress
212
- const nonEssentialIndex = run.entries.findIndex((e) => e.event.type === "log" || e.event.type === "progress");
213
- if (nonEssentialIndex >= 0) {
214
- const removed = run.entries.splice(nonEssentialIndex, 1)[0];
215
- run.totalBytes -= removed.bytes;
216
- }
217
- else {
218
- // 无非核心事件时,淘汰最早的非 finish 事件
219
- const nonTerminalIndex = run.entries.findIndex((e) => e.event.type !== "finish");
220
- if (nonTerminalIndex >= 0) {
221
- const removed = run.entries.splice(nonTerminalIndex, 1)[0];
222
- run.totalBytes -= removed.bytes;
223
- }
224
- else {
225
- const removed = run.entries.shift();
226
- run.totalBytes -= removed.bytes;
227
- }
228
- }
229
- }
230
- if (run.entries.length > 0) {
231
- run.earliestRetainedSequence = run.entries[0].event.sequence;
232
- run.earliestRetainedEventId = run.entries[0].event.eventId;
233
- }
234
- if (run.totalBytes + eventBytes <= this.maxBytesPerRun) {
235
- run.entries.push({ event: processedEvent, bytes: eventBytes });
236
- run.totalBytes += eventBytes;
237
- }
87
+ while (run.entries.length >= this.maxEventsPerRun) {
88
+ run.entries.shift();
89
+ run.droppedCount++;
238
90
  }
239
- // 仅在接收到 finish 事件时才标记已终态,防止在终态 status 与 finish 之间的窗口内订阅过早结束
91
+ run.entries.push(processedEvent);
240
92
  if (processedEvent.type === "finish") {
241
93
  run.isTerminal = true;
242
94
  }
243
- // 广播至当前活跃监听器
95
+ // 广播至当前活跃监听器,错误隔离保护
244
96
  const subs = this.listeners.get(runId);
245
97
  if (subs && subs.size > 0) {
246
- for (const listener of subs) {
98
+ for (const listener of Array.from(subs)) {
247
99
  try {
248
100
  listener(processedEvent);
249
101
  }
@@ -254,7 +106,10 @@ export class InMemoryEventSink {
254
106
  }
255
107
  }
256
108
  async *subscribe(runId, options = {}) {
257
- let afterSequence;
109
+ if (options.signal?.aborted || this.evictedRuns.has(runId)) {
110
+ return;
111
+ }
112
+ let afterSequence = -1;
258
113
  let afterEventId;
259
114
  if (options.after !== undefined) {
260
115
  if (typeof options.after === "number") {
@@ -265,61 +120,37 @@ export class InMemoryEventSink {
265
120
  if (/^-?\d+$/.test(trimmed)) {
266
121
  afterSequence = parseInt(trimmed, 10);
267
122
  }
268
- afterEventId = trimmed;
123
+ else {
124
+ afterEventId = trimmed;
125
+ }
269
126
  }
270
127
  }
271
- const effectiveAfter = afterSequence ?? -1;
272
- const cursor = {
273
- lastYieldedSequence: effectiveAfter,
274
- lastConfirmedEventId: afterEventId,
275
- lastConfirmedSequence: effectiveAfter,
276
- };
277
- if (options.signal?.aborted || this.evictedRuns.has(runId)) {
278
- return;
279
- }
280
- // 检查游标是否在请求前已过期被清理
281
- const run = this.runs.get(runId);
282
- if (run && options.after !== undefined) {
283
- this.assertCursorRetained(run, options.after, afterSequence);
284
- }
285
- const channel = new SubscriberChannel({
286
- runId,
287
- maxQueueSize: options.maxQueueSize ?? this.maxSubscriberQueueSize,
288
- cursor,
289
- allocateEventId: () => String(++this.globalEventSeq),
290
- });
128
+ let lastYieldedSequence = afterSequence;
129
+ const maxQueueSize = options.maxQueueSize ?? this.maxSubscriberQueueSize;
130
+ const channel = new SubscriberChannel(maxQueueSize);
131
+ const listener = (evt) => channel.receive(evt);
291
132
  let subs = this.listeners.get(runId);
292
133
  if (!subs) {
293
134
  subs = new Set();
294
135
  this.listeners.set(runId, subs);
295
136
  }
296
- const cleanupListener = () => {
297
- subs?.delete(channel.listener);
298
- if (subs && subs.size === 0) {
299
- this.listeners.delete(runId);
300
- }
301
- };
302
- channel.onInactive = cleanupListener;
137
+ subs.add(listener);
303
138
  let evictSet = this.evictions.get(runId);
304
139
  if (!evictSet) {
305
140
  evictSet = new Set();
306
141
  this.evictions.set(runId, evictSet);
307
142
  }
308
143
  const onEvict = () => channel.terminate();
309
- const onAbort = () => channel.terminate();
310
- let currentWakeupCleanup = null;
311
- // 先挂载实时监听器,防止历史回放与监听注册之间的竞态丢事件
312
- subs.add(channel.listener);
313
144
  evictSet.add(onEvict);
145
+ const onAbort = () => channel.terminate();
314
146
  if (options.signal) {
315
147
  options.signal.addEventListener("abort", onAbort, { once: true });
316
148
  }
317
149
  const cleanup = () => {
318
- if (currentWakeupCleanup) {
319
- currentWakeupCleanup();
320
- currentWakeupCleanup = null;
150
+ subs?.delete(listener);
151
+ if (subs && subs.size === 0) {
152
+ this.listeners.delete(runId);
321
153
  }
322
- cleanupListener();
323
154
  evictSet?.delete(onEvict);
324
155
  if (evictSet && evictSet.size === 0) {
325
156
  this.evictions.delete(runId);
@@ -328,30 +159,42 @@ export class InMemoryEventSink {
328
159
  options.signal.removeEventListener("abort", onAbort);
329
160
  }
330
161
  };
162
+ const isDeliverable = (evt) => {
163
+ if (afterEventId) {
164
+ if (evt.eventId === afterEventId) {
165
+ lastYieldedSequence = Math.max(lastYieldedSequence, evt.sequence);
166
+ afterEventId = undefined;
167
+ }
168
+ return false;
169
+ }
170
+ return evt.sequence > lastYieldedSequence;
171
+ };
331
172
  try {
332
- // 回放历史快照
173
+ // 1. 回放历史快照
174
+ const run = this.runs.get(runId);
333
175
  if (run) {
334
176
  run.lastAccessedAt = Date.now();
335
- const historySnapshot = run.entries.map((e) => e.event);
336
- for (const evt of historySnapshot) {
177
+ const snapshot = [...run.entries];
178
+ for (const evt of snapshot) {
337
179
  if (options.signal?.aborted)
338
180
  return;
339
- if (evt.sequence > cursor.lastYieldedSequence) {
340
- this.trackDeliveredEvent(cursor, evt);
181
+ if (isDeliverable(evt)) {
182
+ lastYieldedSequence = evt.sequence;
341
183
  yield evt;
342
- if (this.isTerminalDelivery(evt)) {
184
+ if (evt.type === "finish") {
343
185
  channel.done = true;
344
186
  return;
345
187
  }
346
188
  }
347
189
  }
348
- // 若该运行已终态且历史中已无 finish 事件,先排空实时队列已到达的事件
190
+ // 若历史记录已终态,排空实时队列后直接结束
349
191
  if (run.isTerminal) {
350
192
  while (channel.liveQueue.length > 0) {
351
- const evt = this.takeNextDeliverable(channel, cursor);
352
- if (evt) {
193
+ const evt = channel.liveQueue.shift();
194
+ if (isDeliverable(evt)) {
195
+ lastYieldedSequence = evt.sequence;
353
196
  yield evt;
354
- if (this.isTerminalDelivery(evt)) {
197
+ if (evt.type === "finish") {
355
198
  channel.done = true;
356
199
  return;
357
200
  }
@@ -361,38 +204,38 @@ export class InMemoryEventSink {
361
204
  return;
362
205
  }
363
206
  }
364
- // 无缝衔接消费实时队列中的事件
365
- while ((channel.liveQueue.length > 0 || !channel.done) && !options.signal?.aborted) {
207
+ // 2. 消费实时事件队列
208
+ while (!options.signal?.aborted) {
366
209
  if (this.evictedRuns.has(runId)) {
367
- channel.done = true;
368
210
  break;
369
211
  }
370
212
  if (channel.liveQueue.length > 0) {
371
- const evt = this.takeNextDeliverable(channel, cursor);
372
- if (evt) {
213
+ const evt = channel.liveQueue.shift();
214
+ if (isDeliverable(evt)) {
215
+ lastYieldedSequence = evt.sequence;
373
216
  yield evt;
374
- if (this.isTerminalDelivery(evt)) {
217
+ if (evt.type === "finish") {
375
218
  channel.done = true;
376
219
  return;
377
220
  }
378
221
  }
379
222
  }
380
223
  else {
381
- if (this.evictedRuns.has(runId)) {
382
- channel.done = true;
224
+ if (channel.done) {
383
225
  break;
384
226
  }
385
- await this.waitForNextEvent(runId, channel, (cleanupFn) => {
386
- currentWakeupCleanup = cleanupFn;
227
+ await new Promise((resolve) => {
228
+ channel.notify = resolve;
387
229
  });
388
230
  }
389
231
  }
390
- // 消费中止或完成瞬间积压在 liveQueue 中的剩余事件
232
+ // 3. 消费中止或完成瞬间积压的剩余事件
391
233
  while (channel.liveQueue.length > 0) {
392
- const evt = this.takeNextDeliverable(channel, cursor);
393
- if (evt) {
234
+ const evt = channel.liveQueue.shift();
235
+ if (isDeliverable(evt)) {
236
+ lastYieldedSequence = evt.sequence;
394
237
  yield evt;
395
- if (this.isTerminalDelivery(evt)) {
238
+ if (evt.type === "finish") {
396
239
  return;
397
240
  }
398
241
  }
@@ -402,108 +245,6 @@ export class InMemoryEventSink {
402
245
  cleanup();
403
246
  }
404
247
  }
405
- /**
406
- * 断言请求游标仍处于保留窗口内(过期则抛出 EVENT_CURSOR_EXPIRED 错误)。
407
- */
408
- assertCursorRetained(run, after, afterSequence) {
409
- const earliestSeq = run.earliestRetainedSequence ?? 0;
410
- if (afterSequence !== undefined && afterSequence >= 0 && afterSequence < earliestSeq - 1) {
411
- const earliestCursor = run.earliestRetainedEventId ?? String(earliestSeq);
412
- throw new ActionDockError(EVENT_CURSOR_EXPIRED, `Event cursor '${after}' has expired; earliest available cursor is '${earliestCursor}'`, {
413
- cursor: after,
414
- earliestCursor,
415
- earliestRetainedCursor: earliestCursor,
416
- earliestSequence: earliestSeq,
417
- earliestEventId: run.earliestRetainedEventId,
418
- });
419
- }
420
- }
421
- /**
422
- * 记录已交付事件并推进游标(已交付序号与最后确认游标)。
423
- */
424
- trackDeliveredEvent(cursor, evt) {
425
- cursor.lastYieldedSequence = evt.sequence;
426
- cursor.lastConfirmedSequence = evt.sequence;
427
- if (evt.eventId)
428
- cursor.lastConfirmedEventId = evt.eventId;
429
- }
430
- /**
431
- * 终止谓词:finish 事件或背压终止事件交付后订阅应立即结束。
432
- */
433
- isTerminalDelivery(evt) {
434
- return (evt.type === "finish" ||
435
- (evt.type === "error" && evt.error?.code === EVENT_BACKPRESSURE_LIMIT));
436
- }
437
- /**
438
- * 从实时队列取出下一条应交付的事件(跳过已交付序号),并同步推进游标。
439
- */
440
- takeNextDeliverable(channel, cursor) {
441
- const evt = channel.liveQueue.shift();
442
- if (!evt)
443
- return undefined;
444
- if (evt.sequence > cursor.lastYieldedSequence || evt.type === "error") {
445
- this.trackDeliveredEvent(cursor, evt);
446
- return evt;
447
- }
448
- return undefined;
449
- }
450
- /**
451
- * 挂起等待下一条实时事件、通道终止或运行淘汰唤醒(严格注销监听避免泄漏)。
452
- */
453
- async waitForNextEvent(runId, channel, registerCleanup) {
454
- await new Promise((resolve) => {
455
- let runWakeups = this.wakeups.get(runId);
456
- if (!runWakeups) {
457
- runWakeups = new Set();
458
- this.wakeups.set(runId, runWakeups);
459
- }
460
- const unregister = () => {
461
- runWakeups?.delete(onWakeup);
462
- if (runWakeups && runWakeups.size === 0) {
463
- this.wakeups.delete(runId);
464
- }
465
- registerCleanup(null);
466
- };
467
- const onWakeup = () => {
468
- channel.done = true;
469
- unregister();
470
- resolve();
471
- };
472
- runWakeups.add(onWakeup);
473
- registerCleanup(unregister);
474
- channel.notify = () => {
475
- unregister();
476
- resolve();
477
- };
478
- });
479
- }
480
- /**
481
- * 手动修剪已终态运行中早于指定序列号的历史事件(用于模拟过期清理)。
482
- */
483
- pruneEvents(runId, upToSequence) {
484
- const run = this.runs.get(runId);
485
- if (!run)
486
- return;
487
- const idx = run.entries.findIndex((e) => e.event.sequence > upToSequence);
488
- if (idx > 0) {
489
- const removed = run.entries.splice(0, idx);
490
- for (const r of removed) {
491
- run.totalBytes -= r.bytes;
492
- }
493
- }
494
- else if (idx === -1 && run.entries.length > 0) {
495
- run.entries = [];
496
- run.totalBytes = 0;
497
- }
498
- if (run.entries.length > 0) {
499
- run.earliestRetainedSequence = run.entries[0].event.sequence;
500
- run.earliestRetainedEventId = run.entries[0].event.eventId;
501
- }
502
- else {
503
- run.earliestRetainedSequence = upToSequence + 1;
504
- run.earliestRetainedEventId = undefined;
505
- }
506
- }
507
248
  clear(runId) {
508
249
  this.evictedRuns.add(runId);
509
250
  if (this.evictedRuns.size > 2000) {
@@ -526,43 +267,20 @@ export class InMemoryEventSink {
526
267
  }
527
268
  }
528
269
  }
529
- const waiting = this.wakeups.get(runId);
530
- if (waiting) {
531
- const cbs = Array.from(waiting);
532
- this.wakeups.delete(runId);
533
- for (const wake of cbs) {
534
- try {
535
- wake();
536
- }
537
- catch {
538
- // ignore
539
- }
540
- }
541
- }
542
270
  }
543
- /**
544
- * 获取指定运行的事件统计信息(调试与测试用)。
545
- */
546
271
  getRunStats(runId) {
547
272
  const run = this.runs.get(runId);
548
273
  if (!run)
549
274
  return undefined;
550
275
  return {
551
276
  count: run.entries.length,
552
- bytes: run.totalBytes,
553
277
  isTerminal: run.isTerminal,
278
+ droppedCount: run.droppedCount,
554
279
  };
555
280
  }
556
- /**
557
- * 获取全局缓存的运行数。
558
- */
559
281
  getRunCount() {
560
282
  return this.runs.size;
561
283
  }
562
- /**
563
- * 淘汰最久未活跃的运行记录。
564
- * 优先淘汰已终态运行;若无终态运行则淘汰最旧活动运行,严格保证 runs.size <= maxRuns。
565
- */
566
284
  evictOldestRun() {
567
285
  let candidateRunId = null;
568
286
  let candidateAccess = Infinity;
@@ -586,16 +304,18 @@ export class InMemoryEventSink {
586
304
  this.clear(candidateRunId);
587
305
  }
588
306
  }
589
- /**
590
- * 关闭事件接收器并唤醒所有活跃订阅者退出。
591
- */
592
307
  close() {
593
- for (const wakeups of this.wakeups.values()) {
594
- for (const onWakeup of wakeups) {
595
- onWakeup();
308
+ for (const evictSet of this.evictions.values()) {
309
+ for (const onEvict of evictSet) {
310
+ try {
311
+ onEvict();
312
+ }
313
+ catch {
314
+ // ignore
315
+ }
596
316
  }
597
317
  }
598
- this.wakeups.clear();
318
+ this.evictions.clear();
599
319
  this.listeners.clear();
600
320
  }
601
321
  }
@@ -3,10 +3,6 @@ export * from "./context.js";
3
3
  export * from "./action-collection.js";
4
4
  export * from "./action-registry.js";
5
5
  export * from "./run-persistence.js";
6
- export * from "./runner.js";
7
6
  export * from "./standalone.js";
8
7
  export * from "./env.js";
9
- export * from "./clock.js";
10
- export * from "./process.js";
11
8
  export * from "./events.js";
12
- export * from "./module-loader.js";
@@ -3,10 +3,6 @@ export * from "./context.js";
3
3
  export * from "./action-collection.js";
4
4
  export * from "./action-registry.js";
5
5
  export * from "./run-persistence.js";
6
- export * from "./runner.js";
7
6
  export * from "./standalone.js";
8
7
  export * from "./env.js";
9
- export * from "./clock.js";
10
- export * from "./process.js";
11
8
  export * from "./events.js";
12
- export * from "./module-loader.js";
@@ -17,6 +17,8 @@ export interface InitialRunRecordInput {
17
17
  ownerId?: string;
18
18
  /** 执行宿主会话标识 */
19
19
  hostSessionId?: string;
20
+ /** 执行宿主进程标识(存活判定依据,落库到运行记录) */
21
+ hostPid?: number;
20
22
  /** 包物理实例标识 */
21
23
  packageInstanceId?: string;
22
24
  /** 快照代次标识 */