@sema-agent/server 7.142.0 → 7.143.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 (258) hide show
  1. package/MIGRATION.md +442 -0
  2. package/USAGE.md +41 -24
  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 +124 -25
  9. package/dist/approval-card.js +103 -23
  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/governance-seams.d.ts +26 -41
  17. package/dist/boot/governance-seams.js +61 -28
  18. package/dist/boot/late-binds.d.ts +7 -4
  19. package/dist/boot/late-binds.js +3 -1
  20. package/dist/boot/leader.js +2 -4
  21. package/dist/boot/memory-consolidation.d.ts +0 -8
  22. package/dist/boot/memory-consolidation.js +3 -3
  23. package/dist/boot/reapers.js +5 -4
  24. package/dist/boot/resolve-spec.d.ts +1 -1
  25. package/dist/boot/resolve-spec.js +8 -8
  26. package/dist/boot/runner-deps.d.ts +10 -7
  27. package/dist/boot/runner-deps.js +5 -17
  28. package/dist/boot/shutdown.d.ts +9 -0
  29. package/dist/boot/shutdown.js +16 -2
  30. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  31. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  32. package/dist/boot/stage-03-store-backend.js +1 -2
  33. package/dist/boot/stage-04-stores.d.ts +5 -2
  34. package/dist/boot/stage-04-stores.js +3 -5
  35. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  36. package/dist/boot/stage-06-runners.d.ts +5 -2
  37. package/dist/boot/stage-06-runners.js +1 -3
  38. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-08-reapers.d.ts +5 -2
  41. package/dist/boot/stage-09-leader.d.ts +5 -2
  42. package/dist/boot/stage-10-http-server.d.ts +5 -2
  43. package/dist/boot/stage-10-http-server.js +9 -1
  44. package/dist/boot/stores.d.ts +5 -2
  45. package/dist/boot/stores.js +7 -7
  46. package/dist/boot/web-admin.d.ts +5 -1
  47. package/dist/boot/web-admin.js +31 -19
  48. package/dist/capabilities/hands-lane.d.ts +9 -39
  49. package/dist/capabilities/hands-lane.js +8 -13
  50. package/dist/capabilities/mcp-probe.d.ts +3 -2
  51. package/dist/capabilities/scenarios.d.ts +11 -1
  52. package/dist/capabilities/scenarios.js +1 -0
  53. package/dist/capabilities/skills.d.ts +1 -17
  54. package/dist/capabilities/skills.js +0 -8
  55. package/dist/capability-facts.d.ts +8 -8
  56. package/dist/capability-facts.js +4 -4
  57. package/dist/card-void.d.ts +6 -9
  58. package/dist/config-catalog.js +1 -1
  59. package/dist/config-dir-trust.js +58 -29
  60. package/dist/config-invariants.d.ts +7 -1
  61. package/dist/config-invariants.js +7 -0
  62. package/dist/config-provider.d.ts +10 -0
  63. package/dist/config-provider.js +15 -4
  64. package/dist/config-root-migration.d.ts +13 -0
  65. package/dist/config-root-migration.js +99 -0
  66. package/dist/config-types.d.ts +5 -5
  67. package/dist/config.d.ts +4 -3
  68. package/dist/config.js +1 -1
  69. package/dist/delegated-session-owner.d.ts +5 -3
  70. package/dist/delegated-session-owner.js +1 -1
  71. package/dist/device-ws-protocol.d.ts +1 -0
  72. package/dist/device-ws-protocol.js +2 -1
  73. package/dist/env-model-table-gate.d.ts +53 -0
  74. package/dist/env-model-table-gate.js +53 -0
  75. package/dist/fleet/fleet-bus.d.ts +28 -52
  76. package/dist/fleet/fleet-bus.js +35 -100
  77. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  78. package/dist/fleet/fleet-reconciler.js +1 -1
  79. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  80. package/dist/fleet/subagent-tail-bus.js +15 -28
  81. package/dist/hooks/hook-runner.js +28 -7
  82. package/dist/host-decision.d.ts +54 -20
  83. package/dist/host-decision.js +42 -14
  84. package/dist/http/admission.d.ts +1 -1
  85. package/dist/http/admission.js +7 -3
  86. package/dist/http/approval-sweeps.js +11 -8
  87. package/dist/http/caller-trust.d.ts +1 -1
  88. package/dist/http/durable-run-leg.js +2 -2
  89. package/dist/http/engine-refusal.d.ts +16 -0
  90. package/dist/http/engine-refusal.js +17 -0
  91. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  92. package/dist/http/memory-compliance-failure-reply.js +4 -6
  93. package/dist/http/owner-access.d.ts +11 -0
  94. package/dist/http/owner-access.js +13 -0
  95. package/dist/http/resume-legs.d.ts +21 -7
  96. package/dist/http/resume-legs.js +71 -49
  97. package/dist/http/resume-restatement.d.ts +39 -0
  98. package/dist/http/resume-restatement.js +25 -0
  99. package/dist/http/route-ctx.d.ts +35 -10
  100. package/dist/http/route-ctx.js +1 -1
  101. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  102. package/dist/http/routes/approvals-assistant.js +142 -31
  103. package/dist/http/routes/capabilities.js +11 -2
  104. package/dist/http/routes/memory-policy.d.ts +2 -2
  105. package/dist/http/routes/memory-policy.js +1 -1
  106. package/dist/http/routes/notify-wake.js +23 -25
  107. package/dist/http/routes/rules.js +48 -13
  108. package/dist/http/routes/runs.js +9 -8
  109. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  110. package/dist/http/routes/session-memory-sync.js +27 -10
  111. package/dist/http/routes/session-sync.js +13 -20
  112. package/dist/http/routes/sessions.d.ts +2 -2
  113. package/dist/http/routes/sessions.js +22 -23
  114. package/dist/http/routes/tasks.js +27 -49
  115. package/dist/http/server.d.ts +13 -7
  116. package/dist/http/server.js +2 -2
  117. package/dist/http/stored-body-replay.js +15 -4
  118. package/dist/http/wire-types.d.ts +17 -7
  119. package/dist/leader/leader.js +5 -2
  120. package/dist/leader/wire.d.ts +7 -1
  121. package/dist/leader/wire.js +6 -5
  122. package/dist/leg-facts.d.ts +37 -0
  123. package/dist/leg-facts.js +15 -0
  124. package/dist/memory-bundle-engine.d.ts +1 -4
  125. package/dist/memory-bundle-engine.js +2 -2
  126. package/dist/memory-operator-faces.d.ts +30 -66
  127. package/dist/memory-operator-faces.js +17 -88
  128. package/dist/memory-scope-stores.d.ts +38 -40
  129. package/dist/memory-scope-stores.js +51 -36
  130. package/dist/memory-scope.d.ts +0 -9
  131. package/dist/memory-scope.js +0 -9
  132. package/dist/memory-sync-client.js +14 -3
  133. package/dist/memory-sync.d.ts +20 -4
  134. package/dist/memory-sync.js +23 -4
  135. package/dist/model-plane-face.d.ts +13 -7
  136. package/dist/model-plane-face.js +2 -1
  137. package/dist/observability/fail-open.d.ts +20 -4
  138. package/dist/observability/fail-open.js +17 -4
  139. package/dist/observability/run-terminal.js +4 -0
  140. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  141. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  142. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  143. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  144. package/dist/orchestration/workflow-park-decision.js +5 -3
  145. package/dist/org-delivery-gate.d.ts +11 -0
  146. package/dist/org-delivery-gate.js +19 -1
  147. package/dist/parked-decide.d.ts +13 -9
  148. package/dist/parked-decide.js +7 -7
  149. package/dist/parked-revive-surface.js +1 -1
  150. package/dist/peer-inbound-face.d.ts +127 -0
  151. package/dist/peer-inbound-face.js +158 -0
  152. package/dist/permission-rule-list.d.ts +13 -9
  153. package/dist/permission-rule-list.js +7 -3
  154. package/dist/plugins/approval-ask-store-file.js +18 -6
  155. package/dist/plugins/approval-ask-store-memory.js +8 -6
  156. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  157. package/dist/plugins/approval-ask-store-sql.js +30 -11
  158. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  159. package/dist/plugins/checkpoint-store-sql.js +9 -7
  160. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  161. package/dist/plugins/file-history-store-sql.js +24 -19
  162. package/dist/plugins/file-run-store.d.ts +29 -0
  163. package/dist/plugins/file-run-store.js +97 -8
  164. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  165. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  166. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  167. package/dist/plugins/local-checkpoint-store.js +2 -2
  168. package/dist/plugins/local-session-store.d.ts +6 -2
  169. package/dist/plugins/local-session-store.js +8 -2
  170. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  171. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  172. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  173. package/dist/plugins/mailbox-store-sql.js +76 -7
  174. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  175. package/dist/plugins/memory-sync-store-pg.js +21 -0
  176. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  177. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  178. package/dist/plugins/one-time-migrations.d.ts +32 -2
  179. package/dist/plugins/one-time-migrations.js +106 -3
  180. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  181. package/dist/plugins/org-delivery-quota-store.js +2 -12
  182. package/dist/plugins/park-commit-watch.d.ts +1 -1
  183. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  184. package/dist/plugins/permission-rule-store-file.js +15 -2
  185. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  186. package/dist/plugins/permission-rule-store-sql.js +35 -2
  187. package/dist/plugins/remote-env-device.d.ts +8 -11
  188. package/dist/plugins/remote-env-device.js +6 -3
  189. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  190. package/dist/plugins/remote-env-k8s.js +50 -17
  191. package/dist/plugins/retention-store-sql.d.ts +2 -2
  192. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  193. package/dist/plugins/sandbox-read-floor.js +2 -7
  194. package/dist/plugins/sql-errors.d.ts +10 -1
  195. package/dist/plugins/sql-errors.js +13 -0
  196. package/dist/plugins/store-backend.d.ts +23 -28
  197. package/dist/plugins/store-backend.js +9 -1
  198. package/dist/plugins/store-contracts.d.ts +29 -5
  199. package/dist/plugins/store-contracts.js +4 -1
  200. package/dist/plugins/task-attachment-store.d.ts +2 -2
  201. package/dist/plugins/task-attachment-store.js +37 -52
  202. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  203. package/dist/plugins/tool-result-store-sql.js +23 -2
  204. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  205. package/dist/plugins/wiring-section-codec.js +55 -0
  206. package/dist/plugins/workspace-fill.d.ts +1 -1
  207. package/dist/rule-entry-list.d.ts +3 -2
  208. package/dist/rule-entry-list.js +5 -1
  209. package/dist/rules-consent.d.ts +28 -5
  210. package/dist/rules-consent.js +40 -28
  211. package/dist/run-local.js +9 -5
  212. package/dist/runs.d.ts +12 -8
  213. package/dist/runs.js +6 -7
  214. package/dist/runtime-governance.d.ts +1 -1
  215. package/dist/server-secret-env.d.ts +4 -0
  216. package/dist/session-error-wire.d.ts +12 -0
  217. package/dist/session-error-wire.js +6 -0
  218. package/dist/session-incarnation.d.ts +1 -1
  219. package/dist/session-notify-door.d.ts +106 -0
  220. package/dist/session-notify-door.js +93 -0
  221. package/dist/session-sync.d.ts +9 -14
  222. package/dist/session-sync.js +5 -5
  223. package/dist/session-workspace.d.ts +3 -13
  224. package/dist/session-workspace.js +0 -9
  225. package/dist/standing-answer-grant.d.ts +5 -0
  226. package/dist/standing-answer-grant.js +10 -0
  227. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  228. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  229. package/dist/task-mcp.d.ts +1 -1
  230. package/dist/task-settings.d.ts +11 -8
  231. package/dist/task-settings.js +31 -11
  232. package/dist/tool-approval.d.ts +52 -34
  233. package/dist/tool-approval.js +66 -57
  234. package/dist/trace/core-keyset-guard.d.ts +11 -8
  235. package/dist/trace/engine-notice-wire.d.ts +1 -1
  236. package/dist/trace/engine-notice-wire.js +3 -1
  237. package/dist/trace/exit-seat.d.ts +7 -1
  238. package/dist/trace/exit-seat.js +1 -0
  239. package/dist/trace/ledger-sink.d.ts +2 -2
  240. package/dist/trace/model-segment.d.ts +55 -25
  241. package/dist/trace/model-segment.js +101 -33
  242. package/dist/trace/project.d.ts +7 -4
  243. package/dist/trace/project.js +6 -0
  244. package/dist/trace/redact.d.ts +61 -9
  245. package/dist/trace/redact.js +1490 -468
  246. package/dist/trace/self-minted-wire.d.ts +510 -38
  247. package/dist/trace/self-minted-wire.js +165 -17
  248. package/dist/trace/wire-projection-faces.d.ts +1 -1
  249. package/dist/trace/wire-projection-faces.js +1 -1
  250. package/dist/verifier-pause-card.d.ts +33 -5
  251. package/dist/verifier-pause-card.js +15 -10
  252. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  253. package/dist/web-admin-client/apply-effective.js +90 -10
  254. package/dist/workspace-space.d.ts +19 -3
  255. package/dist/workspace-space.js +8 -4
  256. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  257. package/package.json +3 -2
  258. package/wire-changes/server@7.143.0.json +1167 -0
@@ -2,7 +2,7 @@ import { join } from "node:path";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { atomicWriteFile, ensureDir } from "@sema-agent/core";
4
4
  import { mysqlDriver, pgDriver, sqlDbNowMs } from "./sql-driver.js";
5
- import { isDeadlockError } from "./sql-errors.js";
5
+ import { retryOnDeadlock } from "./sql-errors.js";
6
6
  import { alignedWindow } from "../observability/cost-quota.js";
7
7
  export const ORG_DELIVERY_SENDER_QUOTA_TABLE = "org_delivery_sender_quota";
8
8
  export const ORG_DELIVERY_RECIPIENT_QUOTA_TABLE = "org_delivery_recipient_quota";
@@ -122,7 +122,6 @@ export class FileOrgDeliveryQuotaStore extends MemoryOrgDeliveryQuotaStore {
122
122
  atomicWriteFile(this.tmpDir, this.path, `${JSON.stringify(snapshot)}\n`);
123
123
  }
124
124
  }
125
- const DEADLOCK_RETRY_LIMIT = 3;
126
125
  export class SqlOrgDeliveryQuotaStore {
127
126
  db;
128
127
  constructor(db) {
@@ -138,16 +137,7 @@ export class SqlOrgDeliveryQuotaStore {
138
137
  return this.inTx(ORG_DELIVERY_RECIPIENT_QUOTA_TABLE, policy, (conn, at) => this.takeIn(conn, fromScope, toScope, at, policy));
139
138
  }
140
139
  async inTx(table, policy, body) {
141
- for (let tries = 0;; tries++) {
142
- try {
143
- return await this.inTxOnce(table, policy, body);
144
- }
145
- catch (err) {
146
- if (tries < DEADLOCK_RETRY_LIMIT && isDeadlockError(this.db.dialect, err))
147
- continue;
148
- throw err;
149
- }
150
- }
140
+ return retryOnDeadlock(this.db.dialect, () => this.inTxOnce(table, policy, body));
151
141
  }
152
142
  async inTxOnce(table, policy, body) {
153
143
  const at = quotaWindowStart(await sqlDbNowMs(this.db, table), policy.windowMs);
@@ -28,7 +28,7 @@ import type { CheckpointStore } from "@sema-agent/core";
28
28
  /**
29
29
  * 一只**已提交**的审批停车行说它握着哪些调用(停车行自己的陈述,原样,不是推断):
30
30
  * · `sessionId` —— 行停住的会话 `cp.sessionId`(执行这次调用的会话:委派 / 后台子代的行落在**子会话**上)。core 铸行用的是 prepare-task 的
31
- * 同一个 `sessionId`,它也是同一次调用的 ask 上的 `sourceTaskId`(`askSourceIdentity`)⇒ 订阅者按**相等**配,不做别名(车 SO:
31
+ * 同一个 `sessionId`,它也是同一次调用的 ask 上的 `sourceSessionId`(`askSourceIdentity`)⇒ 订阅者按**相等**配,不做别名(车 SO:
32
32
  * 调用 id 只在 run 内唯一,宿主会话若当子代的等价别名,宿主停车行会冒充子代的提交);
33
33
  * · `toolCallIds` —— 停在门前的那只调用(`pendingAction.toolCallId`)∪ 同一条助手消息里**尚未执行**的调用
34
34
  * (`batchToolCallIds − completedCallIds`,core d.ts「#k+1..N are still pending」):那些调用随这一行一起被持久挂起,
@@ -2,7 +2,7 @@ import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApp
2
2
  import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
3
3
  import type { PermissionRuleStoreBundle } from "../rules-consent.js";
4
4
  import type { Logger } from "../observability/logger.js";
5
- import { type RuleCandidate } from "@sema-agent/core";
5
+ import { type RuleCandidate, type RuleOwner } from "@sema-agent/core";
6
6
  /**
7
7
  * `RuleApprovalRecordStore` 的 File 形。
8
8
  *
@@ -26,6 +26,13 @@ export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordSt
26
26
  get(id: string): Promise<RuleApprovalRecord | StaleRuleApprovalRecord | undefined>;
27
27
  create(record: RuleApprovalRecord): Promise<void>;
28
28
  cas(id: string, expectRev: number, next: RuleApprovalRecord): Promise<boolean>;
29
+ /**
30
+ * 一次性迁移 M36(窗口见 `plugins/one-time-migrations.ts` 登记表)在 File 店能叫出名字的桶:本店**导入记录**点名过的每一个属主(去重)。
31
+ * 引擎 File 分区的桶文件按属主名的单向摘要命名、分区后端没有列桶口,记录是本店唯一叫得出桶属主的地方(SQL 店直接列规则表;File 侧的缺口见
32
+ * `imported-layer-root-retirement.ts` 头注)。上一形的记录(Stale 信封)读不出种类 ⇒ 属主读得出就算上(多读一只桶零写,少读一只会漏)。
33
+ * 损坏闸合着 ⇒ 抛(读不全记录就叫不全桶;与本面其余口同一条 fail-closed)。
34
+ */
35
+ importRecordOwners(): Promise<RuleOwner[]>;
29
36
  /** 与 SQL 侧同名方法同义(超帽拒绝时收掉 `prepareCcImport` 已落盘的那条 pending 记录)。
30
37
  * `state === "pending"` 是硬的:已确认/已兑付的记录是一次真人同意的审计事实。 */
31
38
  discardPendingRecord(recordId: string): Promise<boolean>;
@@ -87,6 +94,8 @@ export declare class FileRuleImportTicketStore {
87
94
  * 那一条:过期只挡开始一次兑付,不挡做完一次已经开始的)。抢到之后**换一代认领**。 */
88
95
  reclaim(ticketId: string, principal: string, purpose: RuleTicketPurpose, minClaimAgeMs: number): Promise<RuleTicketReclaimResult>;
89
96
  }
97
+ /** 三面束在数据根下的目录名(唯一一处;束在第一次装配时建它 —— 没开过规则店的数据根下没有它)。 */
98
+ export declare const FILE_PERMISSION_RULE_DIR = "permission-rules";
90
99
  /** local 车道的三面束 + 生命周期。`dispose()` 释放 core File durable 分区 provider 的写锁(见 {@link createFilePermissionRuleStores})。 */
91
100
  export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
92
101
  durable: FileDurableRulePartitionProvider;
@@ -4,7 +4,7 @@ import { readdirSync } from "node:fs";
4
4
  import { performance } from "node:perf_hooks";
5
5
  import { FileDurableRulePartitionProvider, AppendLog, ensureDir, } from "@sema-agent/core";
6
6
  import { z } from "zod";
7
- import { DurableRuleScopeSchema, RuleOfferColumn, buildRulePayloadHash, recordOwnerOf, refusedOwnerSentence } from "./permission-rule-store-sql.js";
7
+ import { DurableRuleScopeSchema, IMPORT_RECORD_KIND, RuleOfferColumn, buildRulePayloadHash, recordOwnerOf, refusedOwnerSentence } from "./permission-rule-store-sql.js";
8
8
  import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
9
9
  import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_KINDS, RULE_APPROVAL_RECORD_SCHEMA, RULE_APPROVAL_STATES, RULE_BEHAVIORS, readRuleOwner } from "@sema-agent/core";
10
10
  const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
@@ -173,6 +173,18 @@ export class FileRuleApprovalRecordStore {
173
173
  this.rows.set(id, structuredClone(next));
174
174
  return true;
175
175
  }
176
+ async importRecordOwners() {
177
+ this.gate.assertUsable();
178
+ const owners = [];
179
+ for (const r of this.rows.values()) {
180
+ if (!("staleSchema" in r) && r.kind !== IMPORT_RECORD_KIND)
181
+ continue;
182
+ const owner = recordOwnerOf(r);
183
+ if (owner !== undefined)
184
+ owners.push(owner);
185
+ }
186
+ return owners;
187
+ }
176
188
  async discardPendingRecord(recordId) {
177
189
  this.gate.assertUsable();
178
190
  const cur = this.rows.get(recordId);
@@ -319,8 +331,9 @@ export class FileRuleImportTicketStore {
319
331
  return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
320
332
  }
321
333
  }
334
+ export const FILE_PERMISSION_RULE_DIR = "permission-rules";
322
335
  export function createFilePermissionRuleStores(root, opts) {
323
- const dir = join(root, "permission-rules");
336
+ const dir = join(root, FILE_PERMISSION_RULE_DIR);
324
337
  ensureDir(dir);
325
338
  const durable = opts?.onError ? new FileDurableRulePartitionProvider(dir, opts.onError) : new FileDurableRulePartitionProvider(dir);
326
339
  const approvals = new FileRuleApprovalRecordStore(dir, opts?.warn);
@@ -158,6 +158,8 @@ export declare function buildRuleBucketChecksum(body: {
158
158
  /** 这只桶的行写在哪一版规则文法里(`SESSION_RULES_GRAMMAR` 的数);缺席 = 存量无戳行(列 NULL)。 */
159
159
  grammar?: number;
160
160
  }): string;
161
+ /** 一只桶要的全部 SQL 能力:方言 + 一只执行口(池,或一次性迁移握着锁的那一条连接 —— M36 在那条连接上经同一只写面落墓碑)。 */
162
+ type BucketSql = Pick<SqlDriver, "dialect" | "query">;
161
163
  /**
162
164
  * SQL 双方言的 {@link DurableRulePartitionProvider} —— [ref] 里的 **durable 分区后端**
163
165
  * (`user` + `project` 两源的 OR-Set CRDT),**不是**引擎接缝本身:
@@ -170,7 +172,7 @@ export declare function buildRuleBucketChecksum(body: {
170
172
  export declare class SqlDurableRulePartitionProvider implements DurableRulePartitionProvider {
171
173
  private readonly db;
172
174
  private readonly now;
173
- constructor(db: SqlDriver, now?: () => number);
175
+ constructor(db: BucketSql, now?: () => number);
174
176
  forPrincipal(principal: string | undefined): DurableRulePartition;
175
177
  forLocalOwner(): DurableRulePartition;
176
178
  }
@@ -182,6 +184,33 @@ export declare function recordOwnerOf(record: {
182
184
  readonly owner?: unknown;
183
185
  readonly principal?: unknown;
184
186
  }): RuleOwner | undefined;
187
+ /**
188
+ * 一次性迁移 M36(窗口见 `plugins/one-time-migrations.ts` 登记表)要读的桶:**规则表里的每一只桶**(按 `owner_key` 分页,与 M11 同一只走法)。
189
+ * 不从审批记录推:收编腿把规则桶整桶改键到新身份、而审批史按 D2 留在旧身份名下(`src/adoption/plan.ts` 的 `permission_rule#owner_key` 腿与
190
+ * `permission-rule-approval-history` 一员)—— 从记录推出的属主集在收编之后漏掉新桶。属主读法只有引擎的 `readRuleOwner`(与本店读口同一只);
191
+ * 读不出属主的行不点名任何可解析的桶(读腿同样解析不到它),不返回。`db` 是池或迁移握锁的那一条连接。
192
+ */
193
+ export declare function listRuleBucketOwners(db: Pick<SqlDriver, "dialect" | "query">): Promise<RuleOwner[]>;
194
+ /**
195
+ * 取 dot 那一步「建行之后、读行之前」这只桶被并发删掉了(另一只副本删桶恰落在这一窗里)。具名类 ⇒ 调用方按类判(M36 退役读作「无可退役」),不按文案。
196
+ */
197
+ export declare class RuleBucketVanishedError extends Error {
198
+ readonly ownerKey: string;
199
+ constructor(ownerKey: string);
200
+ }
201
+ /** 这只抛出物是不是本店的「桶在写它的那一步里没了」({@link RuleBucketVanishedError})。 */
202
+ export declare function isSqlBucketVanished(e: unknown): boolean;
203
+ /**
204
+ * 这只读桶抛出物是不是**本店读腿的整桶拒** —— 判的是读腿({@link SqlDurableRulePartition} 的 `list` / 写的读基)**真往外交的**那两类,不是判官内部的抛出形:
205
+ * · {@link BucketRefusedError} —— 判官四道(文法戳 / 指纹 / 行的 behavior / 引擎校验器)的拒;
206
+ * · {@link StoreCorruptError} —— 某只 JSON 列的体读不出(`not-json` / `malformed`),读腿在 {@link refusingUnreadableColumns} 把判官抛的列错误折成的店拒绝契约。
207
+ * 两类都是这一行的确定性判决:原样再读也读不出,读腿对它同样整桶拒(零规则),所以它什么都不放行。
208
+ * 别的抛出物**不是**这一类:连接 / 超时 / 权限(一次读失败证明不了以后也读不出);以及仍以 {@link SqlJsonColumnError} 穿出读腿的唯一一形 `not-text`
209
+ * (列没按文本投影:读腿与解码点漂了,是码缺陷不是坏数据 —— 按列错误类判它会把一次码漂读成「这只桶坏了」)。
210
+ */
211
+ export declare function isSqlBucketRefusal(e: unknown): boolean;
212
+ /** 导入记录的种类词(引擎闭集 `RULE_APPROVAL_KINDS` 的一员;`imported-cc` 出处由它派生)。 */
213
+ export declare const IMPORT_RECORD_KIND = "import";
185
214
  /** 写口拒句(两只店同一句):属主引擎读不出,记录不落。 */
186
215
  export declare function refusedOwnerSentence(recordId: string): string;
187
216
  /**
@@ -542,4 +571,5 @@ export interface PermissionRuleStores {
542
571
  reapExpired(nowMs: number): Promise<number>;
543
572
  }
544
573
  export declare function createSqlPermissionRuleStores(db: SqlDriver, now?: () => number): PermissionRuleStores;
574
+ export {};
545
575
  //# sourceMappingURL=permission-rule-store-sql.d.ts.map
@@ -6,7 +6,7 @@ import { isMissingColumnError } from "./sql-errors.js";
6
6
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
7
7
  import { canonicalJsonStringify } from "@sema-agent/core";
8
8
  import { ensureIndexes } from "./ensure-index.js";
9
- import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, SqlJsonColumnError } from "./sql-json-column.js";
9
+ import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, SqlJsonColumnError, StoreCorruptError } from "./sql-json-column.js";
10
10
  import { redactSecrets } from "../trace/redact.js";
11
11
  export function writerOfSqlRuleStore(store) {
12
12
  const w = Reflect.get(store, PERMISSION_RULE_WRITER);
@@ -508,7 +508,7 @@ class SqlDurableRulePartition {
508
508
  const { rows } = await this.db.query(this.q(`SELECT actor, counter, grammar FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT actor, counter, grammar FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
509
509
  const row = rows[0];
510
510
  if (row === undefined)
511
- throw new Error(`permission_rule bucket ${this.ownerKey} vanished while minting a dot`);
511
+ throw new RuleBucketVanishedError(this.ownerKey);
512
512
  readBucketStamp(this.ownerKey, row.grammar);
513
513
  const cur = Number(row.counter);
514
514
  const next = cur + 1;
@@ -593,6 +593,39 @@ export class SqlDurableRulePartitionProvider {
593
593
  export function recordOwnerOf(record) {
594
594
  return readRuleOwner(record.owner ?? { kind: "principal", principal: record.principal });
595
595
  }
596
+ export async function listRuleBucketOwners(db) {
597
+ const owners = [];
598
+ let after = "";
599
+ for (;;) {
600
+ const { rows } = await db.query(db.dialect === "mysql"
601
+ ? `SELECT owner_key, owner_kind, principal FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > ? ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`
602
+ : `SELECT owner_key, owner_kind, principal FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > $1 ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`, [after]);
603
+ for (const row of rows) {
604
+ after = String(row["owner_key"]);
605
+ const principal = row["principal"] === null || row["principal"] === undefined ? undefined : String(row["principal"]);
606
+ const owner = readRuleOwner({ kind: row["owner_kind"], ...(principal !== undefined ? { principal } : {}) });
607
+ if (owner !== undefined)
608
+ owners.push(owner);
609
+ }
610
+ if (rows.length < UNDURABLE_PURGE_PAGE)
611
+ return owners;
612
+ }
613
+ }
614
+ export class RuleBucketVanishedError extends Error {
615
+ ownerKey;
616
+ constructor(ownerKey) {
617
+ super(`permission_rule bucket ${ownerKey} vanished while minting a dot`);
618
+ this.name = "RuleBucketVanishedError";
619
+ this.ownerKey = ownerKey;
620
+ }
621
+ }
622
+ export function isSqlBucketVanished(e) {
623
+ return e instanceof RuleBucketVanishedError;
624
+ }
625
+ export function isSqlBucketRefusal(e) {
626
+ return e instanceof BucketRefusedError || e instanceof StoreCorruptError;
627
+ }
628
+ export const IMPORT_RECORD_KIND = "import";
596
629
  export function refusedOwnerSentence(recordId) {
597
630
  return `refusing to create approval record ${recordId}: its owner is not one the engine reads as an owner (a principal name must be a non-blank string; the kind must be a member of the owner set) — nothing was written`;
598
631
  }
@@ -314,21 +314,18 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
314
314
  * 🔴 **工厂认设备不认住客**(读口一只 = 会话店执行席 `seat.read(root)`,与解析器 / 换绑门 / 执行放置读面 / 准入链同一只读):按 placement 根读**当前**格,
315
315
  * 与这条腿带来的设备句柄 `honor` 比,不同即 typed 拒。判序(一次行读):
316
316
  * root = ctx.placementRootSessionId // 空 ⇒ config.placement_root_invalid
317
- * honor = ctx.resumeWorkspace?.deviceId
317
+ * honor = ctx.placementTarget?.deviceId // 引擎交来的放置围栏(这条腿必须落在哪台设备上)
318
318
  * cell = seat.read(root).cell // 行不在 / 格缺 / mirror / writer(device, ∅) ⇒ device.not_bound(唯一读法 `boundDeviceOf`)
319
319
  * honor ≠ undefined ∧ 格上设备 ≠ honor ⇒ device.identity_mismatch(句点名两台,零连接)
320
320
  * readDeviceForAdmission(格上设备, 格上属主)
321
321
  * 工厂**不**读 `task_run`、不读子代分区行、不按 `ctx.taskId`、不比化身、不比属主(住客谓词只用于写与 judged 复核,不是工厂的读者)。
322
- * 续跑腿(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备 ⇒ 本工厂**只**在那台设备上铸 —— 根此刻绑的不是它(换绑过)
323
- * ⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。「绑的就是它但现在不在线」与新跑同形:
324
- * 构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。行没记设备(早于该字段的行)⇒ 引擎已命名的「行未记设备 ⇒ 普通路由」臂,按根读当前格。
325
- * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
326
- *
327
- * 🔴 **过渡期(条件句)**:新派后代腿的放置句柄(引擎在 `ExecutionEnvFactoryContext` 上的放置工作区字段,`honor` 的第二来源)**只在本仓所基的引擎版本带它时**
328
- * 进判序;本仓今天所基的引擎上下文没有这个字段 ⇒ `honor` 只取续跑腿的 `resumeWorkspace`,新派后代腿按根读当前格。本仓**不**注入任何替代见证、不造任何行
329
- * (修在源头)。敞口(在那个字段到货之前成立):活过父 turn 的后台子代,在根换绑之后新派的腿会落到根当前设备 —— 换绑半边由换绑门的子代活性判关闭(归换绑门那一车),
330
- * 删除半边另有票。字段到货后的形:`honor = ctx.resumeWorkspace?.deviceId ?? <放置句柄>.deviceId`,放置句柄的 provider 不是本车道 ⇒ 装配拒(`config.placement_root_invalid` 同级)。
331
- * 本仓任何文本不得宣称工厂「一律按住客定址」。
322
+ * 围栏(`ExecutionEnvFactoryContext.placementTarget`,引擎每条腿一个答案):派生它的那条腿的环境报过的目标 —— 委派子代 / 工作流 agent / 被唤醒的
323
+ * 保留 agent 带唤醒腿的目标,级联后续阶 / 验证腿带第一条腿的目标,续跑腿带检查点行记下的目标。围栏点名了设备 ⇒ 本工厂**只**在那台设备上铸 ——
324
+ * 根此刻绑的不是它(换绑过)⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。
325
+ * 「绑的就是它但现在不在线」与新跑同形:构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。围栏缺席(顶层首腿,或派生它的腿
326
+ * 不是远端环境)或不点名设备 ⇒ 普通路由,按根读当前格。围栏的 `provider` 不进判序(引擎的回执也只比设备)。
327
+ * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄围栏;引擎拿这份报告复核每条带围栏的腿(不符 ⇒ 续跑腿 `resume.env_failed`、
328
+ * 其余腿 `config.placement_not_honored`,都在腿跑之前)—— 本工厂在连接之前就拒,那两个词只在报告与围栏真不一致时出现。
332
329
  */
333
330
  export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
334
331
  /**
@@ -17,6 +17,7 @@ export const DEVICE_OUTCOME_TO_FILE_ERROR = {
17
17
  is_directory: "is_directory",
18
18
  liveness_no_output: "unknown",
19
19
  name_too_long: "name_too_long",
20
+ symlink_loop: "symlink_loop",
20
21
  not_directory: "not_directory",
21
22
  not_found: "not_found",
22
23
  not_supported: "not_supported",
@@ -40,6 +41,7 @@ export const DEVICE_OUTCOME_TO_EXEC_ERROR = {
40
41
  is_directory: "unknown",
41
42
  liveness_no_output: "liveness_no_output",
42
43
  name_too_long: "unknown",
44
+ symlink_loop: "unknown",
43
45
  not_directory: "unknown",
44
46
  not_found: "unknown",
45
47
  not_supported: "shell_unavailable",
@@ -771,6 +773,7 @@ const OUTCOME_TO_REMOTE_ERROR = {
771
773
  is_directory: "unknown",
772
774
  liveness_no_output: "liveness_no_output",
773
775
  name_too_long: "unknown",
776
+ symlink_loop: "unknown",
774
777
  not_directory: "unknown",
775
778
  not_found: "unknown",
776
779
  not_supported: "unsupported",
@@ -806,10 +809,10 @@ export function deviceExecutionEnvFactory(deps) {
806
809
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
807
810
  "the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
808
811
  }
809
- const honor = ctx.resumeWorkspace?.deviceId;
812
+ const honor = ctx.placementTarget?.deviceId;
810
813
  if (honor !== undefined && honor !== binding.id) {
811
- throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${honor}, but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
812
- "a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
814
+ throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this leg is placed on device ${honor} (the device the work it continues ran on), but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
815
+ "work already under way does not follow a rebind (the new device has no copy of that workspace); finish or cancel the running or parked task, then start it again on the device now bound");
813
816
  }
814
817
  const view = await deps.store.readDeviceForAdmission(binding.id, binding.owner);
815
818
  if (!view) {
@@ -386,6 +386,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
386
386
  * loud `workspace_latest_dangling`, the pointer deleted, an empty workspace); absent (404 — the key, with the bucket there) ⇒ a fresh pod; a read that
387
387
  * FAILS ⇒ a loud `connect_failed` — a 404 `NoSuchBucket` included ({@link refuseMissingBucket}) — never a silent fresh workspace (a run that believes it resumed while it did not is
388
388
  * the worst form). Once the pod holds the workspace, this env arms its fence seat (its destroy will commit).
389
+ * The same walk ({@link establish}) is the restore point of a parked run's resumed leg whose park packed nothing ({@link resumeVM}).
389
390
  * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but no such session (the occupant absent) ⇒ a
390
391
  * fresh pod and one `workspace_space_unavailable` line (no restore by any other key). The occupant read FAULTED ⇒ the same
391
392
  * loud `connect_failed` as a failed `latest` read, no pod ({@link provision} refuses it — every entry that would create one).
@@ -397,14 +398,16 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
397
398
  * as it stands to every later connect on every lane — the previous commit is not waited on again, `latest` is not read again, no pod is created.
398
399
  */
399
400
  connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
400
- /** The one walk of a connect ({@link connect} runs it once at a time). */
401
+ /** The one walk of a connect ({@link connect} runs it once at a time) — also the resumed leg's walk when its park packed nothing ({@link restorePointOf},
402
+ * under the resume's own hold of the same gate). */
401
403
  private establish;
402
404
  /**
403
405
  * The restore point's fill (`workspace-fill.ts`): a pod whose workspace this env just minted or restored is handed over only once the fill seat placed
404
406
  * its content — through {@link fillChannel} (this adapter's own exec, never the public face: inside a connect the public face waits on that very connect).
405
407
  * A fill that does not complete releases the pod (no tool and no run-end snapshot may see a half-filled workspace); a refusal that would answer the same
406
408
  * again (`standing`) is held for the env ({@link holdRefusal}). No seat ⇒ the pod as it came. `origin` tells the seat whether the workspace was minted
407
- * (a new pod with nothing restored: the space off / unavailable / without `latest`, or `latest` dangling) or restored (the space's `latest`, or a park snapshot whose archive came back; a park snapshot with no archive — a plan-only park — is minted).
409
+ * (a new pod with nothing restored: the space off / unavailable / without `latest`, or a dangling restore point — `latest` or a park snapshot whose archive is gone)
410
+ * or restored (the space's `latest`, or a park snapshot whose archive came back). A park that packed nothing resumes through a new connect, so it reads as that connect does.
408
411
  */
409
412
  private settle;
410
413
  /** The restore point's channel: a fixed command through the one start seam (start form `adapter`, the engine's commands' identity) and a write in
@@ -447,9 +450,11 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
447
450
  * never its text. Not a knob: there is no honest way to run a space we could not read — a retry of the run reads it again.
448
451
  */
449
452
  private refuseUnreadableOccupant;
450
- /** Every space-lane operator line: the space is named by its incarnation's log prefix only, and free text (a curl
451
- * stderr, a fetch error) passes {@link redactIncarnations} — the full token never reaches a log body. */
453
+ /** Every space-lane operator line: the space is named by its incarnation's log prefix only ({@link snapshotLine} does the rest). */
452
454
  private spaceLine;
455
+ /** Every snapshot operator line, both lanes — the ONE redaction seat: each string field (a key, a curl stderr, a fetch error, an effect) passes
456
+ * {@link redactIncarnations}, so a full incarnation-shaped token never reaches a log body, whatever text carried it there. */
457
+ private snapshotLine;
453
458
  /** A fresh pod (create → Running → workspace root), bounded retry — THE seat every pod of this env is created through
454
459
  * ({@link connect} and, via {@link restoreSnapshot}, {@link resumeVM}). A lane keyed by an occupant read that faulted ⇒ refused
455
460
  * here, before any request ({@link refuseUnreadableOccupant}): never retried (the reading is this env's, fixed at construction). */
@@ -651,11 +656,14 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
651
656
  */
652
657
  private uploadSnapshot;
653
658
  /**
654
- * Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
655
- * provision a pod, then download + untar the snapshot into the workspace ({@link restoreSnapshot}, lane `park`). A
656
- * missing object (a plan-only park: no pod was ever minted, so nothing was uploaded) resumes as a clean workspace whose origin is `minted` —
657
- * no restorable tree — so the restore point seeds it; the resume walks the park lane and never reads the space's `latest` (which a fresh connect with the space on
658
- * would restore); a snapshot that came back is `restored` and never seeded.
659
+ * Resume a parked run's workspace onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM). Where the leg's workspace comes from is
660
+ * read off the park's snapshot id — it says what the park packed ({@link snapshotIdHolds}), never guessed from what the store answers:
661
+ * - an archive ⇒ provision a pod, then download + untar it into the workspace ({@link restoreSnapshot}, lane `park`): it came back ⇒ `restored`
662
+ * (never seeded); it is not there ⇒ a dangling restore point, the same rule on both lanes ({@link dropDangling}: one line, an empty workspace);
663
+ * - nothing (the env held no pod when it parked — the run parked before anything touched its hand) ⇒ there is no workspace of the park to
664
+ * restore, so the leg's restore point is the one a new connect has ({@link establish}): space open ⇒ the previous commit waited out and
665
+ * `latest` restored (none ⇒ a fresh workspace), space off ⇒ a fresh workspace — never a fresh workspace beside a `latest` that the leg's
666
+ * run-end commit would then overwrite. No archive is asked for under such an id.
659
667
  *
660
668
  * [ref]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
661
669
  * 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
@@ -666,15 +674,14 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
666
674
  resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<ConnectResult>;
667
675
  /** The one walk of a resume ({@link resumeVM} runs it under the connect gate). */
668
676
  private resumeWalk;
669
- /**
670
- * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace.
671
- * A missing archive (404) starts from the clean workspace on both lanes, and on both it is the same fact — no restorable tree ⇒ origin
672
- * `minted` (the restore point seeds it, exactly as on a fresh connect). What differs is only what it means:
673
- * - park: a plan-only suspend has no object (the park came before anything touched the hand) — nothing to say;
674
- * - space: `latest` dangles (its pointer is only written after a successful upload, so the archive was removed later —
675
- * by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
676
- * {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
677
- * snapshot then becomes the new `latest`.
677
+ /** The resumed leg's restore point, chosen by what the park's id names ({@link resumeVM}) — a closed set, one arm each. */
678
+ private restorePointOf;
679
+ /**
680
+ * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace. Only an id that names an
681
+ * archive reaches it ({@link snapshotIdHolds}: a park that packed nothing resumes through {@link establish}; `latest` names only an object
682
+ * that was written). So a missing archive (404) is one fact on both lanes — the restore point names data that is gone (removed later, by the
683
+ * bucket's lifecycle rule or a hand) ⇒ loud and continuable, ONE rule: {@link dropDangling} (one `workspace_latest_dangling` line), and the
684
+ * run continues on the clean workspace, origin `minted` (no restorable tree ⇒ the restore point seeds it, exactly as on a fresh connect).
678
685
  * An attempt that does not complete releases the pod it provisioned: no tool and no run-end snapshot may ever see a torn
679
686
  * workspace. A transient class ({@link restoreFailureIsTransient}) is tried again on a fresh pod after the `retry.backoffMs`
680
687
  * pause, up to the lane's {@link RESTORE_MAX_ATTEMPTS}; a non-transient one ends the restore at once. The lane then says what
@@ -691,12 +698,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
691
698
  /** The pause before a restore's next attempt (`retry.backoffMs`, the adapter's one retry pause). `false` ⇒ aborted (now or during it). */
692
699
  private restorePause;
693
700
  /**
694
- * `latest` names an archive that is not there ([ref]): one `workspace_latest_dangling{space, key, deleted}` line and the
695
- * pointer is deleted (the next run must not trip on it again); the run continues on the empty workspace. A pointer
696
- * delete that fails rides on the same line (`deleted: false` + the error) — the next run then says it once more.
697
- * This env is the space's only writer here (the commit fence admits one per space), so the delete is a plain one.
698
- */
699
- private dropDanglingLatest;
701
+ * A restore point names an archive that is not there — the data is gone by an operator rule (a bucket lifecycle rule, a hand): loud and
702
+ * continuable, ONE rule on both lanes — one `workspace_latest_dangling{lane, key, …}` line, and the run continues on the empty workspace
703
+ * {@link restoreSnapshot} hands over. `key` = the object the id names, spelled by {@link snapshotBase} (the one spelling of both partitions).
704
+ * What each lane adds is only what it holds:
705
+ * - space: its `latest` pointer is deleted (the next run must not trip on it again); the line carries `space` and `deleted` — a delete that
706
+ * fails rides on the same line (`deleted: false` + the error) and the next run says it once more. This env is the space's only writer here
707
+ * (the commit fence admits one per space), so the delete is a plain one. The run's own run-end snapshot then becomes the new `latest`;
708
+ * - park: there is no pointer to drop (the id lives in the engine's checkpoint); the line carries the session id. With the space open, the
709
+ * resumed leg's run-end snapshot then becomes the space's new `latest`.
710
+ * Both lines pass the one redaction seat ({@link snapshotLine}).
711
+ */
712
+ private dropDangling;
700
713
  /**
701
714
  * Honest post-resume consistency ([ref] §5 council #8): the workspace FILES are restored byte-for-byte
702
715
  * from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
@@ -8,7 +8,7 @@ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, shellQuote,
8
8
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
9
9
  import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
10
10
  import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
11
- import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
11
+ import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotIdHolds, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
12
12
  import { spaceCommitAlive } from "../workspace-space-commit.js";
13
13
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
14
14
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
@@ -509,8 +509,10 @@ export class RemoteK8sExecutionEnv {
509
509
  return spaceReadFailed("occupant", ` (${code}) — refusing to start this run on a workspace that could be neither restored nor saved; retry the run once the session store answers, or set ${ways}`);
510
510
  }
511
511
  spaceLine(event, space, fields) {
512
- const safe = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, typeof v === "string" ? redactIncarnations(v) : v]));
513
- this.logger?.warn(event, { space: incarnationLogPrefix(space.incarnation), ...safe });
512
+ this.snapshotLine(event, { space: incarnationLogPrefix(space.incarnation), ...fields });
513
+ }
514
+ snapshotLine(event, fields) {
515
+ this.logger?.warn(event, Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, typeof v === "string" ? redactIncarnations(v) : v])));
514
516
  }
515
517
  provision(signal) {
516
518
  const space = this.spaceLane;
@@ -1224,12 +1226,12 @@ export class RemoteK8sExecutionEnv {
1224
1226
  async uploadSnapshot(target, boundMs = this.cfg.dataTimeoutMs) {
1225
1227
  if (target.lane === "park" && this.inFlight.size > 0)
1226
1228
  return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
1227
- const snapshotId = mintSnapshotId(this.occupantIncarnation);
1228
1229
  if (!this.podName) {
1229
1230
  return target.lane === "space"
1230
1231
  ? { ok: false, error: new RemoteExecutionError("connect_failed", "the sandbox pod was gone when the run-end snapshot reached the upload — nothing was saved (the workspace could not be read), and `latest` keeps naming the previous snapshot") }
1231
- : ok(snapshotId);
1232
+ : ok(mintSnapshotId(this.occupantIncarnation, "nothing"));
1232
1233
  }
1234
+ const snapshotId = mintSnapshotId(this.occupantIncarnation, "archive");
1233
1235
  const presigned = this.presignSnapshot({ ...target, method: "PUT", snapshotId });
1234
1236
  if (!presigned.ok)
1235
1237
  return presigned;
@@ -1273,13 +1275,26 @@ export class RemoteK8sExecutionEnv {
1273
1275
  this.logger?.warn("sandbox_pod_released_before_restore", { pod: this.podName, ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}), effect: "this env held a pod minted before the park snapshot's restore — it is deleted; the restore provisions the leg's one pod" });
1274
1276
  await this.releasePod();
1275
1277
  }
1276
- const r = await this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, options?.abortSignal));
1278
+ const r = await this.restorePointOf(snapshotId, options?.abortSignal);
1277
1279
  if (!r.ok)
1278
1280
  return r;
1279
1281
  this.handleSnapshotId = snapshotId;
1280
1282
  this.armSpace();
1281
1283
  return ok(this.workspaceHandle());
1282
1284
  }
1285
+ async restorePointOf(snapshotId, abortSignal) {
1286
+ const holds = snapshotIdHolds(snapshotId);
1287
+ switch (holds) {
1288
+ case "archive":
1289
+ return this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, abortSignal));
1290
+ case "nothing":
1291
+ return this.establish(abortSignal ? { abortSignal } : undefined);
1292
+ default: {
1293
+ const unreachable = holds;
1294
+ throw new Error(`unhandled snapshot id family: ${String(unreachable)}`);
1295
+ }
1296
+ }
1297
+ }
1283
1298
  async restoreSnapshot(target, snapshotId, abortSignal) {
1284
1299
  const presigned = this.presignSnapshot({ ...target, method: "GET", snapshotId });
1285
1300
  if (!presigned.ok)
@@ -1303,8 +1318,7 @@ export class RemoteK8sExecutionEnv {
1303
1318
  : undefined;
1304
1319
  if (!failure) {
1305
1320
  if (r.ok && r.value.stdout.includes("EMPTY_SNAPSHOT")) {
1306
- if (target.lane === "space")
1307
- await this.dropDanglingLatest(target.space, snapshotId);
1321
+ await this.dropDangling(target, snapshotId);
1308
1322
  return { ok: true, value: this.workspaceHandle(), origin: "minted" };
1309
1323
  }
1310
1324
  return { ok: true, value: this.workspaceHandle(), origin: "restored" };
@@ -1347,15 +1361,34 @@ export class RemoteK8sExecutionEnv {
1347
1361
  signal?.addEventListener("abort", onAbort, { once: true });
1348
1362
  });
1349
1363
  }
1350
- async dropDanglingLatest(space, snapshotId) {
1351
- const prefix = this.snapshotKeyPrefix();
1352
- const dropped = await this.writePointer(space, "latest", null);
1353
- this.spaceLine("workspace_latest_dangling", space, {
1354
- key: `${prefix}/${space.incarnation}/space/${snapshotId}.tar.gz`,
1355
- deleted: dropped.ok,
1356
- ...(dropped.ok ? {} : { error: dropped.error.message }),
1357
- effect: "the space's latest snapshot is gone (removed by a bucket lifecycle rule or by hand) — this run starts from an empty /workspace; its run-end snapshot becomes the new latest",
1358
- });
1364
+ async dropDangling(target, snapshotId) {
1365
+ const key = `${this.snapshotBase(target)}${snapshotId}.tar.gz`;
1366
+ switch (target.lane) {
1367
+ case "space": {
1368
+ const dropped = await this.writePointer(target.space, "latest", null);
1369
+ this.spaceLine("workspace_latest_dangling", target.space, {
1370
+ lane: "space",
1371
+ key,
1372
+ deleted: dropped.ok,
1373
+ ...(dropped.ok ? {} : { error: dropped.error.message }),
1374
+ effect: "the space's latest snapshot is gone (removed by a bucket lifecycle rule or by hand) — this run starts from an empty /workspace; its run-end snapshot becomes the new latest",
1375
+ });
1376
+ return;
1377
+ }
1378
+ case "park":
1379
+ this.snapshotLine("workspace_latest_dangling", {
1380
+ lane: "park",
1381
+ key,
1382
+ ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}),
1383
+ effect: "the parked run's snapshot is gone (removed by a bucket lifecycle rule or by hand) — the resumed leg starts from an empty /workspace" +
1384
+ (this.spaceLane.state === "open" ? "; its run-end snapshot becomes the space's new latest" : ""),
1385
+ });
1386
+ return;
1387
+ default: {
1388
+ const unreachable = target;
1389
+ throw new Error(`unhandled snapshot lane: ${String(unreachable.lane)}`);
1390
+ }
1391
+ }
1359
1392
  }
1360
1393
  async postResumeInit() {
1361
1394
  if (!this.cfg.s3Snapshot)
@@ -387,7 +387,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
387
387
  * 它的域完全靠「属主 sessionId 的墓碑」推。而会话 id 是**调用方可自选**的 ⇒ 两个租户先后用过同一个 id
388
388
  * 完全合法;两边都删过之后,同一个 row_key 上会有**两个域**的墓碑,这时任何一边的 sweep 拿自己的墓碑
389
389
  * 去认领这些字节都只是猜。判据因此是:**存在第二个域的同名墓碑 ⇒ 这批行不可归属 ⇒ 一根不删**,
390
- * 实体清理退回既有的 TTL 腿(`reapOlderThan`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
390
+ * 实体清理退回既有的 TTL 腿(core `ToolResultStore.reap`)。少删一次是可接受的;删掉另一个租户的字节、还跳过它的
391
391
  * legal hold 与审计域,不可接受。
392
392
  */
393
393
  protected orphanToolResultCandidates(exec: {
@@ -475,7 +475,7 @@ export declare class SqlRetentionStore implements ManagedRetentionCapability {
475
475
  * 也只计 `skipped` 不删。会话本体的清理归 {@link SqlRetentionStore.deleteExpiredSessions}(它会连着
476
476
  * 这行一起删),两条腿因此不重叠也不互相抢行。
477
477
  * · **一根不碰的那一支**:`owner_session_id IS NULL` 的**无出处**行。没有出处就没有域,按任何域去删它
478
- * 都是猜;实体清理归既有的 TTL 腿(`reapOlderThan`,`boot/reapers.ts` 驱动)。这与 core 的
478
+ * 都是猜;实体清理归既有的 TTL 腿(core `ToolResultStore.reap`,`boot/reapers.ts` 驱动)。这与 core 的
479
479
  * 「an UNOWNED entry is never deleted」是同一条纪律的两处执行点。
480
480
  *
481
481
  * 事务形与另外两只方法逐字同款(hold 哨兵行锁读 → 删除+墓碑 → 审计行,同 commit 同 rollback)。
@@ -5,11 +5,9 @@ export declare const READ_FLOOR_UNAVAILABLE_REASONS: readonly ["arch", "user_not
5
5
  export type ReadFloorUnavailableReason = (typeof READ_FLOOR_UNAVAILABLE_REASONS)[number];
6
6
  /** F2:附着不过的环境上,带地板起动的拒句(纯字符串 ⇒ `build*`)。首句点名原因词;点名表的首行与旋钮。 */
7
7
  export declare function buildReadFloorUnavailableMessage(reason: ReadFloorUnavailableReason, detail: string | undefined, floor: ReadFloor | undefined): string;
8
- /** F6:守地板的环境上,不带地板的引擎起动的拒句。 */
9
- export declare function buildReadFloorMissingMessage(lane: SandboxFloorLane): string;
10
8
  /**
11
9
  * 一次起动是什么(闭集,调用方必须表态 —— 没有缺省臂):
12
- * · `engine` —— 经执行环境公共起动口要起的命令(引擎的,或替引擎干活的服务端调用方的);带不带地板是它交来的事实。`hold` = 适配器之后还会以
10
+ * · `engine` —— 经执行环境公共起动口要起的命令(引擎的,或替引擎干活的服务端调用方的);它带的地板就是它的表,缺席 = 表无行。`hold` = 适配器之后还会以
13
11
  * 同一起动号再起 `run`(k8s 后台作业的轮询 / 停 / 收尾读的是这次起动的产物,按这次起动的地板跑),登记留到 {@link SandboxReadFloor.release};
14
12
  * · `under` —— 以一次已登记(`hold`)起动的号再起一只 `run`(上面那类轮询);
15
13
  * · `adapter` —— 适配器自己的固定命令(文件面助手、探针、数据搬运、全杀);附着成功之后经监督者的 `plain` 起;
@@ -29,7 +27,7 @@ export type FloorStart = {
29
27
  };
30
28
  export declare const ADAPTER_START: FloorStart;
31
29
  export declare const BOOTSTRAP_START: FloorStart;
32
- /** 引擎起动:`readFloor` 原样交(`undefined` = 这次起动不带地板)。 */
30
+ /** 引擎起动:`readFloor` 原样交(`undefined` = 这次起动的表一行都没有)。 */
33
31
  export declare function engineStart(readFloor: ReadFloor | undefined, hold?: boolean): FloorStart;
34
32
  /** 起动身份:`root` = 车道以 root 起(e2b `user:"root"`、local-docker `-u 0`);`exec-user` = 车道自己的执行身份(改前的形)。 */
35
33
  export type FloorLaunchIdentity = "root" | "exec-user";
@@ -125,8 +123,8 @@ export interface RelayAnswer {
125
123
  export declare const UNJUDGED_VERDICT = "read floor: no verdict for this start";
126
124
  /** 判官:适配器进程里唯一回答「这条路径拒不拒」的东西。live object ⇒ `create*`。 */
127
125
  export interface FloorJudge {
128
- /** 登记一次带地板的起动,回它的起动号(随机、不可猜)。`held` ⇒ 之后还会以这个号再起 `run`(`under`),直到 {@link release};否则这一次就是它唯一的起动。 */
129
- register(floor: ReadFloor, held: boolean): string;
126
+ /** 登记一次引擎起动(带它的表;`undefined` = 表无行),回它的起动号(随机、不可猜)。`held` ⇒ 之后还会以这个号再起 `run`(`under`),直到 {@link release};否则这一次就是它唯一的起动。 */
127
+ register(floor: ReadFloor | undefined, held: boolean): string;
130
128
  /** 以这个号再起一次 `run`(`under`)之前问:在册且适配器没说过「不再起」⇒ `true`(记下又有一只监督者将连上);否则 `false`(起动拒)。 */
131
129
  launch(startId: string): boolean;
132
130
  /** 适配器不会再以这个号起 `run` 了。登记要两件都到才撤(协议 §5.5):这一句 ∧ 此后的 `gone` —— `gone` 已在 ⇒ 当即撤;否则等它(还连着的监督者照旧有判官答)。 */
@@ -202,7 +200,7 @@ export interface SandboxReadFloor {
202
200
  readonly state: SandboxReadFloorState;
203
201
  /** 这只模块钉住的监督者二进制(守地板 ⇒ 在;裸模块 ⇒ 缺席)。k8s 的 pod 座({@link buildFloorPodSeat})只读它 —— 放进 pod 的与自检核的是同一份钉。 */
204
202
  readonly binaries: FloorBinaries | undefined;
205
- /** 唯一接缝:一次起动 → 答案,或 `spawn_error`(F2 / F6 / 时机不对的引导起动 / 已撤的号)。 */
203
+ /** 唯一接缝:一次起动 → 答案,或 `spawn_error`(F2 / 时机不对的引导起动 / 已撤的号)。 */
206
204
  wrap(start: FloorStart): Result<FloorLaunch, ExecutionError>;
207
205
  /** 适配器不会再以这个号起 `run`(`hold` 起动的收尾)。登记在其后的 `gone` 到时撤(还连着的监督者照旧有判官答)。 */
208
206
  release(startId: string | undefined): void;