@sema-agent/server 7.135.0 → 7.136.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 (185) hide show
  1. package/MIGRATION.md +64 -2
  2. package/USAGE.md +62 -25
  3. package/deploy/sema-up/README.md +1 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-card.d.ts +60 -4
  7. package/dist/approval-card.js +18 -1
  8. package/dist/approval-content-kind.d.ts +1 -0
  9. package/dist/approval-content-kind.js +1 -1
  10. package/dist/auth-bridge.d.ts +34 -5
  11. package/dist/auth-bridge.js +20 -3
  12. package/dist/boot/budget-tracing.d.ts +1 -2
  13. package/dist/boot/budget-tracing.js +2 -2
  14. package/dist/boot/org-memory.js +1 -1
  15. package/dist/boot/resolve-spec.js +10 -7
  16. package/dist/boot/runner-deps.js +4 -2
  17. package/dist/boot/stage-04-stores.d.ts +1 -1
  18. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  19. package/dist/boot/stage-06-runners.d.ts +1 -1
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-08-reapers.d.ts +1 -1
  22. package/dist/boot/stage-09-leader.d.ts +1 -1
  23. package/dist/boot/stage-10-http-server.d.ts +1 -1
  24. package/dist/boot/stores.js +2 -3
  25. package/dist/boot/web-admin.js +10 -17
  26. package/dist/brain.js +12 -3
  27. package/dist/budget.d.ts +3 -3
  28. package/dist/budget.js +4 -0
  29. package/dist/capabilities/mcp-probe.d.ts +5 -4
  30. package/dist/capability-facts.d.ts +3 -7
  31. package/dist/capability-facts.js +3 -5
  32. package/dist/center-credential.d.ts +18 -4
  33. package/dist/center-credential.js +18 -3
  34. package/dist/config-catalog.js +12 -10
  35. package/dist/config-d-migration.js +1 -1
  36. package/dist/config-invariants.d.ts +1 -1
  37. package/dist/config-invariants.js +8 -0
  38. package/dist/config-lkg.d.ts +0 -38
  39. package/dist/config-lkg.js +1 -83
  40. package/dist/config-types.d.ts +12 -4
  41. package/dist/config.js +26 -8
  42. package/dist/deployment-shape.d.ts +1 -1
  43. package/dist/elicitation.d.ts +8 -7
  44. package/dist/elicitation.js +9 -8
  45. package/dist/file-history-disclosure.d.ts +6 -4
  46. package/dist/file-history-disclosure.js +22 -6
  47. package/dist/fleet-client.d.ts +32 -1
  48. package/dist/fleet-client.js +24 -4
  49. package/dist/fleet-lease.d.ts +7 -3
  50. package/dist/fleet-lease.js +31 -23
  51. package/dist/hooks/hook-llm.js +14 -8
  52. package/dist/hooks/hook-runner.d.ts +4 -4
  53. package/dist/hooks/hook-runner.js +2 -2
  54. package/dist/http/checkpoint-ctx.d.ts +28 -9
  55. package/dist/http/checkpoint-ctx.js +5 -1
  56. package/dist/http/dispatch.js +9 -9
  57. package/dist/http/resume-ctx-read.d.ts +2 -16
  58. package/dist/http/resume-ctx-read.js +2 -3
  59. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  60. package/dist/http/routes/approvals-assistant.js +47 -12
  61. package/dist/http/routes/runs.js +2 -2
  62. package/dist/http/routes/session-sync.js +31 -9
  63. package/dist/http/routes/sessions.js +18 -14
  64. package/dist/http/submit-ctx.js +1 -1
  65. package/dist/http/wire-types.d.ts +5 -3
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/model-route-gap.d.ts +49 -4
  69. package/dist/model-route-gap.js +34 -1
  70. package/dist/object-store-buckets.d.ts +13 -0
  71. package/dist/object-store-buckets.js +3 -0
  72. package/dist/observability/fail-open.d.ts +6 -6
  73. package/dist/observability/fail-open.js +6 -6
  74. package/dist/observability/metrics.js +1 -1
  75. package/dist/observability/principal-context.d.ts +4 -1
  76. package/dist/observability/principal-context.js +6 -1
  77. package/dist/parked-decide.d.ts +4 -3
  78. package/dist/parked-decide.js +26 -7
  79. package/dist/peer-directory.d.ts +2 -1
  80. package/dist/peer-directory.js +5 -5
  81. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  82. package/dist/plugins/approval-ask-store-sql.js +3 -0
  83. package/dist/plugins/blob-backend.js +2 -1
  84. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  85. package/dist/plugins/checkpoint-index-columns.js +28 -0
  86. package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
  87. package/dist/plugins/checkpoint-store-sql.js +58 -29
  88. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  89. package/dist/plugins/file-history-store-sql.js +30 -23
  90. package/dist/plugins/local-checkpoint-store.js +4 -3
  91. package/dist/plugins/one-time-migrations.d.ts +21 -28
  92. package/dist/plugins/one-time-migrations.js +39 -123
  93. package/dist/plugins/remote-env-k8s.d.ts +18 -5
  94. package/dist/plugins/remote-env-k8s.js +24 -7
  95. package/dist/plugins/s3-answer.d.ts +15 -0
  96. package/dist/plugins/s3-answer.js +5 -0
  97. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  98. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  99. package/dist/plugins/sql-errors.d.ts +39 -0
  100. package/dist/plugins/sql-errors.js +49 -1
  101. package/dist/plugins/store-fault.d.ts +4 -0
  102. package/dist/plugins/store-fault.js +8 -0
  103. package/dist/plugins/tidb-pool.js +1 -1
  104. package/dist/plugins/web-search.d.ts +1 -1
  105. package/dist/plugins/web-search.js +2 -2
  106. package/dist/request-declaration-refusal.d.ts +94 -0
  107. package/dist/request-declaration-refusal.js +78 -0
  108. package/dist/request-image-urls.d.ts +6 -0
  109. package/dist/request-image-urls.js +27 -0
  110. package/dist/request-key-closure.d.ts +5 -0
  111. package/dist/request-key-closure.js +4 -1
  112. package/dist/runtime-caps-resolver.js +1 -1
  113. package/dist/sandbox-pkg-source.d.ts +28 -1
  114. package/dist/sandbox-pkg-source.js +1 -2
  115. package/dist/sealed-key.d.ts +19 -0
  116. package/dist/sealed-key.js +47 -4
  117. package/dist/session-incarnation.d.ts +16 -5
  118. package/dist/session-incarnation.js +7 -4
  119. package/dist/session-row.d.ts +3 -17
  120. package/dist/session-row.js +4 -0
  121. package/dist/session-sync.d.ts +15 -3
  122. package/dist/session-sync.js +5 -2
  123. package/dist/task-a2a.d.ts +52 -81
  124. package/dist/task-a2a.js +219 -89
  125. package/dist/task-mcp.d.ts +6 -14
  126. package/dist/task-mcp.js +16 -51
  127. package/dist/tool-approval.d.ts +23 -10
  128. package/dist/tool-approval.js +51 -36
  129. package/dist/trace/core-keyset-guard.d.ts +2 -2
  130. package/dist/trace/exit-seat.js +37 -13
  131. package/dist/trace/redact.d.ts +28 -12
  132. package/dist/trace/redact.js +233 -94
  133. package/dist/trace/self-minted-wire.d.ts +267 -0
  134. package/dist/trace/self-minted-wire.js +79 -0
  135. package/dist/url-credential-refusal.d.ts +52 -0
  136. package/dist/url-credential-refusal.js +14 -0
  137. package/dist/url-refusal.d.ts +13 -4
  138. package/dist/url-refusal.js +8 -2
  139. package/dist/workspace-space.d.ts +27 -0
  140. package/dist/workspace-space.js +26 -0
  141. package/package.json +3 -2
  142. package/scripts/drain-check-local.mjs +2 -1
  143. package/scripts/drain-check.mysql.sql +3 -1
  144. package/scripts/drain-check.pg.sql +3 -1
  145. package/skills/schedule.md +1 -1
  146. package/wire-changes/server@7.104.0.json +134 -0
  147. package/wire-changes/server@7.105.0.json +53 -0
  148. package/wire-changes/server@7.106.0.json +475 -0
  149. package/wire-changes/server@7.107.0.json +64 -0
  150. package/wire-changes/server@7.108.0.json +153 -0
  151. package/wire-changes/server@7.109.0.json +46 -0
  152. package/wire-changes/server@7.110.0.json +90 -0
  153. package/wire-changes/server@7.111.0.json +86 -0
  154. package/wire-changes/server@7.112.0.json +60 -0
  155. package/wire-changes/server@7.113.0.json +26 -0
  156. package/wire-changes/server@7.114.0.json +26 -0
  157. package/wire-changes/server@7.115.0.json +97 -0
  158. package/wire-changes/server@7.116.0.json +218 -0
  159. package/wire-changes/server@7.116.1.json +26 -0
  160. package/wire-changes/server@7.117.0.json +65 -0
  161. package/wire-changes/server@7.118.0.json +129 -0
  162. package/wire-changes/server@7.119.0.json +65 -0
  163. package/wire-changes/server@7.120.0.json +89 -0
  164. package/wire-changes/server@7.121.0.json +219 -0
  165. package/wire-changes/server@7.122.0.json +54 -0
  166. package/wire-changes/server@7.123.0.json +26 -0
  167. package/wire-changes/server@7.124.0.json +336 -0
  168. package/wire-changes/server@7.124.1.json +26 -0
  169. package/wire-changes/server@7.125.0.json +111 -0
  170. package/wire-changes/server@7.126.0.json +285 -0
  171. package/wire-changes/server@7.127.0.json +77 -0
  172. package/wire-changes/server@7.127.1.json +26 -0
  173. package/wire-changes/server@7.127.2.json +53 -0
  174. package/wire-changes/server@7.128.0.json +26 -0
  175. package/wire-changes/server@7.129.0.json +106 -0
  176. package/wire-changes/server@7.129.1.json +26 -0
  177. package/wire-changes/server@7.130.0.json +26 -0
  178. package/wire-changes/server@7.131.0.json +118 -0
  179. package/wire-changes/server@7.132.0.json +226 -0
  180. package/wire-changes/server@7.133.0.json +1155 -0
  181. package/wire-changes/server@7.134.0.json +107 -0
  182. package/wire-changes/server@7.135.0.json +174 -0
  183. package/wire-changes/server@7.136.0.json +448 -0
  184. package/dist/url-credential.d.ts +0 -52
  185. package/dist/url-credential.js +0 -49
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { DEFAULT_AT_MENTION, DEFAULT_MODEL_REF, mintCapabilityKeys } from "./model-select.js";
3
3
  import { homedir } from "node:os";
4
- import { join, posix as posixPath } from "node:path";
4
+ import { isAbsolute, join, posix as posixPath } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { compileAllowList, inheritEnvPolicyProblem, BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE, RECOMMENDED_SENSITIVE_PATTERNS, judgeRuleToolName, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, principalInboxConsumerOf } from "@sema-agent/core";
7
7
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES, WEB_ADMIN_ENV, retiredEnvRefusal, stripControlAndBidi } from "@sema-agent/settings-schema";
@@ -29,7 +29,8 @@ import { NAME_TABLE_BEHAVIOR, TOOL_NAME_ROSTER, describeUnmatchableRuleName, rul
29
29
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
30
30
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
31
31
  import { lkgPathOverrideOf } from "./config-lkg.js";
32
- import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude } from "./workspace-space.js";
32
+ import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude, parseWorkspaceSnapshotPrefix } from "./workspace-space.js";
33
+ import { SESSION_SNAPSHOT_BUCKET_DEFAULT, WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "./object-store-buckets.js";
33
34
  import { laneLivenessOf } from "./exec-bounds.js";
34
35
  import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
35
36
  import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_MOUNT_RESERVED, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap, reservedMountCollision } from "./workspace-cache.js";
@@ -46,7 +47,7 @@ import { modelRouteUrlRefusal } from "./model-route-url.js";
46
47
  import { httpUrlFlaws } from "@sema-agent/settings-schema";
47
48
  import { buildHttpUrlRefusal } from "./url-refusal.js";
48
49
  import { customPkgSourceFromEnv } from "./sandbox-pkg-source.js";
49
- import { csvOf, envLaneTable, mintedCheapId, readModelRouteEnv } from "./model-route-gap.js";
50
+ import { anthropicCredentialRefusal, csvOf, envLaneTable, mintedCheapId, readModelRouteEnv } from "./model-route-gap.js";
50
51
  import { displayListEntry } from "./list-entry-display.js";
51
52
  import { echoCallerText } from "./caller-text-echo.js";
52
53
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
@@ -863,6 +864,18 @@ function parseStoreDomain(ctx) {
863
864
  dbBackend,
864
865
  dbBackendExplicit: dbBackendSet,
865
866
  localDataRoot,
867
+ ...((d) => (d !== undefined ? { sealedKeyDir: d } : {}))((() => {
868
+ const raw = process.env.SEALED_KEY_DIR;
869
+ if (raw === undefined || raw === "")
870
+ return undefined;
871
+ if (!isAbsolute(raw)) {
872
+ throw new Error(`SEALED_KEY_DIR="${raw}" is not an absolute path — it names the model-key custody directory (the sealed-key keypairs ` +
873
+ `and their checkpoint-ctx-keys/ subdirectory) that every engine sharing one config.d must point at to hold the same key, ` +
874
+ `so a value resolved against each process's working directory would silently split them. Spell it absolutely, or unset it ` +
875
+ `to keep the custody directory at <data root>/sealed-key.`);
876
+ }
877
+ return raw;
878
+ })()),
866
879
  runStoreStrictHydrate: parseRunStoreStrictHydrate(process.env.RUN_STORE_STRICT_HYDRATE),
867
880
  tidb: needsDb && dbBackend === "mysql"
868
881
  ? {
@@ -888,7 +901,7 @@ function parseStoreDomain(ctx) {
888
901
  snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
889
902
  ? {
890
903
  endpoint: process.env.MINIO_ENDPOINT,
891
- bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
904
+ bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? SESSION_SNAPSHOT_BUCKET_DEFAULT,
892
905
  accessKey: process.env.MINIO_ACCESS_KEY,
893
906
  secretKey: process.env.MINIO_SECRET_KEY,
894
907
  ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
@@ -907,7 +920,7 @@ function parseStoreDomain(ctx) {
907
920
  ? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
908
921
  : {}),
909
922
  publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
910
- privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
923
+ privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? SESSION_SNAPSHOT_BUCKET_DEFAULT,
911
924
  privateKeyPrefix: "sendfile/",
912
925
  ...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
913
926
  accessKey: process.env.MINIO_ACCESS_KEY,
@@ -941,6 +954,10 @@ function parseModelDomain() {
941
954
  throw new Error(refusal);
942
955
  }
943
956
  }
957
+ const anthropicCredentialConflict = anthropicCredentialRefusal({ apiKey: route.anthropicApiKey, authToken: route.anthropicAuthToken });
958
+ if (anthropicCredentialConflict !== undefined) {
959
+ throw new Error(anthropicCredentialConflict);
960
+ }
944
961
  const provider = route.provider;
945
962
  if (route.providerSource === "inferred") {
946
963
  CONFIG_NOTICES.push({
@@ -2371,7 +2388,7 @@ function parseA2aServeSkills(raw) {
2371
2388
  });
2372
2389
  }
2373
2390
  const STORE_GROUP_KEYS = [
2374
- "sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
2391
+ "sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot", "sealedKeyDir",
2375
2392
  "runStoreStrictHydrate",
2376
2393
  "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "fileHistoryCapture", "sendUserFile",
2377
2394
  ];
@@ -2463,6 +2480,7 @@ function k8sWorkspaceStore() {
2463
2480
  if (!Number.isInteger(workspaceSnapshotTimeoutMs))
2464
2481
  throw new Error(`env WORKSPACE_SNAPSHOT_TIMEOUT_MS=${workspaceSnapshotTimeoutMs} must be an integer (milliseconds)`);
2465
2482
  const exclude = parseWorkspaceSnapshotExclude(process.env.WORKSPACE_SNAPSHOT_EXCLUDE);
2483
+ const keyPrefix = parseWorkspaceSnapshotPrefix(process.env.WORKSPACE_SNAPSHOT_PREFIX);
2466
2484
  const knobs = { workspacePersist, workspaceSnapshotKeep, workspaceSnapshotTimeoutMs, ...(exclude.length > 0 ? { workspaceSnapshotExclude: exclude } : {}) };
2467
2485
  const endpoint = process.env.MINIO_ENDPOINT;
2468
2486
  const accessKey = process.env.MINIO_ACCESS_KEY;
@@ -2482,11 +2500,11 @@ function k8sWorkspaceStore() {
2482
2500
  ...knobs,
2483
2501
  s3Snapshot: {
2484
2502
  endpoint,
2485
- bucket: process.env.WORKSPACE_SNAPSHOT_BUCKET ?? "workspaces",
2503
+ bucket: process.env.WORKSPACE_SNAPSHOT_BUCKET ?? WORKSPACE_SNAPSHOT_BUCKET_DEFAULT,
2486
2504
  accessKey,
2487
2505
  secretKey,
2488
2506
  ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
2489
- ...(process.env.WORKSPACE_SNAPSHOT_PREFIX ? { keyPrefix: process.env.WORKSPACE_SNAPSHOT_PREFIX } : {}),
2507
+ ...(keyPrefix !== undefined ? { keyPrefix } : {}),
2490
2508
  ...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
2491
2509
  },
2492
2510
  };
@@ -18,7 +18,7 @@
18
18
  * ## 什么进表、什么不进
19
19
  * 进表的是**形规则**:只由部署形事实决定的布尔(去重后 14 行;同一条规则的多个消费者读同一行,不各成一行)。
20
20
  * 把一行与**非形事实**(锁键、合规档位、审批意图、请求身份、请求体)合取的能力判据是**消费者**,住在它自己的模块
21
- * (`a2aInjectionHonored` = 锁 ∧ 合规 ∧ `!multiTenant`;`taskHooksHonored`;身份门 …),读行、不读原始键。
21
+ * (`mcpInjectionHonored` = 锁 ∧ 合规 ∧ `!multiTenant`;`taskHooksHonored`;身份门 …),读行、不读原始键。
22
22
  * 车道表(`execution-lane-caps.ts`)按车道词键控的布尔列(`laneIsolated` 等)是车道轴自己的事实表,不是「部署」的谓词;行读它们。
23
23
  *
24
24
  * 依赖纪律:叶模块 —— 值导入只有 `execution-lane-caps.ts`(它本身是叶),零运行期环。
@@ -20,7 +20,7 @@ export interface ElicitationFrame {
20
20
  * coordinate instead, same posture as the stream-approval frames' `serverNowMs`. */
21
21
  serverNowMs?: number;
22
22
  /** "elicitation" only (additive; the same window pair `question` carries): the coordinator's OWN answer window —
23
- * at expiry the ask releases as `decline`. Minted from the SAME `ttlMs` the `setTimeout` below arms (one source), so
23
+ * at expiry the ask is released as `cancel` (nobody answered; see RELEASE_ACTION). Minted from the SAME `ttlMs` the `setTimeout` below arms (one source), so
24
24
  * a form replayed from the ledger can be judged still-waiting (no `elicitation_complete` with this id ∧
25
25
  * `expiresAtMs` not reached) or lapsed even when the close row never landed (a lost write, a crashed replica). */
26
26
  expiresInMs?: number;
@@ -37,13 +37,13 @@ export interface ElicitationRunContext {
37
37
  abortSignal?: AbortSignal;
38
38
  }
39
39
  export interface ElicitationThrottle {
40
- /** Max concurrent in-flight elicitations per run leg (parallel tool calls can each elicit). Breach ⇒ decline. */
40
+ /** Max concurrent in-flight elicitations per run leg (parallel tool calls can each elicit). Breach ⇒ refused (`decline`). */
41
41
  maxConcurrentPerRun: number;
42
- /** Max total elicitations one run leg will surface to the human. Breach ⇒ decline (flood defense). */
42
+ /** Max total elicitations one run leg will surface to the human. Breach ⇒ refused (`decline`; flood defense). */
43
43
  maxTotalPerRun: number;
44
- /** Min gap (ms) between two elicitations from the SAME server within a run. Within ⇒ decline (burst guard). */
44
+ /** Min gap (ms) between two elicitations from the SAME server within a run. Within ⇒ refused (`decline`; burst guard). */
45
45
  minIntervalMsPerServer: number;
46
- /** An unanswered elicitation auto-declines after this (ms) — the human walked away; don't hold core forever. */
46
+ /** An unanswered elicitation is released as `cancel` after this (ms) — the human walked away; don't hold core forever. */
47
47
  ttlMs: number;
48
48
  }
49
49
  export declare const DEFAULT_ELICITATION_THROTTLE: ElicitationThrottle;
@@ -75,8 +75,9 @@ export declare class ElicitationCoordinator {
75
75
  * the case where the leg ends/throws while an elicitation is parked but the leg's own loop has already drained. */
76
76
  runWithContext<T>(ctx: ElicitationRunContext, fn: () => Promise<T>): Promise<T>;
77
77
  /** `RunnerDeps.onElicit`. Core calls this when an opted-in server elicits the END USER mid-tool-call; the resolved
78
- * value is sent back to the server on the still-open connection. Fail-closed (`decline`/`cancel`) in every
79
- * uncertain case. Arrow property so it can be passed as `onElicit: coordinator.elicit` with `this` bound. */
78
+ * value is sent back to the server on the still-open connection. Fail-closed (never `accept`) in every uncertain
79
+ * case: a rule-based refusal is `decline`, a release nobody answered is `cancel` (RELEASE_ACTION).
80
+ * Arrow property so it can be passed as `onElicit: coordinator.elicit` with `this` bound. */
80
81
  elicit: (req: McpElicitRequest, signal?: AbortSignal) => Promise<McpElicitResponse>;
81
82
  /** `POST /v1/elicitations/:id/respond` — resolve a parked elicitation with the shell's answer. Owner-gated with a
82
83
  * 404 (no existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404.
@@ -5,6 +5,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { refusalBody } from "./http/refusal-body.js";
6
6
  const MAX_ELICIT_MESSAGE_CHARS = 4096;
7
7
  const MAX_ELICIT_SCHEMA_BYTES = 8192;
8
+ const RELEASE_ACTION = { refused: "decline", unanswered: "cancel" };
8
9
  export const DEFAULT_ELICITATION_THROTTLE = {
9
10
  maxConcurrentPerRun: 2,
10
11
  maxTotalPerRun: 20,
@@ -65,7 +66,7 @@ export class ElicitationCoordinator {
65
66
  finally {
66
67
  for (const [, p] of [...this.pending]) {
67
68
  if (p.taskId === ctx.taskId)
68
- p.settle({ action: "cancel" });
69
+ p.settle({ action: RELEASE_ACTION.unanswered });
69
70
  }
70
71
  this.counters.delete(ctx.taskId);
71
72
  }
@@ -74,18 +75,18 @@ export class ElicitationCoordinator {
74
75
  elicit = async (req, signal) => {
75
76
  const ctx = this.als.getStore();
76
77
  if (!ctx)
77
- return { action: "decline" };
78
+ return { action: RELEASE_ACTION.refused };
78
79
  if (typeof req.url === "string" && req.url.length > 0)
79
- return { action: "decline" };
80
+ return { action: RELEASE_ACTION.refused };
80
81
  if (signal?.aborted || ctx.abortSignal?.aborted)
81
- return { action: "cancel" };
82
+ return { action: RELEASE_ACTION.unanswered };
82
83
  const rc = this.countersFor(ctx.taskId);
83
84
  const t = this.now();
84
85
  const last = rc.lastByServer.get(req.server);
85
86
  if (rc.inflight >= this.throttle.maxConcurrentPerRun ||
86
87
  rc.total >= this.throttle.maxTotalPerRun ||
87
88
  (last !== undefined && t - last < this.throttle.minIntervalMsPerServer)) {
88
- return { action: "decline" };
89
+ return { action: RELEASE_ACTION.refused };
89
90
  }
90
91
  const id = uuidv7();
91
92
  rc.inflight += 1;
@@ -107,8 +108,8 @@ export class ElicitationCoordinator {
107
108
  rc.inflight = Math.max(0, rc.inflight - 1);
108
109
  resolveAnswer(r);
109
110
  };
110
- const onAbort = () => settle({ action: "cancel" });
111
- timer = setTimeout(() => settle({ action: "decline" }), this.throttle.ttlMs);
111
+ const onAbort = () => settle({ action: RELEASE_ACTION.unanswered });
112
+ timer = setTimeout(() => settle({ action: RELEASE_ACTION.unanswered }), this.throttle.ttlMs);
112
113
  timer.unref?.();
113
114
  signal?.addEventListener("abort", onAbort, { once: true });
114
115
  ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
@@ -124,7 +125,7 @@ export class ElicitationCoordinator {
124
125
  }
125
126
  else if (delivery === "failed") {
126
127
  recordFailOpen("server.elicitation.open-frame-undelivered", `elicitation=${id} server=${req.server}`);
127
- settle({ action: "decline" });
128
+ settle({ action: RELEASE_ACTION.unanswered });
128
129
  }
129
130
  const answer = await answerP;
130
131
  void emitted
@@ -23,8 +23,10 @@
23
23
  * 前置到它自己的 message 前 —— **不 `instanceof` 单挑任何一个类**(身上有字符串 `code` 就折),该店
24
24
  * 的折点全部改走这一只函数。人与模型因此看得到「这是哪一类失败」。
25
25
  * · **判断面**(同一只函数的 `fault`,core 7.55.0 `FileHistoryError.permanence`):永久码表里的 typed 码 ⇒
26
- * `{ permanence: "terminal" }`,其余 ⇒ `{}`(没判 —— 引擎照旧给重试引导语)。折点把它摊进店答的
27
- * `FileHistoryError`;引擎的拒句引导语、rewind 披露、`terminal.detail` 都读这一格,**不读散文**。
26
+ * `{ permanence: "terminal" }`;**连接类故障**(到库的连接坏了 / 库此刻不接 —— 判官是整仓唯一一只
27
+ * `plugins/store-fault.ts` 的 `storeFaultClassOf`,识别表在 `plugins/sql-errors.ts`,本文件不再抄第二份)⇒
28
+ * `{ permanence: "transient" }`;其余 ⇒ `{}`(没判 —— 引擎照旧给重试引导语)。`terminal` 只对永久码。
29
+ * 折点把它摊进店答的 `FileHistoryError`;引擎的拒句引导语、rewind 披露、`terminal.detail` 都读这一格,**不读散文**。
28
30
  * · **机器面**(同一只函数的副作用):码取自 typed 错**对象**、对闭集词表判定,**绝不从散文里抠**。
29
31
  * 🔴 这一条是 codex R1-[high] 验真后的定形:文本通道**天然可伪造** —— 一个名叫
30
32
  * `(blob_too_large_for_sql).txt` 的文件读不出来时,它的路径就在 `snapshot_failed` 的散文里,任何
@@ -51,8 +53,8 @@ export interface CaughtFailure {
51
53
  *
52
54
  * `text` = 散文形:带 typed `code` 的前缀一个括号词(`(blob_too_large_for_sql) blob of … exceeds …`),
53
55
  * 不带的**逐字**是原 message(transient 失败的文案因此与本批之前一字不差)。
54
- * `fault` = 判断形(core 7.55.0 `FileHistoryError.permanence`):永久码表里的码 ⇒ `{ permanence: "terminal" }`;其余 ⇒ `{}`(店不知道 ——
55
- * 驱动 / 库的错码是不是会自己好,不归本店判)。
56
+ * `fault` = 判断形(core 7.55.0 `FileHistoryError.permanence`):永久码表里的码 ⇒ `{ permanence: "terminal" }`;连接类故障 ⇒ `{ permanence: "transient" }`
57
+ * (什么都不改稍后再试可能成);其余 ⇒ `{}`(店不知道 —— 认不出的驱动 / 库错是不是会自己好,不归本店判)。永久码先于连接类判(一只错不会两个都是)。
56
58
  * 副作用 = 机器形:码取自错误对象,逐次计数 + 每进程每码一条 warn。观测绝不抛。
57
59
  */
58
60
  export declare function foldCaughtFailure(err: unknown): CaughtFailure;
@@ -1,4 +1,5 @@
1
1
  import { FILE_HISTORY_ERROR_CODES, FileHistoryStoreError } from "@sema-agent/core";
2
+ import { storeFaultClassOf } from "./plugins/store-fault.js";
2
3
  const PERMANENT_STORE_FAILURE_CODES = {
3
4
  blob_too_large_for_sql: {
4
5
  note: "a blob is larger than this deployment's SQL snapshot-blob cap — the same file will fail every turn until the deployment changes",
@@ -32,12 +33,27 @@ function drainToMetrics() {
32
33
  export function foldCaughtFailure(err) {
33
34
  const message = err instanceof Error ? err.message : String(err);
34
35
  const code = err?.code;
35
- if (typeof code !== "string" || code === "")
36
- return { text: message, fault: {} };
37
- const permanent = isPermanentCode(code);
38
- if (permanent)
39
- recordPermanent(code, message);
40
- return { text: `(${code}) ${message}`, fault: permanent ? { permanence: "terminal" } : {} };
36
+ const typedCode = typeof code === "string" && code !== "" ? code : undefined;
37
+ const permanentCode = typedCode !== undefined && isPermanentCode(typedCode) ? typedCode : undefined;
38
+ if (permanentCode !== undefined)
39
+ recordPermanent(permanentCode, message);
40
+ return {
41
+ text: typedCode !== undefined ? `(${typedCode}) ${message}` : message,
42
+ fault: permanentCode !== undefined ? { permanence: "terminal" } : faultOfClass(storeFaultClassOf(err)),
43
+ };
44
+ }
45
+ function faultOfClass(cls) {
46
+ switch (cls) {
47
+ case "connection":
48
+ return { permanence: "transient" };
49
+ case "corrupt":
50
+ case "unclassified":
51
+ return {};
52
+ default: {
53
+ const unhandled = cls;
54
+ throw new Error(`store fault class without a file-history judgment: ${String(unhandled)}`);
55
+ }
56
+ }
41
57
  }
42
58
  function safely(record) {
43
59
  try {
@@ -25,11 +25,15 @@
25
25
  * 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
26
26
  * (worker,instanceId,windowStartMs) 重发安全;**发送失败 → merge 回累计器**,并入下一窗(失败窗从未入账,
27
27
  * 向前合并不会双计 —— 只延迟,不丢量)。
28
+ * - **用量按中心凭证身份分箱([ref])**:累计器一个身份一只({@link createFleetUsageBins},分箱规则 = `createIdentityBins` 一只),
29
+ * tracer 记进记账身份那一箱(执行开始时钉下的身份;执行外 = 此刻身份);用量环只报此刻身份那一箱、经钉在它身份上的凭证发 —— 文件形凭证进程内换人后,上一个人窗里的用量
30
+ * 不在下一个人的凭证下上报(不记到别人头上);它留在原箱,那个身份回来时照常报出去,收尾时还压着就响亮一行。
28
31
  * - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
29
32
  */
30
33
  import { UsageReport, type CapabilityReading, type WebAdminCapabilities, type ModelRefusalsSignal } from "@sema-agent/settings-schema/fleet";
31
34
  import { type EngineSignalFacts } from "./fleet/engine-signal.js";
32
35
  import { type CenterEndpoint } from "./web-admin-client/center-request.js";
36
+ import { type CenterCredentialSource } from "./center-credential.js";
33
37
  /** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
34
38
  * `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
35
39
  export declare const FLEET_USAGE_KEY_CAP = 1024;
@@ -71,6 +75,32 @@ export declare class FleetUsageAccumulator {
71
75
  mergeBack(entries: UsageReport["entries"]): void;
72
76
  get size(): number;
73
77
  }
78
+ /** [ref] —— 一个中心凭证身份的用量箱:这个身份窗里的累计格,与钉在这个身份上的凭证(用量环只经它报这一箱)。 */
79
+ export interface FleetUsageBin {
80
+ readonly usage: FleetUsageAccumulator;
81
+ readonly credential: CenterCredentialSource;
82
+ }
83
+ /**
84
+ * [ref] —— **按中心凭证身份分箱的用量账**(活对象 ⇒ `create*`)。tracer / side-query 记账写 {@link record}(此刻身份那一箱);
85
+ * 用量环读 {@link current}(此刻身份那一箱 + 钉住的凭证)。分箱规则只有 `createIdentityBins` 一只(与租约管理器、per-principal caps 同规则)。
86
+ */
87
+ export interface FleetUsageBins {
88
+ /** 记进**记账身份**那一箱(执行里 = 执行开始时钉下的身份;执行外 = 此刻身份)。 */
89
+ record(principal: string | undefined, model: string, taskId: string, d: {
90
+ inputTokens: number;
91
+ outputTokens: number;
92
+ costMicroUsd?: number;
93
+ weightedTokens?: number;
94
+ quotaWeightAtUse?: number;
95
+ }): void;
96
+ /** 把这次执行的记账身份钉在此刻(tracer 在 `task.start` 调;规则属 `center-credential.ts` `pinAccountingIdentity`)。 */
97
+ pinExecution(): void;
98
+ /** 此刻身份那一箱(用量环上报读它)。 */
99
+ current(): FleetUsageBin;
100
+ /** **全部**箱里还压着的条目数(收尾那一行 warn 用:最后一窗之后还压着的,进程退出即丢 —— 别的身份的、身份未知的、或最后一窗没报成的,同一待遇)。 */
101
+ held(): number;
102
+ }
103
+ export declare function createFleetUsageBins(credential: CenterCredentialSource): FleetUsageBins;
74
104
  export interface FleetClientOpts {
75
105
  /** [ref]:凭证是**来源**(每条公告 / 用量 / 注销请求各取一次值),不是启动期烧进 header 的字符串。 */
76
106
  center: CenterEndpoint & {
@@ -92,7 +122,8 @@ export interface FleetClientOpts {
92
122
  * 只有 `declared`(交出宣告的位)与 `undeclared`(交 `undefined` = 老中心没宣告)是读数;`unreadable`(体读不出 / 不是对象 /
93
123
  * `capabilities` 键在场而形坏)不是读数 ⇒ 不调,上一次读数与它的钟原样。失败的公告同样不调(没听到 ≠ 没有)。 */
94
124
  onCenterCapabilities?: (caps: WebAdminCapabilities | undefined) => void;
95
- usage?: FleetUsageAccumulator;
125
+ /** [ref]:按凭证身份分箱的用量账(缺席 = 用量环不装)。 */
126
+ usage?: FleetUsageBins;
96
127
  heartbeatMs?: number;
97
128
  usageWindowMs?: number;
98
129
  logger?: {
@@ -2,6 +2,7 @@ import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS, readCapability
2
2
  import { ENGINE_SIGNAL_KEY, buildEngineSignal, fleetMembershipKeys, heartbeatReadinessKeys } from "./fleet/engine-signal.js";
3
3
  import { centerFetch, centerLaneConfigured } from "./web-admin-client/center-request.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
+ import { createIdentityBins, pinAccountingIdentity } from "./center-credential.js";
5
6
  import { WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
6
7
  export const FLEET_USAGE_KEY_CAP = 1024;
7
8
  export const FLEET_CLIENT_FETCH_TIMEOUT_MS = 5000;
@@ -67,6 +68,20 @@ export class FleetUsageAccumulator {
67
68
  return this.cells.size;
68
69
  }
69
70
  }
71
+ export function createFleetUsageBins(credential) {
72
+ const made = [];
73
+ const bins = createIdentityBins(credential, (pinned) => {
74
+ const bin = { usage: new FleetUsageAccumulator(), credential: pinned };
75
+ made.push(bin);
76
+ return bin;
77
+ });
78
+ return {
79
+ record: (principal, model, taskId, d) => bins.accounting().usage.record(principal, model, taskId, d),
80
+ pinExecution: () => pinAccountingIdentity(credential),
81
+ current: bins.now,
82
+ held: () => made.reduce((n, b) => n + b.usage.size, 0),
83
+ };
84
+ }
70
85
  async function rejectionSnippet(res) {
71
86
  try {
72
87
  const text = (await res.text()).slice(0, 200).trim();
@@ -90,7 +105,7 @@ export function startFleetClient(opts) {
90
105
  const f = opts.fetchImpl ?? fetch;
91
106
  const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
92
107
  const usageWindowMs = opts.usageWindowMs ?? 60_000;
93
- const send = (route, method, json) => centerFetch(opts.center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
108
+ const send = (route, method, json, center = opts.center) => centerFetch(center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
94
109
  let stopped = false;
95
110
  let failStreak = 0;
96
111
  let unreadableStreak = 0;
@@ -177,20 +192,21 @@ export function startFleetClient(opts) {
177
192
  const flushUsage = async () => {
178
193
  if (!opts.usage)
179
194
  return;
195
+ const bin = opts.usage.current();
180
196
  const windowEndMs = Date.now();
181
- const entries = opts.usage.drain();
197
+ const entries = bin.usage.drain();
182
198
  const start = windowStartMs;
183
199
  windowStartMs = windowEndMs;
184
200
  if (entries.length === 0)
185
201
  return;
186
202
  const report = { worker: opts.center.worker, instanceId: opts.instanceId, windowStartMs: start, windowEndMs, entries };
187
203
  try {
188
- const { res } = await send("fleet-usage", "POST", { reports: [report] });
204
+ const { res } = await send("fleet-usage", "POST", { reports: [report] }, { ...opts.center, credential: bin.credential });
189
205
  if (!res.ok)
190
206
  throw new Error(`usage ${res.status}${await rejectionSnippet(res)}`);
191
207
  }
192
208
  catch (err) {
193
- opts.usage.mergeBack(entries);
209
+ bin.usage.mergeBack(entries);
194
210
  opts.logger?.warn("fleet_usage_report_failed", { error: err instanceof Error ? err.message : String(err), entries: entries.length });
195
211
  }
196
212
  };
@@ -227,6 +243,10 @@ export function startFleetClient(opts) {
227
243
  if (uw)
228
244
  clearInterval(uw);
229
245
  await flushUsage();
246
+ const held = opts.usage?.held() ?? 0;
247
+ if (held > 0) {
248
+ opts.logger?.warn("fleet_usage_unreported_identity", { entries: held, note: "usage still held at exit is dropped: it accrued under another center credential identity (never reported under the current one — it would be billed to the wrong account), under an unknown identity (credential unreadable), or its final report failed" });
249
+ }
230
250
  };
231
251
  return { announceNow, stopRouting, flushFinalUsage };
232
252
  }
@@ -3,7 +3,7 @@ import { type CenterEndpoint } from "./web-admin-client/center-request.js";
3
3
  export declare const NOT_LEASE_TTL_MS: number;
4
4
  /** center 判 budget exhausted 后,多久内的重复提交直接拒绝而不再打 center(防 per-request 打爆)。 */
5
5
  export declare const EXHAUSTED_RETRY_MS: number;
6
- /** principal 状态表上界(镜像 FLEET_USAGE_KEY_CAP 的姿势):超界丢最旧 —— 被丢的 principal 下次 admit
6
+ /** principal 状态表上界(**每个身份箱各一份**;镜像 FLEET_USAGE_KEY_CAP 的姿势):超界丢最旧 —— 被丢的 principal 下次 admit
7
7
  * 重新协商,一次多余 RTT,无正确性影响。 */
8
8
  export declare const LEASE_PRINCIPAL_CAP = 4096;
9
9
  /** 复审 O1:lease 协商 RTT 的硬上限——leaseDenied 在提交热路径同步 await 它,一个 accept-but-hang 的
@@ -87,8 +87,8 @@ export interface FleetLeaseOpts {
87
87
  }
88
88
  export declare class FleetLeaseManager {
89
89
  private readonly opts;
90
- private readonly state;
91
- private readonly inflight;
90
+ /** 记账身份的那一箱(惰建;分箱规则只有 `createIdentityBins` 一只,取箱口 `accounting`)。 */
91
+ private readonly binNow;
92
92
  private readonly f;
93
93
  private readonly now;
94
94
  private failStreak;
@@ -97,6 +97,8 @@ export declare class FleetLeaseManager {
97
97
  /** [ref] ③ 生效姿态(caps `centerWiring.failMode` 读的就是它 —— 与执行的那一只同源)。 */
98
98
  readonly failMode: LeaseFailMode;
99
99
  constructor(opts: FleetLeaseOpts);
100
+ /** [ref]:把这次执行的记账身份钉在此刻(tracer 在执行的第一帧 `task.start` 调;规则属 `center-credential.ts` `pinAccountingIdentity`)。 */
101
+ pinExecution(): void;
100
102
  /**
101
103
  * Admission check for one billed submission. Fast path is synchronous-shaped (live lease / fresh negative
102
104
  * cache → no await beyond the microtask); only first-touch / expiry / exhaustion pays a center RTT.
@@ -119,6 +121,7 @@ export declare class FleetLeaseManager {
119
121
  /**
120
122
  * [ref] ② —— 调用方这一刻的租约读数(caps `centerWiring.caller` / `leaseHealth` 的唯一来源;7.105.0 的 `peek()` 退役:它只回 kind、
121
123
  * 不看 `until`,过期条目照回原 kind)。**纯读**:不打中心、不写状态、不动 LRU 序(`Map.get` 不改插入序)—— caps 路由自己不 admit。
124
+ * 读的是**此刻凭证身份**那一箱([ref];取箱要读一次凭证,首次见到的身份惰建一只空箱 —— 空箱不是状态)。
122
125
  * 规则表(设计稿 v1.1 §2②,逐行钉在 `test/fleet-lease.test.ts`):
123
126
  * · principal 缺席 / 无条目(含被 LRU 逐出)⇒ `unobserved`;
124
127
  * · `not-lease` 且 `now < until` ⇒ `not-enforced`;到期 ⇒ `unobserved`;
@@ -135,6 +138,7 @@ export declare class FleetLeaseManager {
135
138
  /** [ref] ③:两条连接级失败臂的**唯一**收尾 —— 按姿态穷举(加一种姿态 = 编译红)。 */
136
139
  private connectionFailure;
137
140
  private set;
141
+ /** 单飞在箱内:同一身份同一 principal 的并发谈判共用一条;换了人的谈判各发各的([ref])。 */
138
142
  private issueOrRenew;
139
143
  private issueOrRenewInner;
140
144
  }
@@ -1,5 +1,5 @@
1
1
  import { QuotaLease, leaseExhausted, shouldRenewLease } from "@sema-agent/settings-schema/fleet";
2
- import { CenterCredentialUnavailableError } from "./center-credential.js";
2
+ import { CenterCredentialUnavailableError, createIdentityBins, pinAccountingIdentity } from "./center-credential.js";
3
3
  import { centerFetch, centerLaneConfigured } from "./web-admin-client/center-request.js";
4
4
  export const NOT_LEASE_TTL_MS = 10 * 60 * 1000;
5
5
  export const EXHAUSTED_RETRY_MS = 30 * 1000;
@@ -23,8 +23,7 @@ const LEASE_HEALTH_BY_OUTCOME = {
23
23
  };
24
24
  export class FleetLeaseManager {
25
25
  opts;
26
- state = new Map();
27
- inflight = new Map();
26
+ binNow;
28
27
  f;
29
28
  now;
30
29
  failStreak = 0;
@@ -35,10 +34,19 @@ export class FleetLeaseManager {
35
34
  this.f = opts.fetchImpl ?? fetch;
36
35
  this.now = opts.now ?? Date.now;
37
36
  this.failMode = opts.failMode;
37
+ this.binNow = createIdentityBins(opts.center.credential, (pinned) => ({
38
+ state: new Map(),
39
+ inflight: new Map(),
40
+ center: { ...opts.center, credential: pinned },
41
+ })).accounting;
42
+ }
43
+ pinExecution() {
44
+ pinAccountingIdentity(this.opts.center.credential);
38
45
  }
39
46
  async admit(principal) {
47
+ const bin = this.binNow();
40
48
  const now = this.now();
41
- const st = this.state.get(principal);
49
+ const st = bin.state.get(principal);
42
50
  if (st?.kind === "not-lease" && now < st.until)
43
51
  return { ok: true };
44
52
  if (st?.kind === "exhausted" && now < st.until) {
@@ -46,15 +54,15 @@ export class FleetLeaseManager {
46
54
  }
47
55
  if (st?.kind === "lease" && !leaseExhausted(st.lease, now)) {
48
56
  if (shouldRenewLease(st.lease, now))
49
- void this.issueOrRenew(principal).catch(() => undefined);
57
+ void this.issueOrRenew(bin, principal).catch(() => undefined);
50
58
  return { ok: true };
51
59
  }
52
- return this.issueOrRenew(principal);
60
+ return this.issueOrRenew(bin, principal);
53
61
  }
54
62
  recordSpend(principal, d) {
55
63
  if (!principal)
56
64
  return;
57
- const st = this.state.get(principal);
65
+ const st = this.binNow().state.get(principal);
58
66
  if (st?.kind !== "lease")
59
67
  return;
60
68
  st.lease.consumed.weightedTokens += d.weightedTokens ?? 0;
@@ -62,7 +70,7 @@ export class FleetLeaseManager {
62
70
  }
63
71
  snapshot(principal, now) {
64
72
  const leaseHealth = LEASE_HEALTH_BY_OUTCOME[this.lastAdmit];
65
- const st = principal === undefined ? undefined : this.state.get(principal);
73
+ const st = principal === undefined ? undefined : this.binNow().state.get(principal);
66
74
  if (st === undefined)
67
75
  return { leaseHealth, caller: { lease: "unobserved" } };
68
76
  switch (st.kind) {
@@ -97,25 +105,25 @@ export class FleetLeaseManager {
97
105
  }
98
106
  }
99
107
  }
100
- set(principal, st) {
101
- if (!this.state.has(principal) && this.state.size >= LEASE_PRINCIPAL_CAP) {
102
- const oldest = this.state.keys().next().value;
103
- this.state.delete(oldest);
108
+ set(bin, principal, st) {
109
+ if (!bin.state.has(principal) && bin.state.size >= LEASE_PRINCIPAL_CAP) {
110
+ const oldest = bin.state.keys().next().value;
111
+ bin.state.delete(oldest);
104
112
  }
105
- this.state.delete(principal);
106
- this.state.set(principal, st);
113
+ bin.state.delete(principal);
114
+ bin.state.set(principal, st);
107
115
  }
108
- issueOrRenew(principal) {
109
- const existing = this.inflight.get(principal);
116
+ issueOrRenew(bin, principal) {
117
+ const existing = bin.inflight.get(principal);
110
118
  if (existing)
111
119
  return existing;
112
- const p = this.issueOrRenewInner(principal).finally(() => this.inflight.delete(principal));
113
- this.inflight.set(principal, p);
120
+ const p = this.issueOrRenewInner(bin, principal).finally(() => bin.inflight.delete(principal));
121
+ bin.inflight.set(principal, p);
114
122
  return p;
115
123
  }
116
- async issueOrRenewInner(principal) {
124
+ async issueOrRenewInner(bin, principal) {
117
125
  try {
118
- const { res } = await centerFetch(this.opts.center, { kind: "fleet-leases" }, {
126
+ const { res } = await centerFetch(bin.center, { kind: "fleet-leases" }, {
119
127
  method: "POST",
120
128
  json: { principal, worker: this.opts.center.worker },
121
129
  timeoutMs: LEASE_FETCH_TIMEOUT_MS,
@@ -124,7 +132,7 @@ export class FleetLeaseManager {
124
132
  if (res.status === 200) {
125
133
  const body = (await res.json());
126
134
  const lease = QuotaLease.parse(body.lease);
127
- this.set(principal, { kind: "lease", lease });
135
+ this.set(bin, principal, { kind: "lease", lease });
128
136
  if (this.failStreak > 0)
129
137
  this.opts.logger?.info?.("fleet_lease_recovered", { after: this.failStreak });
130
138
  this.failStreak = 0;
@@ -145,11 +153,11 @@ export class FleetLeaseManager {
145
153
  ...(body.pool !== undefined ? { pool: body.pool } : {}),
146
154
  };
147
155
  const hasDetail = Object.keys(detail).length > 0;
148
- this.set(principal, { kind: "exhausted", until, ...(hasDetail ? { detail } : {}) });
156
+ this.set(bin, principal, { kind: "exhausted", until, ...(hasDetail ? { detail } : {}) });
149
157
  this.opts.metrics?.inc("fleet_lease_exhausted_total");
150
158
  return { ok: false, reason: "budget_exhausted", retryAfterSec: Math.ceil(EXHAUSTED_RETRY_MS / 1000), ...(hasDetail ? { detail } : {}) };
151
159
  }
152
- this.set(principal, { kind: "not-lease", until: this.now() + NOT_LEASE_TTL_MS });
160
+ this.set(bin, principal, { kind: "not-lease", until: this.now() + NOT_LEASE_TTL_MS });
153
161
  return { ok: true };
154
162
  }
155
163
  if (res.status === 401 || res.status === 403 || res.status === 400) {
@@ -2,6 +2,7 @@ import { admitRouteRequest, BrainError, joinRequestUrl, resolveRouteCredential,
2
2
  import { matchCatalogModel, refusedCatalogEntry, servedModelTableLeg } from "../model-select.js";
3
3
  import { echoModelRef, modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
5
+ import { anthropicLaneCredentialOf } from "../model-route-gap.js";
5
6
  import { anthropicRouteRoot, brainLaneFor, routeTableOf } from "../model-provider.js";
6
7
  import { egressRedirectRefusal } from "../egress-redirect.js";
7
8
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
@@ -33,6 +34,16 @@ function gateRefusal(pick, e, declaredRoot) {
33
34
  error: `hook model "${pick.catalogRef}" cannot be routed (${code}) — ` + buildDisplaySafeRefusal(text, [pick.baseUrl, declaredRoot ?? ""]),
34
35
  };
35
36
  }
37
+ function routeCredentialHeader(credential) {
38
+ if (credential === undefined)
39
+ return {};
40
+ switch (credential.kind) {
41
+ case "x-api-key":
42
+ return { "x-api-key": credential.value };
43
+ case "bearer":
44
+ return { authorization: `Bearer ${credential.value}` };
45
+ }
46
+ }
36
47
  export function createHookLlm(deps) {
37
48
  const { config, metrics } = deps;
38
49
  const anthropicRouteBaseUrl = () => anthropicRouteRoot(config.anthropic ?? {});
@@ -122,9 +133,10 @@ export function createHookLlm(deps) {
122
133
  let admitted;
123
134
  let declaredRoot;
124
135
  if (pick.api === "anthropic-messages") {
136
+ const routeCredential = anthropicLaneCredentialOf(config.anthropic ?? {});
125
137
  const routeBase = anthropicRouteBaseUrl();
126
138
  declaredRoot = routeBase;
127
- const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
139
+ const routed = judgeRoute(pick, perModelKey, routeCredential?.value, routeBase);
128
140
  if (!routed.ok)
129
141
  return routed;
130
142
  admitted = routed.admission;
@@ -132,13 +144,7 @@ export function createHookLlm(deps) {
132
144
  headers = {
133
145
  "content-type": "application/json",
134
146
  "anthropic-version": config.anthropic?.version ?? "2023-06-01",
135
- ...(routed.credential.source === "per-model"
136
- ? { "x-api-key": routed.credential.apiKey ?? "" }
137
- : config.anthropic?.authToken
138
- ? { authorization: `Bearer ${config.anthropic.authToken}` }
139
- : config.anthropic?.apiKey
140
- ? { "x-api-key": config.anthropic.apiKey }
141
- : {}),
147
+ ...(routed.credential.source === "per-model" ? { "x-api-key": routed.credential.apiKey ?? "" } : routeCredentialHeader(routeCredential)),
142
148
  };
143
149
  const want = maxOutputTokens ?? 1024;
144
150
  const maxTokens = Math.min(config.models[pick.catalogRef]?.maxTokens ?? want, want);