@sema-agent/server 7.98.0 → 7.100.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 (153) hide show
  1. package/MIGRATION.md +19 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +86 -60
  5. package/deploy/sema-up/lib/envf.sh +110 -42
  6. package/deploy/sema-up/sema-up.sh +76 -34
  7. package/dist/approval-ask-machine.d.ts +10 -0
  8. package/dist/approval-ask-machine.js +3 -0
  9. package/dist/approval-card.d.ts +74 -1
  10. package/dist/approval-card.js +41 -8
  11. package/dist/approval.d.ts +44 -20
  12. package/dist/approval.js +14 -6
  13. package/dist/boot/config-center.d.ts +16 -8
  14. package/dist/boot/config-center.js +206 -103
  15. package/dist/boot/execution-env.d.ts +1 -2
  16. package/dist/boot/execution-env.js +2 -2
  17. package/dist/boot/leader.js +12 -9
  18. package/dist/boot/memory-consolidation.d.ts +38 -0
  19. package/dist/boot/memory-consolidation.js +35 -0
  20. package/dist/boot/org-memory.d.ts +2 -1
  21. package/dist/boot/org-memory.js +9 -5
  22. package/dist/boot/resolve-spec.d.ts +0 -1
  23. package/dist/boot/resolve-spec.js +4 -3
  24. package/dist/boot/runner-deps.d.ts +2 -2
  25. package/dist/boot/runtime-caps.js +4 -4
  26. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +3 -3
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  30. package/dist/boot/stage-07-capability-layer.js +6 -6
  31. package/dist/boot/stage-08-reapers.d.ts +3 -4
  32. package/dist/boot/stage-08-reapers.js +2 -2
  33. package/dist/boot/stage-09-leader.d.ts +2 -3
  34. package/dist/boot/stage-10-http-server.d.ts +2 -3
  35. package/dist/boot/stage-10-http-server.js +2 -2
  36. package/dist/capabilities/center-plugins.d.ts +14 -2
  37. package/dist/capabilities/center-plugins.js +13 -5
  38. package/dist/capabilities/center-prompts.d.ts +3 -0
  39. package/dist/capabilities/center-prompts.js +1 -1
  40. package/dist/capabilities/hands-lane.js +2 -1
  41. package/dist/capabilities/scenarios.js +18 -5
  42. package/dist/capabilities/tool-defer.d.ts +2 -2
  43. package/dist/center-credential.d.ts +112 -0
  44. package/dist/center-credential.js +131 -0
  45. package/dist/config-catalog.d.ts +2 -2
  46. package/dist/config-catalog.js +15 -15
  47. package/dist/config-center/apply-effective.d.ts +34 -39
  48. package/dist/config-center/apply-effective.js +100 -64
  49. package/dist/config-center/apply-ledger.d.ts +35 -16
  50. package/dist/config-center/apply-ledger.js +25 -18
  51. package/dist/config-center/center-request.d.ts +60 -0
  52. package/dist/config-center/center-request.js +71 -0
  53. package/dist/config-center/facade.d.ts +11 -5
  54. package/dist/config-center/facade.js +1 -1
  55. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  56. package/dist/config-center/hot-keys-registry.js +11 -7
  57. package/dist/config-center/http-client.d.ts +17 -9
  58. package/dist/config-center/http-client.js +27 -36
  59. package/dist/config-center/restart-signal.d.ts +21 -14
  60. package/dist/config-center/restart-signal.js +16 -22
  61. package/dist/config-center/skills-mcp.d.ts +4 -1
  62. package/dist/config-center/skills-mcp.js +2 -2
  63. package/dist/config-center/types.d.ts +3 -3
  64. package/dist/config-invariants.d.ts +2 -2
  65. package/dist/config-invariants.js +0 -8
  66. package/dist/config-lkg.d.ts +47 -8
  67. package/dist/config-lkg.js +107 -20
  68. package/dist/config-provider.d.ts +8 -7
  69. package/dist/config-provider.js +5 -5
  70. package/dist/config-types.d.ts +30 -11
  71. package/dist/config.js +11 -13
  72. package/dist/cross-session-settings.js +1 -1
  73. package/dist/fleet-client.d.ts +8 -8
  74. package/dist/fleet-client.js +7 -7
  75. package/dist/fleet-lease.d.ts +11 -11
  76. package/dist/fleet-lease.js +17 -13
  77. package/dist/host-lsp-manager.d.ts +29 -0
  78. package/dist/host-lsp-manager.js +14 -0
  79. package/dist/hosted-posture.d.ts +49 -93
  80. package/dist/hosted-posture.js +1 -25
  81. package/dist/http/active-run-conflict.d.ts +2 -12
  82. package/dist/http/active-run-conflict.js +1 -6
  83. package/dist/http/admission.js +8 -0
  84. package/dist/http/resume-legs.d.ts +1 -1
  85. package/dist/http/resume-legs.js +14 -10
  86. package/dist/http/route-ctx.d.ts +2 -2
  87. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  88. package/dist/http/routes/approvals-assistant.js +2 -2
  89. package/dist/http/routes/memory-compliance.js +5 -0
  90. package/dist/http/routes/memory-origin.js +5 -0
  91. package/dist/http/routes/sessions.js +6 -0
  92. package/dist/http/routes/tasks.js +7 -6
  93. package/dist/http/server.d.ts +6 -5
  94. package/dist/http/wire-types.d.ts +6 -3
  95. package/dist/leader/wire.d.ts +20 -12
  96. package/dist/leader/wire.js +7 -6
  97. package/dist/memory-layer-preflight.d.ts +46 -0
  98. package/dist/memory-layer-preflight.js +121 -0
  99. package/dist/memory-operator-faces.js +17 -1
  100. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  101. package/dist/observability/corrupt-read-seat.js +31 -0
  102. package/dist/observability/fail-open.d.ts +8 -4
  103. package/dist/observability/fail-open.js +8 -4
  104. package/dist/observability/run-terminal.js +1 -0
  105. package/dist/observability/secret-env-scrub.d.ts +3 -26
  106. package/dist/observability/secret-env-scrub.js +3 -2
  107. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  108. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  109. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  110. package/dist/plugins/approval-ask-store-file.js +21 -3
  111. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  112. package/dist/plugins/approval-ask-store-memory.js +39 -13
  113. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  114. package/dist/plugins/approval-ask-store-sql.js +27 -3
  115. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  116. package/dist/plugins/checkpoint-store-sql.js +19 -2
  117. package/dist/plugins/local-checkpoint-store.js +2 -0
  118. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  119. package/dist/plugins/mailbox-store-sql.js +6 -4
  120. package/dist/plugins/permission-rule-store-file.js +3 -2
  121. package/dist/plugins/remote-env-host.d.ts +3 -1
  122. package/dist/plugins/remote-env-host.js +3 -2
  123. package/dist/plugins/sql-json-column.d.ts +16 -3
  124. package/dist/plugins/sql-json-column.js +15 -4
  125. package/dist/plugins/web-search.d.ts +42 -22
  126. package/dist/plugins/web-search.js +141 -49
  127. package/dist/project-memory.d.ts +28 -1
  128. package/dist/project-memory.js +22 -22
  129. package/dist/run-local.js +6 -7
  130. package/dist/runs.js +6 -5
  131. package/dist/runtime-caps-resolver.d.ts +8 -8
  132. package/dist/runtime-caps-resolver.js +16 -14
  133. package/dist/runtime-governance.d.ts +50 -3
  134. package/dist/runtime-governance.js +5 -0
  135. package/dist/sealed-key.js +3 -7
  136. package/dist/server-secret-env.d.ts +43 -0
  137. package/dist/server-secret-env.js +18 -0
  138. package/dist/task-settings.d.ts +1 -1
  139. package/dist/tool-approval.d.ts +144 -9
  140. package/dist/tool-approval.js +74 -18
  141. package/dist/trace/core-keyset-guard.d.ts +5 -5
  142. package/dist/trace/ledger-sink.js +2 -2
  143. package/dist/trace/project.d.ts +13 -0
  144. package/dist/trace/project.js +22 -5
  145. package/dist/trace/projection-drop.d.ts +2 -0
  146. package/dist/trace/projection-drop.js +6 -0
  147. package/dist/trace/sema-provenance.d.ts +4 -1
  148. package/dist/trace/sema-provenance.js +1 -1
  149. package/dist/trace/task-notification-facets.d.ts +65 -0
  150. package/dist/trace/task-notification-facets.js +31 -0
  151. package/dist/trace/wire-projection-faces.d.ts +3 -3
  152. package/dist/trace/wire-projection-faces.js +3 -3
  153. package/package.json +5 -4
@@ -9,6 +9,7 @@ import { readSessionPermissionRules, describeSessionPermissionRulesProblem } fro
9
9
  import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
10
10
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
11
11
  import { assertNever } from "../route-ctx.js";
12
+ import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
12
13
  const MCP_STATUS_TTL_MS = 5_000;
13
14
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
14
15
  const A2A_STATUS_TTL_MS = 5_000;
@@ -499,6 +500,11 @@ export async function handleSessions(req, res, match, ctx) {
499
500
  sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
500
501
  }
501
502
  catch (err) {
503
+ if (err instanceof MemoryLayerUnwritableError) {
504
+ deps.logger?.warn?.("session_memory_erase_layer_unwritable", { sessionId, dirs: err.dirs });
505
+ sendError(res, 409, "memory.layer_unwritable", "memory erasure refused: a memory layer directory on this server is not writable on disk, so the erase was stopped before anything was written (nothing was deleted, no journal was left). Ask whoever operates this deployment to restore the owner write bit on the memory layer directories (a known issue of the memory engine's mount), then retry with the same requestId");
506
+ return;
507
+ }
502
508
  const code = err.code;
503
509
  if (code === "config.memory_erasure_request") {
504
510
  sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
@@ -1,12 +1,12 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
- import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
2
+ import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
3
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
8
8
  import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
9
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
9
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
10
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
@@ -393,7 +393,7 @@ export async function handleTasks(req, res, match, ctx) {
393
393
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
394
394
  }
395
395
  else if (deliverable) {
396
- const key = taskNotificationStreamKey(n);
396
+ const key = taskNotificationDedupKey(n);
397
397
  if (syncNotifiedKeys.get(key) === undefined) {
398
398
  syncNotifiedKeys.set(key, Promise.resolve(true));
399
399
  sseData(res, { type: "task_notification", ...taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) }) });
@@ -465,7 +465,7 @@ export async function handleTasks(req, res, match, ctx) {
465
465
  }
466
466
  else if (ev.type === "task_notification") {
467
467
  const tnv = ev.notification;
468
- const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
468
+ const tnvKey = taskNotificationStreamKeyOf(tnv);
469
469
  const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
470
470
  if (tnvPrior === undefined || !(await tnvPrior)) {
471
471
  if (tnvKey !== undefined)
@@ -553,8 +553,9 @@ export async function handleTasks(req, res, match, ctx) {
553
553
  if (closed || res.writableEnded || res.destroyed)
554
554
  throw new Error("stream ended before the completion frame was written");
555
555
  const f = frame;
556
- if (f.type === "task_notification" && f.task_id)
557
- syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
556
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
557
+ if (fKey !== undefined)
558
+ syncNotifiedKeys.set(fKey, Promise.resolve(true));
558
559
  sseData(res, frame);
559
560
  }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
560
561
  const withElicit = deps.elicitation
@@ -659,7 +659,7 @@ export interface ServiceDeploymentDeps {
659
659
  * registry 部署),现有部署零影响。main.ts 在 applyEffective 落模型后翻 true(one-way latch)。 */
660
660
  modelReady?: () => boolean;
661
661
  /** Sema-registry restart signal: the refresh loop sets this when a pull carries restart-to-apply
662
- * changes (skills/mcp/scenarios/runtime-gates baked at boot) that DIFFER from this process's boot config.
662
+ * changes (skills/mcp/a2a/scenarios/… baked at boot — the `RestartSlice` closed set) that DIFFER from this process's boot config.
663
663
  * Surfaced at GET /health as `restartRequired` + `restart{reasons,version,since}` so the orchestrator can
664
664
  * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot
665
665
  * changed, so polling it never causes a restart loop. */
@@ -686,10 +686,11 @@ export interface ServiceDeploymentDeps {
686
686
  mcpRevocationState?: () => {
687
687
  revoked: string[];
688
688
  } | undefined;
689
- /** codex R18 (models-tiers deferral): the persistent "deferred WITHOUT a next-boot handoff" state — set when a
690
- * changed model plane could not hot-apply (tier-frozen Runner / tiered candidate) AND no restart-surviving LKG
691
- * exists to justify forcing a restart. Surfaced on /health (`modelPlaneDeferred`) so the stuck-degraded state
692
- * outlives the one-shot warn (the etag advances → later ticks 304 and never re-enter the defer branch). */
689
+ /** codex R18: the persistent "retained WITHOUT a next-boot handoff" state — a candidate whose restart signal is
690
+ * withheld because no restart-surviving handoff exists yet (durable-declared LKG publication incomplete, or a
691
+ * boot skills debt not yet proven repairable). Surfaced on /health (`modelPlaneDeferred` — the key keeps its birth
692
+ * name: it was born for the model-plane deferral, which retired with the `models-tiers` slice in S-668 波 0) so
693
+ * the stuck state outlives the one-shot warn (the etag advances → later ticks 304). */
693
694
  planeDeferredState?: () => {
694
695
  version: number;
695
696
  since: number;
@@ -450,9 +450,12 @@ export interface TaskRequestBody {
450
450
  /** The caller's per-request settings stamp (the client-side `SemaSettings` shape). The KEY SET is deliberately
451
451
  * NOT mirrored here — a hand-written second shape on this interface would be the classic mirror-drift form.
452
452
  * Per-key ownership is split, not single: `parseTaskSettings` (src/task-settings.ts) defensively owns the
453
- * shared settings keys on every path, while `settings.webSearch` has its OWN validator on the single-user
454
- * scenario lane (capabilities/scenarios.ts `webSearchConfigFromSettings` — the multi-tenant lane ignores it
455
- * by design). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
453
+ * shared settings keys on every path, while `settings.webSearch` has its OWN judge (plugins/web-search.ts
454
+ * `judgeRequestWebSearch`, S-199): a malformed block — including a missing or unknown `provider` — 400s on submit
455
+ * on every lane, and only the single-user
456
+ * scenario lane honors a well-formed one (capabilities/scenarios.ts — the multi-tenant lane ignores it by design;
457
+ * a malformed block replayed on resume mounts no WebSearch for that call — never a substitute destination — with a
458
+ * fail-open count). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
456
459
  * runtime drops garbage): a non-array object bag, or omit the key. A `null` reads as absence on both the
457
460
  * fresh-submit wall (prepareSpec) and replay (parseTaskSettings), and malformed forms 400 on submit /
458
461
  * no-op on replay. ([ref] 件1 leftover key, declared 2026-08-31; ownership wording per codex adversarial round.)
@@ -3,6 +3,15 @@ import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
3
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
5
5
  import type { LeaderRequestBody } from "./endpoint.js";
6
+ /** leader worker 的 durable 审批座(`LeaderWireConfig.durable` 函数席每次产出的一份)。 */
7
+ export interface LeaderDurableSeat {
8
+ readonly checkpointStore: CheckpointStoreFull;
9
+ readonly requireApproval: string[];
10
+ readonly deny?: string[];
11
+ readonly ttlMs?: number;
12
+ readonly autoBudget?: number;
13
+ readonly neverAuto?: string[];
14
+ }
6
15
  export interface LeaderWireConfig {
7
16
  /** Static-env compat mode (E2B only): the leader provisions/owns each env. Required when `envFactory` unset. */
8
17
  e2bApiKey?: string;
@@ -10,18 +19,17 @@ export interface LeaderWireConfig {
10
19
  * REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory.
11
20
  * Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */
12
21
  envFactory?: ExecutionEnvFactory;
13
- /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when set, each worker task carries the durable
14
- * question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker (checkpoint +
15
- * paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's per-task
16
- * durable wiring; ctx is seeded at provision so the surfaced token is resumable on the single-agent path. */
17
- durable?: {
18
- checkpointStore: CheckpointStoreFull;
19
- requireApproval: string[];
20
- deny?: string[];
21
- ttlMs?: number;
22
- autoBudget?: number;
23
- neverAuto?: string[];
24
- };
22
+ /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when the seat yields a value, that worker task carries
23
+ * the durable question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker
24
+ * (checkpoint + paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's
25
+ * per-task durable wiring; ctx is seeded at provision so the surfaced token is resumable on the single-agent path.
26
+ *
27
+ * 🔴 **函数席,不是快照**(S-668 波 2 ①;先例 = 下方 `deploymentPosture` 的 S-174 函数席):审批名单
28
+ * (`governance.approvalRequire`)自本批起在进程内会被中心热换。boot 期取一次值会把这条腿钉在起服那一刻的名单上
29
+ * —— 起服时名单为空 ⇒ 整块缺席、之后加进来的工具在 leader worker 上**永远**不被门住(fail-OPEN)。
30
+ * `provisionWorker` 每只 worker 调一次(同一只 worker 的 toolPolicy / resource 停车 / ctx 保温读同一份);
31
+ * 返回 `undefined` = 这只 worker 不接 durable 审批(与修前「键缺席」同义)。 */
32
+ durable?: () => LeaderDurableSeat | undefined;
25
33
  /** Durable tool-result offload store (TiDB). REQUIRED for `durable` to actually suspend: core's suspend
26
34
  * pre-commit refuses with the default InMemoryToolResultStore (offloaded results would deref to null after
27
35
  * a cross-replica resume) and silently falls back to the synchronous onAsk gate — which the leader does not
@@ -241,9 +241,10 @@ export function createLeaderRunner(cfg) {
241
241
  };
242
242
  const provisionWorker = async (sub) => {
243
243
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
244
+ const durable = cfg.durable?.();
244
245
  let durableSpec = {};
245
- if (cfg.durable) {
246
- const d = cfg.durable;
246
+ if (durable) {
247
+ const d = durable;
247
248
  durableSpec = {
248
249
  toolPolicy: combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({ requireApproval: d.requireApproval, ...(d.deny ? { deny: d.deny } : {}), ...(d.autoBudget ? { autoBudget: d.autoBudget } : {}), ...(d.neverAuto ? { neverAuto: d.neverAuto } : {}) })),
249
250
  checkpointStore: d.checkpointStore,
@@ -259,9 +260,9 @@ export function createLeaderRunner(cfg) {
259
260
  if (repairLoopActive && resourceCfg) {
260
261
  cfg.logger?.warn?.("leader_resource_suspend_disabled_for_repair_loop", { workerId: sub.workerId, reason: "LEADER_REPAIR_LOOP active — resource-suspend mutually exclusive with the repair loop (§10.5)" });
261
262
  }
262
- if (resourceCfg && !repairLoopActive && cfg.durable && cfg.toolResultStore && cfg.sessionStore) {
263
+ if (resourceCfg && !repairLoopActive && durable && cfg.toolResultStore && cfg.sessionStore) {
263
264
  resourceSpec = {
264
- checkpointStore: cfg.durable.checkpointStore,
265
+ checkpointStore: durable.checkpointStore,
265
266
  resourceSuspend: { scope: `_leader-resource-${sub.workerId}`, totalBudgetUsd: resourceCfg.totalBudgetUsd },
266
267
  limits: { ...workerLimits.limits, maxCostUsd: resourceCfg.sliceMaxCostUsd },
267
268
  maxSuspends: resourceCfg.maxSuspends,
@@ -270,7 +271,7 @@ export function createLeaderRunner(cfg) {
270
271
  else if (resourceCfg && !repairLoopActive) {
271
272
  cfg.logger?.warn?.("leader_resource_suspend_inactive", {
272
273
  workerId: sub.workerId,
273
- reason: !cfg.durable
274
+ reason: !durable
274
275
  ? "no durable checkpoint store"
275
276
  : !cfg.toolResultStore
276
277
  ? "no durable tool-result store"
@@ -278,7 +279,7 @@ export function createLeaderRunner(cfg) {
278
279
  });
279
280
  }
280
281
  const keepCtxWarm = (runner) => {
281
- const d = cfg.durable;
282
+ const d = durable;
282
283
  if (!d)
283
284
  return runner;
284
285
  const orig = runner.runTaskStream.bind(runner);
@@ -0,0 +1,46 @@
1
+ /** 一只挡路的目录。`mode` = 权限位的四位八进制(如 `"0555"`)。 */
2
+ export interface UnwritableLayerDir {
3
+ readonly dir: string;
4
+ readonly mode: string;
5
+ }
6
+ /**
7
+ * 挂载面整棵树里挡路的目录(排序后返回,空数组 = 放行)。判据见文件头「判定集合」,逐目录三步:
8
+ * ① 缺**搜索位**(X_OK)⇒ 挡路、不下钻 —— 经过它的任何查找都撞 EACCES(不存在的删除目标也一样,`force` 只吞
9
+ * ENOENT),里面空不空都一样(codex 验修轮 V2:0400 / 0600 的空目录 + 账本仍绑定 ⇒ journal 卡盘);
10
+ * ② 只缺**写位**(W_OK)⇒ 装着任何非目录项(文件 / 软链)或列不出内容 ⇒ 挡路;空的、只装子目录的不挡
11
+ * (core 锁出来的 0555 有 x,「空的锁住 local 不拒」即此臂);
12
+ * ③ 下钻:子目录照走;**指向挂载面内目录的软链也顺着走**(codex 验修轮 V1:删除路径可以穿过它),按目标的 `dev:ino` 去重防环;
13
+ * 目标在挂载面外 ⇒ 不下钻、抛 {@link MemoryLayerSymlinkEscapeError}(NP-4)。
14
+ *
15
+ * 挂载面本身不存在 ⇒ 空(店还没建出来,没有可删的东西)。可写可搜索却列不出内容的目录、判不了的软链 ⇒ 抛原错误
16
+ * (判不了子树 ⇒ 不放行;不借本码 —— 出路不是 `chmod u+w`)。
17
+ */
18
+ export declare function unwritableLayerDirs(mountRoot: string): UnwritableLayerDir[];
19
+ /**
20
+ * NP-4 —— 挂载面里一条指向**树外**目录的软链:预检判不了(树外那棵子树里会不会删到、删不删得掉,本层不下钻去猜),
21
+ * 按 P 类 fail-closed 响亮拒 —— 与 ELOOP / EACCES 那一臂同形(原样抛 ⇒ 路由的未分类 500 + `memory_*_failed` warn,
22
+ * 文案只进日志)。刻意**不**是 {@link MemoryLayerUnwritableError}:那一码的出路是 `chmod u+w <dir>`,对树外目录开这句出路
23
+ * 等于教运维改系统目录的权限位。出路 = 把这条软链移出记忆层(日志点名软链与目标)。没有 `code` 字段:它不是 wire 码。
24
+ */
25
+ export declare class MemoryLayerSymlinkEscapeError extends Error {
26
+ readonly link: string;
27
+ readonly target: string;
28
+ constructor(link: string, target: string);
29
+ }
30
+ /**
31
+ * 预检拒绝(带结构字段的错误值)。路由按 `instanceof` 判别(不嗅文案、不裸 as-cast);409 文案写在**路由**的
32
+ * 内联模板里(文案冻结门只看得见路由站点),本类只供两串排好版的插值。
33
+ */
34
+ export declare class MemoryLayerUnwritableError extends Error {
35
+ readonly dirs: readonly UnwritableLayerDir[];
36
+ readonly code = "memory.layer_unwritable";
37
+ /** 409 文案的「哪些目录」插值:`"<dir>" (mode 0555), …`,超过上限只报余数。 */
38
+ readonly dirList: string;
39
+ /** 409 文案的出路插值:`chmod u+w` 的参数串 —— POSIX 单引号转义(照抄进 shell 不执行、不展开;codex R1-F3:
40
+ * JSON 双引号挡不住 `$()` / 反引号 / `$VAR`)。目录清单那一串用 JSON 引号只作展示。 */
41
+ readonly chmodTargets: string;
42
+ constructor(dirs: readonly UnwritableLayerDir[]);
43
+ }
44
+ /** 两个 seam 的唯一入口:挡路 ⇒ 带码拒绝;放行 ⇒ `undefined`。 */
45
+ export declare function memoryLayerWriteRefusal(mountRoot: string): MemoryLayerUnwritableError | undefined;
46
+ //# sourceMappingURL=memory-layer-preflight.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { accessSync, constants as fsConstants, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ const NAMED_DIRS_MAX = 8;
4
+ function errnoOf(err) {
5
+ return err.code;
6
+ }
7
+ function accessState(dir, bit) {
8
+ try {
9
+ accessSync(dir, bit);
10
+ return "granted";
11
+ }
12
+ catch (err) {
13
+ return errnoOf(err) === "ENOENT" ? "gone" : "denied";
14
+ }
15
+ }
16
+ function symlinkTarget(path) {
17
+ let st;
18
+ try {
19
+ st = statSync(path);
20
+ }
21
+ catch (err) {
22
+ if (errnoOf(err) === "ENOENT")
23
+ return "dangling";
24
+ throw err;
25
+ }
26
+ return st.isDirectory() ? "directory" : "other";
27
+ }
28
+ export function unwritableLayerDirs(mountRoot) {
29
+ const out = [];
30
+ const seen = new Set();
31
+ const stack = [mountRoot];
32
+ let mountReal;
33
+ const mountRealOnce = () => (mountReal ??= realpathSync(mountRoot));
34
+ for (let dir = stack.pop(); dir !== undefined; dir = stack.pop()) {
35
+ const search = accessState(dir, fsConstants.X_OK);
36
+ if (search === "gone")
37
+ continue;
38
+ if (search === "denied") {
39
+ out.push(...blocker(dir));
40
+ continue;
41
+ }
42
+ const id = statSync(dir, { throwIfNoEntry: false });
43
+ if (id === undefined)
44
+ continue;
45
+ const key = `${id.dev}:${id.ino}`;
46
+ if (seen.has(key))
47
+ continue;
48
+ seen.add(key);
49
+ const write = accessState(dir, fsConstants.W_OK);
50
+ if (write === "gone")
51
+ continue;
52
+ let children;
53
+ try {
54
+ children = readdirSync(dir, { withFileTypes: true });
55
+ }
56
+ catch (err) {
57
+ if (errnoOf(err) === "ENOENT")
58
+ continue;
59
+ if (write === "granted")
60
+ throw err;
61
+ out.push(...blocker(dir));
62
+ continue;
63
+ }
64
+ if (write === "denied" && children.some((c) => !c.isDirectory()))
65
+ out.push(...blocker(dir));
66
+ for (const c of children) {
67
+ const child = join(dir, c.name);
68
+ if (c.isDirectory())
69
+ stack.push(child);
70
+ else if (c.isSymbolicLink() && symlinkTarget(child) === "directory") {
71
+ assertInsideMount(child, mountRealOnce());
72
+ stack.push(child);
73
+ }
74
+ }
75
+ }
76
+ return out.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
77
+ }
78
+ function assertInsideMount(link, mountReal) {
79
+ const target = realpathSync(link);
80
+ const rel = relative(mountReal, target);
81
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
82
+ throw new MemoryLayerSymlinkEscapeError(link, target);
83
+ }
84
+ export class MemoryLayerSymlinkEscapeError extends Error {
85
+ link;
86
+ target;
87
+ constructor(link, target) {
88
+ super(`memory layer preflight cannot judge the mount: ${JSON.stringify(link)} is a symlink to ${JSON.stringify(target)}, outside the memory mount — not followed; move the symlink out of the memory layer and retry`);
89
+ this.link = link;
90
+ this.target = target;
91
+ this.name = "MemoryLayerSymlinkEscapeError";
92
+ }
93
+ }
94
+ function blocker(dir) {
95
+ const st = statSync(dir, { throwIfNoEntry: false });
96
+ return st === undefined ? [] : [{ dir, mode: (st.mode & 0o7777).toString(8).padStart(4, "0") }];
97
+ }
98
+ function shellQuote(s) {
99
+ return `'${s.replace(/'/g, "'\\''")}'`;
100
+ }
101
+ export class MemoryLayerUnwritableError extends Error {
102
+ dirs;
103
+ code = "memory.layer_unwritable";
104
+ dirList;
105
+ chmodTargets;
106
+ constructor(dirs) {
107
+ const named = dirs.slice(0, NAMED_DIRS_MAX);
108
+ const rest = dirs.length - named.length;
109
+ const dirList = named.map((d) => `${JSON.stringify(d.dir)} (mode ${d.mode})`).join(", ") + (rest > 0 ? `, and ${rest} more` : "");
110
+ super(`memory layer directories are not writable on disk: ${dirList}`);
111
+ this.dirs = dirs;
112
+ this.name = "MemoryLayerUnwritableError";
113
+ this.dirList = dirList;
114
+ this.chmodTargets = named.map((d) => shellQuote(d.dir)).join(" ");
115
+ }
116
+ }
117
+ export function memoryLayerWriteRefusal(mountRoot) {
118
+ const dirs = unwritableLayerDirs(mountRoot);
119
+ return dirs.length > 0 ? new MemoryLayerUnwritableError(dirs) : undefined;
120
+ }
121
+ //# sourceMappingURL=memory-layer-preflight.js.map
@@ -1,5 +1,6 @@
1
1
  import { MemoryEngine, } from "@sema-agent/core";
2
2
  import { memoryMountRootOf } from "./memory-scope.js";
3
+ import { memoryLayerWriteRefusal } from "./memory-layer-preflight.js";
3
4
  export function backendOwnsControlPlane(backend) {
4
5
  const b = backend;
5
6
  return typeof b?.controlPlaneRoot === "string" && b.controlPlaneRoot !== "";
@@ -40,6 +41,7 @@ function erasureEnvelopeRefusal(input, allowedScopes) {
40
41
  export function createMemoryComplianceFaces(store, opts) {
41
42
  if (!backendOwnsControlPlane(store.backend))
42
43
  return undefined;
44
+ const mountRoot = memoryMountRootOf(store.backend, store.root);
43
45
  const engine = new MemoryEngine({
44
46
  backend: store.backend,
45
47
  memoryDir: memoryMountRootOf(store.backend, store.root),
@@ -57,6 +59,14 @@ export function createMemoryComplianceFaces(store, opts) {
57
59
  const refusal = erasureEnvelopeRefusal(input, opts?.allowedScopes);
58
60
  if (refusal !== undefined)
59
61
  return Promise.reject(refusal);
62
+ try {
63
+ const locked = memoryLayerWriteRefusal(mountRoot);
64
+ if (locked !== undefined)
65
+ return Promise.reject(locked);
66
+ }
67
+ catch (err) {
68
+ return Promise.reject(err);
69
+ }
60
70
  return engine.eraseMemoryEntries(input);
61
71
  },
62
72
  };
@@ -64,6 +74,7 @@ export function createMemoryComplianceFaces(store, opts) {
64
74
  export function createMemoryOriginFaces(store, opts) {
65
75
  if (!backendOwnsControlPlane(store.backend))
66
76
  return undefined;
77
+ const mountRoot = memoryMountRootOf(store.backend, store.root);
67
78
  const engine = new MemoryEngine({
68
79
  backend: store.backend,
69
80
  memoryDir: memoryMountRootOf(store.backend, store.root),
@@ -73,7 +84,12 @@ export function createMemoryOriginFaces(store, opts) {
73
84
  return {
74
85
  listExternal: (scopes) => engine.listExternalOriginEntries(scopes),
75
86
  listClearances: () => engine.listOriginClearances(),
76
- clear: (entryId, input) => engine.clearEntryOrigin(entryId, input),
87
+ clear: async (entryId, input) => {
88
+ const locked = memoryLayerWriteRefusal(mountRoot);
89
+ if (locked !== undefined)
90
+ throw locked;
91
+ return engine.clearEntryOrigin(entryId, input);
92
+ },
77
93
  };
78
94
  }
79
95
  export function createSessionMemoryStatusFace(store, opts) {
@@ -18,9 +18,10 @@
18
18
  *
19
19
  * 载荷形不是本仓铸的:`FileStorageCorruptReadInfo` 是 **core 的导出**,并且是 core 亲口说的**最宽**那一只
20
20
  * ——"the widest shape any store this backend constructs can deliver, so ONE sink reaches all of them…
21
- * the two-field store seats are satisfied by it"。于是本属主就按那只最宽形收参:两个窄座
22
- * (`CorruptReadInfo = {path, reason}`)与 roster/mailbox 的内联形都因**可选成员**而逐字接受它
23
- * (`{path,reason}` 可赋给最宽形 ⇒ 吃最宽形的函数可赋给窄座)。**本仓因此零镜像类型**。
21
+ * the two-field store seats are satisfied by it"。于是本属主就按那只最宽形收参 —— 窄座的载荷都可赋给它
22
+ * ⇒ 吃最宽形的函数可赋给窄座。**本仓因此零镜像类型**。
23
+ * ⚠️ core 7.30.0(#1041 三态,S-616 提货)起 `CorruptReadInfo` 不再是 `{path, reason}` 两键形,而是按 `kind`
24
+ * 分三级的 union(`record` / `replay` / `file`,`path` + `reason` 每级都在);逐级读法见 {@link corruptReadLevelFields}。
24
25
  *
25
26
  * ## 刻意不做的两件事
26
27
  * · **不在这里下统一结论**:后果按脸各不相同(少了会话规则这一层 / 列表静默少行 / rewind 边界读成缺席 /
@@ -35,7 +36,7 @@
35
36
  * 闭包,天然满足;但**不要**把某个对象的裸方法引用直接塞进座 —— 读 `this` 会抛,而那个抛会被 core 的
36
37
  * guard 吞掉,于是披露**静默丢失**(比没接座更难查)。
37
38
  */
38
- import type { FileStorageCorruptReadInfo } from "@sema-agent/core";
39
+ import type { FileStorageCorruptReadInfo, readJsonlRecords } from "@sema-agent/core";
39
40
  import type { Logger } from "./logger.js";
40
41
  /**
41
42
  * 铸一只腐读披露座。
@@ -53,4 +54,20 @@ export declare function createCorruptReadSeat(warn: Logger["warn"], event: strin
53
54
  * 这一句从前在两处各抄一份、只差一个词;现在词从参数来,句子只有一份。
54
55
  */
55
56
  export declare function buildDurableAbsenceNote(thirdFace: string): string;
57
+ /** core `readJsonlRecords` 的坏行回调入参(core 不从包根导出这两级的型名 ⇒ 从那只函数的签名派生,不手抄)。 */
58
+ export type JsonlCorruptInfo = Parameters<NonNullable<Parameters<typeof readJsonlRecords>[1]>>[0];
59
+ /** 其中「一条被跳过的记录」那一级。 */
60
+ export type JsonlSkippedRecord = Extract<JsonlCorruptInfo, {
61
+ kind: "record";
62
+ }>;
63
+ /**
64
+ * S-616(core 7.30.0 #1041 提货)—— 本仓直接调 `readJsonlRecords` 的店(approval-ask 日志、permission-rule 两本日志)
65
+ * 的坏行回调**唯一**形。core 自这一版起对一次坏读报两级:每条被跳过的记录一发 `record`,这次重放的计数一发
66
+ * `replay`(`dropped` = 同一批 `record` 的条数)。这些店按**记录**计数、按记录告警 / 跳闸 —— 手写回调不分级就把同一条
67
+ * 坏行记两次(k3s8 实测:approval-ask 的「1 条解析不出」读成 2 条,permission-rule 的 onError 一行变两行)。
68
+ * 规则一句:**只按记录计,计数级是同一事实的汇总,不再计一次**;穷举 switch,core 加第三级 ⇒ 编译红。
69
+ * 与 {@link createCorruptReadSeat} 的分工:那只是 core 自带 File 店的**披露座**(两级都转发,日志按 `kind` 分桶);
70
+ * 本函数是本仓自有店**消费**坏行事实的口子(计数 / 跳闸只要记录级)。
71
+ */
72
+ export declare function createSkippedRecordSink(onRecord: (info: JsonlSkippedRecord) => void): (info: JsonlCorruptInfo) => void;
56
73
  //# sourceMappingURL=corrupt-read-seat.d.ts.map
@@ -1,16 +1,47 @@
1
1
  export function createCorruptReadSeat(warn, event, note) {
2
2
  return (info) => warn(event, {
3
+ kind: info.kind,
3
4
  path: info.path,
4
5
  reason: info.reason,
6
+ ...corruptReadLevelFields(info),
5
7
  ...(info.sessionId !== undefined ? { sessionId: info.sessionId } : {}),
6
8
  ...(info.principal !== undefined ? { principal: info.principal } : {}),
7
9
  ...(note !== undefined ? { note } : {}),
8
10
  });
9
11
  }
12
+ function corruptReadLevelFields(info) {
13
+ switch (info.kind) {
14
+ case "record":
15
+ return { line: info.line, form: info.form };
16
+ case "replay":
17
+ return { kept: info.kept, dropped: info.dropped, total: info.total };
18
+ case "file":
19
+ return {};
20
+ default: {
21
+ const unreachable = info;
22
+ return unreachable;
23
+ }
24
+ }
25
+ }
10
26
  export function buildDurableAbsenceNote(thirdFace) {
11
27
  return ("a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
12
28
  "Consequence depends on which store read it: session-policy = that run lost its SUBTRACTIVE session-rule " +
13
29
  "layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
14
30
  `rows; ${thirdFace}. Inspect the named path before deciding.`);
15
31
  }
32
+ export function createSkippedRecordSink(onRecord) {
33
+ return (info) => {
34
+ switch (info.kind) {
35
+ case "record":
36
+ onRecord(info);
37
+ return;
38
+ case "replay":
39
+ return;
40
+ default: {
41
+ const unreachable = info;
42
+ return unreachable;
43
+ }
44
+ }
45
+ };
46
+ }
16
47
  //# sourceMappingURL=corrupt-read-seat.js.map
@@ -39,7 +39,7 @@ export declare const FAIL_OPEN_TAGS: {
39
39
  };
40
40
  readonly "server.trace.projection-input-malformed": {
41
41
  readonly cls: "F";
42
- readonly note: "7.78.0(core 7.18.0 #741/#786/#789/#790,`trace/project.ts` 的五张新投影脸共用一条规则):`tool_progress` / `tool_disclosure` 两帧的必需位读不出 ⇒ **整帧不发**;`wiring_manifest.hooks[]` / `wiring_manifest.lsp` / `context_usage.sections[]` 三段的容器形或成员形读不出 ⇒ 那一行/那一段被丢、全丢光即键缺席。规则一句话:**在场但读不出 ⇒ 计一次并丢弃;缺席从不计数**(缺席是这几张脸上的一等状态,契约把它定义成一个正面事实 —— `hooks` 缺席 = 「本条腿没有自己的钩子记录」、`sections` 缺席 = 「这条腿没装配出段 IR」、`lsp` 缺席 = 「本部署没接 LSP」)。放行的最坏后果=这条腿的消费端少一张观测/渲染面(壳退回上一份已知状态或通用卡片),**不影响任何裁决**——门在引擎里早已判完。反方向(半张脸照发)严格更坏:消费端会把读不出的行当成那个正面事实。计数是**唯一**能把「坏世代 / 第三方 producer 一直在发坏载荷」与那个正面事实分开的位。";
42
+ readonly note: "7.78.0(core 7.18.0 #741/#786/#789/#790,`trace/project.ts` 的五张新投影脸共用一条规则;之后逐版入族 —— NP-5 / 7.100.0 起含 `trace/task-notification-facets.ts` 的 S-616 委派终态 facet / 重供两键与 `compacted.summarizedCrossSessionMessages`,助手住叶模块 `trace/projection-drop.ts`,全表见普查表第 73 行):`tool_progress` / `tool_disclosure` 两帧的必需位读不出 ⇒ **整帧不发**;`wiring_manifest.hooks[]` / `wiring_manifest.lsp` / `context_usage.sections[]` 三段的容器形或成员形读不出 ⇒ 那一行/那一段被丢、全丢光即键缺席。规则一句话:**在场但读不出 ⇒ 计一次并丢弃;缺席从不计数**(缺席是这几张脸上的一等状态,契约把它定义成一个正面事实 —— `hooks` 缺席 = 「本条腿没有自己的钩子记录」、`sections` 缺席 = 「这条腿没装配出段 IR」、`lsp` 缺席 = 「本部署没接 LSP」)。放行的最坏后果=这条腿的消费端少一张观测/渲染面(壳退回上一份已知状态或通用卡片),**不影响任何裁决**——门在引擎里早已判完。反方向(半张脸照发)严格更坏:消费端会把读不出的行当成那个正面事实。计数是**唯一**能把「坏世代 / 第三方 producer 一直在发坏载荷」与那个正面事实分开的位。";
43
43
  };
44
44
  readonly "server.task-mcp.source-label-dropped": {
45
45
  readonly cls: "F";
@@ -121,6 +121,10 @@ export declare const FAIL_OPEN_TAGS: {
121
121
  readonly cls: "F";
122
122
  readonly note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor 的行之后、`shadowedRule` 无可脱敏、读面派生的 `governanceForced` 徽章不亮 —— 三者都是**展示/分诊**,不是执法判据(门早已 park,真按 `shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,不经本读面),且 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 cell 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),连同其余所有行一起消失。故 F 类;tag 与候选那格**分开计**,并计会让「哪一格在坏」在遥测里读不出来。";
123
123
  };
124
+ readonly "server.approvals.park-row-blob-unreadable": {
125
+ readonly cls: "F";
126
+ readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` 而读)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的两位 park 行事实缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
127
+ };
124
128
  readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
125
129
  readonly cls: "F";
126
130
  readonly note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `get()`,core 5.14.0 #147 的耐久转向队列):一条挂起 run 的 `checkpoint.pending_steer_queue` 这一格**没能完整读出来**(不是合法 JSON 文本 / 不是数组 / 某一条目的四个必备键不合形)⇒ 读不出的那部分**丢掉**,run 照常 resume。放行的最坏后果 = 一条已经 park 的操作员转向指令**没有**在这次 resume 上注入(人以为说过的话没送到);不放行的代价是这条挂起的 run **永远不可 resume** —— resume 是它唯一的出路,为一格读不出的转向把整条 run 变成砖是错误的失败方向(写侧刻意相反:`setPendingSteer` 往一份读不全的队列上追加会把读不出的条目永久抹掉,那里 fail-loud)。故 F 类。留痕从一条 `logger.info` 升成登记 tag 的理由:日志行在**遥测轴**上是零信号,「这台机器的转向队列一直读不出来」与「这台机器本来就没人 steer」在指标上完全同形。计数非零 = 去按日志里的 token 指纹查那一行。";
@@ -227,7 +231,7 @@ export declare const FAIL_OPEN_TAGS: {
227
231
  };
228
232
  readonly "server.approval.ambiguous-cas.expire": {
229
233
  readonly cls: "P-DEBT";
230
- readonly note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290)→ S-69 收窄(S-25-R1 终局,core design/384 §4.3 七律 / §4.4 键形对表,[5934]):流内 ask 的**窗到期臂**打原子终局 claim(`claimTerminal(expire)`,S-69 前 `expireAsk` CAS)被 reject(抛错 / 超墙钟,什么也证明不了 —— 律 3 K2)之后,有界退避**重发同一 claim**(6 格 ⇒ 至多 7 把,在飞帽 2 按持久 askId 记、撞帽拍不计 attempt、让路探针读同帽同界、帽满或同 askId 尚有在飞操作时探针不计 rung、真相广播同 askId 全部登记(本登记已 done 亦广播)—— codex S-69 R2-1/R3-1/R3-2/R4-1/R5-1/R6-1/R6-2/R7-1;末格睡后仍再发 —— codex R1-F1 同律;总墙钟硬上界 TERMINAL_LADDER_WALL_MS ≈10.15s —— R2-2 修正算术 + R3-3 硬封顶)**全部 reject** ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(A-075.116②:其兜底覆盖本形经确认后本 tag 降 F)。**不计**的形(S-69 起命中条件收窄到「店对整条阶梯持续不可用」):claim 输 ⇒ 败方答案**携真相**(DECIDED ⇒ 按人的决议结算,零补读;PARKING/PARKED ⇒ park 路由;absent = 行不在/错 scope ⇒ K1 零退避投影),迟到回来的 claim 同样自足(迟到赢 ⇒ 副作用补做,迟到输 ⇒ 按真决议结算 —— 此前迟到回调只认 won 的那条残余窗即 S-25-R1,本批闭合);编辑在飞标记在场 ⇒ 有界让路(不重打 claim)、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);阶梯里曾拿到真相而标记已放开 ⇒ 按那份真相结算。detail 带 askId 与 claim 把数。";
234
+ readonly note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290)→ S-69 收窄(S-25-R1 终局,core design/384 §4.3 七律 / §4.4 键形对表,[5934]):流内 ask 的**窗到期臂**打原子终局 claim(`claimTerminal(expire)`,S-69 前 `expireAsk` CAS;NP-2 / 7.100.0 起 `deny` 政策下意图是 `refuse` = 当场落 DECIDED(deny)+`settled_by=policy`,同一架阶梯、同一个 tag)被 reject(抛错 / 超墙钟,什么也证明不了 —— 律 3 K2)之后,有界退避**重发同一 claim**(6 格 ⇒ 至多 7 把,在飞帽 2 按持久 askId 记、撞帽拍不计 attempt、让路探针读同帽同界、帽满或同 askId 尚有在飞操作时探针不计 rung、真相广播同 askId 全部登记(本登记已 done 亦广播)—— codex S-69 R2-1/R3-1/R3-2/R4-1/R5-1/R6-1/R6-2/R7-1;末格睡后仍再发 —— codex R1-F1 同律;总墙钟硬上界 TERMINAL_LADDER_WALL_MS ≈10.15s —— R2-2 修正算术 + R3-3 硬封顶)**全部 reject** ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(A-075.116②:其兜底覆盖本形经确认后本 tag 降 F)。**不计**的形(S-69 起命中条件收窄到「店对整条阶梯持续不可用」):claim 输 ⇒ 败方答案**携真相**(DECIDED ⇒ 按人的决议结算,零补读;PARKING/PARKED ⇒ park 路由;absent = 行不在/错 scope ⇒ K1 零退避投影),迟到回来的 claim 同样自足(迟到赢 ⇒ 副作用补做,迟到输 ⇒ 按真决议结算 —— 此前迟到回调只认 won 的那条残余窗即 S-25-R1,本批闭合);编辑在飞标记在场 ⇒ 有界让路(不重打 claim)、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);阶梯里曾拿到真相而标记已放开 ⇒ 按那份真相结算。detail 带 askId 与 claim 把数。";
231
235
  };
232
236
  readonly "server.approval.ambiguous-cas.cancel": {
233
237
  readonly cls: "P-DEBT";
@@ -235,7 +239,7 @@ export declare const FAIL_OPEN_TAGS: {
235
239
  };
236
240
  readonly "server.approval.ambiguous-cas.unreachable": {
237
241
  readonly cls: "P-DEBT";
238
- readonly note: "S-25 同族第三员 → S-69 收窄:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `claimTerminal(expire)`(S-69 前 `expireAsk` CAS)被 reject 之后,有界重发至多 7 把 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;S-25 起行读出 DECIDED ⇒ 按人的决议结算;S-69 起真相随败方答案回来、迟到答案自足,只有店对整条阶梯持续 reject 才计数。";
242
+ readonly note: "S-25 同族第三员 → S-69 收窄:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `claimTerminal(expire)`(S-69 前 `expireAsk` CAS;NP-2 起 `deny` 政策下是 `refuse`,同窗到期员)被 reject 之后,有界重发至多 7 把 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;S-25 起行读出 DECIDED ⇒ 按人的决议结算;S-69 起真相随败方答案回来、迟到答案自足,只有店对整条阶梯持续 reject 才计数。";
239
243
  };
240
244
  readonly "server.approval-ask-audit.append-failed": {
241
245
  readonly cls: "F";
@@ -271,7 +275,7 @@ export declare const FAIL_OPEN_TAGS: {
271
275
  };
272
276
  readonly "server.approval-card.closed-word-out-of-set": {
273
277
  readonly cls: "F";
274
- readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` 的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40)。";
278
+ readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
275
279
  };
276
280
  readonly "server.pg-pool.idle-client-error": {
277
281
  readonly cls: "F";