@sema-agent/server 7.117.0 → 7.119.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 (159) hide show
  1. package/MIGRATION.md +182 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -28
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +40 -10
  20. package/dist/approval-card.js +11 -1
  21. package/dist/approval.d.ts +55 -17
  22. package/dist/approval.js +74 -33
  23. package/dist/audit.d.ts +12 -1
  24. package/dist/audit.js +62 -2
  25. package/dist/auto-mode-face.d.ts +2 -3
  26. package/dist/bake-runner/main.js +1 -6
  27. package/dist/bake-runner/protocol.d.ts +18 -0
  28. package/dist/bake-runner/protocol.js +22 -2
  29. package/dist/bake-runner/runner.d.ts +12 -8
  30. package/dist/bake-runner/runner.js +5 -9
  31. package/dist/boot/config-center.js +1 -1
  32. package/dist/boot/coordinators.js +4 -1
  33. package/dist/boot/device-lane.d.ts +3 -3
  34. package/dist/boot/device-lane.js +1 -2
  35. package/dist/boot/leader.d.ts +4 -4
  36. package/dist/boot/leader.js +1 -2
  37. package/dist/boot/parked-revive-gate.d.ts +4 -6
  38. package/dist/boot/parked-revive-gate.js +2 -6
  39. package/dist/boot/resolve-spec.js +9 -11
  40. package/dist/boot/runner-deps.js +2 -2
  41. package/dist/boot/stage-07-capability-layer.js +7 -3
  42. package/dist/boot/stage-10-http-server.js +3 -2
  43. package/dist/boot/stores.js +12 -7
  44. package/dist/capabilities/center-plugins.d.ts +2 -0
  45. package/dist/capabilities/center-plugins.js +24 -6
  46. package/dist/capabilities/code-review-council.js +8 -11
  47. package/dist/capabilities/hands-lane.d.ts +25 -0
  48. package/dist/capabilities/hands-lane.js +1 -1
  49. package/dist/capabilities/mcp-probe.d.ts +2 -2
  50. package/dist/capabilities/scenarios.d.ts +34 -7
  51. package/dist/capabilities/scenarios.js +33 -6
  52. package/dist/config-catalog.js +3 -3
  53. package/dist/config.d.ts +14 -18
  54. package/dist/config.js +15 -26
  55. package/dist/deployment-governance.d.ts +5 -7
  56. package/dist/deployment-governance.js +4 -5
  57. package/dist/host-decision.d.ts +114 -31
  58. package/dist/host-decision.js +39 -2
  59. package/dist/http/admission.js +12 -6
  60. package/dist/http/approval-sweeps.js +1 -1
  61. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  62. package/dist/http/mcp-dial-deadline.js +21 -0
  63. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  64. package/dist/http/memory-compliance-failure-reply.js +17 -8
  65. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  66. package/dist/http/memory-store-refusal-reply.js +16 -5
  67. package/dist/http/resume-legs.js +8 -3
  68. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  69. package/dist/http/routes/approvals-assistant.js +27 -26
  70. package/dist/http/routes/capabilities.js +6 -3
  71. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  72. package/dist/http/routes/mcp-reconnect.js +6 -1
  73. package/dist/http/routes/memory-bundle.js +1 -1
  74. package/dist/http/routes/memory-compliance.js +1 -1
  75. package/dist/http/routes/memory-origin.d.ts +1 -1
  76. package/dist/http/routes/memory-origin.js +1 -1
  77. package/dist/http/routes/runs.js +47 -33
  78. package/dist/http/routes/sessions.d.ts +1 -1
  79. package/dist/http/routes/sessions.js +23 -22
  80. package/dist/http/routes/workflows.js +2 -0
  81. package/dist/http/wire-types.d.ts +17 -13
  82. package/dist/images/manifest.d.ts +7 -1
  83. package/dist/images/manifest.js +18 -16
  84. package/dist/memory-store-refusal.d.ts +38 -18
  85. package/dist/memory-store-refusal.js +25 -7
  86. package/dist/model-unreachable-notice.d.ts +1 -1
  87. package/dist/namespaced-rule-name.d.ts +46 -41
  88. package/dist/namespaced-rule-name.js +36 -22
  89. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  90. package/dist/observability/corrupt-read-seat.js +1 -0
  91. package/dist/observability/fail-open.d.ts +19 -3
  92. package/dist/observability/fail-open.js +19 -3
  93. package/dist/operator-ask.d.ts +6 -2
  94. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  95. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  96. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  97. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  98. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  99. package/dist/orchestration/workflow-notify-journal.js +7 -2
  100. package/dist/outbound-dispatcher.d.ts +29 -20
  101. package/dist/outbound-dispatcher.js +105 -16
  102. package/dist/parked-decide.d.ts +43 -48
  103. package/dist/parked-decide.js +54 -16
  104. package/dist/permission-rule-vocab.d.ts +4 -3
  105. package/dist/permission-rule-vocab.js +1 -1
  106. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  107. package/dist/plugins/approval-ask-store-sql.js +0 -3
  108. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  109. package/dist/plugins/approval-exemption-store.js +1 -0
  110. package/dist/plugins/blob-backend.d.ts +13 -0
  111. package/dist/plugins/blob-backend.js +26 -0
  112. package/dist/plugins/local-session-store.d.ts +78 -47
  113. package/dist/plugins/local-session-store.js +152 -122
  114. package/dist/plugins/one-time-migrations.d.ts +11 -0
  115. package/dist/plugins/one-time-migrations.js +128 -4
  116. package/dist/plugins/pg-pool.js +1 -1
  117. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  118. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  119. package/dist/plugins/store-backend.d.ts +17 -6
  120. package/dist/plugins/store-backend.js +46 -16
  121. package/dist/plugins/store-open-wait.d.ts +33 -0
  122. package/dist/plugins/store-open-wait.js +73 -0
  123. package/dist/plugins/tidb-pool.js +1 -1
  124. package/dist/plugins/web-search.d.ts +20 -1
  125. package/dist/plugins/web-search.js +39 -10
  126. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  127. package/dist/plugins/workflow-run-store-sql.js +5 -7
  128. package/dist/request-key-closure.js +1 -1
  129. package/dist/request-value-closure.d.ts +1 -1
  130. package/dist/rule-entry-list.d.ts +35 -46
  131. package/dist/rule-entry-list.js +18 -26
  132. package/dist/rules-consent.js +3 -0
  133. package/dist/run-local.js +5 -3
  134. package/dist/runs.d.ts +1 -1
  135. package/dist/security.d.ts +5 -3
  136. package/dist/security.js +4 -0
  137. package/dist/session-policy-wire.d.ts +13 -12
  138. package/dist/session-policy-wire.js +11 -11
  139. package/dist/spec-fields.d.ts +13 -11
  140. package/dist/spec-fields.js +4 -7
  141. package/dist/standing-answer-grant.d.ts +14 -2
  142. package/dist/standing-answer-grant.js +8 -1
  143. package/dist/store-posture.d.ts +14 -6
  144. package/dist/store-posture.js +7 -4
  145. package/dist/task-settings.d.ts +27 -55
  146. package/dist/task-settings.js +13 -21
  147. package/dist/tool-approval.d.ts +133 -138
  148. package/dist/tool-approval.js +175 -66
  149. package/dist/trace/core-keyset-guard.d.ts +5 -5
  150. package/dist/trace/redact.d.ts +6 -0
  151. package/dist/trace/redact.js +9 -9
  152. package/dist/trace/self-minted-wire.d.ts +16 -2
  153. package/dist/trace/self-minted-wire.js +7 -0
  154. package/dist/user-input-ledger.d.ts +30 -14
  155. package/dist/user-input-ledger.js +25 -14
  156. package/package.json +2 -2
  157. package/scripts/drain-check-local.mjs +74 -3
  158. package/scripts/drain-check.mysql.sql +43 -17
  159. package/scripts/drain-check.pg.sql +46 -20
@@ -6,7 +6,7 @@
6
6
  * `POST /v1/runs/:id/steer` 对本副本的活 run 回 **200 `delivery:"applied"`**:输入只进了 core `TaskStream` 的**进程内**队列,
7
7
  * 本服务此前不落任何字节。「你的输入丢了」今天唯一的告知是 core 在**腿终局**铸的 `task.user_steer_undrained`(`cause:"leg_end"`)——
8
8
  * 进程被 SIGKILL / 崩溃时那个终局席跑不到 ⇒ **零告知**:壳手里只有一张 200 回执,重启后主请求与转录里都没有它。
9
- * core 7.38.0 #1233 给了词:`detail.cause` 闭集 {@link USER_STEER_UNDRAINED_CAUSES} = `leg_end` / `crashed`,`crashed` 由**服务层**在
9
+ * core 给了词:`detail.cause` 闭集 {@link USER_INPUT_UNDRAINED_CAUSES} = `leg_end` / `crashed`,`crashed` 由**服务层**在
10
10
  * 该会话下一条 run 之前铸。本文件 = 那个服务层。
11
11
  *
12
12
  * ## 对象(一只账本,一类输入,一句规则)
@@ -22,9 +22,10 @@
22
22
  * (core `WorkflowTaskHandle.sessionScoped`),活得比发起它的 run 腿长,没有一条 run 行约束它,而 workflow run 行上没有持有者列 ⇒ 判不出写者死没死(另票);
23
23
  * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**独立的受理口(`src/http/routes` 零
24
24
  * follow-up 入口)。steer 的 `later` 档在引擎里走 follow-up 车道(引擎在腿收场时按车道分账:没投出去的 `later` 记进 `task.user_followup_undrained`),
25
- * 但它仍是一次 `TaskStream.steer` 受理、消费帧同为 `human_input{delivery:"applied"}`(两车道同一只消费口)⇒ 同进本账,消费即结清;崩溃告知走 core 唯一的
26
- * 服务层臂 `task.user_steer_undrained{cause:"crashed"}`(core 对该臂的定义按动词:服务层受理过的 steer)。follow-up 一族在 core 里没有 `cause` 键、没有
27
- * 服务层臂 ⇒ 本账无处按车道归档,要按车道归档须 core 先给那一臂(上游待答)⇒ 闭集一词,不预留第二词。
25
+ * 但它仍是一次 `TaskStream.steer` 受理、消费帧同为 `human_input{delivery:"applied"}`(两车道同一只消费口)⇒ 同进本账,消费即结清。
26
+ * **族 = 受理时它排进的引擎车道**(core `UserInputLane`,经引擎的 `injectionLaneOf(steerPriorityOf(…))` 读出 —— {@link acceptedInputKindOf};落盘 / wire 词 {@link UserInputKind}:
27
+ * `steer` / `follow_up`),崩溃告知的码与条数键按车道取 core `USER_INPUT_UNDRAINED_FAMILIES`(core 7.44.0 #1362:两族同一只成因词表 `USER_INPUT_UNDRAINED_CAUSES`,
28
+ * 服务层的 `crashed` 臂按车道选族,与引擎自己的 `leg_end` 臂同一种分账;core 7.45.0 #1408 起族表与车道读法在包根,本仓不再抄)。
28
29
  *
29
30
  * ## 五形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列 + 删行,同义)
30
31
  * · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
@@ -85,17 +86,30 @@
85
86
  * 不开旋钮(没有「关掉告知」的正当需求)· 不改 steer 的 200 体(回执仍是「已交付」,账本是它的影子)· 不重放丢失的输入(core 明写
86
87
  * NOT redelivered;本账只告知,不替人决定重发)· 不存输入正文(行里只有身份与时刻)。
87
88
  */
88
- import { type EngineNoticeCode, type TaskEvent, type UserSteerUndrainedCause } from "@sema-agent/core";
89
+ import { type EngineNoticeCode, type SystemInjectionPriority, type TaskEvent, type UserInputLane, type UserInputUndrainedCause } from "@sema-agent/core";
89
90
  import { type RunRecord } from "./plugins/store-contracts.js";
90
91
  import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
91
92
  /** local 数据根下的子目录(File 孪生;与 `approval-ask-audit/` 同层同形)。 */
92
93
  export declare const USER_INPUT_LEDGER_DIR = "user-input-ledger";
93
- /** 账本收的输入族(闭集)。follow-up 结论见顶注「对象」段。 */
94
- export type UserInputKind = "steer";
95
- /** 读盘词表(`satisfies Record<…, true>` ⇒ 闭集加词而本表没跟 = 编译红;判定走 `Object.hasOwn`)。 */
96
- export declare const USER_INPUT_KIND_WORDS: {
97
- readonly steer: true;
94
+ /**
95
+ * 账本收的输入族 = 受理时它排进的**引擎车道**(core `UserInputLane`:`steer` / `followUp`;车道怎么读、每条车道的告知码与条数键全归 core,见下)。
96
+ * 本表只做一件事:把车道写成本仓 7.118.0 起**已落盘、已上 wire**(`GET /v1/sessions/:id` 的 `userInputUndrained[].kind`)的那个词 —— `steer` / `follow_up`。
97
+ * `satisfies Record<UserInputLane, …>`:core 加一条车道 ⇒ 编译红,由人裁它落盘写什么。`follow_up` 自 7.118.0 起写(之前的版本受理口一律写 `steer`,`later` 档的旧行照它当时的族读)。
98
+ */
99
+ declare const LEDGER_KIND_OF_LANE: {
100
+ readonly steer: "steer";
101
+ readonly followUp: "follow_up";
98
102
  };
103
+ /** 落盘 / wire 的族词(闭集,派生自 {@link LEDGER_KIND_OF_LANE})。 */
104
+ export type UserInputKind = (typeof LEDGER_KIND_OF_LANE)[UserInputLane];
105
+ /** 读盘词表(判定走 `Object.hasOwn`;派生自 {@link LEDGER_KIND_OF_LANE})。≤7.117.x 的读腿不认 `follow_up`(回滚须知在 MIGRATION)。 */
106
+ export declare const USER_INPUT_KIND_WORDS: Readonly<Record<UserInputKind, true>>;
107
+ /**
108
+ * 一次受理的族词 —— 三个受理口(run steer 的 live 腿 / interrupt 带 text 形 / 子代 steer)同调这一只。车道是引擎的那一只读法
109
+ * `injectionLaneOf(steerPriorityOf(…))`(core 7.45.0 #1408:缺席档 = `next`;`later` 走 follow-up 车道,`now` / `next` 走 steer 车道;
110
+ * 档外值抛 `steering.invalid_content` —— 受理口在这之前已按 `isSystemInjectionPriority` 400 掉档外值,这一抛在本仓不可达)。
111
+ */
112
+ export declare function acceptedInputKindOf(priority: SystemInjectionPriority | undefined): UserInputKind;
99
113
  /** `settled` 的两种收场(闭集,见顶注)。 */
100
114
  export type SettleHow = "leg_end" | "not_applied" | "consumed";
101
115
  /** 读盘词表(File 孪生的折叠守卫 / SQL 孪生的 `settle_how` 列读腿同读这一张;`consumed` 自 7.113.0 起,≤7.112.x 的读腿不认它 —— 回滚须知在 MIGRATION)。 */
@@ -105,7 +119,7 @@ export declare const SETTLE_HOW_WORDS: {
105
119
  readonly consumed: true;
106
120
  };
107
121
  /** 本服务铸的那一臂(core 删掉这个词 ⇒ `Extract` 得 `never` ⇒ 下面的常量编译红)。 */
108
- export type CrashedCause = Extract<UserSteerUndrainedCause, "crashed">;
122
+ export type CrashedCause = Extract<UserInputUndrainedCause, "crashed">;
109
123
  export declare const CRASHED_CAUSE: CrashedCause;
110
124
  /** 盘上 / 行上的 cause 词判:必须是 core 闭集的成员,**且**铸者是服务层(词表外 / 引擎臂 ⇒ 不是本账能写的行)。 */
111
125
  export declare function isServingLayerUndrainedCause(v: unknown): v is CrashedCause;
@@ -328,11 +342,12 @@ export interface UserInputLedger {
328
342
  readSession(sessionId: string): Promise<UndrainedUserInputRow[] | undefined>;
329
343
  }
330
344
  /**
331
- * 一条旧 run 的 `crashed` 通告(纯数据)。`detail` 键集 = core 契约 `{ steer, cause, taskId?, sessionId?, runId? }` 的子集(`runId` 缺席,理由见顶注)。
345
+ * 一条旧 run 一族的 `crashed` 通告(纯数据)。`detail` 键集 = core 契约 `{ steer | followUp, cause, taskId?, sessionId?, runId? }` 的子集(`runId` 缺席,理由见顶注)。
332
346
  * `message` 只是 fallback 展示(core 同族句的形,只换成因);消费端按 `code` + `detail.cause` 分支。
333
347
  */
334
- export declare function buildCrashedSteerNotice(input: {
335
- steer: number;
348
+ export declare function buildCrashedUndrainedNotice(input: {
349
+ family: UserInputKind;
350
+ count: number;
336
351
  taskId: string;
337
352
  sessionId: string;
338
353
  }): {
@@ -368,4 +383,5 @@ export declare function createUserInputLedgerAtBoot(opts: Parameters<typeof crea
368
383
  * · `mysql` / `pg` —— 多副本共享一张表,「我启动之前写的」证明不了写者已死 ⇒ `run_store_verdict`(等 reaper 落款)。
369
384
  */
370
385
  export declare function writerGoneEvidenceFor(backendKind: "mysql" | "pg" | "local", runs: RunStoreReader): WriterGoneEvidence;
386
+ export {};
371
387
  //# sourceMappingURL=user-input-ledger.d.ts.map
@@ -1,14 +1,19 @@
1
- import { USER_STEER_UNDRAINED_CAUSES } from "@sema-agent/core";
1
+ import { USER_INPUT_UNDRAINED_CAUSES, USER_INPUT_UNDRAINED_FAMILIES, injectionLaneOf, steerPriorityOf } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
3
  import { isHolderDeathVerdict } from "./plugins/store-contracts.js";
4
4
  import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
5
5
  export const USER_INPUT_LEDGER_DIR = "user-input-ledger";
6
- export const USER_INPUT_KIND_WORDS = { steer: true };
6
+ const LEDGER_KIND_OF_LANE = { steer: "steer", followUp: "follow_up" };
7
+ const LANE_OF_LEDGER_KIND = Object.fromEntries(Object.entries(LEDGER_KIND_OF_LANE).map(([lane, kind]) => [kind, lane]));
8
+ export const USER_INPUT_KIND_WORDS = Object.freeze(Object.fromEntries(Object.values(LEDGER_KIND_OF_LANE).map((kind) => [kind, true])));
9
+ export function acceptedInputKindOf(priority) {
10
+ return LEDGER_KIND_OF_LANE[injectionLaneOf(steerPriorityOf(priority === undefined ? {} : { priority }))];
11
+ }
7
12
  export const SETTLE_HOW_WORDS = { leg_end: true, not_applied: true, consumed: true };
8
13
  const UNDRAINED_CAUSE_MINTER = { leg_end: "engine", crashed: "server" };
9
14
  export const CRASHED_CAUSE = "crashed";
10
15
  export function isServingLayerUndrainedCause(v) {
11
- if (typeof v !== "string" || !USER_STEER_UNDRAINED_CAUSES.includes(v))
16
+ if (typeof v !== "string" || !USER_INPUT_UNDRAINED_CAUSES.includes(v))
12
17
  return false;
13
18
  return UNDRAINED_CAUSE_MINTER[v] === "server";
14
19
  }
@@ -124,12 +129,17 @@ export function sortAcceptances(list) {
124
129
  export const USER_INPUT_OPEN_SCAN_MAX = 1000;
125
130
  export const USER_INPUT_OPEN_SCAN_PAGES = 100;
126
131
  export const USER_INPUT_SESSION_READ_MAX = 500;
127
- const USER_STEER_UNDRAINED = "task.user_steer_undrained";
128
- export function buildCrashedSteerNotice(input) {
132
+ const UNDRAINED_WHAT_OF_LANE = {
133
+ steer: "user steer(s)",
134
+ followUp: "user steer(s) queued for the run's end (priority later)",
135
+ };
136
+ export function buildCrashedUndrainedNotice(input) {
137
+ const lane = LANE_OF_LEDGER_KIND[input.family];
138
+ const face = USER_INPUT_UNDRAINED_FAMILIES[lane];
129
139
  return {
130
- code: USER_STEER_UNDRAINED,
131
- message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.steer} user steer(s) for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
132
- detail: { steer: input.steer, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
140
+ code: face.code,
141
+ message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.count} ${UNDRAINED_WHAT_OF_LANE[lane]} for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
142
+ detail: { [face.countKey]: input.count, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
133
143
  sessionId: input.sessionId,
134
144
  };
135
145
  }
@@ -323,15 +333,16 @@ export function createUserInputLedger(opts) {
323
333
  }
324
334
  const groups = new Map();
325
335
  for (const a of rows) {
326
- const g = groups.get(a.taskId);
336
+ const key = `${a.taskId}\u0000${a.input}`;
337
+ const g = groups.get(key);
327
338
  if (g)
328
- g.push(a);
339
+ g.group.push(a);
329
340
  else
330
- groups.set(a.taskId, [a]);
341
+ groups.set(key, { taskId: a.taskId, family: a.input, group: [a] });
331
342
  }
332
- for (const [oldTaskId, group] of groups) {
333
- const steer = new Set(group.map((a) => a.messageId)).size;
334
- const delivered = await router.routeConfirmed(buildCrashedSteerNotice({ steer, taskId: oldTaskId, sessionId: at.sessionId }));
343
+ for (const { taskId: oldTaskId, family, group } of groups.values()) {
344
+ const count = new Set(group.map((a) => a.messageId)).size;
345
+ const delivered = await router.routeConfirmed(buildCrashedUndrainedNotice({ family, count, taskId: oldTaskId, sessionId: at.sessionId }));
335
346
  if (delivered === 0)
336
347
  continue;
337
348
  for (const a of group) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.117.0",
3
+ "version": "7.119.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -64,7 +64,7 @@
64
64
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
65
65
  },
66
66
  "dependencies": {
67
- "@sema-agent/core": "7.42.1",
67
+ "@sema-agent/core": "7.45.0",
68
68
  "@sema-agent/settings-schema": "8.0.0",
69
69
  "e2b": "^2.28.0",
70
70
  "libsodium-wrappers": "^0.8.4",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * 换钉前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。随 npm 包出(`scripts/drain-check-local.mjs`)。
4
- * 一次跑**五条判据**(判据表 {@link DRAIN_CRITERIA},每条一行末行、各自的命中行;SQL 车道的同一判据是同目录随包的 `drain-check.mysql.sql` / `drain-check.pg.sql`,
4
+ * 一次跑**六条判据**(判据表 {@link DRAIN_CRITERIA},每条一行末行、各自的命中行;SQL 车道的同一判据是同目录随包的 `drain-check.mysql.sql` / `drain-check.pg.sql`,
5
5
  * 每条判据一段、段首标记 `-- drain-check:<判据名>:<后端>`,判据名与本表的 `name` 同一个词):
6
6
  * · `skill-grants-rollback`(**回滚**到 7.115.x 前,即引擎从 7.41 退回 7.40):四样任一 ——
7
7
  * ① pending 行带 `state.skillGrants`(本次 run 冻结在行上的技能授予行:模型激活 / 子代捕获写下的放行与禁止);
@@ -21,6 +21,14 @@
21
21
  * (行字节不动;出路 = 取消、改好条目重提,见随包 `MIGRATION.md` 7.117.0 节)。命中行点名是哪一层(`member=skills` / `member=agents.skills`)。
22
22
  * 判据**就是**服务提交与续跑两腿同经的那一只函数(`src/rule-entry-list.ts` 的 `skillDenyInertEntryOf`,它调引擎包根的 `compileAllowList`),经编译产物
23
23
  * `dist/rule-entry-list.js` 取 —— 本脚本不写第二份;
24
+ * · `workflow-park-holder`(换钉 7.118.0 前,引擎 7.44.0 起 workflow 子代卡交给哪条 run 只看记录 `parks[]` 条目上的持有者位 `parkedBy` / `generation`):
25
+ * pending 行的子会话被 workflow park 索引点名(`<数据根>/workflows/workflow-agent-sessions/index.jsonl`:一行 = 一条 workflow run 的整份指针集
26
+ * `{ runId, rows: [{ sessionId, runId, agentKey, scope }] }`,同一条 run 后写的一行整份覆盖先写的,空集 = 它不再点名任何子会话)= 一张待批的 workflow 子代卡。
27
+ * 在**换钉之前、对旧版本的数据**跑:引擎 7.43 及以前写下的记录没有持有者位 ⇒ 这样的卡升级后在决定口答 409 `decide.workflow_parks_unreadable`
28
+ * (更早的引擎写下、记录上根本没有 `parks` 的,答 409 `conflict.resume_context_unavailable`;卡都仍待批,出路相同 = 续跑那条 workflow run 再决,见随包 `MIGRATION.md` 7.118.0 节);换钉之后再跑会把新引擎写下的卡也数进来。命中行点名点它的 run(`runId=`)。
29
+ * 账本行的读法是服务里那一只(`src/orchestration/workflow-agent-session-index.ts` 的 `readAgentSessionLedgerLine`),经编译产物
30
+ * `dist/orchestration/workflow-agent-session-index.js` 取 —— 本脚本只按 runId 折叠;读不出的行计入 `corrupt`(rc 2:读不出的那一行可能正是点名某张待批卡的那一份);
31
+ * 窗与删码:本判据是一次性迁移 M9 的读侧半场 —— 窗口与删码清单见引擎启动期一次性迁移登记表(one-time-migrations)M9 一行,随 M9 一起删;这里不写第二份窗。
24
32
  * · `retired-mode-seed`(换钉 7.113.0 前,core 7.39.0 删掉退役 mode-seed 的一次性迁移):pending 行的 `state.inheritedGate` 仍带退役成员;
25
33
  * · `permissions-allow-list`(换钉 7.117.0 前,本服务把 `settings.permissions.allow` 整份交给引擎的 allow 层):pending 行所在会话的续跑上下文
26
34
  * (`checkpoint-ctx/*.json` 的信封 `{ sessionId, updatedAt, ctx }`)里,`ctx.body.settings.permissions.allow` 是非空数组,且 `ctx.permissionsAllowReading` 不是 `"allow_layer"`
@@ -62,14 +70,17 @@
62
70
  * ## 读数(机读末行 + 退出码)
63
71
  * · 每条命中一行:`SKILL-GRANTS-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skillGrants|boundGrantDigest|userInvokedSkills|disallowedTools>` / `SKILL-MANIFEST-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skills|agents.skills>` /
64
72
  * `SKILL-DENY-INERT-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skills|agents.skills>` /
73
+ * `WORKFLOW-PARK-ROW token=<token> sessionId=<id> createdAt=<ms> runId=<点名它的 run,逗号连接>` /
65
74
  * `ALLOW-LIST-ROW token=<token> sessionId=<id> createdAt=<ms>` / `RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
66
75
  * · 每条判据一行末行(表序;`RETIRED-MODE-SEED` 那一行恒在最后,7.113.0 起读「最后一行」的下游不受影响;`PERMISSIONS-ALLOW-LIST` 仍是倒数第二行):
67
76
  * `SKILL-GRANTS rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
68
77
  * `RETIRED-SKILL-MANIFEST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
69
78
  * `SKILL-DENY-INERT rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
79
+ * `WORKFLOW-PARK-HOLDER rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
70
80
  * `PERMISSIONS-ALLOW-LIST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
71
81
  * `RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
72
- * `corrupt` = 共用扫描的坏记录 + 这条判据自己读不懂的旁路输入(`permissions-allow-list`:读不懂的 ctx 档,文件名进 stderr —— 一份读不懂的 ctx 可能正是某条 pending 行带 `allow` 的那一份);
82
+ * `corrupt` = 共用扫描的坏记录 + 这条判据自己读不懂的旁路输入(`permissions-allow-list`:读不懂的 ctx 档,文件名进 stderr —— 一份读不懂的 ctx 可能正是某条 pending 行带 `allow` 的那一份;
83
+ * `workflow-park-holder`:索引账本里读不出的行,行号进 stderr);
73
84
  * 没读成 / 不能下结论(rc 2 的几种缺席与不稳形)时前五个计数是 `-`、多一个 `reason=<词>`,**不会出现 `rows=0`**(下游读 `rows=0` 即「干净」);
74
85
  * · rc 0 = 每条判据都零命中(可换钉 / 可回滚)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 / 读不懂的 ctx 档 /
75
86
  * 数据根缺席 / 缺省根解析不可用 / 判据模块不可用(脚本隔壁没有 `dist/`:源码树没 build)/ 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
@@ -81,7 +92,7 @@ import { join } from "node:path";
81
92
  import { fileURLToPath } from "node:url";
82
93
 
83
94
  /** 从脚本隔壁的编译产物取来的服务判据(脚本不持第二份实现)。
84
- * @typedef {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean, retiredSkillManifestLayerOf: (body: unknown) => "skills" | "agents.skills" | undefined, skillDenyInertEntryOf: (body: unknown) => { layer: "skills" | "agents.skills", message: string } | undefined }} ServiceCriteria */
95
+ * @typedef {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean, retiredSkillManifestLayerOf: (body: unknown) => "skills" | "agents.skills" | undefined, skillDenyInertEntryOf: (body: unknown) => { layer: "skills" | "agents.skills", message: string } | undefined, readAgentSessionLedgerLine: (v: unknown) => { runId: string, rows: Array<{ sessionId: string }> } | undefined }} ServiceCriteria */
85
96
 
86
97
  /** 读期间文件在变时的有界重读次数(含第一次)。 */
87
98
  const STABLE_READ_ATTEMPTS = 3;
@@ -292,6 +303,49 @@ function readCtxIndex(root) {
292
303
  return { bySession, corrupt: unreadable.length, unreadable };
293
304
  }
294
305
 
306
+ /**
307
+ * `<数据根>/workflows/workflow-agent-sessions/index.jsonl` —— workflow park 索引的 local 账本(服务里 `LocalWorkflowAgentSessionIndex` 的盘上形)。
308
+ * 一行 = 一条 workflow run 的整份指针集;行的读法是服务里那一只(`readLine`,经编译产物取),本函数只按 runId 折叠(后写整份覆盖先写,空集 = 不再点名)。
309
+ * 文件不存在 = 没有指针(没跑过 workflow);读不出的行计入 `corrupt`、行号回给调用方(不猜它属于哪条 run)。
310
+ * @param {string} root
311
+ * @param {ServiceCriteria["readAgentSessionLedgerLine"]} readLine
312
+ * @returns {{ bySession: Map<string, string[]>, corrupt: number, unreadable: string[] }} 子会话 → 点名它的 run(排序)。 */
313
+ function readParkIndex(root, readLine) {
314
+ const rel = join("workflows", "workflow-agent-sessions", "index.jsonl");
315
+ /** @type {Map<string, string[]>} runId → 它此刻点名的子会话 */
316
+ const byRun = new Map();
317
+ /** @type {string[]} */
318
+ const unreadable = [];
319
+ const text = textOf(join(root, rel));
320
+ text.split("\n").forEach((line, i) => {
321
+ if (line.trim().length === 0) return;
322
+ let v;
323
+ try {
324
+ v = JSON.parse(line);
325
+ } catch {
326
+ v = undefined;
327
+ }
328
+ const entry = v === undefined ? undefined : readLine(v);
329
+ if (entry === undefined) {
330
+ unreadable.push(`${rel}:${i + 1}`);
331
+ return;
332
+ }
333
+ if (entry.rows.length === 0) byRun.delete(entry.runId);
334
+ else byRun.set(entry.runId, entry.rows.map((r) => r.sessionId));
335
+ });
336
+ /** @type {Map<string, string[]>} */
337
+ const bySession = new Map();
338
+ for (const [runId, sessions] of byRun) {
339
+ for (const sessionId of sessions) {
340
+ const runs = bySession.get(sessionId) ?? [];
341
+ if (!runs.includes(runId)) runs.push(runId);
342
+ bySession.set(sessionId, runs);
343
+ }
344
+ }
345
+ for (const runs of bySession.values()) runs.sort();
346
+ return { bySession, corrupt: unreadable.length, unreadable };
347
+ }
348
+
295
349
  /**
296
350
  * 判据表 —— 「哪些 pending 行算命中」。扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)各条共用,判据只管这一问:
297
351
  * · `tail` / `hit`:末行前缀 / 命中行前缀;
@@ -356,6 +410,22 @@ export const DRAIN_CRITERIA = [
356
410
  return hit === undefined ? undefined : ` member=${hit.layer}`;
357
411
  },
358
412
  },
413
+ {
414
+ // 换钉 7.118.0 之前:待批、且子会话被 workflow park 索引点名的卡(头注里那一条)。账本行的读法是服务里那一只(`readAgentSessionLedgerLine`,经编译产物取);
415
+ // 命中行点名点它的 run。表序在 `permissions-allow-list` 之前 —— 那一条仍倒数第二、`RETIRED-MODE-SEED` 仍最后。
416
+ name: "workflow-park-holder",
417
+ tail: "WORKFLOW-PARK-HOLDER",
418
+ hit: "WORKFLOW-PARK-ROW",
419
+ load: (root, service) => {
420
+ const idx = readParkIndex(root, service.readAgentSessionLedgerLine);
421
+ return { input: idx.bySession, corrupt: idx.corrupt, unreadable: idx.unreadable };
422
+ },
423
+ judge: (row, input) => {
424
+ const bySession = /** @type {Map<string, string[]>} */ (input);
425
+ const runs = typeof row.sessionId === "string" ? bySession.get(row.sessionId) : undefined;
426
+ return runs === undefined ? undefined : ` runId=${runs.join(",")}`;
427
+ },
428
+ },
359
429
  {
360
430
  name: "permissions-allow-list",
361
431
  tail: "PERMISSIONS-ALLOW-LIST",
@@ -401,6 +471,7 @@ async function loadServiceCriteria(rootEcho) {
401
471
  ["dist/http/checkpoint-ctx.js", () => import("../dist/http/checkpoint-ctx.js"), "allowListReadingUnstated"],
402
472
  ["dist/http/retired-skill-manifest.js", () => import("../dist/http/retired-skill-manifest.js"), "retiredSkillManifestLayerOf"],
403
473
  ["dist/rule-entry-list.js", () => import("../dist/rule-entry-list.js"), "skillDenyInertEntryOf"],
474
+ ["dist/orchestration/workflow-agent-session-index.js", () => import("../dist/orchestration/workflow-agent-session-index.js"), "readAgentSessionLedgerLine"],
404
475
  ];
405
476
  /** @type {Record<string, unknown>} */
406
477
  const fns = {};
@@ -8,12 +8,13 @@
8
8
  -- sessions 命中行所在的会话 id,逗号连接,新的在前。以 hit_rows 为准:这一列被会话变量 group_concat_max_len(缺省 1024 字节)截断时,列出的会话数会少于 hit_rows ——
9
9
  -- 先处理列出的再跑一次,或先在同一个会话里 SET SESSION group_concat_max_len = 1048576; 再跑本文件(本文件自己只含 SELECT)。
10
10
  -- 每段只数 pending 行(已决 / 已过期的行不数);判据只看**键在不在 / 值形**,不读行的其余内容。
11
- -- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,五条判据同名。
11
+ -- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,六条判据同名。
12
12
  --
13
- -- 五条判据(顺序同 drain-check-local.mjs 的判据表):
13
+ -- 六条判据(顺序同 drain-check-local.mjs 的判据表):
14
14
  -- skill-grants-rollback 回滚到 7.115.x(引擎 7.40)之前:7.116.0 停驻的行带 7.40 不读的技能授予节 / 卡绑定 / 会话上下文里的人点名技能 / 还没被激活的技能声明的禁止清单。
15
15
  -- retired-skill-manifest 升级到 7.116.0 之前:停驻、持久化提交体里技能(顶层 skills[] 或子代 agents[].skills[])还带已退役的 manifest 键。
16
16
  -- skill-deny-inert 换钉 7.117.0 之前:停驻、提交体里技能(顶层 skills[] 或子代 agents[].skills[])的 disallowedTools 有引擎会读成空的条目的**候选**(见该段注)。
17
+ -- workflow-park-holder 换钉 7.118.0 之前 / 从 7.118.0 回滚到 7.117.x 之前:此刻待批、且子会话被 workflow park 索引点名的卡(workflow 子代卡;见该段注)。
17
18
  -- permissions-allow-list 换钉 7.117.0 之前:停驻、提交体 settings.permissions.allow 是非空数组且读法记号不是当前的 "allow_layer"(无记号 = 名单时代;"statement" = 表态时代)。
18
19
  -- retired-mode-seed 换钉 7.113.0 之前:检查点的 inheritedGate 里仍带退役 mode-seed 成员。
19
20
 
@@ -48,7 +49,9 @@ WHERE k.status = 'pending'
48
49
 
49
50
  -- drain-check:skill-deny-inert:mysql —— 期望 hit_rows = 0;每一行 = 一条**候选**:升级后续跑可能被拒的停驻行(拒的是提交体里技能的一条 disallowedTools 条目 ——
50
51
  -- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
51
- -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
52
+ -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。一个子会话可以被好几条 workflow 记录点名(续跑链 / 分叉;7.118.0 起索引主键是
53
+ -- (session_id, run_id)),续跑腿读的是其中**持有者**那条记录的发起会话,持有者通常是链上最新的、不是最老的,SQL 判不出是哪一条 ⇒ 数的是真超集:
54
+ -- 任一条指针记录的发起会话命中即数(指针没有记录 / 记录没有发起会话 ⇒ 读这张卡自己会话的);整条连接链包在对这张卡的 EXISTS 里(kk = 这张卡本身,按主键取回),一张卡只数一次。
52
55
  -- 引擎 7.42 逐字读规则条目,读成空的拒什么也不拒,服务对这种体在提交与续跑上同拒;7.41 去首尾空白,所以 7.116.x 上它可能还是一条有效的拒)。
53
56
  -- SQL 不能逐条照引擎判读(引擎的文法 —— 非 ASCII 空白、内容形不受支持的工具、括号不配对 —— 在 SQL 里复刻不全,逐形枚举的表必漏),所以这里数的是**真超集**:
54
57
  -- 顶层 skills[*] 或子代 agents[*].skills[*] 的 disallowedTools **非空**的停驻行(与回滚判据 skill-grants-rollback 的第四样同一只形)。两方言都 ⊇ 运行期判据;拒侧清单不是数组的形(受理口 400,盘上不该有)两边数法不同 —— pg 只认数组,MySQL 的 [0] 把非数组当一元数组读。
@@ -57,29 +60,52 @@ WHERE k.status = 'pending'
57
60
  SELECT 'skill-deny-inert' AS criterion, COUNT(*) AS hit_rows,
58
61
  COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
59
62
  FROM checkpoint k
60
- LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
61
- LEFT JOIN workflow_run r ON r.id = w.run_id
62
- JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), k.session_id)
63
63
  WHERE k.status = 'pending'
64
- AND ((JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.skills')) = 'ARRAY'
65
- AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.skills[*].disallowedTools[0]'))
66
- OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
67
- AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].disallowedTools[0]')));
64
+ AND EXISTS (SELECT 1
65
+ FROM checkpoint kk
66
+ LEFT JOIN workflow_agent_session w ON w.session_id = kk.session_id
67
+ LEFT JOIN workflow_run r ON r.id = w.run_id
68
+ JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), kk.session_id)
69
+ WHERE kk.token = k.token
70
+ AND ((JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.skills')) = 'ARRAY'
71
+ AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.skills[*].disallowedTools[0]'))
72
+ OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
73
+ AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].disallowedTools[0]'))));
74
+
75
+ -- drain-check:workflow-park-holder:mysql —— 期望 hit_rows = 0;每一行 = 一张待批的 workflow 子代卡(它的子会话被 workflow_agent_session 里的一条指针点名)。
76
+ -- 在**换钉之前**、对旧版本的数据跑:7.118.0 起 workflow 子代卡交给哪条 run 只看记录 parks[] 条目上的持有者位(parkedBy / generation),
77
+ -- 引擎 7.43 及以前写下的记录没有这两位 ⇒ 这样的卡升级后在决定口答 409 decide.workflow_parks_unreadable(更早的引擎写下、记录上根本没有 parks 的,答 409
78
+ -- conflict.resume_context_unavailable;卡都仍待批,出路相同 = 续跑那条 workflow run 再决)。
79
+ -- 换钉前把它们决掉(或取消发起它的任务)就省掉这一步。换钉之后再跑本段会把新引擎写下的卡也数进来(那时它不再是「会被拒的卡」的读数)。
80
+ -- 从 7.118.0 回滚到 7.117.x 之前、对 7.118.0 的数据跑:数出的就是全部待批的 workflow 子代卡 —— 回滚要删掉这张索引表(7.117.x 按旧主键重建,
81
+ -- 旧副本在新主键上写不进 / 读不准,见随包 MIGRATION.md 7.118.0 节),删表之后这些卡没有指针、决不掉;先把它们决掉或取消。
82
+ -- 对索引表用 EXISTS 而不 JOIN:一个子会话可以被好几条记录点名,JOIN 会把一张卡数成几张。
83
+ -- 窗与删码:本判据是一次性迁移 M9 的读侧半场 —— 窗口与删码清单见引擎启动期一次性迁移登记表(one-time-migrations)M9 一行,随 M9 一起删;这里不写第二份窗。
84
+ SELECT 'workflow-park-holder' AS criterion, COUNT(*) AS hit_rows,
85
+ COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
86
+ FROM checkpoint k
87
+ WHERE k.status = 'pending'
88
+ AND EXISTS (SELECT 1 FROM workflow_agent_session w WHERE w.session_id = k.session_id);
68
89
 
69
90
  -- drain-check:permissions-allow-list:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后续跑会被拒的停驻行。
70
91
  -- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
71
- -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
92
+ -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。一个子会话可以被好几条 workflow 记录点名(续跑链 / 分叉;7.118.0 起索引主键是
93
+ -- (session_id, run_id)),续跑腿读的是其中**持有者**那条记录的发起会话,持有者通常是链上最新的、不是最老的,SQL 判不出是哪一条 ⇒ 数的是真超集:
94
+ -- 任一条指针记录的发起会话命中即数(指针没有记录 / 记录没有发起会话 ⇒ 读这张卡自己会话的);整条连接链包在对这张卡的 EXISTS 里(kk = 这张卡本身,按主键取回),一张卡只数一次。
72
95
  -- 判据 = 服务续跑腿拒臂的同一句话:提交体 settings.permissions.allow 是非空数组,且上下文的读法记号 permissionsAllowReading 不是当前的 "allow_layer"。
73
96
  SELECT 'permissions-allow-list' AS criterion, COUNT(*) AS hit_rows,
74
97
  COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
75
98
  FROM checkpoint k
76
- LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
77
- LEFT JOIN workflow_run r ON r.id = w.run_id
78
- JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), k.session_id)
79
99
  WHERE k.status = 'pending'
80
- AND JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) = 'ARRAY'
81
- AND JSON_LENGTH(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) > 0
82
- AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(c.ctx, '$.permissionsAllowReading')), '') <> 'allow_layer';
100
+ AND EXISTS (SELECT 1
101
+ FROM checkpoint kk
102
+ LEFT JOIN workflow_agent_session w ON w.session_id = kk.session_id
103
+ LEFT JOIN workflow_run r ON r.id = w.run_id
104
+ JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), kk.session_id)
105
+ WHERE kk.token = k.token
106
+ AND JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) = 'ARRAY'
107
+ AND JSON_LENGTH(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) > 0
108
+ AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(c.ctx, '$.permissionsAllowReading')), '') <> 'allow_layer');
83
109
 
84
110
  -- drain-check:retired-mode-seed:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后会被拒的停放行。
85
111
  -- 判据逐字照引擎 refuseRetiredModeSeed:inheritedGate 带 autoModeRequested / approverPosture(任何值,含 JSON null),或 shellGate 不是 "always"(含 JSON null)。
@@ -7,12 +7,13 @@
7
7
  -- hit_rows 命中的停驻行数(pending 行里算命中的那些);**全部为 0 才算排空**,非 0 ⇒ 逐条答完或取消(各判据对应的升级 / 回滚说明见随包 MIGRATION.md);
8
8
  -- sessions 命中行所在的会话 id,逗号连接,新的在前。
9
9
  -- 每段只数 pending 行(已决 / 已过期的行不数);判据只看**键在不在 / 值形**,不读行的其余内容。
10
- -- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,五条判据同名。
10
+ -- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,六条判据同名。
11
11
  --
12
- -- 五条判据(顺序同 drain-check-local.mjs 的判据表):
12
+ -- 六条判据(顺序同 drain-check-local.mjs 的判据表):
13
13
  -- skill-grants-rollback 回滚到 7.115.x(引擎 7.40)之前:7.116.0 停驻的行带 7.40 不读的技能授予节 / 卡绑定 / 会话上下文里的人点名技能 / 还没被激活的技能声明的禁止清单。
14
14
  -- retired-skill-manifest 升级到 7.116.0 之前:停驻、持久化提交体里技能(顶层 skills[] 或子代 agents[].skills[])还带已退役的 manifest 键。
15
15
  -- skill-deny-inert 换钉 7.117.0 之前:停驻、提交体里技能(顶层 skills[] 或子代 agents[].skills[])的 disallowedTools 有引擎会读成空的条目的**候选**(见该段注)。
16
+ -- workflow-park-holder 换钉 7.118.0 之前 / 从 7.118.0 回滚到 7.117.x 之前:此刻待批、且子会话被 workflow park 索引点名的卡(workflow 子代卡;见该段注)。
16
17
  -- permissions-allow-list 换钉 7.117.0 之前:停驻、提交体 settings.permissions.allow 是非空数组且读法记号不是当前的 "allow_layer"(无记号 = 名单时代;"statement" = 表态时代)。
17
18
  -- retired-mode-seed 换钉 7.113.0 之前:检查点的 inheritedGate 里仍带退役 mode-seed 成员。
18
19
 
@@ -52,7 +53,9 @@ WHERE k.status = 'pending'
52
53
 
53
54
  -- drain-check:skill-deny-inert:pg —— 期望 hit_rows = 0;每一行 = 一条**候选**:升级后续跑可能被拒的停驻行(拒的是提交体里技能的一条 disallowedTools 条目 ——
54
55
  -- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
55
- -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
56
+ -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。一个子会话可以被好几条 workflow 记录点名(续跑链 / 分叉;7.118.0 起索引主键是
57
+ -- (session_id, run_id)),续跑腿读的是其中**持有者**那条记录的发起会话,持有者通常是链上最新的、不是最老的,SQL 判不出是哪一条 ⇒ 数的是真超集:
58
+ -- 任一条指针记录的发起会话命中即数(指针没有记录 / 记录没有发起会话 ⇒ 读这张卡自己会话的);整条连接链包在对这张卡的 EXISTS 里(kk = 这张卡本身,按主键取回),一张卡只数一次。
56
59
  -- 引擎 7.42 逐字读规则条目,读成空的拒什么也不拒,服务对这种体在提交与续跑上同拒;7.41 去首尾空白,所以 7.116.x 上它可能还是一条有效的拒)。
57
60
  -- SQL 不能逐条照引擎判读(引擎的文法 —— 非 ASCII 空白、内容形不受支持的工具、括号不配对 —— 在 SQL 里复刻不全,逐形枚举的表必漏),所以这里数的是**真超集**:
58
61
  -- 顶层 skills[*] 或子代 agents[*].skills[*] 的 disallowedTools **非空**的停驻行(与回滚判据 skill-grants-rollback 的第四样同一只形)。两方言都 ⊇ 运行期判据;拒侧清单不是数组的形(受理口 400,盘上不该有)两边数法不同 —— pg 只认数组,MySQL 的 [0] 把非数组当一元数组读。
@@ -62,33 +65,56 @@ WHERE k.status = 'pending'
62
65
  SELECT 'skill-deny-inert' AS criterion, count(*) AS hit_rows,
63
66
  coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
64
67
  FROM checkpoint k
65
- LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
66
- LEFT JOIN workflow_run r ON r.id = w.run_id
67
- JOIN checkpoint_ctx c ON c.session_id = COALESCE(r.run::json->>'originatingSessionId', k.session_id)
68
68
  WHERE k.status = 'pending'
69
69
  AND EXISTS (SELECT 1
70
- FROM (SELECT s
71
- FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,skills}') = 'array' THEN c.ctx #> '{body,skills}' ELSE '[]'::jsonb END) AS s
72
- UNION ALL
73
- SELECT s
74
- FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,agents}') = 'array' THEN c.ctx #> '{body,agents}' ELSE '[]'::jsonb END) AS a,
75
- jsonb_array_elements(CASE WHEN jsonb_typeof(a -> 'skills') = 'array' THEN a -> 'skills' ELSE '[]'::jsonb END) AS s) AS sk
76
- WHERE jsonb_typeof(sk.s) = 'object' AND jsonb_typeof(sk.s -> 'disallowedTools') = 'array' AND (sk.s -> 'disallowedTools') <> '[]'::jsonb);
70
+ FROM checkpoint kk
71
+ LEFT JOIN workflow_agent_session w ON w.session_id = kk.session_id
72
+ LEFT JOIN workflow_run r ON r.id = w.run_id
73
+ JOIN checkpoint_ctx c ON c.session_id = COALESCE(r.run::json->>'originatingSessionId', kk.session_id)
74
+ WHERE kk.token = k.token
75
+ AND EXISTS (SELECT 1
76
+ FROM (SELECT s
77
+ FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,skills}') = 'array' THEN c.ctx #> '{body,skills}' ELSE '[]'::jsonb END) AS s
78
+ UNION ALL
79
+ SELECT s
80
+ FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,agents}') = 'array' THEN c.ctx #> '{body,agents}' ELSE '[]'::jsonb END) AS a,
81
+ jsonb_array_elements(CASE WHEN jsonb_typeof(a -> 'skills') = 'array' THEN a -> 'skills' ELSE '[]'::jsonb END) AS s) AS sk
82
+ WHERE jsonb_typeof(sk.s) = 'object' AND jsonb_typeof(sk.s -> 'disallowedTools') = 'array' AND (sk.s -> 'disallowedTools') <> '[]'::jsonb));
83
+
84
+ -- drain-check:workflow-park-holder:pg —— 期望 hit_rows = 0;每一行 = 一张待批的 workflow 子代卡(它的子会话被 workflow_agent_session 里的一条指针点名)。
85
+ -- 在**换钉之前**、对旧版本的数据跑:7.118.0 起 workflow 子代卡交给哪条 run 只看记录 parks[] 条目上的持有者位(parkedBy / generation),
86
+ -- 引擎 7.43 及以前写下的记录没有这两位 ⇒ 这样的卡升级后在决定口答 409 decide.workflow_parks_unreadable(更早的引擎写下、记录上根本没有 parks 的,答 409
87
+ -- conflict.resume_context_unavailable;卡都仍待批,出路相同 = 续跑那条 workflow run 再决)。
88
+ -- 换钉前把它们决掉(或取消发起它的任务)就省掉这一步。换钉之后再跑本段会把新引擎写下的卡也数进来(那时它不再是「会被拒的卡」的读数)。
89
+ -- 从 7.118.0 回滚到 7.117.x 之前、对 7.118.0 的数据跑:数出的就是全部待批的 workflow 子代卡 —— 回滚要删掉这张索引表(7.117.x 按旧主键重建,
90
+ -- 旧副本在新主键上写不进 / 读不准,见随包 MIGRATION.md 7.118.0 节),删表之后这些卡没有指针、决不掉;先把它们决掉或取消。
91
+ -- 对索引表用 EXISTS 而不 JOIN:一个子会话可以被好几条记录点名,JOIN 会把一张卡数成几张。
92
+ -- 窗与删码:本判据是一次性迁移 M9 的读侧半场 —— 窗口与删码清单见引擎启动期一次性迁移登记表(one-time-migrations)M9 一行,随 M9 一起删;这里不写第二份窗。
93
+ SELECT 'workflow-park-holder' AS criterion, count(*) AS hit_rows,
94
+ coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
95
+ FROM checkpoint k
96
+ WHERE k.status = 'pending'
97
+ AND EXISTS (SELECT 1 FROM workflow_agent_session w WHERE w.session_id = k.session_id);
77
98
 
78
99
  -- drain-check:permissions-allow-list:pg —— 期望 hit_rows = 0;每一行 = 一条升级后续跑会被拒的停驻行。
79
100
  -- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
80
- -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
101
+ -- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。一个子会话可以被好几条 workflow 记录点名(续跑链 / 分叉;7.118.0 起索引主键是
102
+ -- (session_id, run_id)),续跑腿读的是其中**持有者**那条记录的发起会话,持有者通常是链上最新的、不是最老的,SQL 判不出是哪一条 ⇒ 数的是真超集:
103
+ -- 任一条指针记录的发起会话命中即数(指针没有记录 / 记录没有发起会话 ⇒ 读这张卡自己会话的);整条连接链包在对这张卡的 EXISTS 里(kk = 这张卡本身,按主键取回),一张卡只数一次。
81
104
  -- 判据 = 服务续跑腿拒臂的同一句话:提交体 settings.permissions.allow 是非空数组,且上下文的读法记号 permissionsAllowReading 不是当前的 "allow_layer"。
82
105
  SELECT 'permissions-allow-list' AS criterion, count(*) AS hit_rows,
83
106
  coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
84
107
  FROM checkpoint k
85
- LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
86
- LEFT JOIN workflow_run r ON r.id = w.run_id
87
- JOIN checkpoint_ctx c ON c.session_id = COALESCE(r.run::json->>'originatingSessionId', k.session_id)
88
108
  WHERE k.status = 'pending'
89
- AND jsonb_typeof(c.ctx #> '{body,settings,permissions,allow}') = 'array'
90
- AND (c.ctx #> '{body,settings,permissions,allow}') <> '[]'::jsonb
91
- AND (c.ctx -> 'permissionsAllowReading') IS DISTINCT FROM '"allow_layer"'::jsonb;
109
+ AND EXISTS (SELECT 1
110
+ FROM checkpoint kk
111
+ LEFT JOIN workflow_agent_session w ON w.session_id = kk.session_id
112
+ LEFT JOIN workflow_run r ON r.id = w.run_id
113
+ JOIN checkpoint_ctx c ON c.session_id = COALESCE(r.run::json->>'originatingSessionId', kk.session_id)
114
+ WHERE kk.token = k.token
115
+ AND jsonb_typeof(c.ctx #> '{body,settings,permissions,allow}') = 'array'
116
+ AND (c.ctx #> '{body,settings,permissions,allow}') <> '[]'::jsonb
117
+ AND (c.ctx -> 'permissionsAllowReading') IS DISTINCT FROM '"allow_layer"'::jsonb);
92
118
 
93
119
  -- drain-check:retired-mode-seed:pg —— 期望 hit_rows = 0;每一行 = 一条升级后会被拒的停放行。
94
120
  -- 判据逐字照引擎 refuseRetiredModeSeed:inheritedGate 带 autoModeRequested / approverPosture(任何值,含 JSON null),或 shellGate 不是 "always"(含 JSON null)。