@sema-agent/server 7.73.1 → 7.75.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 (46) 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 +4 -3
  15. package/dist/main.js +0 -1
  16. package/dist/observability/fail-open.d.ts +2 -2
  17. package/dist/observability/fail-open.js +2 -2
  18. package/dist/observability/run-terminal-log.d.ts +2 -1
  19. package/dist/orchestration/workflow-agent-session-index.d.ts +41 -83
  20. package/dist/orchestration/workflow-agent-session-index.js +14 -33
  21. package/dist/parked-decide.d.ts +30 -33
  22. package/dist/parked-decide.js +11 -13
  23. package/dist/plugins/approval-ask-store-sql.js +1 -1
  24. package/dist/plugins/blob-backend.d.ts +6 -7
  25. package/dist/plugins/checkpoint-store-sql.d.ts +13 -10
  26. package/dist/plugins/checkpoint-store-sql.js +6 -24
  27. package/dist/plugins/file-run-store.d.ts +18 -0
  28. package/dist/plugins/file-run-store.js +27 -0
  29. package/dist/plugins/one-time-migrations.d.ts +66 -0
  30. package/dist/plugins/one-time-migrations.js +193 -0
  31. package/dist/plugins/pg-pool.d.ts +6 -0
  32. package/dist/plugins/pg-pool.js +5 -14
  33. package/dist/plugins/retention-store-sql.js +0 -1
  34. package/dist/plugins/store-backend.d.ts +11 -15
  35. package/dist/plugins/store-backend.js +7 -26
  36. package/dist/plugins/task-attachment-store.d.ts +2 -2
  37. package/dist/plugins/tidb-pool.js +11 -23
  38. package/dist/plugins/workflow-run-store-sql.d.ts +21 -21
  39. package/dist/plugins/workflow-run-store-sql.js +2 -16
  40. package/dist/task-cwd.d.ts +12 -1
  41. package/dist/task-settings.d.ts +6 -0
  42. package/dist/trace/core-keyset-guard.d.ts +11 -2
  43. package/dist/trace/project.d.ts +31 -31
  44. package/dist/trace/project.js +12 -17
  45. package/dist/trace/redact.js +295 -16
  46. package/package.json +3 -3
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 } : {}),
@@ -3,7 +3,7 @@ import { redactErrorMessage, redactHead } from "../observability/run-terminal-lo
3
3
  import { once } from "node:events";
4
4
  import { createHash } from "node:crypto";
5
5
  import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
6
- import { decideParkedAgent, denyWorkflowParkIdentityLost, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
6
+ import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
7
7
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
8
8
  import {} from "../config-center/facade.js";
9
9
  import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
@@ -1211,9 +1211,9 @@ export function createHttpServer(rawDeps) {
1211
1211
  noteLateSettleIfAccepted(parked);
1212
1212
  return parked;
1213
1213
  }
1214
- if (origin?.kind === "workflow_identity_lost") {
1214
+ if (origin?.kind === "workflow_parks_unreadable") {
1215
1215
  void ctxPromise.catch(() => { });
1216
- return denyWorkflowParkIdentityLost(origin);
1216
+ return denyWorkflowParksUnreadable(origin);
1217
1217
  }
1218
1218
  if (origin?.kind === "workflow") {
1219
1219
  void ctxPromise.catch(() => { });
@@ -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";
@@ -199,7 +199,7 @@ export declare const FAIL_OPEN_TAGS: {
199
199
  };
200
200
  readonly "server.run-terminal.redact-threw": {
201
201
  readonly cls: "F";
202
- readonly note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(关键字/前缀臂对 ≥2^23 单段 RangeError,既有,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
202
+ readonly note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
203
203
  };
204
204
  readonly "server.run-terminal.redact-capped": {
205
205
  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",
@@ -190,7 +190,7 @@ export const FAIL_OPEN_TAGS = {
190
190
  },
191
191
  "server.run-terminal.redact-threw": {
192
192
  cls: "F",
193
- note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(关键字/前缀臂对 ≥2^23 单段 RangeError,既有,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
193
+ note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
194
194
  },
195
195
  "server.run-terminal.redact-capped": { cls: "F", note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃" },
196
196
  "server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
@@ -38,7 +38,8 @@ export declare function notifyRunTerminal(rec: RunTerminalRecord): void;
38
38
  /** S-105(test [ref] 旁生):终态**持久化**的 `errorMessage` 与日志副本同一脱敏口——provider 错误体(403 体带回显请求头/
39
39
  * 订阅链接)里的密钥形不得原文落 run 行再由 `GET /v1/runs/:id` 直出。三 run store 的 `setTerminal` 写入前同调此函数;
40
40
  * `undefined` 透传(缺席仍缺席),不截断(日志上限是日志的事,行上保留全文供取证)。 */
41
- /** r4 最终轮 [medium]:`redactSecrets` 的关键字/前缀臂对 ≥2^23 单段仍会抛 `RangeError`(既有,S-106 登记);S-105 把脱敏
41
+ /** r4 最终轮 [medium]:`redactSecrets` 会抛 `RangeError`(关键字/前缀臂那一形已由 S-219 的 `atLeast` 铸点闭掉;仍能抛的是
42
+ * JSON 秘密字段臂的转义对循环,既有未闭,S-106 登记);S-105 把脱敏
42
43
  * 挪到 `setTerminal` **入口**后,这个抛会先于 CAS/claim 释放 ⇒ run 卡 `running`、会话 claim 悬挂到 reaper。这里 **fail-closed
43
44
  * 不抛**:脱敏失败 ⇒ 落固定占位 `«redacted:oversized»`(绝不落原文,也绝不阻断终态翻转),并走既有 fail-open 遥测 tag。 */
44
45
  export declare const OVERSIZED_REDACTION_PLACEHOLDER = "\u00ABredacted:oversized\u00BB";
@@ -1,6 +1,8 @@
1
+ import type { WorkflowRun } from "@sema-agent/core";
1
2
  /** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
2
3
  export interface WorkflowAgentSessionRow {
3
- /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = checkpoint 的 `sourceTaskId`)。 */
4
+ /** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = 那条 checkpoint 行的 `sessionId`;
5
+ * **不是** `sourceTaskId` —— 后者在 core 契约上是 ECHO-ONLY、明令不可作门输入)。 */
4
6
  sessionId: string;
5
7
  /** 它所属的 workflow run(`w*`)。 */
6
8
  runId: string;
@@ -19,101 +21,57 @@ export interface WorkflowAgentSessionIndex {
19
21
  syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
20
22
  findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
21
23
  }
22
- /** 一条 wa* 行在**恢复面**上的读形(本模块两个派生点共用的最小入参 —— 只声明恢复判据用得到的位)。 */
23
- export interface ParkedAgentReadFace {
24
- label?: unknown;
25
- status?: unknown;
26
- sessionId?: unknown;
27
- callKey?: unknown;
28
- queuedAt?: unknown;
29
- startedAt?: unknown;
30
- parkedCheckpointToken?: unknown;
31
- }
32
24
  /**
33
- * 🔴 **「这一行是不是一条可赎回的 park」的唯一判据**(S-192 提取;此前只内联在
34
- * {@link buildParkedAgentSessionRows} 里,而店层的瘦身腿需要**同一条**判据 —— 两处各写一遍就是两份会漂的
35
- * 定义,而它们的消费面是同一件事:park 的赎回路由)。
25
+ * 🔴 **park 恢复身份的单一真源**(core 7.17.0 [ref])—— 引擎写在记录上的 `parks[]` 的元素形。
36
26
  *
37
- * 三位缺任何一个都不算:缺 `sessionId` 的 park 行 join 不到(checkpoint 那侧的键就是它),缺
38
- * `parkedCheckpointToken` 的行不是一条可赎回的 park(core 契约:在场 ⟺ `status:"parked"`)。
27
+ * 型从 core 的那一位取、不在本仓复述:core 往元素上加一位 ⇒ `trace/core-keyset-guard.ts` 的 ⑬b 成员门
28
+ * 与下面那只读者的构造式**同时**红,加键的人二选一,没有第三条静默的路。
39
29
  */
40
- export declare function isRecoverableParkedAgent<T extends ParkedAgentReadFace>(a: T): a is T & {
41
- sessionId: string;
42
- parkedCheckpointToken: string;
43
- };
44
- /** LLM 撰写的 `label` 在**恢复身份行**上的字符上界。core 的铸点对它零钳零脱(脚本经 `opts.label` 供,
45
- * 缺席才落 auto 名;`parked-decide.ts` 拒把它内插进可信帧时亲核过这一点),而这条行存在的理由是**装得下**
46
- * —— 把一段无界展示文本原样带进来会让「留身份」这件事本身把行撑爆。 */
47
- export declare const PARKED_IDENTITY_LABEL_MAX = 200;
30
+ export type WorkflowRunParkKey = NonNullable<WorkflowRun["parks"]>[number];
48
31
  /**
49
- * 🔴 **一条 parked 行的「恢复身份」形**(S-192)—— 店层瘦身时**最后才放弃**的那一份。
50
- *
51
- * 它是一条**合法的** `WorkflowAgentRun`(core 的必填四位 `label`/`callKey`/`status`/`queuedAt` 齐全),
52
- * 加上赎回路由要的两位(`sessionId` = checkpoint 的 `sourceTaskId`,`parkedCheckpointToken` = 赎回键)。
53
- * 展示载荷(`prompt`/`output`/`activity`/`errorMessage`/`phase`/`model`…)一律不带 —— 那正是被裁掉的东西。
32
+ * 盘上 run blob → `parks[]` 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形;与本文件
33
+ * {@link readAgentSessionLedgerLine} 同一条姿态)。
54
34
  *
55
- * 返回的是**普通数据**(build*),消费方是 `plugins/workflow-run-store-sql.ts` 的 `slimOversizeRun`。
56
- */
57
- export declare function buildParkedRecoveryIdentity(a: ParkedAgentReadFace & {
58
- sessionId: string;
59
- parkedCheckpointToken: string;
60
- }, // 准入判据 {@link isRecoverableParkedAgent} 的守卫输出:类型上绕不开
61
- labelMax?: number): Record<string, unknown>;
62
- /**
63
- * 一只 run 的 parked 行的恢复身份集(顺序 = 原行序;非 park / 无身份的行不产出)。
35
+ * 判据与 core 的 resume 准入**同源**(亲读安装树 `dist/orchestration/workflow.js:2217-2226`):
36
+ * · 这一位**不是数组**(旧引擎写的记录 / 被投影剥掉)⇒ 读不出来。core 对这样的记录整条拒 resume
37
+ * (`workflow.park_truth_unreadable` / `parks_unreadable`),本仓因此也兑付不了它 —— 答缺席,由调用方响亮拒;
38
+ * · **任一条目形不合 ⇒ 整只读不出来**(整档成败制,与本文件账本行同判)。core 对同一形抛
39
+ * `park_binding_broken` 整条拒 resume;「丢掉坏的、留下好的」会静默铸出一份**比真相少几条 park** 的行集,
40
+ * 而这只读者的消费面正是 park 的赎回路由。
64
41
  *
65
- * `labelMax` 是**展示名的预算**,不是第二种形:身份四位(`callKey` / `sessionId` /
66
- * `parkedCheckpointToken` / `status`)恒在,`label` 是这条行上唯一可弃的展示物。店的瘦身阶梯据此把
67
- * 「放弃展示名」排在「放弃身份」**之前** —— 传 0 即得只剩身份的最小行(`redactHead(x, 0)` 恒得空串)。
42
+ * ⚠️ 「读不出来」与「读得出来、只是空的」是**两个状态**:后者 = 这条 run 不负责任何 park(诚实 miss),
43
+ * 前者 = 证不了,两者的处置相反 ⇒ 缺席永远不折成 `[]`。
68
44
  */
69
- export declare function buildParkedRecoveryIdentities(agents: ReadonlyArray<ParkedAgentReadFace> | undefined, labelMax?: number): Array<Record<string, unknown>>;
70
- /**
71
- * 🔴 **park 恢复身份「保不住」的显式标记**(S-192)。
72
- *
73
- * ## 为什么它住在 run 的 blob 上、而且键名带 `server` 前缀
74
- * core 的 `WorkflowRun`(`orchestration/workflow-types.d.ts`)**没有合法座** —— 亲读全部可选成员
75
- * (`agentFailures` / `budgetOvershoot` / `timeoutInterruption` / `resume` / `journalSkips` …)全是 core 自己的
76
- * 观测席,没有一个说得了「本仓的店把这条 run 的 park 身份裁掉了」。而这条事实的**唯一知情点**是
77
- * `plugins/workflow-run-store-sql.ts` 的瘦身腿(店内),它的**唯一消费点**是 `/decide` 的 workflow 车道
78
- * (读 `WorkflowRunStore.get`)—— 两点之间已经有的、且只有的那条通路就是 run 的 blob。
79
- * (本仓的 park join 索引 {@link WorkflowAgentSessionIndex} 承载不了它:索引的**单写者**是
80
- * `JournalingWorkflowRunStore` 的 put/update 观察点,它拿到的是**内存里的整只 run**,结构上看不见店内那次
81
- * 瘦身;把这条事实塞过去要么加第二个写者、要么把店的字节上界漏进装饰器,两者都比现在这条更坏。)
82
- * 键名前缀 `server` 是**诚实标注**:它是本仓铸的位,不是 core 契约的一部分,也因此永远不会与 core 后来加的
83
- * 成员撞名。
45
+ export declare function readRunParks(run: unknown): WorkflowRunParkKey[] | undefined;
46
+ /** 派生的**唯一属主**:一只 `WorkflowRun` 的 park **路由**行。
84
47
  *
85
- * ## 闭集
86
- * `reason` 只有一个词(单一成因:店的字节上界)。`parks` 是被丢掉的 parked 行数 —— **只有计数,不带任何
87
- * 身份位**(带了就等于没裁)。
88
- */
89
- export declare const PARK_IDENTITY_LOSS_KEY = "serverParkIdentityLoss";
90
- export interface ParkIdentityLoss {
91
- /** 闭集单值:行被店的字节上界裁掉。 */
92
- reason: "oversize_slim";
93
- /** 被裁掉的 parked 行数(诊断口径,不含身份位)。 */
94
- parks: number;
95
- }
96
- /** 铸点(普通数据)。 */
97
- export declare function buildParkIdentityLoss(parks: number): ParkIdentityLoss;
98
- /**
99
- * 盘上 run blob → {@link ParkIdentityLoss} 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形;
100
- * 与本文件 {@link readAgentSessionLedgerLine} 同一条姿态)。
48
+ * 🔴 **S-252 的分工线**(两轮 codex [high] 各证伪了一次「把这两件事并成一件」的尝试,红格在
49
+ * `test/workflow-park-decide-lane.test.ts`):
50
+ * · **路由**(本函数)回答「**哪一跑**把这个子会话 park 了」—— 一件关于**这一跑执行过什么**的事实。
51
+ * 它只有一个可靠的载体:`agents[]` 上那一行的执行状态(core 的 `parkAgentLeg` 在写 `parks` 条目的
52
+ * 同一拍把 `rec.status = "parked"` / `rec.sessionId` 落上去)。
53
+ * · **赎回权威**(「这个 token 是不是这条 run 负责的一条 park」)只由记录上的 `parks[]` 回答
54
+ * ——{@link readRunParks} 是它唯一的读者,消费点是 `parked-decide.ts` 的判别式。
55
+ * ⇒ 恢复身份(赎回键)自此**只有一个家**;本函数一个 token 字节都不读、也不存(索引只回答「去哪条 run 找」)。
101
56
  *
102
- * 形不合(缺键 / 词出闭集 / 计数不是有限数)⇒ **缺席**,而不是「铸一个默认标记出来」:标记在场的后果是
103
- * `/decide` 响亮拒,一个凭空铸出来的标记会把一条**可赎回**的 park 拒死。
104
- */
105
- export declare function readParkIdentityLoss(run: unknown): ParkIdentityLoss | undefined;
106
- /** 派生的**唯一属主**:一只 `WorkflowRun` 当前的 parked 索引行。
57
+ * 为什么路由不能改读 `parks[]`(两条都是真跑复现出来的,别再合并):
58
+ * · `parks` 含**继承而未由本跑执行**的条目:core 在读 checkpoint 真值**之前**就把它们写进新一跑的记录
59
+ * (`dist/orchestration/workflow.js:2244`,读真值的循环在 `:2251` 起),那一读抛 ⇒ `:2340` 的 catch 落一条
60
+ * **带 parks 而 journal 为空**的 `failed` 记录。跟着 `parks` 走 ⇒ 坐标被它抢走 ⇒ `/decide` 让宿主 resume 它
61
+ * ⇒ 已完成的前缀**整段重跑**(非幂等副作用重复执行)。
62
+ * · 而「只收 `originRunId === runId`」同样不成立:re-park 时 core 按 callKey 就地替换条目、`originRunId`
63
+ * **沿用继承来的那一个**,只在 `journalAppendParked` 成功之后才改写成本跑 id(`:1027-1042`),而
64
+ * `persist("update")` 打在 append **之前**。⇒ 那条判据会在 append 失败(以及 append 前的那一拍)上把
65
+ * 真正持有**新 token** 的那一跑整只排除,而上一跑的记录里只有**旧** token ⇒ 这条审批再也送不出去。
107
66
  *
108
- * 行的准入判据 = {@link isRecoverableParkedAgent}(与店层瘦身腿**同一条**)。
109
- * ⚠️ token **不进索引行**:它是赎回能力,家在 run 的 blob 里(与 bg 行同判),索引只回答「去哪条 run 找」。 */
67
+ * 准入判据(两位,与 `parkAgentLeg` 的落位一一对应):行 `status === "parked"` ∧ 有非空 `sessionId`
68
+ * (checkpoint 那一侧的 join 键就是它)。`callKey` 缺席写空串 —— 它是行自描述,不是 join 键。 */
110
69
  export declare function buildParkedAgentSessionRows(runId: string, run: {
111
70
  scope: string;
112
71
  agents?: ReadonlyArray<{
113
- status?: string;
114
- sessionId?: string;
115
- callKey?: string;
116
- parkedCheckpointToken?: string;
72
+ status?: unknown;
73
+ sessionId?: unknown;
74
+ callKey?: unknown;
117
75
  }>;
118
76
  }): WorkflowAgentSessionRow[];
119
77
  /** 两处(装饰器的变更检测、local 后端的重放)共用的行集等值判据 —— 顺序无关的集合比较,单一属主。 */