@sema-agent/server 7.2.0 → 7.4.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 (140) hide show
  1. package/README.md +2 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +26 -1
  4. package/dist/approval-ask-machine.d.ts +39 -0
  5. package/dist/approval-ask-machine.js +101 -0
  6. package/dist/approval-card.d.ts +244 -0
  7. package/dist/approval-card.js +237 -0
  8. package/dist/approval-deny-reasons.d.ts +56 -0
  9. package/dist/approval-deny-reasons.js +54 -0
  10. package/dist/approval-reconciler.d.ts +174 -0
  11. package/dist/approval-reconciler.js +307 -0
  12. package/dist/boot/coordinators.d.ts +1 -0
  13. package/dist/boot/coordinators.js +39 -4
  14. package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
  15. package/dist/boot/deferred-sandbox-path-env.js +279 -0
  16. package/dist/boot/execution-env.js +11 -1
  17. package/dist/boot/lexical-path-env.d.ts +10 -0
  18. package/dist/boot/lexical-path-env.js +88 -0
  19. package/dist/boot/reapers.d.ts +34 -0
  20. package/dist/boot/reapers.js +198 -23
  21. package/dist/boot/resolve-spec.js +97 -33
  22. package/dist/capabilities/center-prompts.js +4 -1
  23. package/dist/capabilities/oa-tools.d.ts +15 -0
  24. package/dist/capabilities/oa-tools.js +54 -0
  25. package/dist/config-types.d.ts +68 -1
  26. package/dist/config.d.ts +1 -0
  27. package/dist/config.js +138 -2
  28. package/dist/elicitation.d.ts +4 -0
  29. package/dist/elicitation.js +7 -3
  30. package/dist/finance/cost-taxonomy.d.ts +34 -0
  31. package/dist/finance/cost-taxonomy.js +26 -0
  32. package/dist/hooks/hook-runner.js +32 -0
  33. package/dist/http/routes/capabilities.js +14 -0
  34. package/dist/http/routes/diagnostics.d.ts +84 -0
  35. package/dist/http/routes/diagnostics.js +140 -0
  36. package/dist/http/routes/runs.d.ts +1 -0
  37. package/dist/http/routes/runs.js +548 -16
  38. package/dist/http/routes/tasks.js +175 -12
  39. package/dist/http/server.d.ts +6 -1
  40. package/dist/http/server.js +120 -4
  41. package/dist/http/sse-log.d.ts +51 -0
  42. package/dist/http/sse-log.js +64 -0
  43. package/dist/http/wire-types.d.ts +20 -5
  44. package/dist/main.js +35 -4
  45. package/dist/observability/fail-open.d.ts +98 -0
  46. package/dist/observability/fail-open.js +216 -0
  47. package/dist/observability/prompt-manifest.d.ts +13 -0
  48. package/dist/observability/prompt-manifest.js +8 -0
  49. package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
  50. package/dist/plugins/approval-ask-store-memory.js +299 -0
  51. package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
  52. package/dist/plugins/approval-ask-store-sql.js +705 -0
  53. package/dist/plugins/approval-store-sql.d.ts +116 -0
  54. package/dist/plugins/approval-store-sql.js +151 -0
  55. package/dist/plugins/background-agent-store-sql.js +20 -1
  56. package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
  57. package/dist/plugins/checkpoint-store-sql.js +297 -16
  58. package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
  59. package/dist/plugins/file-workflow-journal-store.js +12 -0
  60. package/dist/plugins/local-checkpoint-store.d.ts +6 -5
  61. package/dist/plugins/local-checkpoint-store.js +4 -0
  62. package/dist/plugins/pg-approval-store.d.ts +9 -0
  63. package/dist/plugins/pg-approval-store.js +9 -0
  64. package/dist/plugins/pg-breaker-state.d.ts +8 -0
  65. package/dist/plugins/pg-breaker-state.js +8 -0
  66. package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
  67. package/dist/plugins/pg-checkpoint-store.js +10 -0
  68. package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
  69. package/dist/plugins/pg-file-snapshot-store.js +8 -0
  70. package/dist/plugins/pg-image-bake.d.ts +12 -0
  71. package/dist/plugins/pg-image-bake.js +11 -0
  72. package/dist/plugins/pg-image-index.d.ts +12 -0
  73. package/dist/plugins/pg-image-index.js +11 -0
  74. package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
  75. package/dist/plugins/pg-outcome-ledger.js +11 -0
  76. package/dist/plugins/pg-pool.js +11 -0
  77. package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
  78. package/dist/plugins/pg-resume-anchor-store.js +7 -0
  79. package/dist/plugins/pg-run-store.d.ts +9 -0
  80. package/dist/plugins/pg-run-store.js +9 -0
  81. package/dist/plugins/pg-session-policy-store.d.ts +7 -0
  82. package/dist/plugins/pg-session-policy-store.js +7 -0
  83. package/dist/plugins/pg-session-store.d.ts +12 -0
  84. package/dist/plugins/pg-session-store.js +12 -0
  85. package/dist/plugins/pg-tool-result-store.d.ts +9 -0
  86. package/dist/plugins/pg-tool-result-store.js +9 -0
  87. package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
  88. package/dist/plugins/pg-workflow-journal-store.js +9 -0
  89. package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
  90. package/dist/plugins/pg-workflow-run-store.js +9 -0
  91. package/dist/plugins/store-backend.d.ts +18 -0
  92. package/dist/plugins/store-backend.js +10 -0
  93. package/dist/plugins/tidb-approval-store.d.ts +8 -0
  94. package/dist/plugins/tidb-approval-store.js +8 -0
  95. package/dist/plugins/tidb-breaker-state.d.ts +7 -0
  96. package/dist/plugins/tidb-breaker-state.js +7 -0
  97. package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
  98. package/dist/plugins/tidb-checkpoint-store.js +9 -0
  99. package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
  100. package/dist/plugins/tidb-file-snapshot-store.js +8 -0
  101. package/dist/plugins/tidb-image-bake.d.ts +12 -0
  102. package/dist/plugins/tidb-image-bake.js +11 -0
  103. package/dist/plugins/tidb-image-index.d.ts +12 -0
  104. package/dist/plugins/tidb-image-index.js +11 -0
  105. package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
  106. package/dist/plugins/tidb-outcome-ledger.js +12 -0
  107. package/dist/plugins/tidb-pool.js +27 -4
  108. package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
  109. package/dist/plugins/tidb-resume-anchor-store.js +7 -0
  110. package/dist/plugins/tidb-run-store.d.ts +10 -0
  111. package/dist/plugins/tidb-run-store.js +9 -0
  112. package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
  113. package/dist/plugins/tidb-session-policy-store.js +7 -0
  114. package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
  115. package/dist/plugins/tidb-tool-result-store.js +10 -0
  116. package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
  117. package/dist/plugins/tidb-workflow-journal-store.js +9 -0
  118. package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
  119. package/dist/plugins/tidb-workflow-run-store.js +10 -0
  120. package/dist/plugins/workflow-journal-limits.d.ts +12 -0
  121. package/dist/plugins/workflow-journal-limits.js +12 -0
  122. package/dist/question.d.ts +21 -14
  123. package/dist/question.js +83 -34
  124. package/dist/runs.d.ts +16 -1
  125. package/dist/runs.js +61 -3
  126. package/dist/runtime-caps-resolver.d.ts +7 -1
  127. package/dist/runtime-caps-resolver.js +65 -3
  128. package/dist/sema-registry.d.ts +41 -0
  129. package/dist/sema-registry.js +40 -0
  130. package/dist/spec-fields.d.ts +4 -0
  131. package/dist/spec-fields.js +6 -0
  132. package/dist/task-settings.d.ts +36 -15
  133. package/dist/task-settings.js +19 -5
  134. package/dist/tool-approval.d.ts +296 -3
  135. package/dist/tool-approval.js +1074 -50
  136. package/dist/trace/core-keyset-guard.d.ts +2 -2
  137. package/dist/trace/ledger-sink.js +14 -1
  138. package/dist/trace/project.d.ts +90 -0
  139. package/dist/trace/project.js +188 -0
  140. package/package.json +5 -4
@@ -230,12 +230,27 @@ export interface TaskRequestBody {
230
230
  /** [1144]/[1146] (core 1.328 R2): 提示词双形轴——simple(CC 212 短形,引擎缺省)| classic(长形,可按
231
231
  * 任务/模型试分)。纯呈现轴无租户门;枚举 fail-loud at submit;缺省不挂键。 */
232
232
  promptProfile?: "simple" | "classic";
233
+ /** [2856]②(core 5.14.0 座已在,`TaskSpec.toolMaterializeStrategy`):deferred 工具在**激活之后**怎么给
234
+ * 模型供 schema。`static` = 占位符形不变(激活只改可调用性,广告的 schema 仍是空对象);`swap` = 激活
235
+ * 后下一轮把占位符换成真 schema。
236
+ * 为什么要上 wire:cli 的 #181 取证表明,忠实跟随广告 schema 解码的 provider(openai-completions 车道)
237
+ * 在 `static` 下会**无界循环** —— 每轮发 `{}` → invalidArgumentsRejection → 下一轮广告仍空,纠错回路
238
+ * 结构上无效。core 的缺省是 `spec.toolMaterializeStrategy ?? env ?? "static"`,进程级 env 说不了
239
+ * 「每个模型」的话(模型目录车道 per-model api 可混),所以必须有一个**按任务**的位子。
240
+ * 缺省不挂键(交给 core 的 env/缺省链);未知词 400 fail-loud(与 promptProfile/permissionMode 同姿势 ——
241
+ * 静默折缺省会让调用方以为切了策略却没切)。 */
242
+ toolMaterializeStrategy?: "static" | "swap";
233
243
  /** R4 (CC parity): the LIGHT top-level per-turn permission-mode intent (CC `permissionMode`). The shell sends
234
- * the RAW mode (axis-agnostic, no client-side interpretation); the SERVICE interprets it tighten-only: `plan`
235
- * read-only hands + `present_plan` (EnterPlanMode parity); `acceptEdits`/`bypassPermissions` LOOSEN coerced to
236
- * `default` (never honored remotely; the client enforces them on its own machine). Folds onto the SAME tighten-only
237
- * governance as `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path. */
238
- permissionMode?: string;
244
+ * the RAW mode (axis-agnostic, no client-side interpretation); the SERVICE interprets it tighten-only vs the
245
+ * deployment baseline. Post-[816] all FIVE modes are honored as gate-SHAPE choices (`plan` read-only hands +
246
+ * `present_plan`; `acceptEdits`/`bypassPermissions`/`auto` select how much the mode-derived fs-write ask gate
247
+ * asks they can never subtract from deployment/operator policy, tightenTaskSpec deny-wins; the old "LOOSEN
248
+ * coerced to default" note was pre-[816] doc-rot). Folds onto the SAME tighten-only governance as
249
+ * `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path.
250
+ * #157-②: CLOSED enum on the wire — an unknown word 400s at submit (`request.field_invalid`, sibling of
251
+ * promptProfile) instead of silently coercing to `default`; resume replay of a stored body stays lenient
252
+ * (coercePermissionMode). */
253
+ permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto";
239
254
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
240
255
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the design/138 memory ENGINE materializes/
241
256
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend } from "@sema-agent/core";
4
+ import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
5
5
  import { createSessionTitler } from "./session-titler.js";
6
6
  import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
@@ -13,7 +13,7 @@ import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
13
13
  import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
14
14
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
15
15
  import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver, shouldProbeWebSearchOnBoot } from "./plugins/web-search.js";
16
- import { createAuthorizer } from "./security.js";
16
+ import { createAuthorizer, encodeCheckpointScope } from "./security.js";
17
17
  import { assertGateIntentServiceable, createDurableAskPolicy, hasOperatorGateIntent } from "./approval.js";
18
18
  import { loadSkills } from "./capabilities/skills.js";
19
19
  import { GiteaClient } from "./capabilities/repo-tools.js";
@@ -22,6 +22,7 @@ import { createLogger } from "./observability/logger.js";
22
22
  import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
23
23
  import { createRegistryJwtVerifier } from "./auth-bridge.js";
24
24
  import { createMetrics } from "./observability/metrics.js";
25
+ import { installFailOpenRecorder } from "./observability/fail-open.js";
25
26
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
26
27
  import { RateLimiter } from "./observability/rate-limit.js";
27
28
  import { createHttpServer, explicitOperatorOk } from "./http/server.js";
@@ -49,6 +50,7 @@ import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js"
49
50
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
50
51
  import { createSessionFaces } from "./boot/session-faces.js";
51
52
  import { createLeaderFace } from "./boot/leader.js";
53
+ import { assertStaticWiringConsistent } from "./http/routes/diagnostics.js";
52
54
  import { installShutdownHandlers } from "./boot/shutdown.js";
53
55
  /** Service version for GET /v1/capabilities — read from the package.json shipped next to src/ in the
54
56
  * image (Dockerfile COPYs it). Best-effort: an unreadable file reports "unknown", never crashes boot. */
@@ -105,6 +107,10 @@ async function main() {
105
107
  }
106
108
  const logger = createLogger(config.logLevel);
107
109
  const metrics = createMetrics();
110
+ // #157 fail-open 治理:把 logger/metrics 接到进程级 failOpen 记录器上(见 observability/fail-open.ts)。
111
+ // 排在这里而不是更晚:兜底臂遍布装配链,越早接上,越少的 fail-open 只剩探针行没有结构化痕迹。
112
+ // 装配前已累计的计数与已喊过的 tag 由 install 继承,不重置也不重喊。
113
+ installFailOpenRecorder({ logger, metrics });
108
114
  // S20 (SILENT-FALLBACK P1) + [792]④ boot notices: config parsing runs before the logger exists — drain both
109
115
  // pre-logger collectors via the shared helper (F10: run-local uses the same one, so entrypoints can't drift).
110
116
  logConfigDiagnostics(logger);
@@ -181,7 +187,7 @@ async function main() {
181
187
  const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
182
188
  // design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
183
189
  // 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
184
- const { elicitation, question, toolApproval, durableEnabled, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
190
+ const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
185
191
  // design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
186
192
  const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
187
193
  // design/170 件A(#148 件3③):org 记忆准入装配(目录源三态选择+C12 能力探测,坏配置在此拒启动)。
@@ -658,7 +664,7 @@ async function main() {
658
664
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
659
665
  rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
660
666
  workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
661
- backgroundAgentStore, mailboxStore,
667
+ backgroundAgentStore, mailboxStore, toolApproval,
662
668
  getRunDenySweep: () => runDenySweep,
663
669
  });
664
670
  // Optional OTLP/HTTP metrics export (1.37). Periodically pushes the registry to an OTel collector;
@@ -704,6 +710,30 @@ async function main() {
704
710
  // asyncRewake wake 管道 holder:server 启动时装 deliver(session→本副本活流 steer);createTaskHooks 的
705
711
  // ctx.wake 闭包经它投递(main 建壳/server 填实,双向不循环依赖)。
706
712
  const hookWakeBus = {};
713
+ // ═══════════ #154 件二:启动期静态装配自检(coordinators 构造后、listen 之前)═══════════
714
+ // core 的 `describeStaticWiring` 与本仓的装配谓词是**两个独立实现**在回答同一组问题。答案矛盾 =
715
+ // 装配谎言(不是「配置不理想」:是服务端对自己接了什么线有两套互斥说法),fail-loud 拒启 ——
716
+ // 与 memory+durable 两条既有拒启同族。判据本体在 http/routes/diagnostics.ts(诊断读面与拒启判据同源)。
717
+ //
718
+ // specTemplate = resolve-spec 对**每一条**任务腿都会盖的那两个 durable 键(它的 durableEnabled 分支)。
719
+ // 静态半场只读它们的**在场性**(`parkDurableApprovalOptIn = spec.durableApproval !== undefined`),
720
+ // 所以这里给的是那个形,而不是某条腿的 principal 真值。
721
+ const wiringSpecTemplate = durableEnabled
722
+ ? { checkpointStore, durableApproval: { scope: encodeCheckpointScope(undefined) } }
723
+ : {};
724
+ const staticWiring = describeStaticWiring(runnerDeps, wiringSpecTemplate);
725
+ // `parkFacility` 传的必须是**协调器真喂给 `resolveStreamApprovalGate` 的那个实参**(durableEnabled),
726
+ // 不是在这里重算一遍 —— 重算会把「两个站点算出了不同答案」这件正要被查的事抹平。
727
+ assertStaticWiringConsistent({ manifest: staticWiring, parkFacility: durableEnabled, backendKind: backend?.kind }, logger);
728
+ // operator 日志面(**非 wire**)。`configFingerprint` 只有 effective 半场才铸(core 按腿算),静态半场
729
+ // 没有这一位 ⇒ 诚实报 null,不自造一个服务端哈希(那会是一个要跨仓对表的新语义)。
730
+ logger.info("wiring_static", {
731
+ fingerprint: staticWiring.configFingerprint ?? null,
732
+ parkEffective: staticWiring.parkLane.effective,
733
+ sessionDurability: staticWiring.session.store,
734
+ posture: staticWiring.interaction.posture,
735
+ streamApproval: streamApprovalGate.active ? "active" : streamApprovalGate.reason,
736
+ });
707
737
  // ═══════════ design/158 A8:ServiceDeps 分组装配(七组窄接口)═══════════
708
738
  // 每组一个**带类型标注**的中间 const —— 标注位触发 TS 多余属性检查,键放错组当场编译红。
709
739
  // (平铺形做不到:69 键的全可选面里任何键名都「属于」它,于是键落错家/从未接线只能靠源码钉
@@ -823,6 +853,7 @@ async function main() {
823
853
  modelReady: () => configCenter.modelReady(), // boot ready 门:roster 未落地=计费提交 503+health ready:false
824
854
  scenarioDetails, // 场景详情只读面(内建+center 同判定源)
825
855
  workflowsCapable, // ① core split: capabilities.workflows = engine-can (workflowsCapability); workflowsList = store (below)
856
+ staticWiring, // #154 件三:GET /v1/diagnostics/wiring 的 static 半场(与上面的拒启自检同一份产物)
826
857
  instanceId,
827
858
  // Capability discovery: the deployment facts only main knows. Store-derived booleans
828
859
  // (asyncRuns/artifacts/approvals/leader) are computed inside the server from the deps themselves.
@@ -0,0 +1,98 @@
1
+ import type { Logger } from "./logger.js";
2
+ import type { Metrics } from "./metrics.js";
3
+ /** 登记项:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装),`note` 说清"放行的最坏后果"。 */
4
+ export interface FailOpenTagEntry {
5
+ /** `F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";`P-DEBT`=保护型欠账,方向明知不对、显式记债。 */
6
+ readonly cls: "F" | "P-DEBT";
7
+ /** 这条兜底放行了什么、最坏后果是什么。写给未来读遥测的人,不写来历。 */
8
+ readonly note: string;
9
+ }
10
+ /** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
11
+ export declare const FAIL_OPEN_TAGS: {
12
+ readonly "server.hitl.frame-undelivered-stream-closed": {
13
+ readonly cls: "P-DEBT";
14
+ readonly note: "HITL 的 **open** 帧(question / elicitation)写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,问到 TTL 才按无人应答结算。方向(no-op 而非 throw)是既有的刻意决定,本条只保证它不再无声。";
15
+ };
16
+ readonly "server.stream.frame-dropped-stream-closed": {
17
+ readonly cls: "F";
18
+ readonly note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。";
19
+ };
20
+ readonly "server.question.open-frame-undelivered": {
21
+ readonly cls: "P-DEBT";
22
+ readonly note: "AskUserQuestion 的 open 帧投递失败 ⇒ 按 headless 默认结算,模型不问人自己往下判。方向是刻意的产品姿态(绝不把 run 挂死),但一次投递故障确实让人在环这道门被跳过——记债,不当合法兜底。";
23
+ };
24
+ readonly "server.elicitation.open-frame-undelivered": {
25
+ readonly cls: "F";
26
+ readonly note: "MCP elicitation 的 open 帧投递失败 ⇒ 结算 decline。方向本身是 fail-closed(问不到人就是拒),缺的只是留痕。";
27
+ };
28
+ readonly "server.question.complete-breadcrumb-dropped": {
29
+ readonly cls: "F";
30
+ readonly note: "question_complete 面包屑(对话框消解提示)持久化失败。答案本身走的是另一条路且早已返回,丢的只是壳里一次收尾渲染。";
31
+ };
32
+ readonly "server.elicitation.complete-breadcrumb-dropped": {
33
+ readonly cls: "F";
34
+ readonly note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。";
35
+ };
36
+ readonly "server.prompts.artifact-cache-write-failed": {
37
+ readonly cls: "F";
38
+ readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
39
+ };
40
+ };
41
+ /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
42
+ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
43
+ /**
44
+ * 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
45
+ *
46
+ * 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
47
+ * 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
48
+ * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
49
+ */
50
+ export declare function failOpenTagForDroppedFrame(frameType: string): FailOpenTag;
51
+ /** {@link createFailOpenRecorder} 返回的活对象(有行为、有状态 ⇒ `create*` 而非 `build*`)。 */
52
+ export interface FailOpenRecorder {
53
+ /** 走到一条登记过的兜底臂。总不抛——观测本身绝不能变成故障(见 reapers 同族判据)。 */
54
+ record(tag: FailOpenTag, detail?: string): void;
55
+ /** 本进程各 tag 的累计次数(逐次计数,与一次性 warn 无关)。 */
56
+ counts(): ReadonlyMap<FailOpenTag, number>;
57
+ /** 已经喊过 stderr 契约行的 tag 集合——装配时由新实例继承,避免 install 让同一 tag 再喊一次。 */
58
+ warnedTags(): ReadonlySet<FailOpenTag>;
59
+ /** 还没进过任何 metrics 汇的次数(装配前命中的欠账)。install 用它补账,补完清零。 */
60
+ metricsBacklog(): ReadonlyMap<FailOpenTag, number>;
61
+ /** 已经进过结构化日志的 tag 集合。与 {@link warnedTags} **分开**:stderr 契约行装配前就能喊,
62
+ * 结构化 warn 却要等 logger 到场——两者共用一个集合会让装配前命中的 tag 永远拿不到结构化痕迹。 */
63
+ loggedTags(): ReadonlySet<FailOpenTag>;
64
+ }
65
+ /** 探针文件的 env 名(跨仓同名同形:`audits/failopen-governance-176.md` §2-r2,壳/引擎同认)。 */
66
+ export declare const FAIL_OPEN_PROBE_ENV = "SEMA_FAILOPEN_PROBE";
67
+ export interface FailOpenRecorderDeps {
68
+ logger?: Logger;
69
+ metrics?: Metrics;
70
+ /** 探针文件路径。缺省=读 `SEMA_FAILOPEN_PROBE`(读一次,不每次调用都碰 env)。 */
71
+ probePath?: string;
72
+ /** 继承自上一个实例的状态(install 换实例时不重置计数、不重喊 warn、不丢欠账)。 */
73
+ seedCounts?: ReadonlyMap<FailOpenTag, number>;
74
+ seedWarned?: ReadonlySet<FailOpenTag>;
75
+ seedBacklog?: ReadonlyMap<FailOpenTag, number>;
76
+ seedLogged?: ReadonlySet<FailOpenTag>;
77
+ /** 测试注入:替掉 stderr 契约行的落点。缺省直写 `process.stderr`。 */
78
+ writeStderr?: (line: string) => void;
79
+ /** 测试注入:替掉探针文件追加。缺省 `appendFileSync`(同步=进程猝死也不丢已记的行)。 */
80
+ appendProbe?: (path: string, line: string) => void;
81
+ }
82
+ export declare function createFailOpenRecorder(deps?: FailOpenRecorderDeps): FailOpenRecorder;
83
+ /**
84
+ * 装配层接线:把 logger/metrics 接到进程单例上。搬四份状态过去,各有各的理由:
85
+ * - `seedCounts` —— 逻辑总数续算,不从 0 重来;
86
+ * - `seedWarned` —— boot 期已喊过的 stderr 契约行不再喊(「每进程每 tag 一次」是它的全部价值);
87
+ * - `seedBacklog` —— 装配**前**命中的次数还没进任何 metrics 汇,交给新实例补账(补完清零,
88
+ * 所以第二次 install 不会重放);
89
+ * - `seedLogged` —— 已真发出去的结构化 warn 不重发;没发过的由新 logger 补一条。
90
+ */
91
+ export declare function installFailOpenRecorder(deps: Omit<FailOpenRecorderDeps, "seedCounts" | "seedWarned" | "seedBacklog" | "seedLogged">): FailOpenRecorder;
92
+ /** 兜底臂的调用口。`tag` 必须已登记(闭集类型),`detail` 是可选的一行现场(路径/原因,不放密钥)。 */
93
+ export declare function recordFailOpen(tag: FailOpenTag, detail?: string): void;
94
+ /** 本进程各 tag 累计次数——`/metrics` 之外的进程内读数(测试与自检面用)。 */
95
+ export declare function failOpenCounts(): ReadonlyMap<FailOpenTag, number>;
96
+ /** test-only:换一个全新单例(计数与 warn 集合清零),让每个用例都从"第一次"看起。 */
97
+ export declare function resetFailOpenRecorderForTest(deps?: FailOpenRecorderDeps): FailOpenRecorder;
98
+ //# sourceMappingURL=fail-open.d.ts.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * #157 fail-open 治理专项 —— 兜底路径的**响亮化**工具(server 半场;跨仓方案见 sema-comms
3
+ * `audits/failopen-governance-176.md` §2,三仓语义对齐、实现各自)。
4
+ *
5
+ * ## 它解决的问题
6
+ * 「走了兜底」与「一切正常」在外部**同形**——成功回执一样、日志一样、指标一样。于是一个静默降级
7
+ * 能带病运行数周,只能靠"该发生的事没发生"这种间接症状发现。本模块给每条合法兜底臂一个**登记过的
8
+ * tag**,走到即留痕三件套:
9
+ *
10
+ * 1. **计数(逐次)** —— `fail_open_total{tag,cls}`;
11
+ * 2. **探针文件(逐次)** —— `SEMA_FAILOPEN_PROBE=<path>` 在场时逐次追加 `fail-open <tag> <detail>`。
12
+ * 逐次是承重的:同进程多腿时"第二腿起无行"会让「无行=没走兜底」的黑盒断言变成假绿;
13
+ * 3. **一次性 warn(每进程每 tag 一次)** —— 防刷屏。**只有这一件是一次性的**,计数与探针都逐次。
14
+ *
15
+ * ## 它**不**解决的问题(封装不是合法化通道)
16
+ * 词表的 `cls` 只有 `F` 与 `P-DEBT` 两种,**结构性**地把 P/Z 类挡在外面:
17
+ * - **P 保护型**(权限门/判定词表/凭证/车道选择):正解是 fail-closed,不是"留痕后继续放行"。
18
+ * 确实一时改不动的,才允许以 `P-DEBT` 登记——让债在遥测里显形,而不是让它消失。
19
+ * - **Z 计量型**(重试/预算/超时/清理范围):正解是归零 + 自己的节流告警
20
+ * (先例:`boot/reapers.ts` 的 `createThrottledReaperCatch`),不走本封装。
21
+ *
22
+ * ## 词表是闭集
23
+ * {@link FailOpenTag} 由 {@link FAIL_OPEN_TAGS} 的键推导,未登记的 tag 是**编译期错误**——
24
+ * "新 tag 须登记"因此是机器执法的,不靠自觉。新增 tag 同时要在 `docs/FAIL-OPEN-CENSUS.md` 补行。
25
+ *
26
+ * ## 进程级而非注入级
27
+ * 三件套里有两件(一次性 warn、进程计数)天然是**进程**范围的,且真正的兜底臂往往在没有 logger/
28
+ * metrics 句柄的深处(`question.ts` / `elicitation.ts` 全文零观测通道)。所以对外主口是自由函数
29
+ * {@link recordFailOpen},装配层用 {@link installFailOpenRecorder} 把 logger/metrics 接上即可;
30
+ * 装配之前的调用不会丢——探针与 stderr 契约行不依赖注入,计数也照常累加并在装配时继承过去。
31
+ */
32
+ import { appendFileSync } from "node:fs";
33
+ /** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
34
+ export const FAIL_OPEN_TAGS = {
35
+ "server.hitl.frame-undelivered-stream-closed": {
36
+ cls: "P-DEBT",
37
+ note: "HITL 的 **open** 帧(question / elicitation)写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,问到 TTL 才按无人应答结算。方向(no-op 而非 throw)是既有的刻意决定,本条只保证它不再无声。",
38
+ },
39
+ "server.stream.frame-dropped-stream-closed": {
40
+ cls: "F",
41
+ note: "非 HITL-open 的运行流帧写向已断连接 ⇒ 丢弃。连接都没了,这一帧本就无人可看;单独立项是为了不让它挤进上面那条保护型计数。",
42
+ },
43
+ "server.question.open-frame-undelivered": {
44
+ cls: "P-DEBT",
45
+ note: "AskUserQuestion 的 open 帧投递失败 ⇒ 按 headless 默认结算,模型不问人自己往下判。方向是刻意的产品姿态(绝不把 run 挂死),但一次投递故障确实让人在环这道门被跳过——记债,不当合法兜底。",
46
+ },
47
+ "server.elicitation.open-frame-undelivered": {
48
+ cls: "F",
49
+ note: "MCP elicitation 的 open 帧投递失败 ⇒ 结算 decline。方向本身是 fail-closed(问不到人就是拒),缺的只是留痕。",
50
+ },
51
+ "server.question.complete-breadcrumb-dropped": {
52
+ cls: "F",
53
+ note: "question_complete 面包屑(对话框消解提示)持久化失败。答案本身走的是另一条路且早已返回,丢的只是壳里一次收尾渲染。",
54
+ },
55
+ "server.elicitation.complete-breadcrumb-dropped": {
56
+ cls: "F",
57
+ note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。",
58
+ },
59
+ "server.prompts.artifact-cache-write-failed": {
60
+ cls: "F",
61
+ note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
62
+ },
63
+ };
64
+ /**
65
+ * 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
66
+ *
67
+ * 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
68
+ * 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
69
+ * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
70
+ */
71
+ export function failOpenTagForDroppedFrame(frameType) {
72
+ switch (frameType) {
73
+ case "question":
74
+ case "elicitation":
75
+ return "server.hitl.frame-undelivered-stream-closed";
76
+ case "question_complete":
77
+ return "server.question.complete-breadcrumb-dropped";
78
+ case "elicitation_complete":
79
+ return "server.elicitation.complete-breadcrumb-dropped";
80
+ default:
81
+ return "server.stream.frame-dropped-stream-closed";
82
+ }
83
+ }
84
+ /** 探针文件的 env 名(跨仓同名同形:`audits/failopen-governance-176.md` §2-r2,壳/引擎同认)。 */
85
+ export const FAIL_OPEN_PROBE_ENV = "SEMA_FAILOPEN_PROBE";
86
+ /** 计数器名。`cls` 进标签:一眼分得出"合法兜底"与"保护型欠账"两种曲线,后者应当只降不升。 */
87
+ const FAIL_OPEN_COUNTER = "fail_open_total";
88
+ export function createFailOpenRecorder(deps = {}) {
89
+ const counts = new Map(deps.seedCounts ?? []);
90
+ const warned = new Set(deps.seedWarned ?? []);
91
+ const logged = new Set(deps.seedLogged ?? []);
92
+ const backlog = new Map(deps.seedBacklog ?? []);
93
+ const probePath = deps.probePath ?? process.env[FAIL_OPEN_PROBE_ENV];
94
+ const writeStderr = deps.writeStderr ?? ((line) => void process.stderr.write(line));
95
+ const appendProbe = deps.appendProbe ?? ((path, line) => appendFileSync(path, line));
96
+ deps.metrics?.counter(FAIL_OPEN_COUNTER, "fallback (fail-open) arms traversed, by registered tag — see docs/FAIL-OPEN-CENSUS.md");
97
+ // 补账:装配前(无 metrics/无 logger)命中的那几次不能凭空消失——否则 `fail_open_total` 永久
98
+ // 少算,少的还正是 boot 期兜底。metrics 到场即把欠账一次性灌进去并清零(所以再 install 一次不会重放),
99
+ // 结构化 warn 同理按 tag 补一条。stderr 契约行**不补**:它在装配前就已经喊过了,一 tag 一次是它的全部价值。
100
+ // 🔴 记账**后于**投递(codex R2 复审 MEDIUM):先清 backlog / 先 `logged.add` 再投递,等于把一次
101
+ // 投递失败记成"已送达"——欠账从此无法重放,而它保的正是最需要补的那批 boot 期兜底。
102
+ if (deps.metrics !== undefined && backlog.size > 0) {
103
+ for (const [tag, n] of [...backlog]) {
104
+ try {
105
+ deps.metrics.inc(FAIL_OPEN_COUNTER, { tag, cls: FAIL_OPEN_TAGS[tag].cls }, n);
106
+ backlog.delete(tag); // 只有真进了汇才销账
107
+ }
108
+ catch {
109
+ /* 留在 backlog 里,下一次装配再补 */
110
+ }
111
+ }
112
+ }
113
+ // 补痕的起算集=**观测到的 tag 全集**(`counts`),不是"stderr 喊成功过的那批"(`warned`)——
114
+ // 后者在"装配前命中 + stderr 正断裂"这条组合路上恒空,那次兜底的结构化痕迹就永久没了(codex R3)。
115
+ if (deps.logger !== undefined) {
116
+ for (const tag of counts.keys()) {
117
+ if (logged.has(tag))
118
+ continue;
119
+ try {
120
+ deps.logger.warn("fail_open", { tag, cls: FAIL_OPEN_TAGS[tag].cls, note: FAIL_OPEN_TAGS[tag].note, backfilled: true });
121
+ logged.add(tag); // 同上:warn 真发出去才算已报
122
+ }
123
+ catch {
124
+ /* 不记 logged,下一次装配/下一次命中再补 */
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ record: (tag, detail) => {
130
+ // 三件套逐件独立 try:一件的失败(坏路径的探针文件、抛错的 logger 传输)绝不能让**另外两件**
131
+ // 也丢,更不能让调用方的兜底臂变成抛错——那等于把"降级"换成"故障",正是本模块要消灭的方向。
132
+ const entry = FAIL_OPEN_TAGS[tag];
133
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
134
+ if (deps.metrics === undefined) {
135
+ backlog.set(tag, (backlog.get(tag) ?? 0) + 1); // 记欠账,等 install 补
136
+ }
137
+ else {
138
+ try {
139
+ deps.metrics.inc(FAIL_OPEN_COUNTER, { tag, cls: entry.cls });
140
+ }
141
+ catch {
142
+ // 汇在场但这一次没进去 ⇒ 同样记欠账(而不是蒸发),等下一次装配补。
143
+ backlog.set(tag, (backlog.get(tag) ?? 0) + 1);
144
+ }
145
+ }
146
+ if (probePath !== undefined && probePath !== "") {
147
+ try {
148
+ appendProbe(probePath, `fail-open ${tag}${detail !== undefined && detail !== "" ? ` ${detail}` : ""}\n`);
149
+ }
150
+ catch {
151
+ /* 探针路径不可写(只读盘/权限)⇒ 静默放弃,不牵连本次兜底 */
152
+ }
153
+ }
154
+ // 只有**真发出去**才记进 logged —— 没有 logger 时记了,会让日后 install 的补账把这条当成
155
+ // "已经报过"而跳过,那正是本轮复审要修的漏账形。
156
+ if (deps.logger !== undefined && !logged.has(tag)) {
157
+ try {
158
+ deps.logger.warn("fail_open", { tag, cls: entry.cls, note: entry.note, ...(detail !== undefined ? { detail } : {}) });
159
+ logged.add(tag); // 记账后于投递:没发出去就不算已报,下一次命中重试
160
+ }
161
+ catch {
162
+ /* 观测不得成为故障 */
163
+ }
164
+ }
165
+ if (warned.has(tag))
166
+ return;
167
+ try {
168
+ // 跨仓约定的黑盒锚,逐字 `[sema] fail-open: <tag>`。**不依赖** logger 是否装配上——
169
+ // 评测线的断言打的就是这一行,它在 boot 之前的兜底上也必须成立。
170
+ writeStderr(`[sema] fail-open: ${tag}\n`);
171
+ warned.add(tag); // 同上。stderr 永久断裂时代价=每次命中试写一次(有界),不是丢掉这条契约行
172
+ }
173
+ catch {
174
+ /* 断裂的 stderr(EPIPE 族)⇒ 丢这行,不丢进程 */
175
+ }
176
+ },
177
+ counts: () => counts,
178
+ warnedTags: () => warned,
179
+ metricsBacklog: () => backlog,
180
+ loggedTags: () => logged,
181
+ };
182
+ }
183
+ /** 进程单例。装配前也可用(探针+stderr 契约行照常,计数照常;只是没有结构化 warn/metric)。 */
184
+ let current = createFailOpenRecorder();
185
+ /**
186
+ * 装配层接线:把 logger/metrics 接到进程单例上。搬四份状态过去,各有各的理由:
187
+ * - `seedCounts` —— 逻辑总数续算,不从 0 重来;
188
+ * - `seedWarned` —— boot 期已喊过的 stderr 契约行不再喊(「每进程每 tag 一次」是它的全部价值);
189
+ * - `seedBacklog` —— 装配**前**命中的次数还没进任何 metrics 汇,交给新实例补账(补完清零,
190
+ * 所以第二次 install 不会重放);
191
+ * - `seedLogged` —— 已真发出去的结构化 warn 不重发;没发过的由新 logger 补一条。
192
+ */
193
+ export function installFailOpenRecorder(deps) {
194
+ current = createFailOpenRecorder({
195
+ ...deps,
196
+ seedCounts: current.counts(),
197
+ seedWarned: current.warnedTags(),
198
+ seedBacklog: current.metricsBacklog(),
199
+ seedLogged: current.loggedTags(),
200
+ });
201
+ return current;
202
+ }
203
+ /** 兜底臂的调用口。`tag` 必须已登记(闭集类型),`detail` 是可选的一行现场(路径/原因,不放密钥)。 */
204
+ export function recordFailOpen(tag, detail) {
205
+ current.record(tag, detail);
206
+ }
207
+ /** 本进程各 tag 累计次数——`/metrics` 之外的进程内读数(测试与自检面用)。 */
208
+ export function failOpenCounts() {
209
+ return current.counts();
210
+ }
211
+ /** test-only:换一个全新单例(计数与 warn 集合清零),让每个用例都从"第一次"看起。 */
212
+ export function resetFailOpenRecorderForTest(deps = {}) {
213
+ current = createFailOpenRecorder(deps);
214
+ return current;
215
+ }
216
+ //# sourceMappingURL=fail-open.js.map
@@ -31,6 +31,12 @@ export interface PromptManifestRecord {
31
31
  shapeDigest: string;
32
32
  wireSchemaDigest: string;
33
33
  }>;
34
+ /** core 5.15.0(#181):deferred 披露策略自证三键(诊断值,零内容)。缺席=core <5.15 或无 deferred。 */
35
+ toolDisclosure?: {
36
+ deferredTools: number;
37
+ strategy: string;
38
+ source: string;
39
+ };
34
40
  totalChars: number;
35
41
  }
36
42
  /** Whitelist one `prompt.assembled` trace event into a {@link PromptManifestRecord} (v1 face required,
@@ -61,6 +67,13 @@ export declare function promptManifestRecordOf(e: {
61
67
  shapeDigest: string;
62
68
  wireSchemaDigest: string;
63
69
  }>;
70
+ /** core 5.15.0(#181,[2872] 提货②):deferred 工具的披露策略自证——{deferredTools(数), strategy
71
+ * ("static"|"swap"), source(四值:spec/env/api-default/default)}。诊断三键(数+两闭集词,零内容)。 */
72
+ toolDisclosure?: {
73
+ deferredTools: number;
74
+ strategy: string;
75
+ source: string;
76
+ };
64
77
  totalChars: number;
65
78
  }): PromptManifestRecord;
66
79
  /**
@@ -14,6 +14,14 @@ export function promptManifestRecordOf(e) {
14
14
  ...(Array.isArray(e.tools)
15
15
  ? { tools: e.tools.map((t) => ({ wireName: String(t.wireName), aliases: (Array.isArray(t.aliases) ? t.aliases : []).map(String), contractId: String(t.contractId), implementationRevision: String(t.implementationRevision), cardId: String(t.cardId), shapeDigest: String(t.shapeDigest), wireSchemaDigest: String(t.wireSchemaDigest) })) }
16
16
  : {}),
17
+ // toolDisclosure(core 5.15.0 #181,[2872] 提货②):三键逐挑——数值有限、两词字符串非空才铸;
18
+ // 畸形按缺席(不铸假值,与 sections/tools 的 additive 同规)。
19
+ ...(typeof e.toolDisclosure === "object" && e.toolDisclosure !== null &&
20
+ typeof e.toolDisclosure.deferredTools === "number" && Number.isFinite(e.toolDisclosure.deferredTools) &&
21
+ typeof e.toolDisclosure.strategy === "string" && e.toolDisclosure.strategy.length > 0 &&
22
+ typeof e.toolDisclosure.source === "string" && e.toolDisclosure.source.length > 0
23
+ ? { toolDisclosure: { deferredTools: e.toolDisclosure.deferredTools, strategy: e.toolDisclosure.strategy, source: e.toolDisclosure.source } }
24
+ : {}),
17
25
  totalChars: e.totalChars,
18
26
  };
19
27
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `InMemoryApprovalAskStore` —— #151 车1,`ApprovalAskStore` 的语义真源(twin 对拆基准)+ LOCAL
3
+ * 车道占位(File 形不在本车,见设计定稿 §7)。单进程内两个 `Map`,方法体全同步(不含 `await` 中断点)——
4
+ * Node 单线程下这就是天然原子:`Promise.all` 发起的并发调用不会在方法体内部交错执行,与
5
+ * `approval-ask-store-sql.ts` 的 CAS UPDATE 语义逐字段一致(SQL twin 对拆套件的基准就是这份实现)。
6
+ *
7
+ * 行为口径与 `SqlApprovalAskStore` 逐条对齐:
8
+ * - ensureAsk 幂等 upsert(ask_id 已在 ⇒ 返回既有行,不改写;batch 行若不存在则以 OPEN 补齐)。
9
+ * - transitionAsk 先过 machine 层 `canAskTransition`(非法转移 throw,不返回 false)。
10
+ * - decideAsk/expireAsk/bindBatch/abortBatch 的双/三步判定顺序与 SQL twin 的事务步骤一一对应
11
+ * (哪一步先判、判什么、失败时是否触碰另一半状态——照抄,不是「反正内存里原子就随便」)。
12
+ * - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
13
+ */
14
+ import { type AskState, type BatchState } from "../approval-ask-machine.js";
15
+ import type { AskDecision, AskRow, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
16
+ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
17
+ private readonly asks;
18
+ private readonly batches;
19
+ ensureAsk(row: NewAskRow): Promise<AskRow>;
20
+ transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
21
+ decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
22
+ expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
23
+ bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
24
+ deferReconcile(askId: string, expectedState: AskState, expectedVersion: number, nowMs: number): Promise<boolean>;
25
+ abortBatch(batchId: string): Promise<string[]>;
26
+ listByState(state: AskState, limit: number): Promise<AskRow[]>;
27
+ /** `signal` 的 twin 语义(车3 刀 3b):同步实现里没有「在飞」这回事,所以能做也只需做**入口复核** ——
28
+ * 已 abort 就一条都不读、如实抛,与 SQL twin 的第①条语义逐字一致(第②条在这里天然平凡成立)。 */
29
+ listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
30
+ listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
31
+ getAsk(askId: string): Promise<AskRow | null>;
32
+ getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
33
+ resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
34
+ deleteByTask(taskId: string): Promise<void>;
35
+ getBatch(batchId: string): Promise<BatchRow | null>;
36
+ }
37
+ export type { AskDecision, AskState, BatchState };
38
+ //# sourceMappingURL=approval-ask-store-memory.d.ts.map