@sema-agent/server 7.120.0 → 7.122.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 (149) hide show
  1. package/MIGRATION.md +173 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +97 -29
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  6. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  7. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  8. package/dist/approval-reconciler.d.ts +8 -0
  9. package/dist/approval-reconciler.js +72 -52
  10. package/dist/auto-mode-face.d.ts +9 -2
  11. package/dist/auto-mode-face.js +5 -3
  12. package/dist/bench/s1/live-deps.js +1 -0
  13. package/dist/boot/config-center.d.ts +5 -7
  14. package/dist/boot/config-center.js +33 -9
  15. package/dist/boot/engine-lease.d.ts +1 -1
  16. package/dist/boot/leader.js +1 -0
  17. package/dist/boot/reapers.d.ts +3 -0
  18. package/dist/boot/reapers.js +2 -1
  19. package/dist/boot/resolve-spec.js +2 -4
  20. package/dist/boot/shutdown.d.ts +9 -10
  21. package/dist/boot/shutdown.js +59 -16
  22. package/dist/boot/stage-08-reapers.js +2 -2
  23. package/dist/boot/stage-09-leader.d.ts +2 -9
  24. package/dist/boot/stage-10-http-server.d.ts +1 -9
  25. package/dist/boot/stage-10-http-server.js +31 -8
  26. package/dist/capabilities/composite-members.d.ts +1 -1
  27. package/dist/capabilities/composite-members.js +1 -1
  28. package/dist/capabilities/memory-notice.d.ts +1 -1
  29. package/dist/config-catalog.js +12 -6
  30. package/dist/config-center/center-request.d.ts +13 -1
  31. package/dist/config-center/center-request.js +25 -10
  32. package/dist/config-types.d.ts +44 -5
  33. package/dist/config-types.js +6 -0
  34. package/dist/config.d.ts +35 -6
  35. package/dist/config.js +113 -33
  36. package/dist/fleet/drain-entry.d.ts +112 -0
  37. package/dist/fleet/drain-entry.js +38 -0
  38. package/dist/fleet/engine-signal.d.ts +144 -0
  39. package/dist/fleet/engine-signal.js +60 -0
  40. package/dist/fleet/serving-lease.d.ts +47 -0
  41. package/dist/fleet/serving-lease.js +233 -0
  42. package/dist/fleet-client.d.ts +35 -19
  43. package/dist/fleet-client.js +49 -25
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +5 -7
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +33 -16
  50. package/dist/http/health-self-report.d.ts +60 -0
  51. package/dist/http/health-self-report.js +53 -0
  52. package/dist/http/principal-gate.d.ts +8 -1
  53. package/dist/http/principal-gate.js +1 -0
  54. package/dist/http/readiness.d.ts +25 -5
  55. package/dist/http/readiness.js +31 -6
  56. package/dist/http/resume-legs.js +9 -9
  57. package/dist/http/route-ctx.d.ts +25 -7
  58. package/dist/http/route-table.d.ts +6 -0
  59. package/dist/http/route-table.js +8 -1
  60. package/dist/http/routes/a2a-serve.d.ts +1 -1
  61. package/dist/http/routes/a2a-serve.js +5 -5
  62. package/dist/http/routes/admin-drain.d.ts +15 -31
  63. package/dist/http/routes/admin-drain.js +28 -10
  64. package/dist/http/routes/approvals-assistant.d.ts +4 -3
  65. package/dist/http/routes/approvals-assistant.js +9 -9
  66. package/dist/http/routes/capabilities.js +7 -5
  67. package/dist/http/routes/devices.js +3 -2
  68. package/dist/http/routes/fleet.d.ts +1 -0
  69. package/dist/http/routes/fleet.js +1 -1
  70. package/dist/http/routes/images.d.ts +1 -0
  71. package/dist/http/routes/images.js +1 -1
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +7 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +4 -3
  79. package/dist/http/routes/runs.d.ts +4 -2
  80. package/dist/http/routes/runs.js +7 -6
  81. package/dist/http/routes/sessions.d.ts +1 -0
  82. package/dist/http/routes/sessions.js +1 -1
  83. package/dist/http/routes/side-query.d.ts +1 -1
  84. package/dist/http/routes/side-query.js +1 -1
  85. package/dist/http/routes/tasks.d.ts +2 -2
  86. package/dist/http/routes/tasks.js +3 -3
  87. package/dist/http/routes/trace-usage.d.ts +1 -0
  88. package/dist/http/routes/trace-usage.js +1 -1
  89. package/dist/http/routes/workflows.d.ts +1 -0
  90. package/dist/http/routes/workflows.js +1 -1
  91. package/dist/http/send.d.ts +13 -0
  92. package/dist/http/send.js +9 -0
  93. package/dist/http/server.d.ts +10 -21
  94. package/dist/http/server.js +20 -1
  95. package/dist/http/stored-body-replay.d.ts +27 -0
  96. package/dist/http/stored-body-replay.js +23 -0
  97. package/dist/http/wire-types.d.ts +7 -1
  98. package/dist/leader/leader.d.ts +22 -1
  99. package/dist/leader/leader.js +4 -1
  100. package/dist/leader/repair-wire.d.ts +1 -1
  101. package/dist/leader/repair-wire.js +2 -2
  102. package/dist/leader/run-entry.d.ts +27 -0
  103. package/dist/leader/run-entry.js +29 -0
  104. package/dist/leader/wire.d.ts +10 -0
  105. package/dist/leader/wire.js +15 -10
  106. package/dist/model-reach-scope.d.ts +15 -3
  107. package/dist/model-reach-scope.js +3 -2
  108. package/dist/model-route-url.d.ts +13 -20
  109. package/dist/model-route-url.js +28 -24
  110. package/dist/model-unreachable-notice.d.ts +2 -1
  111. package/dist/model-unreachable-notice.js +1 -0
  112. package/dist/observability/fail-open.d.ts +35 -2
  113. package/dist/observability/fail-open.js +20 -0
  114. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  115. package/dist/plugins/approval-ask-store-file.js +3 -0
  116. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  117. package/dist/plugins/approval-ask-store-memory.js +6 -0
  118. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  119. package/dist/plugins/approval-ask-store-sql.js +4 -0
  120. package/dist/plugins/one-time-migrations.js +14 -1
  121. package/dist/plugins/park-commit-watch.d.ts +19 -5
  122. package/dist/plugins/park-commit-watch.js +24 -17
  123. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  124. package/dist/plugins/remote-env-device.js +0 -1
  125. package/dist/request-key-closure.js +1 -1
  126. package/dist/router/route-orchestration.d.ts +4 -2
  127. package/dist/router/route-orchestration.js +2 -1
  128. package/dist/rules-consent.d.ts +6 -2
  129. package/dist/rules-consent.js +3 -0
  130. package/dist/run-local.js +3 -1
  131. package/dist/runs.js +1 -1
  132. package/dist/runtime-governance.d.ts +44 -32
  133. package/dist/runtime-governance.js +23 -9
  134. package/dist/sighup-idle.d.ts +7 -1
  135. package/dist/sighup-idle.js +4 -4
  136. package/dist/task-settings.d.ts +37 -14
  137. package/dist/task-settings.js +20 -20
  138. package/dist/tool-approval.d.ts +13 -0
  139. package/dist/tool-approval.js +17 -2
  140. package/dist/trace/core-keyset-guard.d.ts +2 -2
  141. package/dist/trace/redact.d.ts +3 -3
  142. package/dist/trace/self-minted-wire.d.ts +64 -0
  143. package/dist/trace/self-minted-wire.js +30 -0
  144. package/dist/unattended-permission-mode.d.ts +17 -13
  145. package/dist/unattended-permission-mode.js +3 -1
  146. package/dist/url-shape.d.ts +31 -0
  147. package/dist/url-shape.js +41 -0
  148. package/package.json +3 -3
  149. package/scripts/drain-check-local.mjs +1 -1
@@ -7,7 +7,7 @@ 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";
9
9
  import { MAX_APPROVAL_REASON_CHARS, projectQuestionAnswer, questionAnswerFromProjection } from "../../approval-hmac.js";
10
- import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, storedBodyPermissionMode } from "../../task-settings.js";
10
+ import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, planReviewDecisionExitsPlan, storedBodyPermissionMode } from "../../task-settings.js";
11
11
  import { MAX_APPROVAL_NONCE_CHARS } from "../../plugins/approval-nonce-store.js";
12
12
  import { redactedPreview, redactSecrets } from "../../trace/redact.js";
13
13
  import { redactRiskDescriptor } from "../../observability/run-terminal.js";
@@ -59,7 +59,7 @@ const DECISION_DOOR_FAMILY = {
59
59
  "assistant-task-plan-review": {
60
60
  accepted: PLAN_REVIEW_BODY_DECLARED_KEYS,
61
61
  requestNoun: "plan-review-request",
62
- costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task read-only after you approved its plan",
62
+ costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task asking before every write after you approved its plan",
63
63
  },
64
64
  };
65
65
  const SUBMIT_BODY_NEAR_MISS_KEYS = ["permissionMode"];
@@ -534,8 +534,8 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
534
534
  sendError(res, 400, "request.field_invalid", `permissionModeAfter must be one of ${PLAN_APPROVE_PERMISSION_MODES_AFTER.map((m) => JSON.stringify(m)).join(" | ")} when present (the permission mode this task continues in once the plan is approved; absent = "default", which keeps asking before each write)`);
535
535
  return;
536
536
  }
537
- if (decision !== "approve" && body.permissionModeAfter !== undefined) {
538
- sendError(res, 400, "request.field_conflict", `permissionModeAfter is only valid with decision 'approve' (got '${decision}')`);
537
+ if (!planReviewDecisionExitsPlan(decision) && body.permissionModeAfter !== undefined) {
538
+ sendError(res, 400, "request.field_conflict", `permissionModeAfter is only valid with a decision that carries the plan out ('approve' or 'edit'; got '${decision}')`);
539
539
  return;
540
540
  }
541
541
  if (body.reason !== undefined && typeof body.reason !== "string") {
@@ -610,7 +610,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
610
610
  if (body.permissionModeAfter !== undefined) {
611
611
  const storedCtx = plan.prepared.ctx;
612
612
  if (storedCtx.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
613
- 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");
613
+ sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not started in plan mode (its stored permission mode is not \"plan\"), so approving the plan returns it to the mode it was in before planning");
614
614
  return;
615
615
  }
616
616
  }
@@ -910,16 +910,16 @@ export async function streamApprovals(req, res, cs, live, scope, governance, pol
910
910
  }
911
911
  export const APPROVALS_ASSISTANT_ROUTES = [
912
912
  { id: "approvals-list", path: "/v1/approvals", methods: ["GET"] },
913
- { id: "approvals-stream", path: "/v1/approvals/stream", methods: ["GET"] },
913
+ { id: "approvals-stream", path: "/v1/approvals/stream", methods: ["GET"], subscription: true },
914
914
  { id: "assistant-inbox", path: "/v1/assistant/inbox", methods: ["GET"] },
915
915
  { id: "assistant-tasks", path: "/v1/assistant/tasks", methods: ["GET"] },
916
916
  { id: "approval-decide", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/decide$/, label: "/v1/approvals/:id/decide", methods: ["POST"] },
917
917
  { id: "approval-exemptions-list", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["GET"] },
918
918
  { id: "approval-exemption-delete", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["DELETE"] },
919
919
  { id: "assistant-task-preempt", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/preempt$/, label: "/v1/assistant/tasks/:id/preempt", methods: ["POST"] },
920
- { id: "assistant-task-resume", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/resume$/, label: "/v1/assistant/tasks/:id/resume", billable: true, methods: ["POST"] },
921
- { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
922
- { prefix: "/v1/approvals", billable: true },
920
+ { id: "assistant-task-resume", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/resume$/, label: "/v1/assistant/tasks/:id/resume", billable: "resume", methods: ["POST"] },
921
+ { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: "hitl-answer", methods: ["POST"] },
922
+ { prefix: "/v1/approvals", billable: "hitl-answer" },
923
923
  ];
924
924
  const DECISION_DOOR_KEY_TABLE = deriveDecisionDoorKeyTable(DECISION_DOOR_FAMILY, APPROVALS_ASSISTANT_ROUTES);
925
925
  //# sourceMappingURL=approvals-assistant.js.map
@@ -4,7 +4,8 @@ import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { projectSealedKeyCapability } from "../../sealed-key.js";
5
5
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
6
6
  import { taskWriteFaceRefusal } from "../../hosted-posture.js";
7
- import { autonomyOverrides } from "../../runtime-governance.js";
7
+ import { leaderEntryRefusal, leaderRunEntry } from "../../leader/run-entry.js";
8
+ import { autonomyKeepsOpenWriteFace, autonomyModeFloor } from "../../runtime-governance.js";
8
9
  import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
9
10
  import { a2aInjectionHonored } from "../../task-a2a.js";
10
11
  import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey, buildMcpProbeInflightRefusal, buildMcpProbeRateKey, buildMcpProbeRateRefusal, createMcpProbeLocal, buildMcpProbeDeclarationRefusal, buildMcpProbeIncompleteRefusal, McpProbeIncomplete, MCP_PROBE_HTTP_STATUS, MCP_PROBE_MAX_INFLIGHT, MCP_PROBE_VERDICT_TIMEOUT_MS, createMcpProbeWalk, readMcpProbeBody, } from "../../capabilities/mcp-probe.js";
@@ -64,7 +65,7 @@ export async function handleCapabilities(req, res, match, ctx) {
64
65
  return;
65
66
  }
66
67
  const entitlementSource = centerEntitlementSourceWired(deps.config);
67
- const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), localDeny: deps.config.permissionsDisableAutoMode });
68
+ const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, floor: autonomyModeFloor(deps.config.autonomy), principal: gatedPrincipal(req, deps.config), localDeny: deps.config.permissionsDisableAutoMode });
68
69
  if (autoModeFault !== undefined)
69
70
  deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
70
71
  const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
@@ -79,10 +80,10 @@ export async function handleCapabilities(req, res, match, ctx) {
79
80
  : false,
80
81
  sessionEvents: facts.sessionEventStream(deps).available,
81
82
  fleet: facts.fleetStream(deps).available
82
- ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true }
83
+ ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true, leaseClient: true, generationEcho: true }
83
84
  : false,
84
85
  workspace: facts.workspaceBrowse().available,
85
- leader: facts.leaderLane(deps).available,
86
+ leader: facts.leaderLane(deps).available && leaderEntryRefusal(leaderRunEntry(deps.config.autonomy)) === undefined,
86
87
  images: facts.imagePool(deps).available,
87
88
  memory: false,
88
89
  memoryWrite: false,
@@ -130,6 +131,7 @@ export async function handleCapabilities(req, res, match, ctx) {
130
131
  readFace: deps.config.readFace ?? null,
131
132
  writeProtection: projectWriteProtectionCapability(deps.writeProtection),
132
133
  appendSystemPrompt: true,
134
+ outputStyle: true,
133
135
  compactionModel: true,
134
136
  subagentOutput: facts.subagentOutput(deps).available,
135
137
  subagentStream: facts.subagentOutput(deps).available,
@@ -143,7 +145,7 @@ export async function handleCapabilities(req, res, match, ctx) {
143
145
  skillToolLists: true,
144
146
  projectContext: cwdHonored(deps.config),
145
147
  callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
146
- taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
148
+ taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyKeepsOpenWriteFace(deps.config.autonomy),
147
149
  executionLane: projectExecutionLaneCapability(deps.config),
148
150
  mcpInjection: facts.mcpInjection(deps).available,
149
151
  a2a: true,
@@ -8,6 +8,7 @@ import { deviceOwnerDomainOf } from "../device-placement.js";
8
8
  import { echoKeyName } from "../../request-key-closure.js";
9
9
  const DEVICE_NOT_FOUND_MESSAGE = "device not found";
10
10
  const NO_DEVICE_DOMAIN_MESSAGE = "device management needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) and is not an explicit operator — connect through the registry auth bridge, or configure OPERATOR_PRINCIPALS";
11
+ const NO_ENROLL_DOMAIN_MESSAGE = "device enrollment needs a registry login that carries a tenant scope: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`), and an operator principal carries no scope, so it cannot enroll a device or issue an enroll token — connect through the registry auth bridge with a tenant-scoped login";
11
12
  const DEVICE_LANE_REQUIRED_MESSAGE = "device management requires the device execution lane (REMOTE_EXEC=device with a SQL store backend) — this deployment has no device lane wired";
12
13
  const _deviceRouteCodesPinned = ["not_found.device", "device.rebind_rev_conflict", "device.rebind_active_run"];
13
14
  void _deviceRouteCodesPinned;
@@ -164,7 +165,7 @@ export async function handleDevices(req, res, match, ctx) {
164
165
  }
165
166
  if (match.decl.id === "device-enroll-tokens") {
166
167
  if (domain === undefined) {
167
- sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
168
+ sendError(res, 403, "auth.forbidden", NO_ENROLL_DOMAIN_MESSAGE);
168
169
  return;
169
170
  }
170
171
  if ((await readClosedBody(req, res, readJson, [])) === null)
@@ -180,7 +181,7 @@ export async function handleDevices(req, res, match, ctx) {
180
181
  }
181
182
  if (match.decl.id === "device-enroll") {
182
183
  if (domain === undefined) {
183
- sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
184
+ sendError(res, 403, "auth.forbidden", NO_ENROLL_DOMAIN_MESSAGE);
184
185
  return;
185
186
  }
186
187
  const raw = await readClosedBody(req, res, readJson, ENROLL_BODY_KEYS);
@@ -14,6 +14,7 @@ export declare const FLEET_ROUTES: readonly [{
14
14
  readonly id: "fleet-stream";
15
15
  readonly path: "/v1/fleet/stream";
16
16
  readonly methods: readonly ["GET"];
17
+ readonly subscription: true;
17
18
  }];
18
19
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
19
20
  export type FleetRouteId = RouteIdsOf<typeof FLEET_ROUTES>;
@@ -239,6 +239,6 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
239
239
  });
240
240
  }
241
241
  export const FLEET_ROUTES = [
242
- { id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"] },
242
+ { id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"], subscription: true },
243
243
  ];
244
244
  //# sourceMappingURL=fleet.js.map
@@ -55,6 +55,7 @@ export declare const IMAGES_ROUTES: readonly [{
55
55
  readonly pattern: RegExp;
56
56
  readonly label: "/v1/images/bakes/:id/events";
57
57
  readonly methods: readonly ["GET"];
58
+ readonly subscription: true;
58
59
  }, {
59
60
  readonly id: "image-bake-cancel";
60
61
  readonly pattern: RegExp;
@@ -643,7 +643,7 @@ export const IMAGES_ROUTES = [
643
643
  { id: "image-bake-claim-next", path: "/v1/images/bakes/claim", methods: ["POST"] },
644
644
  { id: "image-select", path: "/v1/images/select", methods: ["POST"] },
645
645
  { id: "image-register", path: "/v1/images/register", methods: ["POST"] },
646
- { id: "image-bake-events", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/events$/, label: "/v1/images/bakes/:id/events", methods: ["GET"] },
646
+ { id: "image-bake-events", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/events$/, label: "/v1/images/bakes/:id/events", methods: ["GET"], subscription: true },
647
647
  { id: "image-bake-cancel", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/cancel$/, label: "/v1/images/bakes/:id/cancel", methods: ["POST"] },
648
648
  { id: "image-bake-claim", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/claim$/, label: "/v1/images/bakes/:id/claim", methods: ["POST"] },
649
649
  { id: "image-bake-ingest", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/ingest$/, label: "/v1/images/bakes/:id/ingest", methods: ["POST"] },
@@ -16,7 +16,7 @@ export declare function handleLeader(req: IncomingMessage, res: ServerResponse,
16
16
  export declare const LEADER_ROUTES: readonly [{
17
17
  readonly id: "leader-submit";
18
18
  readonly path: "/v1/leader";
19
- readonly billable: true;
19
+ readonly billable: "submit";
20
20
  readonly methods: readonly ["POST"];
21
21
  }, {
22
22
  readonly id: "leader-detail";
@@ -2,6 +2,7 @@ import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
+ import { leaderEntryRefusal, leaderRunEntry } from "../../leader/run-entry.js";
5
6
  export const LEADER_SUBMIT_PATH = "/v1/leader";
6
7
  export async function handleLeader(req, res, match, ctx) {
7
8
  switch (match.decl.id) {
@@ -35,6 +36,11 @@ export async function handleLeader(req, res, match, ctx) {
35
36
  sendError(res, 400, "request.invalid_json", "invalid JSON body");
36
37
  return;
37
38
  }
39
+ const laneRefusal = leaderEntryRefusal(leaderRunEntry(deps.config.autonomy));
40
+ if (laneRefusal !== undefined) {
41
+ sendError(res, 422, "request.autonomy_lane_refused", laneRefusal);
42
+ return;
43
+ }
38
44
  const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, requester, headerStr(req.headers["idempotency-key"]) ?? null);
39
45
  if (r) {
40
46
  sendJson(res, r.status, r.body);
@@ -51,7 +57,7 @@ export async function handleLeader(req, res, match, ctx) {
51
57
  throw new Error(`leader endpoint did not claim its own declared route: ${match.decl.id}`);
52
58
  }
53
59
  export const LEADER_ROUTES = [
54
- { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: true, methods: ["POST"] },
60
+ { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: "submit", methods: ["POST"] },
55
61
  { id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
56
62
  ];
57
63
  //# sourceMappingURL=leader.js.map
@@ -58,7 +58,7 @@ export declare function handleMemoryConsolidation(req: IncomingMessage, res: Ser
58
58
  export declare const MEMORY_CONSOLIDATION_ROUTES: readonly [{
59
59
  readonly id: "memory-consolidation-run";
60
60
  readonly path: "/v1/admin/memory/consolidation/run";
61
- readonly billable: true;
61
+ readonly billable: "submit";
62
62
  readonly methods: readonly ["POST"];
63
63
  }, {
64
64
  readonly id: "memory-consolidation-status";
@@ -163,7 +163,7 @@ export async function handleMemoryConsolidation(req, res, match, ctx) {
163
163
  }
164
164
  }
165
165
  export const MEMORY_CONSOLIDATION_ROUTES = [
166
- { id: "memory-consolidation-run", path: MEMORY_CONSOLIDATION_RUN_PATH, billable: true, methods: ["POST"] },
166
+ { id: "memory-consolidation-run", path: MEMORY_CONSOLIDATION_RUN_PATH, billable: "submit", methods: ["POST"] },
167
167
  { id: "memory-consolidation-status", path: MEMORY_CONSOLIDATION_STATUS_PATH, methods: ["GET"] },
168
168
  ];
169
169
  //# sourceMappingURL=memory-consolidation.js.map
@@ -22,7 +22,7 @@ export declare const NOTIFY_WAKE_ROUTES: readonly [{
22
22
  readonly id: "session-wake";
23
23
  readonly pattern: RegExp;
24
24
  readonly label: "/v1/sessions/:id/$verb";
25
- readonly billable: true;
25
+ readonly billable: "submit";
26
26
  readonly methods: readonly ["POST"];
27
27
  }, {
28
28
  readonly id: "session-notify";
@@ -143,7 +143,7 @@ export async function handleNotifyWake(req, res, match, ctx) {
143
143
  }
144
144
  }
145
145
  export const NOTIFY_WAKE_ROUTES = [
146
- { id: "session-wake", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/wake$/, label: "/v1/sessions/:id/$verb", billable: true, methods: ["POST"] },
146
+ { id: "session-wake", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/wake$/, label: "/v1/sessions/:id/$verb", billable: "submit", methods: ["POST"] },
147
147
  { id: "session-notify", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/notify$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
148
148
  ];
149
149
  //# sourceMappingURL=notify-wake.js.map
@@ -4,7 +4,7 @@ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sen
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import * as facts from "../../capability-facts.js";
6
6
  import { createLogger } from "../../observability/logger.js";
7
- import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
7
+ import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, buildCcImportLayerRootRefusal, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
8
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
9
9
  import { HttpError } from "../../security.js";
10
10
  import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
@@ -173,8 +173,9 @@ export async function handleRules(req, res, match, ctx) {
173
173
  sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
174
174
  return;
175
175
  }
176
- if (parsed.data.layers.some(ccImportRootRefused)) {
177
- sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
176
+ const refusedLayer = parsed.data.layers.findIndex(ccImportRootRefused);
177
+ if (refusedLayer >= 0) {
178
+ sendError(res, 400, "request.body_shape", buildCcImportLayerRootRefusal(refusedLayer));
178
179
  return;
179
180
  }
180
181
  const prepared = await lane.prepareImport(principal, parsed.data.layers);
@@ -40,7 +40,7 @@ idemKey: string | undefined): Promise<{
40
40
  export declare const RUNS_ROUTES: readonly [{
41
41
  readonly id: "runs-submit";
42
42
  readonly path: "/v1/runs";
43
- readonly billable: true;
43
+ readonly billable: "submit";
44
44
  readonly methods: readonly ["POST"];
45
45
  }, {
46
46
  readonly id: "run-steer";
@@ -72,6 +72,7 @@ export declare const RUNS_ROUTES: readonly [{
72
72
  readonly pattern: RegExp;
73
73
  readonly label: "/v1/runs/:id/events";
74
74
  readonly methods: readonly ["GET"];
75
+ readonly subscription: true;
75
76
  }, {
76
77
  readonly id: "run-detail";
77
78
  readonly pattern: RegExp;
@@ -90,6 +91,7 @@ export declare const RUN_VERBS_ROUTES: readonly [{
90
91
  readonly pattern: RegExp;
91
92
  readonly label: "/v1/runs/:id/subagents/:sub/*";
92
93
  readonly methods: readonly ["GET"];
94
+ readonly subscription: true;
93
95
  }, {
94
96
  readonly id: "run-subagent-steer";
95
97
  readonly pattern: RegExp;
@@ -99,7 +101,7 @@ export declare const RUN_VERBS_ROUTES: readonly [{
99
101
  readonly id: "run-subagent-resume";
100
102
  readonly pattern: RegExp;
101
103
  readonly label: "/v1/runs/:id/subagents/:sub/*";
102
- readonly billable: true;
104
+ readonly billable: "resume";
103
105
  readonly methods: readonly ["POST"];
104
106
  }, {
105
107
  readonly id: "run-task-output";
@@ -33,7 +33,8 @@ import { mintAndDriveDurableRun } from "../durable-run-leg.js";
33
33
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
34
34
  import { withPrincipal } from "../../observability/principal-context.js";
35
35
  import { readResumeCtx } from "../resume-ctx-read.js";
36
- import { storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "../checkpoint-ctx.js";
36
+ import { storedBodyReplayAnswer, storedBodyReplayLogFields } from "../checkpoint-ctx.js";
37
+ import { storedBodyReplayRefusal } from "../stored-body-replay.js";
37
38
  const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
38
39
  const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
39
40
  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]}`;
@@ -647,7 +648,7 @@ export async function handleRuns(req, res, match, ctx) {
647
648
  mintResult.noResumeCtx = true;
648
649
  return;
649
650
  }
650
- const unservable = storedBodyReplayRefusal(resumeRead.ctx);
651
+ const unservable = storedBodyReplayRefusal(resumeRead.ctx, deps.config.autonomy);
651
652
  if (unservable !== undefined) {
652
653
  mintResult.unservable = unservable;
653
654
  return;
@@ -1656,20 +1657,20 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1656
1657
  return resp;
1657
1658
  }
1658
1659
  export const RUNS_ROUTES = [
1659
- { id: "runs-submit", path: "/v1/runs", billable: true, methods: ["POST"] },
1660
+ { id: "runs-submit", path: "/v1/runs", billable: "submit", methods: ["POST"] },
1660
1661
  { id: "run-steer", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/steer$/, label: "/v1/runs/:id/steer", methods: ["POST"] },
1661
1662
  { id: "run-interrupt", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/interrupt$/, label: "/v1/runs/:id/interrupt", methods: ["POST"] },
1662
1663
  { id: "run-detach", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/detach$/, label: "/v1/runs/:id/detach", methods: ["POST"] },
1663
1664
  { id: "run-cancel", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/cancel$/, label: "/v1/runs/:id/cancel", methods: ["POST"] },
1664
1665
  { id: "run-compact", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/compact$/, label: "/v1/runs/:id/compact", methods: ["POST"] },
1665
- { id: "run-events", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/events$/, label: "/v1/runs/:id/events", methods: ["GET"] },
1666
+ { id: "run-events", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/events$/, label: "/v1/runs/:id/events", methods: ["GET"], subscription: true },
1666
1667
  { id: "run-detail", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)$/, label: "/v1/runs/:id", methods: ["GET"] },
1667
1668
  ];
1668
1669
  export const RUN_VERBS_ROUTES = [
1669
1670
  { id: "run-subagent-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/output$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1670
- { id: "run-subagent-stream", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/stream$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1671
+ { id: "run-subagent-stream", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/stream$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"], subscription: true },
1671
1672
  { id: "run-subagent-steer", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/steer$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["POST"] },
1672
- { id: "run-subagent-resume", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/resume$/, label: "/v1/runs/:id/subagents/:sub/*", billable: true, methods: ["POST"] },
1673
+ { id: "run-subagent-resume", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/resume$/, label: "/v1/runs/:id/subagents/:sub/*", billable: "resume", methods: ["POST"] },
1673
1674
  { id: "run-task-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/output$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["GET"] },
1674
1675
  { id: "run-task-stop", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/stop$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["POST"] },
1675
1676
  { id: "elicitation-respond", pattern: /^\/v1\/elicitations\/(?<id>[^/]+)\/respond$/, label: "/v1/elicitations/:id/respond", methods: ["POST"] },
@@ -79,6 +79,7 @@ export declare const SESSIONS_ROUTES: readonly [{
79
79
  readonly pattern: RegExp;
80
80
  readonly label: "/v1/sessions/:id/$verb";
81
81
  readonly methods: readonly ["GET"];
82
+ readonly subscription: true;
82
83
  }, {
83
84
  readonly id: "session-fork";
84
85
  readonly pattern: RegExp;
@@ -844,7 +844,7 @@ export const SESSIONS_ROUTES = [
844
844
  { id: "session-mcp", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/mcp$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
845
845
  { id: "session-a2a", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/a2a$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
846
846
  { id: "session-head", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/head$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
847
- { id: "session-events", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/events$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
847
+ { id: "session-events", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/events$/, label: "/v1/sessions/:id/$verb", methods: ["GET"], subscription: true },
848
848
  { id: "session-fork", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/fork$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
849
849
  { id: "session-policy-get", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/policy$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
850
850
  { id: "session-policy-put", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/policy$/, label: "/v1/sessions/:id/$verb", methods: ["PUT"] },
@@ -17,7 +17,7 @@ export declare function handleSideQuery(req: IncomingMessage, res: ServerRespons
17
17
  export declare const SIDE_QUERY_ROUTES: readonly [{
18
18
  readonly id: "side-query";
19
19
  readonly path: "/v1/side-query";
20
- readonly billable: true;
20
+ readonly billable: "submit";
21
21
  readonly methods: readonly ["POST"];
22
22
  }];
23
23
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
@@ -133,6 +133,6 @@ export async function handleSideQuery(req, res, match, ctx) {
133
133
  }
134
134
  }
135
135
  export const SIDE_QUERY_ROUTES = [
136
- { id: "side-query", path: "/v1/side-query", billable: true, methods: ["POST"] },
136
+ { id: "side-query", path: "/v1/side-query", billable: "submit", methods: ["POST"] },
137
137
  ];
138
138
  //# sourceMappingURL=side-query.js.map
@@ -25,12 +25,12 @@ export declare function handleTasks(req: IncomingMessage, res: ServerResponse, m
25
25
  export declare const TASKS_ROUTES: readonly [{
26
26
  readonly id: "tasks-submit";
27
27
  readonly path: "/v1/tasks";
28
- readonly billable: true;
28
+ readonly billable: "submit";
29
29
  readonly methods: readonly ["POST"];
30
30
  }, {
31
31
  readonly id: "tasks-submit-stream";
32
32
  readonly path: "/v1/tasks/stream";
33
- readonly billable: true;
33
+ readonly billable: "submit";
34
34
  readonly methods: readonly ["POST"];
35
35
  }];
36
36
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
@@ -639,7 +639,7 @@ export async function handleTasks(req, res, match, ctx) {
639
639
  });
640
640
  }
641
641
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
642
- await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
642
+ await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope({ sessionId: prepared.spec.sessionId, taskId: durableTaskId }, () => deps.sendUserFile
643
643
  ? deps.sendUserFile.runWithContext({
644
644
  taskId: askTaskId,
645
645
  emit: async (f) => {
@@ -901,7 +901,7 @@ export async function handleTasks(req, res, match, ctx) {
901
901
  }
902
902
  }
903
903
  export const TASKS_ROUTES = [
904
- { id: "tasks-submit", path: "/v1/tasks", billable: true, methods: ["POST"] },
905
- { id: "tasks-submit-stream", path: "/v1/tasks/stream", billable: true, methods: ["POST"] },
904
+ { id: "tasks-submit", path: "/v1/tasks", billable: "submit", methods: ["POST"] },
905
+ { id: "tasks-submit-stream", path: "/v1/tasks/stream", billable: "submit", methods: ["POST"] },
906
906
  ];
907
907
  //# sourceMappingURL=tasks.js.map
@@ -38,6 +38,7 @@ export declare const TRACE_USAGE_ROUTES: readonly [{
38
38
  readonly pattern: RegExp;
39
39
  readonly label: "/v1/tasks/:id/$sub";
40
40
  readonly methods: readonly ["GET"];
41
+ readonly subscription: true;
41
42
  }, {
42
43
  readonly id: "task-trace-artifacts";
43
44
  readonly pattern: RegExp;
@@ -302,7 +302,7 @@ export const TRACE_USAGE_ROUTES = [
302
302
  { id: "task-trace-list", path: "/v1/tasks", methods: ["GET"] },
303
303
  { id: "task-tool-result", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/tool-results\/(?<ref>[^/]+)$/, label: "/v1/tasks/:id/tool-results/:ref", methods: ["GET"] },
304
304
  { id: "task-trace-turns", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/turns$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
305
- { id: "task-trace-stream", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/stream$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
305
+ { id: "task-trace-stream", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/stream$/, label: "/v1/tasks/:id/$sub", methods: ["GET"], subscription: true },
306
306
  { id: "task-trace-artifacts", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/artifacts$/, label: "/v1/tasks/:id/$sub", methods: ["GET"] },
307
307
  { id: "usage-analytics", pattern: /^\/v1\/usage\/(?<sub>summary|series|breakdown)$/, label: "/v1/usage/$sub", methods: ["GET"] },
308
308
  ];
@@ -15,6 +15,7 @@ export declare const WORKFLOWS_ROUTES: readonly [{
15
15
  readonly pattern: RegExp;
16
16
  readonly label: "/v1/workflows/:id/$sub";
17
17
  readonly methods: readonly ["GET"];
18
+ readonly subscription: true;
18
19
  }, {
19
20
  readonly id: "workflow-journal";
20
21
  readonly pattern: RegExp;
@@ -416,7 +416,7 @@ function sendWorkflowsUnavailable(res, why) {
416
416
  }
417
417
  export const WORKFLOWS_ROUTES = [
418
418
  { id: "workflows-list", path: "/v1/workflows", methods: ["GET"] },
419
- { id: "workflow-stream", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/stream$/, label: "/v1/workflows/:id/$sub", methods: ["GET"] },
419
+ { id: "workflow-stream", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/stream$/, label: "/v1/workflows/:id/$sub", methods: ["GET"], subscription: true },
420
420
  { id: "workflow-journal", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/journal$/, label: "/v1/workflows/:id/$sub", methods: ["GET"] },
421
421
  { id: "workflow-detail", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)$/, label: "/v1/workflows/:id", methods: ["GET"] },
422
422
  ];
@@ -47,6 +47,19 @@ export declare const SSE_MAX_STREAM_MS: number;
47
47
  * 要么给 approvals 减 type=消费端破坏),属候裁,不在本批的零行为码范围内。
48
48
  */
49
49
  export declare const SSE_HEARTBEAT_IDLE_MS = 15000;
50
+ /**
51
+ * S-1281 —— 服务端**主动**收一条已开的 SSE 流:一帧 `event: error`(帧形与流帽那一帧同一套 `{type:"error", errorCode, message}`,
52
+ * 消费端按既有「服务端收流 ⇒ 重连」协议处置,SDK 的流读口把它当一帧事件交出再重连)后 `end()`。
53
+ *
54
+ * 收流的是分派器,不是流自己的泵:泵此刻可能正停在一次 await 上,醒来还会写一帧。Node 上 `end()` 之后、`close` 之前的那一写会发一个
55
+ * `error` 事件(实测 v24.2.0:无监听 ⇒ 进程级未捕获;`close` 之后的写只回 `false`,不再发)。⇒ 先挂一只**只吸收这一种**
56
+ * (`ERR_STREAM_WRITE_AFTER_END`)的监听再 `end()`:那一帧本来就无处可去(连接是我们关的),别的错照旧抛(不改既有失败形)。
57
+ * 已收尾的流不碰(幂等;排空入口与硬停起点各调一次)。
58
+ */
59
+ export declare function endSseWithError(res: ServerResponse, frame: {
60
+ readonly errorCode: string;
61
+ readonly message: string;
62
+ }): void;
50
63
  export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
51
64
  /** A6([ref] lens1 §C3/lens3 §C3a): canonical error-response sender for sites that already carry a
52
65
  * machine `errorCode` — `errorCode` is a required positional arg (not optional-bag) so a call site
package/dist/http/send.js CHANGED
@@ -17,6 +17,15 @@ export function sseHeaders(res, extra) {
17
17
  }
18
18
  export const SSE_MAX_STREAM_MS = 15 * 60 * 1000;
19
19
  export const SSE_HEARTBEAT_IDLE_MS = 15_000;
20
+ export function endSseWithError(res, frame) {
21
+ if (res.writableEnded || res.destroyed)
22
+ return;
23
+ res.on("error", (err) => {
24
+ if (err.code !== "ERR_STREAM_WRITE_AFTER_END")
25
+ throw err;
26
+ });
27
+ res.end(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: frame.errorCode, message: frame.message })}\n\n`);
28
+ }
20
29
  export function sendJson(res, status, body) {
21
30
  res.writeHead(status, { "content-type": "application/json" });
22
31
  res.end(JSON.stringify(body));
@@ -649,8 +649,6 @@ export interface ServiceGovernanceDeps {
649
649
  * 「诊断说亮、位说暗」。缺席(桩/一次性 CLI)⇒ 读面如实答 `deployment_incapable`。 */
650
650
  autoModeFace?: import("../auto-mode-face.js").AutoModeFaceDeps;
651
651
  }
652
- /** **部署自述面**:只被 /health、/v1/capabilities 与提交前置门读的部署事实(多为 live getter,
653
- * 不是快照——/health 必须回答「此刻」)。 */
654
652
  export interface ServiceDeploymentDeps {
655
653
  /** Stable per-process id recorded on runs (audit / stale recovery). */
656
654
  instanceId?: string;
@@ -686,16 +684,14 @@ export interface ServiceDeploymentDeps {
686
684
  * • [ref]:`attachedStreams` 同样 ASSIGNED BY createServer —— 本副本当前打开着的 SSE 流条数
687
685
  * (= 引擎附着租约集的大小)。消费方是 `boot/engine-lease.ts` 的自退判据;与 `inflight` 同族,
688
686
  * 同一个共享盒子,不另开第二条 main↔server 的通道。
687
+ * • S-1281:`inflight` 只数**工作** —— 只读订阅流(路由表 `subscription` 列的八条 GET SSE 读面)不进 admission 计数,只进
688
+ * `attachedStreams`;`endSubscriptions`(同样 ASSIGNED BY createServer)发 `draining` 终帧后关掉它们,收尾链在排空进入那一拍与
689
+ * 硬停起点各调一次。
689
690
  */
690
- drainState?: {
691
- draining: boolean;
692
- since?: number;
693
- reason?: string;
694
- inflight?: () => number;
695
- lastActivityAt?: () => number;
696
- attachedStreams?: () => number;
697
- lastAttachEndedAt?: () => number;
698
- };
691
+ drainState?: import("../fleet/drain-entry.js").DrainState;
692
+ /** 编排器片 2 件 3:服务租约此刻的读数(`fleet/serving-lease.ts` 的客户端交出;引擎信号铸点与计费准入判同读)。缺席 = 没装租约客户端
693
+ * (非 fleet 成员 / 没接中心 / 测试夹具)⇒ `none`(与没有租约的引擎逐字同)。 */
694
+ servingLease?: () => import("../fleet/engine-signal.js").ServingLeaseView;
699
695
  /** boot ready 门(b):false = 布防(`CONFIG_REQUIRE_ROSTER=true`)且首次 effective pull 尚未落 roster
700
696
  * (worker 只有占位模型)。就绪原因 `model_roster_pending`(`http/readiness.ts`):计费提交 503 + `/readyz` 503 +
701
697
  * /health 加性 `ready:false`。absent = 恒 ready(未布防 / 非 registry 部署),现有部署零影响。config-center 的
@@ -722,6 +718,9 @@ export interface ServiceDeploymentDeps {
722
718
  * (`boot/stage-10-http-server.ts` → `modelRefusals: () => deployment.modelRefusalsState?.()`)—— 一处铸、两出口。缺席 / 返回 undefined =
723
719
  * 没有可报的拒或没有配置管道 ⇒ `/health` 形状逐字不变(additive)。 */
724
720
  modelRefusalsState?: () => import("@sema-agent/settings-schema").ModelRefusalsSignal | undefined;
721
+ /** `/health.configHandoff`(只上 `/health`):在场 ⇔ 部署声明了耐久接力 ∧ 应用着的那一代没被确认(config-center 接力账的 `configHandoffState()`;合成律
722
+ * `fleet/engine-signal.ts` 的 `buildConfigHandoff`)。缺席 / 返回 undefined = 有接力、没有中心或本地腿 ⇒ 键缺席。 */
723
+ configHandoffState?: () => import("../fleet/engine-signal.js").ConfigHandoff | undefined;
725
724
  /** [ref] 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。
726
725
  * 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */
727
726
  configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">;
@@ -739,16 +738,6 @@ export interface ServiceDeploymentDeps {
739
738
  mcpRevocationState?: () => {
740
739
  revoked: string[];
741
740
  } | undefined;
742
- /** codex R18: the persistent "retained WITHOUT a next-boot handoff" state — a candidate whose restart signal is
743
- * withheld because no restart-surviving handoff exists yet (durable-declared LKG publication incomplete, or a
744
- * boot skills debt not yet proven repairable). Surfaced on /health (`modelPlaneDeferred` — the key keeps its birth
745
- * name: it was born for the model-plane deferral, which retired with the `models-tiers` slice in S-668 波 0) so
746
- * the stuck state outlives the one-shot warn (the etag advances → later ticks 304). */
747
- planeDeferredState?: () => {
748
- version: number;
749
- since: number;
750
- blocked?: string[];
751
- } | undefined;
752
741
  /** B1(鲁棒性批3 设计件):后台**缓存**探针的读座——/health 绝不逐请求打 DB(center/k8s 高频面),
753
742
  * main.ts 只在 SQL 后端(backend.kind !== "local")挂环(createStoreLiveProbe,STORE_PROBE_INTERVAL_MS)。
754
743
  * 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在