@sema-agent/server 7.55.0 → 7.57.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/MIGRATION.md +3 -3
- package/README.md +14 -7
- package/README.zh-CN.md +10 -6
- package/USAGE.md +207 -59
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -8
- package/dist/boot/runtime-caps.js +50 -20
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/config-catalog.js +14 -5
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/http/route-ctx.d.ts +90 -3
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +21 -8
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +20 -5
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +220 -34
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +49 -15
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +80 -1
- package/dist/leader/wire.js +55 -26
- package/dist/main.js +44 -8
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
- package/dist/plugins/checkpoint-store-sql.js +23 -4
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +21 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +5 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +21 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +25 -11
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +39 -23
- package/dist/plugins/pg-session-storage.js +17 -13
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +36 -5
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +70 -1
- package/dist/plugins/tidb-session-store.js +18 -14
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.d.ts +5 -4
- package/dist/rules-consent.js +25 -46
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +44 -4
- package/dist/security.d.ts +7 -0
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +38 -63
- package/dist/tool-approval.js +102 -74
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +10 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +655 -20
- package/dist/trace/sema-provenance.d.ts +26 -0
- package/dist/trace/sema-provenance.js +11 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +3 -3
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
|
@@ -15,12 +15,77 @@
|
|
|
15
15
|
* code should import from this file.
|
|
16
16
|
*/
|
|
17
17
|
import type { TaskResult, TaskStatus } from "@sema-agent/core";
|
|
18
|
+
/**
|
|
19
|
+
* 取消便签里的**大脑相位**快照 —— core `BrainStatus` 帧经 `brainStatusEventData` 投影后的五键子集。
|
|
20
|
+
*
|
|
21
|
+
* 刻意**不含** `detail`(provider 自由文本,宽面)与 `retryInMs`/`retryInSec`(相对量,过期即失真;
|
|
22
|
+
* 绝对量 `retryAtMs` 才是可跨进程读的那个,core 顶注已裁)。`phase` 是这只快照的存在理由:没有它
|
|
23
|
+
* 就没有快照(不铸半只)。
|
|
24
|
+
*/
|
|
25
|
+
export interface RunBrainStatusSnapshot {
|
|
26
|
+
phase: string;
|
|
27
|
+
attempt?: number;
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
/** 等待结束的**墙钟**时刻(发端进程钟域,非单调)—— 只当近似,别与本地单调计时器比。 */
|
|
30
|
+
retryAtMs?: number;
|
|
31
|
+
errClass?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* [ref] 件3(cli [ref] sema-bug4 (d) / server [ref]):**用户取消时的现场快照**。
|
|
35
|
+
*
|
|
36
|
+
* 病(取证 `audits/evidence/2026-09-03-model-failure-visibility/`):模型挂起 7 分 26 秒后用户放弃,
|
|
37
|
+
* 账本把这条 run 记成 `failed` + `errorCode:"cancelled"` + 「stream client disconnected before
|
|
38
|
+
* completion」—— 与「用户看了一眼不想跑了,秒按 Esc」**同形**。历史 314 条里 96 条 failed、其中 63 条
|
|
39
|
+
* 是 `user_disconnect`,「其中有多少条其实是模型挂死」在数据面**不可考**。
|
|
40
|
+
*
|
|
41
|
+
* 本结构是那一维判别材料:取消发生时,距离**本 run 上一条引擎事件**过了多久。
|
|
42
|
+
*
|
|
43
|
+
* 【语义边界(勿漂)】
|
|
44
|
+
* - 它**不**描述模型调用相位(首字节/重试在 core 的 stream-engine/with-retry 里,server 结构上看不见
|
|
45
|
+
* ——那半场是 [ref] 的 core 钩,别在这里编造)。它只说「server 这一侧多久没收到这条 run 的动静了」。
|
|
46
|
+
* - `lastEventKind`/`lastEventAgeMs` 的数据源是**同副本进程内**的 turn 活性登记(`turn-activity.ts`)。
|
|
47
|
+
* 跨副本 / 本副本重启后读不到 ⇒ 两键**诚实缺席**(缺席 = 「证不出」,不是「0ms 前刚活过」)。
|
|
48
|
+
* - `elapsedMs` 恒在场:run 起跑到取消的墙钟毫秒。
|
|
49
|
+
* - 分诊材料,不是状态机输入:任何门 / CAS / resume 判定都不许读它。
|
|
50
|
+
*
|
|
51
|
+
* 落点 = 终局 `result` blob 里的一个 additive 可缺席键({@link PersistedTaskResult}),**零新列**
|
|
52
|
+
* ——run 行的 `result` 本来就是整只 JSON 存(SQL 双方言同形),所以两个孪生都不需要 DDL 变更。
|
|
53
|
+
*/
|
|
54
|
+
export interface RunCancelContext {
|
|
55
|
+
/** 最后一条进本 run durable 账本的事件类型(`text` / `tool_start` / `status` / …)。
|
|
56
|
+
* 🔴 它比 {@link RunCancelContext.lastEventAgeMs} **更容易缺席**(codex 交叉复审 R1-[medium],验真后
|
|
57
|
+
* 改的正是这条契约):打点方里有几席拿不到事件类型(core 的 `onActivity` 第五席、resume 腿的手动
|
|
58
|
+
* 打点),它们只推进「时刻」不带「词」。所以两键**不是**同在同缺 —— 有 age 无 kind 是合法读数,
|
|
59
|
+
* 意思是「本副本知道那一刻,但说不出那是条什么事件」。 */
|
|
60
|
+
lastEventKind?: string;
|
|
61
|
+
/** 距最后一条 durable 事件过了多久(ms,≥0)。同副本有活性登记就在场。 */
|
|
62
|
+
lastEventAgeMs?: number;
|
|
63
|
+
/** 本 run 账本尾**最后一条** `status`(BrainStatus)帧的五键快照([ref]/[ref])——「已重试几次 /
|
|
64
|
+
* 在等什么 / 什么错类」。这条帧 server 早就在透传与落账(`trace/project.ts` 的 `brainStatusEventData`),
|
|
65
|
+
* 取消时把最后一条摘下来当便签,**不需要 core 新钩**。sticky:后续 text/tool 事件不清它。
|
|
66
|
+
* 这条 run 从没出过 status 帧(或跨副本读不到)⇒ 缺席,恒不铸 null。 */
|
|
67
|
+
lastBrainStatus?: RunBrainStatusSnapshot;
|
|
68
|
+
/** run 起跑到本次取消的墙钟毫秒(≥0)。常态在场;行的 `createdAt` 坏到解析不出时缺席
|
|
69
|
+
* ——**绝不折成 0**(0 会被读成「刚起跑就取消」,与本结构要判别的那件事正好相反)。 */
|
|
70
|
+
elapsedMs?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 落库/上 wire 的终局结果 = core 的 `TaskResult` **加上** server 自铸的可缺席侧记键。
|
|
74
|
+
*
|
|
75
|
+
* 今天只有一个成员({@link RunCancelContext});additive 且可缺席,所以旧行、旧消费端逐字不受影响
|
|
76
|
+
* (`TaskResult` 仍可原样赋给本型)。⚠️ 这不是给「往 core 结果里塞私货」开的口子:每加一个键都要能
|
|
77
|
+
* 回答「为什么它属于 run 账本而不属于 core 的任务结果」——`cancelContext` 的答案是它描述的是
|
|
78
|
+
* **server 侧的取消现场**,core 那一侧根本不知道有人按了取消。
|
|
79
|
+
*/
|
|
80
|
+
export type PersistedTaskResult = TaskResult & {
|
|
81
|
+
cancelContext?: RunCancelContext;
|
|
82
|
+
};
|
|
18
83
|
export interface RunRecord {
|
|
19
84
|
taskId: string;
|
|
20
85
|
sessionId: string;
|
|
21
86
|
owner: string | null;
|
|
22
87
|
status: "running" | TaskStatus;
|
|
23
|
-
result:
|
|
88
|
+
result: PersistedTaskResult | null;
|
|
24
89
|
error: string | null;
|
|
25
90
|
/** Structured failure code (1.36/1.37) denormalized from the result for SQL queryability. */
|
|
26
91
|
errorCode: string | null;
|
|
@@ -94,6 +159,10 @@ export interface SessionSummary {
|
|
|
94
159
|
/** K-5c (shell §K): the LATEST run's task_id — the anchor the shell uses to re-attach the live event tail in ONE
|
|
95
160
|
* hop (`GET /v1/runs/:lastRunId/events`) when resuming a picked session, instead of a second lookup. */
|
|
96
161
|
lastRunId: string | null;
|
|
162
|
+
/** [ref]([ref]/[ref]):latest 终局 run 的 engine runId(`TaskResult.runId`,latest.result JSON 提取的
|
|
163
|
+
* 零迁移路线 —— result 终局才写 ⇒ 在跑 run 键诚实缺席);OMIT-when-absent,恒不铸 null。轴辨析与
|
|
164
|
+
* wire 半场见 `security.ts SessionListItem.lastTaskRunId`(同名同义)。 */
|
|
165
|
+
lastTaskRunId?: string;
|
|
97
166
|
/** The auto-generated session title (cheap-model one-liner, write-once). null = not (yet)
|
|
98
167
|
* titled OR the lister face has no session_meta (the legacy task_run aggregate / memory twins — honest degrade). */
|
|
99
168
|
title: string | null;
|
|
@@ -144,28 +144,32 @@ export class TiDBSessionStore {
|
|
|
144
144
|
params.push(new Date(opts.cursor.lastActivityAt), new Date(opts.cursor.lastActivityAt), opts.cursor.sessionId);
|
|
145
145
|
}
|
|
146
146
|
const sql = "SELECT sm.session_id, sm.owner, sm.updated_at AS last_activity, sm.created_at AS first_activity, sm.title, " +
|
|
147
|
-
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id " +
|
|
147
|
+
" COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id, latest.last_task_run_id " +
|
|
148
148
|
"FROM session_meta sm " +
|
|
149
149
|
"LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run GROUP BY session_id) r ON r.session_id = sm.session_id " +
|
|
150
|
-
"LEFT JOIN (SELECT session_id, objective_preview, status, task_id FROM (" +
|
|
151
|
-
" SELECT session_id, objective_preview, status, task_id, " +
|
|
150
|
+
"LEFT JOIN (SELECT session_id, objective_preview, status, task_id, last_task_run_id FROM (" +
|
|
151
|
+
" SELECT session_id, objective_preview, status, task_id, JSON_UNQUOTE(JSON_EXTRACT(result, '$.runId')) AS last_task_run_id, " +
|
|
152
152
|
" ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn FROM task_run" +
|
|
153
153
|
" ) lr WHERE rn = 1) latest ON latest.session_id = sm.session_id " +
|
|
154
154
|
(where.length ? "WHERE " + where.join(" AND ") + " " : "") +
|
|
155
155
|
"ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ?";
|
|
156
156
|
params.push(opts.limit);
|
|
157
157
|
const [rows] = await this.pool.query(sql, params);
|
|
158
|
-
return rows.map((r) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
return rows.map((r) => {
|
|
159
|
+
const runId = r.last_task_run_id;
|
|
160
|
+
return {
|
|
161
|
+
sessionId: String(r.session_id),
|
|
162
|
+
owner: r.owner ?? null,
|
|
163
|
+
lastActivityAt: r.last_activity instanceof Date ? r.last_activity.toISOString() : String(r.last_activity),
|
|
164
|
+
firstActivityAt: r.first_activity instanceof Date ? r.first_activity.toISOString() : String(r.first_activity),
|
|
165
|
+
runCount: Number(r.run_count),
|
|
166
|
+
objectivePreview: r.objective_preview ?? null,
|
|
167
|
+
lastStatus: r.status == null ? "" : String(r.status),
|
|
168
|
+
lastRunId: r.last_run_id ?? null,
|
|
169
|
+
...(typeof runId === "string" && runId !== "" ? { lastTaskRunId: runId } : {}),
|
|
170
|
+
title: r.title ?? null,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
169
173
|
}
|
|
170
174
|
async probeTitle(sessionId) {
|
|
171
175
|
const [rows] = await this.pool.query("SELECT title FROM session_meta WHERE session_id = ?", [sessionId]);
|
|
@@ -75,6 +75,11 @@ export type InboxWarn = typeof onWarnType;
|
|
|
75
75
|
* with an explicit marker. Returns null only when even the skeleton is oversize (physics — caller keeps the
|
|
76
76
|
* prior revision, matching the old behavior for that corner). Dialect-neutral (pure JS, no SQL) — shared
|
|
77
77
|
* verbatim by both twins. */
|
|
78
|
+
/** S-107:workflow_run 行的 `error`(脚本抛出的 `err.message`,core 源头不脱)在**两条写腿**(put / update 常态 + oversize 降级)
|
|
79
|
+
* 同一口脱;`result` 由 core 完成时源头脱。幂等,不抛(超大 ⇒ 占位)。 */
|
|
80
|
+
export declare function withRedactedError<T extends {
|
|
81
|
+
error?: string | undefined;
|
|
82
|
+
}>(run: T): T;
|
|
78
83
|
export declare function slimOversizeRun(run: WorkflowRun & {
|
|
79
84
|
id: string;
|
|
80
85
|
scope: string;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
|
|
1
2
|
import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
|
|
2
3
|
import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
|
|
3
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
5
|
import { isDupKeyError } from "./sql-errors.js";
|
|
5
6
|
export const MAX_RUN_BLOB_BYTES = 4 * 1024 * 1024;
|
|
6
7
|
const onWarnType = (msg, meta) => void [msg, meta];
|
|
8
|
+
export function withRedactedError(run) {
|
|
9
|
+
if (typeof run.error !== "string")
|
|
10
|
+
return run;
|
|
11
|
+
const error = redactErrorMessage(run.error) ?? "";
|
|
12
|
+
return error === run.error ? run : { ...run, error };
|
|
13
|
+
}
|
|
7
14
|
export function slimOversizeRun(run, maxBytes) {
|
|
8
15
|
const MARK = "…[truncated: run blob exceeded the store cap]";
|
|
9
16
|
const cut = (s) => s.slice(0, 4000) + MARK;
|
|
10
17
|
const step1 = {
|
|
11
18
|
...run,
|
|
12
19
|
...(typeof run.result === "string" && run.result.length > 4000 ? { result: cut(run.result) } : {}),
|
|
13
|
-
...(typeof run.error === "string" && run.error.length > 4000 ? { error:
|
|
20
|
+
...(typeof run.error === "string" && run.error.length > 4000 ? { error: redactHead(run.error, 4000) + MARK } : {}),
|
|
14
21
|
};
|
|
15
22
|
let blob = JSON.stringify(step1);
|
|
16
23
|
if (Buffer.byteLength(blob) <= maxBytes)
|
|
@@ -30,7 +37,7 @@ export class SqlWorkflowRunStore {
|
|
|
30
37
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
31
38
|
}
|
|
32
39
|
async put(id, run) {
|
|
33
|
-
const stored = { ...run, id, rev: run.rev ?? 0 };
|
|
40
|
+
const stored = { ...withRedactedError(run), id, rev: run.rev ?? 0 };
|
|
34
41
|
try {
|
|
35
42
|
await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
|
|
36
43
|
}
|
|
@@ -51,6 +58,7 @@ export class SqlWorkflowRunStore {
|
|
|
51
58
|
return run;
|
|
52
59
|
}
|
|
53
60
|
async update(id, scope, run, expect) {
|
|
61
|
+
run = withRedactedError(run);
|
|
54
62
|
let blob = JSON.stringify({ ...run, id, scope });
|
|
55
63
|
if (Buffer.byteLength(blob) > MAX_RUN_BLOB_BYTES) {
|
|
56
64
|
this.onWarn?.("workflow_run_blob_oversize_slimmed", { id, scope, fullBytes: Buffer.byteLength(blob), capBytes: MAX_RUN_BLOB_BYTES });
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
2
|
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
|
-
import { type RuleImportTicket, type
|
|
3
|
+
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
5
5
|
* 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。
|
|
6
6
|
*
|
|
@@ -31,12 +31,13 @@ export interface RuleConsentStores {
|
|
|
31
31
|
}): Promise<RuleImportTicket>;
|
|
32
32
|
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。
|
|
33
33
|
* `purpose` = 车道绑定(A7 R1-F2):一条车道的口只认它自己铸的票,不符与「票不存在」同形。
|
|
34
|
-
*
|
|
35
|
-
|
|
34
|
+
* 🔴 **不收任何时刻量**([ref]):过期判定是店自己的事,由店那口钟(SQL=数据库的钟 / local=本进程的
|
|
35
|
+
* 墙钟)在**语句执行的那一刻**答。车道递一个时刻进来,就等于让副本的钟参与授权判决。 */
|
|
36
|
+
consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketRedeemResult>;
|
|
36
37
|
/** 把认领**放回去**(认领之后遇到不确定/瞬时失败时)。`claimId` = 本次认领的代次(A7 R1-F1 围栏:
|
|
37
38
|
* 被顶替的旧持有者动不了现持有者的认领);`mustRemainValidMs` = 放回之后这张票至少还要能用多久
|
|
38
39
|
* —— 撑不过就**不算放回成功**(理由逐字见 store 侧 `release` 的头注)。 */
|
|
39
|
-
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number
|
|
40
|
+
release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
40
41
|
/**
|
|
41
42
|
* [ref] A7 三态协议的三条口(CLAIMED → SETTLED / 三态快照 / 崩溃窗抢认领)。
|
|
42
43
|
*
|
package/dist/rules-consent.js
CHANGED
|
@@ -80,7 +80,10 @@ function isSupportedRuleMatch(match) {
|
|
|
80
80
|
switch (match) {
|
|
81
81
|
case "exact":
|
|
82
82
|
case "prefix":
|
|
83
|
+
case "wildcard":
|
|
83
84
|
return true;
|
|
85
|
+
case "subpath":
|
|
86
|
+
return false;
|
|
84
87
|
default: {
|
|
85
88
|
const unknown = match;
|
|
86
89
|
void unknown;
|
|
@@ -166,25 +169,28 @@ function screenLocalImportRow(row) {
|
|
|
166
169
|
detail: "this project root carries control bytes (C0/DEL) — it is not a path, one SQL dialect refuses to store it at all, and it breaks the separator assumption this lane's comparison keys rest on",
|
|
167
170
|
};
|
|
168
171
|
}
|
|
172
|
+
const consistent = (derived) => {
|
|
173
|
+
if (derived.tool !== row.tool || derived.match !== row.match || derived.command !== row.command) {
|
|
174
|
+
return {
|
|
175
|
+
reason: "row_inconsistent",
|
|
176
|
+
detail: `this row's tool/match/command are derived facts of its rule text, and they disagree: the rule reads as ` +
|
|
177
|
+
`${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { rule: derived.rule, scope };
|
|
181
|
+
};
|
|
182
|
+
const written = parseAllowRuleText(row.rule);
|
|
183
|
+
if ("reject" in written)
|
|
184
|
+
return { reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
|
|
185
|
+
if (written.rule.tool === "Read")
|
|
186
|
+
return consistent(written.rule);
|
|
169
187
|
const pre = precheckCardRuleText(row.rule, row.command);
|
|
170
188
|
if (pre === undefined) {
|
|
171
189
|
return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
|
|
172
190
|
}
|
|
173
191
|
if (!pre.ok)
|
|
174
192
|
return { reason: "rule_rejected", detail: pre.message };
|
|
175
|
-
|
|
176
|
-
if ("reject" in canonical) {
|
|
177
|
-
return { reason: "rule_rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
|
|
178
|
-
}
|
|
179
|
-
const derived = canonical.rule;
|
|
180
|
-
if (derived.tool !== row.tool || derived.match !== row.match || derived.command !== row.command) {
|
|
181
|
-
return {
|
|
182
|
-
reason: "row_inconsistent",
|
|
183
|
-
detail: `this row's tool/match/command are derived facts of its rule text, and they disagree: the rule reads as ` +
|
|
184
|
-
`${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return { rule: derived.rule, scope };
|
|
193
|
+
return consistent(written.rule);
|
|
188
194
|
}
|
|
189
195
|
const LOCAL_IMPORT_LAYER_PATH = "local-import:rules";
|
|
190
196
|
const CONTROL_AND_BIDI_DETECT_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
|
|
@@ -230,27 +236,6 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
230
236
|
const monotonic = opts?.monotonic ?? (() => performance.now());
|
|
231
237
|
const sleep = opts?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
232
238
|
const localImportLogger = createLogger();
|
|
233
|
-
const anchors = new Map();
|
|
234
|
-
const ANCHOR_CAP = 4096;
|
|
235
|
-
const pruneAnchors = (nowMono) => {
|
|
236
|
-
for (const [id, a] of anchors) {
|
|
237
|
-
if (nowMono - a.mintedAtMono > ticketTtlMs)
|
|
238
|
-
anchors.delete(id);
|
|
239
|
-
}
|
|
240
|
-
if (anchors.size <= ANCHOR_CAP)
|
|
241
|
-
return;
|
|
242
|
-
const excess = anchors.size - ANCHOR_CAP;
|
|
243
|
-
let dropped = 0;
|
|
244
|
-
for (const id of anchors.keys()) {
|
|
245
|
-
if (dropped++ >= excess)
|
|
246
|
-
break;
|
|
247
|
-
anchors.delete(id);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
const decisionClock = (ticketId) => {
|
|
251
|
-
const a = anchors.get(ticketId);
|
|
252
|
-
return a === undefined ? undefined : { decisionNowMs: Math.ceil(a.mintedAtWall + (monotonic() - a.mintedAtMono)) };
|
|
253
|
-
};
|
|
254
239
|
return {
|
|
255
240
|
async persistCardRule(input) {
|
|
256
241
|
let prepared;
|
|
@@ -262,6 +247,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
262
247
|
...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
|
|
263
248
|
...(input.boundInputHash !== undefined ? { boundInputHash: input.boundInputHash } : {}),
|
|
264
249
|
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
250
|
+
...(input.scope?.kind === "project" ? { cwd: input.scope.root } : {}),
|
|
265
251
|
deps,
|
|
266
252
|
});
|
|
267
253
|
}
|
|
@@ -360,14 +346,10 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
360
346
|
}
|
|
361
347
|
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
362
348
|
}
|
|
363
|
-
const mintedAtMono = monotonic();
|
|
364
349
|
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "cc-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
365
|
-
pruneAnchors(mintedAtMono);
|
|
366
|
-
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
367
350
|
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
368
351
|
},
|
|
369
352
|
async redeemImport(principal, ticket) {
|
|
370
|
-
const clock = decisionClock(ticket);
|
|
371
353
|
const consumeOutcomeAfterThrow = async (err) => {
|
|
372
354
|
localImportLogger.warn("cc_import_claim_write_failed", {
|
|
373
355
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -398,7 +380,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
398
380
|
};
|
|
399
381
|
let consumed;
|
|
400
382
|
try {
|
|
401
|
-
consumed = await stores.tickets.consume(ticket, principal, "cc-import"
|
|
383
|
+
consumed = await stores.tickets.consume(ticket, principal, "cc-import");
|
|
402
384
|
}
|
|
403
385
|
catch (err) {
|
|
404
386
|
return await consumeOutcomeAfterThrow(err);
|
|
@@ -407,7 +389,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
407
389
|
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
408
390
|
const releaseClaim = async () => {
|
|
409
391
|
try {
|
|
410
|
-
return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000
|
|
392
|
+
return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
411
393
|
}
|
|
412
394
|
catch (err) {
|
|
413
395
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
@@ -496,10 +478,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
496
478
|
}
|
|
497
479
|
if (approvalId === undefined)
|
|
498
480
|
return { ok: true, preview: { members } };
|
|
499
|
-
const mintedAtMono = monotonic();
|
|
500
481
|
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "local-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
501
|
-
pruneAnchors(mintedAtMono);
|
|
502
|
-
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
503
482
|
return { ok: true, preview: { members }, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
504
483
|
},
|
|
505
484
|
async redeemLocalImport(principal, ticket) {
|
|
@@ -530,7 +509,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
530
509
|
const driveAndSettle = async (approvalId, payloadHash, claimId) => {
|
|
531
510
|
const tryRelease = async () => {
|
|
532
511
|
try {
|
|
533
|
-
return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000
|
|
512
|
+
return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
534
513
|
}
|
|
535
514
|
catch (err) {
|
|
536
515
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
@@ -638,7 +617,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
638
617
|
};
|
|
639
618
|
let consumed;
|
|
640
619
|
try {
|
|
641
|
-
consumed = await stores.tickets.consume(ticket, principal, "local-import"
|
|
620
|
+
consumed = await stores.tickets.consume(ticket, principal, "local-import");
|
|
642
621
|
}
|
|
643
622
|
catch (err) {
|
|
644
623
|
return await claimOutcomeAfterThrow(err, "consume");
|
|
@@ -660,7 +639,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
660
639
|
case "minted": {
|
|
661
640
|
let again;
|
|
662
641
|
try {
|
|
663
|
-
again = await stores.tickets.consume(ticket, principal, "local-import"
|
|
642
|
+
again = await stores.tickets.consume(ticket, principal, "local-import");
|
|
664
643
|
}
|
|
665
644
|
catch (err) {
|
|
666
645
|
return await claimOutcomeAfterThrow(err, "consume");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RunCancelContext } from "./plugins/store-contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* @param taskId 这条 run 的账本 id(turn 活性登记的键)。
|
|
4
|
+
* @param startedAtMs 这条 run 起跑的墙钟时刻(epoch ms):取消腿手上有行就用行的 `createdAt`,
|
|
5
|
+
* 只有本腿起点时用本腿起点(两者在同一条 run 上相差 ms 级)。**非有限值**
|
|
6
|
+
* (坏 `createdAt` 解析出的 NaN)⇒ `elapsedMs` 诚实缺席,绝不折成 0
|
|
7
|
+
* ——0 会被读成「刚起跑就取消了」,与「模型挂死 8 分钟后用户放弃」正好相反。
|
|
8
|
+
* @param nowMs 取样时刻(缺省 `Date.now()`;测试注入用)。
|
|
9
|
+
* @returns 三键至少有一键可证时的快照;**一键都证不出** ⇒ `undefined`(写一只空对象等于在 wire 上
|
|
10
|
+
* 断言「我看过现场,什么都没有」,而事实是「我什么都没看见」)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildCancelContext(taskId: string, startedAtMs: number, nowMs?: number): RunCancelContext | undefined;
|
|
13
|
+
//# sourceMappingURL=run-cancel-context.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readTurnActivity } from "./turn-activity.js";
|
|
2
|
+
export function buildCancelContext(taskId, startedAtMs, nowMs = Date.now()) {
|
|
3
|
+
const activity = readTurnActivity(taskId);
|
|
4
|
+
const age = activity === undefined ? undefined : Math.max(0, nowMs - activity.at);
|
|
5
|
+
const elapsedMs = Number.isFinite(startedAtMs) ? Math.max(0, nowMs - startedAtMs) : undefined;
|
|
6
|
+
if (age === undefined && elapsedMs === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
return {
|
|
9
|
+
...(activity?.kind !== undefined ? { lastEventKind: activity.kind } : {}),
|
|
10
|
+
...(age !== undefined ? { lastEventAgeMs: age } : {}),
|
|
11
|
+
...(activity?.brainStatus !== undefined ? { lastBrainStatus: activity.brainStatus } : {}),
|
|
12
|
+
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=run-cancel-context.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -14,7 +14,7 @@ import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js
|
|
|
14
14
|
import { taskWallClockSec } from "./task-workflow.js";
|
|
15
15
|
import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
16
16
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
17
|
-
import { loadConfig, logConfigDiagnostics,
|
|
17
|
+
import { loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
18
18
|
import { createConfigProvider } from "./config-provider.js";
|
|
19
19
|
import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
20
20
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
@@ -445,7 +445,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
445
445
|
}
|
|
446
446
|
const mention = parseModelMention(args.objective, Object.keys(config.models));
|
|
447
447
|
const scope = args.user ?? "local";
|
|
448
|
-
const taskTimeoutSec = Math.
|
|
448
|
+
const taskTimeoutSec = Math.floor(parseNumOrFailNonNegative("TASK_TIMEOUT_SEC", process.env.TASK_TIMEOUT_SEC || "0"));
|
|
449
449
|
const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "discuss");
|
|
450
450
|
const mcp = mcpForScenario(config.mcpServers, scenarioName);
|
|
451
451
|
const a2a = a2aForScenario(config.a2aPeers, scenarioName);
|
package/dist/runs.d.ts
CHANGED
|
@@ -308,10 +308,13 @@ promptManifests?: PromptManifestTracker,
|
|
|
308
308
|
* `streamApprovalOn` = 协议上场判据(`resolveStreamApprovalGate`,由路由层求值后传进来 —— 本函数是纯
|
|
309
309
|
* 执行腿,不该自己读 backend/config)。为假 ⇒ 只接既有四键 ctx(呈卡口/腿轴都不接),本腿字节逐字不变。
|
|
310
310
|
*/
|
|
311
|
+
/** `windowMs` 收成 `number | undefined`([ref] + codex r1-[medium]):`undefined` = **这份装配没有
|
|
312
|
+
* `streamApproval` 段**(stub-harness),不是「运维显式关窗」—— 两者自 [ref] 起结局不同(后者即答
|
|
313
|
+
* 无人值守终局),路由层因此不许再折算,见 `resolveApprovalLeg` 的 `windowMs` 域注。 */
|
|
311
314
|
approval?: {
|
|
312
315
|
coordinator: ToolApprovalCoordinator;
|
|
313
316
|
streamApprovalOn: boolean;
|
|
314
|
-
windowMs: number;
|
|
317
|
+
windowMs: number | undefined;
|
|
315
318
|
windowMarginMs: number;
|
|
316
319
|
},
|
|
317
320
|
/** [ref]:park 投影腿的 checkpoint 读口(`deps.checkpointStore`)。在场时,durable park 的
|
package/dist/runs.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { runWithVerification, runCascade, defaultTaskRegistry, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
|
|
2
|
+
import { redactErrorMessage } from "./observability/run-terminal-log.js";
|
|
2
3
|
import { withPrincipal } from "./observability/principal-context.js";
|
|
3
4
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
5
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
6
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
7
|
import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
|
|
7
8
|
import { recordTurnActivity } from "./turn-activity.js";
|
|
9
|
+
import { buildCancelContext } from "./run-cancel-context.js";
|
|
8
10
|
import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
|
|
9
11
|
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
10
12
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
11
13
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
12
14
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
15
|
+
import { errorText as errText } from "./observability/err-text.js";
|
|
13
16
|
import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
|
|
14
17
|
export async function backgroundAgentOutput(registry, handle, access, agentStore) {
|
|
15
18
|
const row = registry.getAccessibleTask(handle, access);
|
|
@@ -162,7 +165,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
162
165
|
const preemptCtrl = new AbortController();
|
|
163
166
|
if (preemptEligible)
|
|
164
167
|
preemptable?.set(taskId, preemptCtrl);
|
|
165
|
-
const cancelledResult = () =>
|
|
168
|
+
const cancelledResult = () => {
|
|
169
|
+
const ctx = buildCancelContext(taskId, startedAt);
|
|
170
|
+
return { taskId, sessionId: spec.sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 }, ...(ctx ? { cancelContext: ctx } : {}) };
|
|
171
|
+
};
|
|
166
172
|
const recordDone = (result, status) => {
|
|
167
173
|
metrics?.addGauge("runs_active", -1);
|
|
168
174
|
metrics?.inc("tasks_total", { status });
|
|
@@ -178,7 +184,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
178
184
|
const owner = principal ?? null;
|
|
179
185
|
const approvalLeg = resolveApprovalLeg({
|
|
180
186
|
streamApprovalOn: approval?.streamApprovalOn === true,
|
|
181
|
-
windowMs: approval?.windowMs
|
|
187
|
+
windowMs: approval?.windowMs,
|
|
182
188
|
windowMarginMs: approval?.windowMarginMs ?? 0,
|
|
183
189
|
...(typeof spec.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: spec.limits.maxWalltimeMs } : {}),
|
|
184
190
|
nowMonotonicMs: performance.now(),
|
|
@@ -221,7 +227,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
221
227
|
metrics?.addGauge("runs_active", -1);
|
|
222
228
|
metrics?.inc("tasks_total", { status: "needs_review" });
|
|
223
229
|
};
|
|
224
|
-
const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: () => recordTurnActivity(taskId) });
|
|
230
|
+
const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData) });
|
|
225
231
|
const append = (type, data) => sink.append(type, data);
|
|
226
232
|
const flush = () => sink.flush();
|
|
227
233
|
const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
|
|
@@ -410,18 +416,33 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
410
416
|
: withApproval();
|
|
411
417
|
});
|
|
412
418
|
if (!reached && cancelCtrl.signal.aborted) {
|
|
413
|
-
|
|
419
|
+
try {
|
|
420
|
+
await flush();
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-clean-cancel ${errText(e)}`);
|
|
424
|
+
}
|
|
414
425
|
const c = cancelledResult();
|
|
415
|
-
|
|
426
|
+
try {
|
|
427
|
+
await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
|
|
431
|
+
}
|
|
416
432
|
reached = { kind: "done", result: c };
|
|
417
433
|
await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
|
|
418
434
|
recordDone(c, "cancelled");
|
|
419
435
|
}
|
|
420
436
|
}
|
|
421
437
|
catch (err) {
|
|
422
|
-
const message =
|
|
438
|
+
const message = errText(err);
|
|
423
439
|
try {
|
|
424
|
-
|
|
440
|
+
try {
|
|
441
|
+
await flush();
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-catch ${errText(e)}`);
|
|
445
|
+
}
|
|
425
446
|
if (reached?.kind === "done") {
|
|
426
447
|
await runStore.setTerminal(taskId, reached.result.status, reached.result, reached.result.errorMessage ?? null);
|
|
427
448
|
}
|
|
@@ -433,12 +454,22 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
433
454
|
}
|
|
434
455
|
else if (cancelCtrl.signal.aborted) {
|
|
435
456
|
const c = cancelledResult();
|
|
436
|
-
|
|
457
|
+
try {
|
|
458
|
+
await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
|
|
462
|
+
}
|
|
437
463
|
await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
|
|
438
464
|
}
|
|
439
465
|
else {
|
|
440
|
-
const safeMsg =
|
|
441
|
-
|
|
466
|
+
const safeMsg = redactErrorMessage(message) ?? message;
|
|
467
|
+
try {
|
|
468
|
+
await append("failed", { errorMessage: safeMsg });
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-failed ${errText(e)}`);
|
|
472
|
+
}
|
|
442
473
|
await runStore.setTerminal(taskId, "failed", null, safeMsg);
|
|
443
474
|
}
|
|
444
475
|
}
|