@actiondock/core 2.8.0 → 2.9.1

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 (53) hide show
  1. package/README.md +21 -1
  2. package/dist/errors.d.ts +5 -1
  3. package/dist/errors.js +2 -0
  4. package/dist/host/host.d.ts +2 -0
  5. package/dist/host/host.js +10 -0
  6. package/dist/host/types.d.ts +3 -0
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.js +3 -1
  9. package/dist/ipc/host.js +1 -0
  10. package/dist/ipc/service.js +3 -0
  11. package/dist/package/runtime.d.ts +3 -0
  12. package/dist/package/runtime.js +8 -0
  13. package/dist/package/types.d.ts +4 -0
  14. package/dist/platform/node.js +1 -0
  15. package/dist/platform/types.d.ts +3 -1
  16. package/dist/profile/client-runs.d.ts +10 -1
  17. package/dist/profile/client-runs.js +14 -1
  18. package/dist/profile/client.d.ts +1 -1
  19. package/dist/profile/client.js +1 -1
  20. package/dist/project/init.js +2 -2
  21. package/dist/project/types.d.ts +5 -0
  22. package/dist/server/index.d.ts +1 -0
  23. package/dist/server/index.js +1 -0
  24. package/dist/server/policy.d.ts +138 -0
  25. package/dist/server/policy.js +368 -0
  26. package/dist/server/routes/actions.js +86 -42
  27. package/dist/server/routes/common.d.ts +14 -25
  28. package/dist/server/routes/common.js +25 -21
  29. package/dist/server/routes/config.d.ts +1 -1
  30. package/dist/server/routes/config.js +8 -8
  31. package/dist/server/routes/doctor.js +4 -4
  32. package/dist/server/routes/health.js +2 -2
  33. package/dist/server/routes/info.js +6 -12
  34. package/dist/server/routes/playbooks.js +8 -8
  35. package/dist/server/routes/runs.js +111 -30
  36. package/dist/server/routes/state.d.ts +1 -1
  37. package/dist/server/routes/state.js +7 -7
  38. package/dist/server/security.d.ts +11 -0
  39. package/dist/server/security.js +36 -19
  40. package/dist/server/server.js +215 -30
  41. package/dist/server/types.d.ts +60 -2
  42. package/dist/service/local.js +3 -0
  43. package/dist/service/remote.js +16 -1
  44. package/dist/service/types.d.ts +4 -0
  45. package/dist/storage/index.d.ts +1 -0
  46. package/dist/storage/index.js +6 -1
  47. package/dist/storage/sqlite.d.ts +18 -1
  48. package/dist/storage/sqlite.js +159 -2
  49. package/dist/storage/types.d.ts +29 -0
  50. package/dist/storage/types.js +12 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +2 -2
@@ -5,7 +5,7 @@ import { SystemClock } from "./clock.js";
5
5
  import { createDefaultSqliteDriver } from "./driver.js";
6
6
  import { safeParseStoredJson } from "./utils.js";
7
7
  import { ActionDockError, AMBIGUOUS_STATE_KEY, STORED_ERROR_DECODE_FAILED, UNSUPPORTED_STORAGE_SCHEMA } from "../errors.js";
8
- import { IDEMPOTENCY_RETENTION_MS, STORAGE_SCHEMA_VERSION, isTerminalRunStatus, } from "./types.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";
9
9
  export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
10
10
  /**
11
11
  * 统一 SQLite 运行时存储实现。
@@ -31,6 +31,11 @@ export class SqliteRuntimeStorage {
31
31
  statementCache = new Map();
32
32
  dbPath;
33
33
  recoverOrphans;
34
+ retentionPolicy;
35
+ lastRunsCleanupAt = 0;
36
+ runInsertCount = 0;
37
+ static RUNS_CLEANUP_INTERVAL_MS = 3600_000;
38
+ static RUNS_CLEANUP_BATCH_THRESHOLD = 50;
34
39
  get isOpen() {
35
40
  return !this.isClosed;
36
41
  }
@@ -43,6 +48,7 @@ export class SqliteRuntimeStorage {
43
48
  const dbPath = options.dbPath || ":memory:";
44
49
  this.dbPath = dbPath;
45
50
  this.recoverOrphans = options.recoverOrphans === true;
51
+ this.retentionPolicy = options.retentionPolicy;
46
52
  if (dbPath !== ":memory:") {
47
53
  const dir = dirname(dbPath);
48
54
  if (!existsSync(dir)) {
@@ -162,10 +168,11 @@ export class SqliteRuntimeStorage {
162
168
  // 忽略非结构化表或兼容驱动异常
163
169
  }
164
170
  }
165
- // 重启恢复:仅持有者身份的打开方在构造阶段收割死亡会话遗留的非终态运行;
171
+ // 重启恢复:仅持有者身份的打开方在构造阶段收割死亡会话遗留的非终态运行并执行历史记录清理;
166
172
  // 旁观查询打开保持只读语义,绝不触碰其他进程的在途记录
167
173
  if (this.recoverOrphans) {
168
174
  this.recoverDeadSessionRuns();
175
+ this.cleanExpiredRuns();
169
176
  }
170
177
  }
171
178
  /**
@@ -504,6 +511,19 @@ export class SqliteRuntimeStorage {
504
511
  const finishedAt = record.finishedAt || null;
505
512
  const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
506
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
+ }
507
527
  }
508
528
  updateRun(id, status, output, error, finishedAt) {
509
529
  if (this.isClosed)
@@ -569,6 +589,27 @@ export class SqliteRuntimeStorage {
569
589
  sql += " AND status = ?";
570
590
  params.push(options.status);
571
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
+ }
572
613
  const stmt = this.getStatement(sql);
573
614
  const res = stmt.run(...params);
574
615
  // 运行记录清理后,级联清理已无对应运行的孤立去重索引记录
@@ -581,6 +622,122 @@ export class SqliteRuntimeStorage {
581
622
  }
582
623
  return res.changes;
583
624
  }
625
+ /**
626
+ * 按保留策略清理过期的终态运行记录与超出容量限制的最旧运行记录。
627
+ *
628
+ * 包含:
629
+ * - 基于时间:删除早于 maxAgeMs 的终态记录(保留 minRetainRuns 保底数量)。
630
+ * - 基于数量:若终态记录总数超过 maxRuns,按时间由旧至新淘汰超出部分。
631
+ * - 级联清理孤立的幂等去重记录。
632
+ */
633
+ 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
+ }
704
+ }
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 };
740
+ }
584
741
  // --- Idempotency 幂等去重管理 ---
585
742
  checkAndRecordIdempotency(record) {
586
743
  if (this.isClosed) {
@@ -9,6 +9,29 @@ export declare const STORAGE_SCHEMA_VERSION = 2;
9
9
  * 中的 idempotencyPolicy.retentionMs 保持单一事实源。
10
10
  */
11
11
  export declare const IDEMPOTENCY_RETENTION_MS = 86400000;
12
+ /**
13
+ * 运行记录默认保留时长常量(14 天)。
14
+ */
15
+ export declare const DEFAULT_RUNS_RETENTION_MS: number;
16
+ /**
17
+ * 运行记录单个包默认最大终态记录保留条数(5000 条)。
18
+ */
19
+ export declare const DEFAULT_MAX_RUNS_PER_PACKAGE = 5000;
20
+ /**
21
+ * 运行记录时间过期清理默认保底保留条数(50 条)。
22
+ */
23
+ export declare const DEFAULT_MIN_RETAIN_RUNS = 50;
24
+ /**
25
+ * 运行记录双重保留策略契约(基于时间与基于数量)。
26
+ */
27
+ export interface RunsRetentionPolicy {
28
+ /** 最大保留时长(毫秒,默认 14 天) */
29
+ maxAgeMs?: number;
30
+ /** 单个包最大保留终态记录数(默认 5000,超出按最旧先淘汰) */
31
+ maxRuns?: number;
32
+ /** 最小保底保留终态记录数(默认 50,防止全清空) */
33
+ minRetainRuns?: number;
34
+ }
12
35
  /**
13
36
  * SQLite 基础参数值类型。
14
37
  */
@@ -95,6 +118,8 @@ export interface StorageOptions {
95
118
  * 避免跨进程互毁在途运行。
96
119
  */
97
120
  recoverOrphans?: boolean;
121
+ /** 运行记录保留策略配置 */
122
+ retentionPolicy?: RunsRetentionPolicy;
98
123
  }
99
124
  /**
100
125
  * 数据库中存储的幂等请求去重记录实体。
@@ -179,7 +204,11 @@ export interface RuntimeStorage {
179
204
  clearRuns(options?: {
180
205
  actionId?: string;
181
206
  status?: string;
207
+ olderThanMs?: number;
208
+ keep?: number;
182
209
  }): number;
210
+ /** 按保留策略清理过期及超额的终态运行记录(基于时间与数量策略) */
211
+ cleanExpiredRuns?(policy?: RunsRetentionPolicy): number;
183
212
  /**
184
213
  * 收敛死亡会话遗留的非终态运行任务(含无会话标识的遗留非终态记录),
185
214
  * 统一收敛为 interrupted。历史别名 recoverRunningRuns 已合并至本方法。
@@ -8,6 +8,18 @@ export const STORAGE_SCHEMA_VERSION = 2;
8
8
  * 中的 idempotencyPolicy.retentionMs 保持单一事实源。
9
9
  */
10
10
  export const IDEMPOTENCY_RETENTION_MS = 86_400_000;
11
+ /**
12
+ * 运行记录默认保留时长常量(14 天)。
13
+ */
14
+ export const DEFAULT_RUNS_RETENTION_MS = 14 * 86_400_000;
15
+ /**
16
+ * 运行记录单个包默认最大终态记录保留条数(5000 条)。
17
+ */
18
+ export const DEFAULT_MAX_RUNS_PER_PACKAGE = 5000;
19
+ /**
20
+ * 运行记录时间过期清理默认保底保留条数(50 条)。
21
+ */
22
+ export const DEFAULT_MIN_RETAIN_RUNS = 50;
11
23
  /**
12
24
  * 判断指定运行状态是否为终态(收敛 success, failed, cancelled, timed_out, interrupted)。
13
25
  */
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * ActionDock 核心版本号单一事实源。
3
3
  */
4
- export declare const ACTIONDOCK_VERSION = "2.8.0";
4
+ export declare const ACTIONDOCK_VERSION = "2.9.1";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * ActionDock 核心版本号单一事实源。
3
3
  */
4
- export const ACTIONDOCK_VERSION = "2.8.0";
4
+ export const ACTIONDOCK_VERSION = "2.9.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/core",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,7 +66,7 @@
66
66
  "test": "node ../../scripts/run-tests.ts"
67
67
  },
68
68
  "dependencies": {
69
- "@actiondock/sdk": "^2.8.0",
69
+ "@actiondock/sdk": "^2.9.1",
70
70
  "ajv": "^8.17.1",
71
71
  "ajv-formats": "^3.0.1",
72
72
  "undici": "^7.29.1"