@sema-agent/server 7.9.0 → 7.11.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 (82) hide show
  1. package/USAGE.md +6 -0
  2. package/dist/adoption/plan.d.ts +152 -0
  3. package/dist/adoption/plan.js +513 -0
  4. package/dist/adoption/runner.d.ts +54 -0
  5. package/dist/adoption/runner.js +505 -0
  6. package/dist/adoption/sql.d.ts +76 -0
  7. package/dist/adoption/sql.js +106 -0
  8. package/dist/adoption/wire.d.ts +250 -0
  9. package/dist/adoption/wire.js +153 -0
  10. package/dist/approval-card.d.ts +24 -0
  11. package/dist/approval-card.js +32 -0
  12. package/dist/approval-reconciler.d.ts +1 -1
  13. package/dist/approval-reconciler.js +1 -1
  14. package/dist/boot/adoption.d.ts +30 -0
  15. package/dist/boot/adoption.js +57 -0
  16. package/dist/boot/coordinators.d.ts +4 -0
  17. package/dist/boot/coordinators.js +3 -1
  18. package/dist/boot/parked-revive-gate.d.ts +38 -5
  19. package/dist/boot/parked-revive-gate.js +53 -6
  20. package/dist/boot/reapers.js +3 -3
  21. package/dist/boot/runner-deps.d.ts +10 -2
  22. package/dist/boot/runner-deps.js +12 -1
  23. package/dist/capabilities/repo-tools.d.ts +36 -2
  24. package/dist/capabilities/repo-tools.js +125 -11
  25. package/dist/capabilities/scenarios.d.ts +2 -2
  26. package/dist/capabilities/scenarios.js +1 -1
  27. package/dist/config-types.d.ts +22 -2
  28. package/dist/config.js +44 -2
  29. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  30. package/dist/fleet/fleet-terminal-window.js +1 -1
  31. package/dist/git-api-kind.d.ts +7 -0
  32. package/dist/git-api-kind.js +8 -0
  33. package/dist/http/routes/adoption.d.ts +26 -0
  34. package/dist/http/routes/adoption.js +120 -0
  35. package/dist/http/routes/approvals-assistant.js +2 -2
  36. package/dist/http/routes/capabilities.js +12 -0
  37. package/dist/http/routes/rules.d.ts +23 -0
  38. package/dist/http/routes/rules.js +117 -0
  39. package/dist/http/routes/shared-memory.d.ts +31 -0
  40. package/dist/http/routes/shared-memory.js +181 -0
  41. package/dist/http/routes/trace-usage.js +139 -3
  42. package/dist/http/server.d.ts +19 -1
  43. package/dist/http/server.js +47 -5
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/main.js +54 -5
  47. package/dist/observability/fail-open.d.ts +8 -0
  48. package/dist/observability/fail-open.js +8 -0
  49. package/dist/plugins/adoption-log-sql.d.ts +191 -0
  50. package/dist/plugins/adoption-log-sql.js +273 -0
  51. package/dist/plugins/checkpoint-store-sql.d.ts +22 -9
  52. package/dist/plugins/checkpoint-store-sql.js +40 -29
  53. package/dist/plugins/local-checkpoint-store.d.ts +11 -1
  54. package/dist/plugins/local-checkpoint-store.js +10 -2
  55. package/dist/plugins/memory-engine-pg.js +3 -3
  56. package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
  57. package/dist/plugins/permission-rule-store-sql.js +817 -0
  58. package/dist/plugins/pg-pool.js +44 -7
  59. package/dist/plugins/run-store-sql.d.ts +3 -3
  60. package/dist/plugins/run-store-sql.js +3 -3
  61. package/dist/plugins/session-policy-store-sql.d.ts +6 -0
  62. package/dist/plugins/session-policy-store-sql.js +7 -1
  63. package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
  64. package/dist/plugins/shared-memory-store-sql.js +516 -0
  65. package/dist/plugins/store-backend.d.ts +30 -0
  66. package/dist/plugins/store-backend.js +14 -0
  67. package/dist/plugins/tidb-pool.js +55 -8
  68. package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
  69. package/dist/plugins/workflow-journal-store-sql.js +6 -6
  70. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  71. package/dist/plugins/workflow-run-store-sql.js +12 -12
  72. package/dist/rules-consent.d.ts +126 -0
  73. package/dist/rules-consent.js +198 -0
  74. package/dist/run-local.js +2 -2
  75. package/dist/shared-memory-scope-authorizer.d.ts +29 -0
  76. package/dist/shared-memory-scope-authorizer.js +17 -0
  77. package/dist/tool-approval.d.ts +55 -0
  78. package/dist/tool-approval.js +124 -5
  79. package/dist/trace/core-keyset-guard.d.ts +2 -2
  80. package/dist/trace/project.d.ts +1 -0
  81. package/dist/trace/project.js +1 -0
  82. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -16,7 +16,7 @@ import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadO
16
16
  import { createAuthorizer, encodeCheckpointScope } from "./security.js";
17
17
  import { assertGateIntentServiceable, hasOperatorGateIntent } from "./approval.js";
18
18
  import { loadSkills } from "./capabilities/skills.js";
19
- import { GiteaClient } from "./capabilities/repo-tools.js";
19
+ import { createRepoClient } from "./capabilities/repo-tools.js";
20
20
  import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
21
21
  import { createHandsLaneRegistry, pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
22
22
  import { createLogger } from "./observability/logger.js";
@@ -43,13 +43,16 @@ import { createResolveSpec } from "./boot/resolve-spec.js";
43
43
  import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
44
44
  import { startReapers } from "./boot/reapers.js";
45
45
  import { openStores } from "./boot/stores.js";
46
+ import { runAdoptionBootScan } from "./boot/adoption.js";
46
47
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
48
+ import { createRuleConsentLane } from "./rules-consent.js";
47
49
  import { createExecutionEnv } from "./boot/execution-env.js";
48
50
  import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
49
51
  import { createLiveCoordinators } from "./boot/coordinators.js";
50
52
  import { createRuntimeCaps } from "./boot/runtime-caps.js";
51
- import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js";
53
+ import { createRunnerDeps, createRunnerDepsOnAsk, createSharedRunnerDeps } from "./boot/runner-deps.js";
52
54
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
55
+ import { createSharedMemoryScopeAuthorizer } from "./shared-memory-scope-authorizer.js";
53
56
  import { createSessionFaces } from "./boot/session-faces.js";
54
57
  import { createLeaderFace } from "./boot/leader.js";
55
58
  import { assertStaticWiringConsistent } from "./http/routes/diagnostics.js";
@@ -168,9 +171,23 @@ async function main() {
168
171
  // design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
169
172
  // 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
170
173
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, } = await openStores({ config, logger, metrics, localRoot });
174
+ // design/183 I6(server 同族):**每副本 boot 必查**收编日志的在飞行 —— 续跑或响亮留痕,禁静默跳过。
175
+ // 位置:store 开完之后(要 backend)、任何路由装配之前(半迁移状态绝不带进服务期)。判据与「为什么
176
+ // 是续跑而不是拒启」见 boot/adoption.ts 顶注。
177
+ const adoptionBoot = await runAdoptionBootScan({ backend, logger });
178
+ if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
179
+ logger.info("adoption_boot_scan", { ...adoptionBoot });
180
+ }
171
181
  // design/158 A10:计费/追踪/预算装配搬到 src/boot/budget-tracing.ts(逐字;tracer 与 side-query 同 sink 实例的
172
182
  // 「同段构造」契约见该文件头注)。
173
183
  const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
184
+ // #154 车二:持久化权限规则店(core 5.18.0 design/179 + 5.22.0 design/182)。三面一束 —— 规则桶
185
+ // provider 上 `RunnerDeps.permissionRuleStore`(引擎据它铸 ruleSuggestions + 把 manifest 的
186
+ // `permissionRules.storeWired` 报成真),审批记录 + 导入票喂同意车道(HTTP 两口的属主)。
187
+ // 缺席(local 车道 / 无 backend)⇒ 诚实缺席:storeWired:false、帧上零候选、两口 501。
188
+ // 🔴 总开关在最前(codex round7 [high] 一):关 ⇒ 整条车道根本不装配(店/车道/帧键/两口一起消失)。
189
+ const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
190
+ const ruleConsent = permissionRuleStores ? createRuleConsentLane(permissionRuleStores) : undefined;
174
191
  // design/158 A10:执行环境装配搬到 src/boot/execution-env.ts(逐字)。
175
192
  // ⚠️ 工厂装饰顺序=行为(scratchpad → worktree → SendUserFile 登记 → 附件物化最外层),见该文件头注。
176
193
  const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
@@ -189,11 +206,25 @@ async function main() {
189
206
  const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
190
207
  // design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
191
208
  // 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
192
- const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
209
+ const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs, ruleConsent });
193
210
  // design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
194
211
  const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
195
212
  // design/170 件A(#148 件3③):org 记忆准入装配(目录源三态选择+C12 能力探测,坏配置在此拒启动)。
196
213
  const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
214
+ // design/177 —— org 共享记忆库(memory_list/memory_read + `/v1/shared-memory/*` 只读面)。
215
+ // 🔴 铸它的合取式是**唯一**的挂载条件,模型面与 HTTP 面共用:
216
+ // ① SQL 供给面在场(`backend.sharedMemoryStore` —— local 车道诚实缺席,理由在 store-backend.ts);
217
+ // ② org 折叠面在场(目录源)—— 没有成员性判据的共享读面只能全放或全拒,两个都比"这个面不存在"差。
218
+ // deployment-origin scope 只在**无租户边界**的部署里授予:多租户下一条部署级声明会同时授予每一个
219
+ // principal,那是跨租户读而不是配置便利(org-memory.ts 的 N2 同判)。择净在这一行,授权模块只忠实使用。
220
+ const sharedMemoryStore = orgMemoryAdmission.directory !== undefined
221
+ ? backend?.sharedMemoryStore?.(createSharedMemoryScopeAuthorizer({
222
+ directory: orgMemoryAdmission.directory,
223
+ deploymentScopes: config.requirePrincipal === true ? [] : orgMemoryAdmission.deploymentMemoryScopes,
224
+ }))
225
+ : undefined;
226
+ if (sharedMemoryStore)
227
+ logger.info("shared_memory_stores_enabled", { backend: backend?.kind });
197
228
  // 🔴 codex 复审(#196 finding-2b):`toolResultStore` 在**无 backend** 形下是 undefined,而 core 的
198
229
  // Runner 构造函数会在缺席时**每只各自私建**一份 `RunnerSharedToolResultStore`(runtask.js
199
230
  // `if (!this.deps.toolResultStore)`)。本部署有多只 Runner(主 / subRunner / hookAgent / #196 的两只无手
@@ -209,8 +240,10 @@ async function main() {
209
240
  config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
210
241
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
211
242
  memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
243
+ permissionRuleStore: permissionRuleStores?.provider,
212
244
  executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
213
245
  workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
246
+ sharedMemoryStores: sharedMemoryStore ? sharedMemoryStore : undefined,
214
247
  getRunStore: () => runStore,
215
248
  });
216
249
  const runner = new Runner(runnerDeps);
@@ -380,6 +413,7 @@ async function main() {
380
413
  sessionPolicyStore,
381
414
  usageWindowStore,
382
415
  orgMemoryAdmission,
416
+ sharedMemoryStores: sharedMemoryStore ? sharedMemoryStore : undefined, // design/177:与主 runner 同实例
383
417
  }),
384
418
  // ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
385
419
  sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
@@ -570,7 +604,7 @@ async function main() {
570
604
  // 解析搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:loadSkills 之后、buildScenarios 之前 ——
571
605
  // LKG 落盘点必须晚于 skill 正文装载(F7/codex R26),plugins 让位判据要求 plugins 晚于 center 直发 skills。
572
606
  skills = await configCenter.applyCenterCapabilities(skills);
573
- const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
607
+ const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
574
608
  // CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
575
609
  // undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
576
610
  const webSearchCfg = webSearchConfigFromEnv();
@@ -679,8 +713,14 @@ async function main() {
679
713
  // 「部署 ⊇ 操作员」两层完整链(design/181 件二收编;实现与全部理由在 boot/parked-revive-gate.ts,
680
714
  // 提出去的唯一理由是 main.ts 顶层 `void main()` 让那条腿的运行期语义在原地一格都钉不住)。
681
715
  // 构造条件逐字保持:裸 Agent 工具在场 ∧ 部署真开了 durable 审批。
716
+ // `approverSeat`:与 `RunnerDeps.onAsk` **同一个具名工厂**(单一属主,禁在此处手搓等价闭包)。core
717
+ // 5.22.0 起链条目的 `durableMandate` 位进了摘要,而该位的判据正是这只席位在不在场——两处若不同源,
718
+ // 赎回腿算出的摘要与 park 记的对不上,跨副本赎回整条腿被 pre-CAS 拒(见 parked-revive-gate.ts
719
+ // `mandatePostureOf`)。此处新铸的转发闭包与 runnerDeps 那只行为逐字相同(都只转 `toolApproval.ask`),
720
+ // 摘要只看在场性;仅 core 的 hook 席位去重按函数身份判,那一侧的身份未命中是**多筛一次**(core 自述
721
+ // 的保守方向),不是漏筛。
682
722
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
683
- ? createParkedReviveInheritedGate({ config, question, approvalExemptionStore, logger, localRoot })
723
+ ? createParkedReviveInheritedGate({ config, question, approvalExemptionStore, logger, localRoot, approverSeat: createRunnerDepsOnAsk(toolApproval) })
684
724
  : undefined;
685
725
  // 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
686
726
  // center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
@@ -782,11 +822,17 @@ async function main() {
782
822
  approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
783
823
  checkpointStore,
784
824
  sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
825
+ ruleConsent, // #154 车二:CC settings 导入两口的属主(缺席 ⇒ 两口 501)
785
826
  // E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
786
827
  // completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
787
828
  // applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
788
829
  // host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
789
830
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
831
+ // [3321] tool-results 读面(GET /v1/tasks/:id/tool-results/:ref)的数据源。传的是 **durable** 的那一只
832
+ // (`toolResultStore`,present ⇔ 有 store backend),**不是** Runner 侧的内存兜底 `runnerOffloadStore`:
833
+ // 无 backend 部署里那只是进程内的、跨副本读不到的,把它接上读面只会让调用方读到「有时有有时无」。
834
+ // undefined ⇒ 路由 404 同形(理由见 ServiceStoreDeps.toolResultStore 的头注)。
835
+ toolResultStore,
790
836
  taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
791
837
  // design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
792
838
  // scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
@@ -855,6 +901,9 @@ async function main() {
855
901
  // (`createOrgMemoryAdmissionWiring` 的产物),两面因此共享 TTL 缓存/退避窗/gen 高水位 —— 一个进程
856
902
  // 对「谁属于 org:acme」只有一个答案。缺席(无 center 且无 env 表)⇒ 策略面 `org:` 键仍 operator-only。
857
903
  orgMemoryDirectory: orgMemoryAdmission.directory,
904
+ // design/177:HTTP 只读面与模型面共用**同一个** provider 实例 —— 两面看到的库集合按定义一致,
905
+ // 不可能出现「壳能读到模型读不到的库」。缺席 ⇒ `/v1/shared-memory/*` 整域不挂载(见 server.ts 域头)。
906
+ sharedMemoryStore: sharedMemoryStore ? sharedMemoryStore : undefined,
858
907
  // sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
859
908
  // 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
860
909
  sessionMirrorRuling: principalCaps
@@ -41,6 +41,14 @@ export declare const FAIL_OPEN_TAGS: {
41
41
  readonly cls: "F";
42
42
  readonly note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。";
43
43
  };
44
+ readonly "server.rules.rejected-prepare-row-left": {
45
+ readonly cls: "F";
46
+ readonly note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。";
47
+ };
48
+ readonly "server.rules.ticket-claim-release-failed": {
49
+ readonly cls: "F";
50
+ readonly note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。";
51
+ };
44
52
  readonly "server.fleet.subscriber-callback-threw": {
45
53
  readonly cls: "F";
46
54
  readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
@@ -64,6 +64,14 @@ export const FAIL_OPEN_TAGS = {
64
64
  cls: "F",
65
65
  note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。",
66
66
  },
67
+ "server.rules.rejected-prepare-row-left": {
68
+ cls: "F",
69
+ note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。",
70
+ },
71
+ "server.rules.ticket-claim-release-failed": {
72
+ cls: "F",
73
+ note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。",
74
+ },
67
75
  "server.fleet.subscriber-callback-threw": {
68
76
  cls: "F",
69
77
  note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
@@ -0,0 +1,191 @@
1
+ /**
2
+ * design/183 I2(form b 位)—— **SQL 收编日志**:phase 真源与被迁的行**同库**。
3
+ *
4
+ * 🔴 为什么 phase 必须住 SQL 而不是一个 sidecar 文件(183 r3 按 codex r2-F2):发起收编的那个 pod 在两条
5
+ * 腿之间被摧毁时,进程本地的标记会与已经半迁移的 SQL 状态**分家** —— 接棒副本读不到「迁到哪了」。
6
+ * 标记与行同事务 ⇒ 崩溃后接棒副本按日志续跑,分家形不存在。
7
+ *
8
+ * ── 幂等身份 = DB 约束,不是「查了再插」(小票 v2/F2)────────────────────────────────────────────
9
+ * `from_principal` 上有 **UNIQUE**(183 D7「拒二次收编」的 DB 强形)。发起走 **insert-or-fetch 原子形**:
10
+ * 一条 `INSERT … ON CONFLICT DO NOTHING` / `INSERT IGNORE`,再按 `from_principal` 读回行。
11
+ * **禁 SELECT-then-INSERT** —— 那两句之间的窗正是两个并发同参 POST 各插一行(或各读到空)的地方,
12
+ * 而 UNIQUE 让「谁插进去了」由数据库仲裁,`affected` 就是判别式。
13
+ * 同 from 异 to ⇒ 读回的行 `to_principal` 不等 ⇒ typed conflict(调用方看到的是 409,不是「成功」)。
14
+ *
15
+ * ── 列的口径 ────────────────────────────────────────────────────────────────────────────────────
16
+ * · `legs` / `configs` / `report` 是 **TEXT 不是 JSON 列**:`report` 是 `immutableReport` 的落库形,
17
+ * 契约是「逐字节恒同」;JSON 列会按引擎自己的规范形重排键序,而 TEXT 是「存什么读什么」。
18
+ * (workflow_run.run / workflow_journal.result 同款先例,理由同源:String()→JSON.parse 的字节面。)
19
+ * · 时间列一律 `_ms BIGINT`(schema-naming 门 ② 咬 `_at BIGINT`)。
20
+ * · `phase` 是**单调**推进的阶段号,不是乐观锁计数 —— 但它的推进走的就是 CAS(`WHERE phase = :expect`),
21
+ * 所以它同时是那把锁。命名不叫 `rev`,因为它不守卫「行内容有没有变」,而守卫「弧走到哪了」。
22
+ */
23
+ import type { Pool as MySqlPool } from "mysql2/promise";
24
+ import type { Pool as PgPool } from "pg";
25
+ import { type SqlDriver, type SqlExec, type SqlTxConn } from "./sql-driver.js";
26
+ import type { AdoptionRejectCode } from "../adoption/wire.js";
27
+ export declare const ADOPTION_LOG_TABLE = "adoption_log";
28
+ /** 弧的阶段(183 §3.2 根级状态机的 form b 投影)。**单调**,只增不减。 */
29
+ export declare const ADOPTION_PHASE: {
30
+ /** ② 意图已落库(行在,尚未动任何数据行)。 */
31
+ readonly INTENT: 2;
32
+ /** ③ 身份轴重绑腿全部完成(与腿的 UPDATE **同一个事务**)。 */
33
+ readonly REBOUND: 3;
34
+ /** ④ 解析切换 / 配置清单产出。form b 的 server 半场:配置账已物化进 `configs`。 */
35
+ readonly CONFIGS: 4;
36
+ /** ⑤ 承运腿。**form b 恒零承运**(数据本来就在 SQL)—— 阶段位仍显式推进,好让 form a 上车时挂得上。 */
37
+ readonly CARRIED: 5;
38
+ /** ⑥ 永久终态。 */
39
+ readonly TERMINAL: 6;
40
+ };
41
+ export type AdoptionState = "in_flight" | "adopted" | "rejected";
42
+ export interface AdoptionRow {
43
+ adoptionId: string;
44
+ fromPrincipal: string;
45
+ toPrincipal: string;
46
+ phase: number;
47
+ state: AdoptionState;
48
+ /** `AdoptionLeg[]` 的 JSON 文本(phase 3 与腿同事务落库,此后不改)。 */
49
+ legsJson: string;
50
+ /** `AdoptionConfigEntry[]` 的 JSON 文本(phase 4 落库;见证位后续可更新)。 */
51
+ configsJson: string;
52
+ /** `AdoptionReport` 的 JSON 文本;`null` = 尚未终态。**落库后逐字节不变**(183 I4)。 */
53
+ reportJson: string | null;
54
+ rejectCode: string | null;
55
+ rejectDetail: string | null;
56
+ createdAtMs: number;
57
+ updatedAtMs: number;
58
+ }
59
+ export interface NewAdoptionRow {
60
+ adoptionId: string;
61
+ fromPrincipal: string;
62
+ toPrincipal: string;
63
+ nowMs: number;
64
+ }
65
+ /** `INSERT … ON CONFLICT DO NOTHING` 之后按 `from_principal` 读回的结果。`inserted` 只有真插进去的那一
66
+ * 路为 true —— 并发同参 POST 里恰有一条为 true,其余全 false 且拿到**同一行**。 */
67
+ export interface EnsureAdoptionResult {
68
+ row: AdoptionRow;
69
+ inserted: boolean;
70
+ }
71
+ /**
72
+ * 收编日志的**消费面接口**。真实现只有 {@link SqlAdoptionLogStore} 一个(双方言);抽出接口是为了让
73
+ * 协议层(`adoption/runner.ts`)只依赖行为而不依赖那个带 `protected db` 的类 —— 后者是**名义类型**,
74
+ * 测试想给一个结构等价的假店都做不到,于是「弧的状态机」与「SQL 文本」这两件事只能一起测,
75
+ * 两个变量同时动的实验没有判别力。
76
+ */
77
+ export interface AdoptionLogStore {
78
+ /** 池级只读执行面(**不在弧锁下**的读用它:锁外的现势查询、boot 扫描的名单)。 */
79
+ readonly reader: SqlExec;
80
+ ensureAdoption(row: NewAdoptionRow): Promise<EnsureAdoptionResult>;
81
+ /**
82
+ * 🔴 每个弧内方法都收一个 `exec`(codex R3-F2):弧锁**占着一条池连接**,如果弧内的读/事务再去池里
83
+ * 要第二条,`connectionLimit=1` 的部署会当场死锁(而 boot 的在飞扫描跑在 listen 之前 ⇒ 副本起不来)。
84
+ * 收编弧因此**全程单连接**:锁、preflight 读、腿的事务、phase CAS,全在 `withLock` 交出来的那一条上。
85
+ */
86
+ getById(adoptionId: string, exec?: SqlExec): Promise<AdoptionRow | null>;
87
+ getByFrom(fromPrincipal: string, exec?: SqlExec): Promise<AdoptionRow | null>;
88
+ listInFlight(exec?: SqlExec): Promise<AdoptionRow[]>;
89
+ /** 在**给定连接**上跑腿 + phase CAS(不自己 connect —— 见 getById 的注)。 */
90
+ rebindOn<L>(conn: SqlTxConn, adoptionId: string, expectPhase: number, nextPhase: number, nowMs: number, work: (exec: SqlExec) => Promise<L[]>, encodeLegs: (legs: L[]) => string): Promise<{
91
+ ok: true;
92
+ legs: L[];
93
+ } | {
94
+ ok: false;
95
+ reason: "phase_moved";
96
+ }>;
97
+ /** 终态后的迟到行清扫,同样在给定连接上。 */
98
+ sweepOn<T>(conn: SqlTxConn, work: (exec: SqlExec) => Promise<T>): Promise<T>;
99
+ advancePhase(adoptionId: string, expectPhase: number, nextPhase: number, nowMs: number, exec?: SqlExec): Promise<boolean>;
100
+ putConfigs(adoptionId: string, expectPhase: number, nextPhase: number, configsJson: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
101
+ finalizeAdopted(adoptionId: string, expectPhase: number, reportJson: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
102
+ finalizeRejected(adoptionId: string, code: AdoptionRejectCode, detail: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
103
+ /** 取弧锁并把**那一条连接**交给回调(整条弧都跑在它上面)。`undefined` = 没取到锁。 */
104
+ withLock<T>(name: string, fn: (conn: SqlTxConn) => Promise<T>, attempts?: number, sleepMs?: number): Promise<T | undefined>;
105
+ }
106
+ /** MySQL 协议方言的建表语句(展开进 tidb-pool 的中央 `SCHEMA_STATEMENTS`,与 approval-ask 同姿势:
107
+ * 跟着中央 ensureSchema 在 named-lock 的那条 conn 上建,不绕开 DDL 串行化)。 */
108
+ export declare const TIDB_ADOPTION_LOG_STATEMENTS: readonly string[];
109
+ /** PostgreSQL 孪生(由 `ensurePgSchema` 在 advisory lock 的那条 client 上调用)。 */
110
+ export declare function ensurePgAdoptionLogSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
111
+ /**
112
+ * 弧锁的名字。**定长**(前缀 14 + 32 位十六进制 = 46 字符)。
113
+ *
114
+ * 🔴 为什么必须 hash 而不是把 principal 直接拼进去(codex R3-F3,亲核属实):TiDB/MySQL 的 `GET_LOCK`
115
+ * 锁名上限是 **64 字符**,而请求面收的 principal 到 190 —— 超过 50 字符的源身份会让取锁**在弧动起来
116
+ * 之前**就失败,而那时意图行已经落库,于是每一次重发、每一次副本 boot 续跑都确定性地再撞一次同一堵墙。
117
+ * (PG 那条腿本来就走 hash 出来的 int4 对象键,这里只是把 MySQL 腿补齐到同一姿势。)
118
+ */
119
+ export declare function adoptionLockName(fromPrincipal: string): string;
120
+ /**
121
+ * 收编日志店(单文件双方言,SqlDriver 形——checkpoint-store / approval-ask-store 同款)。
122
+ *
123
+ * 每条语句的两方言文本**并排写在调用点**(A12 判据:方言差异必须显式,不许用占位符编号循环拼)。
124
+ */
125
+ export declare class SqlAdoptionLogStore implements AdoptionLogStore {
126
+ protected readonly db: SqlDriver;
127
+ constructor(db: SqlDriver);
128
+ private q;
129
+ /** 池级只读执行面(phase 0 的 preflight 扫描用——它**零写**,不需要也不该占一条事务连接)。
130
+ * 写路径一律走 {@link rebindTransaction};把驱动整个暴露出去会让调用方能绕开 phase CAS。 */
131
+ get reader(): SqlExec;
132
+ /**
133
+ * 发起腿的 **insert-or-fetch 原子形**。返回读回的行(可能是别人插的)+ 是否本次插入。
134
+ * 调用方据 `row.toPrincipal !== toPrincipal` 判「同 from 异 to」⇒ typed 409。
135
+ */
136
+ ensureAdoption(row: NewAdoptionRow): Promise<EnsureAdoptionResult>;
137
+ getById(adoptionId: string, exec?: SqlExec): Promise<AdoptionRow | null>;
138
+ getByFrom(fromPrincipal: string, exec?: SqlExec): Promise<AdoptionRow | null>;
139
+ /** I6 server 同族:每副本 boot 必查的在飞名单(**响亮**,不静默跳过)。 */
140
+ listInFlight(exec?: SqlExec): Promise<AdoptionRow[]>;
141
+ /**
142
+ * 迁移事务:腿的全部 UPDATE **与** phase CAS 落在**同一个事务**里。
143
+ *
144
+ * 🔴 为什么必须同事务(而不是「先迁完再推 phase」):两句分开就留下一个窗 —— 腿提交了、phase 没推,
145
+ * 崩溃后接棒副本重跑腿(幂等,零行变化)却把**首次的行数读数**丢了,回执里的 `legs` 会变成一串 0。
146
+ * 同事务之后这个窗根本不存在:要么「行迁了且 phase=3 且 legs 读数在」,要么什么都没发生。
147
+ * 任何异常(含唯一键违例 = 跨轴组合冲突)⇒ ROLLBACK ⇒ **源/目的地两侧字节零变更**。
148
+ */
149
+ rebindOn<L>(conn: SqlTxConn, adoptionId: string, expectPhase: number, nextPhase: number, nowMs: number, work: (exec: SqlExec) => Promise<L[]>, encodeLegs: (legs: L[]) => string): Promise<{
150
+ ok: true;
151
+ legs: L[];
152
+ } | {
153
+ ok: false;
154
+ reason: "phase_moved";
155
+ }>;
156
+ /**
157
+ * 终态**之后**的迟到行清扫事务(无 phase 变更 —— phase 已经是终态,而终态是永久的)。
158
+ *
159
+ * 🔴 它不是「再收编一次」:腿本身幂等,清扫只是把收编**结束后**又落到旧身份下的行(配置未随迁的
160
+ * 部署会持续制造它们)一并搬过去,并让 `current.residualSourceRows` 有个能归零的动作。
161
+ * `immutableReport` 一个字节都不动(史实与现势分家,183 §6)。
162
+ */
163
+ sweepOn<T>(conn: SqlTxConn, work: (exec: SqlExec) => Promise<T>): Promise<T>;
164
+ /** 单调 CAS 推进(无数据写的阶段;`false` = 别人已经推过了/相位不符 ⇒ 调用方重读行)。 */
165
+ advancePhase(adoptionId: string, expectPhase: number, nextPhase: number, nowMs: number, exec?: SqlExec): Promise<boolean>;
166
+ /** phase 4:配置账物化(与 phase 推进同一条语句,免得账落了相位没推)。 */
167
+ putConfigs(adoptionId: string, expectPhase: number, nextPhase: number, configsJson: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
168
+ /**
169
+ * ⑥ 终态:`report` 一次写入,`state='adopted'`。
170
+ * 🔴 `AND report IS NULL` 是 I4(永久终态 + 不可变史实)的**数据库强形**:即使调用序漂了,快照也
171
+ * 不可能被第二次写盖掉 —— 「逐字节恒同」于是不依赖调用方自觉。
172
+ */
173
+ finalizeAdopted(adoptionId: string, expectPhase: number, reportJson: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
174
+ /** 目的地冲突 ⇒ rejected 终态(源/目的地两侧字节零变更;phase 停在 INTENT)。 */
175
+ finalizeRejected(adoptionId: string, code: AdoptionRejectCode, detail: string, nowMs: number, exec?: SqlExec): Promise<boolean>;
176
+ /**
177
+ * 收编弧的 advisory 锁(183 I1 的 form b 形:「与任何活写互斥」在 SQL 侧 = 一次只有一个副本在推这条弧)。
178
+ *
179
+ * **非阻塞取 + 有界轮询**(tidb-pool 的 `acquireEnsureSchemaLock` 同款判据:阻塞式 GET_LOCK 会把并发
180
+ * 调用方全部park 在 TiDB 的悲观锁行上,既耗它的重试预算又饿死应用自己的 FOR UPDATE 路)。取不到 ⇒
181
+ * 返回 `undefined`,调用方按「在飞」应答 —— **不假装成功,也不无限等**。
182
+ */
183
+ withLock<T>(name: string, fn: (conn: SqlTxConn) => Promise<T>, attempts?: number, sleepMs?: number): Promise<T | undefined>;
184
+ }
185
+ export declare class TiDBAdoptionLogStore extends SqlAdoptionLogStore {
186
+ constructor(pool: MySqlPool);
187
+ }
188
+ export declare class PgAdoptionLogStore extends SqlAdoptionLogStore {
189
+ constructor(pool: PgPool);
190
+ }
191
+ //# sourceMappingURL=adoption-log-sql.d.ts.map