@sema-agent/server 7.142.0 → 7.144.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 (301) hide show
  1. package/MIGRATION.md +608 -0
  2. package/USAGE.md +63 -36
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +155 -39
  9. package/dist/approval-card.js +111 -31
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/execution-env.js +1 -1
  17. package/dist/boot/governance-seams.d.ts +26 -41
  18. package/dist/boot/governance-seams.js +61 -28
  19. package/dist/boot/late-binds.d.ts +7 -4
  20. package/dist/boot/late-binds.js +3 -1
  21. package/dist/boot/leader.js +2 -4
  22. package/dist/boot/memory-consolidation.d.ts +0 -8
  23. package/dist/boot/memory-consolidation.js +3 -3
  24. package/dist/boot/reapers.js +5 -4
  25. package/dist/boot/resolve-spec.d.ts +1 -1
  26. package/dist/boot/resolve-spec.js +8 -8
  27. package/dist/boot/runner-deps.d.ts +14 -6
  28. package/dist/boot/runner-deps.js +6 -17
  29. package/dist/boot/shutdown.d.ts +9 -0
  30. package/dist/boot/shutdown.js +18 -2
  31. package/dist/boot/stage-01-config.js +2 -0
  32. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  33. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  34. package/dist/boot/stage-03-store-backend.js +1 -2
  35. package/dist/boot/stage-04-stores.d.ts +5 -2
  36. package/dist/boot/stage-04-stores.js +3 -5
  37. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  38. package/dist/boot/stage-06-runners.d.ts +5 -2
  39. package/dist/boot/stage-06-runners.js +1 -3
  40. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  41. package/dist/boot/stage-07-capability-layer.js +6 -2
  42. package/dist/boot/stage-08-reapers.d.ts +5 -2
  43. package/dist/boot/stage-09-leader.d.ts +5 -2
  44. package/dist/boot/stage-10-http-server.d.ts +5 -2
  45. package/dist/boot/stage-10-http-server.js +4 -1
  46. package/dist/boot/stores.d.ts +5 -2
  47. package/dist/boot/stores.js +7 -7
  48. package/dist/boot/web-admin.d.ts +5 -1
  49. package/dist/boot/web-admin.js +31 -19
  50. package/dist/capabilities/hands-lane.d.ts +9 -39
  51. package/dist/capabilities/hands-lane.js +8 -13
  52. package/dist/capabilities/mcp-probe.d.ts +3 -2
  53. package/dist/capabilities/scenarios.d.ts +23 -17
  54. package/dist/capabilities/scenarios.js +2 -2
  55. package/dist/capabilities/skills.d.ts +1 -17
  56. package/dist/capabilities/skills.js +0 -8
  57. package/dist/capability-facts.d.ts +8 -8
  58. package/dist/capability-facts.js +4 -4
  59. package/dist/card-void.d.ts +6 -9
  60. package/dist/config-catalog.js +1 -1
  61. package/dist/config-d-migration.d.ts +2 -0
  62. package/dist/config-dir-trust.js +58 -29
  63. package/dist/config-invariants.d.ts +7 -1
  64. package/dist/config-invariants.js +7 -0
  65. package/dist/config-provider.d.ts +10 -0
  66. package/dist/config-provider.js +15 -4
  67. package/dist/config-root-migration.d.ts +13 -0
  68. package/dist/config-root-migration.js +99 -0
  69. package/dist/config-types.d.ts +5 -5
  70. package/dist/config.d.ts +4 -3
  71. package/dist/config.js +1 -1
  72. package/dist/delegated-session-owner.d.ts +35 -151
  73. package/dist/delegated-session-owner.js +1 -83
  74. package/dist/device-ws-protocol.d.ts +1 -0
  75. package/dist/device-ws-protocol.js +2 -1
  76. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  77. package/dist/env-model-table-gate.d.ts +53 -0
  78. package/dist/env-model-table-gate.js +53 -0
  79. package/dist/execution-placement.d.ts +7 -4
  80. package/dist/fleet/fleet-bus.d.ts +28 -52
  81. package/dist/fleet/fleet-bus.js +35 -100
  82. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  83. package/dist/fleet/fleet-reconciler.js +1 -1
  84. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  85. package/dist/fleet/subagent-tail-bus.js +15 -28
  86. package/dist/hooks/classic-stations.d.ts +3 -1
  87. package/dist/hooks/classic-stations.js +2 -0
  88. package/dist/hooks/hook-runner.js +28 -7
  89. package/dist/host-decision.d.ts +54 -20
  90. package/dist/host-decision.js +42 -14
  91. package/dist/http/admission.d.ts +1 -1
  92. package/dist/http/admission.js +7 -3
  93. package/dist/http/approval-sweeps.js +11 -8
  94. package/dist/http/caller-trust.d.ts +1 -1
  95. package/dist/http/durable-run-leg.js +2 -2
  96. package/dist/http/engine-refusal.d.ts +16 -0
  97. package/dist/http/engine-refusal.js +17 -0
  98. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  99. package/dist/http/memory-compliance-failure-reply.js +4 -6
  100. package/dist/http/owner-access.d.ts +11 -0
  101. package/dist/http/owner-access.js +13 -0
  102. package/dist/http/principal-gate.d.ts +1 -1
  103. package/dist/http/refusal-body.d.ts +7 -3
  104. package/dist/http/refusal-body.js +1 -0
  105. package/dist/http/resume-legs.d.ts +21 -7
  106. package/dist/http/resume-legs.js +82 -53
  107. package/dist/http/resume-restatement.d.ts +39 -0
  108. package/dist/http/resume-restatement.js +25 -0
  109. package/dist/http/route-ctx.d.ts +35 -10
  110. package/dist/http/route-ctx.js +1 -1
  111. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  112. package/dist/http/routes/approvals-assistant.js +143 -31
  113. package/dist/http/routes/capabilities.js +11 -2
  114. package/dist/http/routes/memory-policy.d.ts +2 -2
  115. package/dist/http/routes/memory-policy.js +1 -1
  116. package/dist/http/routes/notify-wake.js +23 -25
  117. package/dist/http/routes/rules.js +48 -13
  118. package/dist/http/routes/runs.js +9 -8
  119. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  120. package/dist/http/routes/session-memory-sync.js +27 -10
  121. package/dist/http/routes/session-sync.js +13 -20
  122. package/dist/http/routes/sessions.d.ts +2 -2
  123. package/dist/http/routes/sessions.js +22 -23
  124. package/dist/http/routes/tasks.js +27 -49
  125. package/dist/http/server.d.ts +13 -7
  126. package/dist/http/server.js +2 -2
  127. package/dist/http/stored-body-replay.js +15 -4
  128. package/dist/http/wire-types.d.ts +17 -7
  129. package/dist/index.d.ts +1 -0
  130. package/dist/index.js +1 -0
  131. package/dist/lane-home-probe.d.ts +15 -0
  132. package/dist/lane-home-probe.js +4 -0
  133. package/dist/leader/leader.js +5 -2
  134. package/dist/leader/wire.d.ts +7 -1
  135. package/dist/leader/wire.js +6 -5
  136. package/dist/leg-facts.d.ts +37 -0
  137. package/dist/leg-facts.js +15 -0
  138. package/dist/memory-bundle-engine.d.ts +1 -4
  139. package/dist/memory-bundle-engine.js +2 -2
  140. package/dist/memory-operator-faces.d.ts +30 -66
  141. package/dist/memory-operator-faces.js +17 -88
  142. package/dist/memory-scope-stores.d.ts +38 -40
  143. package/dist/memory-scope-stores.js +51 -36
  144. package/dist/memory-scope.d.ts +0 -9
  145. package/dist/memory-scope.js +0 -9
  146. package/dist/memory-sync-client.js +14 -3
  147. package/dist/memory-sync.d.ts +20 -4
  148. package/dist/memory-sync.js +23 -4
  149. package/dist/model-plane-face.d.ts +13 -7
  150. package/dist/model-plane-face.js +2 -1
  151. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  152. package/dist/observability/corrupt-read-seat.js +1 -2
  153. package/dist/observability/fail-open.d.ts +30 -10
  154. package/dist/observability/fail-open.js +27 -10
  155. package/dist/observability/run-terminal.d.ts +1 -1
  156. package/dist/observability/run-terminal.js +6 -2
  157. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  158. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  159. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  160. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  161. package/dist/orchestration/workflow-park-decision.js +5 -3
  162. package/dist/org-delivery-gate.d.ts +11 -0
  163. package/dist/org-delivery-gate.js +19 -1
  164. package/dist/parked-decide.d.ts +13 -9
  165. package/dist/parked-decide.js +7 -7
  166. package/dist/parked-revive-surface.d.ts +6 -7
  167. package/dist/parked-revive-surface.js +1 -1
  168. package/dist/peer-inbound-face.d.ts +122 -0
  169. package/dist/peer-inbound-face.js +123 -0
  170. package/dist/permission-rule-list.d.ts +13 -9
  171. package/dist/permission-rule-list.js +7 -3
  172. package/dist/plugins/approval-ask-store-file.js +18 -6
  173. package/dist/plugins/approval-ask-store-memory.js +8 -6
  174. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  175. package/dist/plugins/approval-ask-store-sql.js +30 -11
  176. package/dist/plugins/caching-session-store.d.ts +0 -3
  177. package/dist/plugins/caching-session-store.js +0 -3
  178. package/dist/plugins/checkpoint-store-sql.d.ts +45 -38
  179. package/dist/plugins/checkpoint-store-sql.js +108 -45
  180. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  181. package/dist/plugins/file-history-store-sql.js +24 -19
  182. package/dist/plugins/file-run-store.d.ts +29 -0
  183. package/dist/plugins/file-run-store.js +97 -8
  184. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  185. package/dist/plugins/fork-routing-session-store.js +6 -0
  186. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  187. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  188. package/dist/plugins/local-checkpoint-store.d.ts +6 -8
  189. package/dist/plugins/local-checkpoint-store.js +5 -4
  190. package/dist/plugins/local-session-store.d.ts +69 -30
  191. package/dist/plugins/local-session-store.js +133 -57
  192. package/dist/plugins/local-session-window.d.ts +4 -2
  193. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  194. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  195. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  196. package/dist/plugins/mailbox-store-sql.js +76 -7
  197. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  198. package/dist/plugins/memory-sync-store-pg.js +21 -0
  199. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  200. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  201. package/dist/plugins/one-time-migrations.d.ts +44 -14
  202. package/dist/plugins/one-time-migrations.js +118 -15
  203. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  204. package/dist/plugins/org-delivery-quota-store.js +2 -12
  205. package/dist/plugins/park-commit-watch.d.ts +1 -1
  206. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  207. package/dist/plugins/permission-rule-store-file.js +15 -2
  208. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  209. package/dist/plugins/permission-rule-store-sql.js +35 -2
  210. package/dist/plugins/pg-session-storage.d.ts +1 -9
  211. package/dist/plugins/pg-session-storage.js +2 -19
  212. package/dist/plugins/remote-env-device.d.ts +8 -11
  213. package/dist/plugins/remote-env-device.js +6 -3
  214. package/dist/plugins/remote-env-host.d.ts +8 -4
  215. package/dist/plugins/remote-env-host.js +7 -9
  216. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  217. package/dist/plugins/remote-env-k8s.js +50 -17
  218. package/dist/plugins/remote-shell.js +12 -2
  219. package/dist/plugins/retention-store-sql.d.ts +2 -2
  220. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  221. package/dist/plugins/sandbox-read-floor.js +2 -7
  222. package/dist/plugins/schema-consistency.d.ts +5 -1
  223. package/dist/plugins/session-fork.d.ts +1 -1
  224. package/dist/plugins/session-ownership.d.ts +9 -0
  225. package/dist/plugins/session-ownership.js +6 -0
  226. package/dist/plugins/session-store.d.ts +13 -16
  227. package/dist/plugins/session-store.js +9 -10
  228. package/dist/plugins/sql-errors.d.ts +10 -1
  229. package/dist/plugins/sql-errors.js +13 -0
  230. package/dist/plugins/sql-json-column.d.ts +24 -2
  231. package/dist/plugins/sql-json-column.js +38 -3
  232. package/dist/plugins/store-backend.d.ts +23 -28
  233. package/dist/plugins/store-backend.js +12 -3
  234. package/dist/plugins/store-contracts.d.ts +29 -5
  235. package/dist/plugins/store-contracts.js +4 -1
  236. package/dist/plugins/task-attachment-store.d.ts +2 -2
  237. package/dist/plugins/task-attachment-store.js +37 -52
  238. package/dist/plugins/tidb-session-store.d.ts +2 -14
  239. package/dist/plugins/tidb-session-store.js +2 -20
  240. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  241. package/dist/plugins/tool-result-store-sql.js +23 -2
  242. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  243. package/dist/plugins/wiring-section-codec.js +55 -0
  244. package/dist/plugins/workspace-fill.d.ts +1 -1
  245. package/dist/rule-entry-list.d.ts +3 -2
  246. package/dist/rule-entry-list.js +5 -1
  247. package/dist/rules-consent.d.ts +28 -5
  248. package/dist/rules-consent.js +40 -28
  249. package/dist/run-local.js +11 -6
  250. package/dist/runs.d.ts +12 -8
  251. package/dist/runs.js +10 -7
  252. package/dist/runtime-governance.d.ts +1 -1
  253. package/dist/security.d.ts +0 -4
  254. package/dist/server-secret-env.d.ts +4 -0
  255. package/dist/session-error-wire.d.ts +34 -23
  256. package/dist/session-error-wire.js +8 -3
  257. package/dist/session-incarnation.d.ts +3 -3
  258. package/dist/session-notify-door.d.ts +106 -0
  259. package/dist/session-notify-door.js +97 -0
  260. package/dist/session-sync.d.ts +9 -14
  261. package/dist/session-sync.js +5 -5
  262. package/dist/session-workspace.d.ts +3 -13
  263. package/dist/session-workspace.js +0 -9
  264. package/dist/standing-answer-grant.d.ts +5 -0
  265. package/dist/standing-answer-grant.js +10 -0
  266. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  267. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  268. package/dist/task-mcp.d.ts +1 -1
  269. package/dist/task-settings.d.ts +11 -8
  270. package/dist/task-settings.js +31 -11
  271. package/dist/tool-approval.d.ts +55 -35
  272. package/dist/tool-approval.js +112 -74
  273. package/dist/trace/core-keyset-guard.d.ts +12 -9
  274. package/dist/trace/engine-notice-wire.d.ts +1 -1
  275. package/dist/trace/engine-notice-wire.js +4 -2
  276. package/dist/trace/exit-seat.d.ts +7 -1
  277. package/dist/trace/exit-seat.js +1 -0
  278. package/dist/trace/ledger-sink.d.ts +2 -2
  279. package/dist/trace/ledger-sink.js +12 -4
  280. package/dist/trace/model-segment.d.ts +74 -25
  281. package/dist/trace/model-segment.js +141 -32
  282. package/dist/trace/project.d.ts +15 -6
  283. package/dist/trace/project.js +12 -4
  284. package/dist/trace/redact.d.ts +72 -9
  285. package/dist/trace/redact.js +1509 -471
  286. package/dist/trace/self-minted-wire.d.ts +598 -47
  287. package/dist/trace/self-minted-wire.js +190 -20
  288. package/dist/trace/wire-projection-faces.d.ts +1 -1
  289. package/dist/trace/wire-projection-faces.js +1 -1
  290. package/dist/verifier-pause-card.d.ts +33 -5
  291. package/dist/verifier-pause-card.js +15 -10
  292. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  293. package/dist/web-admin-client/apply-effective.js +90 -10
  294. package/dist/workspace-space.d.ts +19 -3
  295. package/dist/workspace-space.js +8 -4
  296. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  297. package/package.json +3 -2
  298. package/wire-changes/server@7.143.0.json +1167 -0
  299. package/wire-changes/server@7.144.0.json +291 -0
  300. package/dist/session-records-skipped-notice.d.ts +0 -21
  301. package/dist/session-records-skipped-notice.js +0 -27
@@ -3,7 +3,7 @@ import { ensureIndexes } from "./ensure-index.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { attachmentKindOf, PROJECT_BUNDLE_MIME } from "../project-bundle.js";
5
5
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
- import { isDeadlockError } from "./sql-errors.js";
6
+ import { isDupKeyError, retryOnDeadlock } from "./sql-errors.js";
7
7
  export const TaskAttachmentRecordSchema = z.object({
8
8
  id: z.string(),
9
9
  owner: z.string(),
@@ -183,6 +183,8 @@ export async function materializeAttachmentsInto(env, store, sessionId) {
183
183
  }
184
184
  return placements.length;
185
185
  }
186
+ const SEED_KEY = "uq_att_seed";
187
+ const SEED_KEY_EXPR = `CASE WHEN mime = '${PROJECT_BUNDLE_MIME}' THEN session_id END`;
186
188
  export async function ensureTiDBTaskAttachmentSchema(pool) {
187
189
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
188
190
  id VARCHAR(64) NOT NULL,
@@ -196,15 +198,19 @@ export async function ensureTiDBTaskAttachmentSchema(pool) {
196
198
  session_id VARCHAR(190) NULL,
197
199
  created_at_ms BIGINT NOT NULL,
198
200
  bound_at_ms BIGINT NULL,
201
+ -- 种子键:项目包行的会话,其余行 NULL(引擎推导,没有写者);唯一 ⇒ 一只会话一只种子
202
+ seed_session_id VARCHAR(190) GENERATED ALWAYS AS (${SEED_KEY_EXPR}) VIRTUAL,
199
203
  PRIMARY KEY (id),
200
204
  KEY idx_att_session (session_id),
201
- KEY idx_att_unbound (created_at_ms)
205
+ KEY idx_att_unbound (created_at_ms),
206
+ UNIQUE KEY ${SEED_KEY} (seed_session_id)
202
207
  ) COLLATE utf8mb4_bin`);
203
208
  await ensureIndexes((text, params) => pool.query(text, params), "mysql", TASK_ATTACHMENT_INDEXES);
204
209
  }
205
210
  export const TASK_ATTACHMENT_INDEXES = [
206
211
  { table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_session", columns: ["session_id"] },
207
212
  { table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_unbound", columns: ["created_at_ms"] },
213
+ { table: `${TASK_ATTACHMENT_TABLE}`, name: SEED_KEY, columns: ["seed_session_id"], unique: true },
208
214
  ];
209
215
  export async function ensurePgTaskAttachmentSchema(q) {
210
216
  await q(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
@@ -218,66 +224,50 @@ export async function ensurePgTaskAttachmentSchema(q) {
218
224
  session_id VARCHAR(190) COLLATE "C",
219
225
  created_at_ms BIGINT NOT NULL,
220
226
  bound_at_ms BIGINT,
227
+ -- 种子键(同 MySQL 侧注)
228
+ seed_session_id VARCHAR(190) COLLATE "C" GENERATED ALWAYS AS (${SEED_KEY_EXPR}) STORED,
221
229
  PRIMARY KEY (id)
222
230
  )`);
223
231
  await ensureIndexes(q, "pg", TASK_ATTACHMENT_INDEXES);
224
232
  }
225
- const SEED_BIND_DEADLOCK_RETRIES = 4;
226
233
  async function bindOnSql(db, owner, ids, sessionId, boundAtMs) {
227
234
  const list = [...new Set(ids)];
228
235
  if (list.length === 0)
229
236
  return { bound: [], refused: [] };
230
237
  const mysql = db.dialect === "mysql";
231
238
  const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
232
- const kinds = await db.query(q(`SELECT id, mime FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${list.map(() => "?").join(",")})`, `SELECT id, mime FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...list] : [owner, list]);
233
- const kindOf = new Map(kinds.rows.map((r) => [String(r.id), attachmentKindOf(String(r.mime))]));
234
- const verdicts = new Map();
235
- const plain = list.filter((id) => kindOf.get(id) === "attachment");
236
- if (plain.length > 0) {
237
- const inList = plain.map(() => "?").join(",");
238
- const held = await retryingDeadlock(db.dialect, async () => {
239
- await db.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN ? ELSE bound_at_ms END, session_id = ? WHERE owner = ? AND id IN (${inList}) AND (session_id IS NULL OR session_id = ?)`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN $2::bigint ELSE bound_at_ms END, session_id = $1::varchar WHERE owner = $3 AND id = ANY($4::varchar[]) AND (session_id IS NULL OR session_id = $1::varchar)`), mysql ? [boundAtMs, sessionId, owner, ...plain, sessionId] : [sessionId, boundAtMs, owner, plain]);
240
- return db.query(q(`SELECT id, session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${inList})`, `SELECT id, session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...plain] : [owner, plain]);
241
- });
242
- for (const r of held.rows) {
243
- if (r.session_id === null || r.session_id === undefined)
244
- continue;
245
- verdicts.set(String(r.id), String(r.session_id) === sessionId ? "bound" : "bound_elsewhere");
246
- }
247
- }
239
+ const ownRows = (columns, of) => db.query(q(`SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${of.map(() => "?").join(",")})`, `SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...of] : [owner, of]);
240
+ const kindOf = new Map((await ownRows("id, mime", list)).rows.map((r) => [String(r.id), attachmentKindOf(String(r.mime))]));
241
+ const claim = (of) => retryOnDeadlock(db.dialect, () => inTransaction(db, (conn) => conn.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN ? ELSE bound_at_ms END, session_id = ? WHERE owner = ? AND id IN (${of.map(() => "?").join(",")}) AND (session_id IS NULL OR session_id = ?)`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN $2::bigint ELSE bound_at_ms END, session_id = $1::varchar WHERE owner = $3 AND id = ANY($4::varchar[]) AND (session_id IS NULL OR session_id = $1::varchar)`), mysql ? [boundAtMs, sessionId, owner, ...of, sessionId] : [sessionId, boundAtMs, owner, of])));
242
+ const attachments = list.filter((id) => kindOf.get(id) === "attachment");
243
+ if (attachments.length > 0)
244
+ await claim(attachments);
245
+ const seedFixed = new Set();
248
246
  for (const id of list) {
249
247
  if (kindOf.get(id) !== "project_bundle")
250
248
  continue;
251
- const verdict = await retryingDeadlock(db.dialect, () => inTransaction(db, (conn) => bindProjectBundleOnSql(conn, mysql, owner, id, sessionId, boundAtMs)));
252
- if (verdict !== undefined)
253
- verdicts.set(id, verdict);
249
+ try {
250
+ await claim([id]);
251
+ }
252
+ catch (err) {
253
+ if (!isSeedKeyConflict(db.dialect, err))
254
+ throw err;
255
+ seedFixed.add(id);
256
+ }
254
257
  }
258
+ const holderOf = new Map((await ownRows("id, session_id", list)).rows.map((r) => [String(r.id), r.session_id === null || r.session_id === undefined ? undefined : String(r.session_id)]));
255
259
  const bound = [];
256
260
  const refused = [];
257
261
  for (const id of list) {
258
- const v = verdicts.get(id);
259
- if (v === "bound")
262
+ if (seedFixed.has(id))
263
+ refused.push({ kind: "bundle_seed_fixed", id });
264
+ else if (holderOf.get(id) === sessionId)
260
265
  bound.push(id);
261
- else if (v !== undefined)
262
- refused.push({ kind: v, id });
266
+ else if (holderOf.get(id) !== undefined)
267
+ refused.push({ kind: "bound_elsewhere", id });
263
268
  }
264
269
  return { bound, refused };
265
270
  }
266
- async function bindProjectBundleOnSql(conn, mysql, owner, id, sessionId, boundAtMs) {
267
- const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
268
- const anchor = await conn.query(q(`SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE mime = ? AND (session_id IS NULL OR session_id = ?) ORDER BY id FOR UPDATE`, `SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE mime = $1 AND (session_id IS NULL OR session_id = $2::varchar) ORDER BY id FOR UPDATE`), [PROJECT_BUNDLE_MIME, sessionId]);
269
- const own = await conn.query(q(`SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE id = ? AND owner = ? FOR UPDATE`, `SELECT session_id FROM ${TASK_ATTACHMENT_TABLE} WHERE id = $1 AND owner = $2 FOR UPDATE`), [id, owner]);
270
- const [row] = own.rows;
271
- if (row === undefined)
272
- return undefined;
273
- const holder = row.session_id;
274
- if (holder !== null && holder !== undefined)
275
- return String(holder) === sessionId ? "bound" : "bound_elsewhere";
276
- if (anchor.rows.some((r) => r.session_id !== null && r.session_id !== undefined))
277
- return "bundle_seed_fixed";
278
- await conn.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = ?, session_id = ? WHERE id = ? AND owner = ? AND session_id IS NULL`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = $1::bigint, session_id = $2::varchar WHERE id = $3 AND owner = $4 AND session_id IS NULL`), [boundAtMs, sessionId, id, owner]);
279
- return "bound";
280
- }
281
271
  async function inTransaction(db, work) {
282
272
  const conn = await db.connect();
283
273
  try {
@@ -294,17 +284,12 @@ async function inTransaction(db, work) {
294
284
  conn.release();
295
285
  }
296
286
  }
297
- async function retryingDeadlock(dialect, attempt) {
298
- for (let tries = 0;; tries++) {
299
- try {
300
- return await attempt();
301
- }
302
- catch (err) {
303
- if (tries < SEED_BIND_DEADLOCK_RETRIES && isDeadlockError(dialect, err))
304
- continue;
305
- throw err;
306
- }
307
- }
287
+ function isSeedKeyConflict(dialect, err) {
288
+ if (!isDupKeyError(dialect, err) || err === null || typeof err !== "object")
289
+ return false;
290
+ if (dialect === "pg")
291
+ return Reflect.get(err, "constraint") === SEED_KEY;
292
+ return new RegExp(`\\b${SEED_KEY}\\b`).test(String(Reflect.get(err, "sqlMessage") ?? Reflect.get(err, "message") ?? ""));
308
293
  }
309
294
  export class TiDBTaskAttachmentStore {
310
295
  pool;
@@ -17,7 +17,6 @@ import type { SessionWorkspace } from "../session-workspace.js";
17
17
  import type { RetentionDeclaration } from "@sema-agent/core";
18
18
  import type { SessionSummary } from "./store-contracts.js";
19
19
  import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
20
- import { type OrphanOwnerLog } from "../delegated-session-owner.js";
21
20
  import { type PlacementAcquireOpts } from "./session-placement.js";
22
21
  import { type StagingHandle } from "../session-sync-kernel.js";
23
22
  import { type ForkTarget, type ForkToOutcome } from "./session-fork.js";
@@ -40,8 +39,6 @@ export declare class TiDBSessionStore implements SessionStore {
40
39
  readonly durability: "durable";
41
40
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
42
41
  private readonly pending;
43
- /** 委派子代孤行那一行 warn 的去处(`delegated-session-owner.ts`;装配给部署的 logger,缺席 ⇒ 缺省 logger,不静默)。 */
44
- private readonly log;
45
42
  /**
46
43
  * 执行席 —— 根会话行上这一位住客的那一格的唯一读口与 place / markMirror 两只写口(规则在 `src/execution-placement.ts`;
47
44
  * 实现与语句的唯一属主 = `execution-placement-sql.ts`,两孪生同一只实现,本店只交自己的池)。
@@ -50,7 +47,6 @@ export declare class TiDBSessionStore implements SessionStore {
50
47
  /** 本店的池包成的 SqlDriver:执行席与会话行格写原语的事务都经它借连接(连接初始化钉住事务读语义,`sql-driver.ts`)。 */
51
48
  private readonly db;
52
49
  constructor(pool: Pool, opts?: {
53
- readonly log?: OrphanOwnerLog;
54
50
  readonly retention?: RetentionDeclaration;
55
51
  });
56
52
  acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
@@ -58,18 +54,10 @@ export declare class TiDBSessionStore implements SessionStore {
58
54
  /** Idempotently create a session_meta row with an owner (core `SessionStore.register`). Existing owner is never overwritten. */
59
55
  register(sessionId: string, owner: string | null): Promise<void>;
60
56
  /**
61
- * acquire 的建行(唯一一处):普通会话 ⇒ 无主(按 id 首建的既有读法);placed 会话 ⇒ 属主按委派继承律在**这一笔**里写入
62
- * ([ref]:根会话的属主,根行不在取父,都不在 ⇒ 孤行哨兵 + warn;属主 = 判断时见证到的,读来源 → 判 → INSERT —— 读法与顺序只住在
63
- * `delegated-session-owner.ts` 的 `createPlacedSessionRow`,本店只交既有读写口,PG 孪生同调)。
57
+ * acquire 的建行(唯一一处):普通会话 ⇒ 无主(按 id 首建的既有读法);placed 会话 ⇒ 属主 = `placement.owner` 原样,与元组同一条 INSERT 落库
58
+ * (core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不判、不推断;唯一读法 `placedSessionOwnerOf`,值不是主体名也不是 null ⇒ 响亮拒、不留行。PG 孪生同调)。
64
59
  */
65
60
  private createOnAcquire;
66
- /**
67
- * [ref] 委派继承律的**认领口**:这只会话的那一位住客(判断时读到的化身)仍是无主的 placed 行 ⇒ 写 `owner`,返回是否写成
68
- * (句 = `CLAIM_OWNERLESS_PLACED_SQL`,只 CAS 子行自己,与停驻挂点 / M23 同一只;规则在 `delegated-session-owner.ts`)。普通会话行不在射程(placed 判别位)。
69
- */
70
- claimOwnerless(sessionId: string, incarnation: string, owner: string): Promise<boolean>;
71
- /** 若干来源行的会话面(属主 + 化身)一次读回(委派继承律的来源读;本店没有的 id 不在表里;活住客的投影 `sessionFaceFromRow`,空化身 ⇒ 响亮拒)。 */
72
- private sourceFaces;
73
61
  /** The one session_meta INSERT. Existing owner is never overwritten.
74
62
  * [ref]:`placement` 在场时与行同一条 INSERT 落六列;ON DUPLICATE 臂是 no-op ⇒ 已存在的行的元组
75
63
  * **永不被重写**(first-write 不可变,义务 5 的写侧半场)。
@@ -10,11 +10,9 @@ import { carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnati
10
10
  import { sessionRowTagKey } from "../session-row.js";
11
11
  import { writeSessionRowCellsSql } from "./session-row-sql.js";
12
12
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
13
- import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
14
- import { createLogger } from "../observability/logger.js";
15
13
  import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SESSION_DURABILITY, SESSION_PLACEMENTS, } from "./session-placement.js";
16
14
  import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
17
- import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue } from "./session-ownership.js";
15
+ import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue, placedSessionOwnerOf } from "./session-ownership.js";
18
16
  import { selectForkRows } from "@sema-agent/core";
19
17
  import { FORK_LOG_ROW_SHAPE, ForkTargetUnsettledError, classifyForkTarget, forkByFreshId, forkPointOf } from "./session-fork.js";
20
18
  import { isMysqlDupKeyError } from "./sql-errors.js";
@@ -28,12 +26,10 @@ export class TiDBSessionStore {
28
26
  placements = SESSION_PLACEMENTS;
29
27
  durability = SESSION_DURABILITY;
30
28
  pending = new Map();
31
- log;
32
29
  executionPlacement;
33
30
  db;
34
31
  constructor(pool, opts = {}) {
35
32
  this.pool = pool;
36
- this.log = opts.log ?? createLogger();
37
33
  this.retention = opts.retention ?? UNMANAGED_RETENTION;
38
34
  this.db = mysqlDriver(pool);
39
35
  this.executionPlacement = createSqlExecutionPlacementSeat(this.db);
@@ -81,21 +77,7 @@ export class TiDBSessionStore {
81
77
  await this.insertMeta(sessionId, owner, undefined);
82
78
  }
83
79
  async createOnAcquire(sessionId, placement) {
84
- if (placement === undefined)
85
- return this.insertMeta(sessionId, null, undefined);
86
- await createPlacedSessionRow(sessionId, placement, {
87
- readFaces: (ids) => this.sourceFaces(ids),
88
- insert: (owner) => this.insertMeta(sessionId, owner, placement),
89
- readOwner: async () => (await this.headOf(sessionId))?.owner,
90
- }, this.log);
91
- }
92
- async claimOwnerless(sessionId, incarnation, owner) {
93
- const [res] = await this.pool.query(CLAIM_OWNERLESS_PLACED_SQL.mysql, [...claimOwnerlessParams(sessionId, incarnation, owner)]);
94
- return res.affectedRows > 0;
95
- }
96
- async sourceFaces(ids) {
97
- const [rows] = await this.pool.query(`SELECT session_id, owner, incarnation FROM session_meta WHERE session_id IN (${ids.map(() => "?").join(", ")})`, [...ids]);
98
- return new Map(rows.map((r) => [String(r.session_id), sessionFaceFromRow(String(r.session_id), r.owner ?? null, r.incarnation)]));
80
+ await this.insertMeta(sessionId, placement === undefined ? null : placedSessionOwnerOf(sessionId, placement), placement);
99
81
  }
100
82
  async insertMeta(sessionId, owner, placement) {
101
83
  const now = new Date();
@@ -28,7 +28,6 @@ export declare function ensureSchema(pool: PgPool | PgPoolClient): Promise<void>
28
28
  export declare function assertToolResultProvenanceSchema(query: (sql: string) => Promise<{
29
29
  rows: Record<string, unknown>[];
30
30
  }>, dialect: SqlDialect): Promise<void>;
31
- /** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
32
31
  export declare class SqlToolResultStore implements ToolResultStore {
33
32
  protected readonly db: SqlDriver;
34
33
  /** 托管留存声明 = 装配交给它的词(后端按扫车道开不开给;不给 ⇒ `"none"`)。读法与「为什么 SQL 店答 managed 而三方法的实现体在
@@ -73,14 +72,16 @@ export declare class SqlToolResultStore implements ToolResultStore {
73
72
  offset?: number;
74
73
  limit?: number;
75
74
  }): Promise<ToolResultSlice | undefined>;
76
- /** TTL reap: delete results older than `cutoffMs`. Returns rows removed.
75
+ /** core `ToolResultStore.reap` (core 7.61.0 — the one age bound every backend reads): remove the results stored more than
76
+ * `opts.maxAgeMs` before `now`. No bound ⇒ nothing removed; a bound that cannot be evaluated (`maxAgeMs` not a finite
77
+ * number ≥ 0, `now` not a finite number) ⇒ refused, coded `config.retention_policy_invalid`, before any row is touched;
78
+ * age alone decides (ownership is not consulted — the row and its owner columns go together). Returns rows removed.
77
79
  * C6: a real DB error must NOT collapse into the same `0` a legitimate "nothing was old enough" returns —
78
- * those are different facts (query failed vs. query succeeded on an empty set) and folding them together
79
- * makes a stuck/broken reaper indistinguishable from a healthy quiet one. Matches every sibling reaper's
80
- * form (roster-store-sql.ts `reapOlderThan`, checkpoint-store-sql.ts `reapExpired`,
81
- * workflow-journal-store-sql.ts `reapExpired`): let the error propagate — the periodic-sweep call site
82
- * (boot/reapers.ts) already wraps this call in `.catch(() => undefined)` so the reap loop itself never dies. */
83
- reapOlderThan(cutoffMs: number): Promise<number>;
80
+ * those are different facts (query failed vs. query succeeded on an empty set). Let the error propagate — the
81
+ * periodic-sweep call site (boot/reapers.ts) wraps this call in its throttled catch so the reap loop never dies. */
82
+ reap(now: number, opts?: {
83
+ maxAgeMs?: number;
84
+ }): Promise<number>;
84
85
  /**
85
86
  * E21 (§0.5 session delete) — purge offloaded tool results for one session. Selection is by the row's
86
87
  * **recorded provenance** (`owner_session_id`, the [ref] column `put` writes in the same statement as the
@@ -55,6 +55,24 @@ export async function assertToolResultProvenanceSchema(query, dialect) {
55
55
  (await describeProbedTableDrift(query, dialect, ["tool_result"])));
56
56
  }
57
57
  }
58
+ function toolResultAgeCutoff(now, opts) {
59
+ const refuse = (what, v) => {
60
+ throw Object.assign(new Error(`ToolResultStore.reap: ${what} must be a finite number${what === "maxAgeMs" ? " of milliseconds of 0 or more" : ""} (got ${typeof v === "string" ? JSON.stringify(v) : String(v)}) — a retention bound that cannot be evaluated is refused, never read as "no bound"`), {
61
+ code: "config.retention_policy_invalid",
62
+ });
63
+ };
64
+ if (opts !== undefined) {
65
+ for (const key of Object.keys(opts))
66
+ if (key !== "maxAgeMs")
67
+ refuse(`the bound "${key}" (only maxAgeMs is taken)`, opts[key]);
68
+ }
69
+ const maxAgeMs = opts?.maxAgeMs;
70
+ if (maxAgeMs !== undefined && (typeof maxAgeMs !== "number" || !Number.isFinite(maxAgeMs) || maxAgeMs < 0))
71
+ refuse("maxAgeMs", maxAgeMs);
72
+ if (typeof now !== "number" || !Number.isFinite(now))
73
+ refuse("now", now);
74
+ return maxAgeMs === undefined ? undefined : now - maxAgeMs;
75
+ }
58
76
  export class SqlToolResultStore {
59
77
  db;
60
78
  retention;
@@ -126,8 +144,11 @@ export class SqlToolResultStore {
126
144
  return undefined;
127
145
  return { content: String(rows[0].slice ?? ""), offset, totalChars: Number(rows[0].total) };
128
146
  }
129
- async reapOlderThan(cutoffMs) {
130
- const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoffMs)]);
147
+ async reap(now, opts) {
148
+ const cutoff = toolResultAgeCutoff(now, opts);
149
+ if (cutoff === undefined)
150
+ return 0;
151
+ const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoff)]);
131
152
  return affected;
132
153
  }
133
154
  async deleteBySession(sessionId) {
@@ -0,0 +1,32 @@
1
+ /** 块引用的唯一键。 */
2
+ export declare const WIRING_SECTION_REF_KEY = "$blob";
3
+ /** 一只段体:摘要(`sha256:<64 hex>`)+ 段值本身(JSON 容器)。 */
4
+ export interface WiringSectionBlob {
5
+ readonly digest: string;
6
+ readonly body: object;
7
+ }
8
+ /** {@link splitForStore} 的产物:要落账本的那一形 + 本帧引用到的段体(按摘要去重,首见为准)。 */
9
+ export interface WiringSectionSplit {
10
+ readonly stored: unknown;
11
+ readonly blobs: readonly WiringSectionBlob[];
12
+ }
13
+ /** 装回失败的一段:段名 + 它的引用(引用值不是串时记 `<not-a-digest>`)。 */
14
+ export interface UnresolvedWiringSection {
15
+ readonly section: string;
16
+ readonly digest: string;
17
+ }
18
+ /** {@link joinFromStore} 的结局:整帧,或至少一段装不回(永不交出缺段的帧)。 */
19
+ export type WiringSectionJoin = {
20
+ readonly kind: "joined";
21
+ readonly data: unknown;
22
+ } | {
23
+ readonly kind: "unresolved";
24
+ readonly missing: readonly UnresolvedWiringSection[];
25
+ };
26
+ /** 段体的店键:段体(一只 JSON 值)规范 JSON 的 sha256 全长。 */
27
+ export declare function wiringSectionDigest(body: object): string;
28
+ /** 写口:清单帧里 JSON 形是容器的段换成块引用(段体是那一段的 JSON 形,与原值不共享);别的型、或 `data` 不是对象 ⇒ 原样、零块。 */
29
+ export declare function splitForStore(type: string, data: unknown): WiringSectionSplit;
30
+ /** 读口:清单行里恰为单键 `$blob` 对象的顶层段换回段体;任一段在 `lookup` 里找不到 ⇒ `unresolved`(点名每一段)。 */
31
+ export declare function joinFromStore(type: string, data: unknown, lookup: (digest: string) => object | undefined): WiringSectionJoin;
32
+ //# sourceMappingURL=wiring-section-codec.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJsonStringify } from "@sema-agent/core";
3
+ export const WIRING_SECTION_REF_KEY = "$blob";
4
+ const WIRING_MANIFEST = "wiring_manifest";
5
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
+ export function wiringSectionDigest(body) {
7
+ return `sha256:${createHash("sha256").update(canonicalJsonStringify(body), "utf8").digest("hex")}`;
8
+ }
9
+ function jsonContainerOf(value) {
10
+ if (typeof value !== "object" || value === null)
11
+ return undefined;
12
+ const text = JSON.stringify(value);
13
+ if (text === undefined)
14
+ return undefined;
15
+ const body = JSON.parse(text);
16
+ return typeof body === "object" && body !== null ? body : undefined;
17
+ }
18
+ export function splitForStore(type, data) {
19
+ if (type !== WIRING_MANIFEST || !isRecord(data))
20
+ return { stored: data, blobs: [] };
21
+ const blobs = new Map();
22
+ const stored = Object.fromEntries(Object.entries(data).map(([section, value]) => {
23
+ const body = jsonContainerOf(value);
24
+ if (body === undefined)
25
+ return [section, value];
26
+ const digest = wiringSectionDigest(body);
27
+ if (!blobs.has(digest))
28
+ blobs.set(digest, body);
29
+ return [section, { [WIRING_SECTION_REF_KEY]: digest }];
30
+ }));
31
+ return { stored, blobs: [...blobs].map(([digest, body]) => ({ digest, body })) };
32
+ }
33
+ export function joinFromStore(type, data, lookup) {
34
+ if (type !== WIRING_MANIFEST || !isRecord(data))
35
+ return { kind: "joined", data };
36
+ let refs = 0;
37
+ const missing = [];
38
+ const entries = Object.entries(data).map(([section, value]) => {
39
+ if (!isRecord(value))
40
+ return [section, value];
41
+ const keys = Object.keys(value);
42
+ if (keys.length !== 1 || keys[0] !== WIRING_SECTION_REF_KEY)
43
+ return [section, value];
44
+ refs += 1;
45
+ const ref = value[WIRING_SECTION_REF_KEY];
46
+ const body = typeof ref === "string" ? lookup(ref) : undefined;
47
+ if (body === undefined)
48
+ missing.push({ section, digest: typeof ref === "string" ? ref : "<not-a-digest>" });
49
+ return [section, body ?? value];
50
+ });
51
+ if (missing.length > 0)
52
+ return { kind: "unresolved", missing };
53
+ return { kind: "joined", data: refs === 0 ? data : Object.fromEntries(entries) };
54
+ }
55
+ //# sourceMappingURL=wiring-section-codec.js.map
@@ -27,7 +27,7 @@ export interface WorkspaceChannel {
27
27
  run(command: string): Promise<ChannelRun>;
28
28
  write(absPath: string, bytes: Uint8Array): Promise<ChannelWrite>;
29
29
  }
30
- /** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复的归档不在:`latest` 悬空、停驻快照从未物化);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
30
+ /** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复点指的归档不在:`latest` 悬空、停驻快照的归档不在);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
31
31
  export type WorkspaceOrigin = "minted" | "restored";
32
32
  /** 一次填充的结局。`standing` = 这只执行环境此后每一次恢复点都会得到同一个答(适配器把它记成这只环境的拒,不再建机器)。 */
33
33
  export type FillOutcome = {
@@ -2,11 +2,12 @@
2
2
  * **规则条目表在本仓受理口的判** —— 一份,四张表同调(core 7.42.0 起「这个调用不用问」只有一个去处:引擎的 allow 层;条目的文法、匹配、
3
3
  * 名字、读成空的通告都归引擎,本仓不写第二套):
4
4
  * · 请求自带技能的 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代 `agents[].skills[]` 两处受理口,`spec-fields.ts` / `http/admission.ts`);
5
- * · 请求的 `settings.permissions.allow`(整份原样投 `TaskSpec.allowedTools`,`task-settings.ts`);
5
+ * · 请求的 `settings.permissions.allow`(整份按位置投 `TaskSpec.allowedTools`,`task-settings.ts`;条目也可以是设置层的 `{ rule, layer }`,本文件按它的规则原文读,层只选锚);
6
6
  * · 运维的 `APPROVAL_GRANT`(整份投 `RunnerDeps.allowedTools`,`config.ts` 启动校验)。
7
7
  *
8
8
  * 本仓在这些表上只剩下面几问(内容形、`*`、写路径形、名字引擎怎么读就怎么算):
9
- * ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。
9
+ * ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。请求的 allow 桶的形另由桶门判
10
+ * (`task-settings.ts` 的 `settingsPermissionBucketIssue`:串或带层条目),它进下面 ③ 时带层条目读成规则原文({@link toolListsOf})。
10
11
  * 本谓词不带条数帽(引擎的 `screenAllowedToolsValue` 只判形);条数帽是另一只函数 {@link allowLayerEntryCountIssue}(`MAX_ALLOW_LAYER_ENTRIES`,
11
12
  * 请求 allow 桶、技能两只清单、运维表四张表同一只、同一句)。
12
13
  * ② (7.116.0–7.122.x 的「引擎读不到的一形」—— 工具段为空的协议名 `mcp__srv__` 四张表都拒 —— 随 core 7.48.0 [ref] 退役:引擎判官给它判词
@@ -3,6 +3,7 @@ import { displayListEntry } from "./list-entry-display.js";
3
3
  import { limitRefusal } from "./limit-refusal.js";
4
4
  import { TOOL_NAME_ROSTER } from "./namespaced-rule-name.js";
5
5
  import { NOT_IN_ROSTER, entryProblem, entryProblemsFacts } from "./entry-problems.js";
6
+ import { layeredRuleOk } from "./permission-rule-list.js";
6
7
  export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
7
8
  export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
8
9
  const ALLOW_LAYER_OVER_CAP_TAIL = "Refused rather than truncated: a truncated list silently drops the entries past the cap. Merge entries into broader ones (mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**)) or trim the list";
@@ -162,7 +163,10 @@ function* toolListsOf(body) {
162
163
  const settings = body["settings"];
163
164
  const permissions = isPlainRecord(settings) ? settings["permissions"] : undefined;
164
165
  if (isPlainRecord(permissions))
165
- yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", at: "settings.permissions.allow", list: permissions["allow"] };
166
+ yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", at: "settings.permissions.allow", list: ruleTextsOfBucket(permissions["allow"]) };
167
+ }
168
+ function ruleTextsOfBucket(list) {
169
+ return Array.isArray(list) ? list.map((x) => (layeredRuleOk(x) ? x.rule : x)) : list;
166
170
  }
167
171
  function inertReadingOfList(l) {
168
172
  const face = TOOL_LIST_FACES[l.layer];
@@ -219,16 +219,24 @@ export type LocalImportIndeterminateCode =
219
219
  /** 兑付走本身没走完(本层抛出/中断)⇒ 这一条连走都没走到。 */
220
220
  | "walk_failed";
221
221
  /** prepare 面的一行。判别式联合:被收下的行**没有**拒因字段可读,被拒的行**必然**带拒因 ——
222
- * 「admitted 却带 reason」这种矛盾态在类型上不可表达。 */
222
+ * 「admitted 却带 reason」这种矛盾态在类型上不可表达。
223
+ * 收下的**收紧**(deny / ask)在 prepare 当场经直写接缝落店,行带它的结局 `outcome`(`persisted` = 这次写了 / `no-op` = 等价规则已在店);
224
+ * 收下的**放行**没有 `outcome` —— 它的结局在 redeem 回执里(放行要同意,收紧不等同意;见 `prepareLocalImport`)。 */
223
225
  export type LocalImportPreviewMember = {
224
226
  key: LocalImportRuleKey;
225
227
  admitted: true;
228
+ } | {
229
+ key: LocalImportRuleKey;
230
+ admitted: true;
231
+ outcome: LocalImportTighteningOutcome;
226
232
  } | {
227
233
  key: LocalImportRuleKey;
228
234
  admitted: false;
229
235
  reason: LocalImportRefusalCode;
230
236
  detail: string;
231
237
  };
238
+ /** 一条收紧在 prepare 当场落店的结局(与兑付回执的 `persisted` / `no-op` 同词同义;直写接缝 `addPersistedRule` 的 `added` / `no-op`)。 */
239
+ export type LocalImportTighteningOutcome = "persisted" | "no-op";
232
240
  /** 兑付回执的一行。 */
233
241
  export interface LocalImportOutcomeMember {
234
242
  key: LocalImportRuleKey;
@@ -266,6 +274,14 @@ export type LocalImportPrepared = {
266
274
  ok: false;
267
275
  reason: "batch-too-large";
268
276
  bytes: number;
277
+ }
278
+ /** 一条收紧写不进店(直写接缝答 `failed`)⇒ 这一批的放行全部扣下(不铸记录、不铸票)。`committed` 原样是 core 的两词闭集,
279
+ * wire 上与 `POST /v1/rules` 的写失败同一码同一键;此前已落的收紧留着(收紧方向),同一批重发即幂等(已落的答 `no-op`)。 */
280
+ | {
281
+ ok: false;
282
+ reason: "tightening-write-failed";
283
+ committed: RuleWriteCommitted;
284
+ detail: string;
269
285
  };
270
286
  /** A7 redeem 的结果。 */
271
287
  export type LocalImportRedeemed =
@@ -456,13 +472,20 @@ export type CardRulePersisted = {
456
472
  * 🔴 不 switch、不翻译、不折叠:词表属主是 core,本仓照转。 */
457
473
  code?: string;
458
474
  };
459
- /** 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)。 */
460
- export interface RuleImportLayerInput {
461
- layer: ImportedSettingsLayer;
475
+ /**
476
+ * 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)—— 按 `layer` 的联合,与引擎的 `CcImportLayer` 同形:
477
+ * 用户层**没有** `root`(它的 `/x` 在它自己的配置目录读,引擎不把它带进店;交了 root 的体在边界 400),项目层与本地层带 `root`(规则的作用域根)。
478
+ */
479
+ export type RuleImportLayerInput = {
480
+ layer: "userSettings";
481
+ path: string;
482
+ content: string;
483
+ } | {
484
+ layer: Exclude<ImportedSettingsLayer, "userSettings">;
462
485
  path: string;
463
486
  root: string;
464
487
  content: string;
465
- }
488
+ };
466
489
  /**
467
490
  * scope 的 **wire 形**:`global` | `project:<root>` 的判别式串([ref] v2 §6 F5「显式成文」)。
468
491
  *
@@ -232,25 +232,14 @@ function buildLocalImportLayers(candidates) {
232
232
  const byRoot = new Map();
233
233
  for (const c of candidates) {
234
234
  const key = c.scope.kind === "global" ? "" : c.scope.root;
235
- let bucket = byRoot.get(key);
236
- if (bucket === undefined) {
237
- bucket = { deny: [], ask: [], allow: [] };
238
- byRoot.set(key, bucket);
239
- }
240
- bucket[c.behavior].push(c.rule);
235
+ const bucket = byRoot.get(key) ?? [];
236
+ bucket.push(c.rule);
237
+ byRoot.set(key, bucket);
241
238
  }
242
- return [...byRoot].map(([root, lists]) => ({
243
- layer: root === "" ? "userSettings" : "projectSettings",
244
- path: root === "" ? LOCAL_IMPORT_LAYER_PATH : `${LOCAL_IMPORT_LAYER_PATH}:${root}`,
245
- root: root === "" ? "/" : root,
246
- readFile: async () => JSON.stringify({
247
- permissions: {
248
- ...(lists.deny.length > 0 ? { deny: lists.deny } : {}),
249
- ...(lists.ask.length > 0 ? { ask: lists.ask } : {}),
250
- ...(lists.allow.length > 0 ? { allow: lists.allow } : {}),
251
- },
252
- }),
253
- }));
239
+ return [...byRoot].map(([root, allow]) => {
240
+ const readFile = async () => JSON.stringify({ permissions: { allow } });
241
+ return root === "" ? { layer: "userSettings", path: LOCAL_IMPORT_LAYER_PATH, readFile } : { layer: "projectSettings", path: `${LOCAL_IMPORT_LAYER_PATH}:${root}`, root, readFile };
242
+ });
254
243
  }
255
244
  function buildLocalImportMembers(members) {
256
245
  return members.map((m) => {
@@ -367,12 +356,10 @@ export function createRuleConsentLane(stores, opts) {
367
356
  return { ok: true, kind: "single", rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
368
357
  },
369
358
  async prepareImport(principal, layers) {
370
- const ccLayers = layers.map((l) => ({
371
- layer: l.layer,
372
- path: l.path,
373
- root: l.root,
374
- readFile: async () => l.content,
375
- }));
359
+ const ccLayers = layers.map((l) => {
360
+ const readFile = async () => l.content;
361
+ return l.layer === "userSettings" ? { layer: l.layer, path: l.path, readFile } : { layer: l.layer, path: l.path, root: l.root, readFile };
362
+ });
376
363
  const { preview, approvalId } = await prepareCcImport({ layers: ccLayers, principal, deps });
377
364
  if (approvalId === undefined) {
378
365
  return { ok: true, preview };
@@ -494,16 +481,41 @@ export function createRuleConsentLane(stores, opts) {
494
481
  continue;
495
482
  }
496
483
  seen.add(dedupKey);
484
+ admitted.push({ at: members.length, row: screened });
497
485
  members.push({ key, admitted: true });
498
- admitted.push(screened);
499
486
  }
500
- if (admitted.length === 0)
487
+ const allows = [];
488
+ for (const { at, row: c } of admitted) {
489
+ const key = members[at].key;
490
+ const behavior = c.behavior;
491
+ if (!isDirectWritableRuleBehavior(behavior)) {
492
+ allows.push({ ...c, behavior });
493
+ continue;
494
+ }
495
+ const added = await addPersistedRule({ behavior, rule: c.rule, scope: c.scope, principal, provider: stores.provider });
496
+ switch (added.status) {
497
+ case "added":
498
+ members[at] = { key, admitted: true, outcome: "persisted" };
499
+ break;
500
+ case "no-op":
501
+ members[at] = { key, admitted: true, outcome: "no-op" };
502
+ break;
503
+ case "failed":
504
+ localImportLogger.warn("local_import_tightening_write_failed", { behavior, scope: key.scope, committed: added.committed, note: "a deny / ask of this batch did not land — the batch's allows are withheld (no import record, no ticket); sending the same batch again is idempotent" });
505
+ return { ok: false, reason: "tightening-write-failed", committed: added.committed, detail: added.error };
506
+ default: {
507
+ const unknown = added;
508
+ throw new Error(`unknown addPersistedRule outcome: ${JSON.stringify(unknown)}`);
509
+ }
510
+ }
511
+ }
512
+ if (allows.length === 0)
501
513
  return { ok: true, preview: { members } };
502
- const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(admitted), principal, deps });
514
+ const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(allows), principal, deps });
503
515
  const engineKeys = new Set(preview.candidates.map((c) => `${c.behavior}${LOCAL_IMPORT_KEY_SEP}${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
504
516
  for (let i = 0; i < members.length; i++) {
505
517
  const m = members[i];
506
- if (m === undefined || !m.admitted)
518
+ if (m === undefined || !m.admitted || "outcome" in m)
507
519
  continue;
508
520
  if (engineKeys.has(`${m.key.behavior}${LOCAL_IMPORT_KEY_SEP}${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
509
521
  continue;