@sema-agent/server 7.85.0 → 7.87.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 (73) hide show
  1. package/USAGE.md +19 -1
  2. package/dist/approval-hmac.d.ts +77 -2
  3. package/dist/approval-hmac.js +16 -1
  4. package/dist/boot/stage-01-config.js +2 -2
  5. package/dist/capabilities/execution-lane.d.ts +22 -0
  6. package/dist/capabilities/execution-lane.js +8 -0
  7. package/dist/capabilities/memory-notice.d.ts +4 -1
  8. package/dist/config-catalog.js +2 -1
  9. package/dist/execution-lane-caps.d.ts +13 -0
  10. package/dist/execution-lane-caps.js +1 -0
  11. package/dist/fleet/fleet-bus.d.ts +23 -3
  12. package/dist/fleet/fleet-reconciler.d.ts +35 -0
  13. package/dist/fleet/fleet-reconciler.js +52 -9
  14. package/dist/http/approval-sweeps.js +1 -1
  15. package/dist/http/resume-legs.d.ts +2 -1
  16. package/dist/http/resume-legs.js +24 -8
  17. package/dist/http/route-ctx.d.ts +51 -17
  18. package/dist/http/route-ctx.js +0 -3
  19. package/dist/http/routes/a2a-serve.js +1 -1
  20. package/dist/http/routes/approvals-assistant.d.ts +46 -23
  21. package/dist/http/routes/approvals-assistant.js +167 -45
  22. package/dist/http/routes/capabilities.js +2 -0
  23. package/dist/http/routes/images.js +38 -8
  24. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  25. package/dist/http/routes/mcp-reconnect.js +2 -2
  26. package/dist/http/routes/memory-policy.js +3 -6
  27. package/dist/http/routes/notify-wake.js +11 -3
  28. package/dist/http/routes/run-memory-capture.js +3 -5
  29. package/dist/http/routes/runs.js +21 -23
  30. package/dist/http/routes/sessions.js +10 -2
  31. package/dist/http/routes/shared-memory.js +5 -1
  32. package/dist/http/routes/tasks.js +2 -2
  33. package/dist/http/routes/workflows.js +1 -1
  34. package/dist/http/server.d.ts +2 -2
  35. package/dist/http/server.js +3 -3
  36. package/dist/leader/diffout.js +1 -1
  37. package/dist/leader/endpoint.js +1 -1
  38. package/dist/leader/fanout.js +1 -1
  39. package/dist/leader/leader.js +1 -1
  40. package/dist/leader/merge.js +1 -1
  41. package/dist/leader/planner.js +1 -1
  42. package/dist/leader/repair-oracle.js +1 -1
  43. package/dist/leader/repair-wire.js +1 -1
  44. package/dist/leader/wire.js +1 -1
  45. package/dist/memory-scope.d.ts +16 -0
  46. package/dist/observability/fail-open.d.ts +8 -0
  47. package/dist/observability/fail-open.js +8 -0
  48. package/dist/observability/metrics.d.ts +12 -0
  49. package/dist/observability/metrics.js +17 -0
  50. package/dist/observability/{run-terminal-log.d.ts → run-terminal.d.ts} +50 -10
  51. package/dist/observability/{run-terminal-log.js → run-terminal.js} +27 -8
  52. package/dist/plugins/checkpoint-store-sql.d.ts +8 -2
  53. package/dist/plugins/file-run-store.js +2 -2
  54. package/dist/plugins/memory-run-store.js +2 -2
  55. package/dist/plugins/remote-scratchpad.js +1 -1
  56. package/dist/plugins/run-store-sql.js +8 -7
  57. package/dist/plugins/store-contracts.d.ts +25 -2
  58. package/dist/plugins/store-contracts.js +3 -0
  59. package/dist/plugins/workflow-run-store-sql.js +1 -1
  60. package/dist/principal-jwt.d.ts +15 -7
  61. package/dist/principal-jwt.js +1 -1
  62. package/dist/runs.js +1 -1
  63. package/dist/session-titler.js +9 -5
  64. package/dist/task-settings.d.ts +35 -0
  65. package/dist/task-settings.js +7 -0
  66. package/dist/tool-approval.d.ts +22 -2
  67. package/dist/tool-approval.js +2 -0
  68. package/dist/trace/core-keyset-guard.d.ts +1 -1
  69. package/dist/trace/ledger-events.d.ts +1 -1
  70. package/dist/trace/project.d.ts +1 -1
  71. package/dist/trace/project.js +1 -1
  72. package/dist/trace/redact.d.ts +1 -1
  73. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
1
+ import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, GLOBAL_USAGE_KEY, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
4
4
  import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
@@ -24,6 +24,7 @@ import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-seg
24
24
  import { errorText } from "../observability/err-text.js";
25
25
  import { recordFailOpen } from "../observability/fail-open.js";
26
26
  import { verifyRoundsFromBody } from "./verify-rounds.js";
27
+ import { storedBodyPermissionMode } from "../task-settings.js";
27
28
  import { acceptedResumingReceipt } from "./route-ctx.js";
28
29
  import { mintAndDriveDurableRun } from "./durable-run-leg.js";
29
30
  import { createSessionsLocal } from "./routes/sessions.js";
@@ -320,7 +321,7 @@ export function createResumeLegs(ctx) {
320
321
  hostDecision,
321
322
  };
322
323
  const verifyRounds = verifyRoundsFromBody(ctx.body);
323
- const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
324
+ const driven = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
324
325
  noteLateSettleIfAccepted(driven);
325
326
  return driven;
326
327
  }
@@ -353,7 +354,11 @@ export function createResumeLegs(ctx) {
353
354
  }
354
355
  async function driveResumeLeg(args, onAccepted) {
355
356
  const resumeLegStartedAt = Date.now();
356
- const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
357
+ const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
358
+ const commitResumeCtx = () => deps.checkpointStore.putCtx(resumeCtx.sessionId, resumeCtx.ctx).catch((e) => {
359
+ deps.metrics?.inc("resume_ctx_not_persisted_total");
360
+ deps.logger?.warn?.("resume_ctx_not_persisted", { sessionId: resumeCtx.sessionId, gate: outcome.gate, error: errorText(e), note: "this continuation runs with the body it was given; the durable rebuild input still holds the PREVIOUS body, so the NEXT continuation of this session rebuilds from that one (never wider than this leg's)" });
361
+ });
357
362
  const legKey = createHash("sha256").update(String(token)).digest("hex");
358
363
  const streamApprovalOn = resolveStreamApprovalGate({
359
364
  toolApprovalEnabled: Boolean(deps.toolApproval),
@@ -425,7 +430,8 @@ export function createResumeLegs(ctx) {
425
430
  try {
426
431
  return (await store.get(token))?.status === "pending";
427
432
  }
428
- catch {
433
+ catch (e) {
434
+ recordFailOpen("server.resume.reopen-confirm-unreadable", `task=${taskId ?? ""} err=${e instanceof Error ? e.message : String(e)}`);
429
435
  return true;
430
436
  }
431
437
  };
@@ -539,6 +545,7 @@ export function createResumeLegs(ctx) {
539
545
  };
540
546
  resumeLegLive = true;
541
547
  const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
548
+ await commitResumeCtx();
542
549
  let safe = stripResumeToken(vr);
543
550
  const vrNested = nestedPauseOf(vr.terminal);
544
551
  const reParkStatus = nestedParkStatusOf(vr);
@@ -663,6 +670,7 @@ export function createResumeLegs(ctx) {
663
670
  }
664
671
  : {}),
665
672
  });
673
+ await commitResumeCtx();
666
674
  resumeStreamRef = stream;
667
675
  if (taskId)
668
676
  steerableRuns.set(taskId, stream);
@@ -1026,7 +1034,7 @@ export function createResumeLegs(ctx) {
1026
1034
  const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
1027
1035
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1028
1036
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1029
- return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1037
+ return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1030
1038
  }
1031
1039
  async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
1032
1040
  const cs = deps.checkpointStore;
@@ -1139,6 +1147,7 @@ export function createResumeLegs(ctx) {
1139
1147
  resumeObjective,
1140
1148
  outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
1141
1149
  verifyRounds: undefined,
1150
+ resumeCtx: { sessionId, ctx },
1142
1151
  mintFreshRun: { source: "wake" },
1143
1152
  ...(engineNotices.length > 0 ? { engineNotices } : {}),
1144
1153
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
@@ -1147,7 +1156,7 @@ export function createResumeLegs(ctx) {
1147
1156
  ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
1148
1157
  });
1149
1158
  }
1150
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
1159
+ async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider, permissionModeAfter) {
1151
1160
  const cs = deps.checkpointStore;
1152
1161
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1153
1162
  reason: r,
@@ -1208,7 +1217,13 @@ export function createResumeLegs(ctx) {
1208
1217
  if (!ctx)
1209
1218
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
1210
1219
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
1211
- const { spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth);
1220
+ const effMode = storedBodyPermissionMode(ctx.body);
1221
+ if (permissionModeAfter !== undefined && effMode !== "plan") {
1222
+ 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)");
1223
+ }
1224
+ const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
1225
+ const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
1226
+ const { spec, engineNotices } = await resolveResumeSpec(resumeBody, req, auth);
1212
1227
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1213
1228
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1214
1229
  const outcome = {
@@ -1217,7 +1232,8 @@ export function createResumeLegs(ctx) {
1217
1232
  ...(editedPlan !== undefined ? { editedPlan } : {}),
1218
1233
  ...(reason ? { reason } : {}),
1219
1234
  };
1220
- return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1235
+ const out = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx: { ...ctx, body: resumeBody } }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1236
+ return out;
1221
1237
  }
1222
1238
  const routeCtxBase = {
1223
1239
  deps,
@@ -23,6 +23,7 @@ import type { IdempotencyCache } from "./idempotency.js";
23
23
  import type { ImagesLocal } from "./routes/images.js";
24
24
  import type { SessionsLocal } from "./routes/sessions.js";
25
25
  import type { SessionSyncLocal } from "./routes/session-sync.js";
26
+ import type { PlanApprovePermissionModeAfter } from "../task-settings.js";
26
27
  import type { VerifyRoundsSpec } from "./verify-rounds.js";
27
28
  /** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。 */
28
29
  export interface RunRegistry {
@@ -127,9 +128,55 @@ export interface PreparedTaskSubmission {
127
128
  * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
128
129
  engineNotices?: readonly EngineNotice[];
129
130
  }
131
+ /**
132
+ * 一条会话的**续跑重建输入**(`checkpoint_ctx` 的行体)—— 提交腿写一次,续跑腿读它重建 spec。
133
+ *
134
+ * S-433 F2 起命名化(此前是五处结构等价的内联字面 `{ body: TaskRequestBody; memoryScope?: string }`):
135
+ * 它同时是 {@link DriveResumeArgs.resumeCtx} 的形,而那一位是**写回**口 —— 读形与写形是同一件东西这句话
136
+ * 得由类型说,不能靠五处手抄碰巧一致。
137
+ */
138
+ export interface CheckpointCtx {
139
+ body: TaskRequestBody;
140
+ /** 提交时这条会话绑定的记忆域(重建 `RequestAuth` 用);缺席 = 没有记忆面。 */
141
+ memoryScope?: string;
142
+ }
130
143
  /** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
131
144
  export interface DriveResumeArgs {
132
145
  token: CheckpointToken;
146
+ /**
147
+ * 🔴 **S-433 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
148
+ * (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
149
+ * `resumePreempted`、`resumeWake`、`resumePlanReview`,四条一视同仁)。
150
+ *
151
+ * 病(修前):`resumePlanReview` 在 `driveResumeIntoRunLog` **受理(2xx)之后**才 `putCtx(sessionId,
152
+ * {...ctx, body})`,而 `ctx` 是请求**开头** `getCtx` 读到的那一份,两个店的 `putCtx` 又都是按
153
+ * `session_id` **无条件 upsert**(`checkpoint-store-sql.ts` / `local-checkpoint-store.ts`)。受理点之后
154
+ * 本腿已经可以跑完并 `setTerminal` 释放 `task_active` —— 别的副本随即 `createRun` + 写一份**新的** ctx,
155
+ * 然后这条迟到的写把它整只盖掉。那是一条**跨副本的丢写**,而被盖掉的恰恰是权限模式。
156
+ *
157
+ * 修法(单一写者,栅栏 = CAS 赢点,不引版本号/不加特判):写点移进 `driveResumeLeg`,落在
158
+ * `resumeStream` / `resumeWithVerification` **解析之后**(core 的 pre-CAS 守卫 + 原子 CAS 已经赢了)、
159
+ * 本腿驱终局之前。此时 park 行仍攥着 `task_active`,在本腿 `setTerminal` 之前**谁也建不了新 run** ⇒
160
+ * 写与释放天然串行;输掉 CAS 的决裁在 `resumeStream` 处就 reject,**永远到不了写点**(「先写回则输家
161
+ * 放宽」这个顾虑同时成立)。
162
+ *
163
+ * 不变量(规则变小,四条驱动腿一视同仁):**`ctx.body` 恒等于最近一次赢得 CAS 的续跑实际用的体**。
164
+ * 体没被改写的腿传**原 ctx** —— 同内容 upsert 幂等,不需要「只有 plan_review 才写」这条特判。
165
+ *
166
+ * 🔴 `sessionId` 自带一位:写回的键**恒是读进来的那一个**(各腿的 `sessionId` 形参,**不是**
167
+ * `cp.sessionId`)—— 读写不同键会写出一条谁也读不回来的行。
168
+ *
169
+ * 🔴 写失败**响亮但不致命**(codex r1 两条 [high] 验真后定):core 的 `resumeStream` 是 **EAGER** 的 ——
170
+ * 写点这一拍 run 已经在执行,裸抛会把一条活腿判成 failed + 释放 claim 而执行还在继续。⇒ 一个臂、
171
+ * 四条腿同守:计数 `resume_ctx_not_persisted_total` + 一行 warn,本次续跑照常。方向论证(不是 [ref] 的
172
+ * 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `resumePlanReview` 改写这个体、
173
+ * 且只会**放宽** ⇒ 落盘的恒不比本次续跑更宽。旧的 `plan_approve_mode_not_persisted`(plan 专用)随本批删除,
174
+ * 由上面这只家族级的取代。全文见 `resume-legs.ts` 的 `commitResumeCtx` 顶注。
175
+ */
176
+ resumeCtx: {
177
+ sessionId: string;
178
+ ctx: CheckpointCtx;
179
+ };
133
180
  /** [ref]:续跑腿的执行 Runner。resume 会用同一条 resolveSpec 重解析场景,故与首跑选同一只 —— 否则
134
181
  * 一次续跑就把手带 band 重新 mount 回来(收窄只在首跑成立 = 没成立)。 */
135
182
  runner: Runner;
@@ -354,7 +401,10 @@ export interface RouteLegs {
354
401
  decider?: {
355
402
  principal?: string;
356
403
  explicitOperator: boolean;
357
- }): Promise<{
404
+ },
405
+ /** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在 `approve`
406
+ * 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
407
+ permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
358
408
  status: number;
359
409
  body: object;
360
410
  }>;
@@ -549,22 +599,6 @@ export declare function acceptedResumingReceipt(taskId: string, sessionId: strin
549
599
  status: number;
550
600
  body: object;
551
601
  };
552
- /**
553
- * 「**证得出**这条 run 活在别的副本上」——「他副本」那一句话的**唯一判据**(S-381 收编)。
554
- *
555
- * 成案:这个合取式此前在四处**逐字手抄**(`routes/runs.ts` 的 steer 臂与 interrupt 臂、同文件
556
- * `sendHandleMiss`、`routes/run-memory-capture.ts`),第五处就要随 `POST /v1/sessions/:id/mcp/reconnect`
557
- * 出现 —— 而它的每一个合取项都是一条**被复审逐条挣出来的**纪律,不是随手的判空:
558
- * · `run.instanceId` 缺席(`null` / 建列之前的行 / 替身行的 `undefined`)= **不知道它在哪**,不是「在别处」;
559
- * · `thisInstanceId` 缺席 = 本副本连自己是谁都说不上,更不能断言别人;
560
- * · 两者都在场且**不等**才够格说「他副本」(`markResuming` 认领后该列语义 = **当前持有副本**)。
561
- * 抄第五份 = 第五个写者;收成一只谓词之后,纪律改一次就四处同改。**`status` 不进本谓词**:
562
- * `sendHandleMiss` 只对 `running` 行说这句话,而 steer 族在 park/终态两臂之后才走到这里 —— 各调用点
563
- * 的前置臂不同族,合进来会让某一处悄悄改判。
564
- */
565
- export declare function runProvenElsewhere(run: {
566
- instanceId?: string | null;
567
- }, thisInstanceId: string | undefined): boolean;
568
602
  export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
569
603
  /** [ref] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
570
604
  export declare function runNotFoundMessage(id: string): string;
@@ -24,9 +24,6 @@ export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
24
24
  export function acceptedResumingReceipt(taskId, sessionId) {
25
25
  return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } };
26
26
  }
27
- export function runProvenElsewhere(run, thisInstanceId) {
28
- return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
29
- }
30
27
  export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
31
28
  export function runNotFoundMessage(id) {
32
29
  const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
@@ -1,6 +1,6 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { createHash } from "node:crypto";
3
- import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
3
+ import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
4
4
  import { A2A_TASK_STATES } from "@sema-agent/core";
5
5
  import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
6
6
  import { HttpError } from "../../security.js";
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
+ import { type ToolApprovalCoordinator } from "../../tool-approval.js";
4
5
  import { type GovernancePosture } from "../active-run-conflict.js";
5
6
  import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
6
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
@@ -16,6 +17,17 @@ import { type KeyClosureIssue } from "../../request-key-closure.js";
16
17
  * 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
17
18
  */
18
19
  export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
20
+ /**
21
+ * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
22
+ * 全部受理 ⇒ `null`。判据边界与三个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue` /
23
+ * {@link decideBodyKeyIssue})逐字同一条:**只判键名**(值形校验归各自既有的门,本门零抢话);体本身
24
+ * 非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体被两句话抢答)。
25
+ *
26
+ * 🔴 **重放不额外过门**,也不需要 —— 逐字同 {@link decideBodyKeyIssue} 的同名段:本腿的反重放是
27
+ * 一次性 nonce(S-280),它守的是**同一份现写的字节**;而续跑重建读的是盘上的体、走 `resolveSpec` 的
28
+ * 宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的那份字节上**。
29
+ */
30
+ export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
19
31
  /**
20
32
  * S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
21
33
  *
@@ -38,35 +50,46 @@ export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null
38
50
  export declare function decisionReasonOf(body: {
39
51
  reason?: unknown;
40
52
  }): string | undefined;
41
- /** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
42
- * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
43
- * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
44
- export declare function isQuestionAnswer(v: unknown): v is QuestionAnswer;
53
+ /** {@link checkQuestionAnswer} 的判词。两种拒各配**一个既有的**错误码,不铸第三种。 */
54
+ export type QuestionAnswerCheck = {
55
+ readonly ok: true;
56
+ readonly value: QuestionAnswer;
57
+ }
58
+ /** 形不对(不是对象 / `answers` 空 / 某一位的值形不对)⇒ 400 `request.body_shape`,文案逐字不变。 */
59
+ | {
60
+ readonly ok: false;
61
+ readonly kind: "shape";
62
+ }
63
+ /** 词表外的键 ⇒ 400 `request.field_invalid`,**点名键路径**(`answer.answers[0].selection`)。 */
64
+ | {
65
+ readonly ok: false;
66
+ readonly kind: "unknown_key";
67
+ readonly keyPaths: readonly string[];
68
+ };
69
+ /**
70
+ * Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
71
+ * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
72
+ * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use.
73
+ * S-452 起**同时闭键**:词表外的键(拼错 / 未来键 / `__proto__`)拒不丢。
74
+ */
75
+ export declare function checkQuestionAnswer(v: unknown): QuestionAnswerCheck;
45
76
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
46
77
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
47
78
  export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
79
+ export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">,
48
80
  /**
49
- * GET /v1/approvals/stream ([ref] native push): SSE — pushes pending-approval deltas so the portal
50
- * SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
51
- * loop). Polls listPending(scope) and diffs against the prior snapshot: `pending` for a new suspend, `resolved`
52
- * for a decided/expired/gone one. Cross-replica BY CONSTRUCTION — the poll reads the SHARED checkpoint table,
53
- * so a suspend on ANY replica is seen by an operator streaming on a DIFFERENT replica. A transient DB blip
54
- * heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
55
- * Poll-granularity caveat (**narrowed** by [ref]②): the delta now re-emits a `pending` frame whenever a key's
56
- * WIRE PROJECTION changes (upsert), not only when the key enters — so a same-key re-suspend within one interval
57
- * IS visible as long as anything on the row differs. What still shows no delta is the strictly degenerate case:
58
- * a resolve + re-suspend on the same (session,toolCallId) whose projection is BYTE-IDENTICAL — indistinguishable
59
- * by construction, and harmless (the card the operator sees is the card that is pending). Pins: the two halves of
60
- * `test/wire-pairing-approvals-stream.test.ts` S3b (changed ⇒ re-emit with the NEW payload; unchanged ⇒ silence).
81
+ * 🔴 S-454:**第二条 diff 源** —— `GET /v1/approvals` 的 `livePending` 那**同一只**读函数
82
+ * (`Pick` 而不是另立接口:同源是结构事实,不是两处各写一份的约定)。
83
+ *
84
+ * 为什么必须同源:消费方(client-core `hitl/approvalsFeed`)对 stream 上任一非 heartbeat 事件的
85
+ * 反应是**重取权威 `list()`** —— 推与拉读两份数据时,一条只在推面存在的行会让 feed 空转,
86
+ * 一条只在拉面存在的行则永远等不到催人事件。
61
87
  *
62
- * [ref](2026-08-09 亲验定性=设计接受,非欠账):每连接自跑 pollMs 轮询打共享 checkpoint 表,
63
- * 之所以不建 fan-out/单轮询器基建——①消费方=portal 运营面,并发连接数量级为个位(不是用户面);
64
- * ②listPending 走 (scope,status) 索引,单查成本与一次 /v1/approvals 轮询同阶,而本路正是替代客户端
65
- * ~10s 轮询的(净负载持平或更低);③DB blip 已 fail-soft(心跳+下拍重试),15-min cap 兜顶。连接
66
- * 数假设若被打破(portal 多开成常态),届时把 poll 收敛为进程内单轮询器多路复用——那是量级触发的
67
- * 演化,不是现在的缺陷。
88
+ * `undefined` = 本部署无 live 审批面(`TOOL_APPROVAL_ENABLED=false` ⇒ 协调器未装配)⇒ 本源整个
89
+ * 缺席,流的字节与 S-454 之前逐字相同(能力缺席 = 零变化,不是空数组)。**位置在 `pollMs` 之前**
90
+ * 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
68
91
  */
69
- export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">, scope: string | undefined,
92
+ live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined, scope: string | undefined,
70
93
  /** [ref]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
71
94
  * `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
72
95
  governance?: GovernancePosture, pollMs?: number): Promise<void>;