@sema-agent/server 7.145.0 → 7.146.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 (275) hide show
  1. package/MIGRATION.md +492 -0
  2. package/USAGE.md +218 -47
  3. package/deploy/facts/CHANGES.json +25 -0
  4. package/deploy/facts/README.md +57 -0
  5. package/deploy/facts/lanes.json +54 -0
  6. package/deploy/facts/lanes.schema.json +57 -0
  7. package/deploy/facts/sandbox-rbac.json +113 -0
  8. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  9. package/deploy/facts/sealed-key.json +9 -0
  10. package/deploy/facts/sealed-key.schema.json +52 -0
  11. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  12. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  13. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  14. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  15. package/deploy/sema-up/chart/values.yaml +7 -0
  16. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  17. package/dist/adoption/import-limits.d.ts +6 -0
  18. package/dist/adoption/import-limits.js +7 -0
  19. package/dist/adoption/import.d.ts +62 -0
  20. package/dist/adoption/import.js +107 -0
  21. package/dist/adoption/wire.d.ts +52 -0
  22. package/dist/adoption/wire.js +9 -0
  23. package/dist/approval-ask-machine.d.ts +2 -2
  24. package/dist/approval-card.d.ts +3 -12
  25. package/dist/approval-card.js +0 -4
  26. package/dist/approval-gate-currency.d.ts +1 -1
  27. package/dist/approval-gate-currency.js +1 -1
  28. package/dist/approval-reconciler.d.ts +1 -1
  29. package/dist/boot/deployment-mods.d.ts +11 -1
  30. package/dist/boot/deployment-mods.js +15 -2
  31. package/dist/boot/device-lane.js +2 -0
  32. package/dist/boot/engine-lease.d.ts +193 -21
  33. package/dist/boot/engine-lease.js +146 -20
  34. package/dist/boot/execution-env.d.ts +4 -0
  35. package/dist/boot/execution-env.js +12 -0
  36. package/dist/boot/exit-boundary.d.ts +50 -0
  37. package/dist/boot/exit-boundary.js +32 -0
  38. package/dist/boot/exit-last.d.ts +20 -4
  39. package/dist/boot/exit-last.js +21 -4
  40. package/dist/boot/governance-seams.js +1 -0
  41. package/dist/boot/memory-consolidation.d.ts +29 -17
  42. package/dist/boot/memory-consolidation.js +26 -15
  43. package/dist/boot/reapers.d.ts +12 -4
  44. package/dist/boot/reapers.js +39 -15
  45. package/dist/boot/resolve-spec.d.ts +2 -0
  46. package/dist/boot/resolve-spec.js +38 -15
  47. package/dist/boot/runner-deps.js +1 -1
  48. package/dist/boot/session-faces.js +3 -1
  49. package/dist/boot/shutdown.js +27 -9
  50. package/dist/boot/stage-01-config.js +1 -1
  51. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  52. package/dist/boot/stage-03-store-backend.js +2 -2
  53. package/dist/boot/stage-04-stores.d.ts +1 -0
  54. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  55. package/dist/boot/stage-05-execution-env.js +3 -3
  56. package/dist/boot/stage-06-runners.d.ts +3 -0
  57. package/dist/boot/stage-06-runners.js +11 -10
  58. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  59. package/dist/boot/stage-07-capability-layer.js +7 -2
  60. package/dist/boot/stage-08-reapers.d.ts +3 -0
  61. package/dist/boot/stage-08-reapers.js +3 -3
  62. package/dist/boot/stage-09-leader.d.ts +3 -0
  63. package/dist/boot/stage-10-http-server.d.ts +3 -0
  64. package/dist/boot/stage-10-http-server.js +3 -2
  65. package/dist/boot/stores.d.ts +1 -0
  66. package/dist/boot/stores.js +35 -19
  67. package/dist/boot/workflow-orchestration.d.ts +1 -1
  68. package/dist/boot/workflow-orchestration.js +2 -1
  69. package/dist/capabilities/composite-members.d.ts +3 -1
  70. package/dist/capabilities/composite-members.js +3 -1
  71. package/dist/capabilities/scenarios.d.ts +2 -2
  72. package/dist/capability-facts.d.ts +18 -1
  73. package/dist/capability-facts.js +25 -7
  74. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  75. package/dist/config-catalog.js +13 -4
  76. package/dist/config-provider.d.ts +2 -2
  77. package/dist/config-types.d.ts +43 -4
  78. package/dist/config.d.ts +6 -1
  79. package/dist/config.js +67 -11
  80. package/dist/deleted-session-ids.d.ts +19 -0
  81. package/dist/deleted-session-ids.js +26 -0
  82. package/dist/deploy-facts.d.ts +155 -0
  83. package/dist/deploy-facts.js +94 -0
  84. package/dist/device-ws-hub.d.ts +48 -3
  85. package/dist/device-ws-hub.js +48 -2
  86. package/dist/error-code-family.d.ts +35 -0
  87. package/dist/error-code-family.js +26 -0
  88. package/dist/fleet/drain-entry.d.ts +39 -3
  89. package/dist/fleet/drain-entry.js +6 -0
  90. package/dist/fleet/fleet-bus.d.ts +2 -1
  91. package/dist/hooks/branch-transcript.d.ts +1 -1
  92. package/dist/hooks/branch-transcript.js +1 -0
  93. package/dist/hooks/classic-stations.d.ts +25 -8
  94. package/dist/hooks/classic-stations.js +29 -3
  95. package/dist/hooks/hook-runner.d.ts +33 -16
  96. package/dist/hooks/hook-runner.js +166 -73
  97. package/dist/hooks/hook-source.d.ts +14 -2
  98. package/dist/hooks/hook-source.js +8 -0
  99. package/dist/http/active-run-conflict.d.ts +1 -1
  100. package/dist/http/active-run-conflict.js +1 -1
  101. package/dist/http/admission.js +11 -1
  102. package/dist/http/checkpoint-ctx.d.ts +59 -71
  103. package/dist/http/checkpoint-ctx.js +34 -40
  104. package/dist/http/dispatch.js +18 -2
  105. package/dist/http/durable-run-leg.d.ts +5 -0
  106. package/dist/http/durable-run-leg.js +1 -0
  107. package/dist/http/execution-context.d.ts +4 -0
  108. package/dist/http/execution-context.js +9 -0
  109. package/dist/http/health-self-report.d.ts +6 -2
  110. package/dist/http/health-self-report.js +3 -2
  111. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  112. package/dist/http/memory-compliance-failure-reply.js +2 -15
  113. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  114. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  115. package/dist/http/principal-gate.d.ts +35 -12
  116. package/dist/http/principal-gate.js +16 -12
  117. package/dist/http/request-guards.d.ts +1 -1
  118. package/dist/http/request-guards.js +5 -5
  119. package/dist/http/resume-legs.d.ts +22 -0
  120. package/dist/http/resume-legs.js +26 -17
  121. package/dist/http/route-ctx.d.ts +11 -10
  122. package/dist/http/route-table.js +2 -0
  123. package/dist/http/routes/adoption.d.ts +11 -0
  124. package/dist/http/routes/adoption.js +80 -6
  125. package/dist/http/routes/agents-roster.js +1 -0
  126. package/dist/http/routes/approvals-assistant.js +2 -1
  127. package/dist/http/routes/background.js +1 -0
  128. package/dist/http/routes/capabilities.js +4 -1
  129. package/dist/http/routes/diagnostics.d.ts +9 -14
  130. package/dist/http/routes/diagnostics.js +10 -20
  131. package/dist/http/routes/engine.d.ts +31 -0
  132. package/dist/http/routes/engine.js +81 -0
  133. package/dist/http/routes/memory-bundle.js +3 -0
  134. package/dist/http/routes/memory-consolidation.js +3 -5
  135. package/dist/http/routes/memory-origin.d.ts +1 -1
  136. package/dist/http/routes/memory-origin.js +2 -4
  137. package/dist/http/routes/memory-policy.js +5 -1
  138. package/dist/http/routes/observability.js +3 -6
  139. package/dist/http/routes/runs.js +7 -3
  140. package/dist/http/routes/session-sync.js +12 -25
  141. package/dist/http/routes/sessions-list.d.ts +1 -1
  142. package/dist/http/routes/sessions.js +4 -0
  143. package/dist/http/routes/tasks.js +23 -5
  144. package/dist/http/routes/trace-usage.js +1 -1
  145. package/dist/http/run-concurrency-cap.d.ts +86 -0
  146. package/dist/http/run-concurrency-cap.js +54 -0
  147. package/dist/http/server.d.ts +15 -12
  148. package/dist/http/server.js +18 -3
  149. package/dist/http/stored-body-replay.d.ts +9 -6
  150. package/dist/http/stored-body-replay.js +19 -5
  151. package/dist/http/wire-types.d.ts +8 -3
  152. package/dist/json-body-limit.d.ts +8 -0
  153. package/dist/json-body-limit.js +2 -0
  154. package/dist/leader/diffout.d.ts +1 -1
  155. package/dist/local-data-root.d.ts +2 -2
  156. package/dist/memory-operator-faces.d.ts +9 -4
  157. package/dist/memory-scope-stores.d.ts +9 -7
  158. package/dist/memory-scope-stores.js +3 -14
  159. package/dist/memory-scope.d.ts +36 -0
  160. package/dist/memory-scope.js +32 -1
  161. package/dist/net-address.d.ts +53 -0
  162. package/dist/net-address.js +111 -0
  163. package/dist/observability/fail-open.d.ts +14 -10
  164. package/dist/observability/fail-open.js +14 -10
  165. package/dist/orchestration/workflow-notify-journal.js +2 -3
  166. package/dist/org-delivery-gate.js +1 -1
  167. package/dist/peer-directory.d.ts +6 -12
  168. package/dist/peer-directory.js +5 -1
  169. package/dist/plugins/caching-session-store.d.ts +3 -1
  170. package/dist/plugins/caching-session-store.js +3 -0
  171. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  172. package/dist/plugins/checkpoint-store-face.js +152 -0
  173. package/dist/plugins/file-run-store.d.ts +0 -6
  174. package/dist/plugins/file-run-store.js +1 -12
  175. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  176. package/dist/plugins/fork-routing-session-store.js +2 -2
  177. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  178. package/dist/plugins/local-checkpoint-store.d.ts +2 -2
  179. package/dist/plugins/local-checkpoint-store.js +2 -1
  180. package/dist/plugins/local-session-store.d.ts +28 -5
  181. package/dist/plugins/local-session-store.js +32 -8
  182. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  183. package/dist/plugins/memory-engine-pg.js +96 -214
  184. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  185. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  186. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  187. package/dist/plugins/memory-engine-tidb.js +97 -227
  188. package/dist/plugins/memory-key-guards.d.ts +1 -1
  189. package/dist/plugins/memory-origin-law.d.ts +3 -3
  190. package/dist/plugins/one-time-migrations.d.ts +32 -56
  191. package/dist/plugins/one-time-migrations.js +25 -117
  192. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  193. package/dist/plugins/permission-rule-store-file.js +8 -1
  194. package/dist/plugins/pg-pool.js +1 -1
  195. package/dist/plugins/pg-session-storage.d.ts +17 -4
  196. package/dist/plugins/pg-session-storage.js +8 -4
  197. package/dist/plugins/run-store-sql.js +2 -2
  198. package/dist/plugins/schema-ensure-legs.js +2 -2
  199. package/dist/plugins/session-fork.d.ts +31 -9
  200. package/dist/plugins/session-fork.js +3 -2
  201. package/dist/plugins/session-placement.d.ts +4 -2
  202. package/dist/plugins/sql-json-column.d.ts +5 -1
  203. package/dist/plugins/store-backend.d.ts +11 -8
  204. package/dist/plugins/store-backend.js +8 -7
  205. package/dist/plugins/store-contracts.d.ts +1 -11
  206. package/dist/plugins/store-contracts.js +0 -9
  207. package/dist/plugins/store-sweep.d.ts +63 -0
  208. package/dist/plugins/store-sweep.js +48 -0
  209. package/dist/plugins/tidb-pool.js +1 -1
  210. package/dist/plugins/tidb-session-store.d.ts +16 -3
  211. package/dist/plugins/tidb-session-store.js +8 -4
  212. package/dist/prompt-screen.d.ts +6 -9
  213. package/dist/prompt-screen.js +2 -5
  214. package/dist/rules-consent.d.ts +20 -27
  215. package/dist/rules-consent.js +82 -6
  216. package/dist/run-cancel-context.d.ts +4 -1
  217. package/dist/run-cancel-context.js +2 -1
  218. package/dist/runs.js +1 -1
  219. package/dist/sandbox-egress-policy.js +6 -11
  220. package/dist/sandbox-rbac-rules.d.ts +88 -0
  221. package/dist/sandbox-rbac-rules.js +80 -0
  222. package/dist/sealed-key-layout.d.ts +17 -0
  223. package/dist/sealed-key-layout.js +4 -0
  224. package/dist/sealed-key.js +5 -5
  225. package/dist/security.d.ts +38 -6
  226. package/dist/security.js +9 -2
  227. package/dist/session-notify-door.d.ts +5 -3
  228. package/dist/session-resident-background.d.ts +57 -0
  229. package/dist/session-resident-background.js +20 -0
  230. package/dist/session-sync-kernel.d.ts +53 -14
  231. package/dist/session-sync-kernel.js +70 -1
  232. package/dist/session-sync.d.ts +5 -3
  233. package/dist/session-workspace.d.ts +1 -0
  234. package/dist/session-workspace.js +1 -0
  235. package/dist/sighup-idle.d.ts +9 -7
  236. package/dist/sighup-idle.js +4 -5
  237. package/dist/spec-fields.d.ts +16 -0
  238. package/dist/spec-fields.js +7 -0
  239. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  240. package/dist/stored-request-env-credentials-m38.js +8 -0
  241. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  242. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +13 -427
  243. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +33 -194
  244. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  245. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  246. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  247. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  248. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  249. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  250. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  251. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  252. package/dist/task-settings.d.ts +59 -4
  253. package/dist/task-settings.js +50 -4
  254. package/dist/tool-approval.d.ts +2 -2
  255. package/dist/trace/core-keyset-guard.d.ts +8 -7
  256. package/dist/trace/engine-notice-wire.d.ts +1 -1
  257. package/dist/trace/engine-notice-wire.js +2 -0
  258. package/dist/trace/exit-seat.d.ts +6 -0
  259. package/dist/trace/exit-seat.js +1 -0
  260. package/dist/trace/project.js +9 -0
  261. package/dist/trace/redact.d.ts +13 -2
  262. package/dist/trace/redact.js +305 -24
  263. package/dist/trace/self-minted-wire.d.ts +326 -8
  264. package/dist/trace/self-minted-wire.js +125 -7
  265. package/dist/trace/wire-projection-faces.d.ts +1 -1
  266. package/dist/trace/wire-projection-faces.js +1 -1
  267. package/dist/usage-analytics.d.ts +4 -0
  268. package/package.json +12 -4
  269. package/scripts/drain-check-local.mjs +8 -29
  270. package/scripts/drain-check.mysql.sql +3 -24
  271. package/scripts/drain-check.pg.sql +3 -24
  272. package/skills/update-config.md +1 -1
  273. package/wire-changes/server@7.146.0.json +3221 -0
  274. package/dist/approval-batch-census.d.ts +0 -27
  275. package/dist/approval-batch-census.js +0 -27
@@ -1,10 +1,9 @@
1
1
  import { once } from "node:events";
2
- import { randomBytes } from "node:crypto";
3
- import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
2
+ import { sessionLogDigest, sessionLogDigestsComparable, validateFileHistoryExport } from "@sema-agent/core";
4
3
  import { StringDecoder } from "node:string_decoder";
5
4
  import { FileHistoryUnreadableError, blobGetterOf, exportSessionManifest, exportedFileHistory, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
6
5
  import * as facts from "../../capability-facts.js";
7
- import { classifySyncRelationshipByIds, readImportAnchors, SyncConflictError } from "../../session-sync-kernel.js";
6
+ import { classifySyncRelationshipByIds, createStagedLogFeed, MalformedImportEntryError, newStagingToken, readImportAnchors, SyncConflictError } from "../../session-sync-kernel.js";
8
7
  import { isUuidShape, legacyUnownedSessionOk, sessionOwnerCellOf } from "../../security.js";
9
8
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, logRequestError, msg } from "../send.js";
10
9
  import { StampedSessionArchiveError } from "../../session-error-wire.js";
@@ -14,7 +13,6 @@ import { storeFaultClassOf } from "../../plugins/store-fault.js";
14
13
  import { fleetViewOf, gatedPrincipal, explicitOperatorOk, sendUnauthorized, sessionCallerOf } from "../principal-gate.js";
15
14
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
16
15
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
17
- const SYNC_IMPORT_BATCH = 500;
18
16
  import { SHA256_HEX_RE } from "../../digest-form.js";
19
17
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
20
18
  import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
@@ -501,7 +499,7 @@ export async function handleSessionSync(req, res, match, ctx) {
501
499
  }
502
500
  const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
503
501
  const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config);
504
- const token = randomBytes(8).toString("hex");
502
+ const token = newStagingToken();
505
503
  const handle = sessionStorage.beginImportStaging(sessionId, token);
506
504
  const stagingId = handle.stagingId;
507
505
  importLeases.set(sessionId, { at: Date.now(), stagingId });
@@ -527,13 +525,8 @@ export async function handleSessionSync(req, res, match, ctx) {
527
525
  sendError(res, 404, "not_found.staging", "staging not found");
528
526
  return;
529
527
  }
530
- const validator = new StreamingImportValidator();
528
+ const feed = createStagedLogFeed(pending.handle);
531
529
  let lineBuf = "";
532
- let pendingBatch = [];
533
- const flush = async () => { if (pendingBatch.length) {
534
- await pending.handle.appendBatch(pendingBatch);
535
- pendingBatch = [];
536
- } };
537
530
  class PrecheckError extends Error {
538
531
  status;
539
532
  constructor(status, msg) {
@@ -542,12 +535,7 @@ export async function handleSessionSync(req, res, match, ctx) {
542
535
  }
543
536
  }
544
537
  const streamedIds = new Set();
545
- const onEntry = (e) => {
546
- if (!e || typeof e !== "object" || typeof e.id !== "string")
547
- throw new PrecheckError(422, "malformed entry (missing id)");
548
- streamedIds.add(e.id);
549
- validator.step(e);
550
- };
538
+ const onEntry = async (e) => { streamedIds.add(await feed.push(e)); };
551
539
  const utf8 = new StringDecoder("utf8");
552
540
  try {
553
541
  for await (const chunk of req) {
@@ -568,10 +556,7 @@ export async function handleSessionSync(req, res, match, ctx) {
568
556
  catch {
569
557
  throw new PrecheckError(400, "malformed NDJSON line");
570
558
  }
571
- onEntry(e);
572
- pendingBatch.push(e);
573
- if (pendingBatch.length >= SYNC_IMPORT_BATCH)
574
- await flush();
559
+ await onEntry(e);
575
560
  }
576
561
  }
577
562
  lineBuf += utf8.end();
@@ -584,11 +569,9 @@ export async function handleSessionSync(req, res, match, ctx) {
584
569
  catch {
585
570
  throw new PrecheckError(400, "malformed NDJSON line");
586
571
  }
587
- onEntry(e);
588
- pendingBatch.push(e);
572
+ await onEntry(e);
589
573
  }
590
- await flush();
591
- validator.finish();
574
+ await feed.finish();
592
575
  if (streamedIds.size !== pending.declaredEntryIds.length || !pending.declaredEntryIds.every((id) => streamedIds.has(id))) {
593
576
  throw new PrecheckError(422, "streamed entries do not match the Phase-A declared entryIds");
594
577
  }
@@ -600,6 +583,10 @@ export async function handleSessionSync(req, res, match, ctx) {
600
583
  sendError(res, e.status, "invalid_entries", e.message);
601
584
  return;
602
585
  }
586
+ if (e instanceof MalformedImportEntryError) {
587
+ sendError(res, 422, "invalid_entries", e.message);
588
+ return;
589
+ }
603
590
  if (sendSessionError(res, e, "import"))
604
591
  return;
605
592
  throw e;
@@ -2,7 +2,7 @@
2
2
  * [ref] A9 域模块 —— 会话列表 / 发件链接列表。
3
3
  *
4
4
  * 路由:`GET /v1/sessions`(CC /resume picker,keyset 分页)、`GET /v1/sendfile-links`。
5
- * owner-scope 与 trace 列表同一权威模型(`fleetViewOf` 读档:service 凭据不报主体 / 只读 trace 凭据 / dev-open = fleet-wide;带验过的主体 = 钉住那位主体)。本域零可变状态。
5
+ * owner-scope 与 trace 列表同一权威模型(`fleetViewOf` 读档:service 凭据不报主体〔部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` 时不算〕/ 只读 trace 凭据 / dev-open = fleet-wide;带验过的主体 = 钉住那位主体)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
8
  import { type SessionListItem } from "../../security.js";
@@ -8,6 +8,7 @@ import { recordFailOpen } from "../../observability/fail-open.js";
8
8
  import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
9
9
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
10
10
  import { readLayerSeat, sentMemoryLayerRefusal } from "../memory-layer-reply.js";
11
+ import { DEPLOYMENT_SHAPE_PREDICATES } from "../../deployment-shape.js";
11
12
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
12
13
  import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
13
14
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
@@ -615,6 +616,7 @@ export async function handleSessions(req, res, match, ctx) {
615
616
  memoryStatusCache.delete(cacheKey); });
616
617
  }
617
618
  const s = await pending;
619
+ const workerPathsDisclosed = DEPLOYMENT_SHAPE_PREDICATES.callerOwnsWorkerDisk.holds(deps.config);
618
620
  sendJson(res, 200, {
619
621
  sessionId,
620
622
  autoConsolidationArmed: deps.memoryAutoConsolidationArmed,
@@ -624,6 +626,8 @@ export async function handleSessions(req, res, match, ctx) {
624
626
  ...(s.foldedCount !== undefined ? { foldedCount: s.foldedCount } : {}),
625
627
  ...(s.optOutSource !== undefined ? { optOutSource: s.optOutSource } : {}),
626
628
  ...(s.lastCaptureAt !== undefined ? { lastCaptureAt: s.lastCaptureAt } : {}),
629
+ ...(s.writeHome !== undefined && workerPathsDisclosed ? { writeHome: s.writeHome } : {}),
630
+ ...(s.planes !== undefined && workerPathsDisclosed ? { planes: s.planes.map((row) => ({ plane: row.plane, ...(row.status.writeHome !== undefined ? { writeHome: row.status.writeHome } : {}) })) } : {}),
627
631
  });
628
632
  }
629
633
  catch (err) {
@@ -76,7 +76,7 @@ const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their n
76
76
  export async function handleTasks(req, res, match, ctx) {
77
77
  const { deps } = ctx;
78
78
  const { idemCache, inflightRuns, steerableRuns, counters } = ctx.registry;
79
- const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
79
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, admitRun } = ctx.helpers;
80
80
  const { prepareSpec, finalizeTaskResult } = ctx.legs;
81
81
  const reqState = ctx.req;
82
82
  const source = ctx.req.source;
@@ -144,10 +144,14 @@ export async function handleTasks(req, res, match, ctx) {
144
144
  }
145
145
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
146
146
  return;
147
+ let runSlot = undefined;
147
148
  const prepared = await prepareSpec(req, res, keyedBody, undefined, { liveStream });
148
149
  if (!prepared)
149
150
  return;
150
151
  try {
152
+ runSlot = admitRun(req, res);
153
+ if (runSlot === undefined)
154
+ return;
151
155
  const declaredFamilies = declaredApprovalFrameFamilies(prepared.body.settings);
152
156
  const approvalLegInput = (streamApprovalOn) => {
153
157
  const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
@@ -184,7 +188,7 @@ export async function handleTasks(req, res, match, ctx) {
184
188
  sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
185
189
  if (earlyDurableTid)
186
190
  writeMetaFrame(res, earlyDurableTid, prepared.spec.sessionId, prepared.warnings);
187
- const ac = createRunAbort();
191
+ const ac = createRunAbort(prepared.auth?.principal ?? null);
188
192
  let closed = false;
189
193
  let detachLogged = false;
190
194
  let disconnectParkGrace;
@@ -355,6 +359,7 @@ export async function handleTasks(req, res, match, ctx) {
355
359
  }, HEARTBEAT_MS);
356
360
  durableHeartbeat.unref?.();
357
361
  inflightRuns.set(tid, ac);
362
+ runSlot?.release();
358
363
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
359
364
  abortFromVerb(); }).catch(() => undefined);
360
365
  }
@@ -849,7 +854,10 @@ export async function handleTasks(req, res, match, ctx) {
849
854
  };
850
855
  counters.uncountedBillableInflight++;
851
856
  try {
852
- resp = await idemCache.run(inflightKey, async () => withRunReceipt(await runStreamSubmitLeg()));
857
+ const settling = idemCache.run(inflightKey, async () => withRunReceipt(await runStreamSubmitLeg()));
858
+ if (!ranLive)
859
+ runSlot?.release();
860
+ resp = await settling;
853
861
  }
854
862
  catch (e) {
855
863
  deps.logger?.error("stream_error", { closed, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
@@ -904,7 +912,7 @@ export async function handleTasks(req, res, match, ctx) {
904
912
  return ctxRefusal;
905
913
  }
906
914
  commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
907
- const syncCancelCtrl = createRunAbort();
915
+ const syncCancelCtrl = createRunAbort(principal ?? null);
908
916
  markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
909
917
  if (durableTaskId && deps.runStore) {
910
918
  const rs = deps.runStore;
@@ -920,11 +928,13 @@ export async function handleTasks(req, res, match, ctx) {
920
928
  }, HEARTBEAT_MS);
921
929
  durableHeartbeat.unref?.();
922
930
  inflightRuns.set(tid, syncCancelCtrl);
931
+ runSlot?.release();
923
932
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
924
933
  abortFromVerb(); }).catch(() => undefined);
925
934
  }
926
935
  else if (durableTaskId) {
927
936
  inflightRuns.set(durableTaskId, syncCancelCtrl);
937
+ runSlot?.release();
928
938
  }
929
939
  const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) };
930
940
  const observeSyncLegEvent = (ev) => {
@@ -1001,12 +1011,20 @@ export async function handleTasks(req, res, match, ctx) {
1001
1011
  noteRunOutcome(result);
1002
1012
  return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult)) };
1003
1013
  };
1004
- const resp = await idemCache.run(inflightKey, async () => withRunReceipt(await runSyncSubmitLeg()));
1014
+ let ranSyncLeg = false;
1015
+ const settling = idemCache.run(inflightKey, async () => {
1016
+ ranSyncLeg = true;
1017
+ return withRunReceipt(await runSyncSubmitLeg());
1018
+ });
1019
+ if (!ranSyncLeg)
1020
+ runSlot?.release();
1021
+ const resp = await settling;
1005
1022
  sendSubmitResult(res, resp);
1006
1023
  }
1007
1024
  return;
1008
1025
  }
1009
1026
  finally {
1027
+ runSlot?.release();
1010
1028
  prepared.peerLease.release();
1011
1029
  }
1012
1030
  }
@@ -102,7 +102,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
102
102
  };
103
103
  const now = Date.now();
104
104
  const from = parseT(q.get("from"), now - 7 * 86_400_000);
105
- const to = parseT(q.get("to"), now);
105
+ const to = parseT(q.get("to"), now + 1);
106
106
  if (Number.isNaN(from) || Number.isNaN(to) || from >= to) {
107
107
  sendError(res, 400, "request.query_invalid", "invalid from/to (ISO-8601 or epoch-ms; from < to)");
108
108
  return;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 在飞 run 并发帽 —— `RUN_MAX_CONCURRENT`(本副本全局)/ `RUN_MAX_CONCURRENT_PER_PRINCIPAL`(同一已验提交主体),缺省 0 = 关。
3
+ *
4
+ * ## 为什么有这一只
5
+ * 共享一台 GPU / 一只小模型网关的部署,调用方自己限并发不可靠(多个调用方、重试风暴、脚本一把梭)。`RATE_LIMIT_RPM` 是速率桶(每分钟几条),
6
+ * 不是「此刻有几条在跑」;一条 run 可以跑几十分钟,速率桶放行的新提交会在同一台机器上叠起来。本帽数的就是后一个量。
7
+ *
8
+ * ## 数什么
9
+ * **同一份**在飞集:`inflightRuns ∪ steerableRuns`({@link liveRunIdsOf} —— `drainState.inflight` 读的也是它;`isRunLiveOnThisReplica`(`http/server.ts`)是同一并集的逐 id 判,
10
+ * 另写成两只 `has` 的析取,不经本函数;计数只有这一处写法,不另造第二份)。按主体数时读 `inflightRuns` 值上的 `owner`({@link RunAbort.owner} —— 句柄与腿同寿,不另起旁表);
11
+ * 只在 `steerableRuns` 里的键没有属主可读,只计入全局(今天每条可 steer 的腿都同时登记在 `inflightRuns`,这是形式上的保守)。
12
+ * 数的是**本副本**:副本之间不共享,多副本部署的整体并发 = 各副本的帽之和。
13
+ *
14
+ * ## 判点(哪几扇门判、哪几扇不判)
15
+ * 判 = **新提交要起一条 run** 的三个提交门(`routes/runs.ts` 的 `createDurableRun` —— `POST /v1/runs` 与 A2A `message/send` 共用的建行腿、
16
+ * `routes/tasks.ts` 的 `POST /v1/tasks` 与 `POST /v1/tasks/stream`),且排在**幂等重放之后**(同键 / 自带 taskId 的重试拿回自己的回执,不吃新 429)。
17
+ * 不判 = 已有 run 的延续与作答:续跑、唤醒、审批 / 问答作答、`/decide` —— 它们不是新提交,在飞时照样占一格(被数;没有 run 行的老形续跑不登记进在飞集,不被数),
18
+ * 但拒它们会把一张已经在等人的卡卡死。所以帽闸的是**新提交**,不是在飞总数的硬上限:一条停驻的 run 续跑回来时,在飞数可以越过帽。
19
+ * leader 口的后台 run 不在 `inflightRuns` 里(drain 靠 `uncountedBillableInflight` 看见它),本帽不数它也不判它。
20
+ * 位置的两个如实:三个口的帽都排在受理漏斗(`prepareSpec`,可能为新会话做登记)**之后**、建行与 SSE 头之前 —— `POST /v1/runs` 与 A2A 口因自带 `taskId` 的重放在漏斗之后才答、帽必须排在它后面
21
+ * (与 lease / 治理窗的 429 同位),`POST /v1/tasks` 两口与它们同位;拒时漏斗已做的会话登记不撤。同键的并发重复提交若赶在首发还在受理(尚未落行、尚未进在飞合流)的窗里到达,按新提交判,
22
+ * 帽满时与其它准入门(速率 / 配额)同形可能拿到一次 429 + `Retry-After`,按它重投即拿回首发的回执。
23
+ *
24
+ * ## 席位(为什么放行不是「读一下数就完了」)
25
+ * 判完帽到这条 run 登记进 `inflightRuns` 之间隔着若干次 await(建行、执行放置、续跑输入落库 …)。并发一阵提交若都只「读数」,会全部在登记之前读到同一个数、
26
+ * 一起放行 —— 越帽。所以放行即**占一枚席位**({@link RunSlot}),在飞数 = 在飞集 + 未放的席位。席位在这条 run 登记进在飞集的那一拍放掉:
27
+ * 三个提交门同形:建行腿 `mintAndDriveDurableRun` 在 `runInBackground`(返回前同步登记)返回后就地放(`runSlot` 入参),`POST /v1/tasks` / `/v1/tasks/stream` 两腿在各自的
28
+ * `inflightRuns.set` 之后就地放;提交门的 `finally` 总再放一次兜底(早退、被别的门拒、抛 —— `release` 幂等)。登记到放席位之间没有 await,别的提交读不到
29
+ * 「既在集里又占着席位」的中间态,所以不重复计。
30
+ * 两个尾巴:合流进别人在飞建行的重复提交(同键赛过 `peek`)自己不建行 —— `POST /v1/tasks` 一族席位当场放回;`POST /v1/runs`(与 A2A 提交)的席位到这一次提交的整段结清(`.finally`)才放,
31
+ * 合流的那一次要等共享铸件结清,帽边上可多答一次 429;没有 run store 的部署上同步 / 流式腿根本不登记进在飞集
32
+ * (drain 靠 `uncountedBillableInflight` 数它们),它们的席位就活到请求结束 —— 帽照样数得到它们。
33
+ *
34
+ * ## 拒的形
35
+ * 429 `limit.rate_exceeded`(沿用现有限流码,不新铸一枚:消费端的分支同「稍后再来」)+ 体里机读 `reason`(闭集 {@link RATE_EXCEEDED_REASONS},今天一词)
36
+ * + `retryAfterSec`(固定 {@link RUN_CONCURRENCY_RETRY_AFTER_SEC});头 `Retry-After` 与体里同值,同其它限流 / 配额门的发送形(`res.setHeader("retry-after")` + `sendError`)。
37
+ * 拒在建行、认领会话与 SSE 头之前(受理漏斗已做的会话登记见上面「位置的两个如实」)。
38
+ *
39
+ * ## 谁需要 / 谁受伤 / 补偿
40
+ * 需要:共享一台 GPU 或一只小模型网关的家用 / 小团队部署。受伤:超帽者拿 429(新行为,缺省关)。补偿:缺省 0 = 关(升级零变化)+ `Retry-After` 给退避。
41
+ * 无主体请求(无头 service 凭据、`REQUIRE_PRINCIPAL=false` 的单用户部署)只受全局帽 —— 按主体帽对它不判(没有主体可分账)。
42
+ */
43
+ import type { IncomingMessage, ServerResponse } from "node:http";
44
+ import type { RunAbort } from "../run-cancel-context.js";
45
+ import { type RefusalBody } from "./refusal-body.js";
46
+ import type { FlatServiceDeps } from "./server.js";
47
+ /**
48
+ * 本副本在飞 run 的 id 集 —— `inflightRuns`(durable 腿)∪ `steerableRuns`(活流)的**唯一**并集写法:
49
+ * `drainState.inflight`(排空等多久)、`isRunLiveOnThisReplica`(这条 run 是不是在本副本跑)与并发帽读的是同一份。
50
+ * 参数取 `ReadonlyMap`:读法不改登记簿。
51
+ */
52
+ export declare function liveRunIdsOf(inflightRuns: ReadonlyMap<string, unknown>, steerableRuns: ReadonlyMap<string, unknown>): Set<string>;
53
+ /** 并发帽拒的机读 `reason` 词。 */
54
+ export declare const RUN_CONCURRENCY_CAP_REASON: "run_concurrency_cap";
55
+ /**
56
+ * 429 `limit.rate_exceeded` 体上机读 `reason` 的**闭集**(穷举读它;加一种拒因 ⇒ 这里加一词,`switch` 的消费端编译红)。
57
+ * 其余 `limit.rate_exceeded` 的产地(速率桶、赎回准入、MCP 探活、烘焙提交)不带 `reason` —— 读法是「有 `reason` 才分」。
58
+ */
59
+ export declare const RATE_EXCEEDED_REASONS: readonly ["run_concurrency_cap"];
60
+ export type RateExceededReason = (typeof RATE_EXCEEDED_REASONS)[number];
61
+ /** 退避提示:固定 5 秒(是提示不是承诺 —— 在飞的 run 可能再跑很久;帽满只说「稍后再来」)。 */
62
+ export declare const RUN_CONCURRENCY_RETRY_AFTER_SEC = 5;
63
+ /** 一枚席位:放行与登记进在飞集之间占着的那一格。`release` 幂等(登记点放一次,提交门的 `finally` 再放一次兜底)。 */
64
+ export interface RunSlot {
65
+ release(): void;
66
+ }
67
+ /** 并发帽拒的 `{status, body, retryAfterSec}`(体经全仓唯一组句器;状态与退避供发送口用)。 */
68
+ export declare function runConcurrencyRefusalOf(): {
69
+ readonly status: 429;
70
+ readonly body: RefusalBody;
71
+ readonly retryAfterSec: number;
72
+ };
73
+ export interface RunConcurrencyGate {
74
+ /**
75
+ * 提交门的并发帽准入:放行 ⇒ 交回席位(调用点登记那一拍 / `finally` 放);超帽 ⇒ 已应答 429(+ `Retry-After`),交回 `undefined`,调用点 `return`。
76
+ * 位序:在幂等重放、`rateLimited` / `quotaExceeded` / `leaseDenied` / `usageWindowDenied` 之后,任何建行 / 认领 / SSE 头之前。
77
+ */
78
+ admitRun(req: IncomingMessage, res: ServerResponse): RunSlot | undefined;
79
+ }
80
+ /** 装配口:`deps` 只读 `config`(两旋钮,每次判时现读)与 `logger`;在飞集由 server 装配层交来(两张登记簿本身,不拷)。 */
81
+ export declare function createRunConcurrencyGate(ctx: {
82
+ deps: Pick<FlatServiceDeps, "config" | "logger">;
83
+ inflightRuns: ReadonlyMap<string, RunAbort>;
84
+ steerableRuns: ReadonlyMap<string, unknown>;
85
+ }): RunConcurrencyGate;
86
+ //# sourceMappingURL=run-concurrency-cap.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { gatedPrincipal } from "./principal-gate.js";
2
+ import { refusalBody } from "./refusal-body.js";
3
+ import { sendError } from "./send.js";
4
+ export function liveRunIdsOf(inflightRuns, steerableRuns) {
5
+ return new Set([...inflightRuns.keys(), ...steerableRuns.keys()]);
6
+ }
7
+ export const RUN_CONCURRENCY_CAP_REASON = "run_concurrency_cap";
8
+ export const RATE_EXCEEDED_REASONS = [RUN_CONCURRENCY_CAP_REASON];
9
+ export const RUN_CONCURRENCY_RETRY_AFTER_SEC = 5;
10
+ const UNTRACKED_SLOT = { release: () => undefined };
11
+ export function runConcurrencyRefusalOf() {
12
+ return {
13
+ status: 429,
14
+ body: refusalBody({
15
+ errorCode: "limit.rate_exceeded",
16
+ message: "too many runs are in flight on this server right now — nothing was started; retry after the Retry-After interval",
17
+ facts: { reason: RUN_CONCURRENCY_CAP_REASON, retryAfterSec: RUN_CONCURRENCY_RETRY_AFTER_SEC },
18
+ }),
19
+ retryAfterSec: RUN_CONCURRENCY_RETRY_AFTER_SEC,
20
+ };
21
+ }
22
+ export function createRunConcurrencyGate(ctx) {
23
+ const { deps, inflightRuns, steerableRuns } = ctx;
24
+ const seats = new Set();
25
+ function admitRun(req, res) {
26
+ const { runMaxConcurrent: globalCap, runMaxConcurrentPerPrincipal: ownCap } = deps.config;
27
+ if (globalCap === 0 && ownCap === 0)
28
+ return UNTRACKED_SLOT;
29
+ const owner = gatedPrincipal(req, deps.config);
30
+ const live = liveRunIdsOf(inflightRuns, steerableRuns);
31
+ const inFlight = live.size + seats.size;
32
+ let scope;
33
+ if (globalCap > 0 && inFlight >= globalCap)
34
+ scope = "global";
35
+ else if (ownCap > 0 && owner !== undefined) {
36
+ const holders = [...[...live].map((id) => inflightRuns.get(id)?.owner), ...[...seats].map((seat) => seat.owner)];
37
+ if (holders.filter((o) => o === owner).length >= ownCap)
38
+ scope = "principal";
39
+ }
40
+ if (scope !== undefined) {
41
+ deps.logger?.info?.("run_concurrency_cap_refused", { scope, inFlight, cap: scope === "global" ? globalCap : ownCap, hasPrincipal: owner !== undefined });
42
+ const refusal = runConcurrencyRefusalOf();
43
+ const { error, errorCode, ...extra } = refusal.body;
44
+ res.setHeader("retry-after", String(refusal.retryAfterSec));
45
+ sendError(res, refusal.status, errorCode, error, extra);
46
+ return undefined;
47
+ }
48
+ const seat = { owner };
49
+ seats.add(seat);
50
+ return { release: () => void seats.delete(seat) };
51
+ }
52
+ return { admitRun };
53
+ }
54
+ //# sourceMappingURL=run-concurrency-cap.js.map
@@ -2,7 +2,7 @@ import http from "node:http";
2
2
  import type { MemoryComplianceFaces, MemoryOriginFaces, SessionMemoryStatusFace } from "../memory-operator-faces.js";
3
3
  import type { SideQueryAccounting } from "../budget.js";
4
4
  import type { IncomingMessage } from "node:http";
5
- import { type Runner, type RunnerDeps, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
5
+ import { type Runner, type RunnerDeps, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
6
6
  import type { TaskRequestBody } from "./wire-types.js";
7
7
  import type { ServiceConfig } from "../config-types.js";
8
8
  import { type SessionMirrorRuling } from "../web-admin-client/facade.js";
@@ -31,7 +31,7 @@ import type { Metrics } from "../observability/metrics.js";
31
31
  import { type RateGate } from "../observability/rate-limit.js";
32
32
  import type { QuotaTracker } from "../observability/cost-quota.js";
33
33
  import type { ModelUsageTracker, PromptManifestTracker } from "../budget.js";
34
- import type { ConsolidationSeatLabel, ConsolidationValve } from "../boot/memory-consolidation.js";
34
+ import type { ConsolidationValve, MemoryConsolidationFaces } from "../boot/memory-consolidation.js";
35
35
  import { streamApprovals, checkQuestionAnswer } from "./routes/approvals-assistant.js";
36
36
  export { streamApprovals, checkQuestionAnswer };
37
37
  import { cascadeConfig } from "./run-meta.js";
@@ -581,15 +581,9 @@ export interface ServiceSeamDeps {
581
581
  * **启动期**就被拒了(`boot/memory-consolidation.ts`),那些机器根本起不来。⇒ 缺席形是 **404 族**
582
582
  * (整域不挂),刻意**不是** 501:501 说「换部署形态」,这里的动作是「把旋钮打开」。全文见
583
583
  * `routes/memory-consolidation.ts` 头注。
584
- * 🔴 `seat` 的词表单一真源 = `boot/memory-consolidation.ts` 的 {@link ConsolidationSeatLabel}
585
- * (type-only import,tsc 擦除、非装载边)——在这里再写一遍字面量联合就是第二份会漂的词表。 */
586
- memoryConsolidation?: {
587
- readonly seat: ConsolidationSeatLabel;
588
- readonly model: string;
589
- readonly scopes: readonly string[];
590
- run(scope: string): Promise<ConsolidationRunReceipt>;
591
- lastRun(scope: string): ConsolidationDriverRunRow | undefined;
592
- };
584
+ * 🔴 形的单一真源 = `boot/memory-consolidation.ts` 的 {@link MemoryConsolidationFaces}(type-only import,tsc 擦除、非装载边)——
585
+ * 在这里再写一遍成员表就是第二份会漂的形(core 7.63.0 把 `lastRun` 改成异步时这里的手抄形正好漂了一次)。 */
586
+ memoryConsolidation?: MemoryConsolidationFaces;
593
587
  /** [ref] §3.1 / [ref] §1.3([ref] S-2):per-principal memory-capture opt-out 授权表的管理面
594
588
  * (`/v1/admin/memory-optout` 四口,operator-only)—— `backend.memoryOptOutGrant()` 的同一只(与
595
589
  * `RuntimeCaps.allowMemoryOptOut` 的 resolver 腿读同一张表,零缓存直读 ⇒ 写后下一次 prepare 即生效)。
@@ -729,7 +723,10 @@ export interface ServiceDeploymentDeps {
729
723
  * orchestrator/shell retries against the replacement instance; read paths + in-flight verbs (steer/cancel/
730
724
  * respond/subagent) stay open so the runs we are waiting on can still finish/interact.
731
725
  * • `inflight` is ASSIGNED BY createServer (a live union of this replica's in-flight legs: durable bg/resume
732
- * `inflightRuns` + live sync/resume streams `steerableRuns`) — main.ts polls it to know when drain is done.
726
+ * `inflightRuns` + live sync/resume streams `steerableRuns`, the leader leg, and the session-resident background
727
+ * shells/monitors — `inflightBreakdown` is the same read split into its three terms) — an observable fact (`/health.inflight`).
728
+ * `blockingInflight` (also ASSIGNED BY createServer) is the same breakdown minus the session-resident term: "is there still work that
729
+ * must be waited out" — the drain wait main.ts polls after SIGTERM, the orphan/lease windows and the SIGHUP idle window read THAT.
733
730
  * • /health carries `draining:true` + `version` (build self-description); draining is a readiness reason
734
731
  * (`http/readiness.ts`) ⇒ `/readyz` 503 + `/health` `ready:false` (the k8s readiness 摘流信号).
735
732
  * • [ref]:`attachedStreams` 同样 ASSIGNED BY createServer —— 本副本当前打开着的 SSE 流条数
@@ -740,6 +737,12 @@ export interface ServiceDeploymentDeps {
740
737
  * 硬停起点各调一次。
741
738
  */
742
739
  drainState?: import("../fleet/drain-entry.js").DrainState;
740
+ /**
741
+ * 在飞数第四类的读口:本进程**会话驻留的后台 shell / monitor** 行数(`session-resident-background.ts` 的 `countSessionResidentBackground` 在 composition root 绑上围栏表;
742
+ * 法与边界见该文件头)。`createHttpServer` 把它作为 `inflightBreakdown.sessionResident` 并进总数 `drainState.inflight` 的**同一次读**;它**不**进阻断集
743
+ * `drainState.blockingInflight`(排空 / 孤儿窗与附着租约自退 / SIGHUP 空闲窗判停读阻断集)。缺席(嵌入方 / 夹具)= 这一项恒 0,`inflight` 逐字同改前。
744
+ */
745
+ sessionResidentBackground?: () => number;
743
746
  /** 编排器片 2 件 3:服务租约此刻的读数(`fleet/serving-lease.ts` 的客户端交出;引擎信号铸点与计费准入判同读)。缺席 = 没装租约客户端
744
747
  * (非 fleet 成员 / 没接中心 / 测试夹具)⇒ `none`(与没有租约的引擎逐字同)。 */
745
748
  servingLease?: () => import("../fleet/engine-signal.js").ServingLeaseView;
@@ -9,6 +9,7 @@ import {} from "../memory-sync.js";
9
9
  import {} from "../orchestration/workflow-agent-steer.js";
10
10
  import {} from "../orchestration/workflow-completion-inbox.js";
11
11
  import {} from "../fleet/fleet-bus.js";
12
+ import { blockingInflightOf, inflightTotal } from "../fleet/drain-entry.js";
12
13
  import {} from "../tool-approval.js";
13
14
  import {} from "../observability/rate-limit.js";
14
15
  import { IdempotencyCache } from "./idempotency.js";
@@ -20,6 +21,7 @@ import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
20
21
  export { coarseStatusForState, errorCodeForExit };
21
22
  import { sendError, sendHttpError, sendOpenFaceFault, logRequestError } from "./send.js";
22
23
  import { createRequestGuards } from "./request-guards.js";
24
+ import { createRunConcurrencyGate, liveRunIdsOf } from "./run-concurrency-cap.js";
23
25
  import { sqlEngineSeatIssue } from "../sql-engine-posture.js";
24
26
  import { parkDecisionStoreRefusal } from "../orchestration/workflow-park-decision.js";
25
27
  import { CHECKPOINT_CTX_SEALING_REQUIRED } from "./checkpoint-ctx.js";
@@ -104,16 +106,28 @@ export function createHttpServer(rawDeps) {
104
106
  const steerableRuns = new Map();
105
107
  const wakeParkMints = new Map();
106
108
  const counters = { uncountedBillableInflight: 0, admittedInflight: 0 };
107
- if (deps.drainState)
108
- deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + counters.uncountedBillableInflight + counters.admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
109
+ const blockingParts = () => ({
110
+ http: liveRunIdsOf(inflightRuns, steerableRuns).size + counters.uncountedBillableInflight + counters.admittedInflight,
111
+ leader: deps.leaderEndpoint?.inflight?.() ?? 0,
112
+ });
113
+ const inflightBreakdown = () => ({ ...blockingParts(), sessionResident: deps.sessionResidentBackground?.() ?? 0 });
114
+ if (deps.drainState) {
115
+ deps.drainState.inflightBreakdown = inflightBreakdown;
116
+ deps.drainState.inflight = () => inflightTotal(inflightBreakdown());
117
+ deps.drainState.blockingInflight = () => blockingInflightOf(blockingParts());
118
+ }
109
119
  let lastBillableActivityAt = performance.now();
120
+ let firstBillableActivityAt = null;
110
121
  const noteActivity = (url) => {
111
122
  if (isProbePath(url))
112
123
  return;
113
124
  lastBillableActivityAt = performance.now();
125
+ firstBillableActivityAt ??= lastBillableActivityAt;
114
126
  };
115
127
  if (deps.drainState)
116
128
  deps.drainState.lastActivityAt = () => lastBillableActivityAt;
129
+ if (deps.drainState)
130
+ deps.drainState.firstActivityAt = () => firstBillableActivityAt;
117
131
  if (deps.drainState)
118
132
  deps.drainState.yieldInflight = () => preemptableRuns.yieldAll();
119
133
  if (deps.hookWakeBus) {
@@ -155,7 +169,8 @@ export function createHttpServer(rawDeps) {
155
169
  }
156
170
  const { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody } = createRequestGuards({ deps });
157
171
  const { prepareSpec } = createAdmission({ deps, readJson, runnerFor });
158
- const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, sessionNotifyDoor } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
172
+ const { admitRun } = createRunConcurrencyGate({ deps, inflightRuns, steerableRuns });
173
+ const { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, sessionNotifyDoor } = createResumeLegs({ deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, admitRun, safeDecode, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor });
159
174
  const { denyExpiredApprovals, redeemParkedAsk } = createApprovalSweeps({ deps, resumeCheckpoint, mayDiscloseRowError });
160
175
  const { handle } = createDispatch({ deps, noteActivity, routeCtxBase, counters });
161
176
  const server = http.createServer((req, res) => {
@@ -1,19 +1,20 @@
1
1
  /**
2
- * 「这条停驻行的持久化体在本版上能否重放」—— **唯一**判据(四问一只;五个问点同调:续跑驱动段 `resume-legs.ts` `resolveResumeSpecOrBlocked`、
2
+ * 「这条停驻行的持久化体在本版上能否重放」—— **唯一**判据(四问一只,外加 `settings.env` 凭据一臂;五个问点同调:续跑驱动段 `resume-legs.ts` `resolveResumeSpecOrBlocked`、
3
3
  * plan_review 的 prepare 段、SLA 拒扫腿的跳过臂 `approval-sweeps.ts`、终态 run 的 steer 铸唤醒卡之前 `routes/runs.ts`;`resolveSpec` 本身留作提交腿的拒点)。
4
4
  * 四问的规则、记号、拒句与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注;答复 = 那里的 `storedBodyReplayAnswer`。
5
5
  *
6
- * 为什么判据住在这里而不是 `checkpoint-ctx.ts`:第三问要读体的**生效模式词**(`task-settings.ts` `storedBodyPermissionMode`,请求设置的解析层)与本部署此刻的
6
+ * 为什么判据住在这里而不是 `checkpoint-ctx.ts`:模式词问要读体的**生效模式词**(`task-settings.ts` `storedBodyPermissionMode`,请求设置的解析层)与本部署此刻的
7
7
  * **入口地板**(`runtime-governance.ts` `autonomyModeFloor`)—— 这两只属主带着配置层的包外依赖;`checkpoint-ctx.ts` 被随包的本地排空脚本经编译产物 import,
8
- * 它的闭包只许 node 内建 / 引擎包根(`test/drain-check-local.test.ts` 的闭包门)。判据只有这一只,前两问的谓词照旧住在各自属主里。
8
+ * 它的闭包只许 node 内建 / 引擎包根(`test/drain-check-local.test.ts` 的闭包门)。判据只有这一只,allow 读法问、工具名清单问与退役请求键问的谓词照旧住在各自属主里。
9
9
  */
10
10
  import type { PermissionMode } from "@sema-agent/core";
11
11
  import { type Autonomy } from "../runtime-governance.js";
12
12
  import { type ExecutionConfigRootDeployment } from "../execution-config-root.js";
13
+ import { type ModelApiKeyEnvTable } from "../server-secret-env.js";
13
14
  import { type StoredBodyReplayRefusal } from "./checkpoint-ctx.js";
14
15
  import type { CheckpointCtx } from "./route-ctx.js";
15
16
  /**
16
- * 第三问:本行体里的模式词能否按当前读法重放。不能 ⇔ 本部署此刻有入口地板 ∧ 行上没有当前的模式词读法记号 ∧ 体的生效词进这块地板会变
17
+ * 模式词问:本行体里的模式词能否按当前读法重放。不能 ⇔ 本部署此刻有入口地板 ∧ 行上没有当前的模式词读法记号 ∧ 体的生效词进这块地板会变
17
18
  * (`runtime-governance.ts` `entryPermissionMode`,与提交入口的盖章同一只函数;判的是「词变不变」而不是 `clamped` —— 体无词的旧行进地板取地板本身,
18
19
  * 那也是变)⇒ 返回拒因的两个事实(体的词、地板);能 ⇒ `undefined`。
19
20
  */
@@ -22,13 +23,15 @@ export declare function permissionModeReadingUnstated(ctx: CheckpointCtx, floor:
22
23
  floor: PermissionMode;
23
24
  } | undefined;
24
25
  /**
25
- * 预判读的部署配置:`autonomy`(第三问的模式词地板)、执行车道的 home 两位(`~/` 形规则那一问)与配置根的几位(user 设置层 `/` 规则那一问)。`ServiceConfig` 结构上满足,四个问点传 `deps.config` 整份。
26
+ * 预判读的部署配置:`autonomy`(模式词问的模式词地板)、执行车道的 home 两位(`~/` 形规则那一问)与配置根的几位(user 设置层 `/` 规则那一问)。`ServiceConfig` 结构上满足,四个问点传 `deps.config` 整份。
26
27
  */
27
28
  export type StoredReplayDeployment = ExecutionConfigRootDeployment & {
28
29
  readonly autonomy?: Autonomy | undefined;
30
+ /** 生效配置模型条目 name → `apiKeyEnv`(`settings.env` 凭据判据的名单一臂;缺席 = 没有模型条目,与 `serverCredentialValueSources` 的缺省同读)。 */
31
+ readonly modelApiKeyEnv?: ModelApiKeyEnvTable | undefined;
29
32
  };
30
33
  /**
31
- * 四问一只判据:不能重放 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。顺序:allow 读法记号 → 请求设置里的条目表(**与拒点同序**:桶门〔形 / 空串 / 超帽〕→ 工具名清单读成空〔**只问限制方向**:放行方向的存量条目读成空,续跑照旧、只留一行运维 warn〕→ deny / ask 的名字层 + 规则文法)→ 模式词读法记号 → 体里的退役请求键(几问同中时答先中的那一问 ——
34
+ * 四问一只判据:不能重放 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。顺序:allow 读法(体里带非空 `allow` 而写者版本证不出 ≥ 7.117.0;常设,按写者版本)→ 请求设置里的条目表(**与拒点同序**:桶门〔形 / 空串 / 超帽〕→ 工具名清单读成空〔**只问限制方向**:放行方向的存量条目读成空,续跑照旧、只留一行运维 warn〕→ deny / ask 的名字层 + 规则文法)→ 模式词读法记号 → 体里的退役请求键(几问同中时答先中的那一问 ——
32
35
  * 任一问中都不重放,句子只需一句)。`deployment` = 本部署**此刻**的 `config`(活 config:热改即生效):`autonomy` 的地板由它的唯一属主算,`~/` 形规则的 home 由 `executionLaneHomeDir` 算,问点都不自己算。
33
36
  */
34
37
  export declare function storedBodyReplayRefusal(ctx: CheckpointCtx, deployment: StoredReplayDeployment): StoredBodyReplayRefusal | undefined;
@@ -1,4 +1,4 @@
1
- import { REQUEST_SETTINGS_REFUSAL_PREFIX, SETTINGS_PERMISSION_BUCKETS, SETTINGS_RULE_REFUSAL_CODE, neverAutoNamesRefusal, rulePolicyBucketsRefusal, settingsPermissionBucketIssue, storedBodyPermissionMode } from "../task-settings.js";
1
+ import { REQUEST_SETTINGS_REFUSAL_PREFIX, SETTINGS_PERMISSION_BUCKETS, SETTINGS_RULE_REFUSAL_CODE, neverAutoNamesRefusal, readSettingsEnv, rulePolicyBucketsRefusal, settingsEnvCredentialIssue, settingsPermissionBucketIssue, storedBodyPermissionMode } from "../task-settings.js";
2
2
  import { readTightenOnlySeats, tightenOnlySeatField, tightenOnlySeatIssue } from "../tighten-only-seats.js";
3
3
  import { entryProblemsFacts } from "../entry-problems.js";
4
4
  import { autonomyModeFloor, entryPermissionMode } from "../runtime-governance.js";
@@ -6,7 +6,9 @@ import { inertToolListEntryOf, replayInertEntry } from "../rule-entry-list.js";
6
6
  import { executionLaneHomeDir } from "../execution-lane-caps.js";
7
7
  import { executionConfigRootOf } from "../execution-config-root.js";
8
8
  import { migrationWindowEnd } from "../plugins/one-time-migrations.js";
9
- import { allowListReadingEraOf, allowListReadingUnstated, permissionModeReadingStated, retiredRequestKeyOf } from "./checkpoint-ctx.js";
9
+ import { storedEnvCredentialsDropM38 } from "../stored-request-env-credentials-m38.js";
10
+ import { serverCredentialValueSources } from "../server-secret-env.js";
11
+ import { allowListWriterUnproven, permissionModeReadingStated, retiredRequestKeyOf } from "./checkpoint-ctx.js";
10
12
  export function permissionModeReadingUnstated(ctx, floor) {
11
13
  if (floor === undefined || permissionModeReadingStated(ctx))
12
14
  return undefined;
@@ -57,11 +59,23 @@ function storedRulePolicyRefusal(body, deployment) {
57
59
  const neverAutoRefused = neverAutoNamesRefusal(neverAuto);
58
60
  return neverAutoRefused === undefined ? undefined : replayInertEntry(tightenOnlySeatField("neverAuto"), "deny", SETTINGS_RULE_REFUSAL_CODE, `${REQUEST_SETTINGS_REFUSAL_PREFIX}${neverAutoRefused.message}`, { ...entryProblemsFacts(neverAutoRefused.problems) });
59
61
  }
62
+ function storedEnvCredentialRefusal(body, deployment) {
63
+ const issue = settingsEnvCredentialIssue(readSettingsEnv(body?.settings).reaching, serverCredentialValueSources(deployment.modelApiKeyEnv));
64
+ if (issue === null || storedEnvCredentialsDropM38(new Date().toISOString().slice(0, 10)))
65
+ return undefined;
66
+ return { errorCode: "request.env_credential_refused", message: issue.message, facts: { ...issue.fields } };
67
+ }
60
68
  export function storedBodyReplayRefusal(ctx, deployment) {
61
69
  const writer = ctx.writerVersion !== undefined ? { writerVersion: ctx.writerVersion } : {};
62
- if (allowListReadingUnstated(ctx))
63
- return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx), ...writer };
64
- const entry = storedPermissionBucketRefusal(ctx.body) ?? storedTightenOnlySeatRefusal(ctx.body) ?? inertToolListEntryOf(ctx.body, ["deny"]) ?? storedRulePolicyRefusal(ctx.body, deployment);
70
+ if (allowListWriterUnproven(ctx))
71
+ return { kind: "allow_list_reading", ...writer };
72
+ const early = storedPermissionBucketRefusal(ctx.body) ?? storedTightenOnlySeatRefusal(ctx.body);
73
+ if (early !== undefined)
74
+ return { kind: "inert_tool_list", entry: early };
75
+ const envCredential = storedEnvCredentialRefusal(ctx.body, deployment);
76
+ if (envCredential !== undefined)
77
+ return { kind: "settings_env_credential", refusal: envCredential };
78
+ const entry = inertToolListEntryOf(ctx.body, ["deny"]) ?? storedRulePolicyRefusal(ctx.body, deployment);
65
79
  if (entry !== undefined)
66
80
  return { kind: "inert_tool_list", entry };
67
81
  const mode = permissionModeReadingUnstated(ctx, autonomyModeFloor(deployment.autonomy));
@@ -172,9 +172,14 @@ export interface TaskRequestBody {
172
172
  * `outputSchema` (core ignores it there). */
173
173
  outputRetries?: number;
174
174
  /** Within-task compaction tuning (core [ref] `TaskSpec.compaction`). ⚠️ ONLY `clampTolerance` is a caller
175
- * knob — the rest of core's compaction object is an OPERATOR axis and is deliberately not on the wire, so this
176
- * type is narrower than core's field on purpose. `boot/resolve-spec.ts` accepts a finite number in [0,1] and
177
- * omits the whole `compaction` key otherwise. Pairs with `compactionModel` (which gear compacts). */
175
+ * knob HERE — the rest of core's compaction object is an OPERATOR axis and is deliberately not on the wire, so this
176
+ * type is narrower than core's field on purpose. `spec-fields.ts` `compactionSpecFragment` accepts a finite number
177
+ * in [0,1] and omits the whole `compaction` key otherwise. Pairs with `compactionModel` (which gear compacts).
178
+ * The other caller-owned member of core's `compaction`, the automatic-compaction seat `autoCompact`, rides the CC
179
+ * settings key `settings.autoCompactEnabled` (boolean; 7.146.0, capability bit `taskSettings.autoCompactEnabled`) —
180
+ * ONE seat per fact: it is deliberately NOT repeated here (`compaction.autoCompact` in the body is not read). Both
181
+ * members are minted into the one `TaskSpec.compaction` object by the same mapper. The manual seat `manualCompact`
182
+ * (the pass `POST /v1/runs/:id/compact` asks for) is not caller-writable: it takes the deployment's / engine's default. */
178
183
  compaction?: {
179
184
  clampTolerance?: number;
180
185
  };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 一切 JSON 请求体的字节上限 —— `readJson` 边读边计,越过即 413 `request.payload_too_large`(一条一直都在的门,`http/request-guards.ts`)。
3
+ *
4
+ * 住叶子(零依赖)而不是守门族里:依它定帽的面要读同一个数 —— 收编批量导入的「每个会话信封最多多少字节」只有落在它之下才可达
5
+ * (一个越过它的信封根本进不了门,把旋钮设到那里就是一只永远够不着的帽),配置的上帽从这里算(`adoption/import-limits.ts`)。
6
+ */
7
+ export declare const MAX_JSON_BODY_BYTES: number;
8
+ //# sourceMappingURL=json-body-limit.d.ts.map