@sema-agent/server 7.54.0 → 7.56.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 (76) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +108 -52
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +129 -0
  7. package/dist/approval-ask-audit-store.js +284 -0
  8. package/dist/approval-card.d.ts +18 -6
  9. package/dist/approval-card.js +2 -2
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval-reconciler.d.ts +2 -1
  13. package/dist/approval-reconciler.js +1 -1
  14. package/dist/boot/coordinators.d.ts +2 -0
  15. package/dist/boot/coordinators.js +18 -1
  16. package/dist/boot/leader.d.ts +21 -0
  17. package/dist/boot/leader.js +6 -0
  18. package/dist/boot/reapers.d.ts +2 -1
  19. package/dist/boot/runtime-caps.d.ts +2 -1
  20. package/dist/boot/runtime-caps.js +3 -1
  21. package/dist/boot/stores.js +8 -1
  22. package/dist/config-center/types.d.ts +2 -1
  23. package/dist/config-provider.js +1 -0
  24. package/dist/device-store.d.ts +66 -2
  25. package/dist/device-store.js +35 -0
  26. package/dist/device-ws-hub.d.ts +8 -0
  27. package/dist/device-ws-hub.js +6 -0
  28. package/dist/http/route-ctx.d.ts +15 -3
  29. package/dist/http/routes/approvals-assistant.js +3 -2
  30. package/dist/http/routes/devices.d.ts +64 -0
  31. package/dist/http/routes/devices.js +173 -0
  32. package/dist/http/routes/leader.js +2 -2
  33. package/dist/http/routes/workflows.js +1 -1
  34. package/dist/http/server.d.ts +14 -0
  35. package/dist/http/server.js +30 -5
  36. package/dist/leader/endpoint.js +5 -4
  37. package/dist/leader/wire.d.ts +50 -0
  38. package/dist/leader/wire.js +19 -7
  39. package/dist/main.js +4 -2
  40. package/dist/observability/fail-open.d.ts +7 -3
  41. package/dist/observability/fail-open.js +7 -3
  42. package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
  43. package/dist/plugins/approval-ask-store-memory.js +20 -3
  44. package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
  45. package/dist/plugins/approval-ask-store-sql.js +41 -10
  46. package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
  47. package/dist/plugins/checkpoint-store-sql.js +4 -1
  48. package/dist/plugins/device-store-sql.d.ts +38 -1
  49. package/dist/plugins/device-store-sql.js +82 -2
  50. package/dist/plugins/file-run-store.js +2 -0
  51. package/dist/plugins/local-checkpoint-store.js +2 -0
  52. package/dist/plugins/local-session-store.d.ts +9 -9
  53. package/dist/plugins/local-session-store.js +5 -3
  54. package/dist/plugins/memory-run-store.js +2 -0
  55. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  56. package/dist/plugins/permission-rule-store-file.js +17 -8
  57. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  58. package/dist/plugins/permission-rule-store-sql.js +23 -13
  59. package/dist/plugins/pg-session-storage.js +17 -13
  60. package/dist/plugins/remote-env-device.js +8 -1
  61. package/dist/plugins/run-store-sql.js +6 -4
  62. package/dist/plugins/sql-errors.d.ts +12 -0
  63. package/dist/plugins/sql-errors.js +10 -0
  64. package/dist/plugins/store-contracts.d.ts +4 -0
  65. package/dist/plugins/tidb-session-store.js +18 -14
  66. package/dist/rules-consent.d.ts +5 -4
  67. package/dist/rules-consent.js +5 -33
  68. package/dist/runs.js +1 -0
  69. package/dist/runtime-caps-resolver.js +3 -1
  70. package/dist/security.d.ts +7 -0
  71. package/dist/tool-approval.d.ts +66 -83
  72. package/dist/tool-approval.js +222 -186
  73. package/dist/trace/ledger-events.d.ts +9 -0
  74. package/package.json +3 -3
  75. package/skills/find-skills.md +1 -1
  76. package/skills/loop.md +1 -1
@@ -18,6 +18,7 @@ import { type FleetEventBus } from "../fleet/fleet-bus.js";
18
18
  import type { ElicitationCoordinator } from "../elicitation.js";
19
19
  import type { QuestionCoordinator } from "../question.js";
20
20
  import { type ParkedAskRedeem, type ToolApprovalCoordinator } from "../tool-approval.js";
21
+ import type { ApprovalAskAuditReadFace } from "../approval-ask-audit-store.js";
21
22
  import type { PlanCacheProbe } from "../plan-cache-probe.js";
22
23
  import type { Logger } from "../observability/logger.js";
23
24
  import type { Metrics } from "../observability/metrics.js";
@@ -204,6 +205,11 @@ export interface ServiceCoordinatorDeps {
204
205
  * `POST /v1/tool-approvals/:id/respond` + the per-run ALS wrap on the SYNC streaming leg (live-only by design —
205
206
  * every other leg keeps core's fail-closed headless auto-deny, [ref]⑤/[ref]④). */
206
207
  toolApproval?: ToolApprovalCoordinator;
208
+ /** [ref] 裁 (c) 审计半场:local 车道的 File ask **审计**读面(`GET /v1/approvals` 的 additive 键
209
+ * `crashConverged` 数据源)。在场 ⇔ `DB_BACKEND=local` ∧ 审批面开着(装配 boot/coordinators.ts);
210
+ * SQL 车道恒缺席(那里有真 ask 店,[ref])—— 键缺席 = 本部署无此面,`[]` = 有面且无崩溃残账,
211
+ * 两义与 `livePending` 同律可判别。 */
212
+ approvalAskAudit?: ApprovalAskAuditReadFace;
207
213
  /** SVC-5 ([ref] CORE-5 #6): the process-local registry of STEERABLE workflow-agent handles
208
214
  * (`ctx.agentStream`) live on THIS replica. Enables POST /v1/workflows/:id/agents/:label/steer. See
209
215
  * workflow-agent-steer.ts for the handle-visibility seam status (the registry is real + ready; core does not
@@ -263,6 +269,14 @@ export interface ServiceSeamDeps {
263
269
  * `REMOTE_EXEC=device` 拒启门对齐)属**车A-4/端点车**。在那之前本键只有测试口消费,不是死枝。
264
270
  */
265
271
  deviceHub?: import("../device-ws-hub.js").DeviceWsHub;
272
+ /**
273
+ * O4/S-4([ref]):device lane 四表店 —— `/v1/devices/*` 管理面(routes/devices.ts:列表投影 /
274
+ * 绑定读面 / 显式换绑动词)的持久真源。main.ts 与 {@link deviceHub} 同源装配
275
+ * (`deviceLane?.store`,`REMOTE_EXEC=device` 时在场);缺席 ⇒ 该族路由 501
276
+ * `capability.device_lane_required`(管理面在场性由路由自身答 —— capabilities 刻意不加新位,
277
+ * v2 换绑稿 §7)。
278
+ */
279
+ deviceStore?: import("../device-store.js").DeviceStore;
266
280
  /** Audit回溯: current context (+ summary) for a session, plus its owner. Enables GET /v1/sessions/:id. */
267
281
  sessionAudit?: (sessionId: string) => Promise<({
268
282
  owner: string | null;
@@ -43,6 +43,7 @@ import { handleDiagnostics } from "./routes/diagnostics.js";
43
43
  import { handleAdoption } from "./routes/adoption.js";
44
44
  import { handleRetention } from "./routes/retention-ops.js";
45
45
  import { handleAdminDrain } from "./routes/admin-drain.js";
46
+ import { handleDevices } from "./routes/devices.js";
46
47
  import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js";
47
48
  import { handleConfigCatalog } from "./routes/config-catalog.js";
48
49
  import { handleMemoryOptOut } from "./routes/admin-memory-optout.js";
@@ -149,6 +150,7 @@ const ROUTE_DOMAINS = [
149
150
  handleAdoption,
150
151
  handleRetention,
151
152
  handleAdminDrain,
153
+ handleDevices,
152
154
  handleAdminConfigRefresh,
153
155
  handleConfigCatalog,
154
156
  handleMemoryOptOut,
@@ -466,6 +468,8 @@ export function createHttpServer(rawDeps) {
466
468
  return;
467
469
  if (await handleAdminDrain(req, res, url, ctx))
468
470
  return;
471
+ if (await handleDevices(req, res, url, ctx))
472
+ return;
469
473
  if (await handleAdminConfigRefresh(req, res, url, ctx))
470
474
  return;
471
475
  if (await handleConfigCatalog(req, res, url, ctx))
@@ -1667,6 +1671,7 @@ export function createHttpServer(rawDeps) {
1667
1671
  abortSignal: cancelCtrl.signal,
1668
1672
  sessionId,
1669
1673
  emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
1674
+ emitRevoke: (f) => approvalEmit(f),
1670
1675
  legKey,
1671
1676
  ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
1672
1677
  }, driveResume)
@@ -1804,7 +1809,7 @@ export function createHttpServer(rawDeps) {
1804
1809
  },
1805
1810
  };
1806
1811
  }
1807
- async function resumePreempted(sessionId, req, acceptEarly) {
1812
+ async function resumePreempted(sessionId, req, acceptEarly, decider) {
1808
1813
  const cs = deps.checkpointStore;
1809
1814
  const RESUMABLE_GATE_KIND = "resource_limit";
1810
1815
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: [RESUMABLE_GATE_KIND] })) ?? (await cs.findPendingTokenBySession(sessionId));
@@ -1819,6 +1824,13 @@ export function createHttpServer(rawDeps) {
1819
1824
  }
1820
1825
  if (!cp)
1821
1826
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1827
+ if (decider !== undefined && !decider.explicitOperator) {
1828
+ const cpOwner = decodeCheckpointScope(cp.scope);
1829
+ if (cpOwner !== undefined && cpOwner !== decider.principal) {
1830
+ deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1831
+ return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1832
+ }
1833
+ }
1822
1834
  const gateKind = cp.gate?.kind;
1823
1835
  if (gateKind !== RESUMABLE_GATE_KIND) {
1824
1836
  return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
@@ -1894,7 +1906,7 @@ export function createHttpServer(rawDeps) {
1894
1906
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
1895
1907
  });
1896
1908
  }
1897
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly) {
1909
+ async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
1898
1910
  const cs = deps.checkpointStore;
1899
1911
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1900
1912
  reason: r,
@@ -1918,6 +1930,13 @@ export function createHttpServer(rawDeps) {
1918
1930
  log404("checkpoint-missing");
1919
1931
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1920
1932
  }
1933
+ if (decider !== undefined && !decider.explicitOperator) {
1934
+ const cpOwner = decodeCheckpointScope(cp.scope);
1935
+ if (cpOwner !== undefined && cpOwner !== decider.principal) {
1936
+ deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1937
+ return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1938
+ }
1939
+ }
1921
1940
  const gateKind = cp.gate?.kind;
1922
1941
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
1923
1942
  return {
@@ -2101,7 +2120,7 @@ export function createHttpServer(rawDeps) {
2101
2120
  if (chunks.length === 0)
2102
2121
  return {};
2103
2122
  try {
2104
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2123
+ return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
2105
2124
  }
2106
2125
  catch {
2107
2126
  throw new HttpError(400, "invalid JSON body");
@@ -2279,7 +2298,8 @@ export function isCredentialGatedRewrite(method, url) {
2279
2298
  url === "/v1/memory/import" ||
2280
2299
  url === "/v1/memory/export" ||
2281
2300
  url === "/v1/memory/erase" ||
2282
- url.startsWith("/v1/memory/origin/entries/"));
2301
+ url.startsWith("/v1/memory/origin/entries/") ||
2302
+ url.startsWith("/v1/devices/"));
2283
2303
  }
2284
2304
  if (m === "PUT")
2285
2305
  return url === "/v1/admin/memory-optout" || url.startsWith("/v1/admin/memory-optout/");
@@ -2289,7 +2309,9 @@ export function isCredentialGatedRewrite(method, url) {
2289
2309
  return (/^\/v1\/memory\/entries\/[^/]+\/provenance$/.test(url) ||
2290
2310
  url === "/v1/memory/origin/external" ||
2291
2311
  url === "/v1/memory/origin/clearances" ||
2292
- url === "/v1/admin/memory-optout");
2312
+ url === "/v1/admin/memory-optout" ||
2313
+ url === "/v1/devices" ||
2314
+ /^\/v1\/devices\/sessions\/[^/]+$/.test(url));
2293
2315
  }
2294
2316
  return false;
2295
2317
  }
@@ -2334,6 +2356,8 @@ const ROUTE_LABEL_PATTERNS = [
2334
2356
  [/^\/v1\/tasks\/[^/]+\/tool-results\/[^/]+$/, "/v1/tasks/:id/tool-results/:ref"],
2335
2357
  [/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
2336
2358
  [/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
2359
+ [/^\/v1\/devices\/sessions\/[^/]+\/rebind$/, "/v1/devices/sessions/:id/rebind"],
2360
+ [/^\/v1\/devices\/sessions\/[^/]+$/, "/v1/devices/sessions/:id"],
2337
2361
  [/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
2338
2362
  [/^\/v1\/capabilities\/scenarios\/[^/]+$/, "/v1/capabilities/scenarios/:name"],
2339
2363
  [/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
@@ -2352,6 +2376,7 @@ const ROUTE_LABEL_LITERALS = new Set([
2352
2376
  "/v1/assistant/inbox", "/v1/assistant/tasks", "/v1/usage", "/v1/policy", "/v1/capabilities",
2353
2377
  "/v1/images", "/v1/images/bakes", "/v1/images/bakes/claim", "/v1/images/select", "/v1/images/register",
2354
2378
  "/v1/workflows", "/v1/attachments", "/v1/leader", "/v1/outcomes", "/v1/side-query",
2379
+ "/v1/devices",
2355
2380
  "/v1/fleet/stream",
2356
2381
  "/v1/memory/export", "/v1/sendfile-links",
2357
2382
  "/v1/memory/import",
@@ -1,4 +1,5 @@
1
1
  import { uuidv7 } from "@sema-agent/core";
2
+ import { withPrincipal } from "../observability/principal-context.js";
2
3
  import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
3
4
  function statusForLeaderResult(result) {
4
5
  switch (result.repairTerminal) {
@@ -37,11 +38,11 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
37
38
  }
38
39
  }
39
40
  }
40
- function drive(id, body) {
41
+ function drive(id, body, owner) {
41
42
  void (async () => {
42
43
  let terminal;
43
44
  try {
44
- const result = await runLeader(body);
45
+ const result = await withPrincipal(owner ?? undefined, () => runLeader(body));
45
46
  const status = statusForLeaderResult(result);
46
47
  terminal = { status: status, result };
47
48
  opts.logger?.info?.("leader_run_done", { id, ok: result.ok, status, ...(result.repairTerminal ? { repairTerminal: result.repairTerminal } : {}), merged: result.merge && result.merge.ok ? result.merge.merged : undefined });
@@ -120,7 +121,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
120
121
  return { status: 202, body: { leaderRunId: run.id, status: run.status } };
121
122
  }
122
123
  runs.set(run.id, { id: run.id, status: "running", startedAt: run.startedAtMs, owner });
123
- drive(run.id, b);
124
+ drive(run.id, b, owner);
124
125
  return { status: 202, body: { leaderRunId: run.id, status: "running" } };
125
126
  }
126
127
  if (scopedIdem) {
@@ -133,7 +134,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
133
134
  runs.set(id, { id, status: "running", startedAt: Date.now(), owner });
134
135
  if (scopedIdem)
135
136
  memIdem.set(scopedIdem, id);
136
- drive(id, b);
137
+ drive(id, b, owner);
137
138
  return { status: 202, body: { leaderRunId: id, status: "running" } };
138
139
  }
139
140
  const m = method === "GET" ? LEADER_ID_RE.exec(url) : null;
@@ -48,6 +48,56 @@ export interface LeaderWireConfig {
48
48
  * leader 编排的 planner / worker / repair / conflict 四类任务**静默不生效**(core 内建 deny 表仍在)。
49
49
  */
50
50
  deploymentPosture?: DeploymentPostureSeats;
51
+ /**
52
+ * [ref]([ref])—— **计费/遥测追踪席**,原样递给本车道的每一只 Runner。唯一属主 =
53
+ * `boot/budget-tracing.ts` 的 `createTracer(...)`(主 runner / subRunner / hook runner 经共享基座吃的
54
+ * 就是它),`boot/leader.ts` 搬的是**同一只**。
55
+ *
56
+ * 🔴 为什么这一席是承重的:`src/budget.ts` 的 `createTracer` 是 `brain.call` **唯一**的记账点 ——
57
+ * `costQuota.add` / `fleetUsage.record` / `fleetLease.recordSpend` / `model_cost_micro_usd_total` 四个写口
58
+ * 全在那里(`createBrain` 自己不记账)。而 `POST /v1/leader` 的准入处**读**的正是 costQuota / fleetLease
59
+ * 这两个桶。缺席 ⇒ 门查的桶与花钱的手不是同一只:`MAX_PRINCIPAL_COST_USD` 与 fleet 租约对这条腿结构性
60
+ * 失效,而一条 leader run 扇出 ≤6 个带手 worker + planner + repair/conflict 环,全是实钱。
61
+ * 归因的另一半在 `leader/endpoint.ts` 的 `drive()`(ALS `withPrincipal`)—— 记账臂按 `currentPrincipal()`
62
+ * 分租户,只补席不包裹 = 归因恒 undefined = 假绿。缺席 ⇒ 展开空对象(与接线前逐字相同)。
63
+ */
64
+ tracer?: RunnerDeps["tracer"];
65
+ /**
66
+ * [ref]([ref],[ref]-T1)—— **部署级 token 治理窗**两键,**成对**递给本车道的每一只 Runner。
67
+ * 属主 = `config.usageWindows`(声明)+ `boot/stores.ts` 造的账本店(两者同真同假,与
68
+ * `boot/runner-deps.ts` 的共享基座同规);leader 不自己读 config。
69
+ *
70
+ * 成对的理由是 core 的消费形:`prepare-task.js` 的 `buildUsageGovernance(usageWindows, deps, …)` 要求
71
+ * 声明与 `deps.usageWindowStore` **同时**在场才铸 `usageGovernance`,缺一即 undefined(既不 check 也不
72
+ * commit)⇒ 半席只是死键。缺席 ⇒ 展开空对象。
73
+ * 提交面那一半(`POST /v1/leader` 的 `usageWindowDenied` 门)在 `src/http/routes/leader.ts`。
74
+ */
75
+ usageWindows?: RunnerDeps["usageWindows"];
76
+ usageWindowStore?: RunnerDeps["usageWindowStore"];
77
+ /**
78
+ * [ref]([ref])—— **center 发布的 prompt catalog** 席,原样递给本车道的每一只 Runner。唯一属主 =
79
+ * `boot/config-center.ts` 的 `configCenter.promptSource`(主 runner / subRunner 同一只;run-local 有自己
80
+ * 的等价腿)。缺席 ⇒ leader 的 planner/worker/repair/conflict 四类**顶层**任务跑引擎内置提示词,而同一
81
+ * 部署的 `/v1/tasks`、`/v1/runs` 跑运维发布的那一份 —— 且 core 顶层新任务走的是**静默**臂(pinned /
82
+ * parentCenterArtifactDigest 两支才 `prompt.snapshot_unavailable` fail-loud),运维面零信号。
83
+ * 缺席 ⇒ 展开空对象(与接线前逐字相同)。
84
+ */
85
+ promptSource?: RunnerDeps["promptSource"];
86
+ /**
87
+ * [ref] 的**披露半件**([ref],codex 对抗复审 r2-[high],亲读装树 core 后修)—— 部署级错误汇。
88
+ *
89
+ * 🔴 为什么它与治理窗席**同生共死**:core 对「治理窗最终扣账失败」的唯一出口就是这一席 ——
90
+ * `dist/core/runner/runtask.js` 收尾段 `catch (flushErr) { this.deps.onError?.(flushErr, { phase: "config", … }) }`
91
+ * (以及 10s 慢盘披露的同一只回调),抛错之后任务**按原结果收尾**。挂了窗席却不挂本席 = 一次窗账
92
+ * 永久丢失时这条腿上零日志零计数,后续提交继续读偏低的窗被放行 —— CLAUDE.md [ref]「治理/门轴禁静默
93
+ * fail-open」正是说这个。它顺带也把 leader 车道其余 phase(degraded / rewind / memory / mcp / a2a /
94
+ * prompt-cache …)的既有观测面补齐,与 `onNotice` 席当年([ref]②)同一个理由。
95
+ *
96
+ * 属主唯一 = `boot/runner-deps.ts` `createRunnerDeps` 铸的那一只;`boot/leader.ts` 直引
97
+ * `runnerDeps.onError`(**同一实例**,[ref]§三族A 的最强同源形,与 main.ts subRunner 的先例逐字同姿势),
98
+ * leader 绝不另写一份臂表(两份必漂)。缺席 ⇒ 展开空对象(与接线前逐字相同)。
99
+ */
100
+ onError?: RunnerDeps["onError"];
51
101
  /** Worker model brain + catalog (the same the service runs tasks on). */
52
102
  brain: Brain;
53
103
  models: Record<string, Model>;
@@ -13,6 +13,7 @@ import { runLeaderTask } from "./leader.js";
13
13
  import { attachRepairLoopDeps } from "./repair-wire.js";
14
14
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
15
15
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
16
+ import { currentPrincipal } from "../observability/principal-context.js";
16
17
  const REPAIR_LOOP_WIRED = true;
17
18
  const sh = (env) => async (cmd) => {
18
19
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -130,6 +131,14 @@ export function createLeaderRunner(cfg) {
130
131
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
131
132
  const governanceSeat = cfg.governance ?? {};
132
133
  const deploymentPostureSeat = cfg.deploymentPosture ?? {};
134
+ const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
135
+ const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
136
+ const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
137
+ const onErrorSeat = cfg.onError ? { onError: cfg.onError } : {};
138
+ const principalSpec = () => {
139
+ const p = currentPrincipal();
140
+ return p ? { principal: p } : {};
141
+ };
133
142
  const mkRepair = (rawEnv, repoDir, oracleFiles) => {
134
143
  if (repairRounds <= 0)
135
144
  return undefined;
@@ -143,7 +152,7 @@ export function createLeaderRunner(cfg) {
143
152
  throw new Error(`oracle still present after remove (${f.path}) — refusing repair (measurement integrity)`);
144
153
  }
145
154
  try {
146
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
155
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
147
156
  const objective = [
148
157
  `The integrated project at ${repoDir} fails its build/test. Make the MINIMAL change to the working tree so this command exits 0 (cd into the repo and run it yourself to confirm):`,
149
158
  ` ${testCmd}`,
@@ -155,6 +164,7 @@ export function createLeaderRunner(cfg) {
155
164
  const res = await runner
156
165
  .runTaskStream({
157
166
  objective,
167
+ ...principalSpec(),
158
168
  sessionId: `leader-repair-${Date.now()}-${round}`,
159
169
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
160
170
  })
@@ -186,7 +196,7 @@ export function createLeaderRunner(cfg) {
186
196
  throw new Error(`oracle still present after remove (${f.path}) — refusing conflict-resolve (measurement integrity)`);
187
197
  }
188
198
  try {
189
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
199
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
190
200
  const objective = [
191
201
  `A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
192
202
  `Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
@@ -199,6 +209,7 @@ export function createLeaderRunner(cfg) {
199
209
  const res = await runner
200
210
  .runTaskStream({
201
211
  objective,
212
+ ...principalSpec(),
202
213
  sessionId: `leader-conflict-${Date.now()}-${round}`,
203
214
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
204
215
  })
@@ -249,10 +260,11 @@ export function createLeaderRunner(cfg) {
249
260
  if (Array.isArray(body.subtasks) && body.subtasks.length > 0) {
250
261
  return validateSubtasks(body.subtasks);
251
262
  }
252
- const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat });
263
+ const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat });
253
264
  const runRoute = async (prompt) => {
254
265
  const res = await planRunner.runTaskStream({
255
266
  objective: prompt,
267
+ ...principalSpec(),
256
268
  sessionId: `leader-route-${Date.now()}`,
257
269
  ...(cfg.routerModel ? { model: cfg.routerModel } : {}),
258
270
  }).result();
@@ -339,7 +351,7 @@ export function createLeaderRunner(cfg) {
339
351
  workerId: sub.workerId, sessionId, branch: sub.branch,
340
352
  baseSha,
341
353
  runner: keepCtxWarm(new Runner({
342
- brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat,
354
+ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
343
355
  ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
344
356
  ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
345
357
  executionEnvFactory: (ctx) => withStaging(envFactory(ctx), stage, async (e) => {
@@ -347,7 +359,7 @@ export function createLeaderRunner(cfg) {
347
359
  }),
348
360
  })),
349
361
  fetchDiff: () => fetchUploadedDiff(getUrl),
350
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
362
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
351
363
  };
352
364
  }
353
365
  if (!cfg.e2bApiKey)
@@ -359,10 +371,10 @@ export function createLeaderRunner(cfg) {
359
371
  const baseSha = await sh(env)(`cd ${repo} && git rev-parse HEAD`);
360
372
  return {
361
373
  workerId: sub.workerId, sessionId, branch: sub.branch, baseSha,
362
- runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
374
+ runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
363
375
  diffEnv: env,
364
376
  destroy: () => env.destroy().then(() => { }),
365
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec },
377
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() },
366
378
  };
367
379
  };
368
380
  const provisionIntegrationSandbox = async () => {
package/dist/main.js CHANGED
@@ -165,7 +165,7 @@ async function main() {
165
165
  };
166
166
  const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
167
167
  let parkedAskRedeem;
168
- const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({
168
+ const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, approvalAskAudit, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({
169
169
  config, logger, backend, sendUserFileTaskEnvs, ruleConsent,
170
170
  getParkedRedeem: () => parkedAskRedeem,
171
171
  ruleScopeRootFor: (sessionId) => cardRuleScopeRoot(sessionId !== undefined ? perSessionCwd.get(sessionId) : undefined, config),
@@ -536,7 +536,7 @@ async function main() {
536
536
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryStore,
537
537
  workflowCompletionInbox, taskListLane,
538
538
  });
539
- const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
539
+ const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
540
540
  const drainState = { draining: false };
541
541
  const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
542
542
  const registryJwtVerifier = authBridgeIssuer
@@ -591,6 +591,7 @@ async function main() {
591
591
  elicitation: elicitation ? elicitation : undefined,
592
592
  question: question ? question : undefined,
593
593
  toolApproval: toolApproval ? toolApproval : undefined,
594
+ approvalAskAudit: approvalAskAudit ? approvalAskAudit : undefined,
594
595
  workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined,
595
596
  subagentSteerRegistry,
596
597
  workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined,
@@ -604,6 +605,7 @@ async function main() {
604
605
  };
605
606
  const seams = {
606
607
  deviceHub: deviceLane?.hub,
608
+ deviceStore: deviceLane?.store,
607
609
  sessionAudit,
608
610
  routeJudgeBrain: brain,
609
611
  purgeSession: purgeSession ? purgeSession : undefined,
@@ -151,15 +151,19 @@ export declare const FAIL_OPEN_TAGS: {
151
151
  };
152
152
  readonly "server.approval.ambiguous-cas.expire": {
153
153
  readonly cls: "P-DEBT";
154
- readonly note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290):流内 ask 的**窗到期臂**打 `expireAsk` CAS 抛错或超墙钟(不知道做没做成)之后,恢复读口 6 格退避 7 次读(≈3.1s;末格睡后仍再读一次,末格不是盲窗 —— codex R1-F1)都读不出行 ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(它落地后本 tag 降 F)。**不计**的形:行读得出、只是仍 STREAM_PENDING / 行不在 —— 那是本臂的期望态(K1),零退避直接投影(codex-R2-2 / R3-1 钉;throw K2 pending 投影前重打一把本臂幂等 CAS 换持久背书,R3-F2);编辑在飞标记在场 ⇒ 有界让路、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);行读出 DECIDED 按人的决议结算,也不计。detail 带 askId 与读次数。";
154
+ 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 把数。";
155
155
  };
156
156
  readonly "server.approval.ambiguous-cas.cancel": {
157
157
  readonly cls: "P-DEBT";
158
- readonly note: "S-25 同族第二员:**取消臂**(task signal abort / 连接全灭同路)打 `transitionAsk(STREAM_PENDING→VOID)` CAS 抛错或超墙钟之后,恢复读口 7 次读都读不出行 ⇒ 按本臂意图投影结算 = 裸 `settle(false,\"expired\")`(D1 纯进程内语义,不留墓碑:run 已拆,迟到的 Yes 无处兑)。S-25 之前这条 catch **不读行**直接 fail-open —— 行已 DECIDED(approve) 时把人的批准翻成拒绝、回决端点只能如实 404(round6 钉曾把这一形记成「局部竞态残影」);现在只有店持续读不出才走到这里,债的语义与 expire 员同(行可能已 DECIDED 而本腿没兑现),按臂分计是为了让「哪条臂在丢决议」在遥测里读得出。";
158
+ readonly note: "S-25 同族第二员 → S-69 收窄:**取消臂**(task signal abort / 连接全灭同路)打 `claimTerminal(cancel)`(STREAM_PENDING→VOID;S-69 前 `transitionAsk` CAS)被 reject 之后,有界重发至多 7 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = 裸 `settle(false,\"expired\")`(D1 纯进程内语义,不留墓碑:run 已拆,迟到的 Yes 无处兑)。S-25 之前这条 catch **不读行**直接 fail-open —— 行已 DECIDED(approve) 时把人的批准翻成拒绝、回决端点只能如实 404(round6 钉曾把这一形记成「局部竞态残影」);S-25 起先读行;S-69 起干净输的答案本身携真相、迟到答案同样自足,只有店对整条阶梯持续 reject 才走到这里,债的语义与 expire 员同(行可能已 DECIDED 而本腿没兑现),按臂分计是为了让「哪条臂在丢决议」在遥测里读得出。";
159
159
  };
160
160
  readonly "server.approval.ambiguous-cas.unreachable": {
161
161
  readonly cls: "P-DEBT";
162
- readonly note: "S-25 同族第三员:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `expireAsk` CAS 抛错或超墙钟之后,恢复读口 7 次读都读不出行 ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;现在行读出 DECIDED ⇒ 按人的决议结算,只有店持续读不出才计数。";
162
+ 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 才计数。";
163
+ };
164
+ readonly "server.approval-ask-audit.append-failed": {
165
+ readonly cls: "F";
166
+ readonly note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。";
163
167
  };
164
168
  readonly "server.pg-pool.idle-client-error": {
165
169
  readonly cls: "F";
@@ -142,15 +142,19 @@ export const FAIL_OPEN_TAGS = {
142
142
  },
143
143
  "server.approval.ambiguous-cas.expire": {
144
144
  cls: "P-DEBT",
145
- note: "S-25(设计件 2026-08-31 §3 步骤 3;DEBTS S-25 / #290):流内 ask 的**窗到期臂**打 `expireAsk` CAS 抛错或超墙钟(不知道做没做成)之后,恢复读口 6 格退避 7 次读(≈3.1s;末格睡后仍再读一次,末格不是盲窗 —— codex R1-F1)都读不出行 ⇒ 按本臂意图投影结算 = park 路由(`\"unavailable\"`;`deny` 政策下 `{allow:false,settledBy:\"timeout\"}`)并留 park 墓碑。方向 fail-closed(无 DECIDED=approve 行绝不执行工具),但行**可能**已被另一副本判成 DECIDED(approve) 而本腿没兑现 —— 人批了、工具不跑、run 再挂起要人再批一次 = 丢一次人类决议,故记债不当合法兜底。补偿:core 侧仍 pending 的 checkpoint(人再批一次就走)+ 跨副本收敛器兜底(它落地后本 tag 降 F)。**不计**的形:行读得出、只是仍 STREAM_PENDING / 行不在 —— 那是本臂的期望态(K1),零退避直接投影(codex-R2-2 / R3-1 钉;throw K2 pending 投影前重打一把本臂幂等 CAS 换持久背书,R3-F2);编辑在飞标记在场 ⇒ 有界让路、用尽走 park 路由(标记的事不是店的债,codex R1-F2/R2-F2;负终态与带载荷 approve 不受标记压制,R2-F1/R3-F1);行读出 DECIDED 按人的决议结算,也不计。detail 带 askId 与读次数。",
145
+ 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 把数。",
146
146
  },
147
147
  "server.approval.ambiguous-cas.cancel": {
148
148
  cls: "P-DEBT",
149
- note: "S-25 同族第二员:**取消臂**(task signal abort / 连接全灭同路)打 `transitionAsk(STREAM_PENDING→VOID)` CAS 抛错或超墙钟之后,恢复读口 7 次读都读不出行 ⇒ 按本臂意图投影结算 = 裸 `settle(false,\"expired\")`(D1 纯进程内语义,不留墓碑:run 已拆,迟到的 Yes 无处兑)。S-25 之前这条 catch **不读行**直接 fail-open —— 行已 DECIDED(approve) 时把人的批准翻成拒绝、回决端点只能如实 404(round6 钉曾把这一形记成「局部竞态残影」);现在只有店持续读不出才走到这里,债的语义与 expire 员同(行可能已 DECIDED 而本腿没兑现),按臂分计是为了让「哪条臂在丢决议」在遥测里读得出。",
149
+ note: "S-25 同族第二员 → S-69 收窄:**取消臂**(task signal abort / 连接全灭同路)打 `claimTerminal(cancel)`(STREAM_PENDING→VOID;S-69 前 `transitionAsk` CAS)被 reject 之后,有界重发至多 7 claim(在飞帽 2,墙钟上界同 expire 员)全部 reject ⇒ 按本臂意图投影结算 = 裸 `settle(false,\"expired\")`(D1 纯进程内语义,不留墓碑:run 已拆,迟到的 Yes 无处兑)。S-25 之前这条 catch **不读行**直接 fail-open —— 行已 DECIDED(approve) 时把人的批准翻成拒绝、回决端点只能如实 404(round6 钉曾把这一形记成「局部竞态残影」);S-25 起先读行;S-69 起干净输的答案本身携真相、迟到答案同样自足,只有店对整条阶梯持续 reject 才走到这里,债的语义与 expire 员同(行可能已 DECIDED 而本腿没兑现),按臂分计是为了让「哪条臂在丢决议」在遥测里读得出。",
150
150
  },
151
151
  "server.approval.ambiguous-cas.unreachable": {
152
152
  cls: "P-DEBT",
153
- note: "S-25 同族第三员:**emit 全灭臂**(开卡帧两族都没送到任何连接)打 `expireAsk` CAS 抛错或超墙钟之后,恢复读口 7 次读都读不出行 ⇒ 按本臂意图投影结算 = `emitFailed` ⇒ `\"unavailable\"`(park 路由;`deny` 政策下 deny),不留墓碑(卡没到任何人手上,无人持有这把 approvalId)。S-25 之前这条 catch 不读行直接强改 —— 正是 round2 finding A「durable 行已批准、活终局被 emit 全灭强改成 unavailable」的 catch 同形缺口;现在行读出 DECIDED ⇒ 按人的决议结算,只有店持续读不出才计数。",
153
+ 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 才计数。",
154
+ },
155
+ "server.approval-ask-audit.append-failed": {
156
+ cls: "F",
157
+ note: "S-38 裁 (c) 审计半场:local 车道 File ask 审计店(`approval-ask-audit-store.ts`)的**运行期** append(铸造/决议/腿闭/收敛四类行)失败(盘满/只读/fd 失效)⇒ 丢**这一条**审计行,审批路径照常。方向刻意 fail-open:审批可用性不押在审计盘上 —— 盘满时人还得能批,把一次审计写失败升级成审批失败是把「痕迹缺一条」换成「门整个不可用」,方向更坏。boot 期(mkdir/重放)失败则**拒启**(File 店族同律,store 头注成文),不经本 tag。放行的最坏后果 = 该 ask 在崩溃收敛读面上缺席或分臂失真(与 A-075.55 的零痕迹病同向)—— 所以必须留痕:「审计一直在漏写」与「没有审批发生」在读面上同形,不计数则病灶永不显形。detail 带行类与异常文案。",
154
158
  },
155
159
  "server.pg-pool.idle-client-error": {
156
160
  cls: "F",
@@ -12,13 +12,23 @@
12
12
  * - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
13
13
  */
14
14
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
15
- import type { AskDecision, AskRow, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
15
+ import type { AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
16
16
  export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
17
17
  private readonly asks;
18
18
  private readonly batches;
19
19
  ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
20
20
  transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
21
21
  decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
22
+ /**
23
+ * [ref] 原子终局 claim(接口契约见 SQL 侧 {@link ApprovalAskStore.claimTerminal})。**委托形**:CAS 走
24
+ * 既有 verb(expire ⇒ `this.expireAsk`;cancel ⇒ `this.transitionAsk(STREAM_PENDING→VOID)`)——律 2
25
+ * 「一台机器」在本 twin 的落笔处是**字面同一个函数做写**(零第二套谓词);委托也让测试对 verb 的
26
+ * 实例级故障注入(`store.expireAsk = throwing` 等)对 claim 恒有效,注入面不因原语换名而漂移。
27
+ * 原子性:方法体的判定与读回之间唯一的 await 是对**同步体** verb 的调用(本店头注的单线程天然原子
28
+ * 论证);await 后读到的行 ≥ CAS 线性化点,而非 pending 态无回边 ⇒ 败方读数自足(与 SQL 侧同一条
29
+ * 单调性论证)。
30
+ */
31
+ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
22
32
  expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
23
33
  bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
24
34
  deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
@@ -1,5 +1,5 @@
1
1
  import { canAskTransition } from "../approval-ask-machine.js";
2
- import { assertIdempotencyKeyShape, ApprovalAskReadAbortedError } from "./approval-ask-store-sql.js";
2
+ import { assertIdempotencyKeyShape, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
3
3
  function applyPatch(row, patch) {
4
4
  if (patch.decision !== undefined)
5
5
  row.decision = patch.decision;
@@ -95,7 +95,7 @@ export class InMemoryApprovalAskStore {
95
95
  return { ok: false, reason: "batch_closed", row: row ? { ...row } : null };
96
96
  }
97
97
  const ask = this.asks.get(askId);
98
- if (!ask || ask.state !== "STREAM_PENDING") {
98
+ if (!ask || ask.state !== "STREAM_PENDING" || ask.batchId !== batchId) {
99
99
  return { ok: false, reason: "ask_not_pending", row: ask ? { ...ask } : null };
100
100
  }
101
101
  if (decision.idempotencyKey != null) {
@@ -129,13 +129,30 @@ export class InMemoryApprovalAskStore {
129
129
  ask.updatedAtMs = now;
130
130
  return { ok: true, row: { ...ask } };
131
131
  }
132
+ async claimTerminal(askId, batchId, intent) {
133
+ if (intent === "expire") {
134
+ const res = await this.expireAsk(askId, batchId);
135
+ if (res.won)
136
+ return { claimed: true, voidedSiblings: res.voidedSiblings };
137
+ }
138
+ else {
139
+ const scoped = this.asks.get(askId);
140
+ if (scoped !== undefined && scoped.batchId !== batchId)
141
+ return { claimed: false, current: { state: "absent" } };
142
+ const won = await this.transitionAsk(askId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() });
143
+ if (won)
144
+ return { claimed: true, voidedSiblings: [] };
145
+ }
146
+ const cur = this.asks.get(askId);
147
+ return { claimed: false, current: projectAskTerminalClaimTruth(cur !== undefined && cur.batchId === batchId ? { ...cur } : null) };
148
+ }
132
149
  async expireAsk(askId, batchId) {
133
150
  const now = Date.now();
134
151
  const batch = this.batches.get(batchId);
135
152
  if (!batch)
136
153
  return { won: false, voidedSiblings: [] };
137
154
  const ask = this.asks.get(askId);
138
- if (!ask || ask.state !== "STREAM_PENDING")
155
+ if (!ask || ask.state !== "STREAM_PENDING" || ask.batchId !== batchId)
139
156
  return { won: false, voidedSiblings: [] };
140
157
  ask.state = "PARKING";
141
158
  ask.rev += 1;