@sema-agent/server 7.135.0 → 7.136.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 (185) hide show
  1. package/MIGRATION.md +64 -2
  2. package/USAGE.md +62 -25
  3. package/deploy/sema-up/README.md +1 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-card.d.ts +60 -4
  7. package/dist/approval-card.js +18 -1
  8. package/dist/approval-content-kind.d.ts +1 -0
  9. package/dist/approval-content-kind.js +1 -1
  10. package/dist/auth-bridge.d.ts +34 -5
  11. package/dist/auth-bridge.js +20 -3
  12. package/dist/boot/budget-tracing.d.ts +1 -2
  13. package/dist/boot/budget-tracing.js +2 -2
  14. package/dist/boot/org-memory.js +1 -1
  15. package/dist/boot/resolve-spec.js +10 -7
  16. package/dist/boot/runner-deps.js +4 -2
  17. package/dist/boot/stage-04-stores.d.ts +1 -1
  18. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  19. package/dist/boot/stage-06-runners.d.ts +1 -1
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-08-reapers.d.ts +1 -1
  22. package/dist/boot/stage-09-leader.d.ts +1 -1
  23. package/dist/boot/stage-10-http-server.d.ts +1 -1
  24. package/dist/boot/stores.js +2 -3
  25. package/dist/boot/web-admin.js +10 -17
  26. package/dist/brain.js +12 -3
  27. package/dist/budget.d.ts +3 -3
  28. package/dist/budget.js +4 -0
  29. package/dist/capabilities/mcp-probe.d.ts +5 -4
  30. package/dist/capability-facts.d.ts +3 -7
  31. package/dist/capability-facts.js +3 -5
  32. package/dist/center-credential.d.ts +18 -4
  33. package/dist/center-credential.js +18 -3
  34. package/dist/config-catalog.js +12 -10
  35. package/dist/config-d-migration.js +1 -1
  36. package/dist/config-invariants.d.ts +1 -1
  37. package/dist/config-invariants.js +8 -0
  38. package/dist/config-lkg.d.ts +0 -38
  39. package/dist/config-lkg.js +1 -83
  40. package/dist/config-types.d.ts +12 -4
  41. package/dist/config.js +26 -8
  42. package/dist/deployment-shape.d.ts +1 -1
  43. package/dist/elicitation.d.ts +8 -7
  44. package/dist/elicitation.js +9 -8
  45. package/dist/file-history-disclosure.d.ts +6 -4
  46. package/dist/file-history-disclosure.js +22 -6
  47. package/dist/fleet-client.d.ts +32 -1
  48. package/dist/fleet-client.js +24 -4
  49. package/dist/fleet-lease.d.ts +7 -3
  50. package/dist/fleet-lease.js +31 -23
  51. package/dist/hooks/hook-llm.js +14 -8
  52. package/dist/hooks/hook-runner.d.ts +4 -4
  53. package/dist/hooks/hook-runner.js +2 -2
  54. package/dist/http/checkpoint-ctx.d.ts +28 -9
  55. package/dist/http/checkpoint-ctx.js +5 -1
  56. package/dist/http/dispatch.js +9 -9
  57. package/dist/http/resume-ctx-read.d.ts +2 -16
  58. package/dist/http/resume-ctx-read.js +2 -3
  59. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  60. package/dist/http/routes/approvals-assistant.js +47 -12
  61. package/dist/http/routes/runs.js +2 -2
  62. package/dist/http/routes/session-sync.js +31 -9
  63. package/dist/http/routes/sessions.js +18 -14
  64. package/dist/http/submit-ctx.js +1 -1
  65. package/dist/http/wire-types.d.ts +5 -3
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/model-route-gap.d.ts +49 -4
  69. package/dist/model-route-gap.js +34 -1
  70. package/dist/object-store-buckets.d.ts +13 -0
  71. package/dist/object-store-buckets.js +3 -0
  72. package/dist/observability/fail-open.d.ts +6 -6
  73. package/dist/observability/fail-open.js +6 -6
  74. package/dist/observability/metrics.js +1 -1
  75. package/dist/observability/principal-context.d.ts +4 -1
  76. package/dist/observability/principal-context.js +6 -1
  77. package/dist/parked-decide.d.ts +4 -3
  78. package/dist/parked-decide.js +26 -7
  79. package/dist/peer-directory.d.ts +2 -1
  80. package/dist/peer-directory.js +5 -5
  81. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  82. package/dist/plugins/approval-ask-store-sql.js +3 -0
  83. package/dist/plugins/blob-backend.js +2 -1
  84. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  85. package/dist/plugins/checkpoint-index-columns.js +28 -0
  86. package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
  87. package/dist/plugins/checkpoint-store-sql.js +58 -29
  88. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  89. package/dist/plugins/file-history-store-sql.js +30 -23
  90. package/dist/plugins/local-checkpoint-store.js +4 -3
  91. package/dist/plugins/one-time-migrations.d.ts +21 -28
  92. package/dist/plugins/one-time-migrations.js +39 -123
  93. package/dist/plugins/remote-env-k8s.d.ts +18 -5
  94. package/dist/plugins/remote-env-k8s.js +24 -7
  95. package/dist/plugins/s3-answer.d.ts +15 -0
  96. package/dist/plugins/s3-answer.js +5 -0
  97. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  98. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  99. package/dist/plugins/sql-errors.d.ts +39 -0
  100. package/dist/plugins/sql-errors.js +49 -1
  101. package/dist/plugins/store-fault.d.ts +4 -0
  102. package/dist/plugins/store-fault.js +8 -0
  103. package/dist/plugins/tidb-pool.js +1 -1
  104. package/dist/plugins/web-search.d.ts +1 -1
  105. package/dist/plugins/web-search.js +2 -2
  106. package/dist/request-declaration-refusal.d.ts +94 -0
  107. package/dist/request-declaration-refusal.js +78 -0
  108. package/dist/request-image-urls.d.ts +6 -0
  109. package/dist/request-image-urls.js +27 -0
  110. package/dist/request-key-closure.d.ts +5 -0
  111. package/dist/request-key-closure.js +4 -1
  112. package/dist/runtime-caps-resolver.js +1 -1
  113. package/dist/sandbox-pkg-source.d.ts +28 -1
  114. package/dist/sandbox-pkg-source.js +1 -2
  115. package/dist/sealed-key.d.ts +19 -0
  116. package/dist/sealed-key.js +47 -4
  117. package/dist/session-incarnation.d.ts +16 -5
  118. package/dist/session-incarnation.js +7 -4
  119. package/dist/session-row.d.ts +3 -17
  120. package/dist/session-row.js +4 -0
  121. package/dist/session-sync.d.ts +15 -3
  122. package/dist/session-sync.js +5 -2
  123. package/dist/task-a2a.d.ts +52 -81
  124. package/dist/task-a2a.js +219 -89
  125. package/dist/task-mcp.d.ts +6 -14
  126. package/dist/task-mcp.js +16 -51
  127. package/dist/tool-approval.d.ts +23 -10
  128. package/dist/tool-approval.js +51 -36
  129. package/dist/trace/core-keyset-guard.d.ts +2 -2
  130. package/dist/trace/exit-seat.js +37 -13
  131. package/dist/trace/redact.d.ts +28 -12
  132. package/dist/trace/redact.js +233 -94
  133. package/dist/trace/self-minted-wire.d.ts +267 -0
  134. package/dist/trace/self-minted-wire.js +79 -0
  135. package/dist/url-credential-refusal.d.ts +52 -0
  136. package/dist/url-credential-refusal.js +14 -0
  137. package/dist/url-refusal.d.ts +13 -4
  138. package/dist/url-refusal.js +8 -2
  139. package/dist/workspace-space.d.ts +27 -0
  140. package/dist/workspace-space.js +26 -0
  141. package/package.json +3 -2
  142. package/scripts/drain-check-local.mjs +2 -1
  143. package/scripts/drain-check.mysql.sql +3 -1
  144. package/scripts/drain-check.pg.sql +3 -1
  145. package/skills/schedule.md +1 -1
  146. package/wire-changes/server@7.104.0.json +134 -0
  147. package/wire-changes/server@7.105.0.json +53 -0
  148. package/wire-changes/server@7.106.0.json +475 -0
  149. package/wire-changes/server@7.107.0.json +64 -0
  150. package/wire-changes/server@7.108.0.json +153 -0
  151. package/wire-changes/server@7.109.0.json +46 -0
  152. package/wire-changes/server@7.110.0.json +90 -0
  153. package/wire-changes/server@7.111.0.json +86 -0
  154. package/wire-changes/server@7.112.0.json +60 -0
  155. package/wire-changes/server@7.113.0.json +26 -0
  156. package/wire-changes/server@7.114.0.json +26 -0
  157. package/wire-changes/server@7.115.0.json +97 -0
  158. package/wire-changes/server@7.116.0.json +218 -0
  159. package/wire-changes/server@7.116.1.json +26 -0
  160. package/wire-changes/server@7.117.0.json +65 -0
  161. package/wire-changes/server@7.118.0.json +129 -0
  162. package/wire-changes/server@7.119.0.json +65 -0
  163. package/wire-changes/server@7.120.0.json +89 -0
  164. package/wire-changes/server@7.121.0.json +219 -0
  165. package/wire-changes/server@7.122.0.json +54 -0
  166. package/wire-changes/server@7.123.0.json +26 -0
  167. package/wire-changes/server@7.124.0.json +336 -0
  168. package/wire-changes/server@7.124.1.json +26 -0
  169. package/wire-changes/server@7.125.0.json +111 -0
  170. package/wire-changes/server@7.126.0.json +285 -0
  171. package/wire-changes/server@7.127.0.json +77 -0
  172. package/wire-changes/server@7.127.1.json +26 -0
  173. package/wire-changes/server@7.127.2.json +53 -0
  174. package/wire-changes/server@7.128.0.json +26 -0
  175. package/wire-changes/server@7.129.0.json +106 -0
  176. package/wire-changes/server@7.129.1.json +26 -0
  177. package/wire-changes/server@7.130.0.json +26 -0
  178. package/wire-changes/server@7.131.0.json +118 -0
  179. package/wire-changes/server@7.132.0.json +226 -0
  180. package/wire-changes/server@7.133.0.json +1155 -0
  181. package/wire-changes/server@7.134.0.json +107 -0
  182. package/wire-changes/server@7.135.0.json +174 -0
  183. package/wire-changes/server@7.136.0.json +448 -0
  184. package/dist/url-credential.d.ts +0 -52
  185. package/dist/url-credential.js +0 -49
@@ -1,13 +1,13 @@
1
1
  import { SESSION_RULES_GRAMMAR, compileSessionRuleBucket, isGateKind, readSessionRulesShape, unreadableDenyEntries } from "@sema-agent/core";
2
2
  import { APPROVAL_GATE_KINDS, ASK_USER_QUESTION_TOOL_NAME, isApprovalGateKind } from "../approval-content-kind.js";
3
- import { APPROVAL_CARD_COLUMN_MAX_BYTES, buildCheckpointIndexCells } from "./checkpoint-index-columns.js";
3
+ import { judgeCheckpointIndexCells } from "./checkpoint-index-columns.js";
4
4
  import { dialectProtocolJsonEncoder } from "./sql-driver.js";
5
5
  import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
6
6
  import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
7
7
  import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
8
8
  import { ensureIndexes } from "./ensure-index.js";
9
9
  import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
10
- import { SESSION_META_INCARNATION_COLUMN, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
10
+ import { incarnationFromCell, sessionFaceFromRow } from "../session-incarnation.js";
11
11
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
12
12
  import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
13
13
  import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
@@ -19,7 +19,6 @@ import { MAILBOX_KEYED_TABLES, mailboxKeyDeleteSql, storedMailboxKeyless } from
19
19
  const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
20
20
  export function buildMigrationReceipts() {
21
21
  return {
22
- sessionIncarnationsBackfilled: 0,
23
22
  holderDeathCodeRowsBackfilled: 0,
24
23
  allowRuleShadowRowsMigrated: 0,
25
24
  allowRuleShadowRowsUnreadable: 0,
@@ -49,16 +48,14 @@ export function buildMigrationReceipts() {
49
48
  };
50
49
  }
51
50
  function completedReport(r) {
52
- const { sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn } = r;
53
- if (sessionIncarnationColumn === undefined || workflowParkIndexKey === undefined || sessionRuleColumn === undefined || workflowJournalScopeColumn === undefined) {
54
- throw new Error(`one-time migrations finished without settling a shape reading (M5 ${String(sessionIncarnationColumn)} / M9 ${String(workflowParkIndexKey)} / M17 ${String(sessionRuleColumn)} / M24 ${String(workflowJournalScopeColumn)}) — a step did not record it`);
51
+ const { workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn } = r;
52
+ if (workflowParkIndexKey === undefined || sessionRuleColumn === undefined || workflowJournalScopeColumn === undefined) {
53
+ throw new Error(`one-time migrations finished without settling a shape reading (M9 ${String(workflowParkIndexKey)} / M17 ${String(sessionRuleColumn)} / M24 ${String(workflowJournalScopeColumn)}) — a step did not record it`);
55
54
  }
56
- return { ...r, sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn };
55
+ return { ...r, workflowParkIndexKey, sessionRuleColumn, workflowJournalScopeColumn };
57
56
  }
58
57
  function changedAnything(r) {
59
- return (r.sessionIncarnationColumn === "added" ||
60
- r.sessionIncarnationsBackfilled > 0 ||
61
- r.holderDeathCodeRowsBackfilled > 0 ||
58
+ return (r.holderDeathCodeRowsBackfilled > 0 ||
62
59
  r.allowRuleShadowRowsMigrated > 0 ||
63
60
  r.allowRuleShadowRowsUnreadable > 0 ||
64
61
  r.workflowParkIndexKey === "reshaped" ||
@@ -122,18 +119,6 @@ export class OneTimeMigrationInterruptedError extends Error {
122
119
  }
123
120
  }
124
121
  export const ONE_TIME_MIGRATIONS = [
125
- {
126
- id: "M5",
127
- introducedIn: "7.104.0",
128
- releasedOn: "2026-09-28",
129
- removableAfter: "2026-10-28",
130
- removal: "删 migrateOn 的 M5 步(`session_meta` 在场探针、`incarnation` 列在场探针 + `ADD COLUMN`、空化身回填循环)与 `backfillSessionIncarnations`;" +
131
- "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `sessionIncarnationColumn` / `sessionIncarnationsBackfilled`;**列本身留在中央 DDL 里**(它不是迁移的一部分)",
132
- afterRemoval: "① 列不在的存量库:起服列册探针 `schema-consistency.ts`(S-547)的缺列臂拒启 `config.schema_mismatch`,拒句里的 `ALTER TABLE … ADD COLUMN` " +
133
- "由同一份 DDL 生成(响亮);② 列在但行上仍是空化身(跳过了带 M5 的版本直接升):会话店头读投成活住客(`sessionFaceOfHead`)时对那一行响亮拒 " +
134
- "`session.incarnation_missing`(parked 决定腿因此拒、记忆座退回按 id 绑定并 warn)—— 运维手工给那些行填一只随机令牌即可",
135
- reportKeys: ["sessionIncarnationColumn", "sessionIncarnationsBackfilled"],
136
- },
137
122
  {
138
123
  id: "M6",
139
124
  introducedIn: "7.116.0",
@@ -237,26 +222,13 @@ export const ONE_TIME_MIGRATIONS = [
237
222
  removableAfter: "2026-11-09",
238
223
  removal: "删 `config-lkg.ts` 的 formatVersion 2 读回迁移:`migrateV2Lkg` / `migrateDroppedIdentityRow` / `RETIRED_CATALOG_COORDINATE` 与其上的窗注;`loadLkg` 的 2 臂" +
239
224
  "(版本判据里的 `2` 与 `formatVersion === 2 ? migrateV2Lkg(…)` 那一支 —— 2 回到「unknown formatVersion」拒)、返回型的 `migrated` 位与 JSDoc 里说它的那一行、" +
240
- "文件头注「2 读回时一次性迁移」那一句;`boot/web-admin.ts` 读 `read.migrated` 打 `config_lkg_migrated` 的那一行(formatVersion 1 迁移的同名事件那一行留);" +
241
- "`migrateV1Lkg` 里不带 v1 的 etag 那一句旁注对 `migrateV2Lkg` 的引用(不带 etag 的规则本身留 —— v1 载荷同样没有完整的被丢条目记录);契约 §A.1d " +
225
+ "文件头注「2 读回时一次性迁移」那一句;`boot/web-admin.ts` 读 `read.migrated` 打 `config_lkg_migrated` 的那一行(这一行与 M15 / M22 的读回迁移共用 —— 三条都删完才整段删,先删 M13 时只去掉信封 2 → 3 那一路的 `migrated` 来源);契约 §A.1d " +
242
226
  "`droppedEntries` 一节「升级窗(如实)」那一条与 §A.1d `[config.model_refused_name_minted]` 那一条里「LKG 载体 `droppedIdentities` 读回时一次性迁移」那半句;手写 formatVersion 2 信封的测试夹具" +
243
227
  "(`test/config-lkg.test.ts`、`test/config-hot/batch1-version-ordinal.test.ts` 改写成 3;`test/models-dropped-entries-s1295.test.ts` 的旧 LKG 迁移格改钉 2 读回不可用)",
244
228
  afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 2 答「unknown formatVersion 2」):2 读回 ⇒ 不可用,中心可达照拉(整拉,读腿铸出完整载体,新 LKG 按 3 覆盖)、" +
245
229
  "断着则 env 兜底,下一份干净候选重写 —— 与回滚到 7.122.0 读 3 同一种处置",
246
230
  reportKeys: [],
247
231
  },
248
- {
249
- id: "M14",
250
- introducedIn: "7.99.0",
251
- releasedOn: "2026-09-24",
252
- removableAfter: "2026-10-24",
253
- removal: "删 `config-lkg.ts` 的 formatVersion 1 迁移:`legacyV1LkgPath` / `migrateV1Lkg` 与其上的窗注(`loadLkg` 的 1 臂今天已是拒句,不动);" +
254
- "`boot/web-admin.ts` `bootFromLkg` 里调用 `migrateV1Lkg` 的那一段(含它打 `config_lkg_migrated` / 推不出身份时响亮拒的那两行);" +
255
- "`migrateV1Lkg` 的测试格(`test/config-lkg.test.ts` v1 迁移组)",
256
- afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 1 答「pre-identity LKG (formatVersion 1) — not read back …」):旧默认路径上的 v1 文件不再被迁进箱、" +
257
- "原样留在盘上;中心可达照拉(整拉,新 LKG 按现行箱名落盘)、断着则 env 兜底",
258
- reportKeys: [],
259
- },
260
232
  {
261
233
  id: "M15",
262
234
  introducedIn: "7.124.0",
@@ -322,10 +294,15 @@ export const ONE_TIME_MIGRATIONS = [
322
294
  releasedOn: "2026-10-16",
323
295
  removableAfter: "2026-11-15",
324
296
  removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `readA2aToolAxesAsServer7131M19` / `StoredToolAxesRereadM19`);" +
325
- "`boot/resolve-spec.ts` 里两条请求腿共用的那一行(`const storedToolAxes = opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)、递给 `resolveRequestMcp` / `resolveRequestA2a` 的两处实参与它的 import;" +
297
+ "`boot/resolve-spec.ts` 里两条请求腿共用的那一行(`const storedToolAxes = opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`,在腿首、A2A 快照之前求值)、" +
298
+ "`snapshotRequestA2a(…, { reread: storedToolAxes !== undefined })` 的第二实参、`parts` 里的 `storedToolAxes`(类型与解构)、递给 `resolveRequestMcp` / `resolveRequestA2a` 的两处实参与它的 import;" +
326
299
  "`task-mcp.ts` 里 `resolveRequestMcp` / `classifyRequestMcp` / `classifyHonored` / `validateRequestMcpEntries` / `normalizeServer` 的 `storedToolAxes` 形参、" +
327
300
  "`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import(升级前读法的词表与两只谓词住在本模块里,随它一起删);" +
328
- "`task-a2a.ts` 里 `resolveRequestA2a` / `validateRequestA2aEntries` / `normalizeA2aPeer` 的 `storedToolAxes` 形参、`toolAxesReread` 事实与运维行 `a2a_tool_axes_read_as_before_upgrade`、对本模块的型 import;" +
301
+ "`task-a2a.ts` 里 `resolveRequestA2a` / `validateRequestA2aEntries` / `normalizeA2aPeer` 的 `storedToolAxes` 形参、`toolAxesReread` 事实与运维行 `a2a_tool_axes_read_as_before_upgrade`、对本模块的型 import," +
302
+ "以及只为重读座存在的克隆机器的全部名字:`cloneDeclaration` / `DeclarationClone` / `CloneFrame` / `openCloneNode` / `putCloneMember`、快照类型 `PeerSnapshot` 的字段 `toolAxesDeclared` 与 `mintPeerSnapshot` / `snapshotPeer` / `snapshotRequestA2a` 的 `reread` 形参、`toolAxesNotCloneableIssue`、读器里 `declared` 那一臂(`node:util` 的 `types` 与 `errorKindOf` 的 import 若再无别的读者一并删);" +
303
+ "随克隆一起退役的两句例外文字:`USAGE.md` 里 `a2a_injection_dropped` 条目 `reason` 那句「只有一臂例外 … `declaration not cloneable (<because>) — …`」与 `task-a2a.ts` 里 `A2aInjectionDrop.reason` 注的同一句例外(删后 `reason` 恒与 400 拒句同一句,回到「逐字同一句」不带例外);" +
304
+ "`test/task-a2a.test.ts` 里的克隆钉(标题去掉开头的 `🔴 ` 后以 `克隆钉:` 开头的各格,整格删;按这个精确前缀删,不按标题里的「M19 重读」之类泛词删 —— 那会误删同文件里的通用不变式钉),以及三格不带前缀、删码后要改写的钉:`需要「读」的两个键…` 那格的 `readRequestToolAxes` 调用方名单(`normalizeA2aPeer` 只在 M19 重读臂里调它,删后名单只剩 `mintPeerSnapshot`,标题里「与读器里 M19 重读产物的那一次」半句同删)、`读器与门的差别只有三格…` 改成两格(删 ③ 的注释行与标题里的 ③)、`请求入口快照的形…` 里 `snapshotRequestA2a(…, { reread: false })` 的第二实参(删 `reread` 形参后改成单参);" +
305
+ "`test/one-time-migration-window-gate.test.ts` 里 M19 一节中与克隆有关的三格(声明抽取器正负控 · 删法条与克隆名字两向对账 · 克隆钉标题前缀对账)与它们共用的帮手 `declaredNames`;" +
329
306
  "`test/mcp-tool-axes-m19.test.ts`(窗关那一格 —— 存量坏形整条不进、warn 与通告点名 `field: toolAxes` —— 改钉到 `test/task-mcp.test.ts`);" +
330
307
  "本文件的 `migrationWindowOpen`(若再无别的读者)",
331
308
  afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里引擎判官判不过的 `toolAxes`,续跑重放时这一条 server 整条不进," +
@@ -463,7 +440,7 @@ export const ONE_TIME_MIGRATIONS = [
463
440
  introducedIn: "7.135.0",
464
441
  releasedOn: "2026-10-22",
465
442
  removableAfter: "2026-11-21",
466
- removal: "删 migrateOn 的 M29 步(`backfillParkedApprovalCards`:列在场探针、pending 候选分页扫描、整只文档读 + 同一只派生 + CAS 写列)与 `PARKED_CARD_BACKFILL_PAGE` / `readM29Document` / `m29CardOf`;" +
443
+ removal: "删 migrateOn 的 M29 步(`backfillParkedApprovalCards`:列在场探针、pending 候选分页扫描、整只文档读 + 写口那一只判官 + CAS 写列)与 `PARKED_CARD_BACKFILL_PAGE` / `readM29Document` / `m29Judge`;" +
467
444
  "报告、日志行(含逐行的 `parked_approval_card_row_unreadable` / `parked_approval_card_row_too_long`)与 `NO_SQL_MIGRATIONS` 的 `parkedApprovalCardsBackfilled` / `parkedApprovalCardRowsUnreadable` / `parkedApprovalCardRowsTooLong`;`test/one-time-migrations.test.ts` 的 M29 组与 " +
468
445
  "`test/checkpoint-index-columns-db-integration.test.ts` 的 M29 格。**不随本条删**:`approval_card` 列、写口的派生、到期 facet 的生成谓词(常设)",
469
446
  afterRemoval: "没有新的臂(如实):跳过了带本条的版本直接升的库,升级前停驻、升级后仍挂着的行卡列是 NULL —— 生成谓词读门型的 facet(审批门 ⇒ SLA 拒扫腿),问答行到期被拒扫" +
@@ -475,8 +452,6 @@ export function migrationWindowOpen(window, today) {
475
452
  return window.removableAfter === null || today <= window.removableAfter;
476
453
  }
477
454
  export const NO_SQL_MIGRATIONS = {
478
- sessionIncarnationColumn: "lazy-on-read",
479
- sessionIncarnationsBackfilled: 0,
480
455
  allowRuleShadowRowsMigrated: 0,
481
456
  allowRuleShadowRowsUnreadable: 0,
482
457
  workflowParkIndexKey: "file-ledger",
@@ -592,7 +567,6 @@ async function migrateOn(db, dialect) {
592
567
  return completedReport(receipts);
593
568
  }
594
569
  async function migrateStepsOn(db, dialect, receipts) {
595
- await migrateSessionIncarnations(db, dialect, receipts);
596
570
  receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
597
571
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
598
572
  : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR]);
@@ -1113,25 +1087,32 @@ export async function backfillParkedApprovalCards(db, dialect, receipts) {
1113
1087
  const [full] = await db.rows(`SELECT session_id, ${jsonColumnText(dialect, "checkpoint")} FROM checkpoint WHERE token = ${ph(1)} AND status = 'pending'`, [token]);
1114
1088
  if (full === undefined)
1115
1089
  continue;
1116
- const reading = m29CardOf(full.checkpoint);
1117
- if (reading.kind === "unreadable") {
1118
- receipts.parkedApprovalCardRowsUnreadable.push({ sessionId: String(full.session_id) });
1119
- continue;
1120
- }
1121
- if (reading.card === null)
1122
- continue;
1123
- const bytes = Buffer.byteLength(reading.card, "utf8");
1124
- if (bytes > APPROVAL_CARD_COLUMN_MAX_BYTES[dialect]) {
1125
- receipts.parkedApprovalCardRowsTooLong.push({ sessionId: String(full.session_id), bytes });
1126
- continue;
1090
+ const reading = m29Judge(full.checkpoint, dialect);
1091
+ switch (reading.kind) {
1092
+ case "unreadable":
1093
+ receipts.parkedApprovalCardRowsUnreadable.push({ sessionId: String(full.session_id) });
1094
+ continue;
1095
+ case "card_too_long":
1096
+ receipts.parkedApprovalCardRowsTooLong.push({ sessionId: String(full.session_id), bytes: reading.bytes });
1097
+ continue;
1098
+ case "cells": {
1099
+ const card = reading.cells.approval_card;
1100
+ if (card === null)
1101
+ continue;
1102
+ receipts.parkedApprovalCardsBackfilled += await db.exec(`UPDATE checkpoint SET approval_card = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND approval_card IS NULL`, [card, token]);
1103
+ continue;
1104
+ }
1105
+ default: {
1106
+ const unhandled = reading;
1107
+ throw new Error(`M29: unhandled judgement ${JSON.stringify(unhandled)}`);
1108
+ }
1127
1109
  }
1128
- receipts.parkedApprovalCardsBackfilled += await db.exec(`UPDATE checkpoint SET approval_card = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND approval_card IS NULL`, [reading.card, token]);
1129
1110
  }
1130
1111
  if (page.length < PARKED_CARD_BACKFILL_PAGE)
1131
1112
  return;
1132
1113
  }
1133
1114
  }
1134
- function m29CardOf(cell) {
1115
+ function m29Judge(cell, dialect) {
1135
1116
  let doc;
1136
1117
  try {
1137
1118
  doc = decodeJsonColumn(cell, readM29Document, "checkpoint.checkpoint (M29 approval-card backfill)");
@@ -1141,11 +1122,7 @@ function m29CardOf(cell) {
1141
1122
  throw e;
1142
1123
  return { kind: "unreadable" };
1143
1124
  }
1144
- const gate = doc.gate;
1145
- const pendingAction = doc.pendingAction;
1146
- if (typeof gate !== "object" || gate === null || typeof pendingAction !== "object" || pendingAction === null)
1147
- return { kind: "unreadable" };
1148
- return { kind: "card", card: buildCheckpointIndexCells(doc).approval_card };
1125
+ return judgeCheckpointIndexCells(doc, dialect);
1149
1126
  }
1150
1127
  const PLACED_OWNER_SCAN_PAGE = 500;
1151
1128
  function placedOwnerScanSql(dialect) {
@@ -1207,68 +1184,8 @@ export async function inheritPlacedSessionOwners(db, dialect, receipts) {
1207
1184
  return;
1208
1185
  }
1209
1186
  }
1210
- const INCARNATION_BACKFILL_BATCH = 500;
1211
- async function migrateSessionIncarnations(db, dialect, receipts) {
1212
- if (!(await tableExists(db, dialect, "session_meta"))) {
1213
- receipts.sessionIncarnationColumn = "no-table";
1214
- return;
1215
- }
1216
- const col = SESSION_META_INCARNATION_COLUMN;
1217
- let column = "present";
1218
- if (!(await columnExists(db, dialect, "session_meta", col.name))) {
1219
- try {
1220
- await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
1221
- column = "added";
1222
- }
1223
- catch (e) {
1224
- if (!(await columnExists(db, dialect, "session_meta", col.name)))
1225
- throw e;
1226
- }
1227
- }
1228
- receipts.sessionIncarnationColumn = column;
1229
- await backfillSessionIncarnations(db, dialect, receipts);
1230
- }
1231
- async function backfillSessionIncarnations(db, dialect, receipts) {
1232
- const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
1233
- const initial = await remaining();
1234
- if (initial === 0)
1235
- return;
1236
- const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
1237
- let written = 0;
1238
- let after;
1239
- for (let round = 0; round < ceiling; round++) {
1240
- const ids = (after === undefined
1241
- ? await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`)
1242
- : await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' AND session_id > ${dialect === "pg" ? "$1" : "?"} ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`, [after])).map((r) => String(r.session_id));
1243
- if (ids.length === 0) {
1244
- if (after === undefined)
1245
- return;
1246
- after = undefined;
1247
- continue;
1248
- }
1249
- const params = [];
1250
- const bind = (v) => {
1251
- params.push(v);
1252
- return dialect === "pg" ? `$${params.length}` : "?";
1253
- };
1254
- const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
1255
- const inList = ids.map((id) => bind(id)).join(", ");
1256
- const landed = await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
1257
- written += landed;
1258
- receipts.sessionIncarnationsBackfilled += landed;
1259
- after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
1260
- }
1261
- const left = await remaining();
1262
- if (left > 0) {
1263
- throw new Error(`one-time migration M5: ${left} session_meta row(s) still carry an empty incarnation after ${ceiling} backfill round(s) ` +
1264
- `(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
1265
- `if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
1266
- }
1267
- }
1268
1187
  function readingFields(r) {
1269
1188
  return {
1270
- ...(r.sessionIncarnationColumn !== undefined ? { sessionIncarnationColumn: r.sessionIncarnationColumn } : {}),
1271
- sessionIncarnationsBackfilled: r.sessionIncarnationsBackfilled,
1272
1189
  holderDeathCodeRowsBackfilled: r.holderDeathCodeRowsBackfilled,
1273
1190
  allowRuleShadowRowsMigrated: r.allowRuleShadowRowsMigrated,
1274
1191
  allowRuleShadowRowsUnreadable: r.allowRuleShadowRowsUnreadable,
@@ -1305,8 +1222,7 @@ export function logOneTimeMigrations(log, backend, report) {
1305
1222
  backend,
1306
1223
  ...readingFields(report),
1307
1224
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
1308
- note: "one-time persisted-data migrations (M5: the " +
1309
- "session incarnation column added and every session row without one minted its own token; M6: run rows failed by a " +
1225
+ note: "one-time persisted-data migrations (M6: run rows failed by a " +
1310
1226
  "holder-death verdict written by an earlier version with only the sentence got the machine-readable code run.stalled / " +
1311
1227
  "run.orphaned; M7 writes nothing — it is the window of the refusal that keeps a suspended task whose stored allow list was written " +
1312
1228
  "under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
@@ -1463,7 +1379,7 @@ function logOneTimeMigrationsInterrupted(log, backend, error) {
1463
1379
  ...readingFields(error.receipts),
1464
1380
  note: "the one-time migrations failed after committing changes: the readings on this line and the lines that follow are what this boot already committed — a retried " +
1465
1381
  "boot finds that work done and does not report it again, so these lines are the only record of them, written once by this boot — save them. The failure itself follows " +
1466
- "as this boot's refusal. A shape reading absent from this line (sessionIncarnationColumn / workflowParkIndexKey / sessionRuleColumn / workflowJournalScopeColumn) belongs to a step that did not " +
1382
+ "as this boot's refusal. A shape reading absent from this line (workflowParkIndexKey / sessionRuleColumn / workflowJournalScopeColumn) belongs to a step that did not " +
1467
1383
  "finish; sessionRuleColumn absent after session_rule_row_old_grammar_entries lines means the column still carries its in-progress name — the next boot rewrites the " +
1468
1384
  "remaining rows (rows already rewritten are left alone) and publishes the final name",
1469
1385
  });
@@ -110,7 +110,7 @@ export interface K8sEnvConfig extends LaneHomeConfig {
110
110
  cacheMisses?: CacheMissMemory;
111
111
  /** Operator log seat (the e2b adapter's shape): `workspace_snapshot_failed` / `workspace_snapshot_prune_failed` /
112
112
  * `workspace_space_unavailable` / `workspace_space_unreadable_occupant` / `workspace_commit_abandoned` / `workspace_latest_dangling` /
113
- * `workspace_restore_failed` / `workspace_cache_fresh` / `workspace_cache_unavailable`. */
113
+ * `workspace_restore_failed` / `workspace_snapshot_bucket_missing` / `workspace_cache_fresh` / `workspace_cache_unavailable`. */
114
114
  logger?: {
115
115
  warn(event: string, fields?: Record<string, unknown>): void;
116
116
  };
@@ -311,8 +311,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
311
311
  * Connect = the workspace SPACE's restore point. Space lane open ⇒ first wait out a commit still in flight
312
312
  * ({@link awaitSpaceCommit}: the run store's commit row), then read `<prefix>/<incarnation>/space/latest`: present ⇒
313
313
  * restore it through {@link restoreSnapshot} (the one tar download, shared with `resumeVM`; an archive that is gone ⇒
314
- * loud `workspace_latest_dangling`, the pointer deleted, an empty workspace); absent (404) ⇒ a fresh pod; a read that
315
- * FAILS ⇒ a loud `connect_failed` — never a silent fresh workspace (a run that believes it resumed while it did not is
314
+ * loud `workspace_latest_dangling`, the pointer deleted, an empty workspace); absent (404 — the key, with the bucket there) ⇒ a fresh pod; a read that
315
+ * 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
316
316
  * the worst form). Once the pod holds the workspace, this env arms its fence seat (its destroy will commit).
317
317
  * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but no such session (the occupant absent) ⇒ a
318
318
  * fresh pod and one `workspace_space_unavailable` line (no restore by any other key). The occupant read FAULTED ⇒ the same
@@ -425,10 +425,13 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
425
425
  * charset-validated upstream, so each path segment is allowlisted here (`^[A-Za-z0-9][A-Za-z0-9._-]*$`) before it enters
426
426
  * the object key: a segment of `..` / `../x` / one containing `/` would otherwise let one tenant's snapshot key normalize into
427
427
  * another tenant's prefix (cross-tenant read on resume / clobber on suspend). The space partition (a minted v4 uuid) goes
428
- * through the same one allowlist. Returns a typed error instead of signing a traversing key. Folds the old
428
+ * through the same one allowlist ({@link SNAPSHOT_KEY_SEGMENT}); the key prefix is a `/`-separated path of the same segments
429
+ * ({@link snapshotKeyPrefixProblem}, the rule the config loader refuses boot with). Returns a typed error instead of signing a traversing key. Folds the old
429
430
  * snapshotKey()+s3Presign() pair so the two call sites can't diverge.
430
431
  */
431
432
  private presignSnapshot;
433
+ /** The key prefix of every snapshot object this env signs — the ONE reading of `keyPrefix` (unset ⇒ the default); its form is judged in {@link presignSnapshot}. */
434
+ private snapshotKeyPrefix;
432
435
  /** A lane's partition prefix inside the bucket — the ONE place it is spelled (`presignSnapshot` signs under it; the reclaim reads the listing against it).
433
436
  * The partition segment is validated by `presignSnapshot` before a URL is signed. */
434
437
  private snapshotBase;
@@ -456,9 +459,19 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
456
459
  /** Worker-side object-store request, bounded by the control-plane RPC wall-clock — or a tighter caller deadline — and
457
460
  * the caller's abort. */
458
461
  private storeFetch;
459
- /** Read one space pointer object: its trimmed body, or `undefined` = absent (404); any other failure is a typed
462
+ /** Read one space pointer object: its trimmed body, or `undefined` = absent (404 — the bucket is there, the key is not); a 404 that says
463
+ * `NoSuchBucket` is NOT absence — the bucket itself is missing, and the run is refused ({@link refuseMissingBucket}); any other failure is a typed
460
464
  * `connect_failed` naming the pointer. */
461
465
  private readPointer;
466
+ /**
467
+ * The store answered the `latest` read with `404 NoSuchBucket`: the workspace snapshot bucket does not exist, so this space can be neither restored
468
+ * nor saved — a run started on it would work on an empty /workspace and lose everything it wrote. The env's refusal ({@link holdRefusal}), in the one
469
+ * space-read failure form ({@link spaceReadFailed}), naming the bucket and the knob that picks it; one `workspace_snapshot_bucket_missing{space, bucket, code, effect}`
470
+ * line per env (the refusal stands until destroy: later connects and operations answer it without another read or line; a retry of the run builds a fresh env, which
471
+ * asks the store afresh — a bucket created in between is found). The boot probe (`snapshot-bucket-presence.ts`) refuses a boot where the bucket is already missing;
472
+ * this is the same judgement ({@link isNoSuchBucketAnswer}) for a bucket that went missing afterwards or that the probe could not prove either way.
473
+ */
474
+ private refuseMissingBucket;
462
475
  /** Read the space's `latest` pointer. The body's shape is checked where it is used — restoreSnapshot → presignSnapshot
463
476
  * (the one segment check). */
464
477
  private readLatest;
@@ -6,7 +6,9 @@ import { StringDecoder } from "node:string_decoder";
6
6
  import WebSocket from "ws";
7
7
  import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
8
8
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
9
- import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotNameOwnedBy } from "../workspace-space.js";
9
+ import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
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";
10
12
  import { spaceCommitAlive } from "../workspace-space-commit.js";
11
13
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
12
14
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
@@ -703,21 +705,20 @@ export class RemoteK8sExecutionEnv {
703
705
  }
704
706
  presignSnapshot(req) {
705
707
  const s3 = this.cfg.s3Snapshot;
706
- const prefix = s3.keyPrefix ?? "kata";
707
708
  const [label, partition] = req.lane === "space" ? ["spaceIncarnation", req.space.incarnation] : ["sessionId", this.cfg.sessionId ?? "_"];
708
709
  const snapshotId = "snapshotId" in req ? req.snapshotId : undefined;
709
710
  const objectName = "object" in req ? req.object : undefined;
710
- const safe = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
711
711
  const segments = [
712
712
  [label, partition],
713
713
  ...(snapshotId !== undefined ? [["snapshotId", snapshotId]] : []),
714
714
  ...(objectName !== undefined ? [["object", objectName]] : []),
715
- ["keyPrefix", prefix],
716
715
  ];
717
716
  for (const [label, seg] of segments) {
718
- if (!safe.test(seg))
717
+ if (!SNAPSHOT_KEY_SEGMENT.test(seg))
719
718
  return { ok: false, error: new RemoteExecutionError("connect_failed", `unsafe snapshot key segment (${label}): refusing to build a traversing object key`) };
720
719
  }
720
+ if (snapshotKeyPrefixProblem(this.snapshotKeyPrefix()) !== undefined)
721
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "unsafe snapshot key segment (keyPrefix): refusing to build a traversing object key") };
721
722
  const common = {
722
723
  endpoint: s3.endpoint,
723
724
  bucket: s3.bucket,
@@ -732,8 +733,11 @@ export class RemoteK8sExecutionEnv {
732
733
  const object = "pointer" in req ? req.pointer : "object" in req ? req.object : `${req.snapshotId}.tar.gz`;
733
734
  return { ok: true, value: presignS3Url({ ...common, key: `${base}${object}`, method: req.method }) };
734
735
  }
736
+ snapshotKeyPrefix() {
737
+ return this.cfg.s3Snapshot?.keyPrefix ?? WORKSPACE_SNAPSHOT_PREFIX_DEFAULT;
738
+ }
735
739
  snapshotBase(req) {
736
- const prefix = this.cfg.s3Snapshot.keyPrefix ?? "kata";
740
+ const prefix = this.snapshotKeyPrefix();
737
741
  return req.lane === "space" ? `${prefix}/${req.space.incarnation}/space/` : `${prefix}/${this.cfg.sessionId ?? "_"}/`;
738
742
  }
739
743
  async reclaimSpaceSnapshots(spaceKey, deadline) {
@@ -825,12 +829,25 @@ export class RemoteK8sExecutionEnv {
825
829
  catch (e) {
826
830
  return spaceReadFailed(what, `: ${this.redactUrl(errorMessageOf(e), url.value)}`, e);
827
831
  }
832
+ if (isNoSuchBucketAnswer(res.status, body))
833
+ return this.refuseMissingBucket(space);
828
834
  if (res.status === 404)
829
835
  return ok(undefined);
830
836
  if (!res.ok)
831
837
  return spaceReadFailed(what, ` (HTTP ${res.status})`);
832
838
  return ok(body.trim());
833
839
  }
840
+ refuseMissingBucket(space) {
841
+ const bucket = this.cfg.s3Snapshot.bucket;
842
+ this.spaceLine("workspace_snapshot_bucket_missing", space, {
843
+ bucket,
844
+ code: SNAPSHOT_BUCKET_MISSING,
845
+ effect: "the workspace snapshot bucket does not exist in the object store (the server never creates buckets) — this run is refused rather than started on a workspace that could be neither restored nor saved",
846
+ });
847
+ return this.holdRefusal(spaceReadFailed("pointer", ` (${SNAPSHOT_BUCKET_MISSING}): the workspace snapshot bucket "${bucket}" (WORKSPACE_SNAPSHOT_BUCKET; default ${WORKSPACE_SNAPSHOT_BUCKET_DEFAULT}) does not exist in the object store — ` +
848
+ `refusing to start this run on a workspace that could be neither restored nor saved. Ways out: create the bucket, or set WORKSPACE_SNAPSHOT_BUCKET to one that exists, ` +
849
+ "or set WORKSPACE_PERSIST=off for a deployment that keeps no workspace between runs").error);
850
+ }
834
851
  async readLatest(space, signal) {
835
852
  const r = await this.readPointer(space, "latest", signal);
836
853
  return r.ok ? ok(r.value === undefined ? undefined : r.value) : r;
@@ -1045,7 +1062,7 @@ export class RemoteK8sExecutionEnv {
1045
1062
  });
1046
1063
  }
1047
1064
  async dropDanglingLatest(space, snapshotId) {
1048
- const prefix = this.cfg.s3Snapshot?.keyPrefix ?? "kata";
1065
+ const prefix = this.snapshotKeyPrefix();
1049
1066
  const dropped = await this.writePointer(space, "latest", null);
1050
1067
  this.spaceLine("workspace_latest_dangling", space, {
1051
1068
  key: `${prefix}/${space.incarnation}/space/${snapshotId}.tar.gz`,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How this server reads an S3-compatible store's ANSWER — the one place a "the bucket itself is not there" reading is spelled.
3
+ *
4
+ * A 404 from an object store means two different things: `NoSuchKey` (the bucket is there, this object is not — a space's first run,
5
+ * a pointer never written) and `NoSuchBucket` (the bucket was never created — a deployment error no run can fix by retrying). Reading
6
+ * the second as the first turns a missing bucket into a silently empty workspace on every run. The ONLY evidence of a missing bucket is
7
+ * a 404 whose body carries the S3 error code `NoSuchBucket`: a bare 404, a 404 from a proxy or a wrong path, a 403 (credentials that
8
+ * cannot list ≠ no bucket) and a 5xx prove nothing about the bucket. Every consumer asks this one predicate: the boot probe of a
9
+ * configured bucket (`MinioBlobBackend.probeBucket`) and the runtime read of the workspace `latest` pointer (`remote-env-k8s.ts`).
10
+ */
11
+ /** The code a run's refusal / operator line carries when the store answered "no such bucket" (the `workspace_snapshot_bucket_missing` line's `code`). */
12
+ export declare const SNAPSHOT_BUCKET_MISSING = "snapshot_bucket_missing";
13
+ /** Does this answer prove the bucket is missing? `status` = the HTTP status, `body` = the answer's text (an S3 error document). */
14
+ export declare function isNoSuchBucketAnswer(status: number, body: string): boolean;
15
+ //# sourceMappingURL=s3-answer.d.ts.map
@@ -0,0 +1,5 @@
1
+ export const SNAPSHOT_BUCKET_MISSING = "snapshot_bucket_missing";
2
+ export function isNoSuchBucketAnswer(status, body) {
3
+ return status === 404 && /<Code>NoSuchBucket<\/Code>/.test(body);
4
+ }
5
+ //# sourceMappingURL=s3-answer.js.map
@@ -5,6 +5,8 @@
5
5
  * `SESSION_SNAPSHOT_BUCKET`(缺省 `session-snapshots`)是 snapshot-BLOB offload 与 SendUserFile 私有 presigned 轨 / 沙箱 staging 的桶。server 不建桶
6
6
  * (`docs/DEPLOY-PREREQS.md`「对象存储桶」),sema-up 的建桶清单又漏过它 ⇒ 起服绿,之后每一次对象写、每一拍孤儿 sweep 的列举(每分钟)都是
7
7
  * 404 NoSuchBucket —— 运维读到的是一串「失败的请求」,不是「桶没建」。
8
+ * `WORKSPACE_SNAPSHOT_BUCKET`(缺省 `workspaces`,k8s 工作区快照桶)同病且更隐:起服不核它 ⇒ 缺桶 / 无权限时每条 run 读 `latest` 得 404,
9
+ * 被读成「这个空间头一回」,从空目录起;run 末的保存虽失败,报的却是一条不点名桶的 curl 404,没有一行说「桶没建」。
8
10
  *
9
11
  * ## 形(一条规则,不加特例)
10
12
  * · 核的是 {@link MinioBlobBackend.probeBucket}(presigned 列举 `max-keys=1`);**只有** 404 + `NoSuchBucket` 是「缺」,其余证明不了的形(连不上 / 403 / 5xx / 没有
@@ -13,24 +15,57 @@
13
15
  * · 缺 = 配置好的店资源还没就绪,与「库连不上」同一类处境:在 `STORE_LOCK_WAIT_MS` 窗内重试({@link openWithinWait},同一只窗、同一只节拍、同一条窗尾规则 —— 编排里
14
16
  * 建桶晚于 server 起是常态),到窗时手上有缺桶的证据 ⇒ 拒启,拒句点名桶名、`MINIO_ENDPOINT` 的显示安全形(`buildDisplaySafeUrl` 的产物:userinfo 落地、
15
17
  * query 值成记号,不带密钥)与出路;探针从头到尾没答过(每一次都挂到窗尾)⇒ 同样是「证明不了」,走上面那只兜底口、照常起服。
16
- * · 只核**快照桶**(`config.snapshotBlobStore.bucket`;SendUserFile 的私桶缺省同一只);公桶 `sema-public` 是运维另配的匿名桶,不在本核之内。
18
+ * · 核的桶是一张角色表({@link SNAPSHOT_BUCKET_ROLES},闭集两行)里的两只,**同一只探针、同一条规则、同一只兜底口**:
19
+ * `session` = `config.snapshotBlobStore.bucket`(`SESSION_SNAPSHOT_BUCKET`;SendUserFile 的私桶缺省同一只);
20
+ * `workspace` = k8s 车道的 `config.remoteExec.s3Snapshot.bucket`(`WORKSPACE_SNAPSHOT_BUCKET`,缺省 `workspaces`)—— 车道没开 / 没配对象存储 ⇒ 没有这只桶,不核。
21
+ * 拒句的旋钮名跟着桶走(缺的是哪只桶就只点哪只的旋钮)。公桶 `sema-public` 是运维另配的匿名桶,不在本核之内。
22
+ * · 运行期的对应臂:工作区 `latest` 指针读答 404 `NoSuchBucket`(同一条「桶不在」判据 `isNoSuchBucketAnswer`)⇒ 那条 run 响亮拒绝,不再读成「空间头一回」
23
+ * (`remote-env-k8s.ts` `readPointer`)—— 起服核之后桶被删 / 起服核证明不了的部署,在运行期也有一道。
17
24
  */
18
25
  import type { ServiceConfig } from "../config-types.js";
19
26
  import { type StoreOpenWaitClock } from "./store-open-wait.js";
20
27
  type SnapshotBlobStoreConfig = NonNullable<ServiceConfig["snapshotBlobStore"]>;
28
+ /** 被核的桶的角色(闭集):`session` = 快照 blob / SendUserFile 私桶;`workspace` = k8s 工作区快照桶。 */
29
+ export type SnapshotBucketRole = "session" | "workspace";
30
+ /** 两只被核的桶的角色表 —— 拒句、`snapshot_bucket_wait` 披露与 {@link ensureSnapshotBuckets} 都读这一张(缺一行 = 编译错)。 */
31
+ export declare const SNAPSHOT_BUCKET_ROLES: {
32
+ readonly session: {
33
+ readonly noun: "snapshot bucket";
34
+ readonly knob: "SESSION_SNAPSHOT_BUCKET";
35
+ readonly defaultName: "session-snapshots";
36
+ readonly consequence: "every snapshot-offload write and SendUserFile private-track request would answer 404 NoSuchBucket (and the orphan sweep would log one every minute, forever)";
37
+ readonly createHint: "; this version's sema-up scripts create it — re-run them on an existing sema-up deployment";
38
+ };
39
+ readonly workspace: {
40
+ readonly noun: "workspace snapshot bucket";
41
+ readonly knob: "WORKSPACE_SNAPSHOT_BUCKET";
42
+ readonly defaultName: "workspaces";
43
+ readonly consequence: "no workspace snapshot could be saved or restored (every run would start from an empty /workspace and every run-end save would fail on a bare 404), and no line would say the bucket is missing";
44
+ readonly createHint: "";
45
+ };
46
+ };
21
47
  /** boot 期单次探针的上界(ms):黑洞端点(丢包不拒连)不许把起服拖到对象存储 IO 的 120s;到点 ⇒ `unknown`(证明不了缺桶 ⇒ 不拒、报一次)。 */
22
48
  export declare const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10000;
23
49
  /** 缺桶的拒启句(唯一铸点)。 */
24
50
  /** @param endpoint `MINIO_ENDPOINT` 的**显示安全形**(调用方过 {@link buildDisplaySafeUrl};本函数不碰原串)。 */
25
- export declare function buildSnapshotBucketRefusal(bucket: string, endpoint: string, waitedMs: number): string;
51
+ export declare function buildSnapshotBucketRefusal(bucket: string, endpoint: string, waitedMs: number, role?: SnapshotBucketRole): string;
26
52
  /** 探针到窗尾一次都没答过时,兜底口 detail 里的成因(证明不了缺桶的一形)。 */
27
53
  export declare const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
28
54
  /**
29
- * boot 期核一次快照桶(`boot/stores.ts`)。缺桶 ⇒ 窗内重试、到窗拒启;证明不了 ⇒ 兜底口留痕、照常起服。
55
+ * boot 期核一次**一只**快照桶。缺桶 ⇒ 窗内重试、到窗拒启;证明不了 ⇒ 兜底口留痕、照常起服。
30
56
  * @param waitMs `config.storeLockWaitMs`。`0` ⇒ 只核一次(缺就当场拒)。
57
+ * @param role 这只桶的角色({@link SNAPSHOT_BUCKET_ROLES}):决定拒句点名哪根旋钮;缺省 `session`。
31
58
  */
32
59
  export declare function ensureSnapshotBucket(store: SnapshotBlobStoreConfig, waitMs: number, logger: {
33
60
  warn(msg: string, meta?: Record<string, unknown>): void;
34
- }, clock?: StoreOpenWaitClock): Promise<void>;
61
+ }, clock?: StoreOpenWaitClock, role?: SnapshotBucketRole): Promise<void>;
62
+ /**
63
+ * boot 期核**这份配置要用的全部**快照桶(`boot/stores.ts`,排在打开任何店之前):角色表的每一行,配置里有这只桶才核 —— 会话快照桶 ⇐ 配了对象存储
64
+ * (`snapshotBlobStore`);工作区快照桶 ⇐ k8s 车道配了对象存储(`remoteExec.s3Snapshot`)。依角色表顺序逐只核,每只各有自己的 `STORE_LOCK_WAIT_MS` 窗
65
+ * (缺的桶才占窗;在场的桶一次探针就过),第一只缺的桶即拒启。
66
+ */
67
+ export declare function ensureSnapshotBuckets(config: Pick<ServiceConfig, "snapshotBlobStore" | "remoteExec" | "storeLockWaitMs">, logger: {
68
+ warn(msg: string, meta?: Record<string, unknown>): void;
69
+ }): Promise<void>;
35
70
  export {};
36
71
  //# sourceMappingURL=snapshot-bucket-presence.d.ts.map
@@ -1,21 +1,39 @@
1
1
  import { buildDisplaySafeUrl } from "../trace/redact.js";
2
2
  import { MinioBlobBackend } from "./blob-backend.js";
3
3
  import { recordFailOpen } from "../observability/fail-open.js";
4
+ import { SESSION_SNAPSHOT_BUCKET_DEFAULT, WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
4
5
  import { isStoreOpenWindowExpired, openWithinWait } from "./store-open-wait.js";
6
+ export const SNAPSHOT_BUCKET_ROLES = {
7
+ session: {
8
+ noun: "snapshot bucket",
9
+ knob: "SESSION_SNAPSHOT_BUCKET",
10
+ defaultName: SESSION_SNAPSHOT_BUCKET_DEFAULT,
11
+ consequence: "every snapshot-offload write and SendUserFile private-track request would answer 404 NoSuchBucket (and the orphan sweep would log one every minute, forever)",
12
+ createHint: "; this version's sema-up scripts create it — re-run them on an existing sema-up deployment",
13
+ },
14
+ workspace: {
15
+ noun: "workspace snapshot bucket",
16
+ knob: "WORKSPACE_SNAPSHOT_BUCKET",
17
+ defaultName: WORKSPACE_SNAPSHOT_BUCKET_DEFAULT,
18
+ consequence: "no workspace snapshot could be saved or restored (every run would start from an empty /workspace and every run-end save would fail on a bare 404), and no line would say the bucket is missing",
19
+ createHint: "",
20
+ },
21
+ };
5
22
  export const SNAPSHOT_BUCKET_PROBE_TIMEOUT_MS = 10_000;
6
23
  class SnapshotBucketMissing extends Error {
7
24
  constructor() {
8
25
  super("snapshot bucket missing");
9
26
  }
10
27
  }
11
- export function buildSnapshotBucketRefusal(bucket, endpoint, waitedMs) {
28
+ export function buildSnapshotBucketRefusal(bucket, endpoint, waitedMs, role = "session") {
29
+ const r = SNAPSHOT_BUCKET_ROLES[role];
12
30
  const waited = waitedMs > 0 ? `after retrying for ${waitedMs}ms (STORE_LOCK_WAIT_MS)` : "on the first check (STORE_LOCK_WAIT_MS=0: no wait)";
13
- return (`Object storage is configured (MINIO_ENDPOINT → ${endpoint}) but the snapshot bucket "${bucket}" (SESSION_SNAPSHOT_BUCKET; default session-snapshots) does not exist there ${waited} — refusing to boot: ` +
14
- `the server never creates buckets, so every snapshot-offload write and SendUserFile private-track request would answer 404 NoSuchBucket (and the orphan sweep would log one every minute, forever). ` +
15
- `Next: create the bucket (mc mb <alias>/${bucket}; this version's sema-up scripts create it — re-run them on an existing sema-up deployment), or set SESSION_SNAPSHOT_BUCKET to a bucket that exists.`);
31
+ return (`Object storage is configured (MINIO_ENDPOINT → ${endpoint}) but the ${r.noun} "${bucket}" (${r.knob}; default ${r.defaultName}) does not exist there ${waited} — refusing to boot: ` +
32
+ `the server never creates buckets, so ${r.consequence}. ` +
33
+ `Next: create the bucket (mc mb <alias>/${bucket}${r.createHint}), or set ${r.knob} to a bucket that exists.`);
16
34
  }
17
35
  export const SNAPSHOT_BUCKET_PROBE_PENDING = "probe still pending when the window ended";
18
- export async function ensureSnapshotBucket(store, waitMs, logger, clock) {
36
+ export async function ensureSnapshotBucket(store, waitMs, logger, clock, role = "session") {
19
37
  const probe = new MinioBlobBackend({ endpoint: store.endpoint, bucket: store.bucket, accessKey: store.accessKey, secretKey: store.secretKey, ...(store.region ? { region: store.region } : {}), ...(store.keyPrefix ? { keyPrefix: store.keyPrefix } : {}) });
20
38
  let verdict;
21
39
  try {
@@ -24,11 +42,11 @@ export async function ensureSnapshotBucket(store, waitMs, logger, clock) {
24
42
  if (r.state === "missing")
25
43
  throw new SnapshotBucketMissing();
26
44
  return r;
27
- }, waitMs, (w) => logger.warn("snapshot_bucket_wait", { bucket: store.bucket, endpoint: buildDisplaySafeUrl(store.endpoint), waitMs: w.waitMs, error: w.error }), clock);
45
+ }, waitMs, (w) => logger.warn("snapshot_bucket_wait", { role, bucket: store.bucket, endpoint: buildDisplaySafeUrl(store.endpoint), waitMs: w.waitMs, error: w.error }), clock);
28
46
  }
29
47
  catch (err) {
30
48
  if (err instanceof SnapshotBucketMissing)
31
- throw new Error(buildSnapshotBucketRefusal(store.bucket, buildDisplaySafeUrl(store.endpoint), waitMs), { cause: err });
49
+ throw new Error(buildSnapshotBucketRefusal(store.bucket, buildDisplaySafeUrl(store.endpoint), waitMs, role), { cause: err });
32
50
  if (!isStoreOpenWindowExpired(err))
33
51
  throw err;
34
52
  verdict = { state: "unknown", detail: SNAPSHOT_BUCKET_PROBE_PENDING };
@@ -37,4 +55,14 @@ export async function ensureSnapshotBucket(store, waitMs, logger, clock) {
37
55
  recordFailOpen("server.snapshot-bucket.presence-unproven", `bucket=${store.bucket} endpoint=${buildDisplaySafeUrl(store.endpoint)} ${verdict.detail}`);
38
56
  }
39
57
  }
58
+ export async function ensureSnapshotBuckets(config, logger) {
59
+ const wanted = [
60
+ ["session", config.snapshotBlobStore],
61
+ ["workspace", config.remoteExec?.provider === "k8s" ? config.remoteExec.s3Snapshot : undefined],
62
+ ];
63
+ for (const [role, store] of wanted) {
64
+ if (store)
65
+ await ensureSnapshotBucket(store, config.storeLockWaitMs, logger, undefined, role);
66
+ }
67
+ }
40
68
  //# sourceMappingURL=snapshot-bucket-presence.js.map