@sema-agent/server 7.73.1 → 7.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/USAGE.md +8 -0
  2. package/dist/boot/runner-deps.d.ts +0 -4
  3. package/dist/boot/runner-deps.js +1 -2
  4. package/dist/boot/session-faces.js +0 -2
  5. package/dist/boot/stores.js +3 -16
  6. package/dist/config-types.d.ts +1 -1
  7. package/dist/deployment-governance.d.ts +23 -3
  8. package/dist/deployment-governance.js +23 -2
  9. package/dist/env-facts.d.ts +8 -1
  10. package/dist/http/route-ctx.d.ts +9 -8
  11. package/dist/http/routes/approvals-assistant.js +2 -2
  12. package/dist/http/routes/runs.js +1 -1
  13. package/dist/http/routes/workflows.js +4 -0
  14. package/dist/http/server.js +1 -0
  15. package/dist/main.js +0 -1
  16. package/dist/observability/fail-open.d.ts +1 -1
  17. package/dist/observability/fail-open.js +1 -1
  18. package/dist/orchestration/workflow-agent-session-index.d.ts +3 -2
  19. package/dist/plugins/approval-ask-store-sql.js +1 -1
  20. package/dist/plugins/blob-backend.d.ts +6 -7
  21. package/dist/plugins/checkpoint-store-sql.d.ts +13 -10
  22. package/dist/plugins/checkpoint-store-sql.js +6 -24
  23. package/dist/plugins/file-run-store.d.ts +18 -0
  24. package/dist/plugins/file-run-store.js +27 -0
  25. package/dist/plugins/one-time-migrations.d.ts +66 -0
  26. package/dist/plugins/one-time-migrations.js +193 -0
  27. package/dist/plugins/pg-pool.d.ts +6 -0
  28. package/dist/plugins/pg-pool.js +5 -14
  29. package/dist/plugins/retention-store-sql.js +0 -1
  30. package/dist/plugins/store-backend.d.ts +11 -15
  31. package/dist/plugins/store-backend.js +7 -26
  32. package/dist/plugins/task-attachment-store.d.ts +2 -2
  33. package/dist/plugins/tidb-pool.js +11 -23
  34. package/dist/plugins/workflow-run-store-sql.d.ts +9 -0
  35. package/dist/task-cwd.d.ts +12 -1
  36. package/dist/task-settings.d.ts +6 -0
  37. package/dist/trace/core-keyset-guard.d.ts +11 -2
  38. package/dist/trace/project.d.ts +28 -29
  39. package/dist/trace/project.js +12 -17
  40. package/dist/trace/redact.js +5 -1
  41. package/package.json +2 -2
package/USAGE.md CHANGED
@@ -974,6 +974,14 @@ curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' #
974
974
  > (仅 contents/pull-requests 只读)。未知词拒启;`GIT_API_BASEURL` 指向 github.com 族而 kind 仍是
975
975
  > gitea 也拒启并指路(Gitea 形状对 GitHub 恒 404,启动即报优于运行时逐调用 404)。
976
976
 
977
+ > **⚠️ 升级注记(7.74.0):升级前把未决的 workflow park 决掉。** 本版起引擎对**任何**没有 `parks` 这一位的
978
+ > 历史 workflow run 记录,在派发任何 agent **之前**整条拒绝 `Workflow({resumeFromRunId})`
979
+ > (`workflow.park_truth_unreadable`,`detail.reason: "parks_unreadable"`)。那一位是本版引擎才开始写的,所以
980
+ > 所有**升级前**写下的 workflow run 记录都算数。这是刻意的:把旧记录读成「这条 run 没有 park」会让一只已经
981
+ > 停在人工审批上的子代被**重跑一遍**(付费 + 外部副作用)。**处置**:升级窗内把未决的 park 走
982
+ > `POST /v1/approvals/:sessionId/decide` 决掉,或对那些 run 起一条**新**的 run —— 不要 resume 旧记录。存量行
983
+ > 不需要清理(只在被 resume 时读,拒绝是逐次的、不留残留状态)。**本版不做兼容读腿。**
984
+
977
985
  > **升级注记(7.7.0)**:本版起 `code-review` / `scan` / `discuss`(7.28.0 前叫 `team`)/ `toolset: repo-readonly|none` 的场景改跑
978
986
  > 无执行环境的引擎。**升级前**若还有这些场景的 durable 挂起任务(需同时满足:配了 `REMOTE_EXEC`、开了
979
987
  > `DURABLE_APPROVAL`、任务已 park),它们在新版上续跑会以 409 `checkpoint.unsupported_version` 响亮失败
@@ -160,10 +160,6 @@ export interface RunnerDepsCtx {
160
160
  /** [ref] per-edited-file rewind history (core `RunnerDeps.fileHistoryStore`; replaces the retired E19
161
161
  * whole-tree snapshot seat — core 94632197 renamed the seat, not a coexistence). WIRING IS THE OPT-IN. */
162
162
  fileHistoryStore: ReturnType<StoreBackend["fileHistory"]> | undefined;
163
- /** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): lets the rewind refusal
164
- * distinguish "never existed / reaped" from "exists in the RETIRED whole-tree epoch". Consulted only on
165
- * the refusal path; diagnostic input, never a control input (a bad probe cannot change the outcome). */
166
- legacyRewindBoundaryProbe: RunnerDeps["legacyRewindBoundaryProbe"];
167
163
  /** [ref] 车二:持久化权限规则店 provider(core `RunnerDeps.permissionRuleStore`)。缺席 ⇒ 引擎的
168
164
  * `permissionRules.storeWired` 如实报 false、`AskRequest.ruleOffers` 不铸(诚实缺席)。 */
169
165
  permissionRuleStore: RunnerDeps["permissionRuleStore"];
@@ -91,7 +91,7 @@ export function createSharedRunnerDeps(ctx) {
91
91
  return shared;
92
92
  }
93
93
  export function createRunnerDeps(ctx) {
94
- const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, legacyRewindBoundaryProbe, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
94
+ const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
95
95
  const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
96
96
  const sharedRunnerDeps = createSharedRunnerDeps(ctx);
97
97
  const workflowModelAllowlist = workflowModelAllowlistFor(config);
@@ -185,7 +185,6 @@ export function createRunnerDeps(ctx) {
185
185
  : undefined,
186
186
  runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
187
187
  fileHistoryStore: fileHistoryStore ? fileHistoryStore : undefined,
188
- legacyRewindBoundaryProbe: legacyRewindBoundaryProbe ? legacyRewindBoundaryProbe : undefined,
189
188
  permissionRuleStore: permissionRuleStore ? permissionRuleStore : undefined,
190
189
  onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
191
190
  loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
@@ -77,8 +77,6 @@ export function createSessionFaces(ctx) {
77
77
  });
78
78
  }
79
79
  }
80
- if (backend?.legacySnapshotPurge)
81
- await backend.legacySnapshotPurge(sessionId);
82
80
  if (workflowCompletionInbox)
83
81
  await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
84
82
  await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
@@ -15,6 +15,7 @@ import { PgRosterStore, TiDBRosterStore, ensurePgRosterSchema, ensureTiDBRosterS
15
15
  import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store.js";
16
16
  import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
17
17
  import { createSessionStore } from "../plugins/session-store.js";
18
+ import { logOneTimeMigrations } from "../plugins/one-time-migrations.js";
18
19
  import { createTaskListLane } from "./task-list-lane.js";
19
20
  import { ensurePgTaskListSchema, ensureTiDBTaskListSchema } from "../plugins/task-list-store-sql.js";
20
21
  import { assertDeviceAuditRebindEventSchema, ensurePgDeviceSchema, ensureTiDBDeviceSchema } from "../plugins/device-store-sql.js";
@@ -85,23 +86,9 @@ export async function openStores(ctx) {
85
86
  await assertFileHistorySchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
86
87
  else if (mysqlPool)
87
88
  await assertFileHistorySchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
88
- {
89
- const legacyProbeSql = "SELECT 1 FROM snapshot_manifest LIMIT 1";
90
- const hasLegacyRows = await (pgPool
91
- ? pgPool.query(legacyProbeSql).then((r) => r.rows.length > 0)
92
- : mysqlPool
93
- ? mysqlPool.query(legacyProbeSql).then(([rows]) => rows.length > 0)
94
- : Promise.resolve(false)).catch(() => false);
95
- if (hasLegacyRows) {
96
- logger.warn("legacy_snapshot_epoch_rows_present", {
97
- note: "rows from the RETIRED whole-tree snapshot epoch (snapshot_manifest/snapshot_blob) exist. They are NOT " +
98
- "migrated (whole-tree manifests and per-file history are structurally different); rewind refusals for " +
99
- "legacy-era boundaries name the retired epoch via the boundary probe; the retention lane keeps sweeping " +
100
- "these rows by session, and the maintenance tick GCs their orphaned blob bytes.",
101
- });
102
- }
103
- }
104
89
  }
90
+ if (backend)
91
+ logOneTimeMigrations(logger, backend.kind, await backend.runOneTimeMigrations());
105
92
  if ((process.env.REWIND_SNAPSHOT_MAX_MB ?? "") !== "") {
106
93
  logger.warn("rewind_snapshot_max_mb_retired", {
107
94
  note: "REWIND_SNAPSHOT_MAX_MB no longer has an effect: the whole-tree snapshot epoch is retired (S-15); per-edited-file history captures only files the agent touches, with no tree walk and no size gate. Remove the env var. Per-blob SQL sizing is governed by SNAPSHOT_BLOB_SQL_MAX_BYTES / object storage as before.",
@@ -861,7 +861,7 @@ export interface ServiceConfigFlat {
861
861
  dbQueryTimeoutMs?: number;
862
862
  /** Object-store offload for the E19/2c file-snapshot BLOBS (clay 2026-06-26). The conversation TRANSCRIPT stays
863
863
  * DB-first; ONLY the snapshot BYTES (binary file content, up to ~64 MiB/file — a perf-suboptimal TiDB blob row) move
864
- * to MinIO. The snapshot MANIFEST (relPath→hash) STAYS in SQL (`snapshot_manifest`), so the transcript hot read never
864
+ * to MinIO. The file-history MANIFEST (relPath→hash) STAYS in SQL, so the transcript hot read never
865
865
  * touches MinIO. Built (by the tidb/pg StoreBackend) into a MinioBlobBackend iff MINIO_ENDPOINT + MINIO_ACCESS_KEY +
866
866
  * MINIO_SECRET_KEY are all set — else the file-snapshot stores keep the SQL `snapshot_blob` default (byte-identical to
867
867
  * today). Reuses the SAME MinIO env names as the k8s workspace-snapshot lane (zero remap). */
@@ -118,8 +118,10 @@ export declare function createDurableQuestionGate(live: LiveQuestionFace | undef
118
118
  * · **覆盖面(2026-08-08 按 core 5.19.0 [ref] 校正;旧文见下方「历史」段)**:本层只在
119
119
  * `ToolCallRequest.cwd` **缺席**那一形上说话。core 5.19.0 起每条路径解析型守卫按 `req.cwd ?? rootPath`
120
120
  * 解析写目标(dist `core/sensitive-path-policy.js`),而 `canonicalizeTarget` 拿到 baseCwd 后会先把
121
- * 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `baseCwd && !isAbsolutePathForm(...)`
122
- * 分支)—— 本层的 `absolutePath` 对绝对形一律报错弃权 ⇒ **有戳时本层自动让位**,由真身那一层
121
+ * 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `canonicalizeTarget`,7.17.0 起该分支是
122
+ * `baseCwd && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled`
123
+ * —— [ref] 把「算不算绝对 / 怎么拼」双双改成**按树的家族**判,旧注引的 `isAbsolutePathForm` 那一行已不是
124
+ * 这里的实现行;**结论不变**,本层仍是「有戳时自动让位」)—— 本层的 `absolutePath` 对绝对形一律报错弃权 ⇒ **有戳时本层自动让位**,由真身那一层
123
125
  * (沙箱 `DeferredSandboxPathEnv` / host `NodeExecutionEnv`)按活 cwd 裁决,cwd 里的守卫段现在真看得见。
124
126
  * ⇒ 本层今天的射程 = 「引擎没盖戳」的调用:相对形**自身拼写**里带守卫段的那一类(`Write(".env")`、
125
127
  * `Write("cfg/.ssh/id_rsa")`),仍由本层 fail-closed 兜住。**不删臂**:让位与冗余不是一回事——
@@ -137,8 +139,25 @@ export declare function createDurableQuestionGate(live: LiveQuestionFace | undef
137
139
  * [ref],已在 5.19.0 到货**,两条钉按上述翻面完毕。另一条备选收口(守卫集开启即把沙箱 lane 相对写
138
140
  * 一律 deny,有真受损方且无对应旋钮)因此作废,无需部署方拍板。
139
141
  */
142
+ /**
143
+ * 沙箱 lane 两层守卫的**显式**分工(core 7.17.1 起必须显式,不能再靠「让 `absolutePath` 失败」隐式弃权):
144
+ * `createSensitivePathPolicy` 对 canon 失败自 7.17.1 一律 `deny` 并点名 cause(它闭掉的正是本仓 [ref]② 上板的
145
+ * 自拒形放行洞)。旧形里两层都拿「判不了 ⇒ core 判 allow」当让位:真身层对**缺戳相对形**报错(沙箱 cwd 不是
146
+ * 门能猜的,[ref] 裁定 1)、词法层对**绝对形**报错 —— 新引擎上两处都翻成硬拒,沙箱 lane 的每一次写都被拒(提货批
147
+ * 14 格红)。让位与「判不了」是两件事:让位是**本层的**决定,要由本层自己说出来。分工(与两层类注一致,一字未改):
148
+ * · 真身层(`DeferredSandboxPathEnv`)判 **绝对形 / 带 `req.cwd` 戳的相对形**(core 按戳解析进真身);
149
+ * · 词法层(`RelativeTargetLexicalEnv`)判 **缺戳相对形**(自身拼写里的守卫段,fail-closed);
150
+ * 两层射程互补、无重叠、无缝隙;各自射程外**显式 allow**(= 让位给另一层),canon 失败留给真的判不了的形
151
+ * (UNC/NUL/越根)—— 那正是 7.17.1 要 deny 的。提取写目标用 core 自家 `protectivePathTargetOf` + `pathTargetValue`
152
+ * (与 core 守卫策略同一只提取器,零复刻);「算不算绝对」用 core `isAbsoluteForFamily(undefined, …)`(沙箱 lane
153
+ * 无 root/cwd 声明 ⇒ 家族 undefined ⇒ 只认 `/`-rooted,与引擎同一律)。
154
+ */
155
+ type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
156
+ /** 只在 `form` 那一类写目标上说话,其余显式 allow(让位);非写调用与提不出目标的调用原样交给 policy(它自己判 allow)。 */
157
+ export declare function scopedToWriteTargetForm(policy: ToolPolicy, form: WriteTargetForm): ToolPolicy;
140
158
  export declare class RelativeTargetLexicalEnv extends StubExecutionEnv {
141
- /** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(= 弃权,见类注)。 */
159
+ /** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(fail-closed 守底:`relativeTargetsOnly` 已把这些形挡在外面,
160
+ * 这里不会再被绝对形命中;若有人绕过包装直接用本 env,报错在 7.17.1 上读作 deny,方向仍是保守)。 */
142
161
  absolutePath(path: string): Promise<Result<string, FileError>>;
143
162
  /** 恒「不存在」⇒ core 的 `canonicalizeNewPath` 逐级回退,最终把词法归一形当 canonical key 交给段匹配。
144
163
  * 这里绝不能报错:报错会被 core 读成 `unresolvedSymlink` 而对**每一个**相对目标 deny(含普通文件)。 */
@@ -205,4 +224,5 @@ export declare function createDeploymentGovernanceInputs(config: DeploymentGover
205
224
  * 本口只按调用方给的形铸策略,绝不替它判「这个部署该不该有门」。
206
225
  */
207
226
  export declare function createApprovalBaselinePolicy(config: ApprovalBaselineConfigView, durable?: DurableApprovalSeat): ToolPolicy;
227
+ export {};
208
228
  //# sourceMappingURL=deployment-governance.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { join, posix } from "node:path";
2
- import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, ok, } from "@sema-agent/core";
2
+ import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, isAbsoluteForFamily, isProtectedWrite, ok, pathTargetValue, protectivePathTargetOf, } from "@sema-agent/core";
3
3
  import { createDurableAskPolicy } from "./approval.js";
4
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
5
5
  export function createDurableQuestionGate(live) {
@@ -15,6 +15,27 @@ export function createDurableQuestionGate(live) {
15
15
  },
16
16
  };
17
17
  }
18
+ function writeTargetFormOf(req) {
19
+ if (!isProtectedWrite(req))
20
+ return undefined;
21
+ const pt = protectivePathTargetOf(req);
22
+ const target = pt === undefined ? undefined : pathTargetValue(req.args, pt);
23
+ if (typeof target !== "string" || target.length === 0)
24
+ return undefined;
25
+ if (req.cwd !== undefined || target.includes("\u0000") || isAbsoluteForFamily(undefined, target))
26
+ return "absolute-or-stamped";
27
+ return "unstamped-relative";
28
+ }
29
+ export function scopedToWriteTargetForm(policy, form) {
30
+ return {
31
+ async check(req, signal) {
32
+ const actual = writeTargetFormOf(req);
33
+ if (actual !== undefined && actual !== form)
34
+ return { action: "allow" };
35
+ return policy.check(req, signal);
36
+ },
37
+ };
38
+ }
18
39
  export class RelativeTargetLexicalEnv extends StubExecutionEnv {
19
40
  absolutePath(path) {
20
41
  if (path.length === 0 || path.startsWith("/") || path.includes("\u0000")) {
@@ -64,7 +85,7 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
64
85
  const withTranscripts = (p) => (transcriptGuard ? combinePolicies(p, transcriptGuard) : p);
65
86
  if (pathAdjudication.cwd !== undefined)
66
87
  return withTranscripts(realTarget);
67
- return withTranscripts(combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt })));
88
+ return withTranscripts(combinePolicies(scopedToWriteTargetForm(realTarget, "absolute-or-stamped"), scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative")));
68
89
  })()
69
90
  : undefined;
70
91
  return {
@@ -83,7 +83,14 @@ export declare function ensureScratchpadDir(localDataRoot: string, sessionId: st
83
83
  * - mkdir -p 确保存在(gate 的 canonicalize 与 core 根围栏都要真目录)。
84
84
  * 敏感路径的最后一道**不在**写门折叠里([ref] 起搬家):守卫集 `SENSITIVE_WRITE_PATTERNS` 由
85
85
  * governance 拍(boot/resolve-spec.ts → applyRuntimeGovernance)无条件铸成 DENY 基线,经 tightenTaskSpec
86
- * 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。 */
86
+ * 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。
87
+ *
88
+ * 🔴 **接缝登记(7.74.0 / core 7.17.0 [ref];今日无可达分歧)**:下面那道「绝对路径」闸回答的是「引擎眼里
89
+ * 这条豁免目录算不算绝对」——引擎自本版起按**树的家族**判(`isAbsoluteForFamily`,家族从树的 root/cwd 拼法
90
+ * 读,不看宿主 OS),本函数用的是 Node 宿主口径。**posix 家族两者逐字同判**(2026-09-12 实测六形全同),
91
+ * 而本闸又只在 `hostSemanticsLane` 上场(路径就是这台 worker 自己的盘)⇒ 今天零分歧,不改。**分歧只在 win
92
+ * 家族的 `/`-开头无盘符那一类**(Node 说绝对、引擎说未完全限定)。判据与坐标属主见 `task-cwd.ts` 的
93
+ * {@link import("./task-cwd.js").isValidCwd} 顶注(同一条接缝,那里是唯一成文点,这里不复述第二份)。 */
87
94
  export declare function acceptShellScratchpadDir(raw: unknown, opts: {
88
95
  requirePrincipal: boolean;
89
96
  hostSemanticsLane: boolean;
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner, EngineNotice } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -180,13 +180,14 @@ export interface DriveResumeArgs {
180
180
  * 🔴 **TRUSTED**:它绝不是 `TaskSpec` 字段、也绝不是工具参数(模型不能决定一次审批)。唯一的铸点是
181
181
  * `/decide` 的第三条车道(判据属主 = `parked-decide.ts` 的 `planWorkflowParkedDecide` 顶注)。
182
182
  * 缺席 ⇒ 本位对这条腿是完整 no-op,与本批之前逐字相同。
183
- */
184
- workflowParkedResume?: ReadonlyArray<{
185
- runId: string;
186
- token: string;
187
- outcome: ResumeOutcome;
188
- inheritedGate?: unknown;
189
- }>;
183
+ *
184
+ * 🔴 **形不手抄:直接是 core 的那一位**(`NonNullable<RunInternals["workflowParkedResume"]>`)。手抄一份
185
+ * 结构等价的字面(本位 7.73.0 及以前就是)会让 `token: string` / `inheritedGate: unknown` 这种**更宽**的
186
+ * 形悄悄通过,再靠下游一个 `as NonNullable<RunInternals[...]>` 把它洗进 core 的 branded 位 —— 那个转型
187
+ * 正是「边界必 schema、禁自铸」要挡的东西(裸 string 当赎回键传进去,编译期一个字都不响)。派生之后
188
+ * core 改这一位(本版就把 `WorkflowParkedResume.outcome` 改成了可选)在本仓是**编译事件**,不是靠人去
189
+ * 对表的一行注。 */
190
+ workflowParkedResume?: NonNullable<RunInternals["workflowParkedResume"]>;
190
191
  /** [ref]([ref] Inkglow 案 → [ref]① 定谳):**200 受理语义**——调用方声明「我的消费方按 poll/SSE 跟
191
192
  * 终态,别让 HTTP 等模型往返」。在场为 `true` 时本腿在**受理点**(`resumeStream` 解析之后)立刻把
192
193
  * `200 {taskId, status:"resuming", bindingEnforced:true}` 交回调用方,模型腿(流迭代 + 行驱动)继续
@@ -1,4 +1,4 @@
1
- import { screenedStoredGate } from "../../trace/project.js";
1
+ import { screenedGate } from "../../trace/project.js";
2
2
  import { personDecision } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
@@ -87,7 +87,7 @@ async function decideIdempotentReplay(cs, args) {
87
87
  return undefined;
88
88
  }
89
89
  }
90
- return screenedStoredGate(first);
90
+ return screenedGate(first);
91
91
  })();
92
92
  return {
93
93
  status: 200,
@@ -789,7 +789,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
789
789
  taskId,
790
790
  status: run.status,
791
791
  turnCut: haltReceipt.turnCut,
792
- note: "halt accepted — cut + stop: turnCut reports whether THIS call cut a live turn (repeat calls answer turnCut:false once the stop is latched). The run now ends at this boundary as status:\"completed\" with haltedByUser:true — resume by submitting the next objective on the same session. Each real cut is marked by a task.turn_interrupted event with detail.cause:\"user_halt\"; a halt racing the run's natural last moments reports the natural completion, still carrying the haltedByUser seat.",
792
+ note: "halt accepted — cut + stop: turnCut reports whether THIS call cut a live turn (repeat calls answer turnCut:false once the stop is latched). A halt that CONSUMES (it cut a turn, or stopped the loop at a boundary) ends the run at that boundary as status:\"completed\" with haltedByUser:true — resume by submitting the next objective on the same session. Each real cut is marked by a task.turn_interrupted event with detail.cause:\"user_halt\". A halt racing the run's natural last moments consumes NOTHING: the run's own ending stands (completed with the answer as result, or failed when the model's own abort owns the final) and the result does NOT carry haltedByUser — the only trace of that halt is a task.halt_unconsumed engine notice.",
793
793
  });
794
794
  return;
795
795
  }
@@ -245,6 +245,9 @@ function projectLegApprovalKeys(agent) {
245
245
  ...(approvalWaitedMs !== undefined ? { approvalWaitedMs } : {}),
246
246
  };
247
247
  }
248
+ function projectParkRow(p) {
249
+ return { callKey: p.callKey, sessionId: p.sessionId, originRunId: p.originRunId };
250
+ }
248
251
  function summarizeWorkflowDetail(run) {
249
252
  const isDone = (s) => s === "completed" || s === "failed";
250
253
  const durationOf = (a) => a.startedAt === undefined || a.endedAt === undefined ? undefined : a.endedAt - a.startedAt;
@@ -339,6 +342,7 @@ function summarizeWorkflowDetail(run) {
339
342
  rev: run.rev,
340
343
  ...(run.error !== undefined ? { error: redactErrorMessage(run.error) ?? "" } : {}),
341
344
  ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
345
+ ...(run.parks !== undefined ? { parks: run.parks.map(projectParkRow) } : {}),
342
346
  ...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
343
347
  phases,
344
348
  ...(unphasedView ? { unphased: unphasedView } : {}),
@@ -2525,6 +2525,7 @@ const WORKER_SWAP_REDEEMABLE = {
2525
2525
  env_factory_missing: true,
2526
2526
  governed_unwired: true,
2527
2527
  origin_missing: true,
2528
+ seat_retired: true,
2528
2529
  ledger_event_unknown: true,
2529
2530
  real_approval_damaged: false,
2530
2531
  real_approval_forged: false,
package/dist/main.js CHANGED
@@ -277,7 +277,6 @@ async function main() {
277
277
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
278
278
  sessionCaptureRecords,
279
279
  memorySyncRunner, toolResultStore: runnerOffloadStore, checkpointStore, sessionPolicyStore, runtimeCapsResolver, fileHistoryStore,
280
- legacyRewindBoundaryProbe: backend?.legacyRewindBoundaryProbe ? backend.legacyRewindBoundaryProbe.bind(backend) : undefined,
281
280
  permissionRuleStore: permissionRuleProvider,
282
281
  executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
283
282
  workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
19
19
  };
20
20
  readonly "server.trace.gate-outcome-defective": {
21
21
  readonly cls: "F";
22
- readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
22
+ readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)同走这一只筛,零双读 —— 本服务上一版写下的退役词旧拼法由启动期的一次性迁移在库里原地改完,读腿因此没有第二种读法):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
23
23
  };
24
24
  readonly "server.trace.tool-roster-malformed": {
25
25
  readonly cls: "F";
@@ -10,7 +10,7 @@ export const FAIL_OPEN_TAGS = {
10
10
  },
11
11
  "server.trace.gate-outcome-defective": {
12
12
  cls: "F",
13
- note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
13
+ note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)同走这一只筛,零双读 —— 本服务上一版写下的退役词旧拼法由启动期的一次性迁移在库里原地改完,读腿因此没有第二种读法):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
14
14
  },
15
15
  "server.trace.tool-roster-malformed": {
16
16
  cls: "F",
@@ -1,6 +1,7 @@
1
1
  /** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
2
2
  export interface WorkflowAgentSessionRow {
3
- /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = checkpoint 的 `sourceTaskId`)。 */
3
+ /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = 那条 checkpoint 行的 `sessionId`;
4
+ * **不是** `sourceTaskId` —— 后者在 core 契约上是 ECHO-ONLY、明令不可作门输入)。 */
4
5
  sessionId: string;
5
6
  /** 它所属的 workflow run(`w*`)。 */
6
7
  runId: string;
@@ -49,7 +50,7 @@ export declare const PARKED_IDENTITY_LABEL_MAX = 200;
49
50
  * 🔴 **一条 parked 行的「恢复身份」形**(S-192)—— 店层瘦身时**最后才放弃**的那一份。
50
51
  *
51
52
  * 它是一条**合法的** `WorkflowAgentRun`(core 的必填四位 `label`/`callKey`/`status`/`queuedAt` 齐全),
52
- * 加上赎回路由要的两位(`sessionId` = checkpoint 的 `sourceTaskId`,`parkedCheckpointToken` = 赎回键)。
53
+ * 加上赎回路由要的两位(`sessionId` = 那条 checkpoint 行的 `sessionId`,`parkedCheckpointToken` = 赎回键)。
53
54
  * 展示载荷(`prompt`/`output`/`activity`/`errorMessage`/`phase`/`model`…)一律不带 —— 那正是被裁掉的东西。
54
55
  *
55
56
  * 返回的是**普通数据**(build*),消费方是 `plugins/workflow-run-store-sql.ts` 的 `slimOversizeRun`。
@@ -96,7 +96,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
96
96
  gate_bound_call_id VARCHAR(255) NULL,
97
97
  gate_bound_input_hash VARCHAR(255) NULL,
98
98
  idempotency_key VARCHAR(255) NULL,
99
- -- LONGTEXT 照 image-bake/background-agent 的 pending_steer 先例:重放帧载荷可能超 64K TEXT 墙。
99
+ -- LONGTEXT 照 image-bake/background-agent 的载荷列先例:重放帧载荷可能超 64K TEXT 墙。
100
100
  card_json LONGTEXT NOT NULL,
101
101
  -- #363 规则车道素材两列(语义/类型理由逐字见 AskRow.ruleCommand / ruleScopeRoot 的顶注)。
102
102
  -- rule_command 与 permission_rule_approval.command 同款 LONGTEXT:命令行没有入口上限,VARCHAR
@@ -39,9 +39,8 @@ export interface BlobBackend {
39
39
  * dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated
40
40
  * real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct).
41
41
  *
42
- * NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the `NOT IN (SELECT … snapshot_manifest)` orphan
43
- * filter (that reference check stays in the file-snapshot store, which computes the orphan hash SET from the manifest
44
- * and hands it here). The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
42
+ * NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the reference (`not in the manifest`) filter: that
43
+ * check stays in the owning store, which computes the orphan hash SET from its own domain's manifest and hands it here. The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
45
44
  * that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is
46
45
  * harmless — the next sweep collects it).
47
46
  */
@@ -64,10 +63,10 @@ export declare class SqlBlobBackend implements BlobBackend {
64
63
  getBlob(hash: string): Promise<Uint8Array | undefined>;
65
64
  /** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
66
65
  hasBlobs(hashes: string[]): Promise<Set<string>>;
67
- /** Byte-delete the given hashes (the orphan SET is computed by the caller from snapshot_manifest). Retains the
68
- * grace-window guard so a just-written blob a concurrent in-flight snapshot needs is never delete-in-use — VERBATIM
69
- * the tidb/pg stores' gcOrphanBlobs delete, minus the `NOT IN (SELECT … manifest)` clause (the caller already
70
- * excluded referenced hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
66
+ /** Byte-delete the given hashes (the orphan SET is computed by the CALLER from its own domain's manifest). Retains
67
+ * the grace-window guard so a just-written blob a concurrent in-flight write needs is never delete-in-use — VERBATIM
68
+ * the tidb/pg stores' gcOrphanBlobs delete, minus the reference clause (the caller already excluded referenced
69
+ * hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
71
70
  deleteBlobs(hashes: string[]): Promise<number>;
72
71
  }
73
72
  /** SHARED index op(workspace 浏览面 #3,[ref]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len`
@@ -369,9 +369,9 @@ export declare const DECIDED_REPLAY_SCAN_CAP = 8;
369
369
  * (plan_review / dry_run_review)的判词,那些门的决议入口不是本腿,回放它们等于跨门作答。
370
370
  */
371
371
  export declare function approvalDecisionOfWinner(winner: ResolvedOutcome | undefined, boundCallId: string): "approve" | "deny" | null;
372
- /** `pending_steer` 列承载的两个 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
373
- * 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它们)。 */
374
- type SteerColumnState = Pick<CheckpointState, "pendingSteer" | "pendingSteerQueue">;
372
+ /** `pending_steer_queue` 列承载的 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
373
+ * 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它)。 */
374
+ type SteerColumnState = Pick<CheckpointState, "pendingSteerQueue">;
375
375
  /** 读列结果。`lossy` = 列里有东西但**没能完整读出来**(JSON 坏 / 条目形不认)。两个调用点对它的处置
376
376
  * 刻意相反,见各自的旁注:`get()` 容忍(坏列不许把一条挂起的 run 变成不可 resume),`setPendingSteer`
377
377
  * fail-loud(往一份读不全的队列上追加 = 把别人 park 的指令悄悄写没,正是 [ref] 要消灭的那件事)。 */
@@ -380,14 +380,17 @@ interface SteerColumnRead {
380
380
  lossy: boolean;
381
381
  }
382
382
  /**
383
- * 两个 steering 列 → `{pendingSteer?, pendingSteerQueue?}`(= core `appendPendingSteer`/`readPendingSteerQueue`
384
- * 的入参形)。**两列各管一形**,不是一列两形:
385
- * · `pending_steer`(旧列,本服务只读不写)—— 裸单座 `{text, trusted}`。来源有二:队列化之前的存量行,
386
- * 以及**滚动升级窗里仍在跑的旧副本**写下的座。core 的 `readPendingSteerQueue` 把它折成 member 0。
387
- * · `pending_steer_queue`(新列)—— `PendingSteerEntry[]`。
388
- * 任一列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
383
+ * steering 列 → `{pendingSteerQueue?}`(= core `appendPendingSteer`/`readPendingSteerQueue` 的入参形)。
384
+ * **一列一形**:`pending_steer_queue` 存 `PendingSteerEntry[]`。
385
+ *
386
+ * 🔴 7.74.0 起只剩这**一**列。队列化之前的裸单座列 `pending_steer` 随它的席位一起退役了(core 7.17.0
387
+ * [ref] d:引擎对带席的行 PRE-CAS 拒 `seat_retired`),存量列由启动期的一次性迁移删掉
388
+ * (`plugins/one-time-migrations.ts`:列上还有非空值 ⇒ **响亮拒启**,清空后 `DROP COLUMN`)。
389
+ * ⇒ 这里刻意**没有**第二条读腿:双读臂的寿命就是那一次迁移,不是永远。
390
+ *
391
+ * 列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
389
392
  */
390
- export declare function readPendingSteerColumns(seatRaw: unknown, queueRaw: unknown): SteerColumnRead;
393
+ export declare function readPendingSteerQueueColumn(queueRaw: unknown): SteerColumnRead;
391
394
  /** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
392
395
  export declare class SqlCheckpointStore implements CheckpointStore {
393
396
  protected readonly db: SqlDriver;
@@ -210,15 +210,6 @@ function readActorAssertion(v) {
210
210
  const issuer = "issuer" in v ? v.issuer : undefined;
211
211
  return { id, hostAsserted, ...(typeof issuer === "string" ? { issuer } : {}) };
212
212
  }
213
- function readLegacySeat(v) {
214
- if (typeof v !== "object" || v === null)
215
- return undefined;
216
- const text = "text" in v ? v.text : undefined;
217
- const trusted = "trusted" in v ? v.trusted : undefined;
218
- if (typeof text !== "string" || typeof trusted !== "boolean")
219
- return undefined;
220
- return { text, trusted };
221
- }
222
213
  function readQueueEntry(v) {
223
214
  if (typeof v !== "object" || v === null)
224
215
  return undefined;
@@ -243,7 +234,7 @@ function readQueueEntry(v) {
243
234
  ...(priority === "now" || priority === "next" || priority === "later" ? { priority } : {}),
244
235
  };
245
236
  }
246
- export function readPendingSteerColumns(seatRaw, queueRaw) {
237
+ export function readPendingSteerQueueColumn(queueRaw) {
247
238
  let lossy = false;
248
239
  const parse = (raw) => {
249
240
  if (raw === null || raw === undefined)
@@ -258,10 +249,6 @@ export function readPendingSteerColumns(seatRaw, queueRaw) {
258
249
  return undefined;
259
250
  }
260
251
  };
261
- const seatParsed = parse(seatRaw);
262
- const seat = seatParsed === undefined ? undefined : readLegacySeat(seatParsed);
263
- if (seatParsed !== undefined && seat === undefined)
264
- lossy = true;
265
252
  const queueParsed = parse(queueRaw);
266
253
  let queue;
267
254
  if (queueParsed !== undefined) {
@@ -278,10 +265,7 @@ export function readPendingSteerColumns(seatRaw, queueRaw) {
278
265
  }
279
266
  }
280
267
  }
281
- return {
282
- state: { ...(seat !== undefined ? { pendingSteer: seat } : {}), ...(queue !== undefined ? { pendingSteerQueue: queue } : {}) },
283
- lossy,
284
- };
268
+ return { state: { ...(queue !== undefined ? { pendingSteerQueue: queue } : {}) }, lossy };
285
269
  }
286
270
  const STEER_CAS_ATTEMPTS = 8;
287
271
  const EXECUTION_RECORD_CAS_ATTEMPTS = 8;
@@ -375,7 +359,7 @@ export class SqlCheckpointStore {
375
359
  }
376
360
  }
377
361
  async get(token) {
378
- const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = $1"), [token]);
362
+ const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = $1"), [token]);
379
363
  const r = rows[0];
380
364
  if (!r)
381
365
  return null;
@@ -396,13 +380,11 @@ export class SqlCheckpointStore {
396
380
  if (r.execution_at_ms != null)
397
381
  cp.executionAtMs = Number(r.execution_at_ms);
398
382
  }
399
- const steer = readPendingSteerColumns(r.pending_steer, r.pending_steer_queue);
383
+ const steer = readPendingSteerQueueColumn(r.pending_steer_queue);
400
384
  if (steer.lossy) {
401
385
  this.logger?.info?.("checkpoint_pending_steer_unreadable", { tokenFp: tokenFingerprint(token) });
402
386
  }
403
387
  if (cp.state) {
404
- if (steer.state.pendingSteer !== undefined)
405
- cp.state.pendingSteer = steer.state.pendingSteer;
406
388
  if (steer.state.pendingSteerQueue !== undefined)
407
389
  cp.state.pendingSteerQueue = steer.state.pendingSteerQueue;
408
390
  }
@@ -454,11 +436,11 @@ export class SqlCheckpointStore {
454
436
  async setPendingSteer(token, scope, steer) {
455
437
  const clean = validatePendingSteer(steer);
456
438
  for (let attempt = 0; attempt < STEER_CAS_ATTEMPTS; attempt++) {
457
- const { rows } = await this.db.query(this.q("SELECT status, pending_steer, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = ? AND scope = ?", "SELECT status, pending_steer, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = $1 AND scope = $2"), [token, scope]);
439
+ const { rows } = await this.db.query(this.q("SELECT status, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = ? AND scope = ?", "SELECT status, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = $1 AND scope = $2"), [token, scope]);
458
440
  const row = rows[0];
459
441
  if (!row || row.status !== "pending")
460
442
  return false;
461
- const read = readPendingSteerColumns(row.pending_steer, row.pending_steer_queue);
443
+ const read = readPendingSteerQueueColumn(row.pending_steer_queue);
462
444
  if (read.lossy) {
463
445
  throw new Error(`the parked steering queue on checkpoint ${tokenFingerprint(token)} is unreadable — refusing to append onto it ` +
464
446
  `(appending would silently drop the entries this build cannot parse)`);
@@ -284,6 +284,24 @@ export declare class FileRunStore {
284
284
  private releaseClaim;
285
285
  /** Drop a run entirely (registry + event log + open fd + on-disk dir) — used by deleteBySession. */
286
286
  private dropRun;
287
+ /**
288
+ * 🔴 **一次性迁移(随 7.75.0 连同 `one-time-migrations.ts` 整删)** —— 退役词旧拼法的账本改写,
289
+ * SQL 两孪生那条整表 UPDATE 的 local 形。逐条走**同一只**纯改写函数,所以三条车道改出来的字节一致。
290
+ *
291
+ * 为什么是 store 上的一只动词、而不是外面一个文件走查:本店的账本有**两份**在场 —— 盘上的 jsonl
292
+ * 与 hydrate 进来的内存索引。外部走查只改得到前者,读腿读的却是后者(`getEvents` 从 Map 取),
293
+ * 于是迁移在本次进程里完全不生效、下次重启才"忽然"生效。两份一起改的唯一正确位置就是这里。
294
+ *
295
+ * 写盘用整文件原子替换(不是追加):改的是**已有行**,而 `AppendLog` 只会往后写。替换之前把**全部**
296
+ * append fd 撤掉(rename 之后旧 fd 指向的是被 unlink 的 inode,继续往那儿追加 = 之后的事件全部写进一个
297
+ * 没人看得见的文件),下一次 `appendEvent` 会按惰性规则重新开。撤 fd 复用**既有的那一只** {@link dispose}
298
+ * —— 刻意不在这里另写一段 close+catch:那会是本文件第二处同形的兜底臂,而规则要变少不要变多。
299
+ * 常态下这一撤是空操作:本动词跑在 boot 期,那时一个 append fd 都还没惰性开出来。
300
+ *
301
+ * 幂等:零命中 ⇒ 一个字节都不写、一只 fd 都不撤(第二跑零 I/O)。
302
+ * @returns 被改写的账本**行**数(不是 run 数)。
303
+ */
304
+ migrateRetiredAskOriginRows(): Promise<number>;
287
305
  /** Release every open event-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
288
306
  dispose(): void;
289
307
  }
@@ -7,6 +7,7 @@ import { terminalProjection } from "@sema-agent/core";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { notifyRunTerminal } from "../observability/run-terminal-log.js";
10
+ import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
10
11
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
11
12
  const CLAIM_TAKEOVER_ATTEMPTS = 3;
12
13
  const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_.-]+$/;
@@ -841,6 +842,32 @@ export class FileRunStore {
841
842
  }
842
843
  }
843
844
  }
845
+ async migrateRetiredAskOriginRows() {
846
+ const rewrites = new Map();
847
+ for (const [taskId, list] of this.events) {
848
+ let touched = 0;
849
+ const rows = list.map((e) => {
850
+ const data = migrateRetiredAskOrigin(e.type, e.data);
851
+ if (data === undefined)
852
+ return e;
853
+ touched++;
854
+ return { ...e, data };
855
+ });
856
+ if (touched > 0)
857
+ rewrites.set(taskId, { rows, touched });
858
+ }
859
+ if (rewrites.size === 0)
860
+ return 0;
861
+ this.dispose();
862
+ let migrated = 0;
863
+ for (const [taskId, { rows, touched }] of rewrites) {
864
+ mkdirSync(this.runDir(taskId), { recursive: true });
865
+ atomicWriteFile(this.tmpDir, this.eventsPath(taskId), rows.map((e) => `${JSON.stringify(e)}\n`).join(""));
866
+ this.events.set(taskId, rows);
867
+ migrated += touched;
868
+ }
869
+ return migrated;
870
+ }
844
871
  dispose() {
845
872
  for (const log of this.eventLogs.values()) {
846
873
  try {