@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.
- package/MIGRATION.md +64 -2
- package/USAGE.md +62 -25
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-card.d.ts +60 -4
- package/dist/approval-card.js +18 -1
- package/dist/approval-content-kind.d.ts +1 -0
- package/dist/approval-content-kind.js +1 -1
- package/dist/auth-bridge.d.ts +34 -5
- package/dist/auth-bridge.js +20 -3
- package/dist/boot/budget-tracing.d.ts +1 -2
- package/dist/boot/budget-tracing.js +2 -2
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/resolve-spec.js +10 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-06-runners.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stores.js +2 -3
- package/dist/boot/web-admin.js +10 -17
- package/dist/brain.js +12 -3
- package/dist/budget.d.ts +3 -3
- package/dist/budget.js +4 -0
- package/dist/capabilities/mcp-probe.d.ts +5 -4
- package/dist/capability-facts.d.ts +3 -7
- package/dist/capability-facts.js +3 -5
- package/dist/center-credential.d.ts +18 -4
- package/dist/center-credential.js +18 -3
- package/dist/config-catalog.js +12 -10
- package/dist/config-d-migration.js +1 -1
- package/dist/config-invariants.d.ts +1 -1
- package/dist/config-invariants.js +8 -0
- package/dist/config-lkg.d.ts +0 -38
- package/dist/config-lkg.js +1 -83
- package/dist/config-types.d.ts +12 -4
- package/dist/config.js +26 -8
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/elicitation.d.ts +8 -7
- package/dist/elicitation.js +9 -8
- package/dist/file-history-disclosure.d.ts +6 -4
- package/dist/file-history-disclosure.js +22 -6
- package/dist/fleet-client.d.ts +32 -1
- package/dist/fleet-client.js +24 -4
- package/dist/fleet-lease.d.ts +7 -3
- package/dist/fleet-lease.js +31 -23
- package/dist/hooks/hook-llm.js +14 -8
- package/dist/hooks/hook-runner.d.ts +4 -4
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +28 -9
- package/dist/http/checkpoint-ctx.js +5 -1
- package/dist/http/dispatch.js +9 -9
- package/dist/http/resume-ctx-read.d.ts +2 -16
- package/dist/http/resume-ctx-read.js +2 -3
- package/dist/http/routes/approvals-assistant.d.ts +0 -2
- package/dist/http/routes/approvals-assistant.js +47 -12
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +31 -9
- package/dist/http/routes/sessions.js +18 -14
- package/dist/http/submit-ctx.js +1 -1
- package/dist/http/wire-types.d.ts +5 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model-route-gap.d.ts +49 -4
- package/dist/model-route-gap.js +34 -1
- package/dist/object-store-buckets.d.ts +13 -0
- package/dist/object-store-buckets.js +3 -0
- package/dist/observability/fail-open.d.ts +6 -6
- package/dist/observability/fail-open.js +6 -6
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/principal-context.d.ts +4 -1
- package/dist/observability/principal-context.js +6 -1
- package/dist/parked-decide.d.ts +4 -3
- package/dist/parked-decide.js +26 -7
- package/dist/peer-directory.d.ts +2 -1
- package/dist/peer-directory.js +5 -5
- package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
- package/dist/plugins/approval-ask-store-sql.js +3 -0
- package/dist/plugins/blob-backend.js +2 -1
- package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
- package/dist/plugins/checkpoint-index-columns.js +28 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
- package/dist/plugins/checkpoint-store-sql.js +58 -29
- package/dist/plugins/file-history-store-sql.d.ts +6 -0
- package/dist/plugins/file-history-store-sql.js +30 -23
- package/dist/plugins/local-checkpoint-store.js +4 -3
- package/dist/plugins/one-time-migrations.d.ts +21 -28
- package/dist/plugins/one-time-migrations.js +39 -123
- package/dist/plugins/remote-env-k8s.d.ts +18 -5
- package/dist/plugins/remote-env-k8s.js +24 -7
- package/dist/plugins/s3-answer.d.ts +15 -0
- package/dist/plugins/s3-answer.js +5 -0
- package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
- package/dist/plugins/snapshot-bucket-presence.js +35 -7
- package/dist/plugins/sql-errors.d.ts +39 -0
- package/dist/plugins/sql-errors.js +49 -1
- package/dist/plugins/store-fault.d.ts +4 -0
- package/dist/plugins/store-fault.js +8 -0
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/plugins/web-search.js +2 -2
- package/dist/request-declaration-refusal.d.ts +94 -0
- package/dist/request-declaration-refusal.js +78 -0
- package/dist/request-image-urls.d.ts +6 -0
- package/dist/request-image-urls.js +27 -0
- package/dist/request-key-closure.d.ts +5 -0
- package/dist/request-key-closure.js +4 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/sandbox-pkg-source.d.ts +28 -1
- package/dist/sandbox-pkg-source.js +1 -2
- package/dist/sealed-key.d.ts +19 -0
- package/dist/sealed-key.js +47 -4
- package/dist/session-incarnation.d.ts +16 -5
- package/dist/session-incarnation.js +7 -4
- package/dist/session-row.d.ts +3 -17
- package/dist/session-row.js +4 -0
- package/dist/session-sync.d.ts +15 -3
- package/dist/session-sync.js +5 -2
- package/dist/task-a2a.d.ts +52 -81
- package/dist/task-a2a.js +219 -89
- package/dist/task-mcp.d.ts +6 -14
- package/dist/task-mcp.js +16 -51
- package/dist/tool-approval.d.ts +23 -10
- package/dist/tool-approval.js +51 -36
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/exit-seat.js +37 -13
- package/dist/trace/redact.d.ts +28 -12
- package/dist/trace/redact.js +233 -94
- package/dist/trace/self-minted-wire.d.ts +267 -0
- package/dist/trace/self-minted-wire.js +79 -0
- package/dist/url-credential-refusal.d.ts +52 -0
- package/dist/url-credential-refusal.js +14 -0
- package/dist/url-refusal.d.ts +13 -4
- package/dist/url-refusal.js +8 -2
- package/dist/workspace-space.d.ts +27 -0
- package/dist/workspace-space.js +26 -0
- package/package.json +3 -2
- package/scripts/drain-check-local.mjs +2 -1
- package/scripts/drain-check.mysql.sql +3 -1
- package/scripts/drain-check.pg.sql +3 -1
- package/skills/schedule.md +1 -1
- package/wire-changes/server@7.104.0.json +134 -0
- package/wire-changes/server@7.105.0.json +53 -0
- package/wire-changes/server@7.106.0.json +475 -0
- package/wire-changes/server@7.107.0.json +64 -0
- package/wire-changes/server@7.108.0.json +153 -0
- package/wire-changes/server@7.109.0.json +46 -0
- package/wire-changes/server@7.110.0.json +90 -0
- package/wire-changes/server@7.111.0.json +86 -0
- package/wire-changes/server@7.112.0.json +60 -0
- package/wire-changes/server@7.113.0.json +26 -0
- package/wire-changes/server@7.114.0.json +26 -0
- package/wire-changes/server@7.115.0.json +97 -0
- package/wire-changes/server@7.116.0.json +218 -0
- package/wire-changes/server@7.116.1.json +26 -0
- package/wire-changes/server@7.117.0.json +65 -0
- package/wire-changes/server@7.118.0.json +129 -0
- package/wire-changes/server@7.119.0.json +65 -0
- package/wire-changes/server@7.120.0.json +89 -0
- package/wire-changes/server@7.121.0.json +219 -0
- package/wire-changes/server@7.122.0.json +54 -0
- package/wire-changes/server@7.123.0.json +26 -0
- package/wire-changes/server@7.124.0.json +336 -0
- package/wire-changes/server@7.124.1.json +26 -0
- package/wire-changes/server@7.125.0.json +111 -0
- package/wire-changes/server@7.126.0.json +285 -0
- package/wire-changes/server@7.127.0.json +77 -0
- package/wire-changes/server@7.127.1.json +26 -0
- package/wire-changes/server@7.127.2.json +53 -0
- package/wire-changes/server@7.128.0.json +26 -0
- package/wire-changes/server@7.129.0.json +106 -0
- package/wire-changes/server@7.129.1.json +26 -0
- package/wire-changes/server@7.130.0.json +26 -0
- package/wire-changes/server@7.131.0.json +118 -0
- package/wire-changes/server@7.132.0.json +226 -0
- package/wire-changes/server@7.133.0.json +1155 -0
- package/wire-changes/server@7.134.0.json +107 -0
- package/wire-changes/server@7.135.0.json +174 -0
- package/wire-changes/server@7.136.0.json +448 -0
- package/dist/url-credential.d.ts +0 -52
- 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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
-
...(
|
|
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
|
-
* (`
|
|
21
|
+
* (`mcpInjectionHonored` = 锁 ∧ 合规 ∧ `!multiTenant`;`taskHooksHonored`;身份门 …),读行、不读原始键。
|
|
22
22
|
* 车道表(`execution-lane-caps.ts`)按车道词键控的布尔列(`laneIsolated` 等)是车道轴自己的事实表,不是「部署」的谓词;行读它们。
|
|
23
23
|
*
|
|
24
24
|
* 依赖纪律:叶模块 —— 值导入只有 `execution-lane-caps.ts`(它本身是叶),零运行期环。
|
package/dist/elicitation.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 (`
|
|
79
|
-
*
|
|
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.
|
package/dist/elicitation.js
CHANGED
|
@@ -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:
|
|
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:
|
|
78
|
+
return { action: RELEASE_ACTION.refused };
|
|
78
79
|
if (typeof req.url === "string" && req.url.length > 0)
|
|
79
|
-
return { action:
|
|
80
|
+
return { action: RELEASE_ACTION.refused };
|
|
80
81
|
if (signal?.aborted || ctx.abortSignal?.aborted)
|
|
81
|
-
return { action:
|
|
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:
|
|
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:
|
|
111
|
-
timer = setTimeout(() => settle({ action:
|
|
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:
|
|
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
|
-
* `
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 {
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
125
|
+
/** [ref]:按凭证身份分箱的用量账(缺席 = 用量环不装)。 */
|
|
126
|
+
usage?: FleetUsageBins;
|
|
96
127
|
heartbeatMs?: number;
|
|
97
128
|
usageWindowMs?: number;
|
|
98
129
|
logger?: {
|
package/dist/fleet-client.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/dist/fleet-lease.d.ts
CHANGED
|
@@ -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 状态表上界(
|
|
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
|
-
|
|
91
|
-
private readonly
|
|
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
|
}
|
package/dist/fleet-lease.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 (!
|
|
102
|
-
const oldest =
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
113
|
+
bin.state.delete(principal);
|
|
114
|
+
bin.state.set(principal, st);
|
|
107
115
|
}
|
|
108
|
-
issueOrRenew(principal) {
|
|
109
|
-
const existing =
|
|
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(() =>
|
|
113
|
-
|
|
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(
|
|
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) {
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -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,
|
|
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);
|