@sema-agent/server 7.91.2 → 7.92.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 (71) hide show
  1. package/README.md +1 -1
  2. package/USAGE.md +8 -0
  3. package/dist/approval-ask-machine.d.ts +1 -1
  4. package/dist/approval-reconciler.d.ts +1 -1
  5. package/dist/approval.d.ts +1 -1
  6. package/dist/bench/s1/repair-oracle-adapter.d.ts +1 -1
  7. package/dist/bench/s1/runner-ctx.d.ts +1 -1
  8. package/dist/boot/runner-deps.d.ts +9 -9
  9. package/dist/capabilities/memory-notice.d.ts +27 -10
  10. package/dist/capabilities/memory-notice.js +21 -2
  11. package/dist/config-catalog.js +2 -1
  12. package/dist/config-center/types.d.ts +1 -1
  13. package/dist/config.d.ts +1 -1
  14. package/dist/env-facts.d.ts +1 -1
  15. package/dist/execution-lane-caps.d.ts +1 -1
  16. package/dist/file-history-disclosure.d.ts +2 -2
  17. package/dist/fleet/fleet-bus.d.ts +7 -1
  18. package/dist/fleet/fleet-bus.js +5 -3
  19. package/dist/http/admission.d.ts +1 -1
  20. package/dist/http/idempotency.d.ts +27 -0
  21. package/dist/http/route-ctx.d.ts +2 -5
  22. package/dist/http/routes/memory-compliance.d.ts +1 -1
  23. package/dist/http/routes/memory-origin.d.ts +2 -2
  24. package/dist/http/routes/rules.js +3 -1
  25. package/dist/http/routes/runs.js +3 -6
  26. package/dist/http/routes/session-sync.js +3 -2
  27. package/dist/http/routes/sessions.js +3 -2
  28. package/dist/http/routes/tasks.js +35 -19
  29. package/dist/http/wire-types.d.ts +29 -2
  30. package/dist/leader/wire.d.ts +1 -1
  31. package/dist/lsp/manager.d.ts +2 -2
  32. package/dist/memory-operator-faces.d.ts +13 -13
  33. package/dist/memory-scope.d.ts +2 -2
  34. package/dist/observability/fail-open.d.ts +1 -1
  35. package/dist/observability/fail-open.js +1 -1
  36. package/dist/observability/metrics.js +2 -1
  37. package/dist/orchestration/workflow-agent-session-index.d.ts +2 -2
  38. package/dist/peer-directory.d.ts +3 -2
  39. package/dist/permission-rule-vocab.d.ts +16 -1
  40. package/dist/plugins/file-run-store.d.ts +1 -1
  41. package/dist/plugins/local-session-store.d.ts +1 -1
  42. package/dist/plugins/memory-session-policy-store.d.ts +22 -3
  43. package/dist/plugins/memory-session-policy-store.js +11 -4
  44. package/dist/plugins/permission-rule-store-sql.js +16 -7
  45. package/dist/plugins/pg-pool.js +11 -0
  46. package/dist/plugins/session-policy-store-sql.d.ts +51 -3
  47. package/dist/plugins/session-policy-store-sql.js +77 -38
  48. package/dist/plugins/sql-errors.d.ts +25 -0
  49. package/dist/plugins/sql-errors.js +10 -0
  50. package/dist/plugins/store-backend.d.ts +5 -5
  51. package/dist/plugins/store-contracts.d.ts +2 -2
  52. package/dist/plugins/tidb-pool.d.ts +1 -1
  53. package/dist/plugins/tidb-pool.js +19 -0
  54. package/dist/rules-consent.d.ts +16 -9
  55. package/dist/rules-consent.js +14 -35
  56. package/dist/run-cancel-context.d.ts +1 -1
  57. package/dist/run-local.d.ts +1 -1
  58. package/dist/runtime-caps-resolver.d.ts +10 -10
  59. package/dist/runtime-governance.d.ts +2 -2
  60. package/dist/session-policy-wire.d.ts +25 -1
  61. package/dist/session-policy-wire.js +11 -0
  62. package/dist/spec-fields.d.ts +4 -4
  63. package/dist/task-cwd.d.ts +1 -1
  64. package/dist/task-settings.d.ts +2 -2
  65. package/dist/tool-approval.d.ts +1 -1
  66. package/dist/trace/core-keyset-guard.d.ts +1 -1
  67. package/dist/trace/project.d.ts +25 -1
  68. package/dist/trace/project.js +4 -0
  69. package/dist/trace/redact.d.ts +1 -1
  70. package/dist/trace/redact.js +8 -3
  71. package/package.json +3 -3
@@ -13,7 +13,7 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
13
13
  import { mintCancelledResult } from "../../run-cancel-context.js";
14
14
  import { recordResourceWindow } from "../../resource-window.js";
15
15
  import { redactSecrets } from "../../trace/redact.js";
16
- import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
16
+ import { contextUsageEventData, 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
18
  import { scopedIdempotencyKey } from "../idempotency.js";
19
19
  import { sendJson, sendError, sseHeaders } from "../send.js";
@@ -32,13 +32,20 @@ function sseData(res, payload) {
32
32
  res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
33
33
  }
34
34
  function sendSubmitResult(res, resp) {
35
- const body = resp.body;
36
- if (typeof body === "object" && body !== null && Object.hasOwn(body, "retryAfterSec") && "retryAfterSec" in body) {
37
- const sec = body.retryAfterSec;
35
+ const raw = resp.body;
36
+ if (typeof raw === "object" && raw !== null && Object.hasOwn(raw, "retryAfterSec") && "retryAfterSec" in raw) {
37
+ const sec = raw.retryAfterSec;
38
38
  if (typeof sec === "number" && Number.isFinite(sec))
39
39
  res.setHeader("retry-after", String(sec));
40
40
  }
41
- sendJson(res, resp.status, resp.body);
41
+ if (resp.status !== 200) {
42
+ sendJson(res, resp.status, resp.body);
43
+ return;
44
+ }
45
+ const body = resp.wiringManifest !== undefined && typeof raw === "object" && raw !== null && !Array.isArray(raw)
46
+ ? { ...raw, wiringManifest: resp.wiringManifest }
47
+ : raw;
48
+ sendJson(res, 200, body);
42
49
  }
43
50
  const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For observable events submit via POST /v1/runs and read GET /v1/runs/:id/events.";
44
51
  export async function handleTasks(req, res, match, ctx) {
@@ -99,6 +106,9 @@ export async function handleTasks(req, res, match, ctx) {
99
106
  if (!prepared)
100
107
  return;
101
108
  const principal = prepared.auth?.principal;
109
+ const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
110
+ let wiringReceipt;
111
+ const withRunReceipt = (out) => wiringReceipt !== undefined ? { ...out, wiringManifest: wiringReceipt } : out;
102
112
  if (liveStream) {
103
113
  if (prepared.verify || prepared.cascade) {
104
114
  sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
@@ -156,7 +166,6 @@ export async function handleTasks(req, res, match, ctx) {
156
166
  });
157
167
  if ((res.destroyed || res.closed) && !res.writableEnded)
158
168
  onDisconnect();
159
- const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
160
169
  const replayGate = resolveStreamApprovalGate({
161
170
  toolApprovalEnabled: Boolean(deps.toolApproval),
162
171
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
@@ -486,12 +495,9 @@ export async function handleTasks(req, res, match, ctx) {
486
495
  sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
487
496
  }
488
497
  else if (ev.type === "wiring_manifest") {
489
- sseData(res, {
490
- type: "wiring_manifest",
491
- ...(wiringOperatorFace
492
- ? wiringManifestOperatorEventData(ev)
493
- : wiringManifestEventData(ev)),
494
- });
498
+ const wiringFace = wiringManifestFaceEventData(ev, wiringOperatorFace);
499
+ wiringReceipt ??= wiringFace;
500
+ sseData(res, { type: "wiring_manifest", ...wiringFace });
495
501
  }
496
502
  else if (ev.type === "tool_roster_delta") {
497
503
  const rosterDelta = toolRosterDeltaEventData(ev);
@@ -745,7 +751,7 @@ export async function handleTasks(req, res, match, ctx) {
745
751
  };
746
752
  counters.uncountedBillableInflight++;
747
753
  try {
748
- resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.terminal.kind === "completed");
754
+ resp = await idemCache.run(idemKey, async () => withRunReceipt(await runStreamSubmitLeg()), (r) => r.body?.terminal.kind === "completed");
749
755
  }
750
756
  catch (e) {
751
757
  deps.logger?.error("stream_error", { closed, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
@@ -764,7 +770,7 @@ export async function handleTasks(req, res, match, ctx) {
764
770
  res.end();
765
771
  }
766
772
  else {
767
- const withDiscoveryNotice = (body) => prepared.verify || prepared.cascade ? { ...body, notice: MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE } : body;
773
+ const withSyncSubmitKeys = (body) => prepared.verify || prepared.cascade ? { ...body, notice: MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE } : body;
768
774
  const runSyncSubmitLeg = async () => {
769
775
  const syncLegStartedAt = Date.now();
770
776
  let durableTaskId;
@@ -814,13 +820,23 @@ export async function handleTasks(req, res, match, ctx) {
814
820
  inflightRuns.set(durableTaskId, syncCancelCtrl);
815
821
  }
816
822
  const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) };
823
+ const observeSyncLegEvent = (ev) => {
824
+ if (ev.type !== "wiring_manifest" || wiringReceipt !== undefined)
825
+ return;
826
+ wiringReceipt = wiringManifestFaceEventData(ev, wiringOperatorFace);
827
+ };
817
828
  let result;
818
829
  let cancelVerbLabel = false;
819
830
  counters.uncountedBillableInflight++;
820
831
  try {
821
832
  const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
822
833
  const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
823
- const plainLeg = () => prepared.runner.runTask(specWithSignal);
834
+ const plainLeg = async () => {
835
+ const stream = prepared.runner.runTaskStream(specWithSignal);
836
+ for await (const ev of stream)
837
+ observeSyncLegEvent(ev);
838
+ return stream.result();
839
+ };
824
840
  result = await withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg());
825
841
  }
826
842
  finally {
@@ -856,20 +872,20 @@ export async function handleTasks(req, res, match, ctx) {
856
872
  if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
857
873
  if (durableTaskId)
858
874
  await deps.runStore.setSuspended(durableTaskId);
859
- return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
875
+ return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
860
876
  }
861
877
  if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
862
878
  if (durableTaskId)
863
879
  await deps.runStore.setNeedsReview(durableTaskId);
864
- return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
880
+ return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
865
881
  }
866
882
  if (durableTaskId && deps.runStore)
867
883
  await deps.runStore.setTerminal(durableTaskId, syncPlane.status, stripResumeToken(result), syncPlane.errorMessage ?? null);
868
884
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
869
885
  noteRunOutcome(result);
870
- return { status: 200, body: withDiscoveryNotice(redactTerminalResult(stripResumeToken(result))) };
886
+ return { status: 200, body: withSyncSubmitKeys(redactTerminalResult(stripResumeToken(result))) };
871
887
  };
872
- const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
888
+ const resp = await idemCache.run(idemKey, async () => withRunReceipt(await runSyncSubmitLeg()), (r) => r.status < 400);
873
889
  sendSubmitResult(res, resp);
874
890
  }
875
891
  return;
@@ -7,6 +7,8 @@
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
9
  import type { A2aServerSpec, AgentDefinition, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
+ import type { SettingsPermissionMode } from "../task-settings.js";
11
+ import type { wiringManifestFaceEventData } from "../trace/project.js";
10
12
  export interface TaskRequestBody {
11
13
  objective: string;
12
14
  sessionId?: string;
@@ -310,7 +312,10 @@ export interface TaskRequestBody {
310
312
  * [ref]-②: CLOSED enum on the wire — an unknown word 400s at submit (`request.field_invalid`, sibling of
311
313
  * promptProfile) instead of silently coercing to `default`; resume replay of a stored body stays lenient
312
314
  * (coercePermissionMode). */
313
- permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto";
315
+ /** 🔴 型**从值表派生**(S-496 U1 / [ref]):闭集的属主是 `task-settings.ts` 的
316
+ * `SETTINGS_PERMISSION_MODES`(受理腿 `isSettingsPermissionMode` 与两句 400 文案同读那一份)。
317
+ * 改前这里是第 6 份手抄的五词联合 —— 加/退一个模式时 wire 型面与受理腿会各走各的。 */
318
+ permissionMode?: SettingsPermissionMode;
314
319
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
315
320
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the [ref] memory ENGINE materializes/
316
321
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
@@ -341,7 +346,7 @@ export interface TaskRequestBody {
341
346
  * 普通 Read/Write 照走普通围栏(没有记忆面 = 没有记忆写门,不是多了一道禁令)。
342
347
  * 与 `memoryCapture:"off"` **不是一个轴**:那是「记忆照常挂,但本会话的内容永远不进长期记忆」
343
348
  * (采集面,落一条单向会话记录);这是「本次运行整个记忆面都不挂」(读写整面,逐请求)。
344
- * 🔴 **两键同发时不要以为买到了两件事**(亲核 core 7.21.1 `prepare-memory.js:48/:55`):core 把 capture 腿与
349
+ * 🔴 **两键同发时不要以为买到了两件事**(亲核 core 7.21.1 `prepare-memory.js`):core 把 capture 腿与
345
350
  * 引擎挂载合取,`enabled:false` 下 capture 的**单向会话记录不落**(core 发 `phase:"config"` 一行披露)⇒
346
351
  * 下一 turn 不带本键时采集照常。要「这个会话永久别记」,那一 turn 就**别**同时发本键。详见契约 §12.7。
347
352
  * 与 `memoryWrite:false`(本 run 只读平面:照常挂、照常读、harvest 不提交)同样正交、同样可并存。
@@ -503,4 +508,26 @@ export type ApprovalStaleCurrentPending = {
503
508
  /** 当前 pending 的 server 铸 `boundInputHash`(行上有才带;逐字回显,壳不重算)。 */
504
509
  boundInputHash?: string;
505
510
  };
511
+ /** S-528 —— `wiring_manifest` **起手接线回执**在 wire 上的值形。
512
+ *
513
+ * 🔴 **从投影器的返回型派生,不手抄键**:键表的唯一属主是 `trace/project.ts` 的
514
+ * {@link wiringManifestFaceEventData}(它自己又把 `tools` 段的判形属主让给 core 的 typebox schema)。
515
+ * 手抄一张键表在这里,就是本仓 [ref] 点名的那类「手抄投影」—— core 加一段时它既不红也不跟。
516
+ *
517
+ * 🔴 **诚实边界**:派生出来的是 `Record<string, unknown>` —— 投影器本来就只说得出这个宽度(它的输入是
518
+ * `unknown`,产物按段 present-iff 铸)。所以这一行**不**声称「wire 上的每一段都有静态型」;它声称的是
519
+ * 「这一键的值恒等于那只投影器的产物」,而那正是消费端唯一需要的那句话。段级判据的属主在 core。 */
520
+ export type WiringManifestReceipt = ReturnType<typeof wiringManifestFaceEventData>;
521
+ /** S-528 —— `POST /v1/tasks`(**非流式**)200 体在 core `TaskResult` / park 回执之外带的**附加键**。
522
+ *
523
+ * 两键都是 **additive + present-iff**,同一只装饰点铸(`routes/tasks.ts` 的 `withSyncSubmitKeys`),
524
+ * 所以三处 200 出口(终局 / suspended / needs_review)与幂等重放腿自动同形:
525
+ * · `notice` —— [ref] §2 发现性指路,**present-iff** 本次提交带了 verify / cascade;
526
+ * · `wiringManifest` —— **present-iff** 引擎在本条 run 上真产了 `wiring_manifest` 帧。缺席 = 「这台引擎
527
+ * 没给回执」(老 core / 未挂配置面),**不是** `null`、**不是** `{}` —— 把「没这回事」铸成一个值,
528
+ * 消费端就再也分不出「零工具」与「没说」。 */
529
+ export interface SyncSubmitExtraKeys {
530
+ notice?: string;
531
+ wiringManifest?: WiringManifestReceipt;
532
+ }
506
533
  //# sourceMappingURL=wire-types.d.ts.map
@@ -152,7 +152,7 @@ type WireEnv = Pick<RemoteExecutionEnv, "exec" | "writeFile">;
152
152
  * [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader 各一只)。
153
153
  *
154
154
  * core 契约:`ExecutionEnv` 基面**没有** `destroy`;只有实现了 `RemoteExecutionEnv` 的适配器才有
155
- * (`core/remote-env.d.ts:426`),而「这只 env 到底有没有」是**运行期结构事实**,core 自己也只用结构探测
155
+ * (`core/remote-env.d.ts`),而「这只 env 到底有没有」是**运行期结构事实**,core 自己也只用结构探测
156
156
  * `hasDestroy()`(:487,runtask/prepare-task 全部 destroy 站点都先过它)。修前 leader 三处把它写成编译期
157
157
  * 断言(`as … & { destroy(): Promise<unknown> }`),于是 TS 对「手里其实是个 **Promise**」完全无感——
158
158
  * `env.destroy()` 抛 `env.destroy is not a function`,还把**原始错误盖掉**([ref] 现场)。
@@ -69,7 +69,7 @@ export interface E2bLspManagerOptions {
69
69
  /**
70
70
  * §DESIGN-V2 F1/F8/F9 —— 本文件曾**手抄**一份 core 的 `settleOnAbort`(注释自陈 core「un-exported」,并写下
71
71
  * 「一旦 core 导出即 find-and-delete」的计划)。[ref]([ref])执行了那句话:core 现已从包根导出它
72
- * (`index.d.ts:27`,来自 `core/lsp.js`),手抄件遂删除,本文件的两处调用直接消费 core 的实现。
72
+ * (`index.d.ts`,来自 `core/lsp.js`),手抄件遂删除,本文件的两处调用直接消费 core 的实现。
73
73
  *
74
74
  * 它的语义(读点在 `open` 的两处调用):给**这一个**调用方一个自己的 settle —— 调用方的 `signal` 让它拿到
75
75
  * `onAbort()` 的返回,而**共享作业本身不受打扰**(别的调用方可能还等着它)。铁律仍在:绝不能把 signal 穿进
@@ -104,7 +104,7 @@ export declare class E2bLspManager implements LspServerManager {
104
104
  unavailableReason(filePath: string, env?: ExecutionEnv): LspUnavailableReason | undefined;
105
105
  sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise<LspSession | undefined>;
106
106
  /**
107
- * §DESIGN-V2 F1 (aligned with core's `NodeLspManager.open`, node-lsp-manager.js :123-153 — same self-
107
+ * §DESIGN-V2 F1 (aligned with core's `NodeLspManager.open`, node-lsp-manager.js `open` — same self-
108
108
  * contained-job shape, not the same code: that manager also tracks a TTL/LRU cache and per-job
109
109
  * `SharedAbortScope`, which this one has no equivalent of per F9). The returned job is SELF-completing:
110
110
  *
@@ -12,11 +12,11 @@
12
12
  * ## 控制面归属:为什么 `controlPlaneRoot` 缺席就整口不挂(F-9 裁)
13
13
  *
14
14
  * 这两条路径与 bundle 那两条**不同**:它们**真的读引擎控制面**。亲读 core 5.57.0 `engine.js`(不信 JSDoc,
15
- * 读实现):`provenanceOf`(:628)读 `challengedEntryIds(this.controlDir)` / `lineageAccountOfEntry(this.controlDir, …)`;
16
- * `eraseMemoryEntries` 的降级腿(:485)读 `lineageContributionsOfSession(this.controlDir, …)`,证据腿则整段跑在
15
+ * 读实现):`provenanceOf` 读 `challengedEntryIds(this.controlDir)` / `lineageAccountOfEntry(this.controlDir, …)`;
16
+ * `eraseMemoryEntries` 的降级腿读 `lineageContributionsOfSession(this.controlDir, …)`,证据腿则整段跑在
17
17
  * backend 自己的 `controlPlaneRoot` 上(托管链 `transfers.jsonl`、erasure anchor)。
18
18
  *
19
- * 而 `this.controlDir` 的来源是 core 构造器的这一支(engine.js:235-239 逐字):`backend.controlPlaneRoot`
19
+ * 而 `this.controlDir` 的来源是 core 构造器的这一支(engine.js 逐字):`backend.controlPlaneRoot`
20
20
  * 在场就用它,**否则**回落到 `deriveControlPlaneDir(resolveMemoryEngineRoot(), memoryDir)` —— 一个从**本进程
21
21
  * 的本地盘**派生出来的目录。本仓是 stateless replicas(CLAUDE.md 首段):回落形下,erase 的 write-ahead
22
22
  * 托管行(崩溃窗内条目字节的唯一一份)会落到某个随机 pod 的本地盘上,pod 重建即失,而且操作面引擎与
@@ -40,7 +40,7 @@
40
40
  *
41
41
  * ⚠️ **刻意不把审计/托管/证据三个可选面编进构造判定**:`committedSnapshotOf` / `custodyOf` 缺席时 core
42
42
  * 明写要诚实答 `binding:"unknown"` / `contentState:"capability-absent"` / `custody:"capability-absent"`
43
- * ([ref] absence-reports-not-silent-green,engine.d.ts:748 逐字),那是一个**答得出话**的部署;把它们编进
43
+ * ([ref] absence-reports-not-silent-green,engine.d.ts 逐字),那是一个**答得出话**的部署;把它们编进
44
44
  * 构造判定会让这种部署整口消失,把 core 的诚实答案换成一句 501 谎话。`eraseWithEvidence` 同理:缺它时
45
45
  * core 自己响亮拒(`memory.erasure_evidence_unavailable`),那是**运行期**的诚实拒,与「换部署形态才行」
46
46
  * 不是一回事(附录 A 两条分列)。
@@ -78,10 +78,10 @@ export interface MemoryComplianceFaces {
78
78
  * `{"select":{"scope":"user:harmless","__proto__":{"ids":["victim"]}}}` 这个体:
79
79
  * · `JSON.parse` 把 `__proto__` 建成 select 的**自有数据属性**(JS 对象字面量里写不出这个形 ——
80
80
  * 那里的 `__proto__:` 是设原型的语法糖,所以这条路只能由**原始 JSON 文本**走进来);
81
- * · core 的 `captureErasureInput`(file-backend.js:130)用**赋值**把每个自有键抄进 `{}`,抄到
81
+ * · core 的 `captureErasureInput`(file-backend.js)用**赋值**把每个自有键抄进 `{}`,抄到
82
82
  * `__proto__` 那一下改的是克隆体的**原型**,于是这个键从 `Object.keys` 里消失;
83
83
  * · core 的 `erasureRequestInvalid` 数的是 `Object.keys` ⇒ 只看见一个 `scope` ⇒ **判合法**;
84
- * · 而两条执行腿的分派都写作 `"ids" in select`(engine.js:477 降级腿、file-backend.js:2614 证据腿),
84
+ * · 而两条执行腿的分派都写作 `"ids" in select`(engine.js 降级腿、file-backend.js 证据腿),
85
85
  * `in` **查原型链** ⇒ 继承来的 `ids` 赢,删的是不当调用方点名的那批 id;
86
86
  * · 收执里的 `select` / `selectHash` 记的却是那个自有的 `scope`。
87
87
  * ⇒ **删掉的东西与凭据上写的不是同一批**,而这一口的全部意义就是那份可归档的凭据;删除不可回滚。
@@ -120,7 +120,7 @@ export declare function createMemoryComplianceFaces(store: {
120
120
  },
121
121
  /**
122
122
  * 引擎的 **advisory incident 座**(core `MemoryEngineOptions.onIncident`)。接它不是锦上添花:
123
- * 抹除腿上有一条**只走这个座**的码 —— `memory.erasure_index_residue`(engine.js:616)。它说的是
123
+ * 抹除腿上有一条**只走这个座**的码 —— `memory.erasure_index_residue`(engine.js)。它说的是
124
124
  * 「这次抹除的**索引残留披露**没能写进收执」:于是 operator 归档的那份凭据里少了一句「MEMORY.md
125
125
  * 里可能还留着被删条目的名字」,而收执**看上去是干净的**。座不接 ⇒ 这句话在本部署上无声消失,正是
126
126
  * 安全轴上禁止的那种静默(合规腿尤甚)。同座还带 `memory.announce_failed`(公告队列本身写不进去)/
@@ -174,18 +174,18 @@ export interface MemoryOriginFaces {
174
174
  * ## 控制面归属:判据与合规两口**逐字同一条**(§1 F-9 裁),但理由更硬
175
175
  *
176
176
  * 亲读 core 5.59.0(不信 JSDoc,读 `engine.js` 实装):
177
- * · `listOriginClearances`(:3598)= `readOriginClearances(this.controlDir)`;
178
- * · `clearEntryOrigin`(:3601)整条弧都压在 `this.controlDir` 上 —— 写前开行(`openOriginClearance`)、
177
+ * · `listOriginClearances` = `readOriginClearances(this.controlDir)`;
178
+ * · `clearEntryOrigin` 整条弧都压在 `this.controlDir` 上 —— 写前开行(`openOriginClearance`)、
179
179
  * 墓碑落定登记(`markOriginClearanceTombstoned`)、终态事件(`settleOriginClearance`);
180
- * · `listExternalOriginEntries`(:3582)每一行都要 `provenanceOf`,而它读 `challengedEntryIds` /
180
+ * · `listExternalOriginEntries` 每一行都要 `provenanceOf`,而它读 `challengedEntryIds` /
181
181
  * `lineageAccountOfEntry`,同样是控制面。
182
182
  * 三个动词没有一个能离开控制面。而 `clearEntryOrigin` 的写前行里躺着 `entryText` ——
183
183
  * **被清标条目的完整正文**;core 的原话逐字是「A crash between the tombstone batch and the re-record
184
- * batch leaves this as the only copy」(origin-clearance.d.ts:30)。也就是说:墓碑已落、重录未成的那个
184
+ * batch leaves this as the only copy」(origin-clearance.d.ts)。也就是说:墓碑已落、重录未成的那个
185
185
  * 崩溃窗内,这一行是那条记忆在世界上**唯一的一份**。
186
186
  *
187
187
  * ⇒ 在 `controlPlaneRoot` 缺席的部署上(core 会把控制面落到 `resolveMemoryEngineRoot()` 派生的**副本
188
- * 本地盘**,engine.js:235-239),这唯一的一份会落在某个随机 pod 的本地盘上,pod 重建即**永久丢失**,
188
+ * 本地盘**,engine.js),这唯一的一份会落在某个随机 pod 的本地盘上,pod 重建即**永久丢失**,
189
189
  * 而且下一次 resume 调用大概率落到另一台副本、读不到那行 ⇒ 一条被清标的记忆连同它的托管字节一起消失。
190
190
  * 本仓是 stateless replicas(CLAUDE.md 首段)。所以判在**构造期**:工厂返 undefined、三口整个不挂、
191
191
  * HTTP 面诚实 501,与 `createMemoryComplianceFaces` 同形同码不同文案(要查的旋钮不是同一个)。
@@ -205,7 +205,7 @@ export declare function createMemoryOriginFaces(store: {
205
205
  },
206
206
  /**
207
207
  * 引擎的 advisory incident 座(core `MemoryEngineOptions.onIncident`)。
208
- * 🔴 与合规面的同名座**理由不同,如实写**:亲读 core 5.59.0 的这三条动词路径(engine.js:3577-3760)
208
+ * 🔴 与合规面的同名座**理由不同,如实写**:亲读 core 5.59.0 的这三条动词路径(engine.js)
209
209
  * 一个 `sink(...)` 都没有 —— 今天没有任何码**只**走这个座上来。接它是因为代价为零而失效形是静默的:
210
210
  * core 哪天在这条路上加一条只走 sink 的披露(清标的族里最像的候选是账本尺寸/重建那两条),不接的
211
211
  * 部署会让它无声消失。缺席合法(advisory-silent,与 bundle / 合规三件套同形)。
@@ -201,7 +201,7 @@ originPolicy?: {
201
201
  *
202
202
  * 两键与 `memoryWrite`(本 run 只读平面)在**铸型上**三轴正交、可并存,本函数不替谁做取舍;
203
203
  * 整键缺席 ⇒ 都不铸(形状逐字节不变=additive 锁)。
204
- * ⚠️ **铸型正交 ≠ 裁决正交**(亲核 core 7.21.1 `dist/core/runner/prepare-memory.js:48/:55`):core 把
204
+ * ⚠️ **铸型正交 ≠ 裁决正交**(亲核 core 7.21.1 `dist/core/runner/prepare-memory.js`):core 把
205
205
  * capture 腿与引擎挂载**合取**(`captureDeclared = captureDeclaredRaw && useMemoryEngine`),所以
206
206
  * `memory:"off"` 那一 turn 上 capture 的**单向会话记录不落**(core 发 `phase:"config"` 一行响亮披露)。
207
207
  * 两键同发要不要一起送、送了买到什么,口径全文在契约 §12.7;钉在 memory-off-request.test.ts ⑤。 */
@@ -223,7 +223,7 @@ declarations?: {
223
223
  * 消费方拿本函数的布尔,不拿 `spec.memory` 自己判。
224
224
  *
225
225
  * ⚠️ **本函数会抛,而「抛」本身就是一条判据**(S-424 补注)。`normalizeMemorySpec` 对两形直接 throw
226
- * (亲核安装态 core 7.22.0 `dist/core/memory.js:367-372` 与 `:373-385`):
226
+ * (亲核安装态 core 7.22.0 `dist/core/memory.js` 的 `normalizeMemorySpec`):
227
227
  * · 单数 `scope` 键在场 ⇒ `code:"config.memory_scope_spelling"`(core 已删的旧拼写;它的报错原话
228
228
  * 是「Refusing to start with memory silently off」—— 静默关闭正是那次 HIGH 的伤害面);
229
229
  * · `capture` 在场且不是字面 `"off"` ⇒ `code:"config.memory_capture_spelling"`。
@@ -255,7 +255,7 @@ export declare const FAIL_OPEN_TAGS: {
255
255
  };
256
256
  readonly "server.pg-pool.idle-client-error": {
257
257
  readonly cls: "F";
258
- readonly note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。";
258
+ readonly note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。";
259
259
  };
260
260
  readonly "server.ensure-index.concurrently-unsupported": {
261
261
  readonly cls: "F";
@@ -228,7 +228,7 @@ export const FAIL_OPEN_TAGS = {
228
228
  },
229
229
  "server.pg-pool.idle-client-error": {
230
230
  cls: "F",
231
- note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。",
231
+ note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。",
232
232
  },
233
233
  "server.ensure-index.concurrently-unsupported": {
234
234
  cls: "F",
@@ -1,3 +1,4 @@
1
+ import { DENIED_BY_VALUES } from "@sema-agent/core";
1
2
  import { buildPermissionRuleEventsHelp } from "./permission-rule-events.js";
2
3
  const DEFAULT_BUCKETS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
3
4
  function labelKey(labels) {
@@ -281,7 +282,7 @@ export function createMetrics() {
281
282
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
282
283
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
283
284
  m.counter("auto_mode_classifier_deny_cause_total", "Auto-mode gate denials where the classifier did NOT rule, by the FORM of the deny (core CLASSIFIER_DENY_CAUSES on GateDisposition.cause: 'unavailable' = the classifier could not run at all (threw/rejected/ran past the cap — retryable), 'parse_error' = it ran but answered outside its verdict contract (blocked for safety); 'other' = this build cannot name the word). 🔴 BREAKING in server 7.72.0: this counter was named auto_mode_classifier_unavailable_total through 7.71.0 and is RENAMED with NO alias (a counter that also counts parse_error is not an 'unavailable' counter); its label vocabulary was 'error'/'timeout' (WHY it was unavailable) — those two collapsed into 'unavailable' (the WHY now rides the deny sentence and the auto_mode.classified trace frame), and 'parse_error' is a bucket the old counter could not see, so by-cause series do NOT splice across the upgrade. Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks (the classifier's own <block> ruling carries NO cause) — a spike here means the classifier itself is not working (gateway/credential fault, too slow, or answering off-contract), not that the model kept asking for dangerous things");
284
- m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
285
+ m.counter("permission_denied_total", `Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: ${DENIED_BY_VALUES.join("/")}; 'other' = this build cannot name the layer; label is \`deniedBy\` since server 7.64.0 — it was \`source\` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)`);
285
286
  m.counter("permission_rule_events_total", buildPermissionRuleEventsHelp());
286
287
  m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
287
288
  m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
@@ -60,11 +60,11 @@ export declare function readRunParks(run: unknown): WorkflowRunParkKey[] | undef
60
60
  *
61
61
  * 为什么路由不能改读 `parks[]`(两条都是真跑复现出来的,别再合并):
62
62
  * · `parks` 含**继承而未由本跑执行**的条目:core 在读 checkpoint 真值**之前**就把它们写进新一跑的记录
63
- * (`dist/orchestration/workflow.js:2244`,读真值的循环在 `:2251` 起),那一读抛 ⇒ `:2340` 的 catch 落一条
63
+ * (`dist/orchestration/workflow.js`,`parks = inherited` 发生在读真值的循环**之前**),那一读抛 ⇒ 同处的 catch 落一条
64
64
  * **带 parks 而 journal 为空**的 `failed` 记录。跟着 `parks` 走 ⇒ 坐标被它抢走 ⇒ `/decide` 让宿主 resume 它
65
65
  * ⇒ 已完成的前缀**整段重跑**(非幂等副作用重复执行)。
66
66
  * · 而「只收 `originRunId === runId`」同样不成立:re-park 时 core 按 callKey 就地替换条目、`originRunId`
67
- * **沿用继承来的那一个**,只在 `journalAppendParked` 成功之后才改写成本跑 id(`:1027-1042`),而
67
+ * **沿用继承来的那一个**,只在 `journalAppendParked` 成功之后才改写成本跑 id,而
68
68
  * `persist("update")` 打在 append **之前**。⇒ 那条判据会在 append 失败(以及 append 前的那一拍)上把
69
69
  * 真正持有**新 token** 的那一跑整只排除,而上一跑的记录里只有**旧** token ⇒ 这条审批再也送不出去。
70
70
  *
@@ -55,8 +55,9 @@
55
55
  * · `liveness` = **会话行在 ⇒ `"live"`**;`"deleted"` 来自 mailbox 墓碑(= 删除级联**在飞**那段窗口;
56
56
  * 墓碑随箱亡,级联跑完之后那个 id 就是「不存在」—— 理由全文在 `plugins/mailbox-recipient-lifecycle.ts`),
57
57
  * 同句柄上**墓碑压过活行**;**`"dead"` v1 不铸**
58
- * (core `list-agents-tool.js:60` 跳过 deleted 行、`:62` 默认跳过 dead 行 —— 行号读的是**装树里的 7.24.2**
59
- * dist,7.23.5 上是 `:49`/`:51`;⇒ 把闲置会话映成 dead
58
+ * (core `list-agents-tool.js` 的清单过滤:deleted 行跳过、dead 行在缺省 `includeDead` 下也跳过
59
+ * —— ⚠️ S-516:这里**不写 dist 行号**,那批坐标每次提货都漂(7.23.5→7.24.2 就整段搬了家);
60
+ * 要复核请按符号名搜。⇒ 把闲置会话映成 dead
60
61
  * 等于把用户自己的闲置会话从 `ListAgents` 里藏掉,正是本席要解的那个面。忙闲一律走 `tempo`);
61
62
  * · `tempo` = `sessionTempoOfRunStatus(lastStatus)`(`plugins/store-contracts.ts` 的那**一张**表,
62
63
  * `GET /v1/sessions` 的 `tempo` 列读的是同一只函数 —— 一处定义两处读);
@@ -1,4 +1,4 @@
1
- import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
1
+ import type { RemoveResult, RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
2
2
  /**
3
3
  * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
4
  * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
@@ -55,4 +55,19 @@ export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix"
55
55
  * pending 卡短命,重触发即取新卡(B7/B8 既诺)。
56
56
  */
57
57
  export declare const RULE_APPROVAL_RECORD_SCHEMA: RuleApprovalRecord["schema"];
58
+ /**
59
+ * 一次撤销之后「这条规则还活着吗」的**三词闭集**(core 7.25.0 [ref]:`RemovalLiveness`)。
60
+ * `"yes"` = 店说它还在(add-wins:撤销期间又落了一次新批准);`"no"` = 店说它不在了;
61
+ * `"unknown"` = **回答这一问的那次读回没做成** —— 这个域唯一不肯说的那句话是「看不见 ⇒ 没有」,
62
+ * 第三个词就是它的反面。
63
+ *
64
+ * 🔴 **型从 core 的导出型派生,不是抄一份字面联合**:`RemovalLiveness` 本身没有从包根导出
65
+ * (亲验 `dist/index.d.ts`:只导 `RemoveResult`),而 `RemoveResult` 的 removed 臂上就带着它 ⇒ 从那里
66
+ * `Extract` 出来。抄一份 `"yes"|"no"|"unknown"` 的下场与本文件顶注说的那一族一样:core 加第四个词
67
+ * (或改掉某一个)时,抄件不会红。派生形则**编译期**先红在每一处穷举上。
68
+ * (已向 core 请托把这只型放上包根导出面;到货后本行换成直接 import,词表值零变化。)
69
+ */
70
+ export type RemovalLiveness = Extract<RemoveResult, {
71
+ status: "removed";
72
+ }>["stillLive"];
58
73
  //# sourceMappingURL=permission-rule-vocab.d.ts.map
@@ -64,7 +64,7 @@ export declare class FileRunStore {
64
64
  * hydrate 期**逐文件**读盘的统一口([ref] 件B / codex 对抗复审 R4-[high])。
65
65
  *
66
66
  * `readJsonlRecords` 对 ENOENT 答 `[]`、对其余读错(EACCES / EIO / EISDIR / ENOTDIR)**抛普通 Error**
67
- * (亲验 core `dist/stores/file/fs-atomic.js:110-112`)。那个普通 Error 从构造器冒出去之后,在**默认
67
+ * (亲验 core `dist/stores/file/fs-atomic.js`)。那个普通 Error 从构造器冒出去之后,在**默认
68
68
  * 推导**的 local 后端上会被 `openStoreBackendWithFallback` 的降级臂吞成 memory —— 也就是说 strict 开着
69
69
  * 也照样带着残缺账本(或直接换成内存)起动,两处 readdir 门白守。
70
70
  *
@@ -176,7 +176,7 @@ export declare class LocalSessionStore implements SessionStore {
176
176
  * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session.
177
177
  * [ref] → [ref]([ref] 兑现;上一版「有意缓议」注即本批债锚,兑现随批撤):第三参 engine runId
178
178
  * 自本批起**消费** —— map 值 {taskId, runId?} + 行投影 `lastTaskRunId` + **两座同动清座**(omit ⇒
179
- * 清 runId 座,对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear 契约,core session.d.ts:204-212
179
+ * 清 runId 座,对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear 契约,core session.d.ts
180
180
  * 亲读:旧 run 的 runId 挂在新 task 旁 = 把上一条 run 的身份错标给新 run)。存储与行投影同批落
181
181
  * (codex 5.65 R1-[medium]「存储先行、投影另批」之驳仍成立:本店 runId 唯一读点即行投影)。
182
182
  * wire 半场:HTTP 面零投影透传(sessions-list.ts,additive 键随行上 wire);sema-sdk spec
@@ -11,17 +11,36 @@
11
11
  * BYTE-MATCH DISCIPLINE: getRules/putRules/listBySession replicate core's `InMemorySessionPolicyStore` semantics
12
12
  * exactly (same key shape, same CAS `conflict` on expectedRev mismatch, same tighten-only `loosen_forbidden` gate via
13
13
  * core's exported `normalizeRules`/`loosenReasons`/`stripRev`, same rev start/bump). Any divergence is a bug here.
14
+ * S-529 起同样钉住 `@contract session_policy.rev_survives_delete`:`deleteBySession` 保留每把键的 rev 高水位
15
+ * **并给那把键的世代(`gen`)+1**(见 {@link MemorySessionPolicyStore.deleteBySession});共用试剂盒
16
+ * `test/helpers/session-policy-rev-line-contract.ts` 与 SQL 双生、以及 core 自己的 `InMemorySessionPolicyStore`
17
+ * (7.25.0 起带这条契约,已作为第三只引擎接进同一只试剂盒当 oracle)跑同一段剧本。
14
18
  */
15
19
  import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "@sema-agent/core";
16
20
  export declare class MemorySessionPolicyStore implements SessionPolicyStore {
17
21
  private readonly map;
22
+ /** S-529 —— 每把键的 rev **高水位 + 世代**(`@contract session_policy.rev_survives_delete`):`deleteBySession`
23
+ * 抹掉规则行时把它那条 rev 线的终点留在这里,于是删后首写 stamp 的是 `max(当前行 rev, 记号.rev) + 1`,
24
+ * 而不是从 1 重来;同时把 `gen` +1 —— **抹除终结的是那一行的血统**,下一次写开的是新世代,读侧因此能把
25
+ * 「重建的行」与「它正在执行的那一行的延续」分开,无论新行的 rev 有多大(core 7.25.0 [ref] 判词:
26
+ * 没有世代时读侧只能靠「亲眼看见行消失」那条弱守卫,于是两道门之间未被观察到的 erase+重建抓不住,
27
+ * 而一次瞬时读 null 会把该腿永久闩在旧行上)。
28
+ * 与 SQL 双生的 `session_policy_rev_mark` 表、core File 店的 `<stem>.hwm.json` 侧车是同一条律的三个实现体
29
+ * (SQL 那边的表注写了为什么不留墓碑行)。本表只增不减、条目是两个整数 —— 与 `map` 同为进程内结构,
30
+ * 随进程亡;跨副本的持久面在 SQL 双生那半。 */
31
+ private readonly marks;
18
32
  private key;
19
33
  getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
20
34
  putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
21
35
  listBySession(sessionId: string): Promise<SessionRulesRecord[]>;
22
- /** Purge ALL policy rows for a session, across every principal. Scoped by session_id alone — the caller
23
- * (E21 route / importSession overwrite-dst) already proved ownership. Shape = core 1.423's optional
24
- * `SessionPolicyStore.deleteBySession?` seam (`Promise<void>` — the count was incidental; nothing consumed it). */
36
+ /** Purge ALL policy rows for a session, across every principal — KEEPING each key's rev high-water mark.
37
+ * Scoped by session_id alone — the caller (E21 route / importSession overwrite-dst) already proved ownership.
38
+ * Shape = core 1.423's optional `SessionPolicyStore.deleteBySession?` seam (`Promise<void>` — the count was
39
+ * incidental; nothing consumed it).
40
+ *
41
+ * S-529:规则本身死,rev 线不死(`@contract session_policy.rev_survives_delete`)。记号只升不降(`Math.max`),
42
+ * 所以反复抹除不会把线走回头;从没写过的键不留记号(没有线就不凭空抬)。**抹除同时终结那一行的血统**:
43
+ * `gen` +1,于是下一次写开的是新世代 —— 数字继续爬,但读侧按世代判「这不是我在执行的那一行的延续」。 */
25
44
  deleteBySession(sessionId: string): Promise<void>;
26
45
  }
27
46
  //# sourceMappingURL=memory-session-policy-store.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { normalizeRules, loosenReasons, stripRev, SessionPolicyError, } from "@sema-agent/core";
2
2
  export class MemorySessionPolicyStore {
3
3
  map = new Map();
4
+ marks = new Map();
4
5
  key(sessionId, principal) {
5
6
  return JSON.stringify([sessionId, principal ?? null]);
6
7
  }
@@ -21,8 +22,11 @@ export class MemorySessionPolicyStore {
21
22
  throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
22
23
  }
23
24
  }
24
- const next = { ...structuredClone(clean), rev: (prior?.rev ?? 0) + 1 };
25
+ const mark = this.marks.get(key) ?? { rev: 0, gen: 0 };
26
+ const gen = prior?.gen ?? mark.gen;
27
+ const next = { ...structuredClone(clean), rev: Math.max(prior?.rev ?? 0, mark.rev) + 1, gen };
25
28
  this.map.set(key, next);
29
+ this.marks.set(key, { rev: next.rev, gen });
26
30
  return structuredClone(next);
27
31
  }
28
32
  async listBySession(sessionId) {
@@ -35,10 +39,13 @@ export class MemorySessionPolicyStore {
35
39
  return out;
36
40
  }
37
41
  async deleteBySession(sessionId) {
38
- for (const k of [...this.map.keys()]) {
42
+ for (const [k, rules] of [...this.map]) {
39
43
  const [sid] = JSON.parse(k);
40
- if (sid === sessionId)
41
- this.map.delete(k);
44
+ if (sid !== sessionId)
45
+ continue;
46
+ const mark = this.marks.get(k) ?? { rev: 0, gen: 0 };
47
+ this.marks.set(k, { rev: Math.max(mark.rev, rules.rev), gen: mark.gen + 1 });
48
+ this.map.delete(k);
42
49
  }
43
50
  }
44
51
  }
@@ -478,13 +478,22 @@ class SqlDurableRulePartition {
478
478
  return { conflict: true, rev: cur.rev };
479
479
  let nextRules = cur.rules;
480
480
  let nextTombstones = cur.tombstones;
481
- if (delta.kind === "redemption-add") {
482
- assertRedemptionNotQuarantined(cur.quarantined, delta);
483
- nextRules = foldDelta(cur.rules, delta);
484
- }
485
- else {
486
- assertDeleteDeltaCarriesNoAdd(delta);
487
- nextTombstones = [...cur.tombstones, delta.tombstone];
481
+ switch (delta.kind) {
482
+ case "redemption-add":
483
+ assertRedemptionNotQuarantined(cur.quarantined, delta);
484
+ nextRules = foldDelta(cur.rules, delta);
485
+ break;
486
+ case "tighten-add":
487
+ nextRules = foldDelta(cur.rules, delta);
488
+ break;
489
+ case "tighten-delete":
490
+ assertDeleteDeltaCarriesNoAdd(delta);
491
+ nextTombstones = [...cur.tombstones, delta.tombstone];
492
+ break;
493
+ default: {
494
+ const unknown = delta;
495
+ throw new Error(`the SQL permission-rule backend was handed a write delta it does not know: ${JSON.stringify(unknown)}`);
496
+ }
488
497
  }
489
498
  const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
490
499
  if (!writable.success) {
@@ -150,9 +150,20 @@ export const PG_SCHEMA_STATEMENTS = [
150
150
  principal VARCHAR(190) COLLATE "C",
151
151
  rules JSONB NOT NULL,
152
152
  rev BIGINT NOT NULL,
153
+ -- gen (core 7.25.0 StoredSessionRules.gen, S-529 二期):TiDB 孪生同注 —— 这一行的血统,DEFAULT 0 = 第 0 代。
154
+ gen BIGINT NOT NULL DEFAULT 0,
153
155
  created_at TIMESTAMPTZ(3) NOT NULL,
154
156
  updated_at TIMESTAMPTZ(3) NOT NULL,
155
157
  PRIMARY KEY (policy_key)
158
+ )`,
159
+ `CREATE TABLE IF NOT EXISTS session_policy_rev_mark (
160
+ policy_key CHAR(64) COLLATE "C" NOT NULL,
161
+ session_id VARCHAR(64) COLLATE "C" NOT NULL,
162
+ rev BIGINT NOT NULL,
163
+ -- 这把键的下一行从第几代开始(每次抹除 +1);TiDB 孪生同注。DEFAULT 0 = 本列到货前落下的记号。
164
+ gen BIGINT NOT NULL DEFAULT 0,
165
+ erased_at TIMESTAMPTZ(3) NOT NULL,
166
+ PRIMARY KEY (policy_key)
156
167
  )`,
157
168
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
158
169
  blob_hash CHAR(64) COLLATE "C" PRIMARY KEY,