@sema-agent/server 7.125.1 → 7.126.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 (248) hide show
  1. package/MIGRATION.md +195 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +263 -140
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +1 -1
  10. package/deploy/sema-up/chart/values.yaml +12 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +11 -10
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/parse-machines.py +2 -2
  18. package/deploy/sema-up/lib/shell-access.sh +1 -1
  19. package/deploy/sema-up/preflight.sh +4 -4
  20. package/deploy/sema-up/sema-up.sh +33 -33
  21. package/deploy/sema-up/smoke.sh +3 -3
  22. package/deploy/sema-up/stack/compose.yaml +1 -1
  23. package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
  24. package/dist/approval-card.d.ts +20 -4
  25. package/dist/approval-content-kind.d.ts +38 -22
  26. package/dist/approval-content-kind.js +20 -4
  27. package/dist/audit.js +2 -0
  28. package/dist/bench/s1/arms.d.ts +9 -37
  29. package/dist/bench/s1/arms.js +4 -61
  30. package/dist/bench/s1/live-deps.d.ts +4 -10
  31. package/dist/bench/s1/live-deps.js +1 -222
  32. package/dist/bench/s1/oracle.d.ts +1 -1
  33. package/dist/bench/s1/reviewer.d.ts +1 -1
  34. package/dist/bench/s1/row.d.ts +5 -7
  35. package/dist/bench/s1/row.js +0 -2
  36. package/dist/bench/s1/run-firm.d.ts +2 -2
  37. package/dist/bench/s1/run-firm.js +1 -4
  38. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  39. package/dist/bench/s1/runner-ctx.js +3 -7
  40. package/dist/bench/s1/tasks.d.ts +9 -46
  41. package/dist/bench/s1/tasks.js +4 -63
  42. package/dist/boot/execution-env.d.ts +38 -26
  43. package/dist/boot/execution-env.js +71 -38
  44. package/dist/boot/memory-boundary.d.ts +1 -1
  45. package/dist/boot/parked-revive-gate.d.ts +0 -3
  46. package/dist/boot/parked-revive-gate.js +2 -2
  47. package/dist/boot/resolve-spec.d.ts +0 -2
  48. package/dist/boot/resolve-spec.js +12 -17
  49. package/dist/boot/retention-lane.d.ts +12 -1
  50. package/dist/boot/retention-lane.js +2 -0
  51. package/dist/boot/runner-deps.d.ts +5 -5
  52. package/dist/boot/runner-deps.js +5 -16
  53. package/dist/boot/session-faces.d.ts +5 -1
  54. package/dist/boot/session-faces.js +37 -1
  55. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  56. package/dist/boot/stage-03-store-backend.js +2 -2
  57. package/dist/boot/stage-04-stores.d.ts +1 -0
  58. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  59. package/dist/boot/stage-05-execution-env.js +7 -4
  60. package/dist/boot/stage-06-runners.d.ts +2 -3
  61. package/dist/boot/stage-06-runners.js +8 -7
  62. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  63. package/dist/boot/stage-07-capability-layer.js +3 -4
  64. package/dist/boot/stage-08-reapers.d.ts +2 -3
  65. package/dist/boot/stage-08-reapers.js +2 -2
  66. package/dist/boot/stage-09-leader.d.ts +2 -3
  67. package/dist/boot/stage-09-leader.js +2 -1
  68. package/dist/boot/stage-10-http-server.d.ts +2 -3
  69. package/dist/boot/stage-10-http-server.js +2 -2
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +4 -3
  72. package/dist/boot/web-admin.d.ts +5 -0
  73. package/dist/boot/web-admin.js +28 -14
  74. package/dist/brain.d.ts +2 -2
  75. package/dist/brain.js +1 -1
  76. package/dist/capabilities/builtin-tools.d.ts +2 -1
  77. package/dist/capabilities/composite-members.d.ts +1 -1
  78. package/dist/capabilities/composite-members.js +1 -1
  79. package/dist/capabilities/hands-lane.d.ts +9 -6
  80. package/dist/capabilities/hands-lane.js +2 -2
  81. package/dist/capabilities/memory-notice.d.ts +27 -22
  82. package/dist/capabilities/memory-notice.js +15 -17
  83. package/dist/capabilities/scenarios.d.ts +9 -14
  84. package/dist/capabilities/scenarios.js +5 -50
  85. package/dist/capability-facts.d.ts +22 -11
  86. package/dist/capability-facts.js +13 -6
  87. package/dist/config-catalog.js +11 -4
  88. package/dist/config-provider.d.ts +5 -1
  89. package/dist/config-provider.js +23 -4
  90. package/dist/config-types.d.ts +14 -1
  91. package/dist/config.js +39 -6
  92. package/dist/deployment-governance.d.ts +19 -21
  93. package/dist/deployment-governance.js +7 -8
  94. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  95. package/dist/env-name-allowlist-knobs.js +1 -0
  96. package/dist/execution-lane-caps.js +2 -2
  97. package/dist/hooks/hook-runner.d.ts +84 -30
  98. package/dist/hooks/hook-runner.js +74 -41
  99. package/dist/http/admission.js +12 -9
  100. package/dist/http/durable-run-leg.d.ts +5 -4
  101. package/dist/http/durable-run-leg.js +1 -1
  102. package/dist/http/idempotency.d.ts +28 -6
  103. package/dist/http/idempotency.js +10 -2
  104. package/dist/http/principal-gate.d.ts +14 -0
  105. package/dist/http/principal-gate.js +7 -0
  106. package/dist/http/request-guards.d.ts +2 -1
  107. package/dist/http/request-guards.js +14 -14
  108. package/dist/http/resume-legs.d.ts +2 -2
  109. package/dist/http/resume-legs.js +5 -4
  110. package/dist/http/route-ctx.d.ts +8 -6
  111. package/dist/http/routes/a2a-serve.js +11 -7
  112. package/dist/http/routes/approvals-assistant.js +18 -3
  113. package/dist/http/routes/background.d.ts +1 -1
  114. package/dist/http/routes/background.js +1 -1
  115. package/dist/http/routes/capabilities.js +10 -2
  116. package/dist/http/routes/mcp-reconnect.js +1 -1
  117. package/dist/http/routes/runs.d.ts +4 -3
  118. package/dist/http/routes/runs.js +12 -13
  119. package/dist/http/routes/session-sync.js +14 -11
  120. package/dist/http/routes/sessions.js +9 -9
  121. package/dist/http/routes/side-query.js +6 -19
  122. package/dist/http/routes/tasks.js +12 -11
  123. package/dist/http/server.d.ts +4 -0
  124. package/dist/http/server.js +2 -2
  125. package/dist/http/submit-replay.d.ts +15 -8
  126. package/dist/http/submit-replay.js +6 -0
  127. package/dist/http/wire-types.d.ts +7 -9
  128. package/dist/index.d.ts +0 -1
  129. package/dist/index.js +0 -1
  130. package/dist/internal-run-env.d.ts +30 -0
  131. package/dist/internal-run-env.js +11 -0
  132. package/dist/leader/wire.js +3 -2
  133. package/dist/memory-scope.d.ts +19 -7
  134. package/dist/memory-scope.js +25 -8
  135. package/dist/observability/fail-open.d.ts +15 -3
  136. package/dist/observability/fail-open.js +15 -3
  137. package/dist/observability/metrics.d.ts +0 -1
  138. package/dist/observability/metrics.js +0 -3
  139. package/dist/observability/run-terminal.js +69 -4
  140. package/dist/permission-rule-vocab.d.ts +3 -2
  141. package/dist/permission-rule-vocab.js +1 -1
  142. package/dist/plan-cache-probe.d.ts +3 -2
  143. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  144. package/dist/plugins/caching-session-store.d.ts +3 -0
  145. package/dist/plugins/caching-session-store.js +3 -0
  146. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  147. package/dist/plugins/checkpoint-store-sql.js +57 -11
  148. package/dist/plugins/file-run-store.d.ts +25 -21
  149. package/dist/plugins/file-run-store.js +70 -34
  150. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  151. package/dist/plugins/local-checkpoint-store.js +5 -15
  152. package/dist/plugins/local-session-store.d.ts +66 -7
  153. package/dist/plugins/local-session-store.js +168 -101
  154. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  155. package/dist/plugins/memory-run-store.d.ts +19 -3
  156. package/dist/plugins/memory-run-store.js +38 -3
  157. package/dist/plugins/one-time-migrations.d.ts +141 -39
  158. package/dist/plugins/one-time-migrations.js +32 -143
  159. package/dist/plugins/pg-pool.js +11 -1
  160. package/dist/plugins/pg-session-storage.d.ts +2 -0
  161. package/dist/plugins/pg-session-storage.js +9 -5
  162. package/dist/plugins/remote-env-adb.d.ts +3 -0
  163. package/dist/plugins/remote-env-adb.js +1 -0
  164. package/dist/plugins/remote-env-device.d.ts +3 -0
  165. package/dist/plugins/remote-env-device.js +1 -0
  166. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  167. package/dist/plugins/remote-env-e2b.js +2 -1
  168. package/dist/plugins/remote-env-host.d.ts +12 -0
  169. package/dist/plugins/remote-env-host.js +11 -1
  170. package/dist/plugins/remote-env-k8s.d.ts +227 -39
  171. package/dist/plugins/remote-env-k8s.js +450 -75
  172. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  173. package/dist/plugins/remote-env-local-docker.js +2 -1
  174. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  175. package/dist/plugins/remote-env-ssh.js +1 -0
  176. package/dist/plugins/retention-store-sql.d.ts +4 -1
  177. package/dist/plugins/retention-store-sql.js +11 -9
  178. package/dist/plugins/run-store-sql.d.ts +44 -6
  179. package/dist/plugins/run-store-sql.js +92 -27
  180. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  181. package/dist/plugins/sql-driver.d.ts +2 -1
  182. package/dist/plugins/store-backend.js +1 -1
  183. package/dist/plugins/store-contracts.d.ts +17 -3
  184. package/dist/plugins/tidb-pool.js +15 -3
  185. package/dist/plugins/tidb-session-storage.js +3 -2
  186. package/dist/plugins/tidb-session-store.d.ts +2 -0
  187. package/dist/plugins/tidb-session-store.js +6 -2
  188. package/dist/request-key-closure.d.ts +12 -0
  189. package/dist/request-key-closure.js +3 -3
  190. package/dist/router/route-orchestration.d.ts +3 -11
  191. package/dist/router/route-orchestration.js +0 -4
  192. package/dist/rule-entry-list.js +0 -4
  193. package/dist/rules-consent.js +3 -0
  194. package/dist/run-local.d.ts +5 -4
  195. package/dist/run-local.js +23 -17
  196. package/dist/runs.js +1 -0
  197. package/dist/security.d.ts +7 -0
  198. package/dist/security.js +5 -0
  199. package/dist/server-secret-env.d.ts +10 -4
  200. package/dist/server-secret-env.js +4 -1
  201. package/dist/session-incarnation.d.ts +46 -2
  202. package/dist/session-incarnation.js +14 -3
  203. package/dist/session-sync-kernel.d.ts +10 -3
  204. package/dist/spec-fields.d.ts +24 -8
  205. package/dist/spec-fields.js +21 -2
  206. package/dist/task-workflow.d.ts +6 -4
  207. package/dist/task-workflow.js +3 -2
  208. package/dist/tool-approval.d.ts +78 -6
  209. package/dist/tool-approval.js +7 -3
  210. package/dist/trace/core-keyset-guard.d.ts +4 -4
  211. package/dist/trace/engine-notice-wire.d.ts +26 -1
  212. package/dist/trace/engine-notice-wire.js +7 -2
  213. package/dist/trace/project.d.ts +8 -3
  214. package/dist/trace/project.js +15 -1
  215. package/dist/trace/self-minted-wire.d.ts +104 -2
  216. package/dist/trace/self-minted-wire.js +35 -0
  217. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  218. package/dist/web-admin-client/apply-effective.js +11 -26
  219. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  220. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  221. package/dist/web-admin-client/candidate-gate.js +9 -2
  222. package/dist/web-admin-client/facade.d.ts +2 -2
  223. package/dist/web-admin-client/http-client.d.ts +3 -1
  224. package/dist/web-admin-client/http-client.js +4 -3
  225. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  226. package/dist/web-admin-client/read-warnings.js +41 -6
  227. package/dist/web-admin-client/types.d.ts +11 -5
  228. package/dist/workspace-cache.d.ts +45 -0
  229. package/dist/workspace-cache.js +30 -0
  230. package/dist/workspace-space-commit.d.ts +116 -0
  231. package/dist/workspace-space-commit.js +117 -0
  232. package/dist/workspace-space.d.ts +75 -9
  233. package/dist/workspace-space.js +52 -1
  234. package/package.json +4 -4
  235. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  236. package/dist/boot/session-incarnation-seat.js +0 -69
  237. package/dist/capabilities/code-review-council.d.ts +0 -49
  238. package/dist/capabilities/code-review-council.js +0 -111
  239. package/dist/capabilities/collab-wire.d.ts +0 -36
  240. package/dist/capabilities/collab-wire.js +0 -2
  241. package/dist/capabilities/collab-workflows.d.ts +0 -47
  242. package/dist/capabilities/collab-workflows.js +0 -90
  243. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  244. package/dist/capabilities/prompts/code-review.js +0 -39
  245. package/dist/capabilities/prompts/team.d.ts +0 -10
  246. package/dist/capabilities/prompts/team.js +0 -6
  247. package/dist/capabilities/team.d.ts +0 -113
  248. package/dist/capabilities/team.js +0 -70
@@ -1,4 +1,4 @@
1
- import { expandTiers, isThinkingLevel } from "@sema-agent/core";
1
+ import { MODEL_ROLES, expandTiers, isThinkingLevel } from "@sema-agent/core";
2
2
  import { HttpError } from "../../security.js";
3
3
  import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
4
4
  import { modelUnavailableMessage } from "../../model-entry-refusal.js";
@@ -12,22 +12,8 @@ export function isCorePreflightValidationError(message) {
12
12
  return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
13
13
  }
14
14
  export const CORE_PREFLIGHT_VALIDATION_ANCHORS = CORE_PREFLIGHT_VALIDATION_PREFIXES;
15
- const MODEL_ROLE_TABLE = {
16
- default: true,
17
- summarize: true,
18
- subagent: true,
19
- team: true,
20
- synthesize: true,
21
- advisor: true,
22
- verifier: true,
23
- classifier: true,
24
- consolidate: true,
25
- };
26
15
  function isModelRole(value) {
27
- return typeof value === "string" && Object.hasOwn(MODEL_ROLE_TABLE, value);
28
- }
29
- function passthroughModelRole(value) {
30
- return value;
16
+ return typeof value === "string" && MODEL_ROLES.includes(value);
31
17
  }
32
18
  function shapeSideQueryEcho(result) {
33
19
  if (result.responseModel === undefined || admitProviderEcho(result.responseModel, "side-query.responseModel"))
@@ -76,8 +62,9 @@ export async function handleSideQuery(req, res, match, ctx) {
76
62
  sendError(res, 400, "request.field_invalid", "invalid thinking level");
77
63
  return;
78
64
  }
79
- if (typeof body.model !== "string" && body.modelRole !== undefined && !isModelRole(body.modelRole)) {
80
- sendError(res, 400, "request.field_invalid", `invalid modelRole (expected one of: ${Object.keys(MODEL_ROLE_TABLE).join(", ")}) — a role only resolves a model when 'model' is absent`);
65
+ if (body.modelRole !== undefined && !isModelRole(body.modelRole)) {
66
+ const shown = (typeof body.modelRole === "string" ? JSON.stringify(body.modelRole.slice(0, 80)) : `of type ${body.modelRole === null ? "null" : Array.isArray(body.modelRole) ? "array" : typeof body.modelRole}`);
67
+ sendError(res, 400, "request.field_invalid", `invalid modelRole ${shown} — must be a string, one of ${MODEL_ROLES.join(" / ")} (the engine's role vocabulary); a value outside it is refused before any model call`);
81
68
  return;
82
69
  }
83
70
  if (typeof body.model === "string" && body.model.length > 0) {
@@ -98,7 +85,7 @@ export async function handleSideQuery(req, res, match, ctx) {
98
85
  const spec = {
99
86
  messages: body.messages,
100
87
  ...(typeof body.model === "string" ? { model: body.model } : {}),
101
- ...(typeof body.modelRole === "string" ? { modelRole: passthroughModelRole(body.modelRole) } : {}),
88
+ ...(isModelRole(body.modelRole) ? { modelRole: body.modelRole } : {}),
102
89
  ...(isThinkingLevel(body.thinking) ? { thinking: body.thinking } : {}),
103
90
  ...(typeof body.systemPrompt === "string" ? { systemPrompt: body.systemPrompt } : {}),
104
91
  ...(Array.isArray(body.tools) ? { tools: body.tools } : {}),
@@ -15,7 +15,7 @@ import { recordResourceWindow } from "../../resource-window.js";
15
15
  import { redactSecrets } from "../../trace/redact.js";
16
16
  import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
17
17
  import { cascadeConfig, runMeta } from "../run-meta.js";
18
- import { idemCallerOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
18
+ import { idemCallerOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
19
19
  import { replayOfExistingRun } from "../submit-replay.js";
20
20
  import { sendJson, sendError, sseHeaders } from "../send.js";
21
21
  import { collectSsePreamble } from "../sse-log.js";
@@ -74,7 +74,7 @@ const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their n
74
74
  export async function handleTasks(req, res, match, ctx) {
75
75
  const { deps } = ctx;
76
76
  const { idemCache, inflightRuns, steerableRuns, counters } = ctx.registry;
77
- const { rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
77
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
78
78
  const { prepareSpec, finalizeTaskResult } = ctx.legs;
79
79
  const reqState = ctx.req;
80
80
  const source = ctx.req.source;
@@ -97,12 +97,13 @@ export async function handleTasks(req, res, match, ctx) {
97
97
  {
98
98
  const rawIdem = headerStr(req.headers["idempotency-key"]);
99
99
  const submitter = gatedPrincipal(req, deps.config);
100
- const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "run") : undefined;
101
- const inflightKey = idemKey ? inflightKeyOf(idemKey, "sync") : undefined;
100
+ const keyedBody = rawIdem ? await readJson(req) : undefined;
101
+ const idem = rawIdem ? submissionIdemOf(rawIdem, idemCallerOf(req, ctx), keyedBody) : undefined;
102
+ const inflightKey = idem ? inflightKeyOf(idem, "sync") : undefined;
102
103
  const cached = inflightKey ? idemCache.peek(inflightKey) : undefined;
103
- const keyedRow = idemKey && !cached && deps.runStore ? await deps.runStore.getRunByIdem(idemKey) : undefined;
104
+ const keyedRow = idem && !cached && deps.runStore ? await deps.runStore.getRunByIdem(idem.key) : undefined;
104
105
  if (keyedRow && deps.runStore) {
105
- const replay = await replayOfExistingRun(keyedRow, { principal: submitter }, "sync", deps, deps.runStore);
106
+ const replay = await replayOfExistingRun(keyedRow, { principal: submitter, idem }, "sync", deps, deps.runStore);
106
107
  if (liveStream && replay.status === 200) {
107
108
  sseHeaders(res, { "x-task-id": keyedRow.taskId });
108
109
  writeMetaFrame(res, keyedRow.taskId, keyedRow.sessionId);
@@ -141,7 +142,7 @@ export async function handleTasks(req, res, match, ctx) {
141
142
  }
142
143
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
143
144
  return;
144
- const prepared = await prepareSpec(req, res, undefined, undefined, { liveStream });
145
+ const prepared = await prepareSpec(req, res, keyedBody, undefined, { liveStream });
145
146
  if (!prepared)
146
147
  return;
147
148
  const principal = prepared.auth?.principal;
@@ -257,7 +258,7 @@ export async function handleTasks(req, res, match, ctx) {
257
258
  };
258
259
  if (deps.runStore && prepared.spec.sessionId) {
259
260
  const tid = earlyDurableTid ?? uuidv7();
260
- const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), idemKey: idemKey ?? null });
261
+ const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), idem: idem ?? null });
261
262
  if (created.ok)
262
263
  clearTurnActivity(tid);
263
264
  if (created.ok)
@@ -265,7 +266,7 @@ export async function handleTasks(req, res, match, ctx) {
265
266
  if (created.ok)
266
267
  deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
267
268
  if (created.existing) {
268
- const replay = await replayOfExistingRun(created.existing, { principal: submitter }, "sync", deps, deps.runStore);
269
+ const replay = await replayOfExistingRun(created.existing, { principal: submitter, idem }, "sync", deps, deps.runStore);
269
270
  sseData(res, { type: "done", result: doneResultOfOutcome(replay), replay: true });
270
271
  return replay;
271
272
  }
@@ -830,13 +831,13 @@ export async function handleTasks(req, res, match, ctx) {
830
831
  let durableHeartbeat;
831
832
  if (deps.runStore && prepared.spec.sessionId) {
832
833
  const tid = uuidv7();
833
- const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), idemKey: idemKey ?? null });
834
+ const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", { ...runMeta(prepared, source), idem: idem ?? null });
834
835
  if (created.ok)
835
836
  clearTurnActivity(tid);
836
837
  if (created.ok)
837
838
  recordResourceWindow(tid, prepared.spec.resourceSuspend);
838
839
  if (created.existing)
839
- return await replayOfExistingRun(created.existing, { principal: submitter }, "sync", deps, deps.runStore);
840
+ return await replayOfExistingRun(created.existing, { principal: submitter, idem }, "sync", deps, deps.runStore);
840
841
  if (!created.ok)
841
842
  return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
842
843
  const placementReject = await commitPlacement(deps, deps.runStore, tid, prepared.bindingIntent);
@@ -718,6 +718,10 @@ export interface ServiceDeploymentDeps {
718
718
  * (`boot/stage-10-http-server.ts` → `modelRefusals: () => deployment.modelRefusalsState?.()`)—— 一处铸、两出口。缺席 / 返回 undefined =
719
719
  * 没有可报的拒或没有配置管道 ⇒ `/health` 形状逐字不变(additive)。 */
720
720
  modelRefusalsState?: () => import("@sema-agent/settings-schema").ModelRefusalsSignal | undefined;
721
+ /** S-1457:`GET /v1/models.candidateRejected` 的取值口 —— 最近一次配置候选被判官整拒的结论(`boot/stage-10-http-server.ts` →
722
+ * `candidateRejectionState: () => webAdmin.candidateRejectionState()`;座 = web-admin 的 `candidateRejection`,`config_candidate_rejected` 日志行的去重键读同一份)。
723
+ * present-iff 当前生效配置是因候选被拒而回落的那一份;缺席 / 返回 undefined = 没有被拒的候选或没有配置管道 ⇒ `GET /v1/models` 不出该键(additive)。 */
724
+ candidateRejectionState?: () => import("../web-admin-client/candidate-gate.js").CandidateRejection | undefined;
721
725
  /** `/health.configHandoff`(只上 `/health`):在场 ⇔ 部署声明了耐久接力 ∧ 应用着的那一代没被确认(web-admin 接力账的 `configHandoffState()`;合成律
722
726
  * `fleet/engine-signal.ts` 的 `buildConfigHandoff`)。缺席 / 返回 undefined = 有接力、没有中心或本地腿 ⇒ 键缺席。 */
723
727
  configHandoffState?: () => import("../fleet/engine-signal.js").ConfigHandoff | undefined;
@@ -113,9 +113,9 @@ export function createHttpServer(rawDeps) {
113
113
  }
114
114
  };
115
115
  }
116
- const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
116
+ const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
117
117
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
118
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
118
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
119
119
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
120
120
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
121
121
  const server = http.createServer((req, res) => {
@@ -2,15 +2,19 @@
2
2
  * 提交幂等的**唯一回执函数** —— 一把提交钥匙命中了一条既有 run 行之后,这次提交拿到什么。
3
3
  *
4
4
  * ## 律(一条)
5
- * 幂等 = 资源行上的唯一键。`task_run` 上有两把:`idem_key`(`Idempotency-Key`,唯一铸法 `idemKeyOf(raw, caller, "run")`)
6
- * 与 `task_id`(调用方自铸的 `body.taskId`;A2A 由 messageId 派生)。哪一把命中、在哪条提交腿上命中、是请求进门时
7
- * 读行命中还是建行撞唯一键时观察到,一律经**本函数**作答 —— 提交面不再有第二个判官(进程内缓存只做在飞合流,
8
- * 见 `http/idempotency.ts`)。
5
+ * 幂等 = 同一调用方、同一键、**同一请求**,执法点 = 资源行上的唯一键。`task_run` 上有两把:`idem_key`(`Idempotency-Key`,唯一铸法
6
+ * `idemKeyOf(raw, caller, "run")`)与 `task_id`(调用方自铸的 `body.taskId`;A2A 由 messageId 派生);`idem_key` 旁边记着绑定它的那次
7
+ * 请求的体指纹(`idem_body_fingerprint`,`RunIdemBinding`)。哪一把命中、在哪条提交腿上命中、是请求进门时读行命中还是建行撞唯一键时
8
+ * 观察到,一律经**本函数**作答 —— 提交面不再有第二个判官(进程内缓存只做在飞合流,且只合同一请求,见 `http/idempotency.ts`)。
9
9
  *
10
- * ## 判定(两条,别的一律不放)
10
+ * ## 判定(三条,按序;别的一律不放)
11
11
  * · 这次提交自带 taskId 而命中的行**不是**那一条(`Idempotency-Key` 指向别的行)⇒ 冲突;
12
- * · 调用方读不到这一行(属主门 = `runOwnerReadable`,「这个调用方读得到这一行 run 吗」的唯一谓词)⇒ 冲突。
13
- * 冲突 ⇒ `409 conflict.run_exists`,体里**不回显**那条行的任何标识(不给跨租户预言机)。
12
+ * · 调用方读不到这一行(属主门 = `runOwnerReadable`,「这个调用方读得到这一行 run 吗」的唯一谓词)⇒ 冲突;
13
+ * 冲突 ⇒ `409 conflict.run_exists`,体里**不回显**那条行的任何标识(不给跨租户预言机)。先于下一条判:读不到的行不比体。
14
+ * · 这次提交带 `Idempotency-Key`(`caller.idem` 在场)、那一行绑的**就是这把键**,而它记着的请求体指纹与这次不同 ⇒
15
+ * `409 conflict.idempotency_payload_mismatch`(同形 `{ error, errorCode }`,不回显键与那一行);**不**回放首发、不新建、那一行不动。
16
+ * 行上没有指纹(列之前绑的键)⇒ 这一只绑上(同键先到者绑,`RunStore.bindIdemBodyFingerprint`)并放行。指纹属于绑定它的那把键:
17
+ * 按 taskId 命中而键是另一把(或行是无键提交建的)⇒ 不比、不写;只凭 `body.taskId` 的提交不带绑定,不比体。
14
18
  *
15
19
  * ## 回执两形(按提交腿)
16
20
  * · `durable`(`POST /v1/runs`、A2A `message/send`)—— `202 { taskId, sessionId, status }`(这条腿首答的既有形);
@@ -18,7 +22,7 @@
18
22
  * (读面 = `GET /v1/runs/:id` 的 200 体,同一只投影口 `readRunFace`)。同步腿的首答是引擎结果,而重放时手里只有行;
19
23
  * `replay: true` 是这一形的判别位。
20
24
  */
21
- import type { RunRecord } from "../plugins/store-contracts.js";
25
+ import type { RunIdemBinding, RunRecord } from "../plugins/store-contracts.js";
22
26
  import type { RunStore } from "../plugins/store-backend.js";
23
27
  import type { ServiceConfig } from "../config-types.js";
24
28
  import type { SubmitOutcome } from "./idempotency.js";
@@ -30,6 +34,9 @@ export interface ReplayCaller {
30
34
  principal: string | undefined;
31
35
  /** 这次提交自带的 taskId(`body.taskId`;A2A 派生值)。缺席 = 只凭 `Idempotency-Key` 命中。 */
32
36
  clientTaskId?: string;
37
+ /** 这次提交的账本绑定(键 + 请求体指纹;`RunIdemBinding`)。`undefined` = 这次提交没带 `Idempotency-Key`(只凭 `body.taskId`)⇒ 不比体。
38
+ * 必填位(值可为 `undefined`):每个调用点都得当面说出它有没有绑定,漏传是编译错误,不是静默不比。 */
39
+ idem: RunIdemBinding | undefined;
33
40
  }
34
41
  /** 一份提交回执是不是 {@link replayOfExistingRun} 铸的**冲突**体(判别位 = 机器码;铸点就在下面,码与它同文件)—— 把回执渲成别的协议形的
35
42
  * 调用方(A2A)按它分句,不另抄码。 */
@@ -8,6 +8,12 @@ export async function replayOfExistingRun(row, caller, face, deps, runStore) {
8
8
  if (!sameRun || !runOwnerReadable(caller.principal, row.owner, deps.config)) {
9
9
  return { status: 409, body: { error: "a run already exists for this submission's taskId or Idempotency-Key", errorCode: "conflict.run_exists" } };
10
10
  }
11
+ if (caller.idem !== undefined) {
12
+ const held = await runStore.bindIdemBodyFingerprint(row.taskId, caller.idem);
13
+ if (held !== null && held !== caller.idem.bodyFingerprint) {
14
+ return { status: 409, body: { error: "this Idempotency-Key was already used for a different request body — a retry must resend the same body; send a different request with a new Idempotency-Key", errorCode: "conflict.idempotency_payload_mismatch" } };
15
+ }
16
+ }
11
17
  switch (face) {
12
18
  case "durable":
13
19
  return { status: 202, body: { taskId: row.taskId, sessionId: row.sessionId, status: row.status } };
@@ -13,7 +13,7 @@ export interface TaskRequestBody {
13
13
  objective: string;
14
14
  sessionId?: string;
15
15
  /** Which deployment SCENARIO assembles this task's tools/prompt/skills (the keys of the worker's scenario
16
- * table — e.g. "default"/"code-review"/"scan"/"discuss"). An INTENT bounded by the deployment's own table AND,
16
+ * table — e.g. "default"/"code"/"code-review"/"scan"). An INTENT bounded by the deployment's own table AND,
17
17
  * on a governed worker, by the principal's center-resolved allowlist: an unknown name is a typed 400
18
18
  * `scenario_unknown`, a known-but-not-allowed one a typed 400 `scenario_not_allowed`; absent ⇒ the ruling's
19
19
  * assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`). A name
@@ -94,14 +94,6 @@ export interface TaskRequestBody {
94
94
  * a rung fails the gate (default = it didn't complete). Default off; mutually exclusive with `verify`;
95
95
  * not on /v1/tasks/stream. ⚠️ each rung is a cold re-run — use for read-only / idempotent tasks. */
96
96
  cascade?: boolean;
97
- /** Multi-lens review council (the `code-review` scenario's expensive tier) — `council:true` fans the review out
98
- * to N parallel lenses + an arbiter instead of the lead reviewing in-line; `debate:true` additionally runs the
99
- * L2 peer-debate rounds. Read on TWO seams and both matter: the scenario builder mounts `run_council`
100
- * (`capabilities/scenarios.ts` `codeReview`), and `boot/resolve-spec.ts` treats either flag as an EXPLICIT team
101
- * declaration — it widens the task's wall-clock tenancy budget (`resolveTaskLimits`) and suppresses the value
102
- * router's auto-escalation (`explicitTeam`). Only literal `true` counts on both seams. */
103
- council?: boolean;
104
- debate?: boolean;
105
97
  /** Work-view correlation id (optional): groups the runs of one logical task across both client doors
106
98
  * (MCP façade / portal) so a fragmented set of runs reads as one task. Opaque to the engine — only
107
99
  * persisted on the run row + filterable via `GET /v1/tasks?jobId=`. ≤64 chars (matches the column). */
@@ -317,6 +309,12 @@ export interface TaskRequestBody {
317
309
  /** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
318
310
  * tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
319
311
  excludeTools?: string[];
312
+ /** S-1448(core 7.49.0 #1623,[ref]):引擎的**可用性选择器** —— 这一 run 只用列出的工具(wire 名、别名、`Tool(content)` 整只、
313
+ * `mcp__srv` / `mcp__srv__*` / 工具段通配;`"*"` = 本层不设名单;`[]` = 什么都不给)。宿主**原样透传**进 `TaskSpec.allowTools`:
314
+ * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
315
+ * `excludeTools` 是同一只运算的减项(deny 赢)。值形:字符串数组(受理门 400 `request.field_invalid`,与 `excludeTools` 同一只门);
316
+ * 装着的引擎不认该座 ⇒ 400 `request.field_invalid` 点名所需引擎版本(不静默丢)。无租户门(收窄)。 */
317
+ allowTools?: string[];
320
318
  /** [ref]② (core 1.314): [ref] 延迟披露清单(占位上 wire,ToolSearch 激活)——cli「Workflow 默认开
321
319
  * 不暴露」的承载位。同 excludeTools 的校验/继承姿势。 */
322
320
  deferTools?: string[];
package/dist/index.d.ts CHANGED
@@ -15,7 +15,6 @@ export { Metrics, createMetrics, type Labels } from "./observability/metrics.js"
15
15
  export { RateLimiter, type RateDecision } from "./observability/rate-limit.js";
16
16
  export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo, type GitApiKind, type RepoCoords, type RepoReadClient } from "./capabilities/repo-tools.js";
17
17
  export { nowTool } from "./capabilities/builtin-tools.js";
18
- export { createCouncilTool } from "./capabilities/code-review-council.js";
19
18
  export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
20
19
  export { buildScenarios, selectScenario, type Scenario, type ScenarioBundle, type ScenarioContext, type ScenarioDeps, type ScenarioRequest, } from "./capabilities/scenarios.js";
21
20
  export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS, type TaskListLane, type TaskListLaneBackend, type TaskListLaneOpts } from "./boot/task-list-lane.js";
package/dist/index.js CHANGED
@@ -13,7 +13,6 @@ export { Metrics, createMetrics } from "./observability/metrics.js";
13
13
  export { RateLimiter } from "./observability/rate-limit.js";
14
14
  export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
15
15
  export { nowTool } from "./capabilities/builtin-tools.js";
16
- export { createCouncilTool } from "./capabilities/code-review-council.js";
17
16
  export { loadSkills, skillsForScenario } from "./capabilities/skills.js";
18
17
  export { buildScenarios, selectScenario, } from "./capabilities/scenarios.js";
19
18
  export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS } from "./boot/task-list-lane.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * **服务自己为自己起的 run**(agent 钩子子代理 / leader 的集成沙盒与 grader 沙盒)的执行环境上下文 —— 它们不属于任何用户会话:
3
+ * 会话 id 是服务现铸的(`uuidv7()` / `leader-integ-<ts>`),会话店里没有这一行,也不该有。
4
+ *
5
+ * ── 为什么要在这里表态 ───────────────────────────────────────────────────────────────────────────
6
+ * k8s 车道的工作区空间按**会话住客**分区(`boot/execution-env.ts` `createK8sRunEnvFactory`):建环境时按 id 读会话店的住客,读不到(= 没有这只会话)
7
+ * 就报一行 `workspace_space_unavailable`、开着缓存卷时再计一次 fail-open。对用户的 run 这是真信号(会话被删了);对服务自己的 run 它是**恒真的噪声**
8
+ * —— 每只碰了手的内部 env 一行、计数被内部 runner 抬高。
9
+ *
10
+ * ── 怎么表态:不是新开关,是同一条法 ───────────────────────────────────────────────────────────────
11
+ * 工厂里已有的判别 `placementRootSessionId ≠ sessionId` 读作「这条 run 不拥有自己的空间」(委派子代 / 后台代理与父并发,共用一个空间 = 两个写者,
12
+ * 按法 off:不读住客、不出该行、不挂缓存卷,停驻车道照旧)。内部 run 同样没有自己的空间 —— 它的放置根不是某个用户会话,而是**服务自己**
13
+ * ({@link INTERNAL_RUN_PLACEMENT_ROOT})。所以内部 run 只需如实报它的放置根,工厂那一行判别原样把它读成「无空间」;本模块不加任何第二个判据。
14
+ * 放置根的另一个读者是 device 车道(按根会话找绑定):服务自己的根上没有任何绑定,与修前(根 = 它自己的现铸会话 id,同样没有绑定)同答 —— 拒。
15
+ */
16
+ import type { ExecutionEnvFactory, ExecutionEnvFactoryContext } from "@sema-agent/core";
17
+ /** 内部 run 的放置根:不是用户会话 id —— 提交口的会话 id 形律只收规范 uuid(`security.ts` `sessionIdShapeIssue`),这个词不是 uuid 形,调用方登记不出它。 */
18
+ export declare const INTERNAL_RUN_PLACEMENT_ROOT = "service:internal-run";
19
+ /** 服务为内部 run 直接调工厂时递的上下文(leader 的集成沙盒 / grader 沙盒):自己的现铸会话 id,放置根 = 服务自己。 */
20
+ export declare function buildInternalRunCtx(sessionId: string): ExecutionEnvFactoryContext;
21
+ /**
22
+ * 内部 Runner(agent 钩子子代理)的工厂:把 core 递来的上下文的放置根改报为服务自己 —— core 对这类 run 只知道「根 run,根 = 自己」,不知道它是服务
23
+ * 为自己起的。其余字段(`sessionId` / `taskId` …)原样过境。
24
+ */
25
+ export declare function createInternalRunEnvFactory(factory: ExecutionEnvFactory): ExecutionEnvFactory;
26
+ /** 内部 Runner 的 deps:`executionEnvFactory` 在场就按 {@link createInternalRunEnvFactory} 换装,其余键原样(缺席仍缺席)。 */
27
+ export declare function withInternalRunEnvs<D extends {
28
+ readonly executionEnvFactory?: ExecutionEnvFactory;
29
+ }>(deps: D): D;
30
+ //# sourceMappingURL=internal-run-env.d.ts.map
@@ -0,0 +1,11 @@
1
+ export const INTERNAL_RUN_PLACEMENT_ROOT = "service:internal-run";
2
+ export function buildInternalRunCtx(sessionId) {
3
+ return { sessionId, placementRootSessionId: INTERNAL_RUN_PLACEMENT_ROOT };
4
+ }
5
+ export function createInternalRunEnvFactory(factory) {
6
+ return (ctx) => factory({ ...ctx, placementRootSessionId: INTERNAL_RUN_PLACEMENT_ROOT });
7
+ }
8
+ export function withInternalRunEnvs(deps) {
9
+ return deps.executionEnvFactory === undefined ? deps : { ...deps, executionEnvFactory: createInternalRunEnvFactory(deps.executionEnvFactory) };
10
+ }
11
+ //# sourceMappingURL=internal-run-env.js.map
@@ -19,6 +19,7 @@ import { runLeaderTask, withLeaderLeg } from "./leader.js";
19
19
  import { attachRepairLoopDeps } from "./repair-wire.js";
20
20
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
21
21
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
22
+ import { buildInternalRunCtx } from "../internal-run-env.js";
22
23
  import { currentPrincipal } from "../observability/principal-context.js";
23
24
  import { redactHead } from "../observability/run-terminal.js";
24
25
  const REPAIR_LOOP_WIRED = true;
@@ -379,7 +380,7 @@ export function createLeaderRunner(cfg) {
379
380
  const provisionIntegrationSandbox = async () => {
380
381
  if (cfg.envFactory) {
381
382
  const integSessionId = `leader-integ-${Date.now()}`;
382
- const env = await cfg.envFactory({ sessionId: integSessionId, placementRootSessionId: integSessionId });
383
+ const env = await cfg.envFactory(buildInternalRunCtx(integSessionId));
383
384
  const reap = createEnvReaper(env, "leader integration sandbox");
384
385
  try {
385
386
  await sh(env)(body.seedCmd);
@@ -431,7 +432,7 @@ export function createLeaderRunner(cfg) {
431
432
  let env;
432
433
  if (cfg.envFactory) {
433
434
  const graderSessionId = `leader-grader-${Date.now()}`;
434
- env = await cfg.envFactory({ sessionId: graderSessionId, placementRootSessionId: graderSessionId });
435
+ env = await cfg.envFactory(buildInternalRunCtx(graderSessionId));
435
436
  }
436
437
  else {
437
438
  if (!cfg.e2bApiKey)
@@ -133,19 +133,19 @@ engineWired: boolean): {
133
133
  */
134
134
  /**
135
135
  * S-133(7.59.0 合并重扫 wf_5ac18676 #1)—— **引擎数据根的唯一推导点**。模型是用 fs 工具往这里写记忆的
136
- * ([ref] 记忆边界不变式,boot/memory-boundary.ts),所以三件事必须读**同一只**值:①boot 起哪只记忆后端的根
137
- * (boot/stores.ts 三腿)、②`RunnerDeps.memoryEngineDir`、③守卫集 `createSensitivePathPolicy` 的 `dataRoot`
138
- * (deployment-governance.ts:落在这个根**里面**的目标按相对根判,根自己的祖先段不算目标的账)。
136
+ * ([ref] 记忆边界不变式,boot/memory-boundary.ts),所以两件事必须读**同一只**值:①boot 起哪只记忆后端的根
137
+ * (boot/stores.ts 三腿)、②`RunnerDeps.memoryEngineDir` —— core 7.49.0 [ref] 起它也是守卫集的**唯一**豁免根(落在这个根**里面**的
138
+ * 目标按相对根判,根自己的祖先段不算目标的账;此前守卫集另收一份 `dataRoot`,那是第三个读者,已随 core 退役)。
139
139
  * 此前 ③ 读的是 `config.localDataRoot`(本地**店**根:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR`),
140
140
  * 而 ① 的 file 腿读的是 `MEMORY_ENGINE_DIR ?? AGENT_DATA_DIR`——两条链在「店根另指他处 + 记忆根坐在守卫段下
141
141
  * (壳布局 `~/.sema/engine-data`)」时分家 ⇒ 记忆库每一次 Write 被硬拒(7.58.0 放行 ⇒ 回归);反向
142
142
  * (`CONFIG_LOCAL_DIR=~/.sema` 当 dataRoot)则让配置根自身免检。两根本就是两件事:店根归誊本门,引擎根归本函数。
143
143
  *
144
- * 形与 boot/stores.ts 逐字同律:DB 记忆腿(pg/tidb)的根 = `<localDataRoot ?? fallbackRoot>/memory-work`
145
- * (per-worker 物化区);file 腿 = `resolveMemoryEngineRoot(memoryEngineDir ?? fallbackRoot)`。**不判引擎是否
146
- * 启用**——纯路径推导,禁用态下守卫仍以「若开引擎会写到哪」为根,与 core 缺省(`$AGENT_DATA_DIR`)同向。
144
+ * 形与 boot/stores.ts 逐字同律:DB 记忆腿(pg/tidb)的根 = `<localDataRoot ?? localRoot>/memory-work`
145
+ * (per-worker 物化区);file 腿 = boot file 后端的链 `resolveMemoryEngineRoot(memoryEngineDir)`。**不判引擎是否
146
+ * 启用**——纯路径推导,禁用态下豁免根仍以「若开引擎会写到哪」为根(boot 的 `engineDataRoot` 恒铸,经共享基座交每只 Runner)。
147
147
  */
148
- export declare function memoryEngineRootFor(config: Pick<ServiceConfig, "memoryEngineBackend" | "memoryEngineDir" | "localDataRoot">, fallbackRoot?: string): string;
148
+ export declare function memoryEngineRootFor(config: Pick<ServiceConfig, "memoryEngineBackend" | "memoryEngineDir" | "localDataRoot">, localRoot?: string): string;
149
149
  /** file 后端的根链(`MEMORY_ENGINE_DIR ?? fallback ?? $AGENT_DATA_DIR ?? ~/.ai-agent`)—— 与后端选择器**无关**:
150
150
  * `memoryEngineBackendFor` 恒造 file 后端(run-local 从 env 继承 `MEMORY_ENGINE_BACKEND=pg|tidb` 时仍是 file),
151
151
  * 所以它的根必须走这条链而不是 `memoryEngineRootFor` 的分派(codex 修复验证轮 [medium]:分派会把 run-local 的
@@ -215,6 +215,18 @@ onIncident?: (err: Error & {
215
215
  backend: FileMemoryEngineBackend;
216
216
  root: string;
217
217
  } | undefined;
218
+ /**
219
+ * core 7.49.0 #1103 —— **记忆店根搬动 ⇒ 拒启**(不降级)。店根的物理路径是控制面里的一条记录(`root-anchor.json`),每次打开都比对;
220
+ * 目录被搬走 / 换成指向别处的链接之后,File 后端的**构造器**就抛 `memory.target_outside_store`(`detail` 恰在这一形上带
221
+ * `{ path, controlDir, recorded, now }`)。本函数是本仓构造 File 后端的唯一一处(boot 与 run-local 两个入口都经它),构造在 boot 的 `try`
222
+ * 之外 ⇒ 拒启。不降级成「记忆暗」的理由:降级会让这台部署在运维不知道的情况下丢掉全部记忆(而店还在,只是搬了家);拒启把那一刻交给人。
223
+ * 本函数只把引擎那一句(不带路径)换成**带出路**的一句:放回原处,或按引擎给的两个坐标调 `reanchorMemoryStoreRoot` 重锚后再起服。
224
+ * 码原样(`memory.target_outside_store`),原错误在 `cause`。别的错误(包括同码的别的形:没有 `controlDir` 的就不是根搬动)⇒ `undefined`,原样抛。
225
+ */
226
+ export declare function memoryStoreRootMovedRefusal(e: unknown): (Error & {
227
+ code: string;
228
+ cause: unknown;
229
+ }) | undefined;
218
230
  /**
219
231
  * MF-30 memory PAUSE (option B, per-request — clay 2026-06-27): build the `TaskSpec.memory` spec for ONE run from
220
232
  * the resolved memory `scope` + the request's `memoryWrite` toggle. `memoryWrite === false` ⇒ `writeScope: null`
@@ -81,10 +81,10 @@ export function buildMemoryRemoteLaneWarn(config, engineWired) {
81
81
  "WORKER fs; verify the lane really shares it.",
82
82
  };
83
83
  }
84
- export function memoryEngineRootFor(config, fallbackRoot) {
84
+ export function memoryEngineRootFor(config, localRoot) {
85
85
  if (config.memoryEngineBackend !== "file")
86
- return join(config.localDataRoot ?? fallbackRoot ?? resolveMemoryEngineRoot(undefined), "memory-work");
87
- return fileMemoryEngineRoot(config, fallbackRoot);
86
+ return join(config.localDataRoot ?? localRoot ?? resolveMemoryEngineRoot(undefined), "memory-work");
87
+ return fileMemoryEngineRoot(config);
88
88
  }
89
89
  export function fileMemoryEngineRoot(config, fallbackRoot) {
90
90
  return resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
@@ -109,13 +109,30 @@ export function memoryEngineBackendFor(config, fallbackRoot, onIncident) {
109
109
  const plane = fileMemoryPlaneFor(config, fallbackRoot);
110
110
  if (plane === undefined)
111
111
  return undefined;
112
- return {
113
- backend: new FileMemoryEngineBackend(plane.memoryDir, {
112
+ let backend;
113
+ try {
114
+ backend = new FileMemoryEngineBackend(plane.memoryDir, {
114
115
  controlDir: plane.controlDir,
115
116
  ...(onIncident !== undefined ? { onIncident } : {}),
116
- }),
117
- root: plane.root,
118
- };
117
+ });
118
+ }
119
+ catch (e) {
120
+ throw memoryStoreRootMovedRefusal(e) ?? e;
121
+ }
122
+ return { backend, root: plane.root };
123
+ }
124
+ export function memoryStoreRootMovedRefusal(e) {
125
+ const err = e;
126
+ const d = err?.detail;
127
+ if (err?.code !== "memory.target_outside_store" || typeof d?.path !== "string" || typeof d.controlDir !== "string")
128
+ return undefined;
129
+ const call = `reanchorMemoryStoreRoot(${JSON.stringify(d.path)}, { controlDir: ${JSON.stringify(d.controlDir)} })`;
130
+ const refusal = new Error(`the memory store root moved: its control plane records ${JSON.stringify(String(d.recorded))} but the store now resolves to ${JSON.stringify(String(d.now))} ` +
131
+ `(memory.target_outside_store) — refusing to start rather than read or write memory somewhere else. Put the directory back, or re-anchor it once ` +
132
+ `from this server's directory: node --input-type=module -e 'import("@sema-agent/core").then(m => m.${call})' — then start again.`);
133
+ refusal.code = "memory.target_outside_store";
134
+ refusal.cause = e;
135
+ return refusal;
119
136
  }
120
137
  export function memorySpecForRequest(scope, projectKey, memoryWrite, defaultScopes, originPolicy, declarations) {
121
138
  if (!scope)
@@ -137,7 +137,7 @@ export declare const FAIL_OPEN_TAGS: {
137
137
  };
138
138
  readonly "server.approvals.park-row-blob-unreadable": {
139
139
  readonly cls: "F";
140
- readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
140
+ readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。 **第二座 `member=gate`**(`readParkGateCell`:同一列的 **`gate` 子树**在场却不是一只对象 —— 队列行的卡词与到期两条腿的分法都读它):读不出 = 门上无卡,队列行不带卡词,**到期改道** —— core `facetOf` 读「无卡 = 不是一问」为 `A`,所以一张本该到期**作废**的问答行改走 SLA **拒扫腿**(模型拿到一次「拒」而不是整条 run 作废);改道后的续跑 `get()` 对同一份坏 blob 响亮拒(拒扫腿每拍一条失败),行留停驻、由 `terminal_at_ms` 兜底收割 —— 所以最坏后果**不是**「只少展示」,是一张问答行的到期去向变了、最长挂到绝对上限。放行仍对:不放行的代价与 pendingAction 座同(一行坏 blob 打掉整个租户的队列与到期读腿)。计数 detail = `member=gate` 时,去按 session 查那一行的 `checkpoint.gate`。";
141
141
  };
142
142
  readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
143
143
  readonly cls: "F";
@@ -225,7 +225,7 @@ export declare const FAIL_OPEN_TAGS: {
225
225
  };
226
226
  readonly "server.hooks.matcher-unjudged": {
227
227
  readonly cls: "F";
228
- readonly note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 形只问契约包的 `hookMatcherForm`(literal 形〔名字表 / 空 / `*`〕两档都直接问 `hookMatcherSelects`、不占预算;regex 形 regex 档把同一只契约函数交进 vm 带预算、literal 档不适用;invalid 形 regex 档直接记 `invalid_regex`、literal 档不适用)。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。";
228
+ readonly note: "S-995(`hooks/hook-runner.ts` 的 `toolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 形只问契约包的 `hookMatcherForm`(literal 形〔名字表 / 空 / `*`〕两档都直接问 `hookMatcherSelects`、不占预算;regex 形 regex 档把同一只契约函数交进 vm 带每次求值各一份的固定预算〔超时 = 灾难回溯 ⇒ 确定的 400 拒,不是未判、不登记〕、literal 档不适用;invalid 形 regex 档 = 确定的 400 拒〔S-1455 收尾:不是未判、不登记;运行期另记 `hook_matcher_invalid`〕、literal 档不适用)。现在只有一种 reason —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)。`invalid_regex` 臂自 S-1455 收尾(7.126.0)起退役:它是确定性的坏配置、不是「判不了」,与超预算同类 ⇒ 受理当场 400。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(超长 matcher)显形。";
229
229
  };
230
230
  readonly "server.hooks.notice-sink-threw": {
231
231
  readonly cls: "F";
@@ -313,7 +313,7 @@ export declare const FAIL_OPEN_TAGS: {
313
313
  };
314
314
  readonly "server.approval-card.closed-word-out-of-set": {
315
315
  readonly cls: "F";
316
- readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛)、S-1170 起的 `classifierNoRuling`(core 7.43.0 #1353 的成因词,活卡帧读 `AskRequest.classifierNoRuling`,闭集筛 = core `isAutoModeFallbackCause`)与 `differs`(core 7.44.0 #1374,载体是 `runs.ts` 的 409 `steering.duplicate_input_id` 体读的引擎拒错 `detail.differs`,闭集筛 = core `STEER_REPLAY_FACETS` 的非空子集;集外成员 / 空 / 非数组 ⇒ 不铸 `detail`,码仍是契约);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` / `classifierNoRuling=` / `differs=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
316
+ readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天四个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛)、S-1170 起的 `classifierNoRuling`(core 7.43.0 #1353 的成因词,活卡帧读 `AskRequest.classifierNoRuling`,闭集筛 = core `isAutoModeFallbackCause`)与 `differs`(core 7.44.0 #1374,载体是 `runs.ts` 的 409 `steering.duplicate_input_id` 体读的引擎拒错 `detail.differs`,闭集筛 = core `STEER_REPLAY_FACETS` 的非空子集;集外成员 / 空 / 非数组 ⇒ 不铸 `detail`,码仍是契约);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` / `classifierNoRuling=` / `differs=` 判别是哪一座;第四个成员是 `approval-content-kind.ts` 的 `readApprovalCard` —— 卡词 `approvalCard` 自己的闭集窄读,活 ask / LOCAL 店停驻门 / SQL 店读出的门子树三个载体共用一只,闭集 = core `TOOL_APPROVAL_CARDS`,detail 前缀 `approvalCard=`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
317
317
  };
318
318
  readonly "server.pg-pool.idle-client-error": {
319
319
  readonly cls: "F";
@@ -419,6 +419,18 @@ export declare const FAIL_OPEN_TAGS: {
419
419
  readonly bound: "FLEET_LEASE_OUTAGE_MAX_MS";
420
420
  readonly note: "`fleet/serving-lease.ts` 的 `noteCenterCapabilities`(fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。归 R:能力门控是契约规则(中心不宣告 = 中心不执行围栏),裁定写定替换只来自中心权威终止或运维覆盖;位只开关围栏、不碰钟 —— 位缺席期间 `T_max` 的钟照走(最后一次授予的到期 / 本进程第一次见到位的时刻),位一回来即按它判,所以位的抖动绕不过 `FLEET_LEASE_OUTAGE_MAX_MS`。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务(这一段由中心的选择定界,不由引擎)。计数长期保留:非零 = 这台中心没宣告租约能力。";
421
421
  };
422
+ readonly "server.workspace-cache.unavailable": {
423
+ readonly cls: "F";
424
+ readonly note: "k8s 车道按工作区空间的构建缓存卷(`WORKSPACE_CACHE=volume`,`plugins/remote-env-k8s.ts` 连沙箱那一步):这条 run 拿不到它空间的缓存卷 —— 卷建不成(RBAC 少动词 / API 拒 / 网络错,`reason=claim_failed`)、卷在快照时限内没绑定(集群没有缺省 StorageClass / 供给器不在,`reason=claim_unbound`)、卷绑上了而挂着它的 pod 起不来(挂载失败 / 卷所在节点没了,`reason=claim_unmountable`)—— 后两种把等它的那只 pod 删掉,换一只不挂卷的 —— 、或本副本读不出会话住客没有空间键(`reason=incarnation_unknown`)⇒ run **照常起**:不挂卷、不注入工具缓存 env,工具按镜像自己的缓存目录工作(每轮重下依赖)。放行的最坏后果 = 这一轮构建慢(缓存定义上可再生),不是门、不是凭据、不碰工作区与快照;反方向(卷拿不到就拒这一轮)会让一个缓存把整条 run 打掉。同拍一行运维日志 `workspace_cache_unavailable`(`space` 前缀 / `reason` / `error`)。计数非零 = 这台部署开了缓存卷却拿不到,运维去看 StorageClass / Role / 配额。";
425
+ };
426
+ readonly "server.workspace-cache.reclaim-failed": {
427
+ readonly cls: "F";
428
+ readonly note: "k8s 车道按工作区空间的构建缓存卷的**回收**(`plugins/remote-env-k8s.ts` `createSpaceReclaimer`;两条腿同一只回收座:会话删除协调器 `boot/session-faces.ts` `purgeSession` 与托管留存 sweep `boot/retention-lane.ts` 删掉会话树之后):按标签 `sema.space=<空间摘要>` 删卷的请求失败(Role 少 `deletecollection` / API 拒 / 网络,`reason=delete_failed`)、回收座这次调用的绝对截止(= 快照时限)先到(请求没发或在途被切,`reason=budget`),或在线腿删除前读不出被删会话的住客(`reason=occupant_unreadable`;化身缺席的存量行 = 没有空间,不计)⇒ 卷留着(住客读不出时该空间的快照对象也没回收,归桶的生命周期规则),会话照删(不挡删除)。放行的最坏后果 = 节点盘上多留一只没人再用的卷(存储泄漏,不碰任何别的空间、不是门不是凭据);反方向(删卷失败就拒删会话)会让一个缓存挡住删除权。同拍一行 `workspace_cache_reclaim_failed`(回收座的两形:`space` 前缀 / `label` / `reason` / `error`;住客读不出那一形:`sessionId` / `reason` / `error`)。计数非零 = 有卷没回收掉,运维按行里的标签手清,并核 Role。";
429
+ };
430
+ readonly "server.workspace-space.reclaim-failed": {
431
+ readonly cls: "F";
432
+ readonly note: "k8s 车道工作区空间结束时对**快照对象**的回收(`plugins/remote-env-k8s.ts` `createSpaceReclaimer` 的对象部分;与上一条同一只回收座、两条腿:会话删除协调器与托管留存 sweep):按空间的 `<前缀>/<化身>/space/` 分区(快照 + `latest`)和该会话停驻车道里名字带被结束住客归属记号的 `<前缀>/<会话 id>/*.tar.gz` 分别 LIST、逐个预签 DELETE;LIST 失败、DELETE 失败(HTTP 非 2xx / 非 404、网络)LIST 超过 1000 个对象被截断,或回收座一次调用的绝对截止(= 快照时限;卷回收也在内)到点〔切断在途请求、停掉余下的;这次调用里还没轮到的空间零请求、各记一次;没删的归生命周期规则〕,各记一次(detail `lane=space|park stage=list|delete|budget`;会话删除口把回收座脱离应答路径,座若抛了——契约禁止——记 `stage=seat_threw`)⇒ 这些对象留在桶里,会话照删(不挡删除)。放行的最坏后果 = 一份已无人能读的工作区快照留在桶里(会话已删,经 API 不可达;内容是用户工作区,所以不是「只占盘」—— 留到桶的生命周期规则把它过期掉为止);不碰任何别的空间、不是门不是凭据;反方向(清不掉对象就拒删会话)会让一个对象存储抖动挡住用户的删除权。故 F 类,但必须留痕:静默吞掉之后「这个会话删了、它的工作区内容还在不在桶里」在遥测里没有任何痕迹。同拍一行 `workspace_space_reclaim_failed`(`space` 前缀 / `sessionId` / `lane` / `stage` / `error`)。计数非零 = 有对象没回收掉,运维按行里的空间前缀 / 会话 id 去桶里手清,并核对象存储的可达性与 DELETE 权限。回收与同一空间的尚在途的 run 末提交不是互斥的:会话删除时没有活 run(409 挡),但上一条 run 的提交若恰在删除之后才落对象,这批对象不在本次回收射程内(同样归桶的生命周期规则)。停驻分区按会话 id 分区,所以只删名字带被结束住客归属记号的对象(不读任何钟)—— 会话 id 删后被别人重新登记,新住客停驻出的快照带它自己的记号、不碰;不带记号的旧形与 `none` 留给桶的生命周期规则。";
433
+ };
422
434
  readonly "server.fleet.announce-reply-unreadable": {
423
435
  readonly cls: "P-DEBT";
424
436
  readonly note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。";