@sema-agent/server 7.96.0 → 7.98.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 (89) hide show
  1. package/MIGRATION.md +5 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +10 -1
  5. package/deploy/sema-up/cluster-up.sh +8 -4
  6. package/deploy/sema-up/kube-up.sh +40 -15
  7. package/deploy/sema-up/lib/envf.sh +173 -0
  8. package/deploy/sema-up/sema-up.sh +113 -94
  9. package/deploy/sema-up/smoke.sh +8 -1
  10. package/dist/approval.d.ts +28 -0
  11. package/dist/approval.js +24 -0
  12. package/dist/boot/budget-tracing.d.ts +1 -0
  13. package/dist/boot/budget-tracing.js +3 -2
  14. package/dist/boot/config-center.js +4 -7
  15. package/dist/boot/coordinators.js +2 -2
  16. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
  17. package/dist/boot/deferred-sandbox-path-env.js +3 -1
  18. package/dist/boot/memory-boundary.d.ts +17 -2
  19. package/dist/boot/memory-boundary.js +11 -0
  20. package/dist/boot/reapers.d.ts +3 -1
  21. package/dist/boot/reapers.js +2 -2
  22. package/dist/boot/resolve-spec.js +15 -10
  23. package/dist/boot/runner-deps.d.ts +2 -1
  24. package/dist/boot/session-faces.d.ts +4 -1
  25. package/dist/boot/session-faces.js +5 -5
  26. package/dist/boot/stage-04-stores.d.ts +1 -0
  27. package/dist/boot/stage-04-stores.js +2 -2
  28. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  29. package/dist/boot/stage-06-runners.d.ts +1 -0
  30. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  31. package/dist/boot/stage-07-capability-layer.js +10 -14
  32. package/dist/boot/stage-08-reapers.d.ts +2 -3
  33. package/dist/boot/stage-08-reapers.js +2 -2
  34. package/dist/boot/stage-09-leader.d.ts +1 -0
  35. package/dist/boot/stage-09-leader.js +2 -2
  36. package/dist/boot/stage-10-http-server.d.ts +1 -0
  37. package/dist/boot/stores.js +5 -11
  38. package/dist/brain.js +28 -18
  39. package/dist/capabilities/execution-lane.d.ts +5 -7
  40. package/dist/capabilities/execution-lane.js +2 -1
  41. package/dist/capabilities/memory-notice.d.ts +30 -12
  42. package/dist/capabilities/memory-notice.js +8 -1
  43. package/dist/capabilities/skills.d.ts +14 -0
  44. package/dist/capabilities/skills.js +4 -0
  45. package/dist/config-catalog.js +5 -4
  46. package/dist/config-center/apply-effective.js +7 -1
  47. package/dist/config-center/facade.d.ts +2 -1
  48. package/dist/config-invariants.d.ts +2 -2
  49. package/dist/config-invariants.js +9 -1
  50. package/dist/config-types.d.ts +20 -3
  51. package/dist/config.d.ts +9 -0
  52. package/dist/config.js +46 -8
  53. package/dist/env-facts.js +2 -4
  54. package/dist/execution-lane-caps.d.ts +36 -6
  55. package/dist/execution-lane-caps.js +4 -0
  56. package/dist/file-history-capture.d.ts +65 -0
  57. package/dist/file-history-capture.js +21 -0
  58. package/dist/hooks/hook-llm.d.ts +1 -1
  59. package/dist/hooks/hook-llm.js +3 -1
  60. package/dist/hosted-posture.d.ts +1 -1
  61. package/dist/http/resume-legs.d.ts +14 -4
  62. package/dist/http/resume-legs.js +26 -17
  63. package/dist/http/route-ctx.d.ts +35 -9
  64. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  65. package/dist/http/routes/approvals-assistant.js +16 -6
  66. package/dist/http/routes/capabilities.js +2 -0
  67. package/dist/http/routes/runs.js +2 -2
  68. package/dist/http/routes/session-sync.js +6 -1
  69. package/dist/http/server.d.ts +4 -1
  70. package/dist/http/server.js +1 -1
  71. package/dist/http/verify-rounds.d.ts +1 -1
  72. package/dist/memory-scope.d.ts +68 -13
  73. package/dist/memory-scope.js +35 -5
  74. package/dist/model-provider.d.ts +13 -0
  75. package/dist/model-provider.js +3 -0
  76. package/dist/model-route-endpoint.d.ts +82 -0
  77. package/dist/model-route-endpoint.js +55 -0
  78. package/dist/observability/fail-open.d.ts +4 -0
  79. package/dist/observability/fail-open.js +4 -0
  80. package/dist/project-identity.d.ts +29 -0
  81. package/dist/project-identity.js +120 -0
  82. package/dist/run-local.d.ts +14 -4
  83. package/dist/run-local.js +12 -10
  84. package/dist/session-sync.d.ts +4 -1
  85. package/dist/session-sync.js +2 -2
  86. package/dist/task-cwd.d.ts +9 -3
  87. package/dist/task-cwd.js +3 -1
  88. package/dist/trace/redact.d.ts +1 -1
  89. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * S-589 —— 文件历史的**装配单点**与能力词 `fileHistoryCapture` 的闭集(叶模块:只 type-import store-backend)。
3
+ *
4
+ * ## 病(修前)
5
+ * `boot/budget-tracing.ts` 只要后端有文件历史店就无条件交给 core(`RunnerDeps.fileHistoryStore`)。core 的首次触碰
6
+ * 捕获是**进程级**的:接了店就恒捕获,没有任何单会话 / 单 run 的杠杆;全仓相关旋钮只有保留条数
7
+ * `FILE_HISTORY_RETENTION_KEEP`。于是用户在客户端关掉「文件检查点」关掉的只是提示,文件字节照旧进
8
+ * `file_history_*` —— 隐私 / 留存轴上的用户面语义谎言。
9
+ *
10
+ * ## 一处判定,两张脸(按动词类别切分,不是特例)
11
+ * 「这台部署有没有文件历史店」只在 {@link assembleFileHistory} 判一次,部署旋钮 `FILE_HISTORY_CAPTURE` 在这一处合取:
12
+ * · **捕获脸** `store` = 后端 ∧ 旋钮。喂 core 的首次触碰捕获(`RunnerDeps.fileHistoryStore`)、rewind 族能力位
13
+ * (`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync`)与本模块的能力词、2c sync 路由族。
14
+ * · **擦除脸** `erasure` = 后端在即在,类型上**只有删除类动词**。喂 E21 在线删除(`boot/session-faces.ts`)与
15
+ * 孤儿 blob GC(`boot/reapers.ts`)。
16
+ * 规则一句话:**旋钮只管写入,从不管擦除**。关掉捕获的部署照样要能删掉已经捕获的文件字节 —— 否则翻旋钮之后
17
+ * `DELETE /v1/sessions/:id` 会回成功却把存量文件内容留在库里,而留存车道(`plugins/retention-store-sql.ts` 的
18
+ * 原生 SQL 删六张 `file_history_*` 表,本来就不看店在不在)与在线删除从此不再等价(E21 两个执行体分家)。
19
+ * ⚠️ 关掉 ≠ 删掉:翻旋钮**不**做任何批量删除。已捕获的历史照既有的会话删除 / 留存规则清理;在旋钮关着的
20
+ * 部署上它也不会再被读用(rewind / restore / sync 都随捕获脸一起缺席)。
21
+ *
22
+ * ## 能力词为什么是闭集两词、没有 `on-optional`
23
+ * cli 要的是「`/config` 那个开关该不该出现」。present-iff 布尔在今天的**每一台**部署上都只能回答「关不掉」
24
+ * (core 的捕获是进程级的)⇒ 出一格三词闭集的前两词:`off`(这台部署根本不捕获,也没有 rewind)与
25
+ * `on-always`(捕获中,且无法按会话关闭)。第三词 `on-optional`(「本会话可以不捕获」)要等 core 收一个
26
+ * 单会话 / 单 run 的不捕获位,**在那之前刻意留空**:预先声明一个结构上不可达的词,等于给客户端一个永远等不到的分支。
27
+ */
28
+ import type { ServiceFileHistoryStore, StoreBackend } from "./plugins/store-backend.js";
29
+ /** 能力词闭集的单点属主(`Record` 锁步:联合加词而这里不补 ⇒ 编译红;词表数组由它派生,不手抄)。 */
30
+ declare const FILE_HISTORY_CAPTURE_WORD_SET: {
31
+ readonly off: true;
32
+ readonly "on-always": true;
33
+ };
34
+ /** `GET /v1/capabilities` 的 `fileHistoryCapture` 词。 */
35
+ export type FileHistoryCapture = keyof typeof FILE_HISTORY_CAPTURE_WORD_SET;
36
+ /** 闭集的全部词(契约文档门 / 测试据此对表)。 */
37
+ export declare const FILE_HISTORY_CAPTURE_WORDS: readonly FileHistoryCapture[];
38
+ /** 擦除脸 —— 物理店上**只有删除类**的那几个动词(类型收窄 = 结构上拿它捕获不了、导出不了、还原不了)。
39
+ * `reap` 是 core 契约必填(local File 店没有整 scope 的 `deleteBySession`,E21 在那条车道上退回 `reap(scope, [])`);
40
+ * `deleteBySession` / `sweepOrphanBlobs` 是 SQL 双生的服务端额外动词。 */
41
+ export type FileHistoryErasureFace = Pick<ServiceFileHistoryStore, "reap" | "deleteBySession" | "sweepOrphanBlobs">;
42
+ /** 装配结果:两张脸。 */
43
+ export interface FileHistoryAssembly {
44
+ /** 捕获脸:在场 ⟺ 后端在 ∧ `FILE_HISTORY_CAPTURE`。 */
45
+ readonly store: ServiceFileHistoryStore | undefined;
46
+ /** 擦除脸:在场 ⟺ 后端在(与旋钮无关)。 */
47
+ readonly erasure: FileHistoryErasureFace | undefined;
48
+ }
49
+ /**
50
+ * **唯一**的装配判定。`capture` = 部署旋钮 `FILE_HISTORY_CAPTURE`(`config.fileHistoryCapture`)。
51
+ *
52
+ * 形照 `execution-lane-caps.ts` 的穷举 switch(无 default 臂):意图词加一个(将来的 `on-optional`)而这里不加臂
53
+ * ⇒ 编译红 —— 加臂的那一刻就是决定「这一词下捕获脸接不接、core 那一位单会话不捕获位怎么递」的地方。
54
+ */
55
+ export declare function assembleFileHistory(backend: StoreBackend | undefined, capture: boolean): FileHistoryAssembly;
56
+ /**
57
+ * 能力词 = 捕获脸在不在(读**装配结果**,不照旋钮字面回声:后端本来就没有这只店的部署同样是 `off`)。
58
+ *
59
+ * 与 `rewindFiles` 读的是**同一个判定**(`deps.fileHistoryStore` 在场),今天两位同构。分叉条件(写在这里免得
60
+ * 将来有人把两位当成一回事):core 收了单会话不捕获位之后,本词在捕获脸在场时会变成 `on-optional`,而
61
+ * `rewindFiles` 仍是 true —— 它回答的是「这台部署能不能还原」,本词回答的是「捕获关不关得掉」。
62
+ */
63
+ export declare function fileHistoryCaptureOf(captureFace: object | undefined): FileHistoryCapture;
64
+ export {};
65
+ //# sourceMappingURL=file-history-capture.d.ts.map
@@ -0,0 +1,21 @@
1
+ const FILE_HISTORY_CAPTURE_WORD_SET = {
2
+ off: true,
3
+ "on-always": true,
4
+ };
5
+ export const FILE_HISTORY_CAPTURE_WORDS = Object.keys(FILE_HISTORY_CAPTURE_WORD_SET);
6
+ export function assembleFileHistory(backend, capture) {
7
+ if (backend === undefined)
8
+ return { store: undefined, erasure: undefined };
9
+ const physical = backend.fileHistory();
10
+ const intent = capture ? "on-always" : "off";
11
+ switch (intent) {
12
+ case "off":
13
+ return { store: undefined, erasure: physical };
14
+ case "on-always":
15
+ return { store: physical, erasure: physical };
16
+ }
17
+ }
18
+ export function fileHistoryCaptureOf(captureFace) {
19
+ return captureFace === undefined ? "off" : "on-always";
20
+ }
21
+ //# sourceMappingURL=file-history-capture.js.map
@@ -9,7 +9,7 @@ import type { HookLlmCall } from "./hook-runner.js";
9
9
  * - `openai-completions` ⇒ POST {base}/chat/completions(现状,字节不变);
10
10
  * - `anthropic-messages` ⇒ POST {base}/v1/messages(x-api-key / Authorization: Bearer + anthropic-version
11
11
  * 头,messages 数组,回包取 content[].text)。此前 hookLlm 只会说 openai 形且缺省 baseUrl 恒落
12
- * `config.gatewayBaseUrl`(缺省 http://127.0.0.1:8000/v1)——anthropic 单路由部署(主模型
12
+ * `config.gatewayBaseUrl`(当年它有一个本机 8000 端口的出厂缺省,S-623 已删)——anthropic 单路由部署(主模型
13
13
  * provider=anthropic 走 ANTHROPIC_BASE_URL,没配 MODEL_GATEWAY_BASEURL)下 titler 每会话打死
14
14
  * localhost:8000 ⇒ `session_title_failed: TypeError: fetch failed`;即使端点活着,anthropic-messages
15
15
  * 模型也不能用 /chat/completions 形调。
@@ -1,5 +1,6 @@
1
1
  import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
2
2
  import { matchCatalogModel } from "../model-select.js";
3
+ import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
3
4
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
4
5
  export function buildHookFailureText(raw) {
5
6
  if (raw === "")
@@ -26,7 +27,8 @@ export function createHookLlm(deps) {
26
27
  }
27
28
  return { ok: true, catalogRef, providerModelId, baseUrl, api };
28
29
  }
29
- return { ok: true, catalogRef, providerModelId, baseUrl: m?.baseUrl || config.gatewayBaseUrl, api };
30
+ const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
31
+ return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api } : { ok: false, error: endpoint.refusal };
30
32
  };
31
33
  const hookModelFor = (requested) => {
32
34
  const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
@@ -151,7 +151,7 @@ export type TaskHostPostureScope = HostedPostureInput & Readonly<Pick<ServiceCon
151
151
  *
152
152
  * 🔴 **为什么这一条不拒**(初版给了 422,当批撤回):这个键**零授权** —— 引擎只把它渲给模型,没有任何
153
153
  * 裁决腿读它(`http/wire-types.ts` 的字段注逐字,来源是 core 的 `ApproverPosture` 头注)。而续跑腿
154
- * **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;`resumePlanReview` 上更贵:
154
+ * **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;plan_review 驱动段(`drivePlanReview`)上更贵:
155
155
  * nonce 先烧、拒在其后才抛,运维重签再撞同一个拒 = 无限循环。本仓对这一类的成文结论逐字是
156
156
  * 「**砖死 resume 比降级糟**」(`boot/resolve-spec.ts` 顶部那段列了三处先例)。
157
157
  * ⇒ 把本批最硬的处置给族里**危害最小**的成员,方向是错的。
@@ -17,7 +17,7 @@ import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
18
  import { type HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
- import type { RouteCtxBase, DriveResumeArgs } from "./route-ctx.js";
20
+ import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
22
22
  import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
23
23
  import type { createRequestGuards } from "./request-guards.js";
@@ -67,13 +67,23 @@ export declare function createResumeLegs(ctx: {
67
67
  status: number;
68
68
  body: object;
69
69
  }>;
70
- resumePlanReview: (sessionId: string, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, log?: {
70
+ preparePlanReview: (sessionId: string, log?: {
71
71
  taskId?: string;
72
72
  principalPresent?: boolean;
73
- }, acceptEarly?: boolean, decider?: {
73
+ }, decider?: {
74
74
  principal?: string;
75
75
  explicitOperator: boolean;
76
- }, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
76
+ }) => Promise<{
77
+ readonly ok: false;
78
+ readonly answer: {
79
+ status: number;
80
+ body: object;
81
+ };
82
+ } | {
83
+ readonly ok: true;
84
+ readonly prepared: PreparedPlanReview;
85
+ }>;
86
+ drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
77
87
  status: number;
78
88
  body: object;
79
89
  }>;
@@ -64,6 +64,9 @@ function checkpointRowRedeemableElsewhere(reason) {
64
64
  return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
65
65
  }
66
66
  const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
67
+ function notResumableAnswer() {
68
+ return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
69
+ }
67
70
  export function createResumeLegs(ctx) {
68
71
  const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
69
72
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
@@ -458,7 +461,7 @@ export function createResumeLegs(ctx) {
458
461
  if (taskId && deps.runStore) {
459
462
  claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
460
463
  if (!claimedRow)
461
- return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
464
+ return notResumableAnswer();
462
465
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
463
466
  fleetPub?.onStart();
464
467
  const rs = deps.runStore;
@@ -1179,8 +1182,9 @@ export function createResumeLegs(ctx) {
1179
1182
  ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
1180
1183
  });
1181
1184
  }
1182
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider, permissionModeAfter) {
1185
+ async function preparePlanReview(sessionId, log, decider) {
1183
1186
  const cs = deps.checkpointStore;
1187
+ const refuse = (answer) => ({ ok: false, answer });
1184
1188
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1185
1189
  reason: r,
1186
1190
  ...(log?.taskId !== undefined ? { taskId: log.taskId } : { sessionId }),
@@ -1192,7 +1196,7 @@ export function createResumeLegs(ctx) {
1192
1196
  const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1193
1197
  if (!token) {
1194
1198
  log404("no-pending-checkpoint");
1195
- return noPending;
1199
+ return refuse(noPending);
1196
1200
  }
1197
1201
  if (decider !== undefined && !decider.explicitOperator) {
1198
1202
  const peeked = await cs
@@ -1202,7 +1206,7 @@ export function createResumeLegs(ctx) {
1202
1206
  const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
1203
1207
  if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
1204
1208
  deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
1205
- return noPending;
1209
+ return refuse(noPending);
1206
1210
  }
1207
1211
  }
1208
1212
  let cp;
@@ -1212,40 +1216,45 @@ export function createResumeLegs(ctx) {
1212
1216
  catch (e) {
1213
1217
  if (!(await mayDiscloseRowError(cs, token, decider))) {
1214
1218
  deps.logger?.warn?.("plan_review_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
1215
- return noPending;
1219
+ return refuse(noPending);
1216
1220
  }
1217
- return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1221
+ return refuse({ status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } });
1218
1222
  }
1219
1223
  if (!cp) {
1220
1224
  log404("checkpoint-missing");
1221
1225
  if (!(await mayDiscloseRowError(cs, token, decider)))
1222
- return noPending;
1223
- return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1226
+ return refuse(noPending);
1227
+ return refuse({ status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } });
1224
1228
  }
1225
1229
  if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
1226
1230
  deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1227
- return noPending;
1231
+ return refuse(noPending);
1228
1232
  }
1229
1233
  const gateKind = cp.gate?.kind;
1230
1234
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
1231
- return {
1235
+ return refuse({
1232
1236
  status: 409,
1233
1237
  body: {
1234
1238
  error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a plan_review — a tool approval must be decided via POST /v1/approvals/:id/decide`,
1235
1239
  errorCode: "gate_not_plan_review",
1236
1240
  },
1237
- };
1241
+ });
1238
1242
  }
1239
1243
  const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
1240
1244
  if (ctxRead.kind === "corrupt")
1241
- return resumeCtxCorruptAnswer(ctxRead.where);
1245
+ return refuse(resumeCtxCorruptAnswer(ctxRead.where));
1242
1246
  if (ctxRead.kind === "absent")
1243
- return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
1244
- const ctx = ctxRead.ctx;
1247
+ return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
1248
+ return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1249
+ }
1250
+ async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
1251
+ const { sessionId, token, cp, ctx } = prepared;
1252
+ if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
1253
+ return notResumableAnswer();
1245
1254
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
1246
1255
  const effMode = storedBodyPermissionMode(ctx.body);
1247
1256
  if (permissionModeAfter !== undefined && effMode !== "plan") {
1248
- throw new Error("resumePlanReview: permissionModeAfter was passed for a task that was not submitted with permissionMode \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
1257
+ throw new Error("drivePlanReview: permissionModeAfter was passed for a task that was not submitted with permissionMode \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
1249
1258
  }
1250
1259
  const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
1251
1260
  const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
@@ -1269,8 +1278,8 @@ export function createResumeLegs(ctx) {
1269
1278
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
1270
1279
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1271
1280
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1272
- legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
1281
+ legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
1273
1282
  };
1274
- return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview };
1283
+ return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, preparePlanReview, drivePlanReview };
1275
1284
  }
1276
1285
  //# sourceMappingURL=resume-legs.js.map
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, Checkpoint, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache, SubmitOutcome } from "./idempotency.js";
@@ -154,7 +154,7 @@ export interface DriveResumeArgs {
154
154
  /**
155
155
  * 🔴 **S-433 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
156
156
  * (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
157
- * `resumePreempted`、`resumeWake`、`resumePlanReview`,四条一视同仁)。
157
+ * `resumePreempted`、`resumeWake`、`drivePlanReview`(S-609 前名 `resumePlanReview`),四条一视同仁)。
158
158
  *
159
159
  * 病(修前):`resumePlanReview` 在 `driveResumeIntoRunLog` **受理(2xx)之后**才 `putCtx(sessionId,
160
160
  * {...ctx, body})`,而 `ctx` 是请求**开头** `getCtx` 读到的那一份,两个店的 `putCtx` 又都是按
@@ -177,7 +177,7 @@ export interface DriveResumeArgs {
177
177
  * 🔴 写失败**响亮但不致命**(codex r1 两条 [high] 验真后定):core 的 `resumeStream` 是 **EAGER** 的 ——
178
178
  * 写点这一拍 run 已经在执行,裸抛会把一条活腿判成 failed + 释放 claim 而执行还在继续。⇒ 一个臂、
179
179
  * 四条腿同守:计数 `resume_ctx_not_persisted_total` + 一行 warn,本次续跑照常。方向论证(不是 [ref] 的
180
- * 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `resumePlanReview` 改写这个体、
180
+ * 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `drivePlanReview` 改写这个体、
181
181
  * 且只会**放宽** ⇒ 落盘的恒不比本次续跑更宽。旧的 `plan_approve_mode_not_persisted`(plan 专用)随本批删除,
182
182
  * 由上面这只家族级的取代。全文见 `resume-legs.ts` 的 `commitResumeCtx` 顶注。
183
183
  */
@@ -311,6 +311,17 @@ export interface DriveResumeArgs {
311
311
  */
312
312
  ownerLeaseAdmitted?: boolean;
313
313
  }
314
+ /**
315
+ * S-609(7.97.0):plan_review 纯读段全部过关后交给驱动段的那一份 —— **选中的那张卡**(token + 载入的行)与**它那一刻**
316
+ * 的续跑上下文。三者一起走:驱动段只认这张卡,ctx 读绑在它上面(读在选卡之后;之后任何 ctx 写都得先赢这张卡的
317
+ * CAS,于是一份过期的读只会让驱动段在 CAS 上败,不会被配给别的卡)。
318
+ */
319
+ export interface PreparedPlanReview {
320
+ readonly sessionId: string;
321
+ readonly token: CheckpointToken;
322
+ readonly cp: Checkpoint;
323
+ readonly ctx: CheckpointCtx;
324
+ }
314
325
  /** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
315
326
  * run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
316
327
  * `prepareSpec`,approvals/assistant/notify-wake 三域共用 resume 家族,把它们跟着任一域搬都会造出
@@ -395,21 +406,36 @@ export interface RouteLegs {
395
406
  status: number;
396
407
  body: object;
397
408
  }>;
398
- /** assistant plan_review 三态腿。 */
399
- resumePlanReview(sessionId: string, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
409
+ /**
410
+ * assistant plan_review 三态腿的**纯读段**(S-609,7.97.0):选卡 → 属主轻读 → 载入 → 行级 scope → 门类 → 这条决断
411
+ * 唯一的一次 ctx 读。任何一步拒 ⇒ `{ok:false, answer}`(不改任何状态;端点在消费一次性 nonce **之前**把它答出去);
412
+ * 全部过关 ⇒ `{ok:true, prepared}`,交给 {@link RouteLegs.drivePlanReview}。语义属主 = `resume-legs.ts` 同名函数顶注。
413
+ */
414
+ preparePlanReview(sessionId: string,
400
415
  /** [ref]/[ref] 件5:`decide_404` 判别日志的附加位(只进日志,不上 wire;身份只记有无不记值)。 */
401
416
  log?: {
402
417
  taskId?: string;
403
418
  principalPresent?: boolean;
404
419
  },
405
- /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
406
- acceptEarly?: boolean,
407
- /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
420
+ /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 属主轻读与行级属主复核
408
421
  * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
409
422
  decider?: {
410
423
  principal?: string;
411
424
  explicitOperator: boolean;
412
- },
425
+ }): Promise<{
426
+ readonly ok: false;
427
+ readonly answer: {
428
+ status: number;
429
+ body: object;
430
+ };
431
+ } | {
432
+ readonly ok: true;
433
+ readonly prepared: PreparedPlanReview;
434
+ }>;
435
+ /** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
436
+ drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
437
+ /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
438
+ acceptEarly?: boolean,
413
439
  /** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在 `approve`
414
440
  * 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
415
441
  permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
@@ -36,7 +36,7 @@ export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue |
36
36
  * · **签名域**(直连门的 HMAC 信封第五位)—— 属主是**跨仓字节契约**(§6.2 / `approvalHmacMessage`,
37
37
  * 签名器在 cli / SDK)。那份契约明写空串与 `null` 是两个不同的被签值。⇒ **逐字线值,永不折叠**;
38
38
  * 在这里折一刀,一份合法签发的空理由证明就永远 401(合并复审 R1 [high] 真复现过)。
39
- * · **上送域**(`resumePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
39
+ * · **上送域**(`drivePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
40
40
  * (7.19.0 / [ref] 把「人的结算」收进一个铸点,空文本在那里折成缺席,live 与 durable decide 两条路径同形)。
41
41
  * ⇒ **原样透传**;跨仓宪法「源头修复禁下游旁路」:在下游再折一次就是同一语义的第二个写者,
42
42
  * 今天两者答案相同,上游哪天改法就变成一条谁都查不出来的分歧。
@@ -19,6 +19,8 @@ import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal }
19
19
  import { governanceOriginOf } from "../active-run-conflict.js";
20
20
  import { sendResumeOutcome } from "../route-ctx.js";
21
21
  import { assertNever } from "../route-ctx.js";
22
+ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
23
+ import { recordFailOpen } from "../../observability/fail-open.js";
22
24
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
23
25
  const DECIDE_BODY_DECLARED_KEYS = [
24
26
  "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
@@ -199,7 +201,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
199
201
  const { deps } = ctx;
200
202
  const { preemptableRuns } = ctx.registry;
201
203
  const { readJson, rateLimited, quotaExceeded, safeDecode } = ctx.helpers;
202
- const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
204
+ const { resumeCheckpoint, resumePreempted, preparePlanReview, drivePlanReview } = ctx.legs;
203
205
  {
204
206
  const cs = deps.checkpointStore;
205
207
  const principal = gatedPrincipal(req, deps.config);
@@ -271,7 +273,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
271
273
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
272
274
  const summaries = await cs.listByScope(scope);
273
275
  const inbox = (await Promise.all(summaries.map(async (s) => {
274
- const ctx = await cs.getCtx(s.sessionId).catch(() => null);
276
+ const ctxRead = await readResumeCtx(cs.getCtx(s.sessionId));
277
+ if (ctxRead.kind === "corrupt")
278
+ recordFailOpen("server.assistant-inbox.objective-ctx-corrupt", ctxRead.where);
279
+ const objective = ctxRead.kind === "ok" ? (ctxRead.ctx.body?.objective ?? null) : null;
275
280
  const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
276
281
  return {
277
282
  sessionId: s.sessionId,
@@ -286,7 +291,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
286
291
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
287
292
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
288
293
  ...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
289
- objective: ctx?.body?.objective ?? null,
294
+ objective,
290
295
  input,
291
296
  };
292
297
  }))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0) || (a.createdAt ?? 0) - (b.createdAt ?? 0));
@@ -520,9 +525,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
520
525
  sendError(res, 404, "not_found.run", "task not found");
521
526
  return;
522
527
  }
528
+ const plan = await preparePlanReview(run.sessionId, { taskId, principalPresent: principal !== undefined }, { principal: deciderPrincipal, explicitOperator });
529
+ if (!plan.ok) {
530
+ sendResumeOutcome(res, plan.answer, deps.logger);
531
+ return;
532
+ }
523
533
  if (body.permissionModeAfter !== undefined) {
524
- const storedCtx = await deps.checkpointStore?.getCtx(run.sessionId).catch(() => null);
525
- if (storedCtx?.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
534
+ const storedCtx = plan.prepared.ctx;
535
+ if (storedCtx.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
526
536
  sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not submitted with permissionMode \"plan\", so approving the plan lifts no read-only restriction");
527
537
  return;
528
538
  }
@@ -539,7 +549,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
539
549
  return;
540
550
  }
541
551
  }
542
- const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true, { principal: deciderPrincipal, explicitOperator }, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
552
+ const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
543
553
  sendResumeOutcome(res, out, deps.logger);
544
554
  return;
545
555
  }
@@ -12,6 +12,7 @@ import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey
12
12
  import { mcpForScenario } from "../../config-center/facade.js";
13
13
  import { sendJson, sendError } from "../send.js";
14
14
  import { assertNever } from "../route-ctx.js";
15
+ import { fileHistoryCaptureOf } from "../../file-history-capture.js";
15
16
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
16
17
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
17
18
  import { projectWriteProtectionCapability } from "../../write-protection.js";
@@ -173,6 +174,7 @@ export async function handleCapabilities(req, res, match, ctx) {
173
174
  rewindFiles: Boolean(deps.fileHistoryStore),
174
175
  restoreFiles: Boolean(deps.fileHistoryStore),
175
176
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
177
+ fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
176
178
  scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
177
179
  sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
178
180
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
@@ -589,7 +589,7 @@ export async function handleRuns(req, res, match, ctx) {
589
589
  const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
590
590
  if (leafId === undefined || leafId === null)
591
591
  return;
592
- const resumeRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
592
+ const resumeRead = await readResumeCtx(cs.getCtx(run.sessionId));
593
593
  if (resumeRead.kind === "corrupt") {
594
594
  mintResult.corruptCtxWhere = resumeRead.where;
595
595
  return;
@@ -612,7 +612,7 @@ export async function handleRuns(req, res, match, ctx) {
612
612
  createdAt: Date.now(),
613
613
  sourceTaskId: taskId,
614
614
  });
615
- const stillRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
615
+ const stillRead = await readResumeCtx(cs.getCtx(run.sessionId));
616
616
  if (stillRead.kind === "corrupt") {
617
617
  await cs.expire(wakeToken, cpScope).catch(() => undefined);
618
618
  mintResult.corruptCtxWhere = stillRead.where;
@@ -41,6 +41,11 @@ export async function handleSessionSync(req, res, match, ctx) {
41
41
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
42
42
  return;
43
43
  }
44
+ const historyStore = deps.fileHistoryStore;
45
+ if (historyStore === undefined) {
46
+ sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: file-history capture is turned off (FILE_HISTORY_CAPTURE=false), so there is no file history to export or import");
47
+ return;
48
+ }
44
49
  const scope = sessionOwnerScopeForWrite(req);
45
50
  if (!scope.fleetWide && scope.gateOwner === null) {
46
51
  sendError(res, 401, "auth.unauthorized", "unauthorized");
@@ -76,7 +81,7 @@ export async function handleSessionSync(req, res, match, ctx) {
76
81
  return;
77
82
  let out;
78
83
  try {
79
- out = await exportSessionManifest(sessionId, deps.backend);
84
+ out = await exportSessionManifest(sessionId, deps.backend, historyStore);
80
85
  }
81
86
  catch (e) {
82
87
  if (sendSessionPolicyError(res, e))
@@ -146,7 +146,10 @@ export interface ServiceStoreDeps {
146
146
  toolResultStore?: ToolResultStoreFull;
147
147
  /** [ref] per-edited-file rewind history store. Present ⇒ the rewind capabilities are advertised; core uses it
148
148
  * via RunnerDeps.fileHistoryStore (first-touch tracking + per-turn boundaries + tracked-set restore, ANY
149
- * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces). */
149
+ * env). Also the 2c history-transfer store (exportHistory/importHistory + blob negotiation faces).
150
+ * 🔴 S-589:这是装配单点 `assembleFileHistory` 的**捕获脸**(后端 ∧ `FILE_HISTORY_CAPTURE`),不是「后端有没有这只店」
151
+ * —— 旋钮关着时它缺席,rewind 族四位、能力词 `fileHistoryCapture` 与 2c sync 路由族一起按「店不在」走。
152
+ * 擦除(E21 / 孤儿 blob GC)不读本键,读 boot 侧的擦除脸。 */
150
153
  fileHistoryStore?: ServiceFileHistoryStore;
151
154
  /** D-1 通用文件上传(clay 拍 2026-07-27:独立附件 store,不骑 snapshot blob 面):enables
152
155
  * POST/GET/DELETE /v1/attachments*。缺席 ⇒ 501(与其他可选 store 面同姿)。绑定/物化半场在
@@ -94,7 +94,7 @@ export function createHttpServer(rawDeps) {
94
94
  }
95
95
  const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
96
96
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
97
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
97
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
98
98
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
99
99
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
100
100
  const server = http.createServer((req, res) => {
@@ -19,6 +19,6 @@ export declare function clampVerifyRounds(v: unknown): number;
19
19
  /** `undefined` when `body.verify` is not `true` (the caller didn't opt into the gate); otherwise the clamped
20
20
  * rounds + (if `maxCostUsd` is a positive number) the same USD→micro-USD cost ceiling the cascade lane uses.
21
21
  * Same expression that lived inline at all four verify/resume call sites in http/server.ts (fresh-submit,
22
- * resumeCheckpoint, resumePreempted, resumePlanReview) — collected here VERBATIM ([ref] R9). */
22
+ * resumeCheckpoint, resumePreempted, drivePlanReview — named resumePlanReview before S-609) — collected here VERBATIM ([ref] R9). */
23
23
  export declare function verifyRoundsFromBody(body: Pick<TaskRequestBody, "verify" | "verifyRounds" | "maxCostUsd">): VerifyRoundsSpec | undefined;
24
24
  //# sourceMappingURL=verify-rounds.d.ts.map