@sema-agent/server 7.144.0 → 7.146.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 (305) hide show
  1. package/MIGRATION.md +621 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +233 -54
  5. package/deploy/facts/CHANGES.json +25 -0
  6. package/deploy/facts/README.md +57 -0
  7. package/deploy/facts/lanes.json +54 -0
  8. package/deploy/facts/lanes.schema.json +57 -0
  9. package/deploy/facts/sandbox-rbac.json +113 -0
  10. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  11. package/deploy/facts/sealed-key.json +9 -0
  12. package/deploy/facts/sealed-key.schema.json +52 -0
  13. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  14. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  15. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  16. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  17. package/deploy/sema-up/chart/values.yaml +7 -0
  18. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  19. package/dist/adoption/import-limits.d.ts +6 -0
  20. package/dist/adoption/import-limits.js +7 -0
  21. package/dist/adoption/import.d.ts +62 -0
  22. package/dist/adoption/import.js +107 -0
  23. package/dist/adoption/wire.d.ts +52 -0
  24. package/dist/adoption/wire.js +9 -0
  25. package/dist/approval-ask-machine.d.ts +2 -2
  26. package/dist/approval-card.d.ts +3 -12
  27. package/dist/approval-card.js +0 -4
  28. package/dist/approval-gate-currency.d.ts +1 -1
  29. package/dist/approval-gate-currency.js +1 -1
  30. package/dist/approval-reconciler.d.ts +1 -1
  31. package/dist/audit.d.ts +2 -21
  32. package/dist/audit.js +0 -9
  33. package/dist/boot/deployment-mods.d.ts +11 -1
  34. package/dist/boot/deployment-mods.js +15 -2
  35. package/dist/boot/device-lane.js +2 -0
  36. package/dist/boot/engine-lease.d.ts +193 -21
  37. package/dist/boot/engine-lease.js +146 -20
  38. package/dist/boot/execution-env.d.ts +4 -0
  39. package/dist/boot/execution-env.js +12 -0
  40. package/dist/boot/exit-boundary.d.ts +50 -0
  41. package/dist/boot/exit-boundary.js +32 -0
  42. package/dist/boot/exit-last.d.ts +20 -4
  43. package/dist/boot/exit-last.js +21 -4
  44. package/dist/boot/governance-seams.js +1 -0
  45. package/dist/boot/memory-consolidation.d.ts +29 -17
  46. package/dist/boot/memory-consolidation.js +26 -15
  47. package/dist/boot/reapers.d.ts +12 -4
  48. package/dist/boot/reapers.js +39 -15
  49. package/dist/boot/resolve-spec.d.ts +2 -0
  50. package/dist/boot/resolve-spec.js +38 -15
  51. package/dist/boot/runner-deps.js +1 -1
  52. package/dist/boot/session-faces.d.ts +1 -1
  53. package/dist/boot/session-faces.js +6 -4
  54. package/dist/boot/shutdown.js +27 -9
  55. package/dist/boot/stage-01-config.js +1 -1
  56. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  57. package/dist/boot/stage-03-store-backend.js +2 -2
  58. package/dist/boot/stage-04-stores.d.ts +1 -0
  59. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  60. package/dist/boot/stage-05-execution-env.js +3 -3
  61. package/dist/boot/stage-06-runners.d.ts +3 -0
  62. package/dist/boot/stage-06-runners.js +11 -10
  63. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  64. package/dist/boot/stage-07-capability-layer.js +7 -2
  65. package/dist/boot/stage-08-reapers.d.ts +3 -0
  66. package/dist/boot/stage-08-reapers.js +3 -3
  67. package/dist/boot/stage-09-leader.d.ts +4 -1
  68. package/dist/boot/stage-10-http-server.d.ts +4 -1
  69. package/dist/boot/stage-10-http-server.js +4 -3
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +35 -19
  72. package/dist/boot/workflow-orchestration.d.ts +1 -1
  73. package/dist/boot/workflow-orchestration.js +2 -1
  74. package/dist/capabilities/composite-members.d.ts +3 -1
  75. package/dist/capabilities/composite-members.js +3 -1
  76. package/dist/capabilities/scenarios.d.ts +2 -2
  77. package/dist/capability-facts.d.ts +18 -1
  78. package/dist/capability-facts.js +25 -7
  79. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  80. package/dist/config-catalog.js +13 -4
  81. package/dist/config-d-migration.d.ts +3 -2
  82. package/dist/config-provider.d.ts +2 -2
  83. package/dist/config-types.d.ts +43 -4
  84. package/dist/config.d.ts +6 -1
  85. package/dist/config.js +67 -11
  86. package/dist/deleted-session-ids.d.ts +19 -0
  87. package/dist/deleted-session-ids.js +26 -0
  88. package/dist/deploy-facts.d.ts +155 -0
  89. package/dist/deploy-facts.js +94 -0
  90. package/dist/device-ws-hub.d.ts +48 -3
  91. package/dist/device-ws-hub.js +48 -2
  92. package/dist/error-code-family.d.ts +35 -0
  93. package/dist/error-code-family.js +26 -0
  94. package/dist/fleet/drain-entry.d.ts +39 -3
  95. package/dist/fleet/drain-entry.js +6 -0
  96. package/dist/fleet/fleet-bus.d.ts +15 -4
  97. package/dist/fleet/fleet-bus.js +17 -3
  98. package/dist/hooks/branch-transcript.d.ts +1 -1
  99. package/dist/hooks/branch-transcript.js +1 -0
  100. package/dist/hooks/classic-stations.d.ts +25 -8
  101. package/dist/hooks/classic-stations.js +29 -3
  102. package/dist/hooks/hook-runner.d.ts +33 -16
  103. package/dist/hooks/hook-runner.js +166 -73
  104. package/dist/hooks/hook-source.d.ts +14 -2
  105. package/dist/hooks/hook-source.js +8 -0
  106. package/dist/http/active-run-conflict.d.ts +1 -1
  107. package/dist/http/active-run-conflict.js +1 -1
  108. package/dist/http/admission.js +13 -2
  109. package/dist/http/checkpoint-ctx.d.ts +60 -71
  110. package/dist/http/checkpoint-ctx.js +35 -40
  111. package/dist/http/dispatch.js +18 -2
  112. package/dist/http/durable-run-leg.d.ts +5 -0
  113. package/dist/http/durable-run-leg.js +8 -6
  114. package/dist/http/execution-context.d.ts +4 -0
  115. package/dist/http/execution-context.js +9 -0
  116. package/dist/http/health-self-report.d.ts +6 -2
  117. package/dist/http/health-self-report.js +3 -2
  118. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  119. package/dist/http/memory-compliance-failure-reply.js +2 -15
  120. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  121. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  122. package/dist/http/principal-gate.d.ts +35 -12
  123. package/dist/http/principal-gate.js +16 -12
  124. package/dist/http/request-guards.d.ts +1 -1
  125. package/dist/http/request-guards.js +5 -5
  126. package/dist/http/resume-legs.d.ts +22 -0
  127. package/dist/http/resume-legs.js +31 -20
  128. package/dist/http/route-ctx.d.ts +26 -10
  129. package/dist/http/route-table.js +2 -0
  130. package/dist/http/routes/adoption.d.ts +11 -0
  131. package/dist/http/routes/adoption.js +80 -6
  132. package/dist/http/routes/agents-roster.js +1 -0
  133. package/dist/http/routes/approvals-assistant.d.ts +20 -1
  134. package/dist/http/routes/approvals-assistant.js +29 -7
  135. package/dist/http/routes/background.js +1 -0
  136. package/dist/http/routes/capabilities.js +4 -1
  137. package/dist/http/routes/diagnostics.d.ts +9 -14
  138. package/dist/http/routes/diagnostics.js +10 -20
  139. package/dist/http/routes/engine.d.ts +31 -0
  140. package/dist/http/routes/engine.js +81 -0
  141. package/dist/http/routes/memory-bundle.js +3 -0
  142. package/dist/http/routes/memory-consolidation.js +3 -5
  143. package/dist/http/routes/memory-origin.d.ts +1 -1
  144. package/dist/http/routes/memory-origin.js +2 -4
  145. package/dist/http/routes/memory-policy.js +5 -1
  146. package/dist/http/routes/observability.js +3 -6
  147. package/dist/http/routes/rules.d.ts +1 -1
  148. package/dist/http/routes/rules.js +5 -2
  149. package/dist/http/routes/runs.js +7 -3
  150. package/dist/http/routes/session-sync.js +24 -25
  151. package/dist/http/routes/sessions-list.d.ts +1 -1
  152. package/dist/http/routes/sessions.js +4 -0
  153. package/dist/http/routes/tasks.js +35 -14
  154. package/dist/http/routes/trace-usage.js +1 -1
  155. package/dist/http/run-concurrency-cap.d.ts +86 -0
  156. package/dist/http/run-concurrency-cap.js +54 -0
  157. package/dist/http/run-meta.d.ts +14 -7
  158. package/dist/http/run-meta.js +5 -2
  159. package/dist/http/server.d.ts +27 -16
  160. package/dist/http/server.js +21 -3
  161. package/dist/http/stored-body-replay.d.ts +9 -6
  162. package/dist/http/stored-body-replay.js +19 -5
  163. package/dist/http/submit-ctx.d.ts +12 -0
  164. package/dist/http/submit-ctx.js +23 -0
  165. package/dist/http/wire-types.d.ts +8 -3
  166. package/dist/json-body-limit.d.ts +8 -0
  167. package/dist/json-body-limit.js +2 -0
  168. package/dist/leader/diffout.d.ts +1 -1
  169. package/dist/local-data-root.d.ts +2 -2
  170. package/dist/memory-operator-faces.d.ts +9 -4
  171. package/dist/memory-scope-stores.d.ts +9 -7
  172. package/dist/memory-scope-stores.js +3 -14
  173. package/dist/memory-scope.d.ts +36 -0
  174. package/dist/memory-scope.js +32 -1
  175. package/dist/net-address.d.ts +53 -0
  176. package/dist/net-address.js +111 -0
  177. package/dist/observability/fail-open.d.ts +28 -12
  178. package/dist/observability/fail-open.js +28 -12
  179. package/dist/orchestration/workflow-notify-journal.js +2 -3
  180. package/dist/org-delivery-gate.js +1 -1
  181. package/dist/peer-directory.d.ts +6 -12
  182. package/dist/peer-directory.js +5 -1
  183. package/dist/plugins/approval-ask-store-file.d.ts +16 -2
  184. package/dist/plugins/approval-ask-store-file.js +21 -1
  185. package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
  186. package/dist/plugins/approval-ask-store-memory.js +11 -8
  187. package/dist/plugins/caching-session-store.d.ts +6 -1
  188. package/dist/plugins/caching-session-store.js +6 -0
  189. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  190. package/dist/plugins/checkpoint-store-face.js +152 -0
  191. package/dist/plugins/file-run-store.d.ts +13 -7
  192. package/dist/plugins/file-run-store.js +12 -13
  193. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  194. package/dist/plugins/fork-routing-session-store.js +2 -2
  195. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  196. package/dist/plugins/local-checkpoint-store.d.ts +16 -5
  197. package/dist/plugins/local-checkpoint-store.js +25 -6
  198. package/dist/plugins/local-session-store.d.ts +80 -21
  199. package/dist/plugins/local-session-store.js +79 -22
  200. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  201. package/dist/plugins/memory-engine-pg.js +96 -214
  202. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  203. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  204. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  205. package/dist/plugins/memory-engine-tidb.js +97 -227
  206. package/dist/plugins/memory-key-guards.d.ts +1 -1
  207. package/dist/plugins/memory-origin-law.d.ts +3 -3
  208. package/dist/plugins/memory-run-store.d.ts +2 -0
  209. package/dist/plugins/memory-run-store.js +6 -0
  210. package/dist/plugins/one-time-migrations.d.ts +32 -56
  211. package/dist/plugins/one-time-migrations.js +27 -119
  212. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  213. package/dist/plugins/permission-rule-store-file.js +8 -1
  214. package/dist/plugins/pg-pool.js +1 -1
  215. package/dist/plugins/pg-session-storage.d.ts +17 -4
  216. package/dist/plugins/pg-session-storage.js +8 -4
  217. package/dist/plugins/run-store-sql.d.ts +4 -0
  218. package/dist/plugins/run-store-sql.js +5 -2
  219. package/dist/plugins/schema-ensure-legs.js +2 -2
  220. package/dist/plugins/session-fork.d.ts +31 -9
  221. package/dist/plugins/session-fork.js +3 -2
  222. package/dist/plugins/session-placement.d.ts +4 -2
  223. package/dist/plugins/session-store.d.ts +1 -1
  224. package/dist/plugins/sql-json-column.d.ts +5 -1
  225. package/dist/plugins/store-backend.d.ts +11 -8
  226. package/dist/plugins/store-backend.js +8 -7
  227. package/dist/plugins/store-contracts.d.ts +1 -11
  228. package/dist/plugins/store-contracts.js +0 -9
  229. package/dist/plugins/store-sweep.d.ts +63 -0
  230. package/dist/plugins/store-sweep.js +48 -0
  231. package/dist/plugins/tidb-pool.js +1 -1
  232. package/dist/plugins/tidb-session-store.d.ts +16 -3
  233. package/dist/plugins/tidb-session-store.js +8 -4
  234. package/dist/prompt-screen.d.ts +18 -0
  235. package/dist/prompt-screen.js +5 -0
  236. package/dist/rules-consent.d.ts +56 -51
  237. package/dist/rules-consent.js +86 -14
  238. package/dist/run-acceptance.d.ts +40 -10
  239. package/dist/run-acceptance.js +38 -5
  240. package/dist/run-cancel-context.d.ts +4 -1
  241. package/dist/run-cancel-context.js +2 -1
  242. package/dist/runs.js +2 -2
  243. package/dist/sandbox-egress-policy.js +6 -11
  244. package/dist/sandbox-rbac-rules.d.ts +88 -0
  245. package/dist/sandbox-rbac-rules.js +80 -0
  246. package/dist/sealed-key-layout.d.ts +17 -0
  247. package/dist/sealed-key-layout.js +4 -0
  248. package/dist/sealed-key.js +5 -5
  249. package/dist/security.d.ts +48 -9
  250. package/dist/security.js +9 -2
  251. package/dist/session-agent.d.ts +39 -0
  252. package/dist/session-agent.js +16 -0
  253. package/dist/session-error-wire.d.ts +4 -4
  254. package/dist/session-notify-door.d.ts +5 -3
  255. package/dist/session-resident-background.d.ts +57 -0
  256. package/dist/session-resident-background.js +20 -0
  257. package/dist/session-sync-kernel.d.ts +53 -14
  258. package/dist/session-sync-kernel.js +70 -1
  259. package/dist/session-sync.d.ts +5 -3
  260. package/dist/session-workspace.d.ts +1 -0
  261. package/dist/session-workspace.js +1 -0
  262. package/dist/sighup-idle.d.ts +9 -7
  263. package/dist/sighup-idle.js +4 -5
  264. package/dist/spec-fields.d.ts +16 -0
  265. package/dist/spec-fields.js +7 -0
  266. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  267. package/dist/stored-request-env-credentials-m38.js +8 -0
  268. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  269. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +33 -429
  270. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +70 -209
  271. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  272. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  273. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  274. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  275. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  276. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  277. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  278. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  279. package/dist/task-settings.d.ts +59 -4
  280. package/dist/task-settings.js +50 -4
  281. package/dist/tool-approval.d.ts +2 -2
  282. package/dist/trace/core-keyset-guard.d.ts +8 -7
  283. package/dist/trace/engine-notice-wire.d.ts +1 -1
  284. package/dist/trace/engine-notice-wire.js +2 -0
  285. package/dist/trace/exit-seat.d.ts +6 -0
  286. package/dist/trace/exit-seat.js +1 -0
  287. package/dist/trace/model-segment.d.ts +3 -2
  288. package/dist/trace/model-segment.js +16 -8
  289. package/dist/trace/project.js +9 -0
  290. package/dist/trace/redact.d.ts +79 -4
  291. package/dist/trace/redact.js +523 -123
  292. package/dist/trace/self-minted-wire.d.ts +391 -1
  293. package/dist/trace/self-minted-wire.js +145 -0
  294. package/dist/trace/wire-projection-faces.d.ts +1 -1
  295. package/dist/trace/wire-projection-faces.js +1 -1
  296. package/dist/usage-analytics.d.ts +4 -0
  297. package/package.json +12 -4
  298. package/scripts/drain-check-local.mjs +8 -29
  299. package/scripts/drain-check.mysql.sql +3 -24
  300. package/scripts/drain-check.pg.sql +3 -24
  301. package/skills/update-config.md +1 -1
  302. package/wire-changes/server@7.145.0.json +146 -0
  303. package/wire-changes/server@7.146.0.json +3221 -0
  304. package/dist/approval-batch-census.d.ts +0 -27
  305. package/dist/approval-batch-census.js +0 -27
@@ -8,7 +8,7 @@ import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprin
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
10
  import { joinFromStore, splitForStore } from "./wiring-section-codec.js";
11
- import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode, isTerminalRunStatus, settledLegObservationOf } from "./store-contracts.js";
11
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, isTerminalRunStatus, settledLegObservationOf } from "./store-contracts.js";
12
12
  import { notifyRunTerminal } from "../observability/run-terminal.js";
13
13
  import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
14
14
  import { createInProcessSpaceCommitTable, fenceSpaceCommitAtTerminal, SPACE_COMMIT_RECORD_SCHEMA } from "../workspace-space-commit.js";
@@ -352,9 +352,12 @@ export class FileRunStore {
352
352
  }
353
353
  }
354
354
  }
355
- persist(r) {
355
+ writeRunFile(r) {
356
356
  mkdirSync(this.runDir(r.taskId), { recursive: true });
357
357
  atomicWriteFile(this.tmpDir, this.runJsonPath(r.taskId), `${JSON.stringify(this.toDisk(r))}\n`);
358
+ }
359
+ persist(r) {
360
+ this.writeRunFile(r);
358
361
  this.runs.set(r.taskId, r);
359
362
  }
360
363
  eventLogFor(taskId) {
@@ -427,6 +430,13 @@ export class FileRunStore {
427
430
  this.byIdem.set(idemKey, taskId);
428
431
  return { ok: true };
429
432
  }
433
+ async fillObjectivePreview(taskId, preview) {
434
+ const r = this.runs.get(taskId);
435
+ if (!r || r.objectivePreview !== null)
436
+ return;
437
+ this.writeRunFile({ ...r, objectivePreview: preview });
438
+ r.objectivePreview = preview;
439
+ }
430
440
  async requestCancel(taskId, owner) {
431
441
  const r = this.runs.get(taskId);
432
442
  if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
@@ -1097,17 +1107,6 @@ export class FileRunStore {
1097
1107
  }
1098
1108
  }
1099
1109
  }
1100
- migrateLegacyHolderDeathCodes() {
1101
- let migrated = 0;
1102
- for (const r of this.runs.values()) {
1103
- const code = readLegacyHolderDeathCode(r);
1104
- if (code === null || r.errorCode === code)
1105
- continue;
1106
- this.persist({ ...r, errorCode: code });
1107
- migrated += 1;
1108
- }
1109
- return migrated;
1110
- }
1111
1110
  sessionIdsWithRuns() {
1112
1111
  return new Set([...this.runs.values()].map((r) => r.sessionId));
1113
1112
  }
@@ -34,7 +34,7 @@
34
34
  */
35
35
  import { type SessionStore, type AcquiredSession, type SessionHead, type SessionStoreSummary, type SessionPlacement, type PlacedSessionRow } from "@sema-agent/core";
36
36
  import type { ExecutionPlacementSeat } from "../execution-placement.js";
37
- import type { ForkToFn } from "./session-fork.js";
37
+ import type { ForkPlacement, ForkToFn } from "./session-fork.js";
38
38
  /** acquire 选项形(core 5.48 SessionStore 契约):[ref] placement=受信 spawner 的分区建会话声明。 */
39
39
  type AcquireOpts = {
40
40
  requireExisting?: boolean;
@@ -86,7 +86,11 @@ export declare class ForkRoutingSessionStore implements SessionStore {
86
86
  unpin(sessionId: string): void | Promise<void>;
87
87
  noteTaskRun(sessionId: string, taskId: string, runId?: string): void | Promise<void>;
88
88
  list(): Promise<SessionStoreSummary[]>;
89
- fork(sourceId: string, owner?: string | null): Promise<string | null>;
89
+ /** `opts.placement`(后台 fork 子代的转录进委派分区)原样交给做这次 fork 的那只店 —— 落不落由那只店的声明判(host 声明了分区就落;瞬态店不声明 ⇒ 忽略),
90
+ * 本类不判第二次(声明面从 host 逐字转发,引擎据它决定交不交元组)。 */
91
+ fork(sourceId: string, owner?: string | null, opts?: {
92
+ placement?: ForkPlacement;
93
+ }): Promise<string | null>;
90
94
  get size(): number;
91
95
  dispose(): void | Promise<void>;
92
96
  }
@@ -191,10 +191,10 @@ export class ForkRoutingSessionStore {
191
191
  list() {
192
192
  return this.transient.list?.() ?? Promise.resolve([]);
193
193
  }
194
- async fork(sourceId, owner) {
194
+ async fork(sourceId, owner, opts) {
195
195
  const routed = this.route.get(sourceId);
196
196
  const tryFork = async (store, where) => {
197
- const forked = (await store.fork?.(sourceId, owner)) ?? null;
197
+ const forked = (await store.fork?.(sourceId, owner, opts)) ?? null;
198
198
  if (forked !== null) {
199
199
  this.remember(sourceId, where);
200
200
  this.remember(forked, where);
@@ -16,9 +16,10 @@
16
16
  * 一律问**店**:它能叫出名字的每一只桶,判据在桶里判(不在桶外按「谁可能有」预筛)。
17
17
  * · SQL:规则表里的每一只桶(`listRuleBucketOwners`,按 `owner_key` 分页)。不从审批记录推 —— 收编把规则桶整桶改键到新身份、审批史留在旧身份名下,
18
18
  * 从记录推出的属主集在收编之后漏掉新桶。
19
- * · File:引擎的 File 分区后端**没有**列桶口(桶文件按属主名的单向摘要命名),本步能叫出名字的只有本店导入记录点名过的属主
20
- * (`FileRuleApprovalRecordStore.importRecordOwners`;`imported-cc` 只由导入记录派生,记录是只增日志)。**已知缺口(如实)**:引擎的本地数据根收编
21
- * 把一只桶改名到一个没有任何导入记录点名的身份之后,那只桶本步叫不出、不读 —— 等引擎给 File 分区后端一个列桶口再收。
19
+ * · File:本店导入记录点名过的属主(`FileRuleApprovalRecordStore.importRecordOwners`;`imported-cc` 只由导入记录派生,记录是只增日志)∪ 引擎 File 分区后端
20
+ * 自己列得出名字的属主(`FileDurableRulePartitionProvider.listBucketOwners().owners`:本机属主那只桶,与收编标记点名的身份 —— 收编把桶改名到一个没有任何
21
+ * 导入记录点名的身份之后,那只桶照样被叫出来、被读)。**已知缺口(如实)**:桶文件按属主名的单向摘要命名,记录与收编标记都叫不出属主的桶本步不读;
22
+ * 引擎给的「叫不出名字的桶数」(`unnamedBuckets`)只认本机属主那只桶与收编标记、把导入记录点名过的桶也算进去,不是本步的缺数 —— 不报它(报了会把读过的桶说成没读)。
22
23
  *
23
24
  * ## 写在哪里(一处)
24
25
  * 墓碑经**这只桶自己的写面**落(`writerOf(partition).apply({ kind: "tighten-delete" })`,引擎的写面契约)—— SQL 两孪生走 `SqlDurableRulePartition` 的那只
@@ -30,10 +31,11 @@
30
31
  * 列桶 / 解析桶 / 读桶 / 取点 / 写面**抛** ⇒ 原样抛:起服拒,收据账里已提交的墓碑随拒因交出(与本段其余各步同一条路)。例外两条,都由店的判官按抛出物的**类**判(不按文案):
31
32
  * 取点时店说这只桶**在写它的那一步里没了**(`source.isVanishedBucket`:另一只副本删桶恰落在这一窗)⇒ 无可退役、点名跳过 `vanished`(桶都没了,它什么都不放行);
32
33
  * 另一条是解析桶(属主 → 分区)或读桶时**店的唯一判官整桶拒**了
33
- * 这只桶(解析与读同一条律、同一只判官;`source.isWholeBucketRefusal`:确定性判决,引擎读侧对它同样整桶拒、它什么都不放行;SQL = 读腿真往外交的那两类 —— 判官四道的拒与某只 JSON 列的体读不出)
34
- * ⇒ 点名跳过 `bucket_unreadable`,接着读下一只桶(一只读不出的桶不挡别的桶,也不挡起服);一次读失败(连接 / 超时)
35
- * 证明不了以后也读不出,所以它**不**归这一类 —— 跳过它就是让那几行在读恢复之后照旧放行。File 店没有这种判决证据(引擎的 File 分区对损坏与 I/O 失败抛同一种错,
36
- * 解析时读收编标记也是:一次 EIO 与一份坏标记同一种错)⇒ 那一侧解析 / 读桶抛一律起服拒。另两种店**答了**却做不了的(没有写面或写面拒 / 并发争不过)⇒ 点名跳过,下一次起服再试,其间那几行照旧放行,每次起服都点名。
34
+ * 这只桶(解析与读同一条律、同一只判官;`source.isWholeBucketRefusal`:确定性判决,引擎读侧对它同样整桶拒、它什么都不放行;SQL = 读腿真往外交的那两类 —— 判官四道的拒与某只 JSON 列的体读不出;
35
+ * File = 引擎 File 分区的拒码 `permission_rules.bucket_damaged`〔同一份字节每次读都同样拒〕)
36
+ * ⇒ 点名跳过 `bucket_unreadable`,接着读下一只桶(一只读不出的桶不挡别的桶,也不挡起服);一次读失败(连接 / 超时;File 的拒码 `permission_rules.bucket_unreadable`,
37
+ * 带系统错名 `errno`)证明不了以后也读不出,所以它**不**归这一类 —— 跳过它就是让那几行在读恢复之后照旧放行。File 解析时读收编标记的拒(标记自己的拒,不带桶拒码)同样不归这一类 ⇒ 起服拒。
38
+ * 另两种店**答了**却做不了的(没有写面或写面拒 / 并发争不过)⇒ 点名跳过,下一次起服再试,其间那几行照旧放行,每次起服都点名。
37
39
  */
38
40
  import { type DurableRulePartitionProvider, type PersistedRule, type RuleAdd, type RuleOwner } from "@sema-agent/core";
39
41
  /** 一只被退役的导入 add 所在的行(一行一条读数)。**不带规则正文**(与 M11 同一条纪律:规则正文不出店)—— 桶键 + 工具名足够让运维去列举面对上它。 */
@@ -77,6 +79,6 @@ export interface ImportedLayerRootRetirementSource {
77
79
  }
78
80
  /** 判据(引擎 §5.14 那一句,逐字):这一行里该退役的 add;不选 ⇒ 空。 */
79
81
  export declare function importedLayerRootAddsOf(row: PersistedRule): readonly RuleAdd[];
80
- /** 跑一遍:读记录点名的每只桶(按桶键去重),退役判据选中的 add。读数随每次提交记进 `into`。 */
82
+ /** 跑一遍:读店叫得出名字的每只桶(`candidateOwners()`,见头注「读哪些桶」;两源有重 ⇒ 按桶键去重),退役判据选中的 add。读数随每次提交记进 `into`。 */
81
83
  export declare function retireImportedLayerRootAllows(source: ImportedLayerRootRetirementSource, into: ImportedLayerRootReadings): Promise<void>;
82
84
  //# sourceMappingURL=imported-layer-root-retirement.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ExecutionOutcomeRecordWord, type GateOutcome, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
2
- import { type CheckpointAskCandidate, type CtxClaimRewrite, type CtxRowRewrite, type CtxRowsRewrite, type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-sql.js";
2
+ import { type CheckpointAskCandidate, type CtxClaimRewrite, type CtxRowRewrite, type CtxRowsRewrite, type DecidedApprovalRecord, type PendingCheckpoint, type PendingRowBinding } from "./checkpoint-store-face.js";
3
3
  import { type FencedRun, type InProcessClaimFence } from "./store-contracts.js";
4
4
  import type { SealedCheckpointCtx } from "../http/checkpoint-ctx.js";
5
5
  export interface LocalCheckpointStoreOptions {
@@ -91,6 +91,15 @@ export declare class LocalCheckpointStore {
91
91
  private ensureScopesRebuilt;
92
92
  /** All still-pending checkpoints across the registered scopes (summary + full row via get). */
93
93
  private pendings;
94
+ /**
95
+ * 一条会话的待决行 —— **只读这条会话自己的行**(本店停驻行读者的那一条律:一行的字节只坏它自己那一行;决断不许因为别的会话的一行坏行而失败)。
96
+ * 读口 = core 店的 `listBySession`(按会话取行,一行读不出的行答 `unreadable` 而不是整页失败、也不是跳过;它不经 scope 注册表、不投摘要);
97
+ * `unreadable` 的行经 `get(token)` 取原行(core 店的 `get` 原样拷出,不判形)—— 它仍是这条会话的一张待决行,选不选它由下游的门型绑定判
98
+ * ({@link pickPendingRow}:门型读不出 ⇒ 不命中,与 SQL 孪生 `gate_kind IN (…)` 对 NULL 同向),续不续由续跑的第一级(core 判官)答。
99
+ * 修前三个按会话的读口(`findPendingTokenBySession` / `peekPendingScope` / `deleteBySession`)走 {@link pendings} 的整店枚举,而那一路经 core 的
100
+ * `listByScope` 投摘要 —— 同库任何一行摘要读不动(缺 `state`)⇒ 整只枚举抛,别人那张好卡的决断也 500。
101
+ */
102
+ private sessionPendings;
94
103
  /** Operator approval queue — the same NON-secret card as the SQL twins (never the token). */
95
104
  /** 每一只待决 checkpoint 的会话(去重;不按门型过滤 —— 理由与 SQL 孪生 `listPendingSessionIds` 逐字同,见那里)。 */
96
105
  listPendingSessionIds(): Promise<string[]>;
@@ -157,9 +166,11 @@ export declare class LocalCheckpointStore {
157
166
  * 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
158
167
  *
159
168
  * ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
160
- * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级拒(两只孪生 7.144.0 起都不按版本拒 ——
161
- * 本引擎续不续由 core 判官在续跑时答;本形连 SQL 孪生剩下的坏体一臂也没有,唯一的读不动形是账本重放级 = 整店同时读不动,
162
- * `findPendingTokenBySession` 先抛),所以「行体读得出而 scope 读不出」这一支在本形结构上不存在。哪天本形补了行级拒,这里要跟着改成不碰行体的读。
169
+ * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级拒、也不判形(两只孪生 7.144.0 起都不按版本拒 ——
170
+ * 本引擎续不续由 core 判官在续跑时答),原样拷出一行。读不动的形有两层:整店级 = 账本重放抛(认不出的事件种类等 ⇒ 整只店装不起来);
171
+ * 单行级也存在 —— 按会话的读口({@link sessionPendings})走 core 店的 `listBySession`,摘要读不出的那一行答 `unreadable`(读口不抛,行经 `get(token)` 取原行)。
172
+ * 即便如此,「行体读得出而 scope 读不出」这一支在本形仍结构上不存在:`scope` 是账本重放认一行的门(`token` / `scope` / `status` 皆字符串)里的成员,
173
+ * 门不过的行重放时被跳过并披露、根本不进店;进了店的行,行体其余成员损成什么样,`get(token)?.scope` 都是字符串。哪天本形补了行级拒、或 core 放宽重放的那道门,这里要跟着改成不碰行体的读。
163
174
  */
164
175
  peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
165
176
  /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).
@@ -210,7 +221,7 @@ export declare class LocalCheckpointStore {
210
221
  */
211
222
  rewriteCtxWhileClaimed(sessionId: string, taskId: string, ctx: SealedCheckpointCtx, legHolds: (run: FencedRun) => boolean): Promise<CtxClaimRewrite>;
212
223
  /**
213
- * 🔴 **一次性迁移的 ctx 行改写店腿**(local 孪生;`checkpoint-store-sql.ts` `CheckpointCtxRowsRewriter`,判词与 SQL 孪生 `rewriteCtxRows` 逐条同):
224
+ * 🔴 **一次性迁移的 ctx 行改写店腿**(local 孪生;`checkpoint-store-face.ts` `CheckpointCtxRowsRewriter`,判词与 SQL 孪生 `rewriteCtxRows` 逐条同):
214
225
  * 逐档读(信封经读腿**同一只**判形 —— 读不出 ⇒ 计数、跳过、**不删**),`rewrite` 交回新体 ⇒ 信封原样(`sessionId` / `updatedAt` 不动:收割按 `updatedAt` 判)
215
226
  * 只换 `ctx`,经写腿同一只形闸、同一只原子写落回原档。封签绑的是信封里的 `sessionId`(与 `getCtx` 认行的那一格同一个)。
216
227
  * 数据根独占 + 起服在任何读写者之前跑 ⇒ 本车道没有并发写者,不需要 CAS;没有一行装不下这回事(`tooLarge` 恒 0)。
@@ -5,8 +5,9 @@ import { durableRowIdentity } from "../checkpoint-identity.js";
5
5
  import { createHash } from "node:crypto";
6
6
  import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
7
7
  import { join } from "node:path";
8
- import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
9
- import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, candidateIdentityOf, pendingRowPrecedes, queueRowGateKind, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
8
+ import { CHECKPOINT_LIST_LIMITS, FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
9
+ import { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
10
+ import { approvalDecisionOfWinner, boundedRuleOffers, boundedToolInput, candidateIdentityOf, pendingRowPrecedes, queueRowGateKind, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-face.js";
10
11
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
11
12
  import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, readStringArrayColumn } from "./sql-json-column.js";
12
13
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
@@ -160,6 +161,24 @@ export class LocalCheckpointStore {
160
161
  }
161
162
  return out;
162
163
  }
164
+ async sessionPendings(sessionId, scope) {
165
+ if (sessionId === "")
166
+ return [];
167
+ const out = [];
168
+ let after;
169
+ do {
170
+ const page = await this.inner.listBySession({ sessionId, status: "pending", ...(scope !== undefined ? { scope } : {}), ...(after !== undefined ? { after } : {}), limit: CHECKPOINT_LIST_LIMITS.session.max });
171
+ for (const row of page.rows) {
172
+ const token = row.kind === "row" ? row.checkpoint.token : row.token;
173
+ const cp = row.kind === "row" ? row.checkpoint : await this.inner.get(row.token);
174
+ if (cp === null || cp.status !== "pending" || (scope !== undefined && cp.scope !== scope))
175
+ continue;
176
+ out.push({ token, cp });
177
+ }
178
+ after = page.next;
179
+ } while (after !== undefined);
180
+ return out;
181
+ }
163
182
  async listPendingSessionIds() {
164
183
  return [...new Set((await this.pendings()).map(({ cp }) => cp.sessionId))];
165
184
  }
@@ -200,7 +219,7 @@ export class LocalCheckpointStore {
200
219
  return rows.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
201
220
  }
202
221
  async findPendingTokenBySession(sessionId, scope, binding) {
203
- return this.pickPendingRow(await this.pendings(scope || undefined), sessionId, binding)?.token ?? null;
222
+ return this.pickPendingRow(await this.sessionPendings(sessionId, scope || undefined), sessionId, binding)?.token ?? null;
204
223
  }
205
224
  pickPendingRow(rows, sessionId, binding) {
206
225
  let best;
@@ -292,7 +311,7 @@ export class LocalCheckpointStore {
292
311
  return (await this.inner.get(token))?.scope;
293
312
  }
294
313
  async peekPendingScope(sessionId, binding) {
295
- return this.pickPendingRow(await this.pendings(), sessionId, binding)?.cp.scope;
314
+ return this.pickPendingRow(await this.sessionPendings(sessionId), sessionId, binding)?.cp.scope;
296
315
  }
297
316
  async reapExpired(cutoff) {
298
317
  let n = 0;
@@ -415,8 +434,8 @@ export class LocalCheckpointStore {
415
434
  return 0;
416
435
  }
417
436
  let n = 0;
418
- for (const { token, cp } of await this.pendings()) {
419
- if (cp.sessionId === sessionId && (await this.inner.expire(token, cp.scope)))
437
+ for (const { token, cp } of await this.sessionPendings(sessionId)) {
438
+ if (await this.inner.expire(token, cp.scope))
420
439
  n++;
421
440
  }
422
441
  const p = join(this.ctxDir, ctxFileOf(sessionId));
@@ -2,13 +2,13 @@ import { FileSessionRepo, type AcquiredSession, type PlacedSessionRow, type Sess
2
2
  import { type PlacementAcquireOpts } from "./session-placement.js";
3
3
  import { z } from "zod";
4
4
  import { StoreBootRefusalError, type SessionRunClaimSource, type SessionRunRollupSource, type SessionSummary } from "./store-contracts.js";
5
- import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
5
+ import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowAnswer, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
6
6
  import { type LocalArchiveKeptStamped, type LocalArchiveUnreadable } from "./one-time-migrations.js";
7
7
  import { type Logger } from "../observability/logger.js";
8
8
  import { type SessionFace } from "../session-incarnation.js";
9
9
  import { type ExecutionLane } from "../execution-lane-caps.js";
10
10
  import { type ExecutionPlacementSeat, type LocalPlacementBackfillSeat } from "../execution-placement.js";
11
- import { type ForkTarget, type ForkToOutcome } from "./session-fork.js";
11
+ import { type ForkPlacement, type ForkTarget, type ForkToOutcome } from "./session-fork.js";
12
12
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
13
13
  import type { SessionWorkspace } from "../session-workspace.js";
14
14
  import { type StagedCommitOptions, type StagingHandle, type SyncRelation } from "../session-sync-kernel.js";
@@ -348,8 +348,9 @@ export declare class LocalSessionStore implements SessionStore {
348
348
  */
349
349
  headOf(sessionId: string): Promise<SessionHead | undefined>;
350
350
  /**
351
- * 引擎仓存档的头(core `SessionRepo.headOf`:日志首记录与尾,不重放)—— 本店读存档头的**唯一**一处。存档头在本店只有两种读法:叶子(读内容的人要的那一格)
352
- * 与行代号(换装 / 一次性迁移的 `expect.row`、迁移判「有没有、读不读得出」);属主 / 建档时刻 / 谱系从不从这里读(规则一,行事实只住边档)。
351
+ * 引擎仓存档的头(core `SessionRepo.headOf`:日志首记录与尾,不重放)—— 本店读存档头的**唯一**一处。存档头在本店只有三种读法:叶子(读内容的人要的那一格)、
352
+ * 行代号(换装 / 一次性迁移的 `expect.row`、迁移判「有没有、读不读得出」)与带戳判({@link assertArchiveAnonymous};读的是存档的**形** —— 头上有没有属主戳 —— 不是属主事实);
353
+ * 属主 / 建档时刻 / 谱系作为行事实从不从这里读(规则一,行事实只住边档)。
353
354
  */
354
355
  private archiveHeadOf;
355
356
  /**
@@ -490,14 +491,41 @@ export declare class LocalSessionStore implements SessionStore {
490
491
  * 无主」)一个字节不动,没被记下的也不在这里替它记成无主:打开不是一个属主事实(7.119.0 前这一腿把「不知道」记成进程内存里的 null,重启即失)。 */
491
492
  private openForItselfHeld;
492
493
  /**
493
- * 带 placement 的按 id 首建(锁内一形,调用方持这只会话的写锁、已判过存档不在且不是 fork 创建的格)。写序 = **判别位先落**:先建存档(引擎文件 repo 把元组写进行头、
494
- * 首写不可变),再写会话元数据那一格(化身新铸;属主 = `placement.owner` 原样 —— core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不推断;
495
- * 唯一读法 `placedSessionOwnerOf`,在建存档之前判,坏值 ⇒ 响亮拒、一个字节不写;SQL 孪生同调)。崩在两笔之间 = 存档在而格缺 = 一只 placed 行、属主从没被记下
496
- * (读作无主;本店没有回填臂 —— M23 的扫描与回写是 SQL 语句,只在两只 SQL 店跑 —— 这一行在具名树的停驻挂点被否决、响亮,匿名树 `null === null` 照放;SQL 孪生的同形由 M23 在窗内回填)。
497
- * 格已在而存档不在(登记先落了格、存档还没建 —— 一只普通会话的行)⇒ `placement_refused`(SQL 孪生:登记过的行在,带 placement 的建读回缺席元组,同一判)。
498
- * 建后读回准入(镜像方向):别的进程(同一数据根上的 run-local 腿)在这一刻抢先建了同 id 的存档 ⇒ repo 返回的是那一只 ⇒ 元组缺席 / 异组 ⇒ 响亮拒。
494
+ * 带 placement 的按 id 首建(锁内一形,调用方持这只会话的写锁、已判过存档不在且不是 fork 创建的格)。写序 = **格先、存档后**(与登记先记属主再建存档、fork 先写目标格再拷存档、
495
+ * 导入先落格再导条目同一序;普通 acquire 的两条首建 —— 无 id 新建、按 id 缺档新建 —— 仍是存档先、格后:那两处写的属主是「无主」,崩在两笔之间留下的存档补铸后同样读作无主,
496
+ * 与「登记为无主」只在 M10 认领窗内有别,不在本条):
497
+ * 1. 会话元数据那一格:化身新铸;属主 = `placement.owner` 原样(core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不推断;唯一读法 `placedSessionOwnerOf`,
498
+ * 坏值 ⇒ 响亮拒、一个字节不写;SQL 孪生把它与元组写进同一条 INSERT);建档时刻同一笔写(铸行点律);
499
+ * 2. 存档:元组 = `placement` 去掉属主(`SessionPlacementRecord` 没有属主 —— core 自己的店同样只把元组交给 repo),不交属主(引擎仓是匿名内容仓)。
500
+ * 崩在两笔之间 ⇒ 格在而存档不在 = 一次没建完的建行(与登记 / fork / 导入崩溃留下的同一族;不是 placed 行:不上列表、不进分区枚举、`requireExisting` ⇒ not_found、
501
+ * 同 id 再带 placement 建 ⇒ 下面那一判 `placement_refused`)—— 没有任何一刻盘上是「placed 存档在而格缺」(属主从没被记下)。保证到进程崩溃为止(那一格改名落定在建存档之前;
502
+ * 不另加 fsync 屏障,与另外三个建行点同)。
503
+ * 「placed 存档在而格缺」此后只剩三个来源,都照旧读作无主(补铸的格不写属主:属主没有事实可写),在具名树的停驻挂点被否决、响亮,匿名树 `null === null` 照放 ——
504
+ * ① 7.144.x 及更早崩在两笔之间留下的存量行(当年存档先建;不回填:行头属主格是空的,元组里那一份属主是本店当年漏进去的未声明成员,不作来源);
505
+ * ② `release` 删了格、没删存档就崩(判别位最后删;它的 agent 行已先删,没有复活腿会碰它,重发 release / 分区收割收完);③ 同一数据根上 run-local 腿经 core 店写下的 placed 存档
506
+ * (core 形:属主在行头,本店不读行头属主)。SQL 孪生的存量无主 placed 行由 M23 在窗内回填。
507
+ * 格已在而存档不在(登记先落了格、存档还没建 —— 一只普通会话的行;或本条崩在两笔之间留下的格)⇒ `placement_refused`(SQL 孪生:登记过的行在,带 placement 的建读回缺席元组,同一判)。
508
+ * 建后读回准入(镜像方向):别的写者在这一刻抢先建了同 id 的存档 ⇒ repo 返回的是那一只 ⇒ 元组缺席 / 异组 ⇒ 响亮拒。
509
+ * 第 2 步失败时那一格撤不撤,按「这只 id 上的存档是不是本次的」判,先认确证、说不准才问盘:
510
+ * · 确证不是本次的 ⇒ {@link withdrawCreatedCell} 撤(留着它 = 一只属主落在别人存档上的格):读回准入拒(读到的就是 create 交回那一只存档的元组)·
511
+ * core 的 create 抛 `conflict`(它判的「别的写者在存在判与写之间抢先落盘」);确证不被之后的读推翻;
512
+ * · 说不准(create 抛别的故障 —— 可能在行头已落盘之后,core 的 create 写完行头还要开追加日志;读元数据抛)⇒ 问盘({@link holdsThisPlacement}):本次元组下的 placed 存档
513
+ * ⇒ 那一格就是它的属主事实,留着(撤了正是「placed 存档在而格缺」);不在 / 普通 / 别的元组 ⇒ 撤;读不出 ⇒ 留着(残格惰性;撤了可能丢属主 —— 别的写者同一刻抢建同 id
514
+ * 又恰好不是 `conflict` 的那一形;数据根锁之下没有第二个进程,进程内同 id 的写者都排在这只会话的写锁上,这一形只有注入造得出,如实登记不另设臂)。
515
+ * 确证两臂同理是防线:这一节在写锁里、打开刚答「不在」,正常路上 create 交回的就是本次建的那一只。
516
+ * 撤完(或留下)之后上抛第 2 步的那一只故障。
499
517
  */
500
518
  private createPlacedHeld;
519
+ /**
520
+ * 带 placement 的建行第 2 步说不准地失败之后,盘上这只 id 是不是本次元组下的 placed 存档 —— 判据就是读回准入那一只(`admitSessionPlacement`:缺席 / 普通 / 异组 ⇒ 不是;
521
+ * 它是纯比较,只抛 `placement_refused`),读的是 {@link placementProbe}(行头上的元组,唯一一读)。读不出(`corrupt_session` / `storage`)⇒ 答「是」(见调用方)。
522
+ */
523
+ private holdsThisPlacement;
524
+ /**
525
+ * 格先、存档后的建行点(fork 的目标 · 带 placement 的建行)建存档那一步失败时,撤回第一步刚写的那一格(调用方持这只会话的写锁):撤得回 ⇒ 返回,调用方原样上抛那只故障;
526
+ * 撤不回 ⇒ 两只故障同句点名(句子由调用方给,带出路),原故障挂在 `cause` —— 留下的格就是崩在两步之间的那一形。
527
+ */
528
+ private withdrawCreatedCell;
501
529
  /**
502
530
  * Idempotently record a session's owner — never overwrites a RECORDED owner (incl. a recorded `null`, "registered ownerless";
503
531
  * mirrors TiDBSessionStore.register's first-write rule). TiDB parity: register INSERTs a session_meta row so the session EXISTS
@@ -556,6 +584,17 @@ export declare class LocalSessionStore implements SessionStore {
556
584
  limit: number;
557
585
  q?: string;
558
586
  }): Promise<SessionSummary[]>;
587
+ /** {@link listSessions} 带每行的住客(属主 + 化身那一格,与行同一刻读)—— `OwnerAwareSessionStore.listSessionRows`;化身不进行本身。 */
588
+ listSessionRows(opts: {
589
+ owner?: string;
590
+ includeUnowned?: boolean;
591
+ cursor?: {
592
+ lastActivityAt: string;
593
+ sessionId: string;
594
+ };
595
+ limit: number;
596
+ q?: string;
597
+ }): Promise<Array<SessionRowAnswer<SessionSummary>>>;
559
598
  /** 列表行要读的边档一次装齐(lastActivity / 标题 / 置顶 / 会话元数据);各自记忆化,只有第一次真读盘。 */
560
599
  private loadRowSidecars;
561
600
  /** 列表行上的属主:那一格的属主(没有格 / 属主从没被记下 ⇒ null —— 与 `headOf(id)?.owner` 同答;列表不补铸缺的格,`headOf` 在第一次读时补)。 */
@@ -583,8 +622,11 @@ export declare class LocalSessionStore implements SessionStore {
583
622
  * 修前把参数原样写进新格:`fork(owned, null)` 铸出一份匿名的有主历史。
584
623
  * 拷**已提交**的叶(契约):在飞的 assistant 轮不进孩子 —— 分叉点只有引擎那一只判官(core 7.55.0 起包根导出的 `settledForkOptions`),本店把唤醒出来的源会话交给它、
585
624
  * 把它的答案原样交给引擎自己的 `repo.fork`(与 core 内存店 `TtlSessionStore.fork` 同一条调用链);两只 SQL 孪生同读它(`session-fork.ts`)。钉:
586
- * core 店契约盒的 `[fork]` 格(`test/session-store-contract-kit.test.ts`)。 */
587
- fork(sourceId: string, owner?: string | null): Promise<string | null>;
625
+ * core 店契约盒的 `[fork]` 格(`test/session-store-contract-kit.test.ts`)。
626
+ * `opts.placement`(后台 fork 子代的转录)= fork 进委派分区:见 {@link forkToHeld} 的建行点。 */
627
+ fork(sourceId: string, owner?: string | null, opts?: {
628
+ placement?: ForkPlacement;
629
+ }): Promise<string | null>;
588
630
  /**
589
631
  * **fork 续写的店契约**(local 一形;规则与结果闭集在 `session-fork.ts`,SQL 孪生同义):
590
632
  * · **两把写锁**:按 id 字典序依次取源与目标的写锁(锁不可重入;同 id 只取一把)—— 同 id 的每个写者(删除 / 登记 / 导入 / 按 id 首建 / 别的 fork)都在目标那把锁里,
@@ -594,11 +636,20 @@ export declare class LocalSessionStore implements SessionStore {
594
636
  * (不交属主 —— 引擎仓匿名;子存档的属主格抄自源存档,源匿名 ⇒ 子匿名)。
595
637
  * 崩在两步之间 = fork 创建的格而存档不在 = `forking`:普通取用一律 {@link SessionForkInProgressError};同键重试在两把锁内补拷存档、答 `replayed`。
596
638
  * 拷贝抛错(不是崩溃)⇒ 撤掉刚写的目标格再上抛(撤不掉 ⇒ 留下的就是 `forking`,与崩在中间同形);
597
- * · **撞已在** ⇒ 按目标格的属主与 fork 键分类({@link classifyForkTarget})。
639
+ * · **fork 进委派分区**(`target.placement`,只在声明了分区的店落;没声明 ⇒ 忽略、照常 fork):元组随引擎 `repo.fork` 落进子存档行头 —— 与带 placement 的建行
640
+ * ({@link createPlacedHeld})同一处、同一形(存档行头,first-write 不可变,`placedAt` 由 repo 铸);行的属主仍是上面那一读(元组不带属主)。建后读回准入同
641
+ * `createPlacedHeld`(镜像方向:repo 交回的行没带这一组元组 ⇒ `placement_refused`):拒了就删掉刚拷的存档、再撤目标格(**存档先删、格后撤** —— 崩在中间留下的是
642
+ * 「格在而存档不在」的惰性残格;反过来会留下一份没有属主格的有主历史拷贝),删不掉 ⇒ 两只故障同句点名、出路随句;
643
+ * · **撞已在** ⇒ 按目标格的属主与 fork 键分类({@link classifyForkTarget});placed 目标永不带键(型面互斥)⇒ 永不 `replayed`。
598
644
  */
599
645
  forkTo(sourceId: string, owner: string | null, target: ForkTarget): Promise<ForkToOutcome | null>;
600
646
  /** {@link forkTo} 的锁内一形(调用方持源与目标两把写锁)。 */
601
647
  private forkToHeld;
648
+ /**
649
+ * fork 进委派分区的建后读回准入(锁内,调用方持目标的写锁、刚拷完存档):判据 = 引擎那一只 `admitSessionPlacement`(镜像方向,读 repo 交回的那一份行头元组,零额外读),
650
+ * 与 {@link createPlacedHeld} 的读回同一判。拒 ⇒ 存档先删、目标格后撤(见 {@link forkToHeld} 注),再原样上抛那只 `placement_refused`;删不掉 ⇒ 同码、两只故障同句点名。
651
+ */
652
+ private admitForkedPlacement;
602
653
  /** 2c session-sync — EXPORT a session's full durable entry log (core `SessionRepo.exportEntries`; the
603
654
  * in-memory/file repo holds the whole log so there is no floor to bypass). null if the source is unknown / the
604
655
  * underlying repo lacks the optional seam. The owner is NOT exported — import re-stamps it. */
@@ -648,7 +699,7 @@ export declare class LocalSessionStore implements SessionStore {
648
699
  * ③ 内容,按写口的动词(唯一读法在下面这一只 switch;这里不读那一格、不判属主,只换**内容**;交给引擎仓的属主恒缺席 —— 规则一):
649
700
  * · `create` / `judged`(导入):原地换锁里读到的那一份存档的日志(没有存档 ⇒ 导入新建一只,匿名),以锁里读到的存档头为 `expect.row`(**行代号**:
650
701
  * 读与写之间存档换了一代 ⇒ 引擎仓拒 `conflict`;它判不出同一代里的追加)。匿名导入只换匿名存档的日志 —— 仍带属主戳的存档(更早的版本写下、一次性迁移 M33
651
- * 改写不了的,或跳版升上来的)到不了这里:暂存提交在一切「谁赢」的判之前就以具名拒答了它({@link assertArchiveAnonymousHeld},唯一的一读);`create` 落在没有存档的名上,`replace` 先清存档。
702
+ * 改写不了的,或跳版升上来的)到不了这里:暂存提交在一切「谁赢」的判之前就以具名拒答了它({@link assertArchiveAnonymous});`create` 落在没有存档的名上,`replace` 先清存档。
652
703
  * 读存档、校验、存档判与写在引擎仓里是一步,抛在哪一处内容都一个字节没动 ⇒ 撤回 ②({@link withdrawImportCell}:行原样 —— 净零写,SQL 孪生的事务回滚同义)再原样上抛;
653
704
  * · `replace`(接口句「先清既有对话日志,再导入」):先清掉内容存档(不带 `expect` —— 带了就得先读它的头,读不出的存档就清不掉),再导入一只新存档。
654
705
  * 清不可撤:清过之后导入再抛(I/O 故障),内容就没了,行留 ② 落下的 mirror(只读方向,下次推送补齐)—— 已发版本的 replace 同形(删后导入)。
@@ -695,16 +746,24 @@ export declare class LocalSessionStore implements SessionStore {
695
746
  /** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
696
747
  forgetStaging(stagingId: string): void;
697
748
  /**
698
- * 带戳存档的唯一一判(调用方持这只会话的写锁;只读、零写):这份存档的头仍记着属主戳(更早的版本导入 / 换装写下的,一次性迁移 M33 改写不了的,或跳版升上来的)⇒
699
- * {@link StampedSessionArchiveError}。引擎导入律不让匿名导入替换带戳存档,本店不重写、不绕 —— 只让拒带对的出路(fork 的源另有一读,同一只具名拒,见 {@link forkTo})。
700
- * 这是「这份存档的日志根本换不了」的判,所以排在一切「谁赢」的判之前({@link commitStagedImport}):换不了的存档上谈不上同步冲突或载荷分叉,
701
- * 那两条拒的出路(带同意重来)都是死路 —— 唯一的出路在这一句里。
702
- */
703
- private assertArchiveAnonymousHeld;
749
+ * **带戳存档拒绝判的唯一一读**(fork 与改日志的推送问「这份存档的头仍记着属主戳吗」只此一处,也是 {@link StampedSessionArchiveError} 在本店的唯一铸点;只读、零写;
750
+ * 一次性迁移 M33 为改写存档而读头 / 整行是窗内的另一件事,窗关即删):这份存档的头仍记着属主戳
751
+ * (更早的版本导入 / 换装写下的,一次性迁移 M33 改写不了的,或跳版升上来的)⇒ 抛那只具名拒。引擎的 fork 会把戳抄给子存档、引擎导入律不让匿名导入替换带戳存档,
752
+ * 本店不重写、不绕 —— 只让拒带对的出路(删掉会话后重推)。读的是存档的**形**(「这不是本店的存档形」),不是属主事实(属主读行那一格);读口是 {@link archiveHeadOf}
753
+ * (core `SessionRepo.headOf` 契约:与 `open()` 读到的头同一读)。没有存档的 id ⇒ 放行(没有存档就没有戳)。
754
+ * 三处问它,问的时刻不同、答案同一个:
755
+ * · {@link forkTo} 的源 —— 在写目标格之前(持源与目标两把写锁);
756
+ * · {@link commitStagedImport} —— 这一提交要换日志,排在一切「谁赢」的判之前(持这只会话的写锁):换不了日志的存档上谈不上同步冲突或载荷分叉,
757
+ * 那两条拒的出路(带同意重来)都是死路,唯一的出路在这一句里;
758
+ * · 会话同步导入的**第一阶段**(`http/routes/session-sync.ts`,经 `OwnerAwareSessionStore.assertArchiveAnonymous` 这一缝,不持锁):按 id 分类出 fork / stale 又没带同意时,
759
+ * 通用冲突指的出路同样是「带同意重来」⇒ 先问这一口,不等第二轮。不持锁的一问是早拒(此刻的答案),权威的一问是提交里持锁的那一问 —— 同一个事实的两次读。
760
+ * 只有 local 店有带戳存档这一形;SQL 孪生没有 ⇒ 不交这一口(调用方按「没有带戳存档」处置)。
761
+ */
762
+ assertArchiveAnonymous(sessionId: string): Promise<void>;
704
763
  /**
705
764
  * {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):属主 CAS(此刻的属主格 = 这次导入的门判过的那一格,
706
765
  * 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 分区判 → 提交时重新分类(目的端此刻的日志 vs staged)→
707
- * 这一提交要换日志(不是「id 与内容都相同」那一支)⇒ 带戳存档判({@link assertArchiveAnonymousHeld};只有 local 店有带戳存档,SQL 孪生没有这一步)→
766
+ * 这一提交要换日志(不是「id 与内容都相同」那一支)⇒ 带戳存档判({@link assertArchiveAnonymous};只有 local 店有带戳存档,SQL 孪生没有这一步)→
708
767
  * 拒未经同意的 fork / stale / 载荷分叉(带戳判在它们之前:带戳存档上日志换不了,同步冲突与载荷分叉的出路 —— 带同意重来 —— 无从谈起)→ 本处写者拒 → 换装(导入写序 ①②③,落点 `judged` = 上面这一判;化身与 fork 键按换装律沿用)或 identical 且内容相同时日志不动、
709
768
  * 行照样落(② 那一格:重戳属主 + mirror)→ 两支同记新近(推送即活动)→ **在同一次持锁里**
710
769
  * 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
@@ -1022,12 +1022,51 @@ export class LocalSessionStore {
1022
1022
  if (this.metaCellOf(sessionId) !== undefined) {
1023
1023
  throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
1024
1024
  }
1025
- const owner = placedSessionOwnerOf(sessionId, placement);
1026
- const session = await this.repo.create({ id: sessionId, placement });
1027
- await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
1028
- admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts, placementCapabilityOf(this));
1025
+ const { owner: treeOwner, ...tuple } = placement;
1026
+ const owner = placedSessionOwnerOf(sessionId, { owner: treeOwner });
1027
+ const created = await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
1028
+ if (!created)
1029
+ throw new SessionError("storage", `LocalSessionStore: session ${sessionId} gained a session-meta cell under its own write lock — refusing to guess`);
1030
+ const stuck = (cause, drop) => `LocalSessionStore: creating the subagent transcript ${sessionId} failed (${cause}) and its session-meta cell could not be withdrawn (${drop}) — the cell stays on disk and a placed create on this id is refused; delete the session to clear it`;
1031
+ const unsure = async (err) => {
1032
+ const foreign = err instanceof SessionError && err.code === "conflict";
1033
+ if (foreign || !(await this.holdsThisPlacement(sessionId, opts)))
1034
+ await this.withdrawCreatedCell(sessionId, err, stuck);
1035
+ throw err;
1036
+ };
1037
+ const session = await this.repo.create({ id: sessionId, placement: tuple }).catch(unsure);
1038
+ const persisted = await session.getMetadata().then((meta) => meta.placement, unsure);
1039
+ try {
1040
+ admitSessionPlacement(sessionId, persisted, opts, placementCapabilityOf(this));
1041
+ }
1042
+ catch (err) {
1043
+ await this.withdrawCreatedCell(sessionId, err, stuck);
1044
+ throw err;
1045
+ }
1029
1046
  return this.handOut(sessionId, session);
1030
1047
  }
1048
+ async holdsThisPlacement(sessionId, opts) {
1049
+ const probe = await this.placementProbe(sessionId);
1050
+ if (probe.state === "unusable")
1051
+ return true;
1052
+ try {
1053
+ admitSessionPlacement(sessionId, probe.state === "placed" ? probe.record : undefined, opts, placementCapabilityOf(this));
1054
+ return true;
1055
+ }
1056
+ catch (e) {
1057
+ if (e instanceof SessionError && e.code === "placement_refused")
1058
+ return false;
1059
+ throw e;
1060
+ }
1061
+ }
1062
+ async withdrawCreatedCell(sessionId, cause, stuck) {
1063
+ try {
1064
+ await this.dropMeta(sessionId);
1065
+ }
1066
+ catch (dropErr) {
1067
+ throw new SessionError("storage", stuck(cause instanceof Error ? cause.message : String(cause), dropErr instanceof Error ? dropErr.message : String(dropErr)), cause instanceof Error ? cause : undefined);
1068
+ }
1069
+ }
1031
1070
  async register(sessionId, owner) {
1032
1071
  await this.withSessionWriteLock(sessionId, async () => {
1033
1072
  await this.meta.ensureLoaded();
@@ -1067,6 +1106,9 @@ export class LocalSessionStore {
1067
1106
  });
1068
1107
  }
1069
1108
  async listSessions(opts) {
1109
+ return (await this.listSessionRows(opts)).map((a) => a.row);
1110
+ }
1111
+ async listSessionRows(opts) {
1070
1112
  await this.loadRowSidecars();
1071
1113
  const rows = [];
1072
1114
  for (const header of await this.rowHeaders())
@@ -1077,16 +1119,18 @@ export class LocalSessionStore {
1077
1119
  const owner = this.ownerCellOf(row.sessionId);
1078
1120
  if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
1079
1121
  continue;
1080
- all.push(await this.summaryOf(row, rollups));
1122
+ const meta = this.metaCellOf(row.sessionId);
1123
+ const incarnation = meta === undefined ? undefined : incarnationFromCell(row.sessionId, meta.incarnation, "absent");
1124
+ all.push({ row: await this.summaryOf(row, rollups), occupant: { owner, incarnation } });
1081
1125
  }
1082
1126
  const qlc = opts.q?.toLowerCase();
1083
- const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
1084
- filtered.sort((a, b) => a.lastActivityAt !== b.lastActivityAt
1127
+ const filtered = qlc ? all.filter(({ row: r }) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
1128
+ filtered.sort(({ row: a }, { row: b }) => a.lastActivityAt !== b.lastActivityAt
1085
1129
  ? (a.lastActivityAt < b.lastActivityAt ? 1 : -1)
1086
1130
  : (a.sessionId < b.sessionId ? 1 : -1));
1087
1131
  const c = opts.cursor;
1088
1132
  const after = c
1089
- ? filtered.filter((r) => r.lastActivityAt < c.lastActivityAt || (r.lastActivityAt === c.lastActivityAt && r.sessionId < c.sessionId))
1133
+ ? filtered.filter(({ row: r }) => r.lastActivityAt < c.lastActivityAt || (r.lastActivityAt === c.lastActivityAt && r.sessionId < c.sessionId))
1090
1134
  : filtered;
1091
1135
  return after.slice(0, opts.limit);
1092
1136
  }
@@ -1150,8 +1194,8 @@ export class LocalSessionStore {
1150
1194
  return null;
1151
1195
  }
1152
1196
  }
1153
- async fork(sourceId, owner) {
1154
- return forkByFreshId((s, o, t) => this.forkTo(s, o, t), sourceId, owner);
1197
+ async fork(sourceId, owner, opts) {
1198
+ return forkByFreshId((s, o, t) => this.forkTo(s, o, t), sourceId, owner, opts);
1155
1199
  }
1156
1200
  async forkTo(sourceId, owner, target) {
1157
1201
  await this.meta.ensureLoaded();
@@ -1171,31 +1215,29 @@ export class LocalSessionStore {
1171
1215
  return null;
1172
1216
  throw e;
1173
1217
  }
1174
- if (((await source.getMetadata()).owner ?? null) !== null)
1175
- throw new StampedSessionArchiveError(sourceId);
1218
+ await this.assertArchiveAnonymous(sourceId);
1176
1219
  const forkOptions = await settledForkOptions(source);
1177
1220
  const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
1178
1221
  const existing = this.metaCellOf(target.id);
1179
1222
  const targetArchived = await this.existsInRepo(target.id);
1180
- const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
1223
+ const placement = this.placements !== undefined ? target.placement : undefined;
1224
+ const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id, ...(placement !== undefined ? { placement } : {}) });
1181
1225
  if (existing === undefined && !targetArchived) {
1182
1226
  const created = await this.writeMeta(target.id, (before) => before !== undefined
1183
1227
  ? undefined
1184
1228
  : { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}), createdAt: rowBornNow(), forkedFrom: sourceId });
1185
1229
  if (!created)
1186
1230
  throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
1231
+ let forked;
1187
1232
  try {
1188
- await copy();
1233
+ forked = await copy();
1189
1234
  }
1190
1235
  catch (err) {
1191
- try {
1192
- await this.dropMeta(target.id);
1193
- }
1194
- catch (dropErr) {
1195
- throw new SessionError("storage", `LocalSessionStore: fork into ${target.id} failed while copying (${err instanceof Error ? err.message : String(err)}) and its target cell could not be withdrawn (${dropErr instanceof Error ? dropErr.message : String(dropErr)}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`, err instanceof Error ? err : undefined);
1196
- }
1236
+ await this.withdrawCreatedCell(target.id, err, (cause, drop) => `LocalSessionStore: fork into ${target.id} failed while copying (${cause}) and its target cell could not be withdrawn (${drop}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`);
1197
1237
  throw err;
1198
1238
  }
1239
+ if (placement !== undefined)
1240
+ await this.admitForkedPlacement(target.id, forked, placement);
1199
1241
  return { outcome: "created", id: target.id };
1200
1242
  }
1201
1243
  const out = classifyForkTarget(target, { owner: existing?.owner, forkKey: existing?.forkKey }, forkOwner);
@@ -1203,6 +1245,21 @@ export class LocalSessionStore {
1203
1245
  await copy();
1204
1246
  return out;
1205
1247
  }
1248
+ async admitForkedPlacement(sessionId, forked, placement) {
1249
+ try {
1250
+ admitSessionPlacement(sessionId, (await forked.getMetadata()).placement, { placement }, placementCapabilityOf(this));
1251
+ }
1252
+ catch (refusal) {
1253
+ try {
1254
+ await this.repo.delete({ id: sessionId, createdAt: "" });
1255
+ }
1256
+ catch (left) {
1257
+ throw new SessionError("placement_refused", `${refusal instanceof Error ? refusal.message : String(refusal)} — and the copy ${sessionId} this fork created could not be removed (${left instanceof Error ? left.message : String(left)}); delete the session by id`, refusal instanceof Error ? refusal : undefined);
1258
+ }
1259
+ await this.withdrawCreatedCell(sessionId, refusal, (cause, drop) => `LocalSessionStore: a placed fork into ${sessionId} was refused (${cause}); its copy was removed but its session-meta cell could not be withdrawn (${drop}) — the cell stays on disk, inert; delete the session to clear it`);
1260
+ throw refusal;
1261
+ }
1262
+ }
1206
1263
  async exportEntries(sessionId) {
1207
1264
  if (!this.repo.exportEntries)
1208
1265
  return null;
@@ -1338,7 +1395,7 @@ export class LocalSessionStore {
1338
1395
  forgetStaging(stagingId) {
1339
1396
  this.stagings.delete(stagingId);
1340
1397
  }
1341
- async assertArchiveAnonymousHeld(sessionId) {
1398
+ async assertArchiveAnonymous(sessionId) {
1342
1399
  if (((await this.archiveHeadOf(sessionId))?.owner ?? null) !== null)
1343
1400
  throw new StampedSessionArchiveError(sessionId);
1344
1401
  }
@@ -1354,7 +1411,7 @@ export class LocalSessionStore {
1354
1411
  const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
1355
1412
  const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
1356
1413
  if (!contentEqual)
1357
- await this.assertArchiveAnonymousHeld(sessionId);
1414
+ await this.assertArchiveAnonymous(sessionId);
1358
1415
  if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
1359
1416
  throw new SyncConflictError(sessionId, rel);
1360
1417
  }