@sema-agent/server 7.116.1 → 7.118.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 (131) hide show
  1. package/MIGRATION.md +195 -0
  2. package/USAGE.md +65 -41
  3. package/dist/approval-card.d.ts +82 -18
  4. package/dist/approval-card.js +46 -4
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/budget-tracing.js +2 -3
  8. package/dist/boot/config-center.d.ts +2 -2
  9. package/dist/boot/config-center.js +5 -7
  10. package/dist/boot/parked-revive-gate.d.ts +1 -7
  11. package/dist/boot/parked-revive-gate.js +2 -12
  12. package/dist/boot/resolve-spec.js +18 -16
  13. package/dist/boot/runner-deps.d.ts +5 -3
  14. package/dist/boot/runner-deps.js +6 -4
  15. package/dist/boot/stage-07-capability-layer.js +2 -2
  16. package/dist/brain.d.ts +17 -8
  17. package/dist/brain.js +10 -7
  18. package/dist/capabilities/hands-lane.d.ts +25 -0
  19. package/dist/capabilities/hands-lane.js +1 -1
  20. package/dist/capabilities/mcp-probe.d.ts +2 -2
  21. package/dist/config-catalog.js +3 -2
  22. package/dist/config-center/apply-effective.d.ts +6 -16
  23. package/dist/config-center/apply-effective.js +0 -6
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/restart-signal.d.ts +1 -1
  27. package/dist/config-types.d.ts +12 -16
  28. package/dist/config.d.ts +32 -29
  29. package/dist/config.js +20 -27
  30. package/dist/deployment-governance.d.ts +13 -19
  31. package/dist/deployment-governance.js +7 -14
  32. package/dist/governance-overrides.d.ts +20 -0
  33. package/dist/governance-overrides.js +8 -0
  34. package/dist/host-decision.d.ts +83 -25
  35. package/dist/host-decision.js +31 -1
  36. package/dist/http/admission.js +13 -7
  37. package/dist/http/approval-sweeps.js +16 -13
  38. package/dist/http/checkpoint-ctx.d.ts +83 -24
  39. package/dist/http/checkpoint-ctx.js +68 -8
  40. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  41. package/dist/http/mcp-dial-deadline.js +21 -0
  42. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  43. package/dist/http/memory-compliance-failure-reply.js +17 -8
  44. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  45. package/dist/http/memory-store-refusal-reply.js +16 -5
  46. package/dist/http/resume-legs.js +18 -18
  47. package/dist/http/retired-skill-manifest.d.ts +1 -1
  48. package/dist/http/route-ctx.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +21 -24
  50. package/dist/http/routes/capabilities.js +5 -3
  51. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  52. package/dist/http/routes/mcp-reconnect.js +6 -1
  53. package/dist/http/routes/memory-bundle.js +1 -1
  54. package/dist/http/routes/memory-compliance.js +1 -1
  55. package/dist/http/routes/memory-origin.js +1 -1
  56. package/dist/http/routes/runs.js +53 -33
  57. package/dist/http/routes/sessions.d.ts +1 -1
  58. package/dist/http/routes/sessions.js +3 -6
  59. package/dist/http/routes/workflows.js +2 -0
  60. package/dist/http/wire-types.d.ts +6 -1
  61. package/dist/memory-store-refusal.d.ts +42 -17
  62. package/dist/memory-store-refusal.js +25 -6
  63. package/dist/model-select.d.ts +16 -5
  64. package/dist/model-select.js +7 -3
  65. package/dist/model-unreachable-notice.d.ts +1 -1
  66. package/dist/namespaced-rule-name.d.ts +45 -39
  67. package/dist/namespaced-rule-name.js +36 -22
  68. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  69. package/dist/observability/corrupt-read-seat.js +1 -0
  70. package/dist/observability/fail-open.d.ts +15 -3
  71. package/dist/observability/fail-open.js +15 -3
  72. package/dist/observability/permission-rule-events.d.ts +3 -0
  73. package/dist/observability/permission-rule-events.js +4 -0
  74. package/dist/observability/run-terminal.js +21 -1
  75. package/dist/operator-ask.d.ts +4 -1
  76. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  77. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  78. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  79. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  80. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  81. package/dist/orchestration/workflow-notify-journal.js +7 -2
  82. package/dist/outbound-dispatcher.d.ts +47 -20
  83. package/dist/outbound-dispatcher.js +198 -23
  84. package/dist/parked-decide.d.ts +43 -48
  85. package/dist/parked-decide.js +54 -16
  86. package/dist/permission-rule-list.d.ts +20 -31
  87. package/dist/permission-rule-vocab.d.ts +4 -3
  88. package/dist/permission-rule-vocab.js +1 -1
  89. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  90. package/dist/plugins/approval-ask-store-sql.js +0 -3
  91. package/dist/plugins/one-time-migrations.d.ts +25 -0
  92. package/dist/plugins/one-time-migrations.js +192 -6
  93. package/dist/plugins/pg-pool.js +1 -1
  94. package/dist/plugins/tidb-pool.js +1 -1
  95. package/dist/plugins/web-search.d.ts +20 -1
  96. package/dist/plugins/web-search.js +39 -10
  97. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  98. package/dist/plugins/workflow-run-store-sql.js +5 -7
  99. package/dist/request-key-closure.js +1 -1
  100. package/dist/request-value-closure.d.ts +1 -1
  101. package/dist/router/route-orchestration.d.ts +2 -23
  102. package/dist/rule-entry-list.d.ts +88 -0
  103. package/dist/rule-entry-list.js +102 -0
  104. package/dist/rules-consent.js +3 -0
  105. package/dist/run-local.d.ts +4 -2
  106. package/dist/run-local.js +4 -3
  107. package/dist/runs.d.ts +1 -1
  108. package/dist/runs.js +1 -0
  109. package/dist/runtime-governance.d.ts +5 -21
  110. package/dist/runtime-governance.js +4 -8
  111. package/dist/session-policy-wire.d.ts +13 -12
  112. package/dist/session-policy-wire.js +11 -6
  113. package/dist/spec-fields.d.ts +13 -24
  114. package/dist/spec-fields.js +13 -17
  115. package/dist/standing-answer-grant.d.ts +14 -2
  116. package/dist/standing-answer-grant.js +8 -1
  117. package/dist/task-settings.d.ts +18 -18
  118. package/dist/task-settings.js +63 -32
  119. package/dist/tool-approval.d.ts +77 -145
  120. package/dist/tool-approval.js +80 -62
  121. package/dist/trace/core-keyset-guard.d.ts +6 -6
  122. package/dist/trace/engine-notice-wire.d.ts +4 -6
  123. package/dist/trace/engine-notice-wire.js +14 -1
  124. package/dist/trace/self-minted-wire.d.ts +16 -2
  125. package/dist/trace/self-minted-wire.js +7 -0
  126. package/dist/user-input-ledger.d.ts +24 -12
  127. package/dist/user-input-ledger.js +33 -14
  128. package/package.json +2 -2
  129. package/scripts/drain-check-local.mjs +103 -7
  130. package/scripts/drain-check.mysql.sql +59 -8
  131. package/scripts/drain-check.pg.sql +66 -10
@@ -1,7 +1,7 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
4
- import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
4
+ import { decideParkedAgent, denyWorkflowParkForked, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
5
  const sessionFaceUnavailable = async (sessionId) => {
6
6
  throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no sessionFaceOf read (every built-in store has one) — refusing rather than resuming unverified`);
7
7
  };
@@ -15,7 +15,7 @@ import { recordResumeResourceWindow } from "../resource-window.js";
15
15
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
16
16
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
17
17
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
18
- import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerMayRetire } from "../tool-approval.js";
18
+ import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
19
19
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
20
20
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
21
21
  import { withPrincipal } from "../observability/principal-context.js";
@@ -42,7 +42,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
42
42
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
43
43
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
44
44
  import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
45
- import { allowListReadingRefusal, allowListReadingUnstated } from "./checkpoint-ctx.js";
45
+ import { resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "./checkpoint-ctx.js";
46
46
  import { httpErrorCode } from "./send.js";
47
47
  function isCheckpointReopenedFailure(r) {
48
48
  const code = failureCodeOf(r.terminal);
@@ -274,7 +274,7 @@ export function createResumeLegs(ctx) {
274
274
  const rememberableTool = paForGrant !== undefined &&
275
275
  paForGrant.toolName &&
276
276
  paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
277
- standingAnswerMayRetire(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
277
+ standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
278
278
  ? paForGrant.toolName
279
279
  : null;
280
280
  const boundGrant = onResumeCommitted
@@ -345,6 +345,10 @@ export function createResumeLegs(ctx) {
345
345
  void ctxPromise.catch(() => { });
346
346
  return denyWorkflowParksUnreadable(origin);
347
347
  }
348
+ if (origin?.kind === "workflow_park_forked") {
349
+ void ctxPromise.catch(() => { });
350
+ return denyWorkflowParkForked(origin);
351
+ }
348
352
  if (origin?.kind === "workflow") {
349
353
  void ctxPromise.catch(() => { });
350
354
  const plan = planWorkflowParkedDecide(origin, parkedReq);
@@ -408,26 +412,21 @@ export function createResumeLegs(ctx) {
408
412
  });
409
413
  return { spec, engineNotices };
410
414
  };
411
- const refuseAllowListReading = (sessionId) => {
412
- deps.logger?.warn?.("resume_refused_allow_list_reading", { sessionId, note: "this session's stored request carries settings.permissions.allow written by a server that read it as a whitelist; it is not replayed under the current reading — the task stays suspended until someone cancels it" });
413
- return allowListReadingRefusal();
415
+ const refuseStoredBodyReplay = (sessionId, refusal) => {
416
+ deps.logger?.warn?.("resume_refused_stored_body", { sessionId, ...storedBodyReplayLogFields(refusal) });
417
+ return storedBodyReplayAnswer(refusal);
414
418
  };
415
419
  const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
416
- if (allowListReadingUnstated(ctx))
417
- return { ok: false, answer: refuseAllowListReading(auth.sessionId) };
420
+ const replay = storedBodyReplayRefusal(ctx);
421
+ if (replay !== undefined)
422
+ return { ok: false, answer: refuseStoredBodyReplay(auth.sessionId, replay) };
418
423
  try {
419
424
  const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth);
420
425
  return { ok: true, spec, engineNotices };
421
426
  }
422
427
  catch (e) {
423
428
  if (e instanceof HttpError) {
424
- return {
425
- ok: false,
426
- answer: {
427
- status: 409,
428
- body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", blockedBy: httpErrorCode(e.status, e.code), ...(e.extra ?? {}) },
429
- },
430
- };
429
+ return { ok: false, answer: resumeBlockedByPolicyAnswer(e.message, httpErrorCode(e.status, e.code), e.extra) };
431
430
  }
432
431
  throw e;
433
432
  }
@@ -1348,8 +1347,9 @@ export function createResumeLegs(ctx) {
1348
1347
  return refuse(resumeCtxCorruptAnswer(ctxRead.where));
1349
1348
  if (ctxRead.kind === "absent")
1350
1349
  return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
1351
- if (allowListReadingUnstated(ctxRead.ctx))
1352
- return refuse(refuseAllowListReading(sessionId));
1350
+ const replay = storedBodyReplayRefusal(ctxRead.ctx);
1351
+ if (replay !== undefined)
1352
+ return refuse(refuseStoredBodyReplay(sessionId, replay));
1353
1353
  return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1354
1354
  }
1355
1355
  async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
@@ -9,7 +9,7 @@
9
9
  * 「带」= 键在场、值是什么都算(`null` / 空对象同样是「我带了 manifest」)—— 与引擎的退役键表读法同形(`RETIRED_TASK_SPEC_KEYS["skills[].manifest"]`:`at !== undefined`)。
10
10
  * 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
11
11
  *
12
- * 🔴 本模块**零运行期 import**(随包脚本要在没有服务其余部分的位置加载它,与 `checkpoint-ctx.js` 同一姿势)—— 往里加 import 要先想想脚本那一头。
12
+ * 🔴 本模块**零运行期 import**(随包脚本要在没有服务其余部分的位置加载它;`checkpoint-ctx.js` 7.117.0 起经 `rule-entry-list.js` 依赖引擎包根 —— 包形里有它)—— 往里加 import 要先想想脚本那一头。
13
13
  */
14
14
  /** 一条技能(提交体 `skills[]` / `agents[].skills[]` 的元素)是否还带退役的 `manifest` 键。 */
15
15
  export declare function skillCarriesRetiredManifest(skill: unknown): boolean;
@@ -162,9 +162,9 @@ export interface CheckpointCtx {
162
162
  */
163
163
  unattendedLeg?: true;
164
164
  /**
165
- * 🔴 S-1034(7.115.0)—— 体里 `settings.permissions.allow` 的**读法记号**:在场 = 这一行是 ≥7.115.0 写下的、体里带非空 `allow`,
166
- * 它按「allow 规则(表态)」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
167
- * 缺席而体里带非空 `allow` = ≤7.114.0 停驻的行(那时 `allow` 是名单)⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
165
+ * 体里 `settings.permissions.allow` 的**读法记号**(闭集只有当前读法一词 `"allow_layer"`):在场 = 这一行是当前读法下写下的、体里带非空 `allow`,
166
+ * 它按「交给引擎的 allow 层」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
167
+ * 缺席(≤7.114.0,名单时代)或旧词 `"statement"`(7.115.0–7.116.x,表态时代)而体里带非空 `allow` ⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
168
168
  * 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
169
169
  */
170
170
  permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
@@ -1,8 +1,8 @@
1
1
  import { screenedGate } from "../../trace/project.js";
2
- import { hostDecisionFacts } from "../../host-decision.js";
2
+ import { hostDecisionFacts, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, standingAnswerMayRetire } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, standingAnswerDurablyRecordable } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -17,7 +17,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
- import { answerGrantsBeyondCall } from "../../standing-answer-grant.js";
20
+ import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
21
  import { sendResumeOutcome } from "../route-ctx.js";
22
22
  import { assertNever } from "../route-ctx.js";
23
23
  import * as facts from "../../capability-facts.js";
@@ -25,10 +25,10 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
25
25
  import { recordFailOpen } from "../../observability/fail-open.js";
26
26
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
27
27
  const DECIDE_BODY_DECLARED_KEYS = [
28
- "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
28
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
29
29
  ];
30
30
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
31
- const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
31
+ const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
32
32
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
33
33
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
34
34
  ];
@@ -356,6 +356,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
356
356
  ...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
357
357
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
358
358
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
359
+ ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
360
+ ...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
361
+ ...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
359
362
  objective,
360
363
  input,
361
364
  };
@@ -637,7 +640,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
637
640
  const body = (await readJson(req));
638
641
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
639
642
  if (!decision) {
640
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, settledBy? }");
643
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }");
641
644
  return;
642
645
  }
643
646
  {
@@ -708,29 +711,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
708
711
  sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
709
712
  return;
710
713
  }
711
- const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
712
- switch (settledByVerdict.kind) {
714
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
715
+ switch (decidedByReading.kind) {
713
716
  case "not_in_proof":
714
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
717
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
715
718
  return;
716
- case "deny_only":
717
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
719
+ case "outside_closed_set":
720
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
718
721
  return;
719
- case "not_string":
720
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
721
- return;
722
- case "too_wide":
723
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
724
- return;
725
- case "absent":
726
722
  case "accepted":
727
723
  break;
728
724
  default: {
729
- const unreachable = settledByVerdict;
730
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
725
+ const unreachable = decidedByReading;
726
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
731
727
  }
732
728
  }
733
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
729
+ const decidedBy = decidedByReading.decidedBy;
734
730
  const binding = {
735
731
  checkpointToken: body.checkpointToken,
736
732
  boundCallId: body.boundCallId,
@@ -779,7 +775,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
779
775
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
780
776
  }
781
777
  : undefined;
782
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
778
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decidedBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
783
779
  const outErrorCode = out.body.errorCode;
784
780
  if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
785
781
  try {
@@ -826,11 +822,12 @@ function redactPendingDisclosures(row) {
826
822
  function projectPendingForWire(row, governance) {
827
823
  const redacted = redactPendingDisclosures(row);
828
824
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
829
- const rememberRefused = !standingAnswerMayRetire({
825
+ const rememberRefused = !standingAnswerDurablyRecordable({
830
826
  governanceForced: governanceForced === true,
831
827
  requiresRealApproval: row.requiresRealApproval === true,
832
828
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
833
- grantsBeyondCall: answerGrantsBeyondCall(row),
829
+ grantBinding: grantBindingOf(row),
830
+ classes: row.classes,
834
831
  });
835
832
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
836
833
  }
@@ -1,5 +1,5 @@
1
1
  import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
- import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
2
+ import { modelImageInput, isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { projectSealedKeyCapability } from "../../sealed-key.js";
5
5
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
@@ -120,7 +120,7 @@ export async function handleCapabilities(req, res, match, ctx) {
120
120
  subagentSteer: facts.subagentSteer(deps).available,
121
121
  subagentResume: facts.subagentSteer(deps).available,
122
122
  taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: taskHooksHonored(deps.config) },
123
- settings: { permissions: { allowGrant: true } },
123
+ settings: { permissions: { allowGrant: true, allowLayer: true } },
124
124
  permissionModeAuto: {
125
125
  accepted: true,
126
126
  classifierSeat: deps.autoModeFace?.seatMounted === true,
@@ -137,6 +137,7 @@ export async function handleCapabilities(req, res, match, ctx) {
137
137
  taskAgents: deps.config.requirePrincipal !== true,
138
138
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
139
139
  interactiveTools: true,
140
+ approvalReach: true,
140
141
  disableSkills: true,
141
142
  userInvokedSkills: true,
142
143
  skillToolLists: true,
@@ -158,6 +159,7 @@ export async function handleCapabilities(req, res, match, ctx) {
158
159
  approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
159
160
  streamApproval: facts.streamApproval(deps).available,
160
161
  approvalDecisionNote: true,
162
+ approvalDecidedBy: true,
161
163
  sideQuery: true,
162
164
  agentRoster: facts.agentRoster(deps).available,
163
165
  retention: retentionAdvertisement(deps.config),
@@ -238,7 +240,7 @@ export async function handleCapabilities(req, res, match, ctx) {
238
240
  id: m.id,
239
241
  provider: m.provider,
240
242
  reasoning: m.reasoning,
241
- vision: Array.isArray(m.input) ? m.input.includes("image") : false,
243
+ ...(modelImageInput(m) !== "undeclared" ? { vision: modelImageInput(m) === "yes" } : {}),
242
244
  ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
243
245
  ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
244
246
  ...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
@@ -36,6 +36,23 @@
36
36
  * (`isParkedRunStatus` / `isTerminalRunStatus`)与 `runProvenElsewhere`(手抄已收编成一只谓词,S-430 起同住 `plugins/store-contracts.ts`)。
37
37
  * 显式 operator 可重拨任何租户的会话(监督动作,与 notify / steer 同臂;**不是** bare `isOperator`)。
38
38
  *
39
+ * ── 应答期限(S-431)────────────────────────────────────────────────────────────────────────────
40
+ * 修前这一口对 `live.reconnectMcpServer(server)` 是裸 `await`:server 侧没有期限,上界只来自 core 自己的启动期钟
41
+ * (env `MCP_TIMEOUT`,未设落 SDK 缺省 60s;拨号之前还有一次不在那条钟里的旧连接 `close()`),一台拨不通 / 半开的远端能把这一口
42
+ * HTTP 请求挂很久。现在与 `GET /v1/sessions/:id/mcp` 面板读**同一只**数 `MCP_DIAL_DEADLINE_MS`(`../mcp-dial-deadline.ts`),
43
+ * 到点答 **503 `state.mcp_reconnect_unsettled`**。
44
+ * 🔴 **只界应答,不取消重拨**:core 的 `TaskStream.reconnectMcpServer(server)` 不收 `AbortSignal`(d.ts 逐字:单参),
45
+ * 本面没有取消把手可递,也不伪造一个。到点之后重拨**继续跑**,落定时照常把名册写上这条 run(成功)或撤走(拨号失败)—— 所以
46
+ * 503 说的是「这一口没等到结果」,**不是**「失败」,文案与契约 §G.7c 都按这个读法写。重发**不作保证**:core 的在途守卫
47
+ * (`redialsInFlight`,`dist/core/mcp.js`)只罩**拨号本身**(关旧 + 连 + 列),拨号一落定就放开,而 `redialMcpServer`
48
+ * (`dist/core/runner/mcp-redial.js`)随后还要 `await commitMcpRosterChanges`(重建工具面)—— 那一段里守卫已不在,
49
+ * 此刻再发会开第二次「先关后拨」。所以文案只说「拨号还在跑时 core 会拒第二次」,不说「重发一定得 refused」,也不把重发
50
+ * 推荐成探针(对抗复审 [medium],验真后采纳;守卫延到名册提交之后是 core 的活,本仓不自造第二份在途判)。
51
+ * 为什么是 503 而不是 200 + 第四个 `outcome` 词:`outcome` 三词是 core 结果的投影,这里**没有结果**,编一个词就是替
52
+ * core 说话(与 `unsupported` 那一行「没有 core 结果就不铸 `prefix`」同一条纪律);503 是 `state.*` 族的唯一状态
53
+ * (附录 A 判据 3),与 `state.mcp_probe_incomplete` 同族同档,但**不复用**它的码 —— 那一码的语义钉在 probe 面上。
54
+ * 本面**不**带 `Retry-After`:重发不是无代价的重试(见上),一个会被通用重试臂自动执行的头在一次交易动词上是错的方向。
55
+ *
39
56
  * ── 计费/门 ─────────────────────────────────────────────────────────────────────────────────────
40
57
  * 零模型工作(不进 billable;不吃 drain / roster-pending);mutating ⇒ `rateLimited`。能力位 `mcpReconnect`
41
58
  * = 持久 run 账面 ∧ 会话属主面(两道 501 的同一批 deps 推导出来,「说 yes 但 501」结构上不可能)。
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
3
3
  import { redactSecrets } from "../../trace/redact.js";
4
+ import { DeadlineExceeded, MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
4
5
  import { sendJson, sendError } from "../send.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import {} from "../route-ctx.js";
@@ -99,11 +100,15 @@ export async function handleMcpReconnect(req, res, match, ctx) {
99
100
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
100
101
  if (live) {
101
102
  try {
102
- const result = await live.reconnectMcpServer(server);
103
+ const result = await raceDeadline(live.reconnectMcpServer(server), MCP_DIAL_DEADLINE_MS);
103
104
  sendJson(res, 200, { taskId: activeTaskId, sessionId, ...projectResult(result) });
104
105
  return;
105
106
  }
106
107
  catch (e) {
108
+ if (e instanceof DeadlineExceeded) {
109
+ sendError(res, 503, "state.mcp_reconnect_unsettled", "the MCP re-dial did not settle within the time this endpoint waits, so there is no outcome to report — the re-dial itself was NOT cancelled (the engine takes no cancellation for it) and may still finish, so do not assume it failed; do not resend blindly either: the engine refuses a second re-dial of the same server only while the dial itself is still running, and a request sent after that point starts another close-then-dial on a server that may now be healthy, so check the server's state before re-sending");
110
+ return;
111
+ }
107
112
  const code = e.code;
108
113
  const msg = e instanceof Error ? e.message : String(e);
109
114
  switch (code) {
@@ -37,7 +37,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
37
37
  const code = codeOf(err);
38
38
  if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
39
39
  return true;
40
- if (sentMemoryStoreRefusal(res, err))
40
+ if (sentMemoryStoreRefusal(res, err, ctx.deps.logger))
41
41
  return true;
42
42
  ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
43
43
  return false;
@@ -12,7 +12,7 @@ const EraseRequestSchema = z.object({
12
12
  allowUnevidenced: z.boolean().optional(),
13
13
  });
14
14
  function sentClassifiedFailure(res, ctx, face, err) {
15
- if (sentMemoryComplianceFailure(res, err, { kind: "operator" }))
15
+ if (sentMemoryComplianceFailure(res, err, { kind: "operator" }, ctx.deps.logger))
16
16
  return true;
17
17
  ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
18
18
  return false;
@@ -54,7 +54,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
54
54
  if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
55
55
  return true;
56
56
  }
57
- if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
57
+ if (face !== "clearances" && sentMemoryStoreRefusal(res, err, ctx.deps.logger))
58
58
  return true;
59
59
  if (err instanceof ControlPlaneCorruptError) {
60
60
  sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
@@ -1,7 +1,7 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { z } from "zod";
4
- import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
4
+ import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
5
5
  import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
@@ -14,13 +14,16 @@ import { createHash } from "node:crypto";
14
14
  import { scopedIdempotencyKey } from "../idempotency.js";
15
15
  import { streamSseLog } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
- import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
18
- import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
17
+ import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
18
+ import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
19
+ import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
19
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
20
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
21
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
22
23
  import { readTurnActivityMs } from "../../turn-activity.js";
23
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
+ import { userInputLaneOf } from "../../user-input-ledger.js";
26
+ import { INJECTION_TIERS } from "../../trace/injection-tier.js";
24
27
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
25
28
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
26
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
@@ -30,6 +33,10 @@ import { mintAndDriveDurableRun } from "../durable-run-leg.js";
30
33
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
31
34
  import { withPrincipal } from "../../observability/principal-context.js";
32
35
  import { readResumeCtx } from "../resume-ctx-read.js";
36
+ import { storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "../checkpoint-ctx.js";
37
+ const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
38
+ const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
39
+ const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
33
40
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
34
41
  const ASK_ACTOR_LABEL_MAX = 256;
35
42
  const AskDecisionActorSchema = ActorAssertionWire.strip();
@@ -45,7 +52,7 @@ const AskDecisionBodySchema = z
45
52
  .refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
46
53
  .optional(),
47
54
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
48
- settledBy: z.unknown().optional(),
55
+ decidedBy: z.unknown().optional(),
49
56
  })
50
57
  .strict();
51
58
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -135,11 +142,16 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
135
142
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
136
143
  }, taskId, staleMs);
137
144
  }
145
+ function sendSteerReplayRefusal(res, refusal) {
146
+ const detail = refusal?.detail;
147
+ const differs = detail !== null && typeof detail === "object" ? readClosedWordSeat(detail, "differs") : undefined;
148
+ sendError(res, 409, "steering.duplicate_input_id", refusal instanceof Error ? refusal.message : "this steering input id was already accepted with different values", differs !== undefined ? { detail: { differs } } : undefined);
149
+ }
138
150
  async function acceptLiveUserInput(deps, res, at) {
139
151
  if (!deps.userInputLedger)
140
152
  return undefined;
141
153
  try {
142
- return await deps.userInputLedger.accept({ input: "steer", messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
154
+ return await deps.userInputLedger.accept({ input: userInputLaneOf(at.tier), messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
143
155
  }
144
156
  catch (e) {
145
157
  deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
@@ -428,15 +440,15 @@ export async function handleRuns(req, res, match, ctx) {
428
440
  return;
429
441
  }
430
442
  if (typeof body.text !== "string" || body.text.length === 0) {
431
- sendError(res, 400, "request.body_shape", "body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: 'now' | 'next' | 'later' }");
443
+ sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
432
444
  return;
433
445
  }
434
446
  if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
435
447
  sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
436
448
  return;
437
449
  }
438
- if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
439
- sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
450
+ if (body.priority !== undefined && !isSystemInjectionPriority(body.priority)) {
451
+ sendError(res, 400, "request.field_invalid", `priority must be ${STEER_PRIORITY_CHOICES} when present`);
440
452
  return;
441
453
  }
442
454
  const text = body.text;
@@ -494,19 +506,19 @@ export async function handleRuns(req, res, match, ctx) {
494
506
  return "queue-full";
495
507
  }
496
508
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
497
- parkDuplicateKey = e.message;
509
+ parkDuplicateKey = e;
498
510
  return "duplicate-key";
499
511
  }
500
512
  throw e;
501
513
  }
502
514
  };
503
515
  const sendQueueFull = () => sendError(res, 409, "steering.queue_full", parkQueueFull ?? "the parked steering queue on this checkpoint is full");
504
- const sendDuplicateKey = () => sendError(res, 409, "steering.duplicate_input_id", parkDuplicateKey ?? "this Idempotency-Key is already parked with different steering content — reissue with a fresh key");
516
+ const sendDuplicateKey = () => sendSteerReplayRefusal(res, parkDuplicateKey);
505
517
  const sendParked = (status) => sendJson(res, 202, { taskId, status, delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
506
518
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
507
519
  const live = steerableRuns.get(taskId);
508
520
  if (live) {
509
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
521
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live, tier: priority });
510
522
  if (acceptance === "refused")
511
523
  return;
512
524
  try {
@@ -525,7 +537,7 @@ export async function handleRuns(req, res, match, ctx) {
525
537
  return;
526
538
  }
527
539
  if (code === "steering.duplicate_input_id") {
528
- parkDuplicateKey = e instanceof Error ? e.message : undefined;
540
+ parkDuplicateKey = e;
529
541
  sendDuplicateKey();
530
542
  return;
531
543
  }
@@ -600,7 +612,7 @@ export async function handleRuns(req, res, match, ctx) {
600
612
  return;
601
613
  }
602
614
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
603
- parkDuplicateKey = e.message;
615
+ parkDuplicateKey = e;
604
616
  mintResult.duplicateKey = true;
605
617
  return;
606
618
  }
@@ -634,6 +646,11 @@ export async function handleRuns(req, res, match, ctx) {
634
646
  mintResult.noResumeCtx = true;
635
647
  return;
636
648
  }
649
+ const unservable = storedBodyReplayRefusal(resumeRead.ctx);
650
+ if (unservable !== undefined) {
651
+ mintResult.unservable = unservable;
652
+ return;
653
+ }
637
654
  const cpScope = encodeCheckpointScope(run.owner);
638
655
  const wakeToken = mintCheckpointToken();
639
656
  await cs.put(wakeToken, {
@@ -688,6 +705,12 @@ export async function handleRuns(req, res, match, ctx) {
688
705
  sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — no wake card was minted and nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
689
706
  return;
690
707
  }
708
+ if (mintResult.unservable !== undefined) {
709
+ deps.logger?.warn?.("steer_wake_park_refused_stored_body", { taskId, sessionId: run.sessionId, ...storedBodyReplayLogFields(mintResult.unservable) });
710
+ const { status, body: { error, errorCode, ...rest } } = storedBodyReplayAnswer(mintResult.unservable);
711
+ sendError(res, status, errorCode, error, rest);
712
+ return;
713
+ }
691
714
  if (mintResult.noResumeCtx) {
692
715
  sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer queued now could never be delivered by POST /v1/sessions/:id/wake; submit a new task instead");
693
716
  return;
@@ -819,7 +842,7 @@ export async function handleRuns(req, res, match, ctx) {
819
842
  });
820
843
  return;
821
844
  }
822
- const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
845
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream, tier: "now" });
823
846
  if (acceptance === "refused")
824
847
  return;
825
848
  try {
@@ -836,7 +859,7 @@ export async function handleRuns(req, res, match, ctx) {
836
859
  return;
837
860
  }
838
861
  if (code === "steering.duplicate_input_id") {
839
- sendError(res, 409, "steering.duplicate_input_id", e instanceof Error ? e.message : "this Idempotency-Key was already accepted with different content or at a different tier — reissue with a fresh key");
862
+ sendSteerReplayRefusal(res, e);
840
863
  return;
841
864
  }
842
865
  if (code !== "steering.not_running")
@@ -1209,7 +1232,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1209
1232
  return;
1210
1233
  }
1211
1234
  const messageId = uuidv7();
1212
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1235
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle, tier: undefined });
1213
1236
  if (acceptance === "refused")
1214
1237
  return;
1215
1238
  const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
@@ -1349,33 +1372,26 @@ export async function handleRunVerbs(req, res, match, ctx) {
1349
1372
  }
1350
1373
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1351
1374
  if (!parsedBody.success) {
1352
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
1375
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1353
1376
  return;
1354
1377
  }
1355
1378
  const body = parsedBody.data;
1356
- const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
1357
- switch (settledByVerdict.kind) {
1379
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
1380
+ switch (decidedByReading.kind) {
1358
1381
  case "not_in_proof":
1359
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
1382
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
1360
1383
  return;
1361
- case "deny_only":
1362
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
1384
+ case "outside_closed_set":
1385
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
1363
1386
  return;
1364
- case "not_string":
1365
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
1366
- return;
1367
- case "too_wide":
1368
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
1369
- return;
1370
- case "absent":
1371
1387
  case "accepted":
1372
1388
  break;
1373
1389
  default: {
1374
- const unreachable = settledByVerdict;
1375
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
1390
+ const unreachable = decidedByReading;
1391
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
1376
1392
  }
1377
1393
  }
1378
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
1394
+ const decidedBy = decidedByReading.decidedBy;
1379
1395
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1380
1396
  const taskId = match.params.taskId;
1381
1397
  const askId = match.params.askId;
@@ -1501,6 +1517,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1501
1517
  sendTerminal(row0);
1502
1518
  return;
1503
1519
  }
1520
+ if (policyCannotAllow(decidedBy, body.decision === "approve", storedCardDemandsAPerson(row0.cardJson))) {
1521
+ sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
1522
+ return;
1523
+ }
1504
1524
  let outcome;
1505
1525
  const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
1506
1526
  try {
@@ -1509,7 +1529,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1509
1529
  decisionActor: actor,
1510
1530
  decisionNote: body.note ?? null,
1511
1531
  idempotencyKey: body.idempotencyKey ?? null,
1512
- ...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
1532
+ ...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
1513
1533
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1514
1534
  });
1515
1535
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 单域独占状态(lens1 §C2 的 12 条里的 5 条)全部收进 `SessionsLocal`,每个 server 实例一份:
8
8
  * SSE 连接坑计数 `sseConnections`、owner 查探许可 `sseOwnerLookups`、`/mcp` 面板的短 TTL single-flight
9
- * 缓存 `mcpStatusCache` 与它的两个时限常量。
9
+ * 缓存 `mcpStatusCache` 与它的时限常量(应答期限 `MCP_DIAL_DEADLINE_MS` 与 `POST …/mcp/reconnect` 共读,见 `../mcp-dial-deadline.ts`)。
10
10
  *
11
11
  * ⚠️ 搬运例外(仅此一处,语义等价):两个原为闭包 `let` 的计数器进了 local 盒子,于是 6 个使用点写成
12
12
  * `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。