@sema-agent/server 7.138.0 → 7.140.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 (332) hide show
  1. package/MIGRATION.md +457 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +91 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/runner.js +1 -1
  8. package/dist/adoption/sql.js +1 -1
  9. package/dist/adoption/wire.d.ts +3 -3
  10. package/dist/approval-card.d.ts +26 -26
  11. package/dist/bench/s1/arms.d.ts +1 -1
  12. package/dist/bench/s1/arms.js +1 -3
  13. package/dist/bench/s1/live-deps.js +3 -2
  14. package/dist/bench/s1/row.d.ts +5 -2
  15. package/dist/bench/s1/row.js +2 -22
  16. package/dist/bench/s1/run-firm.js +2 -2
  17. package/dist/boot/adoption.js +1 -1
  18. package/dist/boot/device-lane.d.ts +8 -3
  19. package/dist/boot/device-lane.js +7 -2
  20. package/dist/boot/execution-env.d.ts +4 -2
  21. package/dist/boot/execution-env.js +28 -34
  22. package/dist/boot/execution-lane.d.ts +9 -1
  23. package/dist/boot/execution-lane.js +6 -1
  24. package/dist/boot/governance-seams.d.ts +3 -2
  25. package/dist/boot/governance-seams.js +9 -11
  26. package/dist/boot/memory-boundary.d.ts +14 -51
  27. package/dist/boot/memory-boundary.js +10 -20
  28. package/dist/boot/memory-consolidation.d.ts +15 -11
  29. package/dist/boot/memory-consolidation.js +25 -14
  30. package/dist/boot/resolve-spec.d.ts +4 -6
  31. package/dist/boot/resolve-spec.js +10 -13
  32. package/dist/boot/retention-lane.d.ts +5 -5
  33. package/dist/boot/retention-lane.js +0 -6
  34. package/dist/boot/runner-deps.d.ts +6 -4
  35. package/dist/boot/runner-deps.js +3 -1
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  40. package/dist/boot/stage-03-store-backend.js +2 -3
  41. package/dist/boot/stage-04-stores.d.ts +2 -6
  42. package/dist/boot/stage-04-stores.js +5 -5
  43. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  44. package/dist/boot/stage-05-execution-env.js +2 -2
  45. package/dist/boot/stage-06-runners.d.ts +2 -6
  46. package/dist/boot/stage-06-runners.js +2 -3
  47. package/dist/boot/stage-07-capability-layer.d.ts +2 -6
  48. package/dist/boot/stage-08-reapers.d.ts +2 -6
  49. package/dist/boot/stage-09-leader.d.ts +2 -6
  50. package/dist/boot/stage-09-leader.js +1 -2
  51. package/dist/boot/stage-10-http-server.d.ts +2 -6
  52. package/dist/boot/stage-10-http-server.js +7 -6
  53. package/dist/boot/stores.d.ts +3 -6
  54. package/dist/boot/stores.js +27 -40
  55. package/dist/boot/task-list-lane.d.ts +1 -1
  56. package/dist/boot/task-list-lane.js +1 -1
  57. package/dist/boot/web-admin.js +37 -11
  58. package/dist/capabilities/execution-lane.d.ts +2 -2
  59. package/dist/capabilities/mcp-probe.js +1 -1
  60. package/dist/capabilities/scenarios.d.ts +1 -1
  61. package/dist/capability-facts.d.ts +15 -7
  62. package/dist/capability-facts.js +8 -3
  63. package/dist/config-catalog.js +9 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +33 -37
  67. package/dist/config-provider.js +51 -30
  68. package/dist/config-types.d.ts +21 -7
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +46 -16
  71. package/dist/delegated-session-owner.d.ts +4 -3
  72. package/dist/delegated-session-owner.js +1 -1
  73. package/dist/deployment-shape.d.ts +10 -9
  74. package/dist/deployment-shape.js +4 -4
  75. package/dist/device-enrollment.d.ts +41 -2
  76. package/dist/device-enrollment.js +27 -12
  77. package/dist/device-store.d.ts +58 -85
  78. package/dist/device-store.js +19 -49
  79. package/dist/device-ws-hub.js +2 -1
  80. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  81. package/dist/exec-cut.d.ts +60 -0
  82. package/dist/exec-cut.js +36 -0
  83. package/dist/execution-lane-caps.d.ts +115 -105
  84. package/dist/execution-lane-caps.js +76 -82
  85. package/dist/execution-placement.d.ts +259 -0
  86. package/dist/execution-placement.js +199 -0
  87. package/dist/field-conflict.d.ts +3 -2
  88. package/dist/field-conflict.js +1 -0
  89. package/dist/hosted-posture.d.ts +2 -2
  90. package/dist/http/admission.js +8 -19
  91. package/dist/http/checkpoint-ctx.d.ts +21 -12
  92. package/dist/http/checkpoint-ctx.js +4 -2
  93. package/dist/http/claimed-run.d.ts +1 -1
  94. package/dist/http/durable-run-leg.js +9 -5
  95. package/dist/http/execution-context.d.ts +218 -0
  96. package/dist/http/execution-context.js +330 -0
  97. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  98. package/dist/http/memory-compliance-failure-reply.js +5 -0
  99. package/dist/http/memory-layer-reply.d.ts +30 -0
  100. package/dist/http/memory-layer-reply.js +24 -0
  101. package/dist/http/resume-legs.js +63 -36
  102. package/dist/http/retired-service-deps.d.ts +1 -1
  103. package/dist/http/retired-service-deps.js +1 -1
  104. package/dist/http/route-ctx.d.ts +14 -8
  105. package/dist/http/routes/adoption.js +1 -1
  106. package/dist/http/routes/background.d.ts +29 -1
  107. package/dist/http/routes/background.js +29 -2
  108. package/dist/http/routes/devices.d.ts +13 -19
  109. package/dist/http/routes/devices.js +21 -26
  110. package/dist/http/routes/memory-bundle.js +3 -0
  111. package/dist/http/routes/memory-compliance.js +8 -1
  112. package/dist/http/routes/memory-consolidation.js +12 -1
  113. package/dist/http/routes/memory-origin.js +14 -2
  114. package/dist/http/routes/memory-policy.js +5 -0
  115. package/dist/http/routes/session-sync.js +110 -15
  116. package/dist/http/routes/sessions.d.ts +6 -0
  117. package/dist/http/routes/sessions.js +143 -22
  118. package/dist/http/routes/side-query.d.ts +12 -4
  119. package/dist/http/routes/side-query.js +4 -5
  120. package/dist/http/routes/tasks.js +41 -33
  121. package/dist/http/server.d.ts +40 -44
  122. package/dist/http/server.js +13 -2
  123. package/dist/http/session-inherit-env.d.ts +3 -4
  124. package/dist/http/session-inherit-env.js +1 -1
  125. package/dist/http/wire-types.d.ts +8 -12
  126. package/dist/identity-source.d.ts +1 -1
  127. package/dist/identity-source.js +1 -1
  128. package/dist/index.d.ts +1 -0
  129. package/dist/lane-home-probe.d.ts +68 -42
  130. package/dist/lane-home-probe.js +53 -30
  131. package/dist/leader/wire.js +5 -4
  132. package/dist/memory-bundle-engine.d.ts +16 -9
  133. package/dist/memory-bundle-engine.js +29 -6
  134. package/dist/memory-operator-faces.d.ts +85 -30
  135. package/dist/memory-operator-faces.js +136 -30
  136. package/dist/memory-project-layer.d.ts +3 -21
  137. package/dist/memory-project-layer.js +11 -8
  138. package/dist/memory-scope-stores.d.ts +73 -0
  139. package/dist/memory-scope-stores.js +86 -0
  140. package/dist/memory-scope.d.ts +29 -5
  141. package/dist/memory-scope.js +16 -4
  142. package/dist/memory-sync-client.d.ts +70 -18
  143. package/dist/memory-sync-client.js +215 -117
  144. package/dist/memory-sync.d.ts +1 -1
  145. package/dist/memory-sync.js +3 -2
  146. package/dist/observability/cost-taxonomy.d.ts +2 -2
  147. package/dist/observability/fail-open.d.ts +12 -0
  148. package/dist/observability/fail-open.js +12 -0
  149. package/dist/observability/run-terminal.js +0 -1
  150. package/dist/orchestration/workflow-notify-journal.js +4 -4
  151. package/dist/org-memory-admission.d.ts +4 -3
  152. package/dist/org-memory-admission.js +6 -6
  153. package/dist/parked-decide.d.ts +34 -9
  154. package/dist/parked-decide.js +12 -19
  155. package/dist/parked-revive-surface.d.ts +12 -15
  156. package/dist/parked-revive-surface.js +3 -6
  157. package/dist/peer-seat.d.ts +13 -7
  158. package/dist/peer-seat.js +37 -3
  159. package/dist/plugins/adoption-log-sql.js +4 -4
  160. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  161. package/dist/plugins/approval-ask-store-file.js +11 -0
  162. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  163. package/dist/plugins/approval-ask-store-memory.js +12 -0
  164. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  165. package/dist/plugins/approval-ask-store-sql.js +44 -26
  166. package/dist/plugins/approval-nonce-store.js +4 -4
  167. package/dist/plugins/blob-backend.d.ts +4 -4
  168. package/dist/plugins/blob-backend.js +7 -7
  169. package/dist/plugins/breaker-state-sql.js +2 -2
  170. package/dist/plugins/caching-session-store.d.ts +6 -2
  171. package/dist/plugins/caching-session-store.js +9 -3
  172. package/dist/plugins/checkpoint-index-columns.js +2 -2
  173. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  174. package/dist/plugins/checkpoint-store-sql.js +21 -23
  175. package/dist/plugins/device-store-sql.d.ts +25 -51
  176. package/dist/plugins/device-store-sql.js +68 -122
  177. package/dist/plugins/ensure-index.d.ts +1 -2
  178. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  179. package/dist/plugins/execution-placement-sql.js +223 -0
  180. package/dist/plugins/file-history-store-sql.d.ts +9 -8
  181. package/dist/plugins/file-history-store-sql.js +9 -9
  182. package/dist/plugins/file-run-store.d.ts +6 -1
  183. package/dist/plugins/file-run-store.js +4 -0
  184. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  185. package/dist/plugins/fork-routing-session-store.js +16 -0
  186. package/dist/plugins/image-bake-store-sql.js +9 -9
  187. package/dist/plugins/image-index-sql.js +11 -11
  188. package/dist/plugins/leader-run-store-sql.js +4 -4
  189. package/dist/plugins/local-session-store.d.ts +149 -50
  190. package/dist/plugins/local-session-store.js +382 -107
  191. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  192. package/dist/plugins/mailbox-store-sql.js +4 -4
  193. package/dist/plugins/memory-engine-tidb.js +5 -5
  194. package/dist/plugins/memory-key-guards.d.ts +4 -0
  195. package/dist/plugins/memory-key-guards.js +1 -0
  196. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  197. package/dist/plugins/memory-run-store.d.ts +1 -1
  198. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  199. package/dist/plugins/one-time-migrations.d.ts +117 -8
  200. package/dist/plugins/one-time-migrations.js +408 -38
  201. package/dist/plugins/org-delivery-quota-store.js +2 -2
  202. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  203. package/dist/plugins/outcome-ledger-sql.js +2 -2
  204. package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
  205. package/dist/plugins/permission-rule-store-sql.js +29 -32
  206. package/dist/plugins/pg-pool.d.ts +7 -3
  207. package/dist/plugins/pg-session-storage.d.ts +31 -20
  208. package/dist/plugins/pg-session-storage.js +120 -52
  209. package/dist/plugins/remote-env-adb.d.ts +18 -19
  210. package/dist/plugins/remote-env-adb.js +91 -66
  211. package/dist/plugins/remote-env-device.d.ts +23 -31
  212. package/dist/plugins/remote-env-device.js +19 -17
  213. package/dist/plugins/remote-env-e2b.d.ts +27 -21
  214. package/dist/plugins/remote-env-e2b.js +91 -45
  215. package/dist/plugins/remote-env-file-error.d.ts +3 -1
  216. package/dist/plugins/remote-env-file-error.js +9 -3
  217. package/dist/plugins/remote-env-host.d.ts +31 -34
  218. package/dist/plugins/remote-env-host.js +91 -72
  219. package/dist/plugins/remote-env-k8s.d.ts +13 -29
  220. package/dist/plugins/remote-env-k8s.js +63 -55
  221. package/dist/plugins/remote-env-local-docker.d.ts +24 -12
  222. package/dist/plugins/remote-env-local-docker.js +49 -61
  223. package/dist/plugins/remote-env-ssh.d.ts +11 -19
  224. package/dist/plugins/remote-env-ssh.js +74 -57
  225. package/dist/plugins/remote-shell.d.ts +100 -3
  226. package/dist/plugins/remote-shell.js +166 -2
  227. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  228. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  229. package/dist/plugins/retention-lane-store-sql.js +2 -2
  230. package/dist/plugins/retention-store-sql.d.ts +10 -7
  231. package/dist/plugins/retention-store-sql.js +13 -7
  232. package/dist/plugins/run-store-sql.d.ts +13 -3
  233. package/dist/plugins/run-store-sql.js +20 -15
  234. package/dist/plugins/schema-consistency.d.ts +21 -11
  235. package/dist/plugins/schema-consistency.js +15 -9
  236. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  237. package/dist/plugins/schema-drift-sentence.js +38 -0
  238. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  239. package/dist/plugins/schema-ensure-legs.js +2 -2
  240. package/dist/plugins/schema-live-columns.d.ts +1 -1
  241. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  242. package/dist/plugins/session-fork.d.ts +54 -0
  243. package/dist/plugins/session-fork.js +38 -1
  244. package/dist/plugins/session-row-sql.d.ts +22 -0
  245. package/dist/plugins/session-row-sql.js +81 -0
  246. package/dist/plugins/session-rules-column.d.ts +1 -1
  247. package/dist/plugins/session-rules-column.js +1 -1
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  250. package/dist/plugins/session-workspace-cell.js +1 -1
  251. package/dist/plugins/shared-memory-store-sql.js +2 -2
  252. package/dist/plugins/sql-driver.d.ts +10 -5
  253. package/dist/plugins/sql-driver.js +6 -6
  254. package/dist/plugins/sql-errors.js +1 -1
  255. package/dist/plugins/store-backend.d.ts +5 -7
  256. package/dist/plugins/store-backend.js +23 -11
  257. package/dist/plugins/store-contracts.d.ts +25 -5
  258. package/dist/plugins/task-list-store-sql.js +11 -11
  259. package/dist/plugins/tidb-pool.d.ts +12 -7
  260. package/dist/plugins/tidb-pool.js +13 -7
  261. package/dist/plugins/tidb-session-storage.js +4 -4
  262. package/dist/plugins/tidb-session-store.d.ts +57 -26
  263. package/dist/plugins/tidb-session-store.js +127 -59
  264. package/dist/plugins/tool-result-store-sql.d.ts +5 -4
  265. package/dist/plugins/tool-result-store-sql.js +21 -13
  266. package/dist/plugins/user-input-ledger-sql.js +2 -2
  267. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  268. package/dist/plugins/workflow-run-store-sql.js +19 -19
  269. package/dist/plugins/write-behind-counter.d.ts +2 -3
  270. package/dist/plugins/write-behind-counter.js +5 -5
  271. package/dist/project-memory.d.ts +9 -15
  272. package/dist/project-memory.js +5 -4
  273. package/dist/request-key-closure.js +1 -1
  274. package/dist/request-value-closure.d.ts +6 -7
  275. package/dist/request-value-closure.js +1 -3
  276. package/dist/resource-suspend.d.ts +14 -12
  277. package/dist/resource-suspend.js +1 -1
  278. package/dist/resource-window.d.ts +4 -3
  279. package/dist/run-local.d.ts +6 -0
  280. package/dist/run-local.js +34 -35
  281. package/dist/runs.d.ts +10 -5
  282. package/dist/runs.js +52 -66
  283. package/dist/security.d.ts +38 -12
  284. package/dist/security.js +8 -2
  285. package/dist/server-secret-env.d.ts +9 -5
  286. package/dist/session-children-retire.d.ts +27 -9
  287. package/dist/session-children-retire.js +9 -5
  288. package/dist/session-incarnation.d.ts +20 -10
  289. package/dist/session-incarnation.js +5 -3
  290. package/dist/session-policy-wire.d.ts +1 -1
  291. package/dist/session-row.d.ts +114 -5
  292. package/dist/session-row.js +55 -5
  293. package/dist/session-titler.d.ts +2 -2
  294. package/dist/sql-engine-posture.d.ts +1 -1
  295. package/dist/sql-engine-posture.js +1 -1
  296. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  297. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  298. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  299. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  300. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  301. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  302. package/dist/task-a2a.d.ts +1 -1
  303. package/dist/task-a2a.js +5 -6
  304. package/dist/task-cwd.d.ts +6 -1
  305. package/dist/task-mcp.d.ts +22 -9
  306. package/dist/task-mcp.js +17 -9
  307. package/dist/terminal.d.ts +27 -0
  308. package/dist/terminal.js +6 -0
  309. package/dist/tool-approval.d.ts +6 -0
  310. package/dist/tool-approval.js +8 -1
  311. package/dist/trace/project.d.ts +1 -0
  312. package/dist/trace/project.js +1 -0
  313. package/dist/trace/redact.d.ts +3 -1
  314. package/dist/trace/redact.js +80 -28
  315. package/dist/trace/self-minted-wire.d.ts +169 -2
  316. package/dist/trace/self-minted-wire.js +54 -1
  317. package/dist/url-refusal.d.ts +1 -1
  318. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  319. package/dist/web-admin-client/apply-effective.js +20 -10
  320. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  321. package/dist/web-admin-client/candidate-gate.js +1 -1
  322. package/dist/web-admin-client/http-client.d.ts +5 -4
  323. package/dist/web-admin-client/http-client.js +2 -2
  324. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  325. package/dist/web-admin-client/read-warnings.js +5 -9
  326. package/dist/web-admin-client/skills-mcp.js +3 -2
  327. package/dist/web-admin-client/types.d.ts +6 -0
  328. package/package.json +3 -3
  329. package/wire-changes/server@7.139.0.json +348 -0
  330. package/wire-changes/server@7.140.0.json +184 -0
  331. package/dist/http/device-placement.d.ts +0 -175
  332. package/dist/http/device-placement.js +0 -145
@@ -1,10 +1,114 @@
1
+ import { type SessionLogMark, type SessionRowExpectation } from "@sema-agent/core";
1
2
  /** `title` 列的宽度(字符)= 用户标题的上界。两方言 DDL 内插它。 */
2
3
  export declare const SESSION_TITLE_MAX_CHARS = 120;
3
- /** 会话行的可写位(体闭集;两键至少一键)。会话店唯一写口 `patchSessionMeta` 的形参。`title: null` = 清回自动标题(列写 NULL)。 */
4
+ /** 会话行的可写位(体闭集;两键至少一键)。会话店唯一行格写口 `writeSessionRowCells` 的格参。`title: null` = 清回自动标题(列写 NULL)。 */
4
5
  export interface SessionRowPatch {
5
6
  readonly title?: string | null;
6
7
  readonly pinned?: boolean;
7
8
  }
9
+ /** 一行可写格的整组当前值(每格必在;没被写过的格 = 它的缺省值)。 */
10
+ export type SessionRowCells = {
11
+ readonly [K in keyof SessionRowPatch]-?: Exclude<SessionRowPatch[K], undefined>;
12
+ };
13
+ /**
14
+ * 可写格的闭集与每格缺省值 —— **一只记录**(头注「格的闭集与缺省值是一只记录」):PATCH 体闭集的键表与 {@link sessionRowTagOf} 的取格都读它;
15
+ * 映射型把它与 {@link SessionRowPatch} 双向绑住(漏一键 / 多一键 = 编译红)。
16
+ */
17
+ export declare const SESSION_ROW_CELL_DEFAULTS: SessionRowCells;
18
+ /** 行标签的形:SHA-256 的 base64url(无填充)= 43 个 `[A-Za-z0-9_-]` 字符。体读者对 `expectedRowTag` 判的就是它。 */
19
+ export declare const SESSION_ROW_TAG_SHAPE: RegExp;
20
+ /** 算一只行标签要的四样:会话 id、属主、这一位住客的化身(非空)、可写格的整组当前值。 */
21
+ export interface SessionRowTagInput {
22
+ readonly sessionId: string;
23
+ readonly owner: string | null;
24
+ readonly incarnation: string;
25
+ readonly cells: SessionRowCells;
26
+ }
27
+ /**
28
+ * 行标签 —— 唯一一只(头注「行标签」):纯函数、无进程密钥,多副本对同一行算出同一个串。格按 {@link SESSION_ROW_CELL_DEFAULTS} 的键取、
29
+ * 值等于缺省的不入、按键名升序成 `[[键, 值], …]`。化身只以这只单向摘要的形式出进程(`session-incarnation.ts` 头注)。
30
+ */
31
+ export declare function sessionRowTagOf(row: SessionRowTagInput): string;
32
+ /**
33
+ * 列表行上的 `rowTag` 键(present-iff 这一行可被条件写 = 店有行写口 ∧ 行上有化身):三只店的列表行映射都经它,化身那一格由调用方经
34
+ * `incarnationFromCell(…, "absent")` 解码好交进来(`undefined` = 行上没有令牌 ⇒ 键缺席,不铸)。
35
+ */
36
+ export declare function sessionRowTagKey(row: {
37
+ readonly sessionId: string;
38
+ readonly owner: string | null;
39
+ readonly incarnation: string | undefined;
40
+ readonly cells: SessionRowCells;
41
+ }): {
42
+ readonly rowTag?: string;
43
+ };
44
+ /** 客户端写(PATCH):行期望 + 可选的行标签(只有 PATCH 入口给)。 */
45
+ export interface ClientSessionRowWrite {
46
+ readonly writer: "client";
47
+ readonly row: SessionRowExpectation;
48
+ readonly rowTag?: string;
49
+ }
50
+ /** 行重放(2c 导入落定后的那一笔):只有行期望 —— 它是镜像行的名写者,不判镜像、不带标签。 */
51
+ export interface ReplaySessionRowWrite {
52
+ readonly writer: "replay";
53
+ readonly row: SessionRowExpectation;
54
+ }
55
+ /** 写原语的写法(写者身份闭集两员)。 */
56
+ export type SessionRowWrite = ClientSessionRowWrite | ReplaySessionRowWrite;
57
+ /**
58
+ * 写原语的结局(闭集四员):`written` / `changed` 带提交(回滚)之后另读的那一行(列表行投影,含新 `rowTag`);`mirror`(客户端写在只读镜像上)/
59
+ * `absent`(行期望不中 / 没有行 / 委派分区行 / 应答行已不是判过的那一位住客)一个字节没写。
60
+ */
61
+ export type SessionRowWriteOutcome<Row> = {
62
+ readonly kind: "written";
63
+ readonly row: Row;
64
+ } | {
65
+ readonly kind: "changed";
66
+ readonly row: Row;
67
+ } | {
68
+ readonly kind: "mirror";
69
+ } | {
70
+ readonly kind: "absent";
71
+ };
72
+ /** 行重放的结局:`changed` / `mirror` 对它不可达(判官对 `replay` 不判标签、不判镜像)。 */
73
+ export type ReplaySessionRowWriteOutcome<Row> = Extract<SessionRowWriteOutcome<Row>, {
74
+ readonly kind: "written" | "absent";
75
+ }>;
76
+ /** 写原语在锁下读到的那一行 —— 三店各自读成这一形,交同一只判官 {@link judgeSessionRowWrite}。 */
77
+ export interface LockedSessionRow {
78
+ readonly owner: string | null;
79
+ /** 化身那一格(`incarnationFromCell(…, "absent")` 解码:空 / 非串 ⇒ `undefined` = 住客未知,与 core 同读)。 */
80
+ readonly incarnation: string | undefined;
81
+ /** 委派分区行(SQL `placement_kind` 非空;local 没有)—— 不在列表面上,不写。 */
82
+ readonly placed: boolean;
83
+ /** 执行格是 `mirror`(写者在别处,本处只读)。 */
84
+ readonly mirror: boolean;
85
+ readonly cells: SessionRowCells;
86
+ }
87
+ /** 判官的四答(`write` = 可以写;其余三答 = 一个字节不写)。 */
88
+ export type SessionRowVerdict = "write" | "changed" | "mirror" | "absent";
89
+ /**
90
+ * 写原语的**唯一判官**(头注「写原语」):三店在各自的锁下读出那一行、交这一只,判序因此逐字同形 ——
91
+ * ① **行期望**:core 的 `sessionRowMatches(how.row, 锁住的行, logNow)`;不中、没有行(`undefined`)、委派分区行 ⇒ `absent`;
92
+ * ② **写者身份**:`client` ∧ 执行格为 `mirror` ⇒ `mirror`(镜像在本处只读;`replay` 是镜像的名写者,照写);
93
+ * ③ **标签**:`client` 带 `rowTag` ∧ 锁住的行算出的标签({@link sessionRowTagOf})≠ 它 ⇒ `changed`(行上没有化身 ⇒ 算不出标签 ⇒ 任何标签都不中);
94
+ * ④ `write`。
95
+ * 没有「已生效 ⇒ 写」的豁免:标签不中一律 `changed` —— 整体摘要分不出「住客换了」还是「格变了」,豁免会让恰好已有目标值的新住客拿到写。
96
+ * `logNow` 只在行期望带 `log` 时被调(调用方在同一把锁下先读好那只 mark)。
97
+ */
98
+ export declare function judgeSessionRowWrite(sessionId: string, how: SessionRowWrite, locked: LockedSessionRow | undefined, logNow: () => SessionLogMark): SessionRowVerdict;
99
+ /** 应答行:提交 / 回滚之后另读的那一行 + 读到它时行上的住客(属主 + 化身那一格)。 */
100
+ export interface SessionRowAnswer<Row> {
101
+ readonly row: Row;
102
+ readonly occupant: {
103
+ readonly owner: string | null;
104
+ readonly incarnation: string | undefined;
105
+ };
106
+ }
107
+ /**
108
+ * 判官答 `write` / `changed` 之后的结局 —— 三店同一只:应答行(提交 / 回滚之后另读)仍是判过的那一位住客(同一只 core 判官 `sessionRowMatches`,只比行期望的住客半,
109
+ * 不比日志 mark —— 那一维判的是写之前)⇒ `written` / `changed` 带它;读不到、或已换了住客 / 属主 ⇒ `absent`(不把另一位住客的行答给这位调用方)。
110
+ */
111
+ export declare function settleSessionRowWrite<Row>(how: SessionRowWrite, verdict: "write" | "changed", answer: SessionRowAnswer<Row> | undefined): SessionRowWriteOutcome<Row>;
8
112
  /** 一条已分类的拒:码 + 不回显输入的句子。 */
9
113
  export interface SessionRowProblem {
10
114
  readonly code: "request.body_shape" | "request.field_invalid";
@@ -27,13 +131,18 @@ export declare function readSessionPinned(v: unknown): {
27
131
  } | {
28
132
  readonly problem: SessionRowProblem;
29
133
  };
134
+ /** PATCH 体读出来的东西:要写的格 + 可选的前置条件(客户端读到的行标签)。 */
135
+ export interface SessionRowPatchRequest {
136
+ readonly patch: SessionRowPatch;
137
+ /** 在场 = 「这一行仍是我读到的那一份才写」(写原语在锁住的那一行上比 {@link sessionRowTagOf});缺席 = 无条件写。 */
138
+ readonly expectedRowTag?: string;
139
+ }
30
140
  /**
31
- * `PATCH /v1/sessions/:id` 体的**唯一**读者。形不对(非对象 / 空 / 表外键)⇒ `request.body_shape`;两键各走自己那一格的读者。
141
+ * `PATCH /v1/sessions/:id` 体的**唯一**读者。形不对(非对象 / 表外键 / 一格可写位都没有 —— 只带 `expectedRowTag` 也算)⇒ `request.body_shape`;
142
+ * 各格走自己那一格的读者;`expectedRowTag` 不是 {@link SESSION_ROW_TAG_SHAPE} ⇒ `request.field_invalid`。先形后值,值的判序:`title` → `pinned` → `expectedRowTag`。
32
143
  * 拒句不回显调用方的键名或值。
33
144
  */
34
- export declare function readSessionRowPatch(body: unknown): {
35
- readonly patch: SessionRowPatch;
36
- } | {
145
+ export declare function readSessionRowPatch(body: unknown): SessionRowPatchRequest | {
37
146
  readonly problem: SessionRowProblem;
38
147
  };
39
148
  //# sourceMappingURL=session-row.d.ts.map
@@ -1,7 +1,50 @@
1
- import { normalizePeerName } from "@sema-agent/core";
1
+ import { createHash } from "node:crypto";
2
+ import { normalizePeerName, sessionRowMatches } from "@sema-agent/core";
2
3
  export const SESSION_TITLE_MAX_CHARS = 120;
3
- const SESSION_ROW_PATCH_KEYS = { title: true, pinned: true };
4
- const BODY_SHAPE_MESSAGE = "the body must be a JSON object carrying at least one of `title` and `pinned`, and nothing else";
4
+ export const SESSION_ROW_CELL_DEFAULTS = { title: null, pinned: false };
5
+ const SESSION_ROW_TAG_DOMAIN = "sema.session-row-tag/1";
6
+ export const SESSION_ROW_TAG_SHAPE = /^[A-Za-z0-9_-]{43}$/;
7
+ export function sessionRowTagOf(row) {
8
+ const keys = Object.keys(SESSION_ROW_CELL_DEFAULTS).sort();
9
+ const cells = keys.filter((k) => row.cells[k] !== SESSION_ROW_CELL_DEFAULTS[k]).map((k) => [k, row.cells[k]]);
10
+ return createHash("sha256").update(JSON.stringify([SESSION_ROW_TAG_DOMAIN, row.sessionId, row.owner, row.incarnation, cells]), "utf8").digest("base64url");
11
+ }
12
+ export function sessionRowTagKey(row) {
13
+ return row.incarnation === undefined ? {} : { rowTag: sessionRowTagOf({ sessionId: row.sessionId, owner: row.owner, incarnation: row.incarnation, cells: row.cells }) };
14
+ }
15
+ export function judgeSessionRowWrite(sessionId, how, locked, logNow) {
16
+ if (locked === undefined || !sessionRowMatches(how.row, { owner: locked.owner, incarnation: locked.incarnation }, logNow) || locked.placed)
17
+ return "absent";
18
+ switch (how.writer) {
19
+ case "replay":
20
+ return "write";
21
+ case "client": {
22
+ if (locked.mirror)
23
+ return "mirror";
24
+ if (how.rowTag === undefined)
25
+ return "write";
26
+ const current = locked.incarnation === undefined ? undefined : sessionRowTagOf({ sessionId, owner: locked.owner, incarnation: locked.incarnation, cells: locked.cells });
27
+ return current === how.rowTag ? "write" : "changed";
28
+ }
29
+ default: {
30
+ const unknownWriter = how;
31
+ throw new Error(`judgeSessionRowWrite: no writer ${JSON.stringify(unknownWriter)}`);
32
+ }
33
+ }
34
+ }
35
+ export function settleSessionRowWrite(how, verdict, answer) {
36
+ const judged = how.row.row;
37
+ if (judged === null || answer === undefined)
38
+ return { kind: "absent" };
39
+ if (!sessionRowMatches({ row: judged }, answer.occupant, unreachableLogMark))
40
+ return { kind: "absent" };
41
+ return verdict === "write" ? { kind: "written", row: answer.row } : { kind: "changed", row: answer.row };
42
+ }
43
+ function unreachableLogMark() {
44
+ throw new Error("settleSessionRowWrite: the answer row's occupant check carries no log mark, yet the judge asked for one");
45
+ }
46
+ const BODY_SHAPE_MESSAGE = "the body must be a JSON object carrying at least one of `title` and `pinned` (and, optionally, `expectedRowTag`), and nothing else";
47
+ const ROW_TAG_INVALID_MESSAGE = "expectedRowTag must be the `rowTag` string a session list row carries (43 characters: letters, digits, `-` and `_`)";
5
48
  const EMPTY_TITLE_MESSAGE = "title must not be the empty string — send `null` to clear it back to the automatic title";
6
49
  const INVISIBLE_TITLE_MESSAGE = "title must contain at least one visible character";
7
50
  const C0_CONTROL = /[\u0000-\u001f]/u;
@@ -25,11 +68,13 @@ export function readSessionTitle(v) {
25
68
  export function readSessionPinned(v) {
26
69
  return typeof v === "boolean" ? { pinned: v } : { problem: { code: "request.field_invalid", message: "pinned must be a boolean" } };
27
70
  }
71
+ const EXPECTED_ROW_TAG_KEY = "expectedRowTag";
28
72
  export function readSessionRowPatch(body) {
29
73
  if (body === null || typeof body !== "object" || Array.isArray(body))
30
74
  return { problem: { code: "request.body_shape", message: BODY_SHAPE_MESSAGE } };
31
75
  const keys = Object.keys(body);
32
- if (keys.length === 0 || keys.some((k) => !Object.hasOwn(SESSION_ROW_PATCH_KEYS, k))) {
76
+ const isCell = (k) => Object.hasOwn(SESSION_ROW_CELL_DEFAULTS, k);
77
+ if (!keys.some(isCell) || keys.some((k) => !isCell(k) && k !== EXPECTED_ROW_TAG_KEY)) {
33
78
  return { problem: { code: "request.body_shape", message: BODY_SHAPE_MESSAGE } };
34
79
  }
35
80
  const b = body;
@@ -46,6 +91,11 @@ export function readSessionRowPatch(body) {
46
91
  return read;
47
92
  patch = { ...patch, pinned: read.pinned };
48
93
  }
49
- return { patch };
94
+ if (!(EXPECTED_ROW_TAG_KEY in b))
95
+ return { patch };
96
+ const tag = b.expectedRowTag;
97
+ if (typeof tag !== "string" || !SESSION_ROW_TAG_SHAPE.test(tag))
98
+ return { problem: { code: "request.field_invalid", message: ROW_TAG_INVALID_MESSAGE } };
99
+ return { patch, expectedRowTag: tag };
50
100
  }
51
101
  //# sourceMappingURL=session-row.js.map
@@ -5,8 +5,8 @@
5
5
  * submission's acceptance seat (`run-acceptance.ts`: the objective entered the conversation, or a result-only verify /
6
6
  * cascade leg came back having done work). A run the engine refuses before acceptance never reaches the titler (no
7
7
  * model call for a refused input). No need to wait for the run to finish, and the web Recents gets the title sooner. Fills a NULL title only (`setTitleIfNull`,
8
- * store-side `WHERE title IS NULL`) — the user writes the SAME column unconditionally (`PATCH /v1/sessions/:id` →
9
- * `patchSessionMeta`): a user-set title is never overwritten (the probe below reads "titled" and skips the model
8
+ * store-side `WHERE title IS NULL`) — the user writes the SAME column (`PATCH /v1/sessions/:id` → the session row's write
9
+ * primitive `writeSessionRowCells`, unconditional unless the request carries `expectedRowTag`): a user-set title is never overwritten (the probe below reads "titled" and skips the model
10
10
  * call), and a title the user cleared (`title: null` ⇒ NULL) is filled again at the next submit (law: `session-row.ts`).
11
11
  * "Has it a title?" is read from the column on every submit (one point read) — the process keeps NO "already titled"
12
12
  * memo, because such a memo would be a second answer to that question that a clear turns stale. Generation = one
@@ -38,7 +38,7 @@ export declare function sqlPoolsOf(backend: Pick<StoreBackend, "mysqlPool" | "pg
38
38
  pgPool: PgPool | undefined;
39
39
  };
40
40
  /**
41
- * `ServiceDeps.sqlEngine` 座的**值形**判:`{ dialect: "tidb" | "pg", facts }`,`facts` 是 `null`(池上连接初始化还没成功)或一份引擎事实 —— 判到墙判官 `sqlWallsOf` 读的那几位
41
+ * `ServiceDeps.sqlEngine` 座的**值形**判:`{ dialect: "mysql" | "pg", facts }`,`facts` 是 `null`(池上连接初始化还没成功)或一份引擎事实 —— 判到墙判官 `sqlWallsOf` 读的那几位
42
42
  * (`maxRowBytes` / `maxPacketBytes` 是 `null` 或数,`wallSources` 两位是出处词)。一个函数(7.131.x 的事实读口 `() => facts` 只改了键名)、缺 `facts` 的对象、错方言词都不是它。
43
43
  * `facts` 是活读(getter):这里读一次,判的是装配这一刻看得到的那一份。
44
44
  */
@@ -2,7 +2,7 @@ import { mysqlDriver, pgDriver } from "./plugins/sql-driver.js";
2
2
  export function sqlPoolsOf(backend) {
3
3
  return { mysqlPool: backend?.mysqlPool?.(), pgPool: backend?.pgPool?.() };
4
4
  }
5
- const DIALECT_WORDS = { tidb: true, pg: true };
5
+ const DIALECT_WORDS = { mysql: true, pg: true };
6
6
  const WALL_SOURCE_WORDS = { reported: true, assumed: true };
7
7
  const isWallBytes = (v) => v === null || (typeof v === "number" && Number.isFinite(v) && v >= 0);
8
8
  const isWallSourceWord = (v) => typeof v === "string" && Object.hasOwn(WALL_SOURCE_WORDS, v);
@@ -31,8 +31,8 @@
31
31
  */
32
32
  import type { Pool } from "mysql2/promise";
33
33
  import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "@sema-agent/core";
34
- import type { PgQueryFn } from "./pg-query.js";
35
- import { type IndexSpec } from "./ensure-index.js";
34
+ import type { PgQueryFn } from "../plugins/pg-query.js";
35
+ import { type IndexSpec } from "../plugins/ensure-index.js";
36
36
  export declare const BACKGROUND_AGENT_TABLE = "background_agent";
37
37
  type ReapOpts = {
38
38
  maxAgeMs?: number;
@@ -1,8 +1,8 @@
1
1
  import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "@sema-agent/core";
2
- import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
3
- import { isMysqlDupKeyError } from "./sql-errors.js";
4
- import { ensureIndexes } from "./ensure-index.js";
5
- import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
2
+ import { pgSafeJsonStringify, pgSanitizeText } from "../plugins/pg-safe-json.js";
3
+ import { isMysqlDupKeyError } from "../plugins/sql-errors.js";
4
+ import { ensureIndexes } from "../plugins/ensure-index.js";
5
+ import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText } from "../plugins/sql-json-column.js";
6
6
  import { backgroundAgentNameOf } from "../background-agent-name.js";
7
7
  export const BACKGROUND_AGENT_TABLE = "background_agent";
8
8
  function summaryCols(dialect) {
@@ -215,7 +215,7 @@ export class TiDBBackgroundAgentStore {
215
215
  }
216
216
  }
217
217
  async get(handle, scope) {
218
- const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("tidb", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]));
218
+ const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("mysql", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]));
219
219
  return rows.length > 0 ? readRecordColumn(rows[0].record_json, handle) : null;
220
220
  }
221
221
  async update(handle, scope, record, expect) {
@@ -234,12 +234,12 @@ export class TiDBBackgroundAgentStore {
234
234
  return res.affectedRows === 1;
235
235
  }
236
236
  async listBySession(scope, sessionId, opts) {
237
- const [rows] = (await this.pool.query(`SELECT ${summaryCols("tidb")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND (session_anchor = ? OR root_session_id = ?)${opts?.status !== undefined ? " AND status = ?" : ""}
237
+ const [rows] = (await this.pool.query(`SELECT ${summaryCols("mysql")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND (session_anchor = ? OR root_session_id = ?)${opts?.status !== undefined ? " AND status = ?" : ""}
238
238
  ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, sessionId, sessionId, opts.status] : [scope, sessionId, sessionId]));
239
239
  return rows.map(rowToSummary);
240
240
  }
241
241
  async listByScope(scope, opts) {
242
- const [rows] = (await this.pool.query(`SELECT ${summaryCols("tidb")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ?${opts?.status !== undefined ? " AND status = ?" : ""}
242
+ const [rows] = (await this.pool.query(`SELECT ${summaryCols("mysql")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ?${opts?.status !== undefined ? " AND status = ?" : ""}
243
243
  ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, opts.status] : [scope]));
244
244
  return rows.map(rowToSummary);
245
245
  }
@@ -252,7 +252,7 @@ export class TiDBBackgroundAgentStore {
252
252
  return 0;
253
253
  const flipped = new Set();
254
254
  if (opts.staleRunningMaxAgeMs !== undefined) {
255
- const [stale] = (await this.pool.query(`SELECT handle, ${jsonColumnText("tidb", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
255
+ const [stale] = (await this.pool.query(`SELECT handle, ${jsonColumnText("mysql", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
256
256
  for (const row of stale) {
257
257
  const live = readRecordColumn(row.record_json, String(row.handle));
258
258
  if (await this.update(String(row.handle), scope, flippedForm(live, now), { rev: live.rev }))
@@ -1,9 +1,8 @@
1
1
  import { type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions } from "@sema-agent/core";
2
2
  import type { Pool as MySqlPool } from "mysql2/promise";
3
3
  import type { Pool as PgPool } from "pg";
4
- import { type JsonColumnShape } from "./sql-json-column.js";
5
- import { type SqlDriver } from "./sql-driver.js";
6
- import type { Logger } from "../observability/logger.js";
4
+ import { type JsonColumnShape } from "../plugins/sql-json-column.js";
5
+ import { type SqlDriver } from "../plugins/sql-driver.js";
7
6
  /** `session_policy.policy_key` 的**唯一铸造点**(core 复合键的 sha256)。
8
7
  *
9
8
  * 🔴 导出而不是让收编腿再抄一份(clay 宪法「schema 单一属主禁复制」):[ref] form b 的身份重绑要
@@ -45,8 +44,11 @@ export type StoredSessionRulesBody = SessionPermissionRules & Pick<StoredSession
45
44
  * + 恰好解一次 `decodeJsonColumn` + 折码 `foldJsonColumn`);本店只留「形判据是哪一只」与「坏体折成哪个码」。
46
45
  */
47
46
  export declare function readSessionRulesBody(body: unknown): JsonColumnShape<StoredSessionRulesBody>;
48
- /** 本店唯一的日志口:抹除封键那一臂([ref])的点名。缺席 ⇒ 退回 `console.warn`(不静默)。 */
49
- export type SessionPolicyStoreLogger = Pick<Logger, "warn">;
47
+ /** 本店唯一的日志口:抹除封键那一臂([ref])的点名。缺席 ⇒ 退回 `console.warn`(不静默)。
48
+ * 结构型,与服务日志口的 `warn` 逐位同形:本目录对服务的观测模块零依赖(目录的可搬性),服务的日志口照样直接交进来。 */
49
+ export type SessionPolicyStoreLogger = {
50
+ warn(msg: string, fields?: Record<string, unknown>): void;
51
+ };
50
52
  /** Dual-dialect durable SessionPolicyStore. See the file header for the dialect-delta ledger. */
51
53
  export declare class SqlSessionPolicyStore implements SessionPolicyStore {
52
54
  private readonly db;
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { judgeSessionRulesWrite, readSessionRulesShape, SessionPolicyError } from "@sema-agent/core";
3
- import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
4
- import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "./sql-json-column.js";
5
- import { pgHasUnstorable } from "./pg-safe-json.js";
6
- import { mysqlDriver, pgDriver } from "./sql-driver.js";
7
- import { isDeadlockError, isDupKeyError } from "./sql-errors.js";
3
+ import { SESSION_RULES_COLUMN } from "../plugins/session-rules-column.js";
4
+ import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "../plugins/sql-json-column.js";
5
+ import { pgHasUnstorable } from "../plugins/pg-safe-json.js";
6
+ import { mysqlDriver, pgDriver } from "../plugins/sql-driver.js";
7
+ import { isDeadlockError, isDupKeyError } from "../plugins/sql-errors.js";
8
8
  export const policyKeyFor = (sessionId, principal) => createHash("sha256").update(JSON.stringify([sessionId, principal ?? null])).digest("hex");
9
9
  const policyKey = policyKeyFor;
10
10
  const RETRY_LIMIT = 8;
@@ -63,15 +63,15 @@ export class SqlSessionPolicyStore {
63
63
  this.db = db;
64
64
  this.logger = logger;
65
65
  }
66
- q(tidb, pg) {
67
- return this.db.dialect === "tidb" ? tidb : pg;
66
+ q(mysql, pg) {
67
+ return this.db.dialect === "mysql" ? mysql : pg;
68
68
  }
69
69
  isDupKey(e) {
70
70
  return isDupKeyError(this.db.dialect, e);
71
71
  }
72
72
  async getRules(sessionId, principal) {
73
73
  const key = policyKey(sessionId, principal);
74
- const { rows } = await this.db.query(this.q(`SELECT 'row' AS src, ${jsonColumnText("tidb", RULES)}, rev, gen FROM session_policy WHERE policy_key = ?` +
74
+ const { rows } = await this.db.query(this.q(`SELECT 'row' AS src, ${jsonColumnText("mysql", RULES)}, rev, gen FROM session_policy WHERE policy_key = ?` +
75
75
  " UNION ALL SELECT 'seal', NULL, rev, gen FROM session_policy_rev_mark WHERE policy_key = ? AND rev = ?", `SELECT 'row' AS src, ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE policy_key = $1` +
76
76
  " UNION ALL SELECT 'seal', NULL, rev, gen FROM session_policy_rev_mark WHERE policy_key = $2 AND rev = $3"), [key, key, SEALED_MARK_REV]);
77
77
  if (rows.some((r) => r.src === "seal"))
@@ -82,7 +82,7 @@ export class SqlSessionPolicyStore {
82
82
  return rowToStored(row, `for session ${JSON.stringify(sessionId)}`);
83
83
  }
84
84
  async listBySession(sessionId) {
85
- const { rows } = await this.db.query(this.q(`SELECT 'row' AS src, principal, ${jsonColumnText("tidb", RULES)}, rev, gen FROM session_policy WHERE session_id = ?` +
85
+ const { rows } = await this.db.query(this.q(`SELECT 'row' AS src, principal, ${jsonColumnText("mysql", RULES)}, rev, gen FROM session_policy WHERE session_id = ?` +
86
86
  " UNION ALL SELECT 'seal', NULL, NULL, rev, gen FROM session_policy_rev_mark WHERE session_id = ? AND rev = ?", `SELECT 'row' AS src, principal, ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE session_id = $1` +
87
87
  " UNION ALL SELECT 'seal', NULL, NULL, rev, gen FROM session_policy_rev_mark WHERE session_id = $2 AND rev = $3"), [sessionId, sessionId, SEALED_MARK_REV]);
88
88
  if (rows.some((r) => r.src === "seal"))
@@ -95,7 +95,7 @@ export class SqlSessionPolicyStore {
95
95
  async putRules(sessionId, principal, rules, opts) {
96
96
  const key = policyKey(sessionId, principal);
97
97
  return this.retrying(() => this.tx(async (conn) => {
98
- const { rows } = await conn.query(this.q(`SELECT ${jsonColumnText("tidb", RULES)}, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE`, `SELECT ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE`), [key]);
98
+ const { rows } = await conn.query(this.q(`SELECT ${jsonColumnText("mysql", RULES)}, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE`, `SELECT ${jsonColumnText("pg", RULES)}, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE`), [key]);
99
99
  const prior = rows[0] ? rowToStored(rows[0], `for session ${JSON.stringify(sessionId)} (pre-write snapshot)`) : undefined;
100
100
  const priorRev = prior?.rev ?? 0;
101
101
  if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
@@ -157,12 +157,12 @@ export class SqlSessionPolicyStore {
157
157
  sealedKeys.push({ policyKey: String(r.policy_key), rev: String(r.rev), gen: String(r.gen) });
158
158
  params.push(String(r.policy_key), sessionId, readable ? counters.rev : SEALED_MARK_REV, readable ? counters.gen + 1 : 0, now);
159
159
  const base = params.length - 5;
160
- return this.db.dialect === "tidb" ? "(?,?,?,?,?)" : `($${base + 1},$${base + 2},$${base + 3},$${base + 4},$${base + 5})`;
160
+ return this.db.dialect === "mysql" ? "(?,?,?,?,?)" : `($${base + 1},$${base + 2},$${base + 3},$${base + 4},$${base + 5})`;
161
161
  });
162
162
  await conn.query(this.q(`INSERT INTO session_policy_rev_mark (policy_key, session_id, rev, gen, erased_at) VALUES ${tuples.join(",")}` +
163
163
  " ON DUPLICATE KEY UPDATE rev = GREATEST(session_policy_rev_mark.rev, VALUES(rev)), gen = GREATEST(session_policy_rev_mark.gen + 1, VALUES(gen)), erased_at = VALUES(erased_at)", `INSERT INTO session_policy_rev_mark (policy_key, session_id, rev, gen, erased_at) VALUES ${tuples.join(",")}` +
164
164
  " ON CONFLICT (policy_key) DO UPDATE SET rev = GREATEST(session_policy_rev_mark.rev, EXCLUDED.rev), gen = GREATEST(session_policy_rev_mark.gen + 1, EXCLUDED.gen), erased_at = EXCLUDED.erased_at"), params);
165
- const holes = keys.map((_, i) => (this.db.dialect === "tidb" ? "?" : `$${i + 1}`)).join(",");
165
+ const holes = keys.map((_, i) => (this.db.dialect === "mysql" ? "?" : `$${i + 1}`)).join(",");
166
166
  await conn.query(`DELETE FROM session_policy WHERE policy_key IN (${holes})`, keys);
167
167
  return sealedKeys;
168
168
  }));
@@ -26,7 +26,7 @@
26
26
  import type { Pool as MySqlPool } from "mysql2/promise";
27
27
  import type { Pool as PgPool } from "pg";
28
28
  import type { UsageWindow, UsageWindowReading, UsageWindowRecord, UsageWindowStore } from "@sema-agent/core";
29
- import { type JsonColumnShape } from "./sql-json-column.js";
29
+ import { type JsonColumnShape } from "../plugins/sql-json-column.js";
30
30
  export declare const USAGE_WINDOW_TABLE = "usage_window";
31
31
  /**
32
32
  * `usage_window.record` 的**形读者**([ref] 车③a:本店既有的那一份深度判据逐字搬进来,一个字没改判)。
@@ -1,5 +1,5 @@
1
1
  import { EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord } from "@sema-agent/core";
2
- import { decodeJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
2
+ import { decodeJsonColumn, foldJsonColumn, jsonColumnText } from "../plugins/sql-json-column.js";
3
3
  export const USAGE_WINDOW_TABLE = "usage_window";
4
4
  class UsageRecordShapeProblem extends Error {
5
5
  }
@@ -84,7 +84,7 @@ export class TiDBUsageWindowStore {
84
84
  try {
85
85
  await c.beginTransaction();
86
86
  await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE usage_key = usage_key`, [key, JSON.stringify(EMPTY_USAGE_WINDOW_RECORD), at]);
87
- const [rows] = (await c.query(`SELECT ${jsonColumnText("tidb", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
87
+ const [rows] = (await c.query(`SELECT ${jsonColumnText("mysql", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
88
88
  if (rows.length === 0) {
89
89
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
90
90
  }
@@ -102,7 +102,7 @@ export class TiDBUsageWindowStore {
102
102
  }
103
103
  }
104
104
  async read(key, windows, now) {
105
- const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("tidb", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
105
+ const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("mysql", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
106
106
  const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
107
107
  return readUsageRecord(record, windows, now);
108
108
  }
@@ -57,7 +57,7 @@ export declare const A2A_PEER_DISPOSITION: {
57
57
  readonly toolAxes: "engine";
58
58
  };
59
59
  /** 请求体 `a2aPeers[i]` 的键闭集 = 处置表的键(书写序即判序)。 */
60
- export declare const A2A_PEER_ENTRY_KEYS: readonly ("name" | "toolAxes" | "url" | "principalHeader" | "cardUrl" | "allowSkills" | "headers")[];
60
+ export declare const A2A_PEER_ENTRY_KEYS: readonly ("name" | "headers" | "principalHeader" | "toolAxes" | "url" | "cardUrl" | "allowSkills")[];
61
61
  /**
62
62
  * **请求入口的一次读**(`boot/resolve-spec.ts` 在腿首铸一份,受理门与读器——续跑重放腿只有读器——都吃这一份):`body.a2aPeers` 若是数组,返回同形数组
63
63
  * (每个对象条目换成它的 {@link PeerSnapshot};非对象条目原样;长度保真 —— 界外的条数仍是受理门撞帽拒与读器丢弃留声的依据,只快照界内的条目);不是数组(缺席 / `null` / 别的形)原样返回。
package/dist/task-a2a.js CHANGED
@@ -8,7 +8,7 @@ import { readRequestToolAxes } from "./request-tool-axes.js";
8
8
  import { MAX_KEY_ECHO, echoKeyName, keyClosureClause, scanKeyNames } from "./request-key-closure.js";
9
9
  import { urlCredentialHeaderExit, urlCredentialRefusalReason } from "./url-credential-refusal.js";
10
10
  import { declarationEntryNotObject, declarationIssueFacts, declarationNameIssue, declarationOverCapFacts, declaredName, judgeDeclarationKeys, requestDeclarationRefusal, throwDeclarationRefusal, } from "./request-declaration-refusal.js";
11
- import { principalHeaderOwnershipIssue } from "./task-mcp.js";
11
+ import { sendHeaderOwnershipIssue } from "./task-mcp.js";
12
12
  import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
13
13
  export const MAX_REQUEST_A2A_PEERS = 32;
14
14
  export function a2aInjectionHonored(config) {
@@ -52,6 +52,7 @@ function peerUrlIssue(key) {
52
52
  return found === undefined ? undefined : { path: key, reason: urlCredentialRefusalReason(found, urlCredentialHeaderExit(`${at}.headers`)), credential: found };
53
53
  };
54
54
  }
55
+ const A2A_SEND_HEADER_PATHS = { headers: "headers", principalHeader: "principalHeader" };
55
56
  const HEADERS_RESERVED_MEMBER_PATH = "headers.__proto__";
56
57
  const PEER_SNAPSHOT = Symbol("a2aPeerSnapshot");
57
58
  function openCloneNode(v, ancestors) {
@@ -170,11 +171,9 @@ const A2A_PEER_KEY_JUDGES = {
170
171
  const reserved = read.stripped[0];
171
172
  return reserved === undefined ? undefined : { path: HEADERS_RESERVED_MEMBER_PATH, reason: reserved.fault };
172
173
  },
173
- principalHeader: (o) => o.principalHeader === undefined
174
- ? undefined
175
- : typeof o.principalHeader !== "string"
176
- ? { path: "principalHeader", reason: `must be a string (a header name) — got ${describeValueShape(o.principalHeader)}` }
177
- : principalHeaderOwnershipIssue(o.principalHeader, "a2a", "principalHeader"),
174
+ principalHeader: (o) => o.principalHeader !== undefined && typeof o.principalHeader !== "string"
175
+ ? { path: "principalHeader", reason: `must be a string (a header name) — got ${describeValueShape(o.principalHeader)}` }
176
+ : sendHeaderOwnershipIssue({ headers: o.headers?.ok === true ? o.headers.record : undefined, principalHeader: o.principalHeader }, "a2a", A2A_SEND_HEADER_PATHS),
178
177
  allowSkills: (o) => o.allowSkills === undefined || isStringArray(o.allowSkills)
179
178
  ? undefined
180
179
  : { path: "allowSkills", reason: `must be an array of strings (omit the key to leave the peer's skills unrestricted) — got ${describeNotStringArray(o.allowSkills)}` },
@@ -87,8 +87,13 @@ export declare function parseAdditionalDirectories(raw: unknown): string[] | und
87
87
  * per-session choice always wins;
88
88
  * 2. the TRUSTED `ctx.parentCwd` (core threads the PARENT task's working root via `RunInternals` when this
89
89
  * task is a delegated child — never a TaskSpec field), UNLESS the child asked for `isolation:"worktree"`
90
- * (core contract: worktree wins — an isolated fan-out child must not share the parent's mutable dir);
90
+ * (an isolated fan-out child must not share the parent's mutable dir) — then `undefined`;
91
91
  * 3. undefined → the factory's ephemeral per-task dir (top-level task with no registered cwd).
92
+ * The `isolation:"worktree"` ⇒ `undefined` rule is this function's own and holds by itself (the child never inherits the
93
+ * parent's dir). It is NOT what answers a worktree ask on the host lane: `boot/execution-env.ts` never reaches here with one —
94
+ * the worktree wrapper (one layer out) mints the tree and does not call the base factory, and without the wrapper the base
95
+ * factory REFUSES (`isolation.unavailable`) instead of minting the empty ephemeral dir rule 3 would give. That refusal arm is
96
+ * revoked when engine 7.61.0 refuses by itself; this function's rule then goes on keeping the child off the parent's dir.
92
97
  * CC parity: a Task sub-agent inherits the main session's cwd. Trust: `parentCwd` comes from core's trusted
93
98
  * run-internals channel (same posture as `isolation`), so no tenancy gate is needed here — but the HOST lane
94
99
  * itself is single-user by deployment shape, and container lanes (k8s/e2b/local-docker) never consult this.
@@ -69,9 +69,9 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
69
69
  */
70
70
  export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "sessionMetaKey", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
71
71
  /** `transport.kind:"stdio"` 的键闭集 = stdio 臂处置表的键(`env` 是件,其余进声明)。 */
72
- export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ("kind" | "command" | "args" | "env")[];
72
+ export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ("env" | "kind" | "args" | "command")[];
73
73
  /** `transport.kind:"http"` 的键闭集 = http 臂处置表的键(`headers` 是件,`assertionHeader` 承载不发送,其余进声明)。 */
74
- export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ("kind" | "url" | "principalHeader" | "headers" | "assertionHeader")[];
74
+ export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ("headers" | "kind" | "principalHeader" | "url" | "assertionHeader")[];
75
75
  /**
76
76
  * Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
77
77
  * (deployment-shape row `multiTenant` does not hold) — the requester is the super-admin of their own worker, so their `.mcp.json` is
@@ -158,7 +158,7 @@ export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHe
158
158
  * `name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = 契约包读器 {@link readRequestMcpTransport} 的纯数据记录读法;`elicitation` = {@link isElicitationFlag};
159
159
  * `transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestMcpTransport}(读器投影 transport 的那一只函数,门读它报的 `lost`;读器在契约包,本仓只据 `lost[0]` 拒、据 `lost` 在重放腿逐处点名);
160
160
  * 读器交回的 http `url` 再问「带不带凭据」= 契约包根的 `findUrlCredential`([ref];拒体另带机读 `field` + `reason`,见 {@link transportUrlCredentialIssue});
161
- * http `principalHeader` 再问「这个名有没有别的属主」= 引擎包根的 `headerOwnershipCollision`(见 {@link principalHeaderOwnershipIssue})。
161
+ * http 这次发送的头集合(件头 + `principalHeader`)再问「有没有一个头名两个属主」= 引擎包根的 `headerOwnershipCollision`(见 {@link sendHeaderOwnershipIssue})。
162
162
  * 于是判官受理的条目,读器({@link normalizeServer})交回的就是申报本身(只差判官自己的合法归一:空容器 / `skillScopeEligible:false` 折成缺席、`toolAxes` 取引擎的拷贝)——
163
163
  * 探测口拨的那一份因此就是提交腿会挂的那一份,不另比一遍(另比就是第二只回答者)。唯一按门分的后果是「声明了 `assertionHeader`」这一个事实(本部署不发送断言,
164
164
  * 承载、不发送):新提交照挂 + 运维 info,探测口整条拒 —— 判据是同一只谓词 `specWithholdsMcpAssertionHeader`(开关 `MCP_ASSERTION_HEADER_SENT` 假时「声明了」即「被扣下」),不是第二份判法(见 `readMcpProbeBody`)。
@@ -185,14 +185,27 @@ export declare function requestMcpDeclarationRefusal(list: unknown): Declaration
185
185
  * 交出去的剔掉 `__proto__` 名成员),server 照挂,运维 warn `mcp_transport_members_dropped` 逐处点名 —— 续跑重放腿不静默丢声明。
186
186
  */
187
187
  export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
188
+ /** 一次发送从一份声明里带出的头:件头(调用方原样写的名 → 值,由对端座按原字节作答)与身份头名(引擎按主体写值)。 */
189
+ export interface DeclaredSendHeaders {
190
+ readonly headers?: Readonly<Record<string, string>> | undefined;
191
+ readonly principalHeader?: string | undefined;
192
+ }
188
193
  /**
189
- * `principalHeader` 的**名**有没有别的属主:判官是引擎包根的 `headerOwnershipCollision`(拨号时判同一条的那一只;这里只问身份头这一个名,件头名由引擎在发送时判)——
190
- * 名(任何大小写)是这一协议的传输自己写的头(MCP:`mcp-session-id` · `mcp-protocol-version` · `content-type` · `accept` · `last-event-id`;A2A 没有),或是引擎的断言头
191
- * (`connector-assertion`)⇒ 这一台拨号时必被拒,新提交在门上按名拒。原因句只点名**属主的拼写**(引擎 / 协议的词),不回显调用方写的串。
192
- * 新提交腿上按名 400(MCP 门表的 `transport`、A2A 判官表的 `principalHeader`)。续跑重放腿不 4xx:MCP 的读器不问它 —— 存量体里这一形照挂,由引擎在拨号时拒那一台、兄弟照挂;
193
- * A2A 的读器问同一张判官表 —— 这一条对端整条不进、`a2a_injection_dropped` 点名原因(引擎物化时同样拒那一台;结局同,原因说在本仓这一行)。
194
+ * 一次发送的**头属主** —— 一条规则:这次发送会带的头集合(件头 + 身份头名)只有一只属主判官,引擎包根的 `headerOwnershipCollision`(引擎每次发送时判同一组的那一只);
195
+ * 受理口用**全部**问它一次。MCP 与 A2A 两座都调这一只:MCP 门表的 `transport` 格,A2A 判官表的 `principalHeader` 格(两键里判序靠后的那一格,读两键的读数)。
196
+ * 判官答撞 ⇒ 这一台拨号即拒(`config.protocol_header_collision`)或此后每一次发送都被拒(`credential_unavailable`)⇒ 新提交在门上按格 400:
197
+ * · 身份头名本身是这一协议的传输自己写的头(MCP:`mcp-session-id` · `mcp-protocol-version` · `content-type` · `accept` · `last-event-id`,任何大小写;A2A 没有),或是引擎的断言头
198
+ * (`connector-assertion`)⇒ `paths.principalHeader`;
199
+ * · 件头名是传输自己写的头 ⇒ `paths.headers`;件里同一头名两个拼写、件头名撞身份头名 ⇒ `paths.headers`。
200
+ * 件里名叫 `connector-assertion` 的头不撞(断言头在件里是座的;引擎只在它作身份头名时保留这个名)。原因句只点名**规范化小写**的头名(属主拼写,或调用方所写名字的小写形、经回显清洗),
201
+ * 不回显调用方的大小写形,从不带值。
202
+ * 不在本规则里的件形 —— 件值字节(CR / LF / NUL)、头名不是 HTTP token、stdio 变量名 —— 只由引擎内部的答案读器判(包根不导出),受理口没有同一只判官可问,照旧运行期 `credential_unavailable`。
203
+ * 续跑重放腿不 4xx:MCP 的读器不问门表 —— 存量体里撞属主的那一台照挂,由引擎拨号 / 发送时拒、兄弟照挂;A2A 的读器问同一张判官表 —— 这一条对端整条不进、`a2a_injection_dropped` 点名原因。
194
204
  */
195
- export declare function principalHeaderOwnershipIssue(principalHeader: string | undefined, protocol: "mcp" | "a2a", path: string): DeclarationKeyIssue | undefined;
205
+ export declare function sendHeaderOwnershipIssue(send: DeclaredSendHeaders, protocol: "mcp" | "a2a", paths: {
206
+ readonly headers: string;
207
+ readonly principalHeader: string;
208
+ }): DeclarationKeyIssue | undefined;
196
209
  /**
197
210
  * The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
198
211
  * this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
package/dist/task-mcp.js CHANGED
@@ -171,17 +171,25 @@ function transportUrlCredentialIssue(url) {
171
171
  return undefined;
172
172
  return { path: "transport.url", reason: urlCredentialRefusalReason(found, urlCredentialHeaderExit("transport.headers")), credential: found };
173
173
  }
174
- export function principalHeaderOwnershipIssue(principalHeader, protocol, path) {
175
- if (principalHeader === undefined)
176
- return undefined;
177
- const collision = headerOwnershipCollision({ principalHeader }, protocol);
174
+ export function sendHeaderOwnershipIssue(send, protocol, paths) {
175
+ const collision = headerOwnershipCollision({ headers: send.headers === undefined ? undefined : { ...send.headers }, principalHeader: send.principalHeader }, protocol);
178
176
  if (collision === undefined)
179
177
  return undefined;
180
- return {
181
- path,
182
- reason: `names the header "${collision.first}" (header names are case-insensitive), which is not the principal header's to own — it is ${protocol === "mcp" && collision.first !== MCP_ASSERTION_HEADER ? "a header the MCP transport writes itself" : "the engine's connector assertion header"}; name the principal header something of its own`,
183
- };
178
+ if (collision.engineOwned) {
179
+ const owner = echoKeyName("", collision.first);
180
+ const ownedBy = collision.first === MCP_ASSERTION_HEADER ? "the engine's connector assertion header" : "a header the MCP transport writes itself";
181
+ if (collision.second === send.principalHeader) {
182
+ return { path: paths.principalHeader, reason: `names the header "${owner}" (header names are case-insensitive), which is not the principal header's to own — it is ${ownedBy}; name the principal header something of its own` };
183
+ }
184
+ return { path: paths.headers, reason: `carries the header "${owner}" (header names are case-insensitive), which is not this declaration's to send — it is ${ownedBy}; leave it out` };
185
+ }
186
+ const name = echoKeyName("", collision.first.toLowerCase());
187
+ if (collision.first === send.principalHeader || collision.second === send.principalHeader) {
188
+ return { path: paths.headers, reason: `carries a header named as the principal header "${name}" (header names are case-insensitive) — the engine writes the principal under that name; rename one of them` };
189
+ }
190
+ return { path: paths.headers, reason: `carries the header "${name}" under two spellings (header names are case-insensitive) — one send carries one value per header name; keep one` };
184
191
  }
192
+ const MCP_SEND_HEADER_PATHS = { headers: "transport.headers", principalHeader: "transport.principalHeader" };
185
193
  const ENTRY_KEY_JUDGES = {
186
194
  allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
187
195
  ? undefined
@@ -231,7 +239,7 @@ const ENTRY_KEY_JUDGES = {
231
239
  return read.lost[0];
232
240
  if (read.transport.kind !== "http")
233
241
  return undefined;
234
- return transportUrlCredentialIssue(read.transport.url) ?? principalHeaderOwnershipIssue(read.transport.principalHeader, "mcp", "transport.principalHeader");
242
+ return transportUrlCredentialIssue(read.transport.url) ?? sendHeaderOwnershipIssue({ headers: read.transport.headers, principalHeader: read.transport.principalHeader }, "mcp", MCP_SEND_HEADER_PATHS);
235
243
  },
236
244
  };
237
245
  function requestEntryRefusal(entry, at) {