@sema-agent/server 7.108.0 → 7.110.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 (109) hide show
  1. package/MIGRATION.md +60 -1
  2. package/README.md +8 -0
  3. package/README.zh-CN.md +5 -0
  4. package/USAGE.md +19 -29
  5. package/dist/approval-ask-machine.d.ts +9 -25
  6. package/dist/approval-ask-machine.js +5 -2
  7. package/dist/approval-card.d.ts +33 -3
  8. package/dist/approval-card.js +26 -2
  9. package/dist/bench/s1/live-deps.d.ts +4 -5
  10. package/dist/bench/s1/live-deps.js +9 -10
  11. package/dist/boot/leader.js +0 -2
  12. package/dist/boot/reapers.d.ts +3 -0
  13. package/dist/boot/reapers.js +3 -2
  14. package/dist/boot/resolve-spec.d.ts +1 -1
  15. package/dist/boot/resolve-spec.js +6 -12
  16. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  17. package/dist/boot/stage-07-capability-layer.js +17 -3
  18. package/dist/boot/stage-08-reapers.d.ts +1 -0
  19. package/dist/boot/stage-08-reapers.js +2 -2
  20. package/dist/boot/stage-09-leader.d.ts +1 -0
  21. package/dist/boot/stage-10-http-server.d.ts +1 -0
  22. package/dist/boot/stage-10-http-server.js +2 -1
  23. package/dist/capabilities/scenarios.d.ts +3 -2
  24. package/dist/config-catalog.js +2 -4
  25. package/dist/config-provider.js +1 -0
  26. package/dist/config-types.d.ts +5 -11
  27. package/dist/config.js +21 -12
  28. package/dist/deployment-governance.d.ts +8 -2
  29. package/dist/deployment-governance.js +9 -8
  30. package/dist/hooks/hook-runner.d.ts +2 -2
  31. package/dist/hooks/hook-runner.js +40 -26
  32. package/dist/http/admission.js +8 -2
  33. package/dist/http/durable-run-leg.js +1 -1
  34. package/dist/http/resume-legs.js +6 -1
  35. package/dist/http/route-ctx.d.ts +3 -3
  36. package/dist/http/routes/capabilities.js +1 -0
  37. package/dist/http/routes/runs.d.ts +0 -1
  38. package/dist/http/routes/runs.js +22 -0
  39. package/dist/http/routes/sessions.js +12 -2
  40. package/dist/http/routes/tasks.js +5 -1
  41. package/dist/http/server.d.ts +7 -2
  42. package/dist/http/wire-types.d.ts +8 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -0
  45. package/dist/instruction-source-notice.d.ts +3 -3
  46. package/dist/leader/diffup.d.ts +53 -0
  47. package/dist/leader/diffup.js +95 -1
  48. package/dist/leader/leader.d.ts +0 -6
  49. package/dist/leader/leader.js +1 -1
  50. package/dist/leader/repair-wire.js +1 -1
  51. package/dist/leader/wire.d.ts +3 -10
  52. package/dist/leader/wire.js +39 -13
  53. package/dist/memory-layer-legacy-lock.d.ts +3 -3
  54. package/dist/model-entry-headers.d.ts +5 -2
  55. package/dist/model-entry-headers.js +4 -4
  56. package/dist/model-unreachable-notice.d.ts +2 -2
  57. package/dist/observability/corrupt-read-seat.d.ts +6 -0
  58. package/dist/observability/corrupt-read-seat.js +6 -0
  59. package/dist/observability/fail-open.d.ts +13 -5
  60. package/dist/observability/fail-open.js +13 -5
  61. package/dist/observability/metrics.js +2 -1
  62. package/dist/observability/run-terminal.js +10 -0
  63. package/dist/operator-ask.d.ts +9 -8
  64. package/dist/operator-ask.js +9 -1
  65. package/dist/plugins/approval-ask-store-file.d.ts +2 -1
  66. package/dist/plugins/approval-ask-store-file.js +3 -0
  67. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  68. package/dist/plugins/approval-ask-store-memory.js +7 -1
  69. package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
  70. package/dist/plugins/approval-ask-store-sql.js +7 -0
  71. package/dist/plugins/file-run-store.js +2 -2
  72. package/dist/plugins/memory-run-store.d.ts +1 -1
  73. package/dist/plugins/memory-run-store.js +2 -1
  74. package/dist/plugins/pg-pool.js +15 -0
  75. package/dist/plugins/run-store-sql.d.ts +1 -1
  76. package/dist/plugins/run-store-sql.js +3 -2
  77. package/dist/plugins/schema-indexes.js +2 -0
  78. package/dist/plugins/store-backend.d.ts +4 -0
  79. package/dist/plugins/store-backend.js +8 -0
  80. package/dist/plugins/store-contracts.d.ts +7 -0
  81. package/dist/plugins/store-contracts.js +1 -0
  82. package/dist/plugins/tidb-pool.js +17 -0
  83. package/dist/plugins/user-input-ledger-file.d.ts +33 -0
  84. package/dist/plugins/user-input-ledger-file.js +98 -0
  85. package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
  86. package/dist/plugins/user-input-ledger-sql.js +101 -0
  87. package/dist/request-key-closure.js +1 -1
  88. package/dist/run-local.d.ts +18 -5
  89. package/dist/run-local.js +26 -12
  90. package/dist/runs.d.ts +8 -1
  91. package/dist/runs.js +6 -2
  92. package/dist/runtime-governance.d.ts +23 -11
  93. package/dist/runtime-governance.js +7 -12
  94. package/dist/session-records-skipped-notice.d.ts +4 -3
  95. package/dist/task-settings.d.ts +0 -6
  96. package/dist/task-settings.js +0 -1
  97. package/dist/tool-approval.d.ts +44 -15
  98. package/dist/tool-approval.js +12 -8
  99. package/dist/trace/core-keyset-guard.d.ts +17 -2
  100. package/dist/trace/engine-notice-wire.d.ts +30 -35
  101. package/dist/trace/engine-notice-wire.js +23 -23
  102. package/dist/trace/project.js +2 -3
  103. package/dist/trace/self-minted-wire.d.ts +14 -0
  104. package/dist/trace/self-minted-wire.js +6 -0
  105. package/dist/unattended-permission-mode.d.ts +54 -93
  106. package/dist/unattended-permission-mode.js +8 -61
  107. package/dist/user-input-ledger.d.ts +296 -0
  108. package/dist/user-input-ledger.js +337 -0
  109. package/package.json +4 -4
@@ -201,7 +201,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
201
201
  }
202
202
  deps.logger?.info?.("module_repair", { workerId: r.workerId, attempt: attempt + 1, reason: redactHead(verdict.reason ?? "", 500) });
203
203
  try {
204
- const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` }, deps.unattendedLeg)).result();
204
+ const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` })).result();
205
205
  if (r.stats && rr?.stats)
206
206
  r.stats = { ...r.stats, tokens: (r.stats.tokens ?? 0) + (rr.stats.tokens ?? 0), costMicroUsd: (r.stats.costMicroUsd ?? 0) + (rr.stats.costMicroUsd ?? 0) };
207
207
  }
@@ -87,7 +87,7 @@ export function attachRepairLoopDeps(deps, resolve) {
87
87
  deps.repairLoop = {
88
88
  run: async (solo, sub) => {
89
89
  const cfg = await resolve(solo, sub);
90
- const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId }, deps.unattendedLeg);
90
+ const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId });
91
91
  return runRepairLoopForLeader(solo, implSpec, cfg);
92
92
  },
93
93
  };
@@ -1,4 +1,4 @@
1
- import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps, type PermissionMode } from "@sema-agent/core";
1
+ import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core";
2
2
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
3
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
@@ -19,14 +19,6 @@ export interface LeaderWireConfig {
19
19
  * REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory.
20
20
  * Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */
21
21
  envFactory?: ExecutionEnvFactory;
22
- /**
23
- * S-877 候裁 15 → B —— 本车道**有手**的每一条 run(worker 两车道、集成沙箱修复、模块门修复、单代理修复环)的
24
- * `TaskSpec.permissionMode`。**必填**:缺席 = core「Absent = `default`」⇒ 写与非只读命令在无人可答的这条腿上被
25
- * headless 当场拒(codex F1 的病);必填让每一个构造本配置的地方都得表态,不许静默落回那一形。
26
- * 唯一取值点 = `boot/leader.ts` 调 `unattendedPermissionMode(config)`(`src/unattended-permission-mode.ts`,与 run-local 同一只);
27
- * 本文件经 `createUnattendedLeg` / `withUnattendedLeg` 铸并(模式 + 写面 roots + 候裁 18 的写保护拒层)。planner 路由无手,不读。
28
- */
29
- unattendedPermissionMode: PermissionMode;
30
22
  /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when the seat yields a value, that worker task carries
31
23
  * the durable question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker
32
24
  * (checkpoint + paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's
@@ -165,7 +157,8 @@ export interface LeaderWireConfig {
165
157
  /** The env surface wire needs (both the E2B and k8s adapters implement core's RemoteExecutionEnv). */
166
158
  type WireEnv = Pick<RemoteExecutionEnv, "exec" | "writeFile">;
167
159
  /**
168
- * [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader 各一只)。
160
+ * [ref] —— leader 自铸沙盒的**回收闸**(集成沙盒 / repair-loop grader 各一只;判据本身 S-949 起抽成下方
161
+ * {@link assertLeaderOwnedEnvReapable},worker 腿同判)。
169
162
  *
170
163
  * core 契约:`ExecutionEnv` 基面**没有** `destroy`;只有实现了 `RemoteExecutionEnv` 的适配器才有
171
164
  * (`core/remote-env.d.ts`),而「这只 env 到底有没有」是**运行期结构事实**,core 自己也只用结构探测
@@ -5,13 +5,13 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
- import { createUnattendedLeg, withUnattendedLeg } from "../unattended-permission-mode.js";
8
+ import { withUnattendedLeg } from "../unattended-permission-mode.js";
9
9
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
10
10
  import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
11
11
  import { createDurableAskPolicy } from "../approval.js";
12
12
  import { gitSubprocessEnv } from "../server-secret-env.js";
13
13
  import { presignS3Url } from "../plugins/s3-presign.js";
14
- import { buildUploadScript, uploadStepSuffix, fetchUploadedDiff, withStaging } from "./diffup.js";
14
+ import { buildUploadScript, buildUploadLoopScript, uploadLoopCommand, uploadLoopScriptPath, uploadStepSuffix, fetchUploadedDiff, withStaging, UPLOAD_LOOP_PERIOD_SEC } from "./diffup.js";
15
15
  import { pullDiff, safeRev } from "./diffout.js";
16
16
  import { runLeaderTask } from "./leader.js";
17
17
  import { attachRepairLoopDeps } from "./repair-wire.js";
@@ -37,10 +37,13 @@ const asIntegrationEnv = (env) => ({
37
37
  throw r.error; },
38
38
  });
39
39
  export function createEnvReaper(env, what) {
40
+ assertLeaderOwnedEnvReapable(env, what);
41
+ return async () => void (await env.destroy());
42
+ }
43
+ function assertLeaderOwnedEnvReapable(env, what) {
40
44
  if (!hasDestroy(env)) {
41
- throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and this closure is its only teardown, so it would leak (and keep billing) until the backend's own timeout reaps it. Deploy a factory whose env implements RemoteExecutionEnv.destroy for the leader lane.`);
45
+ throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and destroy() is its only teardown (the leader's own reaper for the integration / grader sandboxes; core's run teardown for a worker, where the staged incremental-upload loop and the belt upload ride it), so without it the sandbox and whatever the leader started inside it leak until the backend's own timeout reaps them — and a host-shell env has no such timeout. Deploy a factory whose env implements RemoteExecutionEnv.destroy for the leader lane.`);
42
46
  }
43
- return async () => void (await env.destroy());
44
47
  }
45
48
  export async function stageWorkerEnv(env, opts) {
46
49
  const { seedCmd, repo, branch, uploadScriptPath, uploadScript } = opts;
@@ -133,13 +136,32 @@ export function createCoordinatorPush(durableRemote, targetRef, ident) {
133
136
  export function createLeaderRunner(cfg) {
134
137
  const W = cfg.workspace ?? "/home/user";
135
138
  const repo = `${W}/repo`;
139
+ const uploadLoopScript = buildUploadLoopScript({ workDir: W, periodSec: UPLOAD_LOOP_PERIOD_SEC });
140
+ const startUploadLoop = async (env, workerId) => {
141
+ try {
142
+ const wr = await env.writeFile(uploadLoopScriptPath(W), uploadLoopScript);
143
+ if (!wr.ok)
144
+ throw wr.error;
145
+ await sh(env)(uploadLoopCommand(W, "start"));
146
+ }
147
+ catch (e) {
148
+ cfg.logger?.warn?.("leader_upload_loop_start_failed", { workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
149
+ }
150
+ };
151
+ const stopUploadLoop = async (env, workerId) => {
152
+ try {
153
+ await sh(env)(uploadLoopCommand(W, "stop"));
154
+ }
155
+ catch (e) {
156
+ cfg.logger?.warn?.("leader_upload_loop_stop_failed", { workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
157
+ }
158
+ };
136
159
  const ident = cfg.git ?? { name: "leader", email: "leader@local" };
137
160
  const { leaderTimeoutMs, workerLimits, presignTtlSec, resourceSuspend: resourceCfg } = leaderResourceConfig();
138
161
  const { repairRounds, repairBudgetUsd, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK, replanBudgetUsd, } = leaderLoopConfig({ repairRounds: cfg.repairRounds });
139
162
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
140
163
  const governanceSeat = cfg.governance ?? {};
141
164
  const deploymentPostureSeat = () => cfg.deploymentPosture?.() ?? {};
142
- const unattendedLeg = () => createUnattendedLeg(cfg.unattendedPermissionMode, deploymentPostureSeat());
143
165
  const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
144
166
  const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
145
167
  const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
@@ -176,7 +198,7 @@ export function createLeaderRunner(cfg) {
176
198
  ...principalSpec(),
177
199
  sessionId: `leader-repair-${Date.now()}-${round}`,
178
200
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
179
- }, unattendedLeg()))
201
+ }))
180
202
  .result();
181
203
  if (res.terminal.kind !== "completed")
182
204
  throw new Error(`repair round ${round} did not complete (status=${outwardPlane(res.terminal).status})`);
@@ -311,7 +333,7 @@ export function createLeaderRunner(cfg) {
311
333
  const script = buildUploadScript({ repoDir: repo, baseSha, putUrl, workDir: W });
312
334
  const stage = async (env) => {
313
335
  await stageWorkerEnv(env, { seedCmd, repo, branch: sub.branch, uploadScriptPath: `${W}/.leader-upload.sh`, uploadScript: script });
314
- void sh(env)(`nohup sh -c 'while true; do sleep 90; sh ${W}/.leader-upload.sh >/dev/null 2>&1 || true; done' </dev/null >/dev/null 2>&1 &`).catch(() => { });
336
+ await startUploadLoop(env, sub.workerId);
315
337
  };
316
338
  return {
317
339
  workerId: sub.workerId, sessionId, branch: sub.branch,
@@ -320,12 +342,17 @@ export function createLeaderRunner(cfg) {
320
342
  brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
321
343
  ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
322
344
  ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
323
- executionEnvFactory: async (ctx) => withStaging(await envFactory(ctx), stage, async (e) => {
324
- await sh(e)(`cd ${repo} && git add -A && git -c user.name='leader-belt' -c user.email='belt@leader' commit -qm 'belt: auto-commit uncommitted worker output' || true; sh ${W}/.leader-upload.sh`);
325
- }),
345
+ executionEnvFactory: async (ctx) => {
346
+ const env = await envFactory(ctx);
347
+ assertLeaderOwnedEnvReapable(env, "leader worker sandbox");
348
+ return withStaging(env, stage, async (e) => {
349
+ await stopUploadLoop(e, sub.workerId);
350
+ await sh(e)(`cd ${repo} && git add -A && git -c user.name='leader-belt' -c user.email='belt@leader' commit -qm 'belt: auto-commit uncommitted worker output' || true; sh ${W}/.leader-upload.sh`);
351
+ });
352
+ },
326
353
  })),
327
354
  fetchDiff: () => fetchUploadedDiff(getUrl),
328
- spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }, unattendedLeg()),
355
+ spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }),
329
356
  };
330
357
  }
331
358
  if (!cfg.e2bApiKey)
@@ -340,7 +367,7 @@ export function createLeaderRunner(cfg) {
340
367
  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 })),
341
368
  diffEnv: env,
342
369
  destroy: () => env.destroy().then(() => { }),
343
- spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }, unattendedLeg()),
370
+ spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }),
344
371
  };
345
372
  };
346
373
  const provisionIntegrationSandbox = async () => {
@@ -382,7 +409,6 @@ export function createLeaderRunner(cfg) {
382
409
  const deps = {
383
410
  plan,
384
411
  provisionWorker,
385
- unattendedLeg: unattendedLeg(),
386
412
  provisionIntegrationSandbox,
387
413
  push,
388
414
  testCmd: body.testCmd,
@@ -1,6 +1,6 @@
1
- import type { EngineNotice } from "@sema-agent/core";
2
- import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
3
- export declare const MEMORY_LAYER_LOCKED_LEGACY: ServerNoticeCode;
1
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
2
+ /** 码常量型 = core 码册(#1240 入册:受众 operator、铸者 `NOTICE_MINTER` = `"server"`;不上会话流,只落转发体的运维日志)。 */
3
+ export declare const MEMORY_LAYER_LOCKED_LEGACY: EngineNoticeCode;
4
4
  /** 探测读数:锁住的层名(排序)+ 看不全的目录条数。挂载面不存在(新数据根)⇒ 两者皆空。 */
5
5
  export interface LockedLegacyLayerProbe {
6
6
  readonly layers: readonly string[];
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * 白名单 = 名字 → 读它的线路(闭集;**加一个名字 = 加一行并写明哪条线路读它、为什么是部署的事**)。
3
3
  * `anthropic-beta`:Anthropic Messages 的 beta 开关(逗号分隔的 beta 名;core anthropic 腿会把自己的 beta 并进同一个头)。
4
+ *
5
+ * S-963:本表与 {@link CATALOG_ENTRY_HEADER_NAMES} / {@link CatalogEntryHeaderName} 从包根 `src/index.ts` 原对象导出(web-admin 目录编辑页构建期取白名单,
6
+ * 不手抄;`RETIRED_ENVS` 同族)—— 它是**本进程准入判据的活表**,所以三只(表 / 每行线路数组 / 名字数组)在这里一次冻结:导出面上的嵌入方改不了它。
4
7
  */
5
- export declare const CATALOG_ENTRY_HEADER_LANES: {
8
+ export declare const CATALOG_ENTRY_HEADER_LANES: Readonly<{
6
9
  readonly "anthropic-beta": readonly ["anthropic-messages"];
7
- };
10
+ }>;
8
11
  export type CatalogEntryHeaderName = keyof typeof CATALOG_ENTRY_HEADER_LANES;
9
12
  export declare const CATALOG_ENTRY_HEADER_NAMES: readonly CatalogEntryHeaderName[];
10
13
  /** 判过的袋(只含白名单里的名字,值逐字)。 */
@@ -1,8 +1,8 @@
1
1
  import { redactSecrets } from "./trace/redact.js";
2
- export const CATALOG_ENTRY_HEADER_LANES = {
3
- "anthropic-beta": ["anthropic-messages"],
4
- };
5
- export const CATALOG_ENTRY_HEADER_NAMES = Object.keys(CATALOG_ENTRY_HEADER_LANES);
2
+ export const CATALOG_ENTRY_HEADER_LANES = Object.freeze({
3
+ "anthropic-beta": Object.freeze(["anthropic-messages"]),
4
+ });
5
+ export const CATALOG_ENTRY_HEADER_NAMES = Object.freeze(Object.keys(CATALOG_ENTRY_HEADER_LANES));
6
6
  export const MAX_HEADER_VALUE_CHARS = 1024;
7
7
  const HEADER_VALUE_SHAPE = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/;
8
8
  const HEADER_NAME_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
@@ -5,8 +5,8 @@
5
5
  * ## 病
6
6
  * 运营用户看不到日志 / 诊断面:VPN 中途切换之后模型网关建连超时,用户只拿到一句 undici 原句(`fetch failed (connect …)`),
7
7
  * 分不清是网络断了还是起服时读进的代理已经失效,也没有「查 VPN / 代理、代理变过就重启」这条出路。只落运维日志不算闭环
8
- * (test 立案原话)。core 没有现成的「模型不可达」用户通告码(`ENGINE_NOTICE_CODES` 亲查零命中)⇒ 本仓自铸,走既有的
9
- * `SERVER_NOTICE_AUDIENCE` 族(S-690 形:白名单 / 归属 / 消毒 / 隔离四件判据一个不绕),不开第二条通道。
8
+ * (test 立案原话)。core 没有现成的「模型不可达」用户通告码 ⇒ 本仓铸,走既有的本仓铸码族(S-690 形:白名单 / 归属 / 消毒 / 隔离四件判据
9
+ * 一个不绕),不开第二条通道。core 7.38.0(#1240)起本码在 core 码册里:受众 `NOTICE_AUDIENCE` = user、铸者 `NOTICE_MINTER` = `"server"`。
10
10
  *
11
11
  * ## 判据(一句):**这一次模型调用的终局是 `stopReason: "error"`,且它发出的最后一条请求在建连时就失败了**
12
12
  * 「最后一条请求落在哪」由出网观察者记在调用作用域上(`model-reach-scope.ts`,undici 诊断通道,见 `outbound-dispatcher.ts`)——
@@ -150,6 +150,12 @@ export declare const JSONL_REPLAY_SEATS: {
150
150
  readonly tag: "server.send-file-ledger.corrupt-record-skipped";
151
151
  readonly note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).";
152
152
  };
153
+ readonly "user-input-ledger": {
154
+ readonly direction: "fail-open";
155
+ readonly event: "user_input_ledger_corrupt_read";
156
+ readonly tag: "server.user-input-ledger.corrupt-record-skipped";
157
+ readonly note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.";
158
+ };
153
159
  };
154
160
  /** 登记名(闭集)。 */
155
161
  export type JsonlReplaySeat = keyof typeof JSONL_REPLAY_SEATS;
@@ -93,6 +93,12 @@ export const JSONL_REPLAY_SEATS = {
93
93
  tag: "server.send-file-ledger.corrupt-record-skipped",
94
94
  note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).",
95
95
  },
96
+ "user-input-ledger": {
97
+ direction: "fail-open",
98
+ event: "user_input_ledger_corrupt_read",
99
+ tag: "server.user-input-ledger.corrupt-record-skipped",
100
+ note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.",
101
+ },
96
102
  };
97
103
  function corruptRefusalSentence(seat, path, reason) {
98
104
  const entry = JSONL_REPLAY_SEATS[seat];
@@ -25,10 +25,6 @@ export declare const FAIL_OPEN_TAGS: {
25
25
  readonly cls: "F";
26
26
  readonly note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。";
27
27
  };
28
- readonly "server.unattended-leg.mode-releases-protective-asks": {
29
- readonly cls: "P-DEBT";
30
- readonly note: "S-877 四续 ⑧(候裁 18 D 的已披露残余,`unattended-permission-mode.ts` 的 `createUnattendedLeg`):无人值守腿(leader worker / 修复腿 / run-local / 续跑重挂)的旋钮 `UNATTENDED_PERMISSION_MODE` 缺省 `bypassPermissions` 释放六类保护问(`probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` / `artifact_shared_read`;写保护一类已改拒)—— 7.105.0 这些腿上它们照问、无人答即拒。放行的最坏后果=无人值守的写 / 读 / MCP·A2A 标记工具调用不再被这几道保护问拦下,`rule_unreadable` 是「规则店读不出照放」(安全轴 fail-open 方向)。core 对这几类没有拒座;S-906(core 7.37.0 按类作答)到货后本臂与旋钮一并硬删。计数 = 铸出的这类腿条数,detail = `mode=<词> released=<类…>`。";
31
- };
32
28
  readonly "server.workflow-park-index.sync-failed": {
33
29
  readonly cls: "P-DEBT";
34
30
  readonly note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。";
@@ -295,7 +291,7 @@ export declare const FAIL_OPEN_TAGS: {
295
291
  };
296
292
  readonly "server.approval-card.closed-word-out-of-set": {
297
293
  readonly cls: "F";
298
- readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
294
+ readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
299
295
  };
300
296
  readonly "server.pg-pool.idle-client-error": {
301
297
  readonly cls: "F";
@@ -321,6 +317,18 @@ export declare const FAIL_OPEN_TAGS: {
321
317
  readonly cls: "F";
322
318
  readonly note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。";
323
319
  };
320
+ readonly "server.user-input-ledger.append-failed": {
321
+ readonly cls: "F";
322
+ readonly note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 进程之后**若再崩**(local 车道下一次 boot),它会被当成崩溃丢失告知一次(一句多余的「你的 steer 丢了」,而它其实被正常收场了;SQL 车道的收敛判据只认 run 店已落盘的 reap 判决 —— `failed` + `RUN_STALLED_ERROR` ⇒ crashed、仍 `running` ⇒ live、正常翻面 ⇒ leg_over 补结清 —— 正常收场的 run 不会被误收);converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。";
323
+ };
324
+ readonly "server.user-input-ledger.read-failed": {
325
+ readonly cls: "F";
326
+ readonly note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(SQL 臂判词要读 run 店,run 店读失败 ⇒ 该行留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。";
327
+ };
328
+ readonly "server.user-input-ledger.corrupt-record-skipped": {
329
+ readonly cls: "F";
330
+ readonly note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 一次正常收场的受理被当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。";
331
+ };
324
332
  readonly "server.outbound.connect-observer-threw": {
325
333
  readonly cls: "F";
326
334
  readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";
@@ -16,10 +16,6 @@ export const FAIL_OPEN_TAGS = {
16
16
  cls: "F",
17
17
  note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。",
18
18
  },
19
- "server.unattended-leg.mode-releases-protective-asks": {
20
- cls: "P-DEBT",
21
- note: "S-877 四续 ⑧(候裁 18 D 的已披露残余,`unattended-permission-mode.ts` 的 `createUnattendedLeg`):无人值守腿(leader worker / 修复腿 / run-local / 续跑重挂)的旋钮 `UNATTENDED_PERMISSION_MODE` 缺省 `bypassPermissions` 释放六类保护问(`probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` / `artifact_shared_read`;写保护一类已改拒)—— 7.105.0 这些腿上它们照问、无人答即拒。放行的最坏后果=无人值守的写 / 读 / MCP·A2A 标记工具调用不再被这几道保护问拦下,`rule_unreadable` 是「规则店读不出照放」(安全轴 fail-open 方向)。core 对这几类没有拒座;S-906(core 7.37.0 按类作答)到货后本臂与旋钮一并硬删。计数 = 铸出的这类腿条数,detail = `mode=<词> released=<类…>`。",
22
- },
23
19
  "server.workflow-park-index.sync-failed": {
24
20
  cls: "P-DEBT",
25
21
  note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。",
@@ -268,7 +264,7 @@ export const FAIL_OPEN_TAGS = {
268
264
  "server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
269
265
  "server.approval-card.closed-word-out-of-set": {
270
266
  cls: "F",
271
- note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
267
+ note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
272
268
  },
273
269
  "server.pg-pool.idle-client-error": {
274
270
  cls: "F",
@@ -294,6 +290,18 @@ export const FAIL_OPEN_TAGS = {
294
290
  cls: "F",
295
291
  note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。",
296
292
  },
293
+ "server.user-input-ledger.append-failed": {
294
+ cls: "F",
295
+ note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 进程之后**若再崩**(local 车道下一次 boot),它会被当成崩溃丢失告知一次(一句多余的「你的 steer 丢了」,而它其实被正常收场了;SQL 车道的收敛判据只认 run 店已落盘的 reap 判决 —— `failed` + `RUN_STALLED_ERROR` ⇒ crashed、仍 `running` ⇒ live、正常翻面 ⇒ leg_over 补结清 —— 正常收场的 run 不会被误收);converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。",
296
+ },
297
+ "server.user-input-ledger.read-failed": {
298
+ cls: "F",
299
+ note: "S-928(`src/user-input-ledger.ts` / `http/routes/sessions.ts`):一次账本**读**失败 —— `op=converge`(收敛扫描读不出 open 受理 ⇒ 本轮不收敛,下一个 tick 再来)/ `op=judge`(SQL 臂判词要读 run 店,run 店读失败 ⇒ 该行留在 open、下一次扫描再判)/ `op=announce`(新 run 首拍读不出本会话的待告知受理 ⇒ 这一拍不告知,run 照跑,下一条 run 再试)/ `op=session-view`(`GET /v1/sessions/:id` 的 additive 键 `userInputUndrained` 读不出 ⇒ 键缺席、会话视图照常 200)。放行的最坏后果 = 一次「你的输入因崩溃丢了」的告知被推迟,或会话视图上这一格读作零条;不阻断任何 run、不改任何回执。反方向(读失败就拒跑 run / 整只会话视图 500)是拿一个告知面的抖动去打掉主路径,方向更坏。",
300
+ },
301
+ "server.user-input-ledger.corrupt-record-skipped": {
302
+ cls: "F",
303
+ note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 一次正常收场的受理被当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。",
304
+ },
297
305
  "server.outbound.connect-observer-threw": {
298
306
  cls: "F",
299
307
  note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。",
@@ -215,7 +215,7 @@ export function createMetrics() {
215
215
  m.counter("http_requests_total", "HTTP requests by route/method/status");
216
216
  m.histogram("http_request_duration_seconds", "HTTP request duration");
217
217
  m.counter("rate_limited_total", "Requests rejected by the rate limiter");
218
- m.counter("brain_echo_dropped_total", "Provider echo fields (response request-id header / reported model name) the engine's brain dropped at write time for failing the echo shape (visible ASCII, 1-128 chars), by field (requestId|responseModel); non-zero = the endpoint or a gateway in front of it echoes out-of-shape bytes");
218
+ m.counter("brain_echo_dropped_total", "Provider echo fields (response request-id header / reported model name) the engine's brain dropped at write time for failing the echo shape (visible ASCII, 1-128 chars), by field (requestId|responseModel|apiErrorCode — the last is the provider's own refusal word on a 4xx, core 7.38.0); non-zero = the endpoint or a gateway in front of it echoes out-of-shape bytes");
219
219
  m.counter("mcp_probe_total", `No-run MCP status face outcomes by outcome (${MCP_PROBE_OUTCOMES.join("/")} — see src/capabilities/mcp-probe.ts MCP_PROBE_OUTCOMES; rate_limited has TWO causes: the per-identity fixed window AND the replica-wide in-flight cap, which can refuse caller B because of caller A)`);
220
220
  m.counter("tasks_total", "Finished tasks by terminal status");
221
221
  m.counter("task_tokens_total", "Total model tokens consumed by tasks");
@@ -265,6 +265,7 @@ export function createMetrics() {
265
265
  m.counter("images_omitted_total", "Tasks whose images will be degraded to text placeholders (no-vision model), by model (S6)");
266
266
  m.counter("runs_reaped_total", "Run rows flipped by the background reapers (S7), by kind (stale/suspended/expired_checkpoint)");
267
267
  m.counter("checkpoints_reaped_total", "Durable checkpoints expired by the backstop reaper (S7)");
268
+ m.counter("user_input_converged_total", "User-input acceptances (S-928 ledger) converged to crashed by the maintenance tick (the boot pass reports its count on the user_input_ledger_crash_converged warn line instead): steers accepted whose writer process ended before the run settled");
268
269
  m.counter("bakes_reaped_total", "Image bakes failed by the stale-lease reaper (S7)");
269
270
  m.counter("counter_flush_failed_total", "Write-behind counter flush/refresh failures (S9), by table and kind");
270
271
  m.gauge("counter_flush_fail_streak", "Current consecutive write-behind flush/refresh failures (S9), by table and kind");
@@ -176,6 +176,11 @@ const STRANDED_ANSWER_TREATMENT = {
176
176
  deliveryId: "verbatim",
177
177
  toolCallId: "provider-join-key",
178
178
  };
179
+ const TERMINAL_FAILURE_DETAIL_TREATMENT = {
180
+ kind: "verbatim",
181
+ status: "verbatim",
182
+ code: "provider-echo",
183
+ };
179
184
  const PROVIDER_ECHO_SHAPERS = {
180
185
  responseModel: (v) => (admitProviderEcho(v, "responseModel") ? v : undefined),
181
186
  apiFailure: (v) => shapeEchoMembers(v, API_FAILURE_TREATMENT, "apiFailure"),
@@ -238,6 +243,11 @@ export function redactTerminalResult(result) {
238
243
  const g = gateOf(np);
239
244
  if (g?.changed === true)
240
245
  nt = { ...base, nestedPause: { ...np, gate: g.gate } };
246
+ const cur = nt ?? base;
247
+ const detail = cur.detail;
248
+ const shaped = shapeEchoMembers(detail, TERMINAL_FAILURE_DETAIL_TREATMENT, "terminal.detail");
249
+ if (shaped !== detail)
250
+ nt = { ...cur, detail: shaped };
241
251
  }
242
252
  if (nt !== undefined) {
243
253
  out.terminal = nt;
@@ -33,7 +33,7 @@
33
33
  * (`dist/core/gate-fold.js`),那是 `allowLayerMayClear` 的另一条独立豁免;本仓再盖一次是第二个写者。
34
34
  * · **问答工具**(`deployment-governance.ts` 的 `AskUserQuestion`)—— core 的谓词把保留问答工具单列豁免。
35
35
  */
36
- import type { PermissionResult } from "@sema-agent/core";
36
+ import { type PermissionResult, type ToolPolicy } from "@sema-agent/core";
37
37
  /** 本文件两只铸点的返回形 = core `PermissionResult` 的 ask 臂(窄型:`authoredBy` 只长在这一臂上)。 */
38
38
  export type OperatorAskResult = Extract<PermissionResult, {
39
39
  action: "ask";
@@ -63,12 +63,13 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
63
63
  */
64
64
  export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
65
65
  /**
66
- * core 部署出身位 `authoredBy:"deployment"` 的**唯一**盖位函数(S-897 起收成一处):给一只**部署自己的声明**产的 ask 盖上出身 ⇒
67
- * core 类读 `operator_forced`、`mandated`、不铸 `mandate` 词(表外的门;缺词 = 另有门在)⇒ bypassPermissions 放不掉、站着的答案
68
- * 退不掉、壳也不会按单个强制词替人答。三个调用者:{@link operatorAlwaysHumanAsk}(`APPROVAL_NEVER_AUTO` / 预算耗尽)、
69
- * 治理层包装(`runtime-governance.ts` 的 `createGovernanceAskMarkingPolicy`)、SUP 路由姿态(`router/route-orchestration.ts`,
70
- * 经 {@link operatorAlwaysHumanAsk})。修前前两处各写一份 `{ ...d, authoredBy: "deployment" }`(两个写者),S-897 若再抄就是第三份。
71
- * 只作用于 ask 臂(类型上就只收 ask);其余键逐字透传。
66
+ * 🔴 **S-958 —— 治理层的叶策略包装**:`leaf` 产的每一只 ask 盖部署出身(经 {@link withDeploymentAuthorship}),allow / deny 原物交回;
67
+ * 成员(`projection` / `pathDenyRules` / `writeTargetGuard` / `nameSets`)经 core `wrapToolPolicy` 随行(契约「包装至少拒内层所拒」:
68
+ * 只加一位、从不放宽)。同步叶保持同步返回;叶抛照抛(fail-closed 的属主仍是叶)。
69
+ *
70
+ * **只收叶**:治理层(`runtime-governance.ts` `applyRuntimeGovernance`)对每一片叶各盖一次,再交 core `combinePolicies` 折**一次**
71
+ * (折叠是最外层,其输出原样交回 —— core 把落选 ask 的类(含本位带出的 `operator_forced`)写进 `foldedAskClasses` 并担保那只对象)。
72
+ * 把一只折叠交给本函数 = 在折叠的输出上展开 —— 那正是 S-958 修掉的病(7.39.0 起按名拒,响亮、fail-closed,不是静默)。
72
73
  */
73
- export declare function withDeploymentAuthorship<A extends OperatorAskResult>(ask: A): A;
74
+ export declare function createDeploymentAuthoredPolicy(leaf: ToolPolicy): ToolPolicy;
74
75
  //# sourceMappingURL=operator-ask.d.ts.map
@@ -1,10 +1,18 @@
1
+ import { wrapToolPolicy } from "@sema-agent/core";
1
2
  export function operatorAsk(rule, message) {
2
3
  return { action: "ask", message, decisionReason: "rule", matchedAskRule: rule };
3
4
  }
4
5
  export function operatorAlwaysHumanAsk(rule, message) {
5
6
  return withDeploymentAuthorship(operatorAsk(rule, message));
6
7
  }
7
- export function withDeploymentAuthorship(ask) {
8
+ function withDeploymentAuthorship(ask) {
8
9
  return { ...ask, authoredBy: "deployment" };
9
10
  }
11
+ export function createDeploymentAuthoredPolicy(leaf) {
12
+ const authored = (d) => (d.action === "ask" ? withDeploymentAuthorship(d) : d);
13
+ return wrapToolPolicy(leaf, (req, signal) => {
14
+ const decision = leaf.check(req, signal);
15
+ return decision instanceof Promise ? decision.then(authored) : authored(decision);
16
+ });
17
+ }
10
18
  //# sourceMappingURL=operator-ask.js.map
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from "../observability/logger.js";
2
2
  import type { AskState } from "../approval-ask-machine.js";
3
- import type { AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
3
+ import type { AskCallCoordinate, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
4
4
  /** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` /
5
5
  * `approval-exemptions` / `resume-anchors` / `checkpoint-ctx` / …)—— 首版写的是族名 `approvals`,而那
6
6
  * 正是读者会去找**持久 park 行**的地方(它们其实在 `checkpoint-ctx`),且持久数据面的名字按硬 breaking
@@ -175,6 +175,7 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
175
175
  listByState(state: AskState, limit: number): Promise<AskRow[]>;
176
176
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
177
177
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
178
+ listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
178
179
  getAsk(askId: string): Promise<AskRow | null>;
179
180
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
180
181
  getBatch(batchId: string): Promise<BatchRow | null>;
@@ -253,6 +253,9 @@ export class FileApprovalAskStore {
253
253
  listPendingBySession(sessionId, owner, signal) {
254
254
  return this.core.listPendingBySession(sessionId, owner, signal);
255
255
  }
256
+ listByCall(call) {
257
+ return this.core.listByCall(call);
258
+ }
256
259
  getAsk(askId) {
257
260
  return this.core.getAsk(askId);
258
261
  }
@@ -25,7 +25,7 @@
25
25
  * - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
26
26
  */
27
27
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
28
- import type { AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
28
+ import type { AskCallCoordinate, AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
29
29
  /**
30
30
  * [ref] 的 `updatedInput` **入店编码**——两 twin 共用的那一只(本函数是唯一真源)。
31
31
  *
@@ -98,6 +98,7 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
98
98
  * 已 abort 就一条都不读、如实抛,与 SQL twin 的第①条语义逐字一致(第②条在这里天然平凡成立)。 */
99
99
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
100
100
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
101
+ listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
101
102
  getAsk(askId: string): Promise<AskRow | null>;
102
103
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
103
104
  resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
@@ -1,5 +1,5 @@
1
1
  import { canAskTransition } from "../approval-ask-machine.js";
2
- import { assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
2
+ import { askRowOnCall, assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, 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;
@@ -304,6 +304,12 @@ export class InMemoryApprovalAskStore {
304
304
  .sort((a, b) => a.createdAtMs - b.createdAtMs)
305
305
  .map((r) => ({ ...r }));
306
306
  }
307
+ async listByCall(call) {
308
+ return [...this.asks.values()]
309
+ .filter((r) => askRowOnCall(r, call))
310
+ .sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
311
+ .map((r) => ({ ...r }));
312
+ }
307
313
  async getAsk(askId) {
308
314
  const r = this.asks.get(askId);
309
315
  return r ? { ...r } : null;
@@ -410,6 +410,10 @@ export interface BatchRow {
410
410
  updatedAtMs: number;
411
411
  }
412
412
  /** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
413
+ /** 一次调用在一条腿上的坐标({@link ApprovalAskStore.listByCall} 的四列;型从 {@link AskRow} 派生,不手抄)。 */
414
+ export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "sourceTaskId">;
415
+ /** 「这一行在这次调用上」的唯一判据(四列逐字相等):InMemory twin 的筛子与 SQL twin 的出口对齐读同一只。 */
416
+ export declare function askRowOnCall(row: AskRow, call: AskCallCoordinate): boolean;
413
417
  export interface ApprovalAskStore {
414
418
  /** 幂等 upsert。返回**行 + 本次是否真插入**(判别位语义见 {@link EnsureAskResult})。 */
415
419
  ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
@@ -464,6 +468,19 @@ export interface ApprovalAskStore {
464
468
  * 等待」只被**部分**缓解(不再叠加调用方侧的等待,池侧仍会堆)。真正的取消要 driver 级 seam,登记为
465
469
  * 后续件。 */
466
470
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
471
+ /**
472
+ * 🔴 S-944 / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 = 不同 askId 的几只问、升级前按旧派生
473
+ * 写的行,都在其中)。坐标 = {@link AskCallCoordinate} 四列**精确**匹配;序 = `created_at_ms ASC, ask_id ASC`(确定性)。
474
+ *
475
+ * 唯一消费者 = 会话授权短路的争议判据(`tool-approval.ts` `sessionGrantUncontestedByRow`):「这次调用在腿内有行 ⇒ 一揽子放行不得代答」。
476
+ * 为什么按调用而不是按 askId 点读:争议的真源是**调用**(人对这次调用说过什么 / 这次调用还有一张卡挂着),ask 身份只是其中一问的主键;
477
+ * 身份可以加轴(S-944 加了类集),争议判据不得依赖身份 —— 按身份点读在身份加轴的那一刻就把同调用的别的问、旧派生的行读成「不存在」。
478
+ *
479
+ * 三 twin 同答:SQL twin 读后按四列逐字再滤一遍 —— MySQL 协议的 VARCHAR 等值带 PAD SPACE(`'tc'` = `'tc '`),PG(`COLLATE "C"`)与
480
+ * InMemory 严格比较;不在入口拒(调用 id 是引擎铸的原值,首尾空白不归本店判),在出口对齐。索引:走 `idx_ask_task_state`
481
+ * `(task_id, state)` 的 `task_id` 前缀(一条 run 的行数 = 真出过卡的问数;grant 代答的问不落行),不另加索引。
482
+ */
483
+ listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
467
484
  getAsk(askId: string): Promise<AskRow | null>;
468
485
  /** 回决幂等回放的读口。🔴 `taskId` **必填**且是键的第一维——UNIQUE 已改 `(task_id, idempotency_key)`
469
486
  * (车4 §12-E),裸按 key 全局查在多租户库上既可能回**别人 task** 的行(存在性外泄),也可能在同 key
@@ -621,6 +638,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
621
638
  listByState(state: AskState, limit: number): Promise<AskRow[]>;
622
639
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
623
640
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
641
+ listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
624
642
  getAsk(askId: string): Promise<AskRow | null>;
625
643
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
626
644
  deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
@@ -81,6 +81,9 @@ export function projectAskTerminalClaimTruth(row) {
81
81
  settledBy: row.settledBy,
82
82
  };
83
83
  }
84
+ export function askRowOnCall(row, call) {
85
+ return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceTaskId === call.sourceTaskId;
86
+ }
84
87
  export const APPROVAL_ASK_TABLE = "approval_ask";
85
88
  export const APPROVAL_BATCH_TABLE = "approval_batch";
86
89
  export const TIDB_APPROVAL_ASK_STATEMENTS = [
@@ -675,6 +678,10 @@ export class SqlApprovalAskStore {
675
678
  : this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
676
679
  return rows.map(mapAskRow);
677
680
  }
681
+ async listByCall(call) {
682
+ const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
683
+ return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
684
+ }
678
685
  async getAsk(askId) {
679
686
  const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
680
687
  askId,