@actiondock/core 2.10.0 → 2.12.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 (252) 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 +8 -14
  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 +20 -88
  28. package/dist/doctor/checks/runtime.d.ts +0 -4
  29. package/dist/doctor/checks/runtime.js +0 -28
  30. package/dist/doctor/doctor.js +1 -2
  31. package/dist/errors.d.ts +53 -3
  32. package/dist/errors.js +53 -3
  33. package/dist/execution/service.d.ts +55 -37
  34. package/dist/execution/service.js +601 -212
  35. package/dist/execution/types.d.ts +24 -5
  36. package/dist/host/host.d.ts +42 -22
  37. package/dist/host/host.js +562 -149
  38. package/dist/host/routing.js +6 -6
  39. package/dist/host/types.d.ts +25 -13
  40. package/dist/index.d.ts +9 -4
  41. package/dist/index.js +5 -2
  42. package/dist/input/advice.d.ts +0 -44
  43. package/dist/input/advice.js +7 -141
  44. package/dist/input/flat-materializer.js +1 -1
  45. package/dist/input/flat-parser.js +1 -1
  46. package/dist/input/index.d.ts +0 -1
  47. package/dist/input/index.js +0 -1
  48. package/dist/input/input-resolver.js +1 -1
  49. package/dist/input/validation-mapper.d.ts +1 -1
  50. package/dist/ipc/diagnostic.d.ts +8 -21
  51. package/dist/ipc/diagnostic.js +24 -57
  52. package/dist/ipc/host.js +21 -7
  53. package/dist/ipc/service.d.ts +14 -0
  54. package/dist/ipc/service.js +132 -26
  55. package/dist/ipc/types.d.ts +0 -4
  56. package/dist/package/index.d.ts +4 -5
  57. package/dist/package/index.js +1 -2
  58. package/dist/package/runtime.d.ts +94 -23
  59. package/dist/package/runtime.js +628 -274
  60. package/dist/package/static-index.d.ts +3 -6
  61. package/dist/package/static-index.js +11 -40
  62. package/dist/package/types.d.ts +19 -4
  63. package/dist/platform/index.d.ts +2 -1
  64. package/dist/platform/index.js +2 -1
  65. package/dist/platform/loader-hook.d.ts +32 -0
  66. package/dist/platform/loader-hook.js +90 -0
  67. package/dist/{node → platform}/module-loader.d.ts +13 -0
  68. package/dist/{node → platform}/module-loader.js +51 -4
  69. package/dist/platform/node.d.ts +19 -8
  70. package/dist/platform/node.js +50 -20
  71. package/dist/platform/types.d.ts +7 -47
  72. package/dist/process/context-process.d.ts +3 -18
  73. package/dist/process/context-process.js +3 -44
  74. package/dist/process/control.d.ts +21 -0
  75. package/dist/process/control.js +34 -0
  76. package/dist/process/converters.d.ts +23 -0
  77. package/dist/process/converters.js +92 -0
  78. package/dist/process/cursor.js +2 -22
  79. package/dist/process/{diagnostics-sink.d.ts → diagnostics.d.ts} +0 -7
  80. package/dist/process/{diagnostics-sink.js → diagnostics.js} +6 -8
  81. package/dist/process/index.d.ts +1 -3
  82. package/dist/process/index.js +1 -3
  83. package/dist/process/input-dispatcher.d.ts +4 -13
  84. package/dist/process/input-dispatcher.js +48 -69
  85. package/dist/process/managed-record.d.ts +3 -31
  86. package/dist/process/metadata-store.d.ts +0 -41
  87. package/dist/process/metadata-store.js +2 -341
  88. package/dist/process/process-driver.js +2 -2
  89. package/dist/process/process-manager.d.ts +8 -50
  90. package/dist/process/process-manager.js +28 -417
  91. package/dist/process/quotas.d.ts +14 -0
  92. package/dist/process/quotas.js +29 -0
  93. package/dist/process/run-executor.js +0 -1
  94. package/dist/process/terminal-output-cache.d.ts +20 -34
  95. package/dist/process/terminal-output-cache.js +43 -64
  96. package/dist/profile/index.d.ts +1 -2
  97. package/dist/profile/index.js +1 -2
  98. package/dist/profile/manager.d.ts +2 -4
  99. package/dist/profile/manager.js +10 -40
  100. package/dist/profile/types.d.ts +1 -17
  101. package/dist/project/digest.d.ts +5 -1
  102. package/dist/project/digest.js +81 -219
  103. package/dist/project/index.d.ts +4 -5
  104. package/dist/project/index.js +4 -5
  105. package/dist/project/init.js +2 -2
  106. package/dist/project/loader.d.ts +3 -3
  107. package/dist/project/loader.js +72 -50
  108. package/dist/project/lockfile.js +6 -5
  109. package/dist/project/manifest.js +5 -4
  110. package/dist/project/transactions.d.ts +4 -42
  111. package/dist/project/transactions.js +83 -138
  112. package/dist/project/types-generator.js +2 -1
  113. package/dist/project/types.d.ts +2 -0
  114. package/dist/registry/index.d.ts +0 -1
  115. package/dist/registry/index.js +0 -1
  116. package/dist/registry/lock.d.ts +2 -11
  117. package/dist/registry/lock.js +14 -183
  118. package/dist/registry/registry.d.ts +1 -6
  119. package/dist/registry/registry.js +5 -17
  120. package/dist/runtime/action-registry.d.ts +1 -5
  121. package/dist/runtime/action-registry.js +0 -12
  122. package/dist/runtime/context.js +2 -2
  123. package/dist/runtime/events.d.ts +11 -50
  124. package/dist/runtime/events.js +88 -368
  125. package/dist/runtime/index.d.ts +0 -4
  126. package/dist/runtime/index.js +0 -4
  127. package/dist/runtime/run-persistence.d.ts +2 -0
  128. package/dist/runtime/run-persistence.js +2 -1
  129. package/dist/runtime/standalone-commands.d.ts +18 -0
  130. package/dist/runtime/standalone-commands.js +466 -0
  131. package/dist/runtime/standalone-help.d.ts +20 -0
  132. package/dist/runtime/standalone-help.js +72 -0
  133. package/dist/runtime/standalone.d.ts +1 -44
  134. package/dist/runtime/standalone.js +32 -573
  135. package/dist/schema/validator.d.ts +2 -2
  136. package/dist/schema/validator.js +2 -9
  137. package/dist/server/index.d.ts +0 -2
  138. package/dist/server/index.js +0 -2
  139. package/dist/server/routes/actions.js +1 -1
  140. package/dist/server/routes/info.js +1 -1
  141. package/dist/server/routes/playbooks.js +1 -1
  142. package/dist/server/routes/runs.js +123 -16
  143. package/dist/server/routes/state.js +43 -18
  144. package/dist/server/security.d.ts +1 -1
  145. package/dist/server/security.js +2 -2
  146. package/dist/server/server.js +3 -7
  147. package/dist/service/factory.js +4 -3
  148. package/dist/service/index.d.ts +1 -1
  149. package/dist/service/index.js +1 -1
  150. package/dist/service/remote-errors.d.ts +1 -1
  151. package/dist/service/remote-errors.js +6 -6
  152. package/dist/service/remote-polling.d.ts +3 -0
  153. package/dist/service/remote-polling.js +10 -5
  154. package/dist/service/remote.d.ts +5 -1
  155. package/dist/service/remote.js +108 -17
  156. package/dist/service/sse-stream.js +2 -2
  157. package/dist/service/types.d.ts +22 -19
  158. package/dist/service/types.js +1 -24
  159. package/dist/storage/clock.d.ts +1 -1
  160. package/dist/storage/clock.js +1 -1
  161. package/dist/storage/data-dir-lock.d.ts +10 -22
  162. package/dist/storage/data-dir-lock.js +28 -56
  163. package/dist/storage/file-lock.d.ts +64 -0
  164. package/dist/storage/file-lock.js +447 -0
  165. package/dist/storage/index.d.ts +2 -2
  166. package/dist/storage/index.js +5 -4
  167. package/dist/storage/run-liveness.d.ts +48 -0
  168. package/dist/storage/run-liveness.js +44 -0
  169. package/dist/storage/sqlite-config.d.ts +16 -0
  170. package/dist/storage/sqlite-config.js +59 -0
  171. package/dist/storage/sqlite-driver.d.ts +3 -1
  172. package/dist/storage/sqlite-driver.js +14 -4
  173. package/dist/storage/sqlite-events.d.ts +13 -0
  174. package/dist/storage/sqlite-events.js +48 -0
  175. package/dist/storage/sqlite-runs.d.ts +107 -0
  176. package/dist/storage/sqlite-runs.js +622 -0
  177. package/dist/storage/sqlite-schema.d.ts +5 -0
  178. package/dist/storage/sqlite-schema.js +143 -0
  179. package/dist/storage/sqlite-state.d.ts +33 -0
  180. package/dist/storage/sqlite-state.js +270 -0
  181. package/dist/storage/sqlite.d.ts +30 -57
  182. package/dist/storage/sqlite.js +84 -741
  183. package/dist/storage/types.d.ts +35 -2
  184. package/dist/utils/index.d.ts +18 -14
  185. package/dist/utils/index.js +37 -29
  186. package/dist/utils/semver.d.ts +18 -0
  187. package/dist/utils/semver.js +19 -0
  188. package/dist/utils/warning.d.ts +1 -0
  189. package/dist/utils/warning.js +52 -0
  190. package/dist/{json/value-validator.d.ts → value-validator.d.ts} +9 -0
  191. package/dist/value-validator.js +330 -0
  192. package/dist/version.d.ts +1 -1
  193. package/dist/version.js +1 -1
  194. package/package.json +10 -12
  195. package/dist/execution/index.d.ts +0 -2
  196. package/dist/execution/index.js +0 -2
  197. package/dist/filter/index.d.ts +0 -1
  198. package/dist/filter/index.js +0 -1
  199. package/dist/graph/index.d.ts +0 -4
  200. package/dist/graph/index.js +0 -4
  201. package/dist/input/metadata.d.ts +0 -99
  202. package/dist/input/metadata.js +0 -99
  203. package/dist/invocation/index.d.ts +0 -2
  204. package/dist/invocation/index.js +0 -2
  205. package/dist/ipc/index.d.ts +0 -4
  206. package/dist/ipc/index.js +0 -4
  207. package/dist/json/index.d.ts +0 -1
  208. package/dist/json/index.js +0 -1
  209. package/dist/json/value-validator.js +0 -330
  210. package/dist/node/index.d.ts +0 -1
  211. package/dist/node/index.js +0 -1
  212. package/dist/platform/node-fs.d.ts +0 -37
  213. package/dist/platform/node-fs.js +0 -81
  214. package/dist/process/control-arbiter.d.ts +0 -108
  215. package/dist/process/control-arbiter.js +0 -250
  216. package/dist/process/process-executor.d.ts +0 -47
  217. package/dist/process/process-executor.js +0 -296
  218. package/dist/profile/client-health.d.ts +0 -14
  219. package/dist/profile/client-query.d.ts +0 -30
  220. package/dist/profile/client-query.js +0 -69
  221. package/dist/profile/client.d.ts +0 -26
  222. package/dist/profile/client.js +0 -31
  223. package/dist/project/closure.d.ts +0 -25
  224. package/dist/project/closure.js +0 -74
  225. package/dist/registry/store.d.ts +0 -42
  226. package/dist/registry/store.js +0 -43
  227. package/dist/runtime/clock.d.ts +0 -7
  228. package/dist/runtime/clock.js +0 -7
  229. package/dist/runtime/module-loader.d.ts +0 -2
  230. package/dist/runtime/module-loader.js +0 -1
  231. package/dist/runtime/process.d.ts +0 -8
  232. package/dist/runtime/process.js +0 -1
  233. package/dist/runtime/runner.d.ts +0 -234
  234. package/dist/runtime/runner.js +0 -609
  235. package/dist/schema/index.d.ts +0 -1
  236. package/dist/schema/index.js +0 -1
  237. package/dist/server/dispatcher.d.ts +0 -25
  238. package/dist/server/dispatcher.js +0 -54
  239. package/dist/service/local.d.ts +0 -27
  240. package/dist/service/local.js +0 -141
  241. package/dist/storage/driver.d.ts +0 -10
  242. package/dist/storage/driver.js +0 -10
  243. package/dist/storage/lazy.d.ts +0 -8
  244. package/dist/storage/lazy.js +0 -45
  245. package/dist/storage/lock-core.d.ts +0 -158
  246. package/dist/storage/lock-core.js +0 -872
  247. package/dist/utils/net.d.ts +0 -10
  248. package/dist/utils/net.js +0 -16
  249. package/dist/utils/process.d.ts +0 -10
  250. package/dist/utils/process.js +0 -21
  251. /package/dist/{filter → utils}/intent.d.ts +0 -0
  252. /package/dist/{filter → utils}/intent.js +0 -0
@@ -2,50 +2,88 @@ import { existsSync } from "node:fs";
2
2
  import { parseActionRef } from "../catalog/resolve-action.js";
3
3
  import { computeDigest } from "../project/digest.js";
4
4
  import { loadActions, loadProjectConfig } from "../project/loader.js";
5
+ import { SystemClock } from "../storage/clock.js";
5
6
  import { InMemoryEventSink } from "../runtime/events.js";
6
- import { ActionRunner } from "../runtime/runner.js";
7
- import { ActionDockError, ACTION_NOT_FOUND, EXECUTION_FAILED, IDEMPOTENCY_CONFLICT, RUN_REPOSITORY_UNAVAILABLE, UNHANDLED_EXECUTION_ERROR, describeActionLoadFailure, } from "../errors.js";
7
+ import { ActionDockError, ACTION_CANCELLED, ACTION_FAILED, ACTION_NOT_FOUND, ACTION_TIMEOUT, EXECUTION_CONCURRENCY_LIMIT, EXECUTION_FAILED, IDEMPOTENCY_CONFLICT, INPUT_NOT_JSON, INPUT_VALIDATION_FAILED, INVALID_ARGUMENT, INVOCATION_UNSUPPORTED, OUTPUT_NOT_JSON, OUTPUT_VALIDATION_FAILED, RUN_REPOSITORY_UNAVAILABLE, SERVICE_CLOSED, UNHANDLED_EXECUTION_ERROR, describeActionLoadFailure, } from "../errors.js";
8
8
  import { resultStatusToRunStatus } from "../storage/types.js";
9
+ import { createDefaultProcessOwner, } from "../invocation/types.js";
10
+ import { ActionRegistry, isActionDefinitionObject, resolveAnonymousActionId, } from "../runtime/action-registry.js";
11
+ import { buildInitialRunRecord, createRunFinalizer, createRunOrThrow, tryCreateRun, } from "../runtime/run-persistence.js";
12
+ import { createActionContext } from "../runtime/context.js";
13
+ import { validateActionInputValue, validateJsonValue } from "../value-validator.js";
14
+ import { validateSchemaOnly } from "../schema/validator.js";
9
15
  /**
10
- * 统一执行协调服务实现。
16
+ * 在途运行心跳刷新间隔(毫秒)。
17
+ *
18
+ * 必须显著小于存活判定的宽限期(见 run-liveness 的 RUN_LIVENESS_GRACE_MS),
19
+ * 保证心跳正常的宿主绝不被误判过期;间隔内崩溃由进程探测兜底,
20
+ * 心跳只服务无进程标识的遗留记录收敛判定。
21
+ */
22
+ export const RUN_HEARTBEAT_INTERVAL_MS = 30_000;
23
+ /**
24
+ * 将输入值校验结果映射为标准运行时错误(单一事实源)。
25
+ */
26
+ function buildInputValidationError(targetActionId, check) {
27
+ if (check.kind === "json-value") {
28
+ return {
29
+ code: INPUT_NOT_JSON,
30
+ message: `Input validation failed for action '${targetActionId}': ${check.reason}`,
31
+ };
32
+ }
33
+ return {
34
+ code: INPUT_VALIDATION_FAILED,
35
+ message: `Input validation failed for action '${targetActionId}': ${check.reason}`,
36
+ details: [check.reason],
37
+ };
38
+ }
39
+ /**
40
+ * 统一执行引擎(融合原协调服务与运行器)。
41
+ * 消除双层 AbortController 与重复参数透传,收敛唯一生命周期与存储落库。
11
42
  */
12
43
  export class DefaultExecutionService {
13
44
  identity;
14
45
  packageId;
46
+ packageInstanceId;
47
+ generationId;
48
+ hostSessionId;
49
+ /** 宿主进程标识:落库到运行记录供跨进程存活判定(并发打开同一数据目录时不误收割) */
50
+ hostPid;
51
+ eventSink;
15
52
  storage;
53
+ globalStorage;
16
54
  projectConfig;
17
55
  projectRoot;
56
+ configOverrides;
18
57
  moduleLoader;
19
- eventSink;
20
- packageInstanceId;
21
- generationId;
22
58
  maxActiveRuns;
23
59
  ownerId;
24
- hostSessionId;
25
- _runner;
60
+ actionResolver;
26
61
  logger;
27
62
  clock;
28
63
  process;
29
- actionResolver;
30
- activeRuns = new Map();
31
- globalStorage;
32
64
  actionInvoker;
65
+ registry;
66
+ activeRuns = new Map();
67
+ /** 在途运行心跳定时器:周期性刷新心跳时间戳,支撑无进程标识判定路径的兜底收敛 */
68
+ heartbeatTimer;
33
69
  isClosing = false;
34
70
  reservedSlots = 0;
35
71
  constructor(options) {
36
72
  if (!options.identity) {
37
- throw new Error("DefaultExecutionService requires 'identity' PackageIdentity option");
73
+ throw new ActionDockError(INVALID_ARGUMENT, "DefaultExecutionService requires 'identity' PackageIdentity option");
38
74
  }
39
75
  if (!options.storage) {
40
- throw new Error("DefaultExecutionService requires 'storage' option");
76
+ throw new ActionDockError(INVALID_ARGUMENT, "DefaultExecutionService requires 'storage' option");
41
77
  }
42
78
  this.identity = options.identity;
43
79
  this.packageId = this.identity.id;
44
80
  this.packageInstanceId = this.identity.instanceId;
45
81
  this.generationId = this.identity.generation;
46
82
  this.hostSessionId = options.hostSessionId;
83
+ this.hostPid = process.pid;
47
84
  this.projectConfig = options.projectConfig;
48
85
  this.projectRoot = options.projectRoot;
86
+ this.configOverrides = options.configOverrides || {};
49
87
  this.moduleLoader = options.moduleLoader;
50
88
  this.eventSink = options.eventSink || new InMemoryEventSink();
51
89
  this.maxActiveRuns = options.maxActiveRuns || 32;
@@ -55,65 +93,58 @@ export class DefaultExecutionService {
55
93
  this.actionInvoker = options.actionInvoker;
56
94
  this.storage = options.storage;
57
95
  this.globalStorage = options.globalStorage;
58
- this.clock = options.clock;
96
+ this.clock = options.clock ?? new SystemClock();
59
97
  this.process = options.process;
60
- this._runner = new ActionRunner({
61
- identity: this.identity,
62
- hostSessionId: this.hostSessionId,
63
- storage: this.storage,
64
- globalStorage: this.globalStorage,
65
- projectRoot: options.projectRoot,
66
- projectConfig: this.projectConfig,
67
- configOverrides: options.configOverrides,
68
- actions: options.actions,
69
- process: this.process,
70
- clock: this.clock,
71
- actionResolver: this.actionResolver,
72
- customHome: options.customHome,
73
- actionInvoker: this.actionInvoker,
74
- });
98
+ this.registry = new ActionRegistry(options.actions);
99
+ }
100
+ /** 获取运行底层存储实例 */
101
+ getStorage() {
102
+ return this.storage;
103
+ }
104
+ /** 兼容性引用,执行引擎即执行器自身 */
105
+ get _runner() {
106
+ return this;
107
+ }
108
+ get activeRunsCount() {
109
+ return this.activeRuns.size + this.reservedSlots;
75
110
  }
76
111
  setActionInvoker(invoker) {
77
112
  this.actionInvoker = invoker;
78
- this._runner.setActionInvoker(invoker);
79
113
  }
80
114
  registerAction(idOrAction, actionDef) {
81
115
  if (typeof idOrAction === "string") {
82
- this._runner.registerAction(idOrAction, actionDef);
116
+ this.registry.registerAction(idOrAction, actionDef);
83
117
  }
84
118
  else {
85
- this._runner.registerAction(idOrAction);
119
+ this.registry.registerAction(idOrAction);
86
120
  }
87
121
  }
88
122
  getAction(id) {
89
- return this._runner.getAction(id);
123
+ return this.registry.getAction(id);
90
124
  }
91
125
  listActions() {
92
- return this._runner.listActions();
126
+ return this.registry.listActions();
93
127
  }
94
128
  getActiveHandle(runId) {
95
- return this.activeRuns.get(runId)?.handle;
129
+ return this.activeRuns.get(runId)?.ticket;
96
130
  }
97
- async execute(ref, input, context) {
98
- const ticket = await this.start(ref, input, context);
131
+ async execute(ref, input = {}, contextOrOptions) {
132
+ const ticket = await this.start(ref, input, contextOrOptions);
99
133
  if (!ticket.result) {
100
- throw new Error(`Execution ticket for run '${ticket.runId}' has no result Promise`);
134
+ throw new ActionDockError(EXECUTION_FAILED, `Execution ticket for run '${ticket.runId}' has no result Promise`);
101
135
  }
102
136
  return ticket.result;
103
137
  }
104
- async run(ref, input, context) {
105
- return this.execute(ref, input, context);
106
- }
107
- get activeRunsCount() {
108
- return this.activeRuns.size + this.reservedSlots;
138
+ async run(ref, input = {}, contextOrOptions) {
139
+ return this.execute(ref, input, contextOrOptions);
109
140
  }
110
- async start(ref, input, context) {
141
+ async start(ref, input = {}, contextOrOptions) {
111
142
  if (this.isClosing) {
112
- throw new Error("ExecutionService is closing: new tasks rejected");
143
+ throw new ActionDockError(SERVICE_CLOSED, "ExecutionService is closing: new tasks rejected");
113
144
  }
114
145
  const currentTotal = this.activeRuns.size + this.reservedSlots;
115
146
  if (currentTotal >= this.maxActiveRuns) {
116
- throw new Error(`Concurrency limit reached: ${currentTotal}/${this.maxActiveRuns} active runs`);
147
+ throw new ActionDockError(EXECUTION_CONCURRENCY_LIMIT, `Concurrency limit reached: ${currentTotal}/${this.maxActiveRuns} active runs`);
117
148
  }
118
149
  this.reservedSlots++;
119
150
  let hasReservedSlot = true;
@@ -125,16 +156,27 @@ export class DefaultExecutionService {
125
156
  };
126
157
  try {
127
158
  let parsedRef;
128
- try {
129
- parsedRef = parseActionRef(ref);
159
+ let targetActionDef;
160
+ if (isActionDefinitionObject(ref)) {
161
+ targetActionDef = ref;
162
+ const actObj = ref;
163
+ const targetActionId = actObj.id || resolveAnonymousActionId(this.registry.map, ref);
164
+ this.registry.registerAction(targetActionId, ref);
165
+ parsedRef = { packageId: this.packageId, actionId: targetActionId };
130
166
  }
131
- catch {
132
- parsedRef = typeof ref === "object" ? ref : { actionId: ref };
167
+ else {
168
+ try {
169
+ parsedRef = parseActionRef(ref);
170
+ }
171
+ catch {
172
+ parsedRef = typeof ref === "object" ? ref : { actionId: ref };
173
+ }
133
174
  }
134
175
  const targetActionId = parsedRef.actionId;
135
176
  const targetPackageId = parsedRef.packageId || this.packageId;
136
177
  const actionRef = `${targetPackageId}/${targetActionId}`;
137
178
  const effectiveClock = this.clock;
179
+ const context = this.normalizeContext(contextOrOptions);
138
180
  // requestId 幂等检查与去重处理
139
181
  const gateResult = await this.checkIdempotencyGate(input, context, actionRef, effectiveClock);
140
182
  if (gateResult.ticket) {
@@ -142,7 +184,9 @@ export class DefaultExecutionService {
142
184
  return gateResult.ticket;
143
185
  }
144
186
  const designatedRunId = gateResult.designatedRunId;
145
- const target = await this.resolveExecutionTarget(parsedRef, targetPackageId, targetActionId);
187
+ const target = targetActionDef
188
+ ? { action: targetActionDef }
189
+ : await this.resolveExecutionTarget(parsedRef, targetPackageId, targetActionId);
146
190
  if (!target.action) {
147
191
  releaseSlot();
148
192
  return this.failTicketForMissingAction({
@@ -155,77 +199,358 @@ export class DefaultExecutionService {
155
199
  effectiveClock,
156
200
  });
157
201
  }
202
+ const currentAction = target.action;
158
203
  if (this.isClosing) {
159
- throw new Error("ExecutionService is closing: new tasks rejected");
204
+ throw new ActionDockError(SERVICE_CLOSED, "ExecutionService is closing: new tasks rejected");
205
+ }
206
+ const runId = designatedRunId || context.runId || crypto.randomUUID();
207
+ const rootRunId = context.rootRunId || context.parentRunId || runId;
208
+ const bridge = this.createEventBridge({ runId, context, effectiveClock });
209
+ // 输入参数 JSON 格式与合法性校验
210
+ const inputCheck = validateActionInputValue(input);
211
+ if (!inputCheck.valid) {
212
+ releaseSlot();
213
+ const error = buildInputValidationError(targetActionId, inputCheck);
214
+ tryCreateRun(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
215
+ runId,
216
+ rootRunId,
217
+ context,
218
+ targetPackageId,
219
+ targetActionId,
220
+ input,
221
+ effectiveClock,
222
+ }), "failed", error));
223
+ bridge.emitEvent({ type: "status", status: "failed" });
224
+ bridge.emitEvent({ type: "finish", result: { ok: false, runId, error } });
225
+ return {
226
+ runId,
227
+ status: "failed",
228
+ result: Promise.resolve({ ok: false, runId, error }),
229
+ cancel: () => false,
230
+ };
160
231
  }
232
+ // 输入 Schema 校验
233
+ const schemaError = this.checkActionInputSchema(currentAction, targetActionId, input);
234
+ if (schemaError) {
235
+ releaseSlot();
236
+ tryCreateRun(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
237
+ runId,
238
+ rootRunId,
239
+ context,
240
+ targetPackageId,
241
+ targetActionId,
242
+ input,
243
+ effectiveClock,
244
+ }), "failed", schemaError));
245
+ bridge.emitEvent({ type: "status", status: "failed" });
246
+ bridge.emitEvent({ type: "finish", result: { ok: false, runId, error: schemaError } });
247
+ return {
248
+ runId,
249
+ status: "failed",
250
+ result: Promise.resolve({ ok: false, runId, error: schemaError }),
251
+ cancel: () => false,
252
+ };
253
+ }
254
+ // 持久化 running 初始记录
255
+ createRunOrThrow(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
256
+ runId,
257
+ rootRunId,
258
+ context,
259
+ targetPackageId,
260
+ targetActionId,
261
+ input,
262
+ effectiveClock,
263
+ }), "running"));
264
+ const finalizer = createRunFinalizer(this.storage, runId);
265
+ // 调用栈快照跟踪
266
+ const callStack = context.callStack ? [...context.callStack] : [];
267
+ const callKey = targetPackageId ? `${targetPackageId}/${targetActionId}` : targetActionId;
268
+ const lastStackItem = callStack[callStack.length - 1];
269
+ const isAlreadyAtTop = lastStackItem === callKey ||
270
+ (targetActionId && lastStackItem === targetActionId) ||
271
+ (targetPackageId && lastStackItem === `${targetPackageId}/${targetActionId}`);
272
+ if (!isAlreadyAtTop) {
273
+ callStack.push(callKey);
274
+ }
275
+ // 单一 AbortController 控制器
161
276
  const controller = new AbortController();
162
277
  let onAbort;
163
- if (context.signal && typeof context.signal.addEventListener === "function") {
278
+ if (context.signal) {
164
279
  if (context.signal.aborted) {
165
280
  controller.abort(context.signal.reason);
166
281
  }
167
282
  else {
168
283
  onAbort = () => controller.abort(context.signal?.reason);
169
284
  context.signal.addEventListener("abort", onAbort, { once: true });
285
+ finalizer.signal = context.signal;
286
+ finalizer.onAbort = onAbort;
170
287
  }
171
288
  }
172
- const runId = designatedRunId || context.runId || crypto.randomUUID();
173
- const bridge = this.createEventBridge({ runId, context, effectiveClock });
174
- target.runner.registerAction(targetActionId, target.action);
175
- const handle = target.runner.start(targetActionId, input, {
289
+ if (typeof context.timeoutMs === "number" && context.timeoutMs > 0) {
290
+ finalizer.startTimeout(controller, context.timeoutMs);
291
+ }
292
+ const effectiveOwner = context.owner ||
293
+ createDefaultProcessOwner({
294
+ tenantId: context.tenantId,
295
+ principalId: context.principalId,
296
+ packageInstanceId: this.packageInstanceId,
297
+ generationId: this.generationId,
298
+ });
299
+ const effectiveProcess = context.process || this.process;
300
+ const actionCtx = createActionContext({
301
+ actionId: targetActionId,
302
+ storage: this.storage,
303
+ globalStorage: this.globalStorage,
304
+ overrides: { ...this.configOverrides, ...(context.config || {}) },
305
+ projectConfig: this.projectConfig,
176
306
  runId,
177
- rootRunId: context.rootRunId,
307
+ rootRunId,
178
308
  parentRunId: context.parentRunId,
179
- callStack: context.callStack ? [...context.callStack] : undefined,
180
- hostSessionId: context.hostSessionId || this.hostSessionId,
181
- maxCallDepth: context.maxCallDepth,
182
- configOverrides: context.config,
183
309
  signal: controller.signal,
184
- timeoutMs: context.timeoutMs,
310
+ process: effectiveProcess,
311
+ owner: effectiveOwner,
185
312
  progress: bridge.progressReporter,
186
313
  logger: bridge.executionLogger,
187
- process: context.process || this.process,
188
- packageInstanceId: context.package.instanceId,
189
- generationId: context.package.generation,
190
- tenantId: context.owner?.tenantId || context.tenantId,
191
- principalId: context.owner?.principalId || context.principalId,
192
- owner: context.owner
193
- ? {
194
- tenantId: context.owner.tenantId,
195
- principalId: context.owner.principalId,
196
- packageInstanceId: context.owner.packageInstanceId || context.package.instanceId,
197
- generationId: context.owner.generationId || context.package.generation,
198
- }
199
- : undefined,
200
- actionInvoker: this.actionInvoker,
314
+ onActionInvoke: (childAction, childInput, parentRunId) => this.invokeChildAction({
315
+ controller,
316
+ rootRunId,
317
+ childAction,
318
+ childInput,
319
+ parentRunId: parentRunId || runId,
320
+ currentActionId: targetActionId,
321
+ currentAction,
322
+ callStack,
323
+ context,
324
+ effectiveProcess,
325
+ effectiveOwner,
326
+ }),
327
+ });
328
+ const executionPromise = this.raceExecution({
329
+ currentAction,
330
+ targetActionId,
331
+ input,
332
+ actionCtx,
333
+ controller,
334
+ finalizer,
335
+ runId,
336
+ context,
201
337
  });
338
+ const cancelFn = (reason) => {
339
+ if (finalizer.finalized || controller.signal.aborted) {
340
+ return false;
341
+ }
342
+ if (context.signal && onAbort) {
343
+ context.signal.removeEventListener("abort", onAbort);
344
+ finalizer.onAbort = undefined;
345
+ }
346
+ controller.abort(new Error(reason || "Action execution was cancelled"));
347
+ return true;
348
+ };
349
+ const ticket = {
350
+ runId,
351
+ status: "running",
352
+ result: executionPromise,
353
+ cancel: cancelFn,
354
+ };
202
355
  const activeItem = {
203
- runId: handle.runId,
204
- handle,
356
+ runId,
205
357
  controller,
206
358
  status: "running",
207
- startedAt: (effectiveClock?.now() ?? new Date()).toISOString(),
359
+ startedAt: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
208
360
  signal: context.signal,
209
361
  onAbort,
362
+ ticket,
210
363
  };
211
- this.activeRuns.set(handle.runId, activeItem);
364
+ this.activeRuns.set(runId, activeItem);
212
365
  releaseSlot();
366
+ this.ensureHeartbeatTimer();
213
367
  bridge.emitEvent({ type: "status", status: "running" });
214
- this.watchHandleCompletion(handle, activeItem, bridge);
215
- return {
216
- runId: handle.runId,
217
- status: "running",
218
- result: handle.result,
219
- };
368
+ executionPromise
369
+ .then((result) => {
370
+ const finalStatus = resultStatusToRunStatus(result.ok, result.ok ? undefined : result.error?.code);
371
+ activeItem.status = finalStatus;
372
+ bridge.emitEvent({ type: "status", status: finalStatus });
373
+ bridge.emitEvent({ type: "finish", result });
374
+ })
375
+ .catch((err) => {
376
+ activeItem.status = "failed";
377
+ bridge.emitEvent({ type: "status", status: "failed" });
378
+ bridge.emitEvent({
379
+ type: "finish",
380
+ result: {
381
+ ok: false,
382
+ runId,
383
+ error: {
384
+ code: UNHANDLED_EXECUTION_ERROR,
385
+ message: err?.message || String(err),
386
+ },
387
+ },
388
+ });
389
+ })
390
+ .finally(async () => {
391
+ if (activeItem.signal && activeItem.onAbort) {
392
+ activeItem.signal.removeEventListener("abort", activeItem.onAbort);
393
+ activeItem.onAbort = undefined;
394
+ }
395
+ this.activeRuns.delete(runId);
396
+ if (actionCtx &&
397
+ actionCtx.process?.runScoped === true &&
398
+ typeof actionCtx.process.dispose === "function") {
399
+ try {
400
+ await actionCtx.process.dispose();
401
+ }
402
+ catch { }
403
+ }
404
+ });
405
+ return ticket;
220
406
  }
221
407
  catch (err) {
222
408
  releaseSlot();
223
409
  throw err;
224
410
  }
225
411
  }
226
- /**
227
- * 幂等门检查:digest 计算与冲突/重复分流(重复命中时直接返回已有票据)。
228
- */
412
+ normalizeContext(contextOrOptions) {
413
+ if (contextOrOptions && contextOrOptions.package) {
414
+ return contextOrOptions;
415
+ }
416
+ const opts = (contextOrOptions || {});
417
+ return {
418
+ runId: opts.runId,
419
+ rootRunId: opts.rootRunId,
420
+ parentRunId: opts.parentRunId,
421
+ callStack: opts.callStack ? [...opts.callStack] : [],
422
+ package: opts.package || this.identity,
423
+ signal: opts.signal,
424
+ timeoutMs: opts.timeoutMs,
425
+ maxCallDepth: opts.maxCallDepth,
426
+ config: opts.config || opts.configOverrides,
427
+ tenantId: opts.tenantId || opts.owner?.tenantId,
428
+ principalId: opts.principalId || opts.ownerId || opts.owner?.principalId,
429
+ hostSessionId: opts.hostSessionId || this.hostSessionId,
430
+ logger: opts.logger,
431
+ progress: opts.progress,
432
+ process: opts.process || this.process,
433
+ owner: opts.owner,
434
+ requestId: opts.requestId,
435
+ };
436
+ }
437
+ async raceExecution(args) {
438
+ const { currentAction, targetActionId, input, actionCtx, controller, finalizer, runId, context } = args;
439
+ const abortPromise = new Promise((_, reject) => {
440
+ const rejectWithReason = () => reject(controller.signal.reason || new Error("Action execution was cancelled"));
441
+ if (controller.signal.aborted) {
442
+ rejectWithReason();
443
+ }
444
+ else {
445
+ controller.signal.addEventListener("abort", rejectWithReason, { once: true });
446
+ finalizer.raceListenerRemovers.push(() => {
447
+ controller.signal.removeEventListener("abort", rejectWithReason);
448
+ });
449
+ }
450
+ });
451
+ try {
452
+ const rawOutput = await Promise.race([
453
+ Promise.resolve().then(() => currentAction.run(input, actionCtx)),
454
+ abortPromise,
455
+ ]);
456
+ const outputCheck = validateJsonValue(rawOutput);
457
+ if (!outputCheck.valid) {
458
+ const error = {
459
+ code: OUTPUT_NOT_JSON,
460
+ message: `Output validation failed for action '${targetActionId}': ${outputCheck.reason}`,
461
+ };
462
+ finalizer.finalize("failed", undefined, error);
463
+ return { ok: false, runId, error };
464
+ }
465
+ const outputSchemaError = this.checkActionOutputSchema(currentAction, targetActionId, rawOutput);
466
+ if (outputSchemaError) {
467
+ finalizer.finalize("failed", undefined, outputSchemaError);
468
+ return { ok: false, runId, error: outputSchemaError };
469
+ }
470
+ finalizer.finalize("success", rawOutput);
471
+ if (finalizer.persistError) {
472
+ return { ok: false, runId, error: finalizer.persistError };
473
+ }
474
+ return {
475
+ ok: true,
476
+ runId,
477
+ data: rawOutput,
478
+ };
479
+ }
480
+ catch (err) {
481
+ return this.classifyExecutionError(err, runId, context.timeoutMs, controller, finalizer);
482
+ }
483
+ }
484
+ classifyExecutionError(err, runId, timeoutMs, controller, finalizer) {
485
+ if (finalizer.isTimeout) {
486
+ const error = {
487
+ code: ACTION_TIMEOUT,
488
+ message: `Action exceeded timeout of ${timeoutMs}ms`,
489
+ };
490
+ finalizer.finalize("timed_out", undefined, error);
491
+ return { ok: false, runId, error: finalizer.persistError || error };
492
+ }
493
+ if (controller.signal.aborted) {
494
+ const reason = controller.signal.reason;
495
+ const reasonMsg = reason instanceof Error
496
+ ? reason.message
497
+ : typeof reason === "string"
498
+ ? reason
499
+ : undefined;
500
+ const error = {
501
+ code: ACTION_CANCELLED,
502
+ message: "Action execution was cancelled",
503
+ details: reasonMsg ? { reason: reasonMsg } : undefined,
504
+ };
505
+ finalizer.finalize("cancelled", undefined, error);
506
+ return { ok: false, runId, error: finalizer.persistError || error };
507
+ }
508
+ const error = {
509
+ code: err?.code || ACTION_FAILED,
510
+ message: err?.message || String(err),
511
+ details: err?.details,
512
+ };
513
+ finalizer.finalize("failed", undefined, error);
514
+ return {
515
+ ok: false,
516
+ runId,
517
+ error: finalizer.persistError || error,
518
+ };
519
+ }
520
+ async invokeChildAction(args) {
521
+ const { controller, rootRunId, childAction, childInput, parentRunId, currentActionId, currentAction, callStack, context, effectiveProcess, effectiveOwner, } = args;
522
+ const invoker = this.actionInvoker;
523
+ if (!invoker) {
524
+ throw new ActionDockError(INVOCATION_UNSUPPORTED, "Nested action invocation requires a Host ActionInvoker");
525
+ }
526
+ const childRunId = crypto.randomUUID();
527
+ const invocationContext = {
528
+ runId: childRunId,
529
+ rootRunId,
530
+ parentRunId,
531
+ caller: {
532
+ packageId: this.packageId,
533
+ actionId: currentActionId,
534
+ runId: parentRunId,
535
+ declaredUses: currentAction?.uses ||
536
+ this.projectConfig?.actions?.[currentActionId]?.uses,
537
+ },
538
+ callStack: [...callStack],
539
+ package: this.identity,
540
+ signal: controller.signal,
541
+ timeoutMs: context.timeoutMs,
542
+ maxCallDepth: context.maxCallDepth,
543
+ config: context.config,
544
+ tenantId: effectiveOwner.tenantId,
545
+ principalId: effectiveOwner.principalId,
546
+ hostSessionId: context.hostSessionId || this.hostSessionId,
547
+ logger: context.logger,
548
+ progress: context.progress,
549
+ process: effectiveProcess,
550
+ owner: effectiveOwner,
551
+ };
552
+ return await invoker(childAction, childInput, invocationContext);
553
+ }
229
554
  async checkIdempotencyGate(input, context, actionRef, effectiveClock) {
230
555
  if (!context.requestId) {
231
556
  return {};
@@ -246,7 +571,7 @@ export class DefaultExecutionService {
246
571
  requestId: context.requestId,
247
572
  inputDigest,
248
573
  runId: provisionalRunId,
249
- createdAt: (effectiveClock?.now() ?? new Date()).toISOString(),
574
+ createdAt: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
250
575
  });
251
576
  if (idemp.outcome === "conflict") {
252
577
  throw new ActionDockError(IDEMPOTENCY_CONFLICT, `Idempotency conflict for requestId '${context.requestId}': input parameters digest mismatch`, {
@@ -261,11 +586,7 @@ export class DefaultExecutionService {
261
586
  const active = this.activeRuns.get(existingRunId);
262
587
  if (active) {
263
588
  return {
264
- ticket: {
265
- runId: existingRunId,
266
- status: active.status,
267
- result: active.handle.result,
268
- },
589
+ ticket: active.ticket,
269
590
  };
270
591
  }
271
592
  const record = await this.get(existingRunId);
@@ -285,91 +606,90 @@ export class DefaultExecutionService {
285
606
  runId: existingRunId,
286
607
  status: record.status,
287
608
  result: Promise.resolve(execRes),
609
+ cancel: () => false,
288
610
  },
289
611
  };
290
612
  }
291
613
  }
292
614
  return { designatedRunId: provisionalRunId };
293
615
  }
294
- /**
295
- * 解析执行目标:确定目标 Action 定义。
296
- * 铁律 4:Runner 不找 Package,ExecutionService 仅执行本 Package 的 Action。
297
- */
298
616
  async resolveExecutionTarget(parsedRef, targetPackageId, targetActionId) {
299
- const runnerToUse = this._runner;
300
- let resolveError;
301
617
  if (targetPackageId && targetPackageId !== this.packageId) {
302
- resolveError = {
618
+ const resolveError = {
303
619
  code: ACTION_NOT_FOUND,
304
620
  message: `Package '${this.packageId}' cannot execute action for external package '${targetPackageId}'`,
621
+ details: {
622
+ reason: `Cross-package action '${targetPackageId}/${targetActionId}' cannot be resolved by ActionRunner of package '${this.packageId}'`,
623
+ },
305
624
  };
306
- return { runner: runnerToUse, action: undefined, resolveError };
625
+ return { action: undefined, resolveError };
307
626
  }
308
- let action = runnerToUse.getAction(targetActionId) || runnerToUse.getAction(`${targetPackageId}/${targetActionId}`);
309
- if (!action) {
310
- const resolution = await runnerToUse.resolveAction(parsedRef);
311
- if (resolution.status === "found") {
312
- action = resolution.action;
313
- }
314
- else {
315
- const targetAction = await this.actionResolver?.(targetActionId);
316
- if (targetAction) {
317
- action = targetAction;
318
- }
319
- else if (this.projectRoot && existsSync(this.projectRoot)) {
320
- let config;
321
- try {
322
- config = this.projectConfig || loadProjectConfig(this.projectRoot);
323
- }
324
- catch (err) {
325
- resolveError = describeActionLoadFailure(err, {
326
- actionId: targetActionId,
327
- packageId: this.packageId,
328
- projectRoot: this.projectRoot,
329
- });
330
- return { runner: runnerToUse, action: undefined, resolveError };
331
- }
332
- let actionsMap;
333
- try {
334
- actionsMap = await loadActions(this.projectRoot, config.actionsDir, {
335
- loader: this.moduleLoader,
336
- });
337
- }
338
- catch (err) {
339
- resolveError = describeActionLoadFailure(err, {
340
- actionId: targetActionId,
341
- packageId: this.packageId,
342
- projectRoot: this.projectRoot,
343
- });
344
- return { runner: runnerToUse, action: undefined, resolveError };
345
- }
346
- const matched = actionsMap.get(targetActionId);
347
- if (matched) {
348
- action = matched;
349
- runnerToUse.registerAction(targetActionId, matched);
350
- }
351
- else {
352
- resolveError = {
353
- code: ACTION_NOT_FOUND,
354
- message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
355
- details: resolution.reason ? { reason: resolution.reason } : undefined,
356
- };
627
+ let action = this.registry.getAction(targetActionId) ||
628
+ this.registry.getAction(`${targetPackageId}/${targetActionId}`);
629
+ if (action) {
630
+ return { action };
631
+ }
632
+ if (this.actionResolver) {
633
+ try {
634
+ const customResolved = await this.actionResolver(targetActionId);
635
+ if (customResolved) {
636
+ this.registry.registerAction(targetActionId, customResolved);
637
+ if (this.packageId) {
638
+ this.registry.registerAction(`${this.packageId}/${targetActionId}`, customResolved);
357
639
  }
640
+ return { action: customResolved };
358
641
  }
359
- else {
360
- resolveError = {
642
+ }
643
+ catch (err) {
644
+ return {
645
+ action: undefined,
646
+ resolveError: {
361
647
  code: ACTION_NOT_FOUND,
362
648
  message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
363
- details: resolution.reason ? { reason: resolution.reason } : undefined,
364
- };
365
- }
649
+ details: { reason: err.message },
650
+ },
651
+ };
366
652
  }
367
653
  }
368
- return { runner: runnerToUse, action, resolveError };
654
+ if (this.projectRoot && existsSync(this.projectRoot)) {
655
+ let config;
656
+ try {
657
+ config = this.projectConfig || loadProjectConfig(this.projectRoot);
658
+ }
659
+ catch (err) {
660
+ const resolveError = describeActionLoadFailure(err, {
661
+ actionId: targetActionId,
662
+ packageId: this.packageId,
663
+ projectRoot: this.projectRoot,
664
+ });
665
+ return { action: undefined, resolveError };
666
+ }
667
+ let actionsMap;
668
+ try {
669
+ actionsMap = await loadActions(this.projectRoot, config.actionsDir, {
670
+ loader: this.moduleLoader,
671
+ });
672
+ }
673
+ catch (err) {
674
+ const resolveError = describeActionLoadFailure(err, {
675
+ actionId: targetActionId,
676
+ packageId: this.packageId,
677
+ projectRoot: this.projectRoot,
678
+ });
679
+ return { action: undefined, resolveError };
680
+ }
681
+ const matched = actionsMap.get(targetActionId);
682
+ if (matched) {
683
+ this.registry.registerAction(targetActionId, matched);
684
+ return { action: matched };
685
+ }
686
+ }
687
+ const resolveError = {
688
+ code: ACTION_NOT_FOUND,
689
+ message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
690
+ };
691
+ return { action: undefined, resolveError };
369
692
  }
370
- /**
371
- * 解析指定 Action 定义(供 PackageRuntime 或内部使用)。
372
- */
373
693
  async resolveAction(ref) {
374
694
  let parsedRef;
375
695
  try {
@@ -383,13 +703,10 @@ export class DefaultExecutionService {
383
703
  const target = await this.resolveExecutionTarget(parsedRef, targetPackageId, targetActionId);
384
704
  return target.action;
385
705
  }
386
- /**
387
- * 目标 Action 缺失时的失败票据:落库 failed 记录并补发 status 与 finish 事件。
388
- */
389
706
  failTicketForMissingAction(args) {
390
707
  const { target, input, context, targetPackageId, targetActionId, designatedRunId, effectiveClock } = args;
391
708
  const runId = designatedRunId || context.runId || crypto.randomUUID();
392
- const now = (effectiveClock?.now() ?? new Date()).toISOString();
709
+ const now = (effectiveClock?.now() ?? this.clock.now()).toISOString();
393
710
  const error = target.resolveError || {
394
711
  code: ACTION_NOT_FOUND,
395
712
  message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
@@ -400,19 +717,20 @@ export class DefaultExecutionService {
400
717
  rootRunId,
401
718
  parentRunId: context.parentRunId,
402
719
  packageId: targetPackageId,
403
- packageInstanceId: context.package.instanceId || target.runner.packageInstanceId,
720
+ packageInstanceId: context.package?.instanceId || this.packageInstanceId,
404
721
  actionId: targetActionId,
405
- generationId: context.package.generation || target.runner.generationId,
722
+ generationId: context.package?.generation || this.generationId,
406
723
  ownerId: this.ownerId,
407
724
  hostSessionId: context.hostSessionId || this.hostSessionId,
725
+ hostPid: this.hostPid,
408
726
  status: "failed",
409
- input,
727
+ input: input,
410
728
  error,
411
729
  startedAt: now,
412
730
  finishedAt: now,
413
731
  };
414
732
  try {
415
- target.runner.getStorage().createRun(initialRun);
733
+ this.storage.createRun(initialRun);
416
734
  }
417
735
  catch (err) {
418
736
  throw new ActionDockError(RUN_REPOSITORY_UNAVAILABLE, `RUN_REPOSITORY_UNAVAILABLE: Failed to initialize run record in repository: ${err?.message || String(err)}`, { originalError: err?.message });
@@ -446,11 +764,66 @@ export class DefaultExecutionService {
446
764
  runId,
447
765
  error,
448
766
  }),
767
+ cancel: () => false,
449
768
  };
450
769
  }
451
- /**
452
- * 创建执行事件桥:事件序列号分配、进度报告器与双写日志适配器。
453
- */
770
+ buildPersistenceInput(args) {
771
+ const { runId, rootRunId, context, targetPackageId, targetActionId, input, effectiveClock } = args;
772
+ const startedAt = effectiveClock?.now().toISOString() ||
773
+ (typeof this.storage.clock?.now === "function"
774
+ ? this.storage.clock.now().toISOString()
775
+ : this.clock.now().toISOString());
776
+ return {
777
+ runId,
778
+ rootRunId,
779
+ parentRunId: context.parentRunId,
780
+ ownerId: context.principalId || context.tenantId || this.ownerId,
781
+ hostSessionId: context.hostSessionId || this.hostSessionId,
782
+ hostPid: this.hostPid,
783
+ packageInstanceId: context.package?.instanceId || this.packageInstanceId,
784
+ generationId: context.package?.generation || this.generationId,
785
+ targetPackageId,
786
+ targetActionId,
787
+ startedAt,
788
+ input,
789
+ runnerPackageId: this.packageId,
790
+ runnerPackageInstanceId: this.packageInstanceId,
791
+ runnerGenerationId: this.generationId,
792
+ runnerHostSessionId: this.hostSessionId,
793
+ };
794
+ }
795
+ checkActionInputSchema(action, targetActionId, input) {
796
+ const targetInputSchema = action?.inputSchema !== undefined
797
+ ? action.inputSchema
798
+ : this.projectConfig?.actions?.[targetActionId]?.inputSchema;
799
+ if (targetInputSchema !== undefined) {
800
+ const val = validateSchemaOnly(targetInputSchema, input);
801
+ if (!val.valid) {
802
+ return {
803
+ code: INPUT_VALIDATION_FAILED,
804
+ message: `Input schema validation failed for action '${targetActionId}'`,
805
+ details: val.errors,
806
+ };
807
+ }
808
+ }
809
+ return undefined;
810
+ }
811
+ checkActionOutputSchema(action, targetActionId, rawOutput) {
812
+ const targetOutputSchema = action?.outputSchema !== undefined
813
+ ? action.outputSchema
814
+ : this.projectConfig?.actions?.[targetActionId]?.outputSchema;
815
+ if (targetOutputSchema !== undefined) {
816
+ const outVal = validateSchemaOnly(targetOutputSchema, rawOutput);
817
+ if (!outVal.valid) {
818
+ return {
819
+ code: OUTPUT_VALIDATION_FAILED,
820
+ message: `Output schema validation failed for action '${targetActionId}'`,
821
+ details: outVal.errors,
822
+ };
823
+ }
824
+ }
825
+ return undefined;
826
+ }
454
827
  createEventBridge(args) {
455
828
  const { runId, context, effectiveClock } = args;
456
829
  let sequence = 0;
@@ -460,7 +833,7 @@ export class DefaultExecutionService {
460
833
  runId,
461
834
  rootRunId: context.rootRunId || context.parentRunId || runId,
462
835
  sequence: sequence++,
463
- timestamp: (effectiveClock?.now() ?? new Date()).toISOString(),
836
+ timestamp: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
464
837
  };
465
838
  this.eventSink.emit(evt);
466
839
  };
@@ -493,40 +866,6 @@ export class DefaultExecutionService {
493
866
  };
494
867
  return { emitEvent, progressReporter, executionLogger };
495
868
  }
496
- /**
497
- * 监听执行句柄终态:映射终态状态、补发 status 与 finish 事件并注销活跃运行。
498
- */
499
- watchHandleCompletion(handle, activeItem, bridge) {
500
- handle.result
501
- .then((result) => {
502
- const finalStatus = resultStatusToRunStatus(result.ok, result.ok ? undefined : result.error?.code);
503
- activeItem.status = finalStatus;
504
- bridge.emitEvent({ type: "status", status: finalStatus });
505
- bridge.emitEvent({ type: "finish", result });
506
- })
507
- .catch((err) => {
508
- activeItem.status = "failed";
509
- bridge.emitEvent({ type: "status", status: "failed" });
510
- bridge.emitEvent({
511
- type: "finish",
512
- result: {
513
- ok: false,
514
- runId: handle.runId,
515
- error: {
516
- code: UNHANDLED_EXECUTION_ERROR,
517
- message: err?.message || String(err),
518
- },
519
- },
520
- });
521
- })
522
- .finally(() => {
523
- if (activeItem.signal && activeItem.onAbort) {
524
- activeItem.signal.removeEventListener("abort", activeItem.onAbort);
525
- activeItem.onAbort = undefined;
526
- }
527
- this.activeRuns.delete(handle.runId);
528
- });
529
- }
530
869
  async get(runId) {
531
870
  return this.storage.getRun(runId) ?? undefined;
532
871
  }
@@ -544,22 +883,69 @@ export class DefaultExecutionService {
544
883
  active.onAbort = undefined;
545
884
  }
546
885
  active.controller.abort(new Error(reason || "Execution cancelled"));
547
- active.handle.cancel(reason);
548
886
  return { outcome: "requested", runId };
549
887
  }
550
888
  events(runId, options = {}) {
551
889
  return this.eventSink.subscribe(runId, options);
552
890
  }
891
+ /**
892
+ * 确保在途运行心跳定时器存在。
893
+ *
894
+ * 存活判定的第一依据是进程探测(运行记录已落库宿主进程标识),
895
+ * 心跳是对遗留记录(无进程标识)与其他协作场景的兜底信号:
896
+ * 持有进程周期性刷新在途记录的心跳时间戳,收割方仅对心跳过期的
897
+ * 遗留记录做宽限期判定,正常运行中的记录绝不会被误判过期。
898
+ * 定时器不阻塞进程退出(unref),无在途运行时不产生任何调度。
899
+ */
900
+ ensureHeartbeatTimer() {
901
+ if (this.heartbeatTimer)
902
+ return;
903
+ const touch = () => {
904
+ if (this.isClosing || this.activeRuns.size === 0)
905
+ return;
906
+ if (typeof this.storage.touchRunHeartbeat !== "function")
907
+ return;
908
+ try {
909
+ this.storage.touchRunHeartbeat(Array.from(this.activeRuns.keys()));
910
+ }
911
+ catch (err) {
912
+ // 心跳失败不影响执行主链路,存活判定仍可依赖进程探测,但必须通过 logger.warn 记录告警
913
+ const errDetail = err instanceof Error ? `${err.message}${err.stack ? `\n${err.stack}` : ""}` : String(err);
914
+ const warnMsg = `[ExecutionService] Failed to touch run heartbeat: ${errDetail}`;
915
+ if (this.logger) {
916
+ this.logger.warn(warnMsg, {
917
+ error: err instanceof Error ? err.message : String(err),
918
+ stack: err instanceof Error ? err.stack : undefined,
919
+ });
920
+ }
921
+ else {
922
+ console.warn(warnMsg);
923
+ }
924
+ }
925
+ };
926
+ this.heartbeatTimer = setInterval(touch, RUN_HEARTBEAT_INTERVAL_MS);
927
+ if (this.heartbeatTimer.unref) {
928
+ this.heartbeatTimer.unref();
929
+ }
930
+ }
553
931
  async close(options = {}) {
554
932
  this.isClosing = true;
933
+ if (this.heartbeatTimer) {
934
+ clearInterval(this.heartbeatTimer);
935
+ this.heartbeatTimer = undefined;
936
+ }
555
937
  this.reservedSlots = 0;
556
938
  const graceMs = options.graceMs ?? 5000;
557
939
  for (const [_, active] of this.activeRuns) {
558
940
  active.controller.abort(new Error("Service shutting down"));
559
- active.handle.cancel("Service shutting down");
560
941
  }
561
942
  if (this.activeRuns.size > 0) {
562
- const waitPromise = Promise.all(Array.from(this.activeRuns.values()).map((a) => a.handle.result.catch(() => { })));
943
+ const waitPromise = Promise.all(Array.from(this.activeRuns.values()).map((a) => a.ticket.result?.catch(() => { })));
944
+ // 此处优雅关闭竞速保留原生 setTimeout 与 clearTimeout 的取舍说明:
945
+ // Clock 接口仅提供 sleep 异步等待能力,未提供注销或取消计划中休眠的句柄;
946
+ // 若直接使用 clock.sleep 与 waitPromise 竞速,在任务提早完成时未触发的休眠 Promise 将在后台悬挂直至超时;
947
+ // 保留原生定时器并在 finally 中即时清理,可确保优雅关闭在任何分支下均不产生定时器泄漏;
948
+ // 且此处仅为服务终结阶段的兜底防护,对确定性测试执行链路无负面干扰。
563
949
  let timeoutTimer;
564
950
  const timeoutPromise = new Promise((resolve) => {
565
951
  timeoutTimer = setTimeout(resolve, graceMs);
@@ -574,7 +960,10 @@ export class DefaultExecutionService {
574
960
  }
575
961
  }
576
962
  this.activeRuns.clear();
577
- await this._runner.dispose();
963
+ }
964
+ async dispose() {
965
+ await this.close();
578
966
  }
579
967
  }
580
968
  export { DefaultExecutionService as ExecutionService };
969
+ export { DefaultExecutionService as ActionRunner };