@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.
- package/README.md +1 -2
- package/dist/catalog/action-catalog.js +3 -33
- package/dist/catalog/discovery.d.ts +0 -3
- package/dist/catalog/discovery.js +4 -4
- package/dist/catalog/graph.d.ts +0 -4
- package/dist/catalog/graph.js +8 -14
- package/dist/{profile/client-actions.d.ts → client/actions.d.ts} +1 -1
- package/dist/{profile/client-actions.js → client/actions.js} +2 -8
- package/dist/{profile/client-config.d.ts → client/config.d.ts} +1 -1
- package/dist/{profile/client-config.js → client/config.js} +1 -1
- package/dist/client/dispatcher.d.ts +4 -0
- package/dist/client/dispatcher.js +21 -0
- package/dist/client/health.d.ts +38 -0
- package/dist/{profile/client-health.js → client/health.js} +9 -6
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.js +20 -0
- package/dist/{profile/client-playbooks.d.ts → client/playbooks.d.ts} +1 -1
- package/dist/{profile/client-playbooks.js → client/playbooks.js} +1 -1
- package/dist/{profile/client-runs.d.ts → client/runs.d.ts} +4 -2
- package/dist/{profile/client-runs.js → client/runs.js} +7 -1
- package/dist/{profile/client-state.d.ts → client/state.d.ts} +1 -1
- package/dist/{profile/client-state.js → client/state.js} +1 -1
- package/dist/{profile/client-transport.d.ts → client/transport.d.ts} +29 -10
- package/dist/{profile/client-transport.js → client/transport.js} +125 -35
- package/dist/doctor/checks/index.d.ts +1 -1
- package/dist/doctor/checks/index.js +1 -1
- package/dist/doctor/checks/project.js +20 -88
- package/dist/doctor/checks/runtime.d.ts +0 -4
- package/dist/doctor/checks/runtime.js +0 -28
- package/dist/doctor/doctor.js +1 -2
- package/dist/errors.d.ts +53 -3
- package/dist/errors.js +53 -3
- package/dist/execution/service.d.ts +55 -37
- package/dist/execution/service.js +601 -212
- package/dist/execution/types.d.ts +24 -5
- package/dist/host/host.d.ts +42 -22
- package/dist/host/host.js +562 -149
- package/dist/host/routing.js +6 -6
- package/dist/host/types.d.ts +25 -13
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/input/advice.d.ts +0 -44
- package/dist/input/advice.js +7 -141
- package/dist/input/flat-materializer.js +1 -1
- package/dist/input/flat-parser.js +1 -1
- package/dist/input/index.d.ts +0 -1
- package/dist/input/index.js +0 -1
- package/dist/input/input-resolver.js +1 -1
- package/dist/input/validation-mapper.d.ts +1 -1
- package/dist/ipc/diagnostic.d.ts +8 -21
- package/dist/ipc/diagnostic.js +24 -57
- package/dist/ipc/host.js +21 -7
- package/dist/ipc/service.d.ts +14 -0
- package/dist/ipc/service.js +132 -26
- package/dist/ipc/types.d.ts +0 -4
- package/dist/package/index.d.ts +4 -5
- package/dist/package/index.js +1 -2
- package/dist/package/runtime.d.ts +94 -23
- package/dist/package/runtime.js +628 -274
- package/dist/package/static-index.d.ts +3 -6
- package/dist/package/static-index.js +11 -40
- package/dist/package/types.d.ts +19 -4
- package/dist/platform/index.d.ts +2 -1
- package/dist/platform/index.js +2 -1
- package/dist/platform/loader-hook.d.ts +32 -0
- package/dist/platform/loader-hook.js +90 -0
- package/dist/{node → platform}/module-loader.d.ts +13 -0
- package/dist/{node → platform}/module-loader.js +51 -4
- package/dist/platform/node.d.ts +19 -8
- package/dist/platform/node.js +50 -20
- package/dist/platform/types.d.ts +7 -47
- package/dist/process/context-process.d.ts +3 -18
- package/dist/process/context-process.js +3 -44
- package/dist/process/control.d.ts +21 -0
- package/dist/process/control.js +34 -0
- package/dist/process/converters.d.ts +23 -0
- package/dist/process/converters.js +92 -0
- package/dist/process/cursor.js +2 -22
- package/dist/process/{diagnostics-sink.d.ts → diagnostics.d.ts} +0 -7
- package/dist/process/{diagnostics-sink.js → diagnostics.js} +6 -8
- package/dist/process/index.d.ts +1 -3
- package/dist/process/index.js +1 -3
- package/dist/process/input-dispatcher.d.ts +4 -13
- package/dist/process/input-dispatcher.js +48 -69
- package/dist/process/managed-record.d.ts +3 -31
- package/dist/process/metadata-store.d.ts +0 -41
- package/dist/process/metadata-store.js +2 -341
- package/dist/process/process-driver.js +2 -2
- package/dist/process/process-manager.d.ts +8 -50
- package/dist/process/process-manager.js +28 -417
- package/dist/process/quotas.d.ts +14 -0
- package/dist/process/quotas.js +29 -0
- package/dist/process/run-executor.js +0 -1
- package/dist/process/terminal-output-cache.d.ts +20 -34
- package/dist/process/terminal-output-cache.js +43 -64
- package/dist/profile/index.d.ts +1 -2
- package/dist/profile/index.js +1 -2
- package/dist/profile/manager.d.ts +2 -4
- package/dist/profile/manager.js +10 -40
- package/dist/profile/types.d.ts +1 -17
- package/dist/project/digest.d.ts +5 -1
- package/dist/project/digest.js +81 -219
- package/dist/project/index.d.ts +4 -5
- package/dist/project/index.js +4 -5
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +3 -3
- package/dist/project/loader.js +72 -50
- package/dist/project/lockfile.js +6 -5
- package/dist/project/manifest.js +5 -4
- package/dist/project/transactions.d.ts +4 -42
- package/dist/project/transactions.js +83 -138
- package/dist/project/types-generator.js +2 -1
- package/dist/project/types.d.ts +2 -0
- package/dist/registry/index.d.ts +0 -1
- package/dist/registry/index.js +0 -1
- package/dist/registry/lock.d.ts +2 -11
- package/dist/registry/lock.js +14 -183
- package/dist/registry/registry.d.ts +1 -6
- package/dist/registry/registry.js +5 -17
- package/dist/runtime/action-registry.d.ts +1 -5
- package/dist/runtime/action-registry.js +0 -12
- package/dist/runtime/context.js +2 -2
- package/dist/runtime/events.d.ts +11 -50
- package/dist/runtime/events.js +88 -368
- package/dist/runtime/index.d.ts +0 -4
- package/dist/runtime/index.js +0 -4
- package/dist/runtime/run-persistence.d.ts +2 -0
- package/dist/runtime/run-persistence.js +2 -1
- package/dist/runtime/standalone-commands.d.ts +18 -0
- package/dist/runtime/standalone-commands.js +466 -0
- package/dist/runtime/standalone-help.d.ts +20 -0
- package/dist/runtime/standalone-help.js +72 -0
- package/dist/runtime/standalone.d.ts +1 -44
- package/dist/runtime/standalone.js +32 -573
- package/dist/schema/validator.d.ts +2 -2
- package/dist/schema/validator.js +2 -9
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +0 -2
- package/dist/server/routes/actions.js +1 -1
- package/dist/server/routes/info.js +1 -1
- package/dist/server/routes/playbooks.js +1 -1
- package/dist/server/routes/runs.js +123 -16
- package/dist/server/routes/state.js +43 -18
- package/dist/server/security.d.ts +1 -1
- package/dist/server/security.js +2 -2
- package/dist/server/server.js +3 -7
- package/dist/service/factory.js +4 -3
- package/dist/service/index.d.ts +1 -1
- package/dist/service/index.js +1 -1
- package/dist/service/remote-errors.d.ts +1 -1
- package/dist/service/remote-errors.js +6 -6
- package/dist/service/remote-polling.d.ts +3 -0
- package/dist/service/remote-polling.js +10 -5
- package/dist/service/remote.d.ts +5 -1
- package/dist/service/remote.js +108 -17
- package/dist/service/sse-stream.js +2 -2
- package/dist/service/types.d.ts +22 -19
- package/dist/service/types.js +1 -24
- package/dist/storage/clock.d.ts +1 -1
- package/dist/storage/clock.js +1 -1
- package/dist/storage/data-dir-lock.d.ts +10 -22
- package/dist/storage/data-dir-lock.js +28 -56
- package/dist/storage/file-lock.d.ts +64 -0
- package/dist/storage/file-lock.js +447 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +5 -4
- package/dist/storage/run-liveness.d.ts +48 -0
- package/dist/storage/run-liveness.js +44 -0
- package/dist/storage/sqlite-config.d.ts +16 -0
- package/dist/storage/sqlite-config.js +59 -0
- package/dist/storage/sqlite-driver.d.ts +3 -1
- package/dist/storage/sqlite-driver.js +14 -4
- package/dist/storage/sqlite-events.d.ts +13 -0
- package/dist/storage/sqlite-events.js +48 -0
- package/dist/storage/sqlite-runs.d.ts +107 -0
- package/dist/storage/sqlite-runs.js +622 -0
- package/dist/storage/sqlite-schema.d.ts +5 -0
- package/dist/storage/sqlite-schema.js +143 -0
- package/dist/storage/sqlite-state.d.ts +33 -0
- package/dist/storage/sqlite-state.js +270 -0
- package/dist/storage/sqlite.d.ts +30 -57
- package/dist/storage/sqlite.js +84 -741
- package/dist/storage/types.d.ts +35 -2
- package/dist/utils/index.d.ts +18 -14
- package/dist/utils/index.js +37 -29
- package/dist/utils/semver.d.ts +18 -0
- package/dist/utils/semver.js +19 -0
- package/dist/utils/warning.d.ts +1 -0
- package/dist/utils/warning.js +52 -0
- package/dist/{json/value-validator.d.ts → value-validator.d.ts} +9 -0
- package/dist/value-validator.js +330 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -12
- package/dist/execution/index.d.ts +0 -2
- package/dist/execution/index.js +0 -2
- package/dist/filter/index.d.ts +0 -1
- package/dist/filter/index.js +0 -1
- package/dist/graph/index.d.ts +0 -4
- package/dist/graph/index.js +0 -4
- package/dist/input/metadata.d.ts +0 -99
- package/dist/input/metadata.js +0 -99
- package/dist/invocation/index.d.ts +0 -2
- package/dist/invocation/index.js +0 -2
- package/dist/ipc/index.d.ts +0 -4
- package/dist/ipc/index.js +0 -4
- package/dist/json/index.d.ts +0 -1
- package/dist/json/index.js +0 -1
- package/dist/json/value-validator.js +0 -330
- package/dist/node/index.d.ts +0 -1
- package/dist/node/index.js +0 -1
- package/dist/platform/node-fs.d.ts +0 -37
- package/dist/platform/node-fs.js +0 -81
- package/dist/process/control-arbiter.d.ts +0 -108
- package/dist/process/control-arbiter.js +0 -250
- package/dist/process/process-executor.d.ts +0 -47
- package/dist/process/process-executor.js +0 -296
- package/dist/profile/client-health.d.ts +0 -14
- package/dist/profile/client-query.d.ts +0 -30
- package/dist/profile/client-query.js +0 -69
- package/dist/profile/client.d.ts +0 -26
- package/dist/profile/client.js +0 -31
- package/dist/project/closure.d.ts +0 -25
- package/dist/project/closure.js +0 -74
- package/dist/registry/store.d.ts +0 -42
- package/dist/registry/store.js +0 -43
- package/dist/runtime/clock.d.ts +0 -7
- package/dist/runtime/clock.js +0 -7
- package/dist/runtime/module-loader.d.ts +0 -2
- package/dist/runtime/module-loader.js +0 -1
- package/dist/runtime/process.d.ts +0 -8
- package/dist/runtime/process.js +0 -1
- package/dist/runtime/runner.d.ts +0 -234
- package/dist/runtime/runner.js +0 -609
- package/dist/schema/index.d.ts +0 -1
- package/dist/schema/index.js +0 -1
- package/dist/server/dispatcher.d.ts +0 -25
- package/dist/server/dispatcher.js +0 -54
- package/dist/service/local.d.ts +0 -27
- package/dist/service/local.js +0 -141
- package/dist/storage/driver.d.ts +0 -10
- package/dist/storage/driver.js +0 -10
- package/dist/storage/lazy.d.ts +0 -8
- package/dist/storage/lazy.js +0 -45
- package/dist/storage/lock-core.d.ts +0 -158
- package/dist/storage/lock-core.js +0 -872
- package/dist/utils/net.d.ts +0 -10
- package/dist/utils/net.js +0 -16
- package/dist/utils/process.d.ts +0 -10
- package/dist/utils/process.js +0 -21
- /package/dist/{filter → utils}/intent.d.ts +0 -0
- /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
|
+
}
|