@actiondock/core 2.5.0 → 2.6.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/dist/app/app.d.ts +14 -1
- package/dist/app/app.js +88 -215
- package/dist/app/static-index.d.ts +39 -0
- package/dist/app/static-index.js +164 -0
- package/dist/app/types.d.ts +5 -0
- package/dist/catalog/action-index.js +6 -4
- package/dist/catalog/package-catalog.js +2 -1
- package/dist/doctor/checks/index.d.ts +5 -0
- package/dist/doctor/checks/index.js +5 -0
- package/dist/doctor/checks/project-locate.d.ts +6 -0
- package/dist/doctor/checks/project-locate.js +36 -0
- package/dist/doctor/checks/project.d.ts +6 -0
- package/dist/doctor/checks/project.js +383 -0
- package/dist/doctor/checks/registry.d.ts +13 -0
- package/dist/doctor/checks/registry.js +207 -0
- package/dist/doctor/checks/runtime.d.ts +13 -0
- package/dist/doctor/checks/runtime.js +90 -0
- package/dist/doctor/checks/storage.d.ts +5 -0
- package/dist/doctor/checks/storage.js +34 -0
- package/dist/doctor/context.d.ts +35 -0
- package/dist/doctor/context.js +11 -0
- package/dist/doctor/doctor.d.ts +5 -0
- package/dist/doctor/doctor.js +33 -602
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/execution/service.js +14 -9
- package/dist/host/host.d.ts +33 -0
- package/dist/host/host.js +222 -362
- package/dist/host/routing.d.ts +92 -0
- package/dist/host/routing.js +238 -0
- package/dist/host/types.d.ts +8 -0
- package/dist/ipc/host.js +38 -0
- package/dist/platform/types.d.ts +7 -0
- package/dist/process/control-arbiter.d.ts +107 -0
- package/dist/process/control-arbiter.js +251 -0
- package/dist/process/diagnostics-sink.d.ts +26 -0
- package/dist/process/diagnostics-sink.js +35 -0
- package/dist/process/index.d.ts +7 -0
- package/dist/process/index.js +7 -0
- package/dist/process/input-dispatcher.d.ts +88 -0
- package/dist/process/input-dispatcher.js +240 -0
- package/dist/process/managed-record.d.ts +78 -0
- package/dist/process/managed-record.js +1 -0
- package/dist/process/process-manager.d.ts +15 -56
- package/dist/process/process-manager.js +119 -707
- package/dist/process/reservation-table.d.ts +39 -0
- package/dist/process/reservation-table.js +80 -0
- package/dist/process/run-executor.d.ts +16 -0
- package/dist/process/run-executor.js +155 -0
- package/dist/process/terminal-output-cache.d.ts +66 -0
- package/dist/process/terminal-output-cache.js +132 -0
- package/dist/profile/client-actions.d.ts +56 -0
- package/dist/profile/client-actions.js +215 -0
- package/dist/profile/client-config.d.ts +20 -0
- package/dist/profile/client-config.js +51 -0
- package/dist/profile/client-health.d.ts +14 -0
- package/dist/profile/client-health.js +68 -0
- package/dist/profile/client-playbooks.d.ts +19 -0
- package/dist/profile/client-playbooks.js +36 -0
- package/dist/profile/client-query.d.ts +30 -0
- package/dist/profile/client-query.js +70 -0
- package/dist/profile/client-runs.d.ts +42 -0
- package/dist/profile/client-runs.js +58 -0
- package/dist/profile/client-state.d.ts +56 -0
- package/dist/profile/client-state.js +83 -0
- package/dist/profile/client-transport.d.ts +61 -0
- package/dist/profile/client-transport.js +155 -0
- package/dist/profile/client.d.ts +23 -142
- package/dist/profile/client.js +28 -588
- package/dist/profile/manager.js +1 -1
- package/dist/project/closure.d.ts +4 -4
- package/dist/project/closure.js +11 -5
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +0 -25
- package/dist/project/loader.js +1 -154
- package/dist/project/transactions.d.ts +12 -14
- package/dist/project/transactions.js +60 -836
- package/dist/registry/registry.js +6 -4
- package/dist/registry/resolve.js +1 -1
- package/dist/runtime/action-registry.d.ts +45 -0
- package/dist/runtime/action-registry.js +108 -0
- package/dist/runtime/clock.d.ts +5 -17
- package/dist/runtime/clock.js +5 -15
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.js +17 -56
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/package-runner-factory.d.ts +82 -0
- package/dist/runtime/package-runner-factory.js +186 -0
- package/dist/runtime/run-persistence.d.ts +85 -0
- package/dist/runtime/run-persistence.js +111 -0
- package/dist/runtime/runner.d.ts +23 -33
- package/dist/runtime/runner.js +92 -286
- package/dist/runtime/standalone.d.ts +11 -1
- package/dist/runtime/standalone.js +137 -25
- package/dist/server/dispatcher.d.ts +8 -3
- package/dist/server/dispatcher.js +16 -40
- package/dist/server/routes/runs.js +6 -4
- package/dist/server/security.d.ts +1 -7
- package/dist/server/security.js +2 -13
- package/dist/storage/clock.d.ts +25 -0
- package/dist/storage/clock.js +23 -0
- package/dist/storage/data-dir-lock.d.ts +3 -36
- package/dist/storage/data-dir-lock.js +48 -881
- package/dist/storage/index.d.ts +8 -1
- package/dist/storage/index.js +8 -2
- package/dist/storage/lazy.js +1 -0
- package/dist/storage/lock-core.d.ts +162 -0
- package/dist/storage/lock-core.js +885 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +81 -89
- package/dist/storage/types.d.ts +27 -5
- package/dist/storage/types.js +21 -0
- package/dist/storage/utils.d.ts +22 -0
- package/dist/storage/utils.js +34 -0
- package/dist/target/local.d.ts +1 -0
- package/dist/target/local.js +23 -17
- package/dist/target/remote-errors.d.ts +21 -0
- package/dist/target/remote-errors.js +51 -0
- package/dist/target/remote-polling.d.ts +35 -0
- package/dist/target/remote-polling.js +104 -0
- package/dist/target/remote.d.ts +4 -0
- package/dist/target/remote.js +108 -301
- package/dist/target/sse-parser.d.ts +35 -0
- package/dist/target/sse-parser.js +163 -0
- package/dist/target/target.js +2 -0
- package/dist/target/types.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/net.d.ts +10 -0
- package/dist/utils/net.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/storage/sqlite.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegmen
|
|
|
4
4
|
/**
|
|
5
5
|
* 统一 SQLite 运行时存储实现。
|
|
6
6
|
* 通过 SqliteDriver 抽象驱动,解耦底层具体运行时引擎。
|
|
7
|
+
*
|
|
8
|
+
* 跨进程并发模型:
|
|
9
|
+
* - 同一数据目录下的 runtime.db 允许持有者进程(serve、mcp、ad run 等执行宿主)
|
|
10
|
+
* 与旁观查询进程(ad state、ad runs、ad config 等只读命令)并发打开。
|
|
11
|
+
* - 死亡会话遗留非终态运行记录的收割(recoverDeadSessionRuns)仅由显式声明
|
|
12
|
+
* 持有者身份的打开方执行:StorageOptions.recoverOrphans 为 true 时构造阶段收割,
|
|
13
|
+
* 宿主接管路径亦可显式调用 recoverDeadSessionRuns 收敛死亡会话记录。
|
|
14
|
+
* - 旁观打开(默认 recoverOrphans 为 false)保持只读语义,绝不收敛其他进程的在途
|
|
15
|
+
* running 记录,防止查询进程把执行宿主的在途运行误收割为 interrupted,导致持有者
|
|
16
|
+
* 侧终态结算静默丢失。
|
|
17
|
+
* - updateRun 以 WHERE id = ? AND status = 'running' 条件更新;若命中零行且新状态
|
|
18
|
+
* 为终态,说明记录已被其他持有者或恢复流程改写,此处输出告警保证状态漂移可观测。
|
|
7
19
|
*/
|
|
8
20
|
export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
9
21
|
private driver;
|
|
@@ -12,6 +24,7 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
12
24
|
private isClosed;
|
|
13
25
|
private statementCache;
|
|
14
26
|
private dbPath;
|
|
27
|
+
private recoverOrphans;
|
|
15
28
|
get isOpen(): boolean;
|
|
16
29
|
get closed(): boolean;
|
|
17
30
|
constructor(options: StorageOptions);
|
|
@@ -24,11 +37,6 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
24
37
|
* - 打开旧版本或不兼容版本的数据库时,在写事务前直接抛出 UNSUPPORTED_STORAGE_SCHEMA 异常并拒绝启动。
|
|
25
38
|
*/
|
|
26
39
|
private init;
|
|
27
|
-
/**
|
|
28
|
-
* 重启恢复:将未正常结算的非终态(running/pending)记录自动收敛为 interrupted。
|
|
29
|
-
* 若指定了当前新 Host 会话标识,将收敛不属于该会话(包括旧会话或空会话)的死亡任务。
|
|
30
|
-
*/
|
|
31
|
-
recoverRunningRuns(currentHostSessionId?: string): number;
|
|
32
40
|
/**
|
|
33
41
|
* 收敛死亡会话遗留的非终态运行任务。
|
|
34
42
|
*/
|
|
@@ -48,7 +56,11 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
48
56
|
deleteState(namespace: string, key: string): Promise<boolean>;
|
|
49
57
|
deleteStateSmart(targetKey: string, namespace?: string): Promise<boolean>;
|
|
50
58
|
/**
|
|
51
|
-
*
|
|
59
|
+
* 清理已过期的状态记录。
|
|
60
|
+
*
|
|
61
|
+
* 同时按 TargetInfo.idempotencyPolicy 声明的 24 小时保留窗口,
|
|
62
|
+
* 清理 idempotency_keys 表中超过保留期的幂等去重记录,
|
|
63
|
+
* 保证承诺的 retentionMs 策略与实际清理逻辑一致。
|
|
52
64
|
*/
|
|
53
65
|
cleanExpiredState(): Promise<number>;
|
|
54
66
|
clearState(options?: {
|
|
@@ -66,6 +78,7 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
66
78
|
getRun(id: string): RunRecord | null;
|
|
67
79
|
listRuns(options?: {
|
|
68
80
|
actionId?: string;
|
|
81
|
+
status?: string;
|
|
69
82
|
limit?: number;
|
|
70
83
|
}): RunRecord[];
|
|
71
84
|
clearRuns(options?: {
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
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
|
-
import { SystemClock } from "
|
|
4
|
+
import { SystemClock } from "./clock.js";
|
|
5
5
|
import { createDefaultSqliteDriver } from "./driver.js";
|
|
6
|
-
import {
|
|
6
|
+
import { safeParseStoredJson } from "./utils.js";
|
|
7
|
+
import { IDEMPOTENCY_RETENTION_MS, STORAGE_SCHEMA_VERSION, isTerminalRunStatus, } from "./types.js";
|
|
7
8
|
export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
|
|
8
9
|
/**
|
|
9
10
|
* 统一 SQLite 运行时存储实现。
|
|
10
11
|
* 通过 SqliteDriver 抽象驱动,解耦底层具体运行时引擎。
|
|
12
|
+
*
|
|
13
|
+
* 跨进程并发模型:
|
|
14
|
+
* - 同一数据目录下的 runtime.db 允许持有者进程(serve、mcp、ad run 等执行宿主)
|
|
15
|
+
* 与旁观查询进程(ad state、ad runs、ad config 等只读命令)并发打开。
|
|
16
|
+
* - 死亡会话遗留非终态运行记录的收割(recoverDeadSessionRuns)仅由显式声明
|
|
17
|
+
* 持有者身份的打开方执行:StorageOptions.recoverOrphans 为 true 时构造阶段收割,
|
|
18
|
+
* 宿主接管路径亦可显式调用 recoverDeadSessionRuns 收敛死亡会话记录。
|
|
19
|
+
* - 旁观打开(默认 recoverOrphans 为 false)保持只读语义,绝不收敛其他进程的在途
|
|
20
|
+
* running 记录,防止查询进程把执行宿主的在途运行误收割为 interrupted,导致持有者
|
|
21
|
+
* 侧终态结算静默丢失。
|
|
22
|
+
* - updateRun 以 WHERE id = ? AND status = 'running' 条件更新;若命中零行且新状态
|
|
23
|
+
* 为终态,说明记录已被其他持有者或恢复流程改写,此处输出告警保证状态漂移可观测。
|
|
11
24
|
*/
|
|
12
25
|
export class SqliteRuntimeStorage {
|
|
13
26
|
driver;
|
|
@@ -16,6 +29,7 @@ export class SqliteRuntimeStorage {
|
|
|
16
29
|
isClosed = false;
|
|
17
30
|
statementCache = new Map();
|
|
18
31
|
dbPath;
|
|
32
|
+
recoverOrphans;
|
|
19
33
|
get isOpen() {
|
|
20
34
|
return !this.isClosed;
|
|
21
35
|
}
|
|
@@ -27,6 +41,7 @@ export class SqliteRuntimeStorage {
|
|
|
27
41
|
this.clock = options.clock ?? new SystemClock();
|
|
28
42
|
const dbPath = options.dbPath || ":memory:";
|
|
29
43
|
this.dbPath = dbPath;
|
|
44
|
+
this.recoverOrphans = options.recoverOrphans === true;
|
|
30
45
|
if (dbPath !== ":memory:") {
|
|
31
46
|
const dir = dirname(dbPath);
|
|
32
47
|
if (!existsSync(dir)) {
|
|
@@ -142,15 +157,11 @@ export class SqliteRuntimeStorage {
|
|
|
142
157
|
// 忽略非结构化表或兼容驱动异常
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* 若指定了当前新 Host 会话标识,将收敛不属于该会话(包括旧会话或空会话)的死亡任务。
|
|
151
|
-
*/
|
|
152
|
-
recoverRunningRuns(currentHostSessionId) {
|
|
153
|
-
return this.recoverDeadSessionRuns(currentHostSessionId);
|
|
160
|
+
// 重启恢复:仅持有者身份的打开方在构造阶段收割死亡会话遗留的非终态运行;
|
|
161
|
+
// 旁观查询打开保持只读语义,绝不触碰其他进程的在途记录
|
|
162
|
+
if (this.recoverOrphans) {
|
|
163
|
+
this.recoverDeadSessionRuns();
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
/**
|
|
156
167
|
* 收敛死亡会话遗留的非终态运行任务。
|
|
@@ -176,7 +187,8 @@ export class SqliteRuntimeStorage {
|
|
|
176
187
|
const res = stmt.run(...params);
|
|
177
188
|
return res.changes;
|
|
178
189
|
}
|
|
179
|
-
catch {
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.warn(`[actiondock] recoverDeadSessionRuns failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
180
192
|
return 0;
|
|
181
193
|
}
|
|
182
194
|
}
|
|
@@ -198,24 +210,14 @@ export class SqliteRuntimeStorage {
|
|
|
198
210
|
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
199
211
|
return undefined;
|
|
200
212
|
}
|
|
201
|
-
|
|
202
|
-
return JSON.parse(row.value_json);
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return row.value_json;
|
|
206
|
-
}
|
|
213
|
+
return safeParseStoredJson(row.value_json, `config db=${this.dbPath} package=${this.packageId} key=${key}`);
|
|
207
214
|
}
|
|
208
215
|
listConfig() {
|
|
209
216
|
const stmt = this.getStatement("SELECT key, value_json FROM config WHERE package_id = ?");
|
|
210
217
|
const rows = stmt.all(this.packageId);
|
|
211
218
|
const result = {};
|
|
212
219
|
for (const row of rows) {
|
|
213
|
-
|
|
214
|
-
result[row.key] = JSON.parse(row.value_json);
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
result[row.key] = row.value_json;
|
|
218
|
-
}
|
|
220
|
+
result[row.key] = safeParseStoredJson(row.value_json, `config db=${this.dbPath} package=${this.packageId} key=${row.key}`);
|
|
219
221
|
}
|
|
220
222
|
return result;
|
|
221
223
|
}
|
|
@@ -250,12 +252,7 @@ export class SqliteRuntimeStorage {
|
|
|
250
252
|
return undefined;
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
|
-
|
|
254
|
-
return JSON.parse(row.value_json);
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return row.value_json;
|
|
258
|
-
}
|
|
255
|
+
return safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${namespace} key=${key}`);
|
|
259
256
|
}
|
|
260
257
|
async findMatchingStateRows(targetKey) {
|
|
261
258
|
let decoded;
|
|
@@ -311,13 +308,7 @@ export class SqliteRuntimeStorage {
|
|
|
311
308
|
return undefined;
|
|
312
309
|
}
|
|
313
310
|
const row = matchingRows[0];
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
parsedVal = JSON.parse(row.value_json);
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
parsedVal = row.value_json;
|
|
320
|
-
}
|
|
311
|
+
const parsedVal = safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${row.namespace} key=${row.key}`);
|
|
321
312
|
return {
|
|
322
313
|
packageId: this.packageId,
|
|
323
314
|
namespace: row.namespace,
|
|
@@ -365,18 +356,32 @@ export class SqliteRuntimeStorage {
|
|
|
365
356
|
return this.deleteState("", targetKey);
|
|
366
357
|
}
|
|
367
358
|
/**
|
|
368
|
-
*
|
|
359
|
+
* 清理已过期的状态记录。
|
|
360
|
+
*
|
|
361
|
+
* 同时按 TargetInfo.idempotencyPolicy 声明的 24 小时保留窗口,
|
|
362
|
+
* 清理 idempotency_keys 表中超过保留期的幂等去重记录,
|
|
363
|
+
* 保证承诺的 retentionMs 策略与实际清理逻辑一致。
|
|
369
364
|
*/
|
|
370
365
|
async cleanExpiredState() {
|
|
371
366
|
if (this.isClosed)
|
|
372
367
|
return 0;
|
|
373
368
|
try {
|
|
374
|
-
const now = this.clock.now()
|
|
369
|
+
const now = this.clock.now();
|
|
375
370
|
const stmt = this.getStatement("DELETE FROM state WHERE package_id = ? AND expires_at IS NOT NULL AND expires_at <= ?");
|
|
376
|
-
const res = stmt.run(this.packageId, now);
|
|
371
|
+
const res = stmt.run(this.packageId, now.toISOString());
|
|
372
|
+
// 幂等去重记录保留窗口与 info() 声明的 retentionMs(24 小时)保持一致
|
|
373
|
+
const retentionCutoff = new Date(now.getTime() - IDEMPOTENCY_RETENTION_MS).toISOString();
|
|
374
|
+
try {
|
|
375
|
+
this.getStatement("DELETE FROM idempotency_keys WHERE created_at < ?").run(retentionCutoff);
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
// 独立记录幂等清理失败,不影响状态清理主路径的返回语义
|
|
379
|
+
console.warn(`[actiondock] idempotency retention cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
380
|
+
}
|
|
377
381
|
return res.changes;
|
|
378
382
|
}
|
|
379
|
-
catch {
|
|
383
|
+
catch (err) {
|
|
384
|
+
console.warn(`[actiondock] cleanExpiredState failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
380
385
|
return 0;
|
|
381
386
|
}
|
|
382
387
|
}
|
|
@@ -449,13 +454,7 @@ export class SqliteRuntimeStorage {
|
|
|
449
454
|
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
450
455
|
continue;
|
|
451
456
|
}
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
parsedVal = JSON.parse(row.value_json);
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
parsedVal = row.value_json;
|
|
458
|
-
}
|
|
457
|
+
const parsedVal = safeParseStoredJson(row.value_json, `state db=${this.dbPath} package=${this.packageId} namespace=${row.namespace} key=${row.key}`);
|
|
459
458
|
results.push({
|
|
460
459
|
packageId: this.packageId,
|
|
461
460
|
namespace: row.namespace,
|
|
@@ -505,7 +504,12 @@ export class SqliteRuntimeStorage {
|
|
|
505
504
|
END
|
|
506
505
|
WHERE id = ? AND status = 'running'
|
|
507
506
|
`);
|
|
508
|
-
stmt.run(status, output !== undefined ? JSON.stringify(output) : null, error ? JSON.stringify(error) : null, finishIso, finishIso, id);
|
|
507
|
+
const res = stmt.run(status, output !== undefined ? JSON.stringify(output) : null, error ? JSON.stringify(error) : null, finishIso, finishIso, id);
|
|
508
|
+
// 命中零行且新状态为终态:说明该记录已不再处于 running 状态(被其他持有者进程或
|
|
509
|
+
// 恢复流程改写),本次终态写入被丢弃;保持返回值语义不变,仅输出告警保证漂移可观测
|
|
510
|
+
if (res.changes === 0 && isTerminalRunStatus(status)) {
|
|
511
|
+
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)`);
|
|
512
|
+
}
|
|
509
513
|
}
|
|
510
514
|
catch (err) {
|
|
511
515
|
if (this.isClosed)
|
|
@@ -523,25 +527,20 @@ export class SqliteRuntimeStorage {
|
|
|
523
527
|
}
|
|
524
528
|
listRuns(options = {}) {
|
|
525
529
|
const limit = options.limit || 50;
|
|
526
|
-
let
|
|
530
|
+
let sql = "SELECT * FROM runs WHERE package_id = ?";
|
|
531
|
+
const params = [this.packageId];
|
|
527
532
|
if (options.actionId) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
WHERE package_id = ? AND action_id = ?
|
|
531
|
-
ORDER BY started_at DESC
|
|
532
|
-
LIMIT ?
|
|
533
|
-
`);
|
|
534
|
-
rows = stmt.all(this.packageId, options.actionId, limit);
|
|
533
|
+
sql += " AND action_id = ?";
|
|
534
|
+
params.push(options.actionId);
|
|
535
535
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
WHERE package_id = ?
|
|
540
|
-
ORDER BY started_at DESC
|
|
541
|
-
LIMIT ?
|
|
542
|
-
`);
|
|
543
|
-
rows = stmt.all(this.packageId, limit);
|
|
536
|
+
if (options.status) {
|
|
537
|
+
sql += " AND status = ?";
|
|
538
|
+
params.push(options.status);
|
|
544
539
|
}
|
|
540
|
+
sql += " ORDER BY started_at DESC LIMIT ?";
|
|
541
|
+
params.push(limit);
|
|
542
|
+
const stmt = this.getStatement(sql);
|
|
543
|
+
const rows = stmt.all(...params);
|
|
545
544
|
return rows.map((r) => this.mapRunRecord(r));
|
|
546
545
|
}
|
|
547
546
|
clearRuns(options = {}) {
|
|
@@ -561,7 +560,10 @@ export class SqliteRuntimeStorage {
|
|
|
561
560
|
try {
|
|
562
561
|
this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
|
|
563
562
|
}
|
|
564
|
-
catch {
|
|
563
|
+
catch (err) {
|
|
564
|
+
// 孤立索引清理失败不影响主清理结果的返回语义,但必须可观测
|
|
565
|
+
console.warn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
566
|
+
}
|
|
565
567
|
return res.changes;
|
|
566
568
|
}
|
|
567
569
|
// --- Idempotency 幂等去重管理 ---
|
|
@@ -634,27 +636,16 @@ export class SqliteRuntimeStorage {
|
|
|
634
636
|
};
|
|
635
637
|
}
|
|
636
638
|
mapRunRecord(row) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
output = row.output_json ? JSON.parse(row.output_json) : undefined;
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
output = row.output_json;
|
|
651
|
-
}
|
|
652
|
-
try {
|
|
653
|
-
error = row.error_json ? JSON.parse(row.error_json) : undefined;
|
|
654
|
-
}
|
|
655
|
-
catch {
|
|
656
|
-
error = undefined;
|
|
657
|
-
}
|
|
639
|
+
// 保留旧有的真值判断:空字符串列视为未存储,与历史行为一致
|
|
640
|
+
const input = safeParseStoredJson(row.input_json || undefined, `runs.input db=${this.dbPath} package=${this.packageId} run=${row.id}`);
|
|
641
|
+
const output = safeParseStoredJson(row.output_json || undefined, `runs.output db=${this.dbPath} package=${this.packageId} run=${row.id}`);
|
|
642
|
+
// error_json 损坏时保留原始文本到 message 字段,避免错误信息丢失
|
|
643
|
+
const error = safeParseStoredJson(row.error_json || undefined, `runs.error db=${this.dbPath} package=${this.packageId} run=${row.id}`);
|
|
644
|
+
const normalizedError = error !== undefined
|
|
645
|
+
? typeof error === "object" && error !== null && typeof error.message === "string"
|
|
646
|
+
? error
|
|
647
|
+
: { code: "STORED_ERROR_DECODE_FAILED", message: String(error) }
|
|
648
|
+
: undefined;
|
|
658
649
|
return {
|
|
659
650
|
id: row.id,
|
|
660
651
|
rootRunId: row.root_run_id || row.id,
|
|
@@ -682,8 +673,9 @@ export class SqliteRuntimeStorage {
|
|
|
682
673
|
try {
|
|
683
674
|
this.driver.close();
|
|
684
675
|
}
|
|
685
|
-
catch {
|
|
686
|
-
//
|
|
676
|
+
catch (err) {
|
|
677
|
+
// 驱动关闭异常必须可观测,但不再重复抛出以免阻断上层关停链路
|
|
678
|
+
console.warn(`[actiondock] storage close failed (db=${this.dbPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
687
679
|
}
|
|
688
680
|
}
|
|
689
681
|
}
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { RuntimeError, RunRecord } from "@actiondock/sdk";
|
|
2
|
-
import type { Clock } from "
|
|
1
|
+
import type { RuntimeError, RunRecord, RunStatus } from "@actiondock/sdk";
|
|
2
|
+
import type { Clock } from "./clock.js";
|
|
3
3
|
/**
|
|
4
4
|
* 固定的存储 Schema 目标版本常量。
|
|
5
5
|
*/
|
|
6
6
|
export declare const STORAGE_SCHEMA_VERSION = 2;
|
|
7
|
+
/**
|
|
8
|
+
* 幂等去重记录保留窗口(24 小时),与 LocalActionDockTarget.info()
|
|
9
|
+
* 声明的 idempotencyPolicy.retentionMs 承诺保持单一事实源。
|
|
10
|
+
*/
|
|
11
|
+
export declare const IDEMPOTENCY_RETENTION_MS = 86400000;
|
|
7
12
|
/**
|
|
8
13
|
* SQLite 基础参数值类型。
|
|
9
14
|
*/
|
|
@@ -82,6 +87,14 @@ export interface StorageOptions {
|
|
|
82
87
|
driver?: SqliteDriver;
|
|
83
88
|
/** 可选注入的时间提供器,便于与模拟时钟联动 */
|
|
84
89
|
clock?: Clock;
|
|
90
|
+
/**
|
|
91
|
+
* 是否在打开时收割死亡会话遗留的非终态运行记录(收敛为 interrupted)。
|
|
92
|
+
*
|
|
93
|
+
* 默认 false:旁观查询打开(CLI 的 state/runs/config 类命令)不收割其他进程的在途记录。
|
|
94
|
+
* 仅数据目录持有者(serve、mcp、ad run 等执行宿主)显式置 true,
|
|
95
|
+
* 避免跨进程互毁在途运行。
|
|
96
|
+
*/
|
|
97
|
+
recoverOrphans?: boolean;
|
|
85
98
|
}
|
|
86
99
|
/**
|
|
87
100
|
* 数据库中存储的幂等请求去重记录实体。
|
|
@@ -120,6 +133,13 @@ export type TerminalRunStatus = "success" | "failed" | "cancelled" | "timed_out"
|
|
|
120
133
|
* 判断指定运行状态是否为终态(收敛 success, failed, cancelled, timed_out, interrupted)。
|
|
121
134
|
*/
|
|
122
135
|
export declare function isTerminalRunStatus(status: string): status is TerminalRunStatus;
|
|
136
|
+
/**
|
|
137
|
+
* 执行终态到运行记录状态的统一映射单一事实源。
|
|
138
|
+
*
|
|
139
|
+
* 成功映射为 success;超时错误码映射为 timed_out;取消错误码映射为 cancelled;
|
|
140
|
+
* 其余失败一律映射为 failed。错误码常量来自全仓统一错误码单一事实源 errors.ts。
|
|
141
|
+
*/
|
|
142
|
+
export declare function resultStatusToRunStatus(ok: boolean, errorCode?: string): RunStatus;
|
|
123
143
|
/**
|
|
124
144
|
* 统一运行时存储抽象接口。
|
|
125
145
|
*/
|
|
@@ -153,15 +173,17 @@ export interface RuntimeStorage {
|
|
|
153
173
|
getRun(id: string): RunRecord | null;
|
|
154
174
|
listRuns(options?: {
|
|
155
175
|
actionId?: string;
|
|
176
|
+
status?: string;
|
|
156
177
|
limit?: number;
|
|
157
178
|
}): RunRecord[];
|
|
158
179
|
clearRuns(options?: {
|
|
159
180
|
actionId?: string;
|
|
160
181
|
status?: string;
|
|
161
182
|
}): number;
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
/**
|
|
184
|
+
* 收敛死亡会话遗留的非终态运行任务(含无会话标识的遗留非终态记录),
|
|
185
|
+
* 统一收敛为 interrupted。历史别名 recoverRunningRuns 已合并至本方法。
|
|
186
|
+
*/
|
|
165
187
|
recoverDeadSessionRuns?(currentHostSessionId?: string): number | Promise<number>;
|
|
166
188
|
/** 确保底层存储与 Schema 初始化完成 */
|
|
167
189
|
ensureInitialized?(): Promise<void>;
|
package/dist/storage/types.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { ACTION_CANCELLED, ACTION_TIMEOUT } from "../errors.js";
|
|
1
2
|
/**
|
|
2
3
|
* 固定的存储 Schema 目标版本常量。
|
|
3
4
|
*/
|
|
4
5
|
export const STORAGE_SCHEMA_VERSION = 2;
|
|
6
|
+
/**
|
|
7
|
+
* 幂等去重记录保留窗口(24 小时),与 LocalActionDockTarget.info()
|
|
8
|
+
* 声明的 idempotencyPolicy.retentionMs 承诺保持单一事实源。
|
|
9
|
+
*/
|
|
10
|
+
export const IDEMPOTENCY_RETENTION_MS = 86_400_000;
|
|
5
11
|
/**
|
|
6
12
|
* 判断指定运行状态是否为终态(收敛 success, failed, cancelled, timed_out, interrupted)。
|
|
7
13
|
*/
|
|
@@ -12,3 +18,18 @@ export function isTerminalRunStatus(status) {
|
|
|
12
18
|
status === "timed_out" ||
|
|
13
19
|
status === "interrupted");
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 执行终态到运行记录状态的统一映射单一事实源。
|
|
23
|
+
*
|
|
24
|
+
* 成功映射为 success;超时错误码映射为 timed_out;取消错误码映射为 cancelled;
|
|
25
|
+
* 其余失败一律映射为 failed。错误码常量来自全仓统一错误码单一事实源 errors.ts。
|
|
26
|
+
*/
|
|
27
|
+
export function resultStatusToRunStatus(ok, errorCode) {
|
|
28
|
+
if (ok)
|
|
29
|
+
return "success";
|
|
30
|
+
if (errorCode === ACTION_TIMEOUT)
|
|
31
|
+
return "timed_out";
|
|
32
|
+
if (errorCode === ACTION_CANCELLED)
|
|
33
|
+
return "cancelled";
|
|
34
|
+
return "failed";
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储层 JSON 载荷统一安全解析工具。
|
|
3
|
+
*
|
|
4
|
+
* 单一事实源:所有从 SQLite 读出的 JSON 字段(config.value_json、
|
|
5
|
+
* state.value_json、runs.input_json / output_json / error_json 等)
|
|
6
|
+
* 禁止在各自调用点散落 try/catch 静默降级,统一经由本函数完成
|
|
7
|
+
* 「解析 + 可观测告警 + 保守降级」的完整链路。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 解析存储层 JSON 字符串,失败时输出单行警告并保守返回原始字符串。
|
|
11
|
+
*
|
|
12
|
+
* 降级策略说明:各调用方的返回类型为业务泛型 T,无法以标记对象区分
|
|
13
|
+
* 损坏数据,因此保持「返回原始字符串」的既有降级行为,但通过
|
|
14
|
+
* console.warn 保证损坏事件可观测,不再无声吞没。警告仅携带
|
|
15
|
+
* 上下文描述与解析失败原因,不输出完整原始值,避免敏感数据
|
|
16
|
+
* 泄漏到日志。
|
|
17
|
+
*
|
|
18
|
+
* @param raw 存储层原始 JSON 字符串
|
|
19
|
+
* @param context 存储路径上下文描述(包含数据库位置、包标识与键名)
|
|
20
|
+
* @returns 解析成功返回解析后的值;入参为空返回 undefined;解析失败返回原始字符串
|
|
21
|
+
*/
|
|
22
|
+
export declare function safeParseStoredJson<T = unknown>(raw: string | null | undefined, context: string): T | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储层 JSON 载荷统一安全解析工具。
|
|
3
|
+
*
|
|
4
|
+
* 单一事实源:所有从 SQLite 读出的 JSON 字段(config.value_json、
|
|
5
|
+
* state.value_json、runs.input_json / output_json / error_json 等)
|
|
6
|
+
* 禁止在各自调用点散落 try/catch 静默降级,统一经由本函数完成
|
|
7
|
+
* 「解析 + 可观测告警 + 保守降级」的完整链路。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 解析存储层 JSON 字符串,失败时输出单行警告并保守返回原始字符串。
|
|
11
|
+
*
|
|
12
|
+
* 降级策略说明:各调用方的返回类型为业务泛型 T,无法以标记对象区分
|
|
13
|
+
* 损坏数据,因此保持「返回原始字符串」的既有降级行为,但通过
|
|
14
|
+
* console.warn 保证损坏事件可观测,不再无声吞没。警告仅携带
|
|
15
|
+
* 上下文描述与解析失败原因,不输出完整原始值,避免敏感数据
|
|
16
|
+
* 泄漏到日志。
|
|
17
|
+
*
|
|
18
|
+
* @param raw 存储层原始 JSON 字符串
|
|
19
|
+
* @param context 存储路径上下文描述(包含数据库位置、包标识与键名)
|
|
20
|
+
* @returns 解析成功返回解析后的值;入参为空返回 undefined;解析失败返回原始字符串
|
|
21
|
+
*/
|
|
22
|
+
export function safeParseStoredJson(raw, context) {
|
|
23
|
+
if (raw === null || raw === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
31
|
+
console.warn(`[actiondock] stored JSON corrupted at ${context}: ${reason}`);
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/target/local.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare class LocalActionDockTarget implements ActionDockTarget {
|
|
|
34
34
|
maxQueueSize?: number;
|
|
35
35
|
}): AsyncIterable<ExecutionEvent>;
|
|
36
36
|
private resolveApp;
|
|
37
|
+
private fallbackGlobalStorage?;
|
|
37
38
|
private getGlobalStorage;
|
|
38
39
|
private findDeclaredConfigItem;
|
|
39
40
|
getConfig(packageId: string, key: string): Promise<ConfigValueView>;
|
package/dist/target/local.js
CHANGED
|
@@ -89,10 +89,12 @@ export class LocalActionDockTarget {
|
|
|
89
89
|
const apps = options?.packageId
|
|
90
90
|
? [this.target.getApp(options.packageId)].filter(Boolean)
|
|
91
91
|
: this.target.listApps();
|
|
92
|
-
|
|
92
|
+
const records = [];
|
|
93
93
|
for (const app of apps) {
|
|
94
|
+
// 状态过滤下推到存储层 SQL,避免先分页后过滤导致某包前 N 条非目标状态时贡献 0 条
|
|
94
95
|
const recs = app.storage.listRuns({
|
|
95
96
|
actionId: options?.actionId,
|
|
97
|
+
status: options?.status,
|
|
96
98
|
limit: options?.limit,
|
|
97
99
|
});
|
|
98
100
|
for (const r of recs) {
|
|
@@ -103,11 +105,9 @@ export class LocalActionDockTarget {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
records.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
106
|
-
|
|
107
|
-
records = records.filter((r) => r.status === options.status);
|
|
108
|
-
}
|
|
108
|
+
// 全局 limit 语义:合并后总条数不超过 limit,单处截断
|
|
109
109
|
if (options?.limit && records.length > options.limit) {
|
|
110
|
-
records =
|
|
110
|
+
records.length = options.limit;
|
|
111
111
|
}
|
|
112
112
|
return records;
|
|
113
113
|
}
|
|
@@ -115,17 +115,12 @@ export class LocalActionDockTarget {
|
|
|
115
115
|
if (options?.packageId && options.packageId !== this.target.packageId) {
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// 状态过滤下推到存储层 SQL,单包场景由数据库直接返回目标状态记录
|
|
119
|
+
return this.target.storage.listRuns({
|
|
119
120
|
actionId: options?.actionId,
|
|
121
|
+
status: options?.status,
|
|
120
122
|
limit: options?.limit,
|
|
121
123
|
});
|
|
122
|
-
if (options?.status) {
|
|
123
|
-
records = records.filter((r) => r.status === options.status);
|
|
124
|
-
}
|
|
125
|
-
if (options?.limit && records.length > options.limit) {
|
|
126
|
-
records = records.slice(0, options.limit);
|
|
127
|
-
}
|
|
128
|
-
return records;
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
126
|
async clearRuns(options) {
|
|
@@ -176,6 +171,7 @@ export class LocalActionDockTarget {
|
|
|
176
171
|
return undefined;
|
|
177
172
|
}
|
|
178
173
|
}
|
|
174
|
+
fallbackGlobalStorage;
|
|
179
175
|
getGlobalStorage() {
|
|
180
176
|
if ("globalStorage" in this.target && this.target.globalStorage) {
|
|
181
177
|
return this.target.globalStorage;
|
|
@@ -185,10 +181,13 @@ export class LocalActionDockTarget {
|
|
|
185
181
|
if (app.globalStorage)
|
|
186
182
|
return app.globalStorage;
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
if (!this.fallbackGlobalStorage) {
|
|
185
|
+
this.fallbackGlobalStorage = createGlobalStorage({
|
|
186
|
+
dataDir: this.target.options?.dataDir,
|
|
187
|
+
customHome: this.target.options?.customHome,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return this.fallbackGlobalStorage;
|
|
192
191
|
}
|
|
193
192
|
findDeclaredConfigItem(key) {
|
|
194
193
|
if ("listApps" in this.target) {
|
|
@@ -323,6 +322,13 @@ export class LocalActionDockTarget {
|
|
|
323
322
|
return app.storage.listStateEntries(options);
|
|
324
323
|
}
|
|
325
324
|
async close(options) {
|
|
325
|
+
try {
|
|
326
|
+
this.fallbackGlobalStorage?.close();
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// 忽略兜底全局存储关闭异常
|
|
330
|
+
}
|
|
331
|
+
this.fallbackGlobalStorage = undefined;
|
|
326
332
|
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
327
333
|
let timer;
|
|
328
334
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远端通信错误翻译层。
|
|
3
|
+
*
|
|
4
|
+
* 职责单一:将 profile/client 传输层抛出的底层异常翻译为统一的
|
|
5
|
+
* TargetError 结构化异常,供 RemoteActionDockTarget 各操作边界复用;
|
|
6
|
+
* 优先读取传输层透传的结构化错误码,无法识别时原样透传。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 将远端通信异常包装翻译为统一的 TargetError。
|
|
10
|
+
*
|
|
11
|
+
* 无法识别的异常按防御与透明原则原样抛出,严禁静默吞没。
|
|
12
|
+
*/
|
|
13
|
+
export declare function wrapRemoteError(err: any): never;
|
|
14
|
+
/**
|
|
15
|
+
* 判定远端状态键访问异常是否为键不存在。
|
|
16
|
+
*
|
|
17
|
+
* 优先读取传输层透传的结构化错误码(fetchRemoteJson 会将响应体 error.code
|
|
18
|
+
* 附加到抛出异常的 code 字段),仅当旧版服务器未透传 code 时回退到
|
|
19
|
+
* HTTP 状态与消息文本兼容嗅探。
|
|
20
|
+
*/
|
|
21
|
+
export declare function isRemoteStateKeyNotFound(err: any): boolean;
|