@actiondock/core 2.10.0 → 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 (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
@@ -0,0 +1,622 @@
1
+ import { isTerminalRunStatus, DEFAULT_MAX_RUNS_PER_PACKAGE, DEFAULT_MIN_RETAIN_RUNS, DEFAULT_RUNS_RETENTION_MS } from "./types.js";
2
+ import { safeParseStoredJson } from "./utils.js";
3
+ import { isRunHostDead } from "./run-liveness.js";
4
+ import { ActionDockError, STORAGE_CLOSED, STORED_ERROR_DECODE_FAILED } from "../errors.js";
5
+ export class SqliteRunsStore {
6
+ statementCache = new Map();
7
+ runInsertCount = 0;
8
+ lastRunsCleanupAt = 0;
9
+ static RUNS_CLEANUP_INTERVAL_MS = 3600_000;
10
+ static RUNS_CLEANUP_BATCH_THRESHOLD = 50;
11
+ driver;
12
+ packageId;
13
+ dbPath;
14
+ clock;
15
+ retentionPolicy;
16
+ getConfigFn;
17
+ logger;
18
+ logWarnFn;
19
+ constructor(driver, packageId, dbPath, clock, retentionPolicy, getConfigFn, logger, logWarnFn) {
20
+ this.driver = driver;
21
+ this.packageId = packageId;
22
+ this.dbPath = dbPath;
23
+ this.clock = clock;
24
+ this.retentionPolicy = retentionPolicy;
25
+ this.getConfigFn = getConfigFn;
26
+ this.logger = logger;
27
+ this.logWarnFn = logWarnFn;
28
+ }
29
+ logWarn(message) {
30
+ if (this.logWarnFn) {
31
+ this.logWarnFn(message);
32
+ }
33
+ else if (this.logger) {
34
+ this.logger.warn(message);
35
+ }
36
+ else {
37
+ console.warn(message);
38
+ }
39
+ }
40
+ getStatement(sql) {
41
+ let stmt = this.statementCache.get(sql);
42
+ if (!stmt) {
43
+ stmt = this.driver.prepare(sql);
44
+ this.statementCache.set(sql, stmt);
45
+ }
46
+ return stmt;
47
+ }
48
+ /**
49
+ * 收敛死亡宿主遗留的非终态运行记录(保守收割)。
50
+ *
51
+ * 判定依据是宿主存活而不是会话归属:会话归属只区分「不是当前会话」,
52
+ * 无法区分「持有进程已死」与「另一活跃进程仍在执行」。
53
+ * 先选取非当前会话的非终态候选,再逐条做存活校验,仅收割确认死亡的记录:
54
+ * - 携带宿主进程标识的记录:进程探测失败(明确不存在)才收割;
55
+ * - 遗留记录(无进程标识):心跳(缺省回退开始时间)超过宽限期才收割。
56
+ *
57
+ * 无法确认死亡时一律保留,宁司不动也不误杀活跃任务。
58
+ */
59
+ recoverDeadSessionRuns(isClosed, currentHostSessionId, options) {
60
+ if (isClosed)
61
+ return 0;
62
+ try {
63
+ const candidates = this.selectOrphanCandidates(currentHostSessionId);
64
+ if (candidates.length === 0)
65
+ return 0;
66
+ const deadIds = [];
67
+ for (const candidate of candidates) {
68
+ if (isRunHostDead(candidate, { clock: this.clock, ...options })) {
69
+ deadIds.push(candidate.id);
70
+ }
71
+ }
72
+ if (deadIds.length === 0)
73
+ return 0;
74
+ return this.interruptRunsByIds(deadIds);
75
+ }
76
+ catch (err) {
77
+ this.logWarn(`[actiondock] recoverDeadSessionRuns failed: ${err instanceof Error ? err.message : String(err)}`);
78
+ return 0;
79
+ }
80
+ }
81
+ /** 选取非当前会话的非终态候选记录(仅判定所需列) */
82
+ selectOrphanCandidates(currentHostSessionId) {
83
+ let sql = `
84
+ SELECT id, host_pid, heartbeat_at, started_at
85
+ FROM runs
86
+ WHERE status IN ('running', 'pending')
87
+ `;
88
+ const params = [];
89
+ if (currentHostSessionId) {
90
+ sql += " AND (host_session_id IS NULL OR host_session_id != ?)";
91
+ params.push(currentHostSessionId);
92
+ }
93
+ const stmt = this.getStatement(sql);
94
+ return stmt.all(...params).map((row) => ({
95
+ id: row.id,
96
+ hostPid: typeof row.host_pid === "number" ? row.host_pid : null,
97
+ heartbeatAt: typeof row.heartbeat_at === "string" ? row.heartbeat_at : null,
98
+ startedAt: typeof row.started_at === "string" ? row.started_at : null,
99
+ }));
100
+ }
101
+ /** 将确认死亡的记录收敛为 interrupted(单条 SQL 批量执行,不误伤其他在途记录) */
102
+ interruptRunsByIds(ids) {
103
+ if (ids.length === 0)
104
+ return 0;
105
+ const uniqueIds = Array.from(new Set(ids));
106
+ if (uniqueIds.length === 0)
107
+ return 0;
108
+ const updateStmt = this.getStatement(`
109
+ UPDATE runs
110
+ SET status = 'interrupted',
111
+ finished_at = ?,
112
+ error_json = '{"code":"RUN_INTERRUPTED","message":"Execution interrupted by system shutdown or restart"}'
113
+ WHERE id IN (${SqliteRunsStore.placeholders(uniqueIds.length)})
114
+ AND status IN ('running', 'pending')
115
+ `);
116
+ const res = updateStmt.run(this.clock.now().toISOString(), ...uniqueIds);
117
+ return res.changes;
118
+ }
119
+ /**
120
+ * 刷新在途运行记录的心跳时间戳。
121
+ *
122
+ * 执行宿主周期性调用,使无进程标识判定路径(遗留记录兜底)以及其他
123
+ * 协作方能够区分「在途执行中」与「宿主崩溃后的遗留」。
124
+ * 仅更新非终态记录,终态记录心跳无意义不更新。
125
+ */
126
+ touchRunHeartbeat(isClosed, runIds) {
127
+ if (isClosed || runIds.length === 0)
128
+ return 0;
129
+ try {
130
+ const uniqueIds = Array.from(new Set(runIds));
131
+ const now = this.clock.now().toISOString();
132
+ const stmt = this.getStatement(`
133
+ UPDATE runs
134
+ SET heartbeat_at = ?
135
+ WHERE id IN (${SqliteRunsStore.placeholders(uniqueIds.length)})
136
+ AND status IN ('running', 'pending')
137
+ `);
138
+ const res = stmt.run(now, ...uniqueIds);
139
+ return res.changes;
140
+ }
141
+ catch (err) {
142
+ this.logWarn(`[actiondock] touchRunHeartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
143
+ return 0;
144
+ }
145
+ }
146
+ createRun(record) {
147
+ const stmt = this.getStatement(`
148
+ INSERT INTO runs (
149
+ id, root_run_id, parent_run_id, package_id, package_instance_id,
150
+ action_id, generation_id, owner_id, host_session_id, host_pid, heartbeat_at,
151
+ status, input_json, output_json,
152
+ error_json, started_at, finished_at, duration_ms
153
+ )
154
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
155
+ `);
156
+ const rootRunId = record.rootRunId || record.id;
157
+ const parentRunId = record.parentRunId || null;
158
+ const packageInstanceId = record.packageInstanceId || record.packageId || this.packageId;
159
+ const generationId = record.generationId || "1";
160
+ const ownerId = record.ownerId || "local";
161
+ const hostSessionId = record.hostSessionId || null;
162
+ // 宿主进程标识与初始心跳:落库即登记存活判定依据(遗留调用方缺省时不写入)
163
+ const hostPid = typeof record.hostPid === "number" ? record.hostPid : null;
164
+ const startedIso = record.startedAt || this.clock.now().toISOString();
165
+ const heartbeatAt = record.status === "running" || record.status === "pending" ? startedIso : null;
166
+ const inputJson = record.input !== undefined ? JSON.stringify(record.input) : null;
167
+ const outputJson = record.output !== undefined ? JSON.stringify(record.output) : null;
168
+ const errorJson = record.error ? JSON.stringify(record.error) : null;
169
+ const finishedAt = record.finishedAt || null;
170
+ const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
171
+ stmt.run(record.id, rootRunId, parentRunId, record.packageId || this.packageId, packageInstanceId, record.actionId ?? "", generationId, ownerId, hostSessionId, hostPid, heartbeatAt, record.status, inputJson, outputJson, errorJson, startedIso, finishedAt, durationMs);
172
+ this.runInsertCount++;
173
+ const nowMs = this.clock.now().getTime();
174
+ if (this.runInsertCount >= SqliteRunsStore.RUNS_CLEANUP_BATCH_THRESHOLD &&
175
+ nowMs - this.lastRunsCleanupAt >= SqliteRunsStore.RUNS_CLEANUP_INTERVAL_MS) {
176
+ this.lastRunsCleanupAt = nowMs;
177
+ this.runInsertCount = 0;
178
+ try {
179
+ this.cleanExpiredRuns(false);
180
+ }
181
+ catch (cleanupErr) {
182
+ this.logWarn(`[actiondock] opportunistic cleanExpiredRuns failed: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
183
+ }
184
+ }
185
+ }
186
+ updateRun(id, status, isClosed, output, error, finishedAt) {
187
+ if (isClosed)
188
+ return;
189
+ try {
190
+ const finishIso = finishedAt || this.clock.now().toISOString();
191
+ const stmt = this.getStatement(`
192
+ UPDATE runs
193
+ SET status = ?, output_json = ?, error_json = ?, finished_at = ?,
194
+ duration_ms = CASE
195
+ WHEN started_at IS NOT NULL THEN MAX(0, CAST(ROUND((julianday(?) - julianday(started_at)) * 86400000) AS INTEGER))
196
+ ELSE NULL
197
+ END
198
+ WHERE id = ? AND status = 'running'
199
+ `);
200
+ const res = stmt.run(status, output !== undefined ? JSON.stringify(output) : null, error ? JSON.stringify(error) : null, finishIso, finishIso, id);
201
+ if (res.changes === 0 && isTerminalRunStatus(status)) {
202
+ this.logWarn(`[actiondock] updateRun matched 0 rows: run '${id}' is no longer 'running' in db=${this.dbPath}; terminal status '${status}' was not persisted (record likely settled or recovered by another holder process)`);
203
+ }
204
+ }
205
+ catch (err) {
206
+ if (isClosed)
207
+ return;
208
+ this.logWarn(`[SqliteRuntimeStorage] Failed to update run "${id}": ${err instanceof Error ? err.message : String(err)}`);
209
+ throw err;
210
+ }
211
+ }
212
+ getRun(id) {
213
+ const stmt = this.getStatement("SELECT * FROM runs WHERE id = ? AND package_id = ?");
214
+ const row = stmt.get(id, this.packageId);
215
+ if (!row)
216
+ return null;
217
+ return this.mapRunRecord(row);
218
+ }
219
+ /**
220
+ * 查询单条运行记录并补齐幂等请求标识关联(详情路径专用)。
221
+ */
222
+ getRunWithRequestId(id, isClosed) {
223
+ const record = this.getRun(id);
224
+ if (!record || isClosed)
225
+ return record;
226
+ const mapping = this.getRunRequestIds(isClosed, [id]);
227
+ const requestId = mapping[id];
228
+ if (requestId)
229
+ record.requestId = requestId;
230
+ return record;
231
+ }
232
+ listRuns(options = {}) {
233
+ const limit = options.limit || 50;
234
+ let sql = "SELECT * FROM runs WHERE package_id = ?";
235
+ const params = [this.packageId];
236
+ if (options.actionId) {
237
+ sql += " AND action_id = ?";
238
+ params.push(options.actionId);
239
+ }
240
+ if (options.status) {
241
+ sql += " AND status = ?";
242
+ params.push(options.status);
243
+ }
244
+ sql += " ORDER BY started_at DESC LIMIT ?";
245
+ params.push(limit);
246
+ if (typeof options.offset === "number" && options.offset > 0) {
247
+ sql += " OFFSET ?";
248
+ params.push(options.offset);
249
+ }
250
+ const stmt = this.getStatement(sql);
251
+ const rows = stmt.all(...params);
252
+ return rows.map((r) => this.mapRunRecord(r));
253
+ }
254
+ /**
255
+ * 统计符合过滤条件的真实运行记录总数。
256
+ * 支持按 actionId、status 以及包含 requestIds 的反查场景统计总数。
257
+ */
258
+ countRuns(options = {}, isClosed = false) {
259
+ if (isClosed)
260
+ return 0;
261
+ if (options.requestIds && options.requestIds.length > 0) {
262
+ const uniqueIds = Array.from(new Set(options.requestIds.filter((id) => typeof id === "string" && id.length > 0)));
263
+ if (uniqueIds.length === 0)
264
+ return 0;
265
+ let sql = `
266
+ SELECT COUNT(DISTINCT runs.id) as count
267
+ FROM runs
268
+ INNER JOIN idempotency_keys AS ik ON ik.run_id = runs.id
269
+ WHERE runs.package_id = ?
270
+ AND ik.request_id IN (${SqliteRunsStore.placeholders(uniqueIds.length)})
271
+ `;
272
+ const params = [this.packageId, ...uniqueIds];
273
+ if (options.actionId) {
274
+ sql += " AND runs.action_id = ?";
275
+ params.push(options.actionId);
276
+ }
277
+ if (options.status) {
278
+ sql += " AND runs.status = ?";
279
+ params.push(options.status);
280
+ }
281
+ const stmt = this.getStatement(sql);
282
+ const row = stmt.get(...params);
283
+ return row?.count ?? 0;
284
+ }
285
+ let sql = "SELECT COUNT(*) as count FROM runs WHERE package_id = ?";
286
+ const params = [this.packageId];
287
+ if (options.actionId) {
288
+ sql += " AND action_id = ?";
289
+ params.push(options.actionId);
290
+ }
291
+ if (options.status) {
292
+ sql += " AND status = ?";
293
+ params.push(options.status);
294
+ }
295
+ const stmt = this.getStatement(sql);
296
+ const row = stmt.get(...params);
297
+ return row?.count ?? 0;
298
+ }
299
+ /**
300
+ * 按客户端幂等请求标识反查关联运行记录总数。
301
+ */
302
+ countRunsByRequestIds(isClosed, requestIds, options = {}) {
303
+ return this.countRuns({ ...options, requestIds }, isClosed);
304
+ }
305
+ /**
306
+ * 构造 IN (…) 占位符列表。
307
+ */
308
+ static placeholders(count) {
309
+ return Array.from({ length: count }, () => "?").join(", ");
310
+ }
311
+ /**
312
+ * 批量反查当前包范围内、由指定幂等请求标识登记的运行记录。
313
+ *
314
+ * 关联链为 idempotency_keys.run_id -> runs.id,且强制 runs.package_id 等于
315
+ * 当前包,保证多包共库(全局库)时不越界读取其他包的运行记录。
316
+ * 不按 owner_id 过滤:写入方与查询方分属不同宿主进程,ownerId 必然不同。
317
+ */
318
+ listRunsByRequestIds(isClosed, requestIds) {
319
+ if (isClosed || requestIds.length === 0)
320
+ return [];
321
+ const uniqueIds = Array.from(new Set(requestIds.filter((id) => typeof id === "string" && id.length > 0)));
322
+ if (uniqueIds.length === 0)
323
+ return [];
324
+ const sql = `
325
+ SELECT runs.*, ik.request_id AS request_id
326
+ FROM runs
327
+ INNER JOIN idempotency_keys AS ik ON ik.run_id = runs.id
328
+ WHERE runs.package_id = ?
329
+ AND ik.request_id IN (${SqliteRunsStore.placeholders(uniqueIds.length)})
330
+ ORDER BY runs.started_at DESC
331
+ `;
332
+ const stmt = this.getStatement(sql);
333
+ const rows = stmt.all(this.packageId, ...uniqueIds);
334
+ const records = rows.map((r) => this.mapRunRecord(r));
335
+ // requestId 来自联查列,直接补齐(mapRunRecord 无法从 runs 行还原)
336
+ const requestIdByRunId = new Map();
337
+ for (const row of rows) {
338
+ if (row && typeof row.id === "string" && typeof row.request_id === "string") {
339
+ requestIdByRunId.set(row.id, row.request_id);
340
+ }
341
+ }
342
+ for (const record of records) {
343
+ const requestId = requestIdByRunId.get(record.id);
344
+ if (requestId)
345
+ record.requestId = requestId;
346
+ }
347
+ return records;
348
+ }
349
+ /**
350
+ * 反查当前包范围内运行记录关联的幂等请求标识。
351
+ *
352
+ * 仅返回存在关联的键值对;未登记幂等键的运行记录不产生对应键。
353
+ */
354
+ getRunRequestIds(isClosed, runIds) {
355
+ if (isClosed || runIds.length === 0)
356
+ return {};
357
+ const uniqueIds = Array.from(new Set(runIds.filter((id) => typeof id === "string" && id.length > 0)));
358
+ if (uniqueIds.length === 0)
359
+ return {};
360
+ const sql = `
361
+ SELECT ik.run_id, ik.request_id
362
+ FROM idempotency_keys AS ik
363
+ INNER JOIN runs ON runs.id = ik.run_id
364
+ WHERE runs.package_id = ?
365
+ AND ik.run_id IN (${SqliteRunsStore.placeholders(uniqueIds.length)})
366
+ `;
367
+ const stmt = this.getStatement(sql);
368
+ const rows = stmt.all(this.packageId, ...uniqueIds);
369
+ const result = {};
370
+ for (const row of rows) {
371
+ if (row && typeof row.run_id === "string" && typeof row.request_id === "string") {
372
+ result[row.run_id] = row.request_id;
373
+ }
374
+ }
375
+ return result;
376
+ }
377
+ clearRuns(options = {}) {
378
+ let sql = "DELETE FROM runs WHERE package_id = ?";
379
+ const params = [this.packageId];
380
+ if (options.actionId) {
381
+ sql += " AND action_id = ?";
382
+ params.push(options.actionId);
383
+ }
384
+ if (options.status) {
385
+ sql += " AND status = ?";
386
+ params.push(options.status);
387
+ }
388
+ if (typeof options.olderThanMs === "number" && options.olderThanMs > 0) {
389
+ const cutoff = new Date(this.clock.now().getTime() - options.olderThanMs).toISOString();
390
+ sql += " AND started_at < ?";
391
+ params.push(cutoff);
392
+ }
393
+ if (typeof options.keep === "number" && options.keep > 0) {
394
+ let subQuery = "SELECT id FROM runs WHERE package_id = ?";
395
+ const subParams = [this.packageId];
396
+ if (options.actionId) {
397
+ subQuery += " AND action_id = ?";
398
+ subParams.push(options.actionId);
399
+ }
400
+ if (options.status) {
401
+ subQuery += " AND status = ?";
402
+ subParams.push(options.status);
403
+ }
404
+ subQuery += " ORDER BY started_at DESC LIMIT ?";
405
+ subParams.push(options.keep);
406
+ sql += ` AND id NOT IN (${subQuery})`;
407
+ params.push(...subParams);
408
+ }
409
+ const stmt = this.getStatement(sql);
410
+ const res = stmt.run(...params);
411
+ try {
412
+ this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
413
+ }
414
+ catch (err) {
415
+ this.logWarn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
416
+ }
417
+ return res.changes;
418
+ }
419
+ cleanExpiredRuns(isClosed, policy) {
420
+ if (isClosed)
421
+ return 0;
422
+ try {
423
+ const resolved = this.resolveRetentionPolicy(policy);
424
+ const { maxAgeMs, maxRuns, minRetainRuns } = resolved;
425
+ let totalDeleted = 0;
426
+ if (maxAgeMs > 0) {
427
+ const now = this.clock.now().getTime();
428
+ const cutoff = new Date(now - maxAgeMs).toISOString();
429
+ if (minRetainRuns > 0) {
430
+ const stmt = this.getStatement(`
431
+ DELETE FROM runs
432
+ WHERE package_id = ?
433
+ AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
434
+ AND started_at < ?
435
+ AND id NOT IN (
436
+ SELECT id FROM runs
437
+ WHERE package_id = ?
438
+ AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
439
+ ORDER BY started_at DESC
440
+ LIMIT ?
441
+ )
442
+ `);
443
+ const res = stmt.run(this.packageId, cutoff, this.packageId, minRetainRuns);
444
+ totalDeleted += res.changes;
445
+ }
446
+ else {
447
+ const stmt = this.getStatement(`
448
+ DELETE FROM runs
449
+ WHERE package_id = ?
450
+ AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
451
+ AND started_at < ?
452
+ `);
453
+ const res = stmt.run(this.packageId, cutoff);
454
+ totalDeleted += res.changes;
455
+ }
456
+ }
457
+ if (maxRuns > 0) {
458
+ const stmt = this.getStatement(`
459
+ DELETE FROM runs
460
+ WHERE package_id = ?
461
+ AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
462
+ AND id NOT IN (
463
+ SELECT id FROM runs
464
+ WHERE package_id = ?
465
+ AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
466
+ ORDER BY started_at DESC
467
+ LIMIT ?
468
+ )
469
+ `);
470
+ const res = stmt.run(this.packageId, this.packageId, maxRuns);
471
+ totalDeleted += res.changes;
472
+ }
473
+ if (totalDeleted > 0) {
474
+ try {
475
+ this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
476
+ }
477
+ catch (err) {
478
+ this.logWarn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
479
+ }
480
+ }
481
+ return totalDeleted;
482
+ }
483
+ catch (err) {
484
+ this.logWarn(`[actiondock] cleanExpiredRuns failed: ${err instanceof Error ? err.message : String(err)}`);
485
+ return 0;
486
+ }
487
+ }
488
+ resolveRetentionPolicy(policy) {
489
+ let configDays;
490
+ let configMaxRuns;
491
+ let configMinRetain;
492
+ if (this.getConfigFn) {
493
+ try {
494
+ configDays = this.getConfigFn("runs.retentionDays");
495
+ configMaxRuns = this.getConfigFn("runs.maxRuns");
496
+ configMinRetain = this.getConfigFn("runs.minRetainRuns");
497
+ }
498
+ catch { }
499
+ }
500
+ const envDays = process.env.ACTIONDOCK_RUNS_RETENTION_DAYS
501
+ ? parseInt(process.env.ACTIONDOCK_RUNS_RETENTION_DAYS, 10)
502
+ : undefined;
503
+ const envMaxRuns = process.env.ACTIONDOCK_RUNS_MAX_COUNT
504
+ ? parseInt(process.env.ACTIONDOCK_RUNS_MAX_COUNT, 10)
505
+ : undefined;
506
+ const envMinRetain = process.env.ACTIONDOCK_RUNS_MIN_RETAIN
507
+ ? parseInt(process.env.ACTIONDOCK_RUNS_MIN_RETAIN, 10)
508
+ : undefined;
509
+ const maxAgeMs = policy?.maxAgeMs ??
510
+ this.retentionPolicy?.maxAgeMs ??
511
+ (typeof configDays === "number" && configDays > 0 ? configDays * 86_400_000 : undefined) ??
512
+ (typeof envDays === "number" && !isNaN(envDays) && envDays > 0 ? envDays * 86_400_000 : undefined) ??
513
+ DEFAULT_RUNS_RETENTION_MS;
514
+ const maxRuns = policy?.maxRuns ??
515
+ this.retentionPolicy?.maxRuns ??
516
+ (typeof configMaxRuns === "number" && configMaxRuns > 0 ? configMaxRuns : undefined) ??
517
+ (typeof envMaxRuns === "number" && !isNaN(envMaxRuns) && envMaxRuns > 0 ? envMaxRuns : undefined) ??
518
+ DEFAULT_MAX_RUNS_PER_PACKAGE;
519
+ const minRetainRuns = policy?.minRetainRuns ??
520
+ this.retentionPolicy?.minRetainRuns ??
521
+ (typeof configMinRetain === "number" && configMinRetain >= 0 ? configMinRetain : undefined) ??
522
+ (typeof envMinRetain === "number" && !isNaN(envMinRetain) && envMinRetain >= 0 ? envMinRetain : undefined) ??
523
+ DEFAULT_MIN_RETAIN_RUNS;
524
+ return { maxAgeMs, maxRuns, minRetainRuns };
525
+ }
526
+ checkAndRecordIdempotency(isClosed, record) {
527
+ if (isClosed) {
528
+ throw new ActionDockError(STORAGE_CLOSED, "SqliteRuntimeStorage is closed");
529
+ }
530
+ const checkStmt = this.getStatement(`
531
+ SELECT input_digest, run_id FROM idempotency_keys
532
+ WHERE owner_id = ? AND action_ref = ? AND request_id = ?
533
+ `);
534
+ const existing = checkStmt.get(record.ownerId, record.actionRef, record.requestId);
535
+ if (existing) {
536
+ const runStmt = this.getStatement("SELECT id FROM runs WHERE id = ?");
537
+ const runExists = runStmt.get(existing.run_id);
538
+ if (!runExists) {
539
+ this.driver.transaction(() => {
540
+ const delStmt = this.getStatement(`
541
+ DELETE FROM idempotency_keys
542
+ WHERE owner_id = ? AND action_ref = ? AND request_id = ?
543
+ `);
544
+ delStmt.run(record.ownerId, record.actionRef, record.requestId);
545
+ const insertStmt = this.getStatement(`
546
+ INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
547
+ VALUES (?, ?, ?, ?, ?, ?)
548
+ `);
549
+ const createdAt = record.createdAt || this.clock.now().toISOString();
550
+ insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
551
+ });
552
+ return { outcome: "new" };
553
+ }
554
+ if (existing.input_digest !== record.inputDigest) {
555
+ return { outcome: "conflict", existingDigest: existing.input_digest };
556
+ }
557
+ return { outcome: "duplicate", runId: existing.run_id };
558
+ }
559
+ this.driver.transaction(() => {
560
+ const insertStmt = this.getStatement(`
561
+ INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
562
+ VALUES (?, ?, ?, ?, ?, ?)
563
+ `);
564
+ const createdAt = record.createdAt || this.clock.now().toISOString();
565
+ insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
566
+ });
567
+ return { outcome: "new" };
568
+ }
569
+ getIdempotencyRecord(isClosed, ownerId, actionRef, requestId) {
570
+ if (isClosed)
571
+ return undefined;
572
+ const stmt = this.getStatement(`
573
+ SELECT owner_id, action_ref, request_id, input_digest, run_id, created_at
574
+ FROM idempotency_keys
575
+ WHERE owner_id = ? AND action_ref = ? AND request_id = ?
576
+ `);
577
+ const row = stmt.get(ownerId, actionRef, requestId);
578
+ if (!row)
579
+ return undefined;
580
+ return {
581
+ ownerId: row.owner_id,
582
+ actionRef: row.action_ref,
583
+ requestId: row.request_id,
584
+ inputDigest: row.input_digest,
585
+ runId: row.run_id,
586
+ createdAt: row.created_at,
587
+ };
588
+ }
589
+ mapRunRecord(row) {
590
+ const input = safeParseStoredJson(row.input_json || undefined, `runs.input db=${this.dbPath} package=${this.packageId} run=${row.id}`);
591
+ const output = safeParseStoredJson(row.output_json || undefined, `runs.output db=${this.dbPath} package=${this.packageId} run=${row.id}`);
592
+ const error = safeParseStoredJson(row.error_json || undefined, `runs.error db=${this.dbPath} package=${this.packageId} run=${row.id}`);
593
+ const normalizedError = error !== undefined
594
+ ? typeof error === "object" && error !== null && typeof error.message === "string"
595
+ ? error
596
+ : { code: STORED_ERROR_DECODE_FAILED, message: String(error) }
597
+ : undefined;
598
+ return {
599
+ id: row.id,
600
+ rootRunId: row.root_run_id || row.id,
601
+ parentRunId: row.parent_run_id || undefined,
602
+ packageId: row.package_id,
603
+ packageInstanceId: row.package_instance_id || row.package_id,
604
+ actionId: row.action_id,
605
+ generationId: row.generation_id || "1",
606
+ ownerId: row.owner_id || "local",
607
+ hostSessionId: row.host_session_id || undefined,
608
+ hostPid: typeof row.host_pid === "number" ? row.host_pid : undefined,
609
+ heartbeatAt: row.heartbeat_at || undefined,
610
+ status: row.status,
611
+ input,
612
+ output,
613
+ error: normalizedError,
614
+ startedAt: row.started_at,
615
+ finishedAt: row.finished_at || undefined,
616
+ durationMs: typeof row.duration_ms === "number" ? row.duration_ms : undefined,
617
+ };
618
+ }
619
+ close() {
620
+ this.statementCache.clear();
621
+ }
622
+ }
@@ -0,0 +1,5 @@
1
+ import { type SqliteDriver } from "./types.js";
2
+ /**
3
+ * 数据库 Schema 定义、初始化与版本校验。
4
+ */
5
+ export declare function initSchema(driver: SqliteDriver, dbPath: string, recoverOrphans: boolean, recoverDeadSessionFn: () => void, cleanExpiredRunsFn: () => void): void;