@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,49 +2,44 @@ import { chmodSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, } from "@actiondock/sdk";
4
4
  import { SystemClock } from "./clock.js";
5
- import { createDefaultSqliteDriver } from "./driver.js";
6
- import { safeParseStoredJson } from "./utils.js";
7
- import { ActionDockError, AMBIGUOUS_STATE_KEY, STORED_ERROR_DECODE_FAILED, UNSUPPORTED_STORAGE_SCHEMA } from "../errors.js";
8
- import { DEFAULT_MAX_RUNS_PER_PACKAGE, DEFAULT_MIN_RETAIN_RUNS, DEFAULT_RUNS_RETENTION_MS, IDEMPOTENCY_RETENTION_MS, STORAGE_SCHEMA_VERSION, isTerminalRunStatus, } from "./types.js";
5
+ import { NodeSqliteDriver } from "./sqlite-driver.js";
6
+ import { initSchema } from "./sqlite-schema.js";
7
+ import { SqliteRunsStore } from "./sqlite-runs.js";
8
+ import { SqliteStateStore } from "./sqlite-state.js";
9
+ import { SqliteConfigStore } from "./sqlite-config.js";
10
+ import { SqliteEventsStore } from "./sqlite-events.js";
9
11
  export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
10
- /**
11
- * 统一 SQLite 运行时存储实现。
12
- * 通过 SqliteDriver 抽象驱动,解耦底层具体运行时引擎。
13
- *
14
- * 跨进程并发模型:
15
- * - 同一数据目录下的 runtime.db 允许持有者进程(serve、mcp、ad run 等执行宿主)
16
- * 与旁观查询进程(ad state、ad runs、ad config 等只读命令)并发打开。
17
- * - 死亡会话遗留非终态运行记录的收割(recoverDeadSessionRuns)仅由显式声明
18
- * 持有者身份的打开方执行:StorageOptions.recoverOrphans 为 true 时构造阶段收割,
19
- * 宿主接管路径亦可显式调用 recoverDeadSessionRuns 收敛死亡会话记录。
20
- * - 旁观打开(默认 recoverOrphans 为 false)保持只读语义,绝不收敛其他进程的在途
21
- * running 记录,防止查询进程把执行宿主的在途运行误收割为 interrupted,导致持有者
22
- * 侧终态结算静默丢失。
23
- * - updateRun 以 WHERE id = ? AND status = 'running' 条件更新;若命中零行且新状态
24
- * 为终态,说明记录已被其他持有者或恢复流程改写,此处输出告警保证状态漂移可观测。
25
- */
26
12
  export class SqliteRuntimeStorage {
27
13
  driver;
28
14
  packageId;
29
15
  clock;
16
+ logger;
30
17
  isClosed = false;
31
- statementCache = new Map();
32
18
  dbPath;
33
19
  recoverOrphans;
34
20
  retentionPolicy;
35
- lastRunsCleanupAt = 0;
36
- runInsertCount = 0;
37
- static RUNS_CLEANUP_INTERVAL_MS = 3600_000;
38
- static RUNS_CLEANUP_BATCH_THRESHOLD = 50;
21
+ runsStore;
22
+ stateStore;
23
+ configStore;
24
+ eventsStore;
39
25
  get isOpen() {
40
26
  return !this.isClosed;
41
27
  }
42
28
  get closed() {
43
29
  return this.isClosed;
44
30
  }
31
+ logWarn(message, ...args) {
32
+ if (this.logger) {
33
+ this.logger.warn(message, ...args);
34
+ }
35
+ else {
36
+ console.warn(message, ...args);
37
+ }
38
+ }
45
39
  constructor(options) {
46
40
  this.packageId = options.packageId;
47
41
  this.clock = options.clock ?? new SystemClock();
42
+ this.logger = options.logger;
48
43
  const dbPath = options.dbPath || ":memory:";
49
44
  this.dbPath = dbPath;
50
45
  this.recoverOrphans = options.recoverOrphans === true;
@@ -61,7 +56,7 @@ export class SqliteRuntimeStorage {
61
56
  }
62
57
  }
63
58
  }
64
- this.driver = options.driver ?? createDefaultSqliteDriver(dbPath);
59
+ this.driver = options.driver ?? new NodeSqliteDriver(dbPath);
65
60
  if (dbPath !== ":memory:" && existsSync(dbPath)) {
66
61
  try {
67
62
  chmodSync(dbPath, 0o600);
@@ -72,782 +67,130 @@ export class SqliteRuntimeStorage {
72
67
  }
73
68
  this.init();
74
69
  }
75
- /**
76
- * 初始化数据库结构并执行严格版本校验。
77
- *
78
- * 规则:
79
- * - 空数据目录首次初始化在单个事务中完成完整 Schema 构建。
80
- * - 若创建事务失败则拒绝启动且不留下残缺表。
81
- * - 打开旧版本或不兼容版本的数据库时,在写事务前直接抛出 UNSUPPORTED_STORAGE_SCHEMA 异常并拒绝启动。
82
- */
83
70
  init() {
84
- this.driver.exec("PRAGMA journal_mode = WAL;");
85
- this.driver.exec("PRAGMA synchronous = NORMAL;");
86
- // 跨进程写并发基线:SQLite 默认忙等为零,持有者与旁观写进程的写窗口重叠时
87
- // 会立即抛 SQLITE_BUSY。统一设置 5 秒忙等,使默认同步驱动、工作线程驱动
88
- // (worker 脚本已内置同值)与任何注入驱动的行为基线一致。
89
- this.driver.exec("PRAGMA busy_timeout = 5000;");
90
- // 读取 Schema 版本号
91
- const versionRes = this.driver.prepare("PRAGMA user_version;").get();
92
- const version = Number(versionRes?.user_version ?? 0);
93
- if (version === 0) {
94
- this.driver.transaction(() => {
95
- this.driver.exec(`
96
- CREATE TABLE IF NOT EXISTS config (
97
- package_id TEXT NOT NULL,
98
- key TEXT NOT NULL,
99
- value_json TEXT,
100
- updated_at TEXT NOT NULL,
101
- PRIMARY KEY (package_id, key)
102
- );
103
-
104
- CREATE TABLE IF NOT EXISTS state (
105
- package_id TEXT NOT NULL,
106
- namespace TEXT NOT NULL,
107
- key TEXT NOT NULL,
108
- value_json TEXT,
109
- updated_at TEXT NOT NULL,
110
- expires_at TEXT,
111
- PRIMARY KEY (package_id, namespace, key)
112
- );
113
-
114
- CREATE TABLE IF NOT EXISTS runs (
115
- id TEXT PRIMARY KEY,
116
- root_run_id TEXT NOT NULL,
117
- parent_run_id TEXT,
118
- package_id TEXT NOT NULL,
119
- package_instance_id TEXT NOT NULL,
120
- action_id TEXT NOT NULL,
121
- generation_id TEXT NOT NULL,
122
- owner_id TEXT NOT NULL,
123
- host_session_id TEXT,
124
- status TEXT NOT NULL,
125
- input_json TEXT,
126
- output_json TEXT,
127
- error_json TEXT,
128
- started_at TEXT NOT NULL,
129
- finished_at TEXT,
130
- duration_ms INTEGER
131
- );
132
-
133
- CREATE INDEX IF NOT EXISTS idx_runs_action ON runs(package_id, action_id);
134
- CREATE INDEX IF NOT EXISTS idx_runs_root ON runs(root_run_id);
135
- CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
136
- CREATE INDEX IF NOT EXISTS idx_runs_host_session ON runs(host_session_id);
137
- CREATE INDEX IF NOT EXISTS idx_state_expires ON state(expires_at);
138
-
139
- CREATE TABLE IF NOT EXISTS idempotency_keys (
140
- owner_id TEXT NOT NULL,
141
- action_ref TEXT NOT NULL,
142
- request_id TEXT NOT NULL,
143
- input_digest TEXT NOT NULL,
144
- run_id TEXT NOT NULL,
145
- created_at TEXT NOT NULL,
146
- PRIMARY KEY (owner_id, action_ref, request_id)
147
- );
148
- CREATE INDEX IF NOT EXISTS idx_idemp_run ON idempotency_keys(run_id);
149
-
150
- PRAGMA user_version = ${STORAGE_SCHEMA_VERSION};
151
- `);
152
- });
153
- }
154
- else if (version !== STORAGE_SCHEMA_VERSION) {
155
- const err = new Error(`UNSUPPORTED_STORAGE_SCHEMA: Database schema version ${version} is incompatible (expected ${STORAGE_SCHEMA_VERSION}). Silent upgrade and database overwriting are strictly prohibited.`);
156
- err.code = UNSUPPORTED_STORAGE_SCHEMA;
157
- throw err;
158
- }
159
- // 确保已有历史数据库补充迁移 host_session_id 列
160
- if (version !== 0) {
71
+ // 延迟初始化子存储以便它们可以访问 driver 等,但把初始化本身移到了 schema
72
+ this.configStore = new SqliteConfigStore(this.driver, this.packageId, this.dbPath, this.clock);
73
+ this.stateStore = new SqliteStateStore(this.driver, this.packageId, this.dbPath, this.clock, this.logger, (msg) => this.logWarn(msg));
74
+ this.runsStore = new SqliteRunsStore(this.driver, this.packageId, this.dbPath, this.clock, this.retentionPolicy, (k) => this.getConfig(k), this.logger, (msg) => this.logWarn(msg));
75
+ this.eventsStore = new SqliteEventsStore(this.driver, this.packageId, this.clock);
76
+ initSchema(this.driver, this.dbPath, this.recoverOrphans, () => this.recoverDeadSessionRuns(), () => this.cleanExpiredRuns());
77
+ if (this.dbPath !== ":memory:" && existsSync(this.dbPath)) {
161
78
  try {
162
- const columns = this.driver.prepare("PRAGMA table_info(runs);").all();
163
- if (Array.isArray(columns) && !columns.some((c) => c.name === "host_session_id")) {
164
- this.driver.exec("ALTER TABLE runs ADD COLUMN host_session_id TEXT;");
165
- }
79
+ chmodSync(this.dbPath, 0o600);
166
80
  }
167
81
  catch {
168
- // 忽略非结构化表或兼容驱动异常
82
+ // 忽略文件权限设置异常
169
83
  }
170
84
  }
171
- // 重启恢复:仅持有者身份的打开方在构造阶段收割死亡会话遗留的非终态运行并执行历史记录清理;
172
- // 旁观查询打开保持只读语义,绝不触碰其他进程的在途记录
173
- if (this.recoverOrphans) {
174
- this.recoverDeadSessionRuns();
175
- this.cleanExpiredRuns();
176
- }
177
85
  }
178
- /**
179
- * 收敛死亡会话遗留的非终态运行任务。
180
- */
181
- recoverDeadSessionRuns(currentHostSessionId) {
182
- if (this.isClosed)
183
- return 0;
184
- try {
185
- const now = this.clock.now().toISOString();
186
- let sql = `
187
- UPDATE runs
188
- SET status = 'interrupted',
189
- finished_at = ?,
190
- error_json = '{"code":"RUN_INTERRUPTED","message":"Execution interrupted by system shutdown or restart"}'
191
- WHERE status IN ('running', 'pending')
192
- `;
193
- const params = [now];
194
- if (currentHostSessionId) {
195
- sql += " AND (host_session_id IS NULL OR host_session_id != ?)";
196
- params.push(currentHostSessionId);
197
- }
198
- const stmt = this.getStatement(sql);
199
- const res = stmt.run(...params);
200
- return res.changes;
201
- }
202
- catch (err) {
203
- console.warn(`[actiondock] recoverDeadSessionRuns failed: ${err instanceof Error ? err.message : String(err)}`);
204
- return 0;
205
- }
206
- }
207
- /**
208
- * 获取或复用预编译 SQL 语句缓存。
209
- */
210
- getStatement(sql) {
211
- let stmt = this.statementCache.get(sql);
212
- if (!stmt) {
213
- stmt = this.driver.prepare(sql);
214
- this.statementCache.set(sql, stmt);
215
- }
216
- return stmt;
86
+ recoverDeadSessionRuns(currentHostSessionId, options) {
87
+ return this.runsStore.recoverDeadSessionRuns(this.isClosed, currentHostSessionId, options);
217
88
  }
218
89
  // --- Config 配置管理 ---
219
90
  getConfig(key) {
220
- const stmt = this.getStatement("SELECT value_json FROM config WHERE package_id = ? AND key = ?");
221
- const row = stmt.get(this.packageId, key);
222
- if (!row || row.value_json === undefined || row.value_json === null) {
223
- return undefined;
224
- }
225
- return safeParseStoredJson(row.value_json, `config db=${this.dbPath} package=${this.packageId} key=${key}`);
91
+ return this.configStore.getConfig(key);
226
92
  }
227
93
  listConfig() {
228
- const stmt = this.getStatement("SELECT key, value_json FROM config WHERE package_id = ?");
229
- const rows = stmt.all(this.packageId);
230
- const result = {};
231
- for (const row of rows) {
232
- result[row.key] = safeParseStoredJson(row.value_json, `config db=${this.dbPath} package=${this.packageId} key=${row.key}`);
233
- }
234
- return result;
94
+ return this.configStore.listConfig();
235
95
  }
236
96
  setConfig(key, value) {
237
- const stmt = this.getStatement(`
238
- INSERT INTO config (package_id, key, value_json, updated_at)
239
- VALUES (?, ?, ?, ?)
240
- ON CONFLICT(package_id, key) DO UPDATE SET
241
- value_json = excluded.value_json,
242
- updated_at = excluded.updated_at
243
- `);
244
- const valJson = JSON.stringify(value);
245
- const now = this.clock.now().toISOString();
246
- stmt.run(this.packageId, key, valJson, now);
97
+ this.configStore.setConfig(key, value);
247
98
  }
248
99
  deleteConfig(key) {
249
- const stmt = this.getStatement("DELETE FROM config WHERE package_id = ? AND key = ?");
250
- const res = stmt.run(this.packageId, key);
251
- return res.changes > 0;
100
+ return this.configStore.deleteConfig(key);
252
101
  }
253
102
  // --- State 状态管理 ---
254
103
  async getState(namespace, key) {
255
- const stmt = this.getStatement("SELECT value_json, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
256
- const row = stmt.get(this.packageId, namespace, key);
257
- if (!row || row.value_json === undefined || row.value_json === null) {
258
- return undefined;
259
- }
260
- if (row.expires_at) {
261
- const expires = new Date(row.expires_at).getTime();
262
- if (this.clock.now().getTime() >= expires) {
263
- // 惰性删除失败仅降级不影响读取语义,但必须可观测,与其他存储降级点对齐
264
- this.deleteState(namespace, key).catch((err) => {
265
- const reason = err instanceof Error ? err.message : String(err);
266
- console.warn(`[actiondock] expired state lazy delete failed (namespace='${namespace}' key='${key}'): ${reason}`);
267
- });
268
- return undefined;
269
- }
270
- }
271
- return safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${namespace} key=${key}`);
272
- }
273
- async findMatchingStateRows(targetKey) {
274
- let decoded;
275
- try {
276
- decoded = decodeStateKey(targetKey);
277
- }
278
- catch {
279
- // 键名包含多个未转义冒号
280
- }
281
- const candidateStmt = this.getStatement(`
282
- SELECT namespace, key, value_json, updated_at, expires_at FROM state
283
- WHERE package_id = ? AND (
284
- (CASE WHEN ? IS NOT NULL THEN (namespace = ? AND key = ?) ELSE 0 END)
285
- OR (CASE WHEN namespace = '' THEN key ELSE namespace || ':' || key END) = ?
286
- OR key = ?
287
- )
288
- `);
289
- const rawResult = candidateStmt.all(this.packageId, decoded ? 1 : null, decoded?.namespace ?? "", decoded?.key ?? "", targetKey, targetKey);
290
- const rawRows = Array.isArray(rawResult) ? rawResult : [];
291
- const now = this.clock.now().getTime();
292
- const uniqueMap = new Map();
293
- for (const r of rawRows) {
294
- if (r.expires_at && now >= new Date(r.expires_at).getTime()) {
295
- // 惰性删除失败仅降级不影响匹配语义,但必须可观测,与其他存储降级点对齐
296
- const ns = r.namespace;
297
- const k = r.key;
298
- this.deleteState(ns, k).catch((err) => {
299
- const reason = err instanceof Error ? err.message : String(err);
300
- console.warn(`[actiondock] expired state lazy delete failed (namespace='${ns}' key='${k}'): ${reason}`);
301
- });
302
- continue;
303
- }
304
- uniqueMap.set(`${r.namespace}\0${r.key}`, r);
305
- }
306
- return Array.from(uniqueMap.values());
104
+ return this.stateStore.getState(namespace, key);
307
105
  }
308
106
  async findState(targetKey, namespace) {
309
- if (namespace !== undefined) {
310
- const val = await this.getState(namespace, targetKey);
311
- if (val === undefined)
312
- return undefined;
313
- const stmt = this.getStatement("SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
314
- const row = stmt.get(this.packageId, namespace, targetKey);
315
- return {
316
- packageId: this.packageId,
317
- namespace,
318
- key: targetKey,
319
- fullKey: encodeStateKey(namespace, targetKey),
320
- value: val,
321
- updatedAt: row?.updated_at || this.clock.now().toISOString(),
322
- expiresAt: row?.expires_at,
323
- };
324
- }
325
- const matchingRows = await this.findMatchingStateRows(targetKey);
326
- if (matchingRows.length > 1) {
327
- throw new ActionDockError(AMBIGUOUS_STATE_KEY, `Ambiguous state key '${targetKey}': matches ${matchingRows.length} entries (${matchingRows.map((r) => `${r.namespace}:${r.key}`).join(", ")})`);
328
- }
329
- if (matchingRows.length === 0) {
330
- return undefined;
331
- }
332
- const row = matchingRows[0];
333
- const parsedVal = safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${row.namespace} key=${row.key}`);
334
- return {
335
- packageId: this.packageId,
336
- namespace: row.namespace,
337
- key: row.key,
338
- fullKey: encodeStateKey(row.namespace, row.key),
339
- value: parsedVal,
340
- updatedAt: row.updated_at,
341
- expiresAt: row.expires_at,
342
- };
107
+ return this.stateStore.findState(targetKey, namespace);
343
108
  }
344
109
  async setState(namespace, key, value, ttl) {
345
- const stmt = this.getStatement(`
346
- INSERT INTO state (package_id, namespace, key, value_json, updated_at, expires_at)
347
- VALUES (?, ?, ?, ?, ?, ?)
348
- ON CONFLICT(package_id, namespace, key) DO UPDATE SET
349
- value_json = excluded.value_json,
350
- updated_at = excluded.updated_at,
351
- expires_at = excluded.expires_at
352
- `);
353
- const valJson = JSON.stringify(value);
354
- const now = this.clock.now();
355
- const updatedAt = now.toISOString();
356
- let expiresAt = null;
357
- if (typeof ttl === "number" && ttl > 0) {
358
- expiresAt = new Date(now.getTime() + ttl * 1000).toISOString();
359
- }
360
- stmt.run(this.packageId, namespace, key, valJson, updatedAt, expiresAt);
110
+ return this.stateStore.setState(namespace, key, value, ttl);
361
111
  }
362
112
  async deleteState(namespace, key) {
363
- const stmt = this.getStatement("DELETE FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
364
- const res = stmt.run(this.packageId, namespace, key);
365
- return res.changes > 0;
113
+ return this.stateStore.deleteState(namespace, key);
366
114
  }
367
115
  async deleteStateSmart(targetKey, namespace) {
368
- if (namespace !== undefined) {
369
- return this.deleteState(namespace, targetKey);
370
- }
371
- const matchingRows = await this.findMatchingStateRows(targetKey);
372
- if (matchingRows.length > 1) {
373
- throw new ActionDockError(AMBIGUOUS_STATE_KEY, `Ambiguous state key '${targetKey}': matches ${matchingRows.length} entries for deletion (${matchingRows.map((r) => `${r.namespace}:${r.key}`).join(", ")})`);
374
- }
375
- if (matchingRows.length === 1) {
376
- return this.deleteState(matchingRows[0].namespace, matchingRows[0].key);
377
- }
378
- return this.deleteState("", targetKey);
379
- }
380
- /**
381
- * 清理已过期的状态记录。
382
- *
383
- * 同时按 TargetInfo.idempotencyPolicy 声明的 24 小时保留窗口,
384
- * 清理 idempotency_keys 表中超过保留期的幂等去重记录,
385
- * 保证承诺的 retentionMs 策略与实际清理逻辑一致。
386
- */
116
+ return this.stateStore.deleteStateSmart(targetKey, namespace);
117
+ }
387
118
  async cleanExpiredState() {
388
- if (this.isClosed)
389
- return 0;
390
- try {
391
- const now = this.clock.now();
392
- const stmt = this.getStatement("DELETE FROM state WHERE package_id = ? AND expires_at IS NOT NULL AND expires_at <= ?");
393
- const res = stmt.run(this.packageId, now.toISOString());
394
- // 幂等去重记录保留窗口与 info() 声明的 retentionMs(24 小时)保持一致
395
- const retentionCutoff = new Date(now.getTime() - IDEMPOTENCY_RETENTION_MS).toISOString();
396
- try {
397
- this.getStatement("DELETE FROM idempotency_keys WHERE created_at < ?").run(retentionCutoff);
398
- }
399
- catch (err) {
400
- // 独立记录幂等清理失败,不影响状态清理主路径的返回语义
401
- console.warn(`[actiondock] idempotency retention cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
402
- }
403
- return res.changes;
404
- }
405
- catch (err) {
406
- console.warn(`[actiondock] cleanExpiredState failed: ${err instanceof Error ? err.message : String(err)}`);
407
- return 0;
408
- }
119
+ return this.stateStore.cleanExpiredState(this.isClosed);
409
120
  }
410
121
  async clearState(options = {}) {
411
- let sql = "DELETE FROM state WHERE package_id = ?";
412
- const params = [this.packageId];
413
- if (options.namespace !== undefined) {
414
- sql += " AND namespace = ?";
415
- params.push(options.namespace);
416
- }
417
- if (options.prefix) {
418
- sql += " AND key LIKE ? ESCAPE '\\'";
419
- const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
420
- params.push(`${escapedPrefix}%`);
421
- }
422
- const stmt = this.getStatement(sql);
423
- const res = stmt.run(...params);
424
- return res.changes;
122
+ return this.stateStore.clearState(options);
425
123
  }
426
124
  async listStateKeys(namespace, prefix) {
427
- await this.cleanExpiredState();
428
- let sql = "SELECT namespace, key, expires_at FROM state WHERE package_id = ?";
429
- const params = [this.packageId];
430
- if (namespace !== null && namespace !== undefined) {
431
- sql += " AND namespace = ?";
432
- params.push(namespace);
433
- }
434
- if (prefix) {
435
- sql += " AND key LIKE ? ESCAPE '\\'";
436
- const escapedPrefix = prefix.replace(/([%_\\])/g, "\\$1");
437
- params.push(`${escapedPrefix}%`);
438
- }
439
- const stmt = this.getStatement(sql);
440
- const rawResult = stmt.all(...params);
441
- const rows = Array.isArray(rawResult) ? rawResult : [];
442
- const now = this.clock.now().getTime();
443
- const result = [];
444
- for (const row of rows) {
445
- if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
446
- continue;
447
- }
448
- if (namespace !== null && namespace !== undefined) {
449
- result.push(row.key);
450
- }
451
- else {
452
- result.push(encodeStateKey(row.namespace, row.key));
453
- }
454
- }
455
- return result;
125
+ return this.stateStore.listStateKeys(namespace, prefix, this.isClosed);
456
126
  }
457
127
  async listStateEntries(options = {}) {
458
- await this.cleanExpiredState();
459
- let sql = "SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ?";
460
- const params = [this.packageId];
461
- if (options.namespace !== undefined) {
462
- sql += " AND namespace = ?";
463
- params.push(options.namespace);
464
- }
465
- if (options.prefix) {
466
- sql += " AND key LIKE ? ESCAPE '\\'";
467
- const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
468
- params.push(`${escapedPrefix}%`);
469
- }
470
- const stmt = this.getStatement(sql);
471
- const rawResult = stmt.all(...params);
472
- const rows = Array.isArray(rawResult) ? rawResult : [];
473
- const now = this.clock.now().getTime();
474
- const results = [];
475
- for (const row of rows) {
476
- if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
477
- continue;
478
- }
479
- const parsedVal = safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${row.namespace} key=${row.key}`);
480
- results.push({
481
- packageId: this.packageId,
482
- namespace: row.namespace,
483
- key: row.key,
484
- fullKey: encodeStateKey(row.namespace, row.key),
485
- value: parsedVal,
486
- updatedAt: row.updated_at,
487
- expiresAt: row.expires_at,
488
- });
489
- }
490
- return results;
128
+ return this.stateStore.listStateEntries(options, this.isClosed);
491
129
  }
492
130
  // --- Runs 运行记录管理 ---
493
131
  createRun(record) {
494
- const stmt = this.getStatement(`
495
- INSERT INTO runs (
496
- id, root_run_id, parent_run_id, package_id, package_instance_id,
497
- action_id, generation_id, owner_id, host_session_id, status, input_json, output_json,
498
- error_json, started_at, finished_at, duration_ms
499
- )
500
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
501
- `);
502
- const rootRunId = record.rootRunId || record.id;
503
- const parentRunId = record.parentRunId || null;
504
- const packageInstanceId = record.packageInstanceId || record.packageId || this.packageId;
505
- const generationId = record.generationId || "1";
506
- const ownerId = record.ownerId || "local";
507
- const hostSessionId = record.hostSessionId || null;
508
- const inputJson = record.input !== undefined ? JSON.stringify(record.input) : null;
509
- const outputJson = record.output !== undefined ? JSON.stringify(record.output) : null;
510
- const errorJson = record.error ? JSON.stringify(record.error) : null;
511
- const finishedAt = record.finishedAt || null;
512
- const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
513
- stmt.run(record.id, rootRunId, parentRunId, record.packageId || this.packageId, packageInstanceId, record.actionId ?? "", generationId, ownerId, hostSessionId, record.status, inputJson, outputJson, errorJson, record.startedAt || new Date().toISOString(), finishedAt, durationMs);
514
- this.runInsertCount++;
515
- const nowMs = this.clock.now().getTime();
516
- if (this.runInsertCount >= SqliteRuntimeStorage.RUNS_CLEANUP_BATCH_THRESHOLD &&
517
- nowMs - this.lastRunsCleanupAt >= SqliteRuntimeStorage.RUNS_CLEANUP_INTERVAL_MS) {
518
- this.lastRunsCleanupAt = nowMs;
519
- this.runInsertCount = 0;
520
- try {
521
- this.cleanExpiredRuns();
522
- }
523
- catch (cleanupErr) {
524
- console.warn(`[actiondock] opportunistic cleanExpiredRuns failed: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
525
- }
526
- }
132
+ this.runsStore.createRun(record);
527
133
  }
528
134
  updateRun(id, status, output, error, finishedAt) {
529
- if (this.isClosed)
530
- return;
531
- try {
532
- const finishIso = finishedAt || this.clock.now().toISOString();
533
- const stmt = this.getStatement(`
534
- UPDATE runs
535
- SET status = ?, output_json = ?, error_json = ?, finished_at = ?,
536
- duration_ms = CASE
537
- WHEN started_at IS NOT NULL THEN MAX(0, CAST(ROUND((julianday(?) - julianday(started_at)) * 86400000) AS INTEGER))
538
- ELSE NULL
539
- END
540
- WHERE id = ? AND status = 'running'
541
- `);
542
- const res = stmt.run(status, output !== undefined ? JSON.stringify(output) : null, error ? JSON.stringify(error) : null, finishIso, finishIso, id);
543
- // 命中零行且新状态为终态:说明该记录已不再处于 running 状态(被其他持有者进程或
544
- // 恢复流程改写),本次终态写入被丢弃;保持返回值语义不变,仅输出告警保证漂移可观测
545
- if (res.changes === 0 && isTerminalRunStatus(status)) {
546
- console.warn(`[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)`);
547
- }
548
- }
549
- catch (err) {
550
- if (this.isClosed)
551
- return;
552
- console.warn(`[SqliteRuntimeStorage] Failed to update run "${id}":`, err);
553
- throw err;
554
- }
135
+ this.runsStore.updateRun(id, status, this.isClosed, output, error, finishedAt);
555
136
  }
556
137
  getRun(id) {
557
- const stmt = this.getStatement("SELECT * FROM runs WHERE id = ? AND package_id = ?");
558
- const row = stmt.get(id, this.packageId);
559
- if (!row)
560
- return null;
561
- return this.mapRunRecord(row);
138
+ return this.runsStore.getRun(id);
562
139
  }
563
140
  listRuns(options = {}) {
564
- const limit = options.limit || 50;
565
- let sql = "SELECT * FROM runs WHERE package_id = ?";
566
- const params = [this.packageId];
567
- if (options.actionId) {
568
- sql += " AND action_id = ?";
569
- params.push(options.actionId);
570
- }
571
- if (options.status) {
572
- sql += " AND status = ?";
573
- params.push(options.status);
574
- }
575
- sql += " ORDER BY started_at DESC LIMIT ?";
576
- params.push(limit);
577
- const stmt = this.getStatement(sql);
578
- const rows = stmt.all(...params);
579
- return rows.map((r) => this.mapRunRecord(r));
141
+ return this.runsStore.listRuns(options);
142
+ }
143
+ countRuns(options = {}) {
144
+ return this.runsStore.countRuns(options, this.isClosed);
580
145
  }
581
146
  clearRuns(options = {}) {
582
- let sql = "DELETE FROM runs WHERE package_id = ?";
583
- const params = [this.packageId];
584
- if (options.actionId) {
585
- sql += " AND action_id = ?";
586
- params.push(options.actionId);
587
- }
588
- if (options.status) {
589
- sql += " AND status = ?";
590
- params.push(options.status);
591
- }
592
- if (typeof options.olderThanMs === "number" && options.olderThanMs > 0) {
593
- const cutoff = new Date(this.clock.now().getTime() - options.olderThanMs).toISOString();
594
- sql += " AND started_at < ?";
595
- params.push(cutoff);
596
- }
597
- if (typeof options.keep === "number" && options.keep > 0) {
598
- let subQuery = "SELECT id FROM runs WHERE package_id = ?";
599
- const subParams = [this.packageId];
600
- if (options.actionId) {
601
- subQuery += " AND action_id = ?";
602
- subParams.push(options.actionId);
603
- }
604
- if (options.status) {
605
- subQuery += " AND status = ?";
606
- subParams.push(options.status);
607
- }
608
- subQuery += " ORDER BY started_at DESC LIMIT ?";
609
- subParams.push(options.keep);
610
- sql += ` AND id NOT IN (${subQuery})`;
611
- params.push(...subParams);
612
- }
613
- const stmt = this.getStatement(sql);
614
- const res = stmt.run(...params);
615
- // 运行记录清理后,级联清理已无对应运行的孤立去重索引记录
616
- try {
617
- this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
618
- }
619
- catch (err) {
620
- // 孤立索引清理失败不影响主清理结果的返回语义,但必须可观测
621
- console.warn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
622
- }
623
- return res.changes;
624
- }
625
- /**
626
- * 按保留策略清理过期的终态运行记录与超出容量限制的最旧运行记录。
627
- *
628
- * 包含:
629
- * - 基于时间:删除早于 maxAgeMs 的终态记录(保留 minRetainRuns 保底数量)。
630
- * - 基于数量:若终态记录总数超过 maxRuns,按时间由旧至新淘汰超出部分。
631
- * - 级联清理孤立的幂等去重记录。
632
- */
147
+ return this.runsStore.clearRuns(options);
148
+ }
633
149
  cleanExpiredRuns(policy) {
634
- if (this.isClosed)
635
- return 0;
636
- try {
637
- const resolved = this.resolveRetentionPolicy(policy);
638
- const { maxAgeMs, maxRuns, minRetainRuns } = resolved;
639
- let totalDeleted = 0;
640
- // 1. 基于时间策略清理(仅针对终态记录)
641
- if (maxAgeMs > 0) {
642
- const now = this.clock.now().getTime();
643
- const cutoff = new Date(now - maxAgeMs).toISOString();
644
- if (minRetainRuns > 0) {
645
- const stmt = this.getStatement(`
646
- DELETE FROM runs
647
- WHERE package_id = ?
648
- AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
649
- AND started_at < ?
650
- AND id NOT IN (
651
- SELECT id FROM runs
652
- WHERE package_id = ?
653
- AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
654
- ORDER BY started_at DESC
655
- LIMIT ?
656
- )
657
- `);
658
- const res = stmt.run(this.packageId, cutoff, this.packageId, minRetainRuns);
659
- totalDeleted += res.changes;
660
- }
661
- else {
662
- const stmt = this.getStatement(`
663
- DELETE FROM runs
664
- WHERE package_id = ?
665
- AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
666
- AND started_at < ?
667
- `);
668
- const res = stmt.run(this.packageId, cutoff);
669
- totalDeleted += res.changes;
670
- }
671
- }
672
- // 2. 基于数量策略清理(若当前终态记录数超过 maxRuns,淘汰最旧的记录)
673
- if (maxRuns > 0) {
674
- const stmt = this.getStatement(`
675
- DELETE FROM runs
676
- WHERE package_id = ?
677
- AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
678
- AND id NOT IN (
679
- SELECT id FROM runs
680
- WHERE package_id = ?
681
- AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
682
- ORDER BY started_at DESC
683
- LIMIT ?
684
- )
685
- `);
686
- const res = stmt.run(this.packageId, this.packageId, maxRuns);
687
- totalDeleted += res.changes;
688
- }
689
- // 3. 级联清理无对应运行记录的孤立幂等去重索引
690
- if (totalDeleted > 0) {
691
- try {
692
- this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
693
- }
694
- catch (err) {
695
- console.warn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
696
- }
697
- }
698
- return totalDeleted;
699
- }
700
- catch (err) {
701
- console.warn(`[actiondock] cleanExpiredRuns failed: ${err instanceof Error ? err.message : String(err)}`);
702
- return 0;
703
- }
150
+ return this.runsStore.cleanExpiredRuns(this.isClosed, policy);
704
151
  }
705
- resolveRetentionPolicy(policy) {
706
- let configDays;
707
- let configMaxRuns;
708
- let configMinRetain;
709
- try {
710
- configDays = this.getConfig("runs.retentionDays");
711
- configMaxRuns = this.getConfig("runs.maxRuns");
712
- configMinRetain = this.getConfig("runs.minRetainRuns");
713
- }
714
- catch { }
715
- const envDays = process.env.ACTIONDOCK_RUNS_RETENTION_DAYS
716
- ? parseInt(process.env.ACTIONDOCK_RUNS_RETENTION_DAYS, 10)
717
- : undefined;
718
- const envMaxRuns = process.env.ACTIONDOCK_RUNS_MAX_COUNT
719
- ? parseInt(process.env.ACTIONDOCK_RUNS_MAX_COUNT, 10)
720
- : undefined;
721
- const envMinRetain = process.env.ACTIONDOCK_RUNS_MIN_RETAIN
722
- ? parseInt(process.env.ACTIONDOCK_RUNS_MIN_RETAIN, 10)
723
- : undefined;
724
- const maxAgeMs = policy?.maxAgeMs ??
725
- this.retentionPolicy?.maxAgeMs ??
726
- (typeof configDays === "number" && configDays > 0 ? configDays * 86_400_000 : undefined) ??
727
- (typeof envDays === "number" && !isNaN(envDays) && envDays > 0 ? envDays * 86_400_000 : undefined) ??
728
- DEFAULT_RUNS_RETENTION_MS;
729
- const maxRuns = policy?.maxRuns ??
730
- this.retentionPolicy?.maxRuns ??
731
- (typeof configMaxRuns === "number" && configMaxRuns > 0 ? configMaxRuns : undefined) ??
732
- (typeof envMaxRuns === "number" && !isNaN(envMaxRuns) && envMaxRuns > 0 ? envMaxRuns : undefined) ??
733
- DEFAULT_MAX_RUNS_PER_PACKAGE;
734
- const minRetainRuns = policy?.minRetainRuns ??
735
- this.retentionPolicy?.minRetainRuns ??
736
- (typeof configMinRetain === "number" && configMinRetain >= 0 ? configMinRetain : undefined) ??
737
- (typeof envMinRetain === "number" && !isNaN(envMinRetain) && envMinRetain >= 0 ? envMinRetain : undefined) ??
738
- DEFAULT_MIN_RETAIN_RUNS;
739
- return { maxAgeMs, maxRuns, minRetainRuns };
152
+ touchRunHeartbeat(runIds) {
153
+ return this.runsStore.touchRunHeartbeat(this.isClosed, runIds);
740
154
  }
741
155
  // --- Idempotency 幂等去重管理 ---
742
156
  checkAndRecordIdempotency(record) {
743
- if (this.isClosed) {
744
- throw new Error("SqliteRuntimeStorage is closed");
745
- }
746
- // 事务外预读:同步契约下驱动的事务录制器仅允许写语句,
747
- // 幂等检查所需的读操作全部前移至事务开始之前执行。
748
- // 同步单连接串行执行保证预读与事务提交之间不存在并发窗口。
749
- const checkStmt = this.getStatement(`
750
- SELECT input_digest, run_id FROM idempotency_keys
751
- WHERE owner_id = ? AND action_ref = ? AND request_id = ?
752
- `);
753
- const existing = checkStmt.get(record.ownerId, record.actionRef, record.requestId);
754
- if (existing) {
755
- // 检查关联的运行记录是否仍存在于运行表中
756
- const runStmt = this.getStatement("SELECT id FROM runs WHERE id = ?");
757
- const runExists = runStmt.get(existing.run_id);
758
- if (!runExists) {
759
- // 原运行记录已被清理淘汰,事务内仅执行删除与重登记写操作
760
- this.driver.transaction(() => {
761
- const delStmt = this.getStatement(`
762
- DELETE FROM idempotency_keys
763
- WHERE owner_id = ? AND action_ref = ? AND request_id = ?
764
- `);
765
- delStmt.run(record.ownerId, record.actionRef, record.requestId);
766
- const insertStmt = this.getStatement(`
767
- INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
768
- VALUES (?, ?, ?, ?, ?, ?)
769
- `);
770
- const createdAt = record.createdAt || this.clock.now().toISOString();
771
- insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
772
- });
773
- return { outcome: "new" };
774
- }
775
- if (existing.input_digest !== record.inputDigest) {
776
- return { outcome: "conflict", existingDigest: existing.input_digest };
777
- }
778
- return { outcome: "duplicate", runId: existing.run_id };
779
- }
780
- this.driver.transaction(() => {
781
- const insertStmt = this.getStatement(`
782
- INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
783
- VALUES (?, ?, ?, ?, ?, ?)
784
- `);
785
- const createdAt = record.createdAt || this.clock.now().toISOString();
786
- insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
787
- });
788
- return { outcome: "new" };
157
+ return this.runsStore.checkAndRecordIdempotency(this.isClosed, record);
789
158
  }
790
159
  getIdempotencyRecord(ownerId, actionRef, requestId) {
791
- if (this.isClosed)
792
- return undefined;
793
- const stmt = this.getStatement(`
794
- SELECT owner_id, action_ref, request_id, input_digest, run_id, created_at
795
- FROM idempotency_keys
796
- WHERE owner_id = ? AND action_ref = ? AND request_id = ?
797
- `);
798
- const row = stmt.get(ownerId, actionRef, requestId);
799
- if (!row)
800
- return undefined;
801
- return {
802
- ownerId: row.owner_id,
803
- actionRef: row.action_ref,
804
- requestId: row.request_id,
805
- inputDigest: row.input_digest,
806
- runId: row.run_id,
807
- createdAt: row.created_at,
808
- };
809
- }
810
- mapRunRecord(row) {
811
- // 保留旧有的真值判断:空字符串列视为未存储,与历史行为一致
812
- const input = safeParseStoredJson(row.input_json || undefined, `runs.input db=${this.dbPath} package=${this.packageId} run=${row.id}`);
813
- const output = safeParseStoredJson(row.output_json || undefined, `runs.output db=${this.dbPath} package=${this.packageId} run=${row.id}`);
814
- // error_json 损坏时保留原始文本到 message 字段,避免错误信息丢失
815
- const error = safeParseStoredJson(row.error_json || undefined, `runs.error db=${this.dbPath} package=${this.packageId} run=${row.id}`);
816
- const normalizedError = error !== undefined
817
- ? typeof error === "object" && error !== null && typeof error.message === "string"
818
- ? error
819
- : { code: STORED_ERROR_DECODE_FAILED, message: String(error) }
820
- : undefined;
821
- return {
822
- id: row.id,
823
- rootRunId: row.root_run_id || row.id,
824
- parentRunId: row.parent_run_id || undefined,
825
- packageId: row.package_id,
826
- packageInstanceId: row.package_instance_id || row.package_id,
827
- actionId: row.action_id,
828
- generationId: row.generation_id || "1",
829
- ownerId: row.owner_id || "local",
830
- hostSessionId: row.host_session_id || undefined,
831
- status: row.status,
832
- input,
833
- output,
834
- error,
835
- startedAt: row.started_at,
836
- finishedAt: row.finished_at || undefined,
837
- durationMs: typeof row.duration_ms === "number" ? row.duration_ms : undefined,
838
- };
160
+ return this.runsStore.getIdempotencyRecord(this.isClosed, ownerId, actionRef, requestId);
161
+ }
162
+ listRunsByRequestIds(requestIds) {
163
+ return this.runsStore.listRunsByRequestIds(this.isClosed, requestIds);
164
+ }
165
+ countRunsByRequestIds(requestIds, options = {}) {
166
+ return this.runsStore.countRunsByRequestIds(this.isClosed, requestIds, options);
167
+ }
168
+ getRunRequestIds(runIds) {
169
+ return this.runsStore.getRunRequestIds(this.isClosed, runIds);
170
+ }
171
+ getRunWithRequestId(id) {
172
+ return this.runsStore.getRunWithRequestId(id, this.isClosed);
173
+ }
174
+ // --- Events 审计事件仓储 ---
175
+ appendEvent(eventType, payload) {
176
+ this.eventsStore.appendEvent(eventType, payload);
177
+ }
178
+ queryEvents(eventType) {
179
+ return this.eventsStore.queryEvents(eventType);
839
180
  }
840
181
  async close() {
841
182
  if (this.isClosed)
842
183
  return;
843
184
  this.isClosed = true;
844
- this.statementCache.clear();
185
+ this.configStore.close();
186
+ this.stateStore.close();
187
+ this.runsStore.close();
188
+ this.eventsStore.close();
845
189
  try {
846
190
  this.driver.close();
847
191
  }
848
192
  catch (err) {
849
- // 驱动关闭异常必须可观测,但不再重复抛出以免阻断上层关停链路
850
- console.warn(`[actiondock] storage close failed (db=${this.dbPath}): ${err instanceof Error ? err.message : String(err)}`);
193
+ this.logWarn(`[actiondock] storage close failed (db=${this.dbPath}): ${err instanceof Error ? err.message : String(err)}`);
851
194
  }
852
195
  }
853
196
  }