@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/158 A10:composition root 分段 —— session 只读/删除面(审计、watch 注册表、E21 purge 协调器、
|
|
3
|
-
* degenerate 仪表、plan-cache 探针)。
|
|
4
|
-
*
|
|
5
|
-
* 纯搬运:函数体逐字来自 `main.ts`(原 2967-3092 行),缩进不变;新增的只有 import 与包壳。
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ **位置即契约**:
|
|
8
|
-
* 1. `sessionWatchRegistry` 必须在 `purgeSession` **之前**构造 —— purge 尾部的 `dropSession` 是
|
|
9
|
-
* 同副本租户轮转围栏(delete 即终结旧 owner 的 SSE 订阅),两者是一对。
|
|
10
|
-
* 2. `setLeafAdvanceListener(...)` 是**进程级单点**装载(leaf-bus 只有一个监听位),必须与
|
|
11
|
-
* registry 的构造同段发生,晚于它、早于任何会 append 的路由。
|
|
12
|
-
*/
|
|
13
1
|
import { buildSessionAudit, buildSessionAuditLocal, buildSessionAuditPg } from "../audit.js";
|
|
14
2
|
import { makeDegenerateInstrument } from "../degenerate-instrument.js";
|
|
15
3
|
import { PlanCacheProbe } from "../plan-cache-probe.js";
|
|
@@ -19,11 +7,9 @@ import { SessionWatchRegistry, posIntEnv } from "../session-watch.js";
|
|
|
19
7
|
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
20
8
|
export function createSessionFaces(ctx) {
|
|
21
9
|
const { config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore, resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore, workflowCompletionInbox, taskListLane, } = ctx;
|
|
22
|
-
// Audit回溯 + degenerate-output instrument are TiDB-specific raw-SQL consumers (outside the store
|
|
23
|
-
// abstraction); they run only on the TiDB backend (the mysql2 pool), and are a no-op on PG until ported.
|
|
24
10
|
const mysqlPool = backend?.mysqlPool();
|
|
25
|
-
const auditPgPool = backend?.pgPool();
|
|
26
|
-
const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
|
|
11
|
+
const auditPgPool = backend?.pgPool();
|
|
12
|
+
const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
|
|
27
13
|
const ownerAware = sessionStore;
|
|
28
14
|
const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
|
|
29
15
|
? async (sessionId) => {
|
|
@@ -37,24 +23,6 @@ export function createSessionFaces(ctx) {
|
|
|
37
23
|
return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
|
|
38
24
|
}
|
|
39
25
|
: undefined;
|
|
40
|
-
// §0.5 E21 — DELETE /v1/sessions/:id purge coordinator. Deletes the session abstraction's CONVERSATION HISTORY
|
|
41
|
-
// (sessionStorage.deleteSession → session_meta/session_event) PLUS the service-owned operational rows for that
|
|
42
|
-
// session: the runs ledger (runStore.deleteBySession → task_run/task_active/task_event, session_id-scoped), the
|
|
43
|
-
// durable checkpoints + resume-ctx (checkpointStore.deleteBySession, present only under DURABLE_APPROVAL), and
|
|
44
|
-
// offloaded tool results (toolResultStore.deleteBySession). Wired only when the session store can actually
|
|
45
|
-
// delete (a DB backend).
|
|
46
|
-
//
|
|
47
|
-
// 🔴 ORDERING (adversarial-review HIGH — privacy/right-to-delete + silent-success): the session HISTORY delete
|
|
48
|
-
// (session_meta) is the `ownerOf` gate the DELETE route consults to decide idempotency. It MUST be the LAST
|
|
49
|
-
// committed leg. If a privacy-relevant child leg (runs/checkpoint/tool-result) is deleted AFTER the meta row is
|
|
50
|
-
// gone and then FAILS, the idempotent retry sees `ownerOf===undefined` (history already gone) → the route
|
|
51
|
-
// short-circuits to 200 {deleted:false} and NEVER re-purges → those child rows are stranded while the API
|
|
52
|
-
// reports success. So we purge the child legs FIRST and PROPAGATE any error (do NOT swallow): on a child
|
|
53
|
-
// failure the history delete is SKIPPED, `session_meta` survives, the DELETE returns 500, and the idempotent
|
|
54
|
-
// retry's `ownerOf` still resolves → it re-runs the FULL purge to convergence. `deleted` is computed from the
|
|
55
|
-
// FINAL committed result (history-deleted || runsRemoved>0 — the privacy-relevant data that actually existed).
|
|
56
|
-
// [1196] session-watch registry(SSE 订阅共享探针面)。谓词与 /head 探针同门(getLeafId+ownerOf);
|
|
57
|
-
// env 旋钮经 posIntEnv 有界校验;getLeafId 箭头包一层=天然绑定宿主(SESSION_CACHE_TTL_SEC=0 裸类实例)。
|
|
58
26
|
const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
|
|
59
27
|
? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
|
|
60
28
|
hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
|
|
@@ -62,73 +30,21 @@ export function createSessionFaces(ctx) {
|
|
|
62
30
|
maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
|
|
63
31
|
})
|
|
64
32
|
: undefined;
|
|
65
|
-
// S2 fast path([1208]③ 兑现):同副本 append 落点(tidb/pg persist post-commit)经 leaf-bus 直推
|
|
66
|
-
// notifyLocal——单副本部署零延迟;owner 随 bus 传(commit-time row owner,session-leaf-bus 头注契约:
|
|
67
|
-
// "Owner IS carried"),notifyLocal 据此做 generation 围栏(旧 A 世代不会假匹配新租户的叶子)。
|
|
68
|
-
// local lane 无写钩,探针道照旧。
|
|
69
33
|
setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
|
|
70
34
|
const purgeSession = ownerAware.deleteSession
|
|
71
35
|
? async (sessionId, owner) => {
|
|
72
|
-
// Runs-ledger FIRST: it transactionally re-asserts the no-active-run invariant (FOR UPDATE on task_active,
|
|
73
|
-
// serializing against a concurrent createRun). If a run claimed the session in the route's check→purge
|
|
74
|
-
// window it returns `{ active }` and deletes nothing → we bubble it up so the route 409s, BEFORE touching
|
|
75
|
-
// any other table (no partial purge of a live session). Each leg propagates its error so a child failure
|
|
76
|
-
// aborts the purge BEFORE the history-delete gate is removed (keeping the idempotent retry convergent).
|
|
77
|
-
// 🔴 run 账本按 session_id 单键删,**没有**行级属主门(F-A 修,2026-08-17 合并码扫描):`task_run.owner`
|
|
78
|
-
// 是每次提交的 principal,与会话的 canonical owner 会分叉(无主存量会话被带 principal 的壳续聊 ⇒ 新行
|
|
79
|
-
// owner="default",而这里递的是会话行的 owner=null)。行级门于是只删一半、把具名行连同 task_event 留成
|
|
80
|
-
// 孤儿(session_meta 已删 ⇒ 留存腿的 sessionCandidates 永远枚举不到它们),路由却回 {deleted:true} ——
|
|
81
|
-
// 与下面 anchors 腿注里那句 "a right-to-delete violation reported as success" 是同一个病,理由也同一条:
|
|
82
|
-
// 会话所有权已在路由的 DELETE 门证过。
|
|
83
|
-
// 🔴 **化身围栏**(codex R1-[high] 验真后加):属主门下来了,「路由验过权」这句话就必须还**成立到此刻**。
|
|
84
|
-
// 会话 id 由调用方自选、删除后可被别人 `register` 重新登记 ⇒ 两只并发 purge 交错时(A 与 A' 同时过门,
|
|
85
|
-
// A' 跑完整条 purge、租户 B 用同一 uuid 重登记并跑完一次 run),慢的那只会按 session_id 把 **B 的**行删掉。
|
|
86
|
-
// 复读一次 canonical owner,不齐即整条中止(一行不删、回 {deleted:false} —— 属于调用方的那个化身已经不在)。
|
|
87
|
-
// 这一读是**所有后端**都有的那道围栏;SQL 双生在 store 事务里还有一道 `session_meta … FOR UPDATE` 的
|
|
88
|
-
// 原子复核(那才是真正消灭竞态的一道),local 车道靠它 + 数据根独占 BootLock。
|
|
89
|
-
// 「行不在」(undefined)与「换了主」一律中止(codex R2 采纳):meta 不在 = 调用方验过权的那个化身
|
|
90
|
-
// 已经没了,再按 session_id 往下删,删到的只可能是**别人**重新登记后写的行。E21 的次序契约保证
|
|
91
|
-
// meta 是**最后**删的(子腿失败就不删 meta,让幂等重试收敛)⇒ 「meta 不在却还有本会话的孤儿子行」
|
|
92
|
-
// 在协调器这条路上构造不出来,中止不会丢掉任何该删的东西。
|
|
93
|
-
// ⚠️ `ownerOf` **方法缺席**的后端(可选 seam)与「行不在」必须分开判:合起来会让那些后端的删除
|
|
94
|
-
// 整条恒中止(fail-closed 到把功能关掉),那是另一种缺陷。
|
|
95
36
|
if (ownerAware.ownerOf) {
|
|
96
37
|
const stillOwner = await ownerAware.ownerOf(sessionId);
|
|
97
38
|
if (stillOwner !== owner)
|
|
98
39
|
return { deleted: false };
|
|
99
40
|
}
|
|
100
|
-
// 📌 **残余,如实登记**(codex R2 判真但**射程外**,不在本批修):本围栏钉住的是「purge 开始那一刻
|
|
101
|
-
// 化身是对的」+ run 腿的事务内原子复核。其余按 session_id 单键删的腿(anchors / exemption / policy /
|
|
102
|
-
// attachment / snapshot / task_list(#318 新入册,codex R2-[high] 同判)/ tool_result 的 provenance 形)
|
|
103
|
-
// 在**本批之前就是**这个 scoping(理由逐字见下方
|
|
104
|
-
// anchors 腿注,可追到公开快照那一版),它们各自开始的时刻理论上仍可能落在一次化身漂移之后。
|
|
105
|
-
// 要真正消灭它得让**整条**级联化身原子(SQL 单事务横跨十余张表 + 对象存储,即留存腿那个形),而在线
|
|
106
|
-
// 删除面刻意不是那个形:它要跨事务崩溃收敛(meta 最后删 ⇒ 幂等重试),还要动 MinIO 字节。那是设计级
|
|
107
|
-
// 改动(属主裁决 + 三问),不是复审批能顺手做的事;已作后续件登记,别把这条注读成「已解」。
|
|
108
41
|
const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
|
|
109
42
|
if ("active" in runs)
|
|
110
43
|
return { active: runs.active };
|
|
111
|
-
// checkpoint rows carry no owner column → owner-guarded via a session_meta.owner sub-select, so they MUST
|
|
112
|
-
// run while session_meta still exists (before the history delete below); tool_result selects by its
|
|
113
|
-
// RECORDED PROVENANCE column (`owner_session_id`) and stays route-guarded too (see its deleteBySession doc).
|
|
114
44
|
if (checkpointStore)
|
|
115
45
|
await checkpointStore.deleteBySession(sessionId, owner);
|
|
116
|
-
// optional extra(R3 复扫更正:不再是「SQL twins only」——core 5.28 起 FileToolResultStore **实现了**
|
|
117
|
-
// deleteBySession,local 车道这条腿**真的会跑**。File 孪生的形:同步遍历目录、无 .owner.json 边车的
|
|
118
|
-
// 存量文件计入 unattributable 不删——所以 local 部署上这条 warn 在有 5.28 前存量时会以大数出现,
|
|
119
|
-
// 这是诚实读数不是事故。⚠️ R4 复扫再纠:**local 无 TTL 腿**(File 孪生没有 reapOlderThan,
|
|
120
|
-
// reapers 的可选链在 local 恒 no-op)⇒ 这些无主存量今天在 local 上**没有任何清理路径**
|
|
121
|
-
// (会话删除按契约不动它、TTL 也够不着)——只占磁盘不泄权,单用户 CC 姿态下按已知缺口登记,
|
|
122
|
-
// 清理腿(File 孪生补 reapOlderThan 或一次性迁移铸边车)是独立后续件,别把本注读成已有补偿。)
|
|
123
46
|
if (toolResultStore) {
|
|
124
47
|
const report = await toolResultStore.deleteBySession?.(sessionId);
|
|
125
|
-
// 🔴 **回执不许丢**(codex 复审 R1-[high],验真后修):core 把 `unattributable` 写成「the caller
|
|
126
|
-
// learns the deletion was **incomplete** rather than being told a clean "done"」——无出处的行按
|
|
127
|
-
// 契约永不删除,协调器把报告丢掉就等于把这句话吞了,而删除面吞掉的是**删除权**的真相。
|
|
128
|
-
// 这一格是**全库**无出处行数(core InMemory 孪生口径),读法 = 「本店存在没人认领的字节,任何
|
|
129
|
-
// 会话删除都不可能完整」,不是「本会话残留了 N 行」;实体清理归 TTL(`reapOlderThan`)。
|
|
130
|
-
// ⚠️ 登记后续件:把它也带上 DELETE 的 HTTP 回执是**加 wire 键**,归属主裁决,不在本批自作主张;
|
|
131
|
-
// 本批先保证它不再无声消失(结构化 warn = 运维查得到、审计追得上)。
|
|
132
48
|
if (report !== undefined && report.unattributable > 0) {
|
|
133
49
|
logger.warn("tool_result_unattributable_rows_survive_session_delete", {
|
|
134
50
|
sessionId,
|
|
@@ -137,60 +53,21 @@ export function createSessionFaces(ctx) {
|
|
|
137
53
|
});
|
|
138
54
|
}
|
|
139
55
|
}
|
|
140
|
-
// E18 resume-at anchors are per-session privacy-relevant metadata → purge ALL of them with the session, scoped
|
|
141
|
-
// by session_id ALONE (the route already proved session ownership at the DELETE gate). A per-ROW owner guard
|
|
142
|
-
// here would LEAK: an anchor's owner is the per-run submitting principal, which diverges from the canonical
|
|
143
|
-
// session owner when an anonymous session is later attached by a principal (REQUIRE_PRINCIPAL=false) — those
|
|
144
|
-
// mixed-owner rows would survive the delete (a right-to-delete violation reported as success).
|
|
145
56
|
if (resumeAnchorStore)
|
|
146
57
|
await resumeAnchorStore.deleteBySession(sessionId);
|
|
147
|
-
// Approval exemptions are per-session operator decisions → purge with the session (same
|
|
148
|
-
// session_id-alone scoping rationale as the anchors above).
|
|
149
58
|
if (approvalExemptionStore)
|
|
150
59
|
await approvalExemptionStore.deleteBySession(sessionId);
|
|
151
|
-
// E6 session-policy rows are per-session operator rules → purge them too, scoped by session_id (the route
|
|
152
|
-
// owner-gated the session). Since core 1.423 the seam is on the INTERFACE (optional) and core's File/InMemory
|
|
153
|
-
// stores carry it too — so this fires on EVERY backend now. (The old note here claimed local rules were
|
|
154
|
-
// "process-ephemeral anyway" — false for the file-backed local store, whose policy rows survive restarts;
|
|
155
|
-
// that right-to-delete hole is what [1796]§三 → core 1.423 closed.)
|
|
156
60
|
if (sessionPolicyStore?.deleteBySession)
|
|
157
61
|
await sessionPolicyStore.deleteBySession(sessionId);
|
|
158
|
-
// D-1 附件随会话删(E21 级联;session_id 单键 scoping,与 anchors 同理由——路由已证会话所有权)。
|
|
159
62
|
if (taskAttachmentStore)
|
|
160
63
|
await taskAttachmentStore.deleteBySession(sessionId);
|
|
161
|
-
// #318 任务清单(TaskCreate/TaskList 家族)是 per-session 的用户内容(任务标题/描述/元数据)
|
|
162
|
-
// → 随会话删,scoping 与 anchors 同理由(路由已在 DELETE 门证过会话所有权)。
|
|
163
|
-
// 🔴 **不是可选的卫生腿**:会话 id 由调用方自选、删除后可被别人 `register` 重新登记,而车道的
|
|
164
|
-
// listKey 是 sessionId 的确定性派生 ⇒ 不删的话新化身第一次 TaskList 就读到上一位主人的任务,
|
|
165
|
-
// TaskCreate 还从上一位的高水位续号(codex R1-[high] 验真后接)。错误如实上抛 = 与同族腿一致
|
|
166
|
-
// (子腿失败 ⇒ 不删 meta ⇒ 幂等重试收敛)。
|
|
167
64
|
if (taskListLane)
|
|
168
65
|
await taskListLane.deleteBySession(sessionId);
|
|
169
|
-
// E19 rewind-files snapshots are per-session working-tree state → purge them too (scoped by sessionId; the
|
|
170
|
-
// durable twins carry deleteBySession, local InMemory omits it → optional no-op on local).
|
|
171
66
|
if (fileSnapshotStore?.deleteBySession)
|
|
172
67
|
await fileSnapshotStore.deleteBySession(sessionId);
|
|
173
|
-
// P1 ①②: drop any pending async-workflow completions for this session (else they orphan when a later
|
|
174
|
-
// tenant reclaims the sessionId). Best-effort + non-fatal — a leftover entry is ALSO gated by the
|
|
175
|
-
// drain-time owner check, so a purge failure never leaks; it must not abort the session delete.
|
|
176
|
-
// `owner` scopes the purge fence (1.80): only the DELETED session's own late completions are fenced —
|
|
177
|
-
// a new tenant legitimately re-claiming this sessionId within the window keeps its push (review MED).
|
|
178
68
|
if (workflowCompletionInbox)
|
|
179
69
|
await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
|
|
180
|
-
// 修8(三路复审 absorb-2,接线 (a)):the session's scratchpad dir ([820]③, ensureScratchpadDir) rides
|
|
181
|
-
// the session lifecycle — purge it with the session. purgeScratchpadDir SELF-SWALLOWS (env-facts.ts
|
|
182
|
-
// contract: cleanup never faults the delete path; the periodic sweep below is the convergence backstop),
|
|
183
|
-
// so it cannot strand the E21 ordering invariant. Same root the ensure used (config.localDataRoot ?? localRoot).
|
|
184
70
|
await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
|
|
185
|
-
// design/129 (core 1.239): reap this session's SESSION-scoped background children — they
|
|
186
|
-
// deliberately outlive turns (backgroundScope:"session"), so the session's DELETE is their lifecycle
|
|
187
|
-
// end (core's parent-teardown reap skips them by design; without this they run to their forced timeout).
|
|
188
|
-
// Sync + replica-local (the registry is in-process); best-effort — a reap of 0 on the wrong replica is
|
|
189
|
-
// covered by core's forced child-timeout cap.
|
|
190
|
-
// [1892]{core} 跨仓待办:scope 轴必传——core canAccess 对缺省 scope fail-closed(拒绝非通配,
|
|
191
|
-
// 1.441 复审①正过极性),不传 = 四类后台任务在会话删除时恒 0 回收(潜伏至今,core 复审顺手
|
|
192
|
-
// 核出)。铸值同源 runs.ts:25 契约:core 按 `spec.principal ?? "default"` 注册 ⇒ 此处
|
|
193
|
-
// owner(会话主)?? "default" 对齐;错 scope 不越租户由 canAccess 保证(session-reap-scope 钉)。
|
|
194
71
|
try {
|
|
195
72
|
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
|
|
196
73
|
if (reaped > 0)
|
|
@@ -199,20 +76,12 @@ export function createSessionFaces(ctx) {
|
|
|
199
76
|
catch (err) {
|
|
200
77
|
logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
|
|
201
78
|
}
|
|
202
|
-
// History/meta LAST: once committed, the `ownerOf` gate is gone and the delete is durably complete. The
|
|
203
|
-
// owner is threaded into the SQL guard (session_meta.owner) for data-layer defense-in-depth.
|
|
204
79
|
const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
|
|
205
|
-
// [1196] 三轮复审(租户轮转围栏,同副本半场):session 删除即终结其 SSE 订阅者+watch 条目——
|
|
206
|
-
// 旧 owner 的活流不得跨越 delete/reclaim 边界收新租户的 head;跨副本半场=路由心跳期 owner 复核。
|
|
207
80
|
sessionWatchRegistry?.dropSession(sessionId);
|
|
208
81
|
return { deleted: historyDeleted || runs.removed > 0 };
|
|
209
82
|
}
|
|
210
83
|
: undefined;
|
|
211
|
-
// Degenerate-output a/b instrument: needs the durable session log to inspect turn history (design/39
|
|
212
|
-
// ② gating data). Only on the TiDB backend (mysql2 pool); env-only / PG deploys get no-op.
|
|
213
84
|
const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
|
|
214
|
-
// Plan-cache recurrence probe (core design/42, INSTRUMENT-FIRST): pure in-memory, no backend needed —
|
|
215
|
-
// counts per-scope task-objective recurrence so core can decide whether to build plan caching.
|
|
216
85
|
const planCacheProbe = new PlanCacheProbe(metrics, logger);
|
|
217
86
|
return { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe };
|
|
218
87
|
}
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/158 A10:composition root 分段 —— 进程收尾(hardShutdown / drain / 三个信号处理器)。
|
|
3
|
-
*
|
|
4
|
-
* 纯搬运:函数体逐字来自 `main.ts`(原 4385-4488 行),缩进不变;新增的只有 import 与
|
|
5
|
-
* `installShutdownHandlers(ctx)` 包壳。
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ **位置即契约**(搬运不得改次序):
|
|
8
|
-
* 1. 本段必须在 `server.listen()` **之后**调用 —— `process.on("SIGTERM"|"SIGINT"|"SIGHUP")` 的注册
|
|
9
|
-
* 时点即"从此刻起信号可被优雅处理";提前注册会让一个尚未 listen 的进程走 drain 分支。
|
|
10
|
-
* 2. 三个 `process.on` 的**相对次序**保持 SIGTERM → SIGINT → SIGHUP(与原文件逐字一致);
|
|
11
|
-
* 它们互不覆盖,但 handler 之间共享 `closing`/`draining` 两个 latch,次序变化会改变
|
|
12
|
-
* "第二个信号"的语义读数。
|
|
13
|
-
* 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
|
|
14
|
-
* 收尾期还会有 reaper tick 打向正在关闭的池。
|
|
15
|
-
*/
|
|
16
1
|
import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
17
2
|
import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
|
|
18
3
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
19
4
|
import { createParentWatch } from "../parent-watch.js";
|
|
20
|
-
/** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
|
|
21
5
|
export function installShutdownHandlers(ctx) {
|
|
22
6
|
const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, } = ctx;
|
|
23
|
-
// #320①([4728] 五点令)—— 崩溃遗言面。契约:`crash-last.json` 在场 ⇔ 上一次退出=崩溃。
|
|
24
|
-
// · boot 提示:上次崩溃留了遗言 ⇒ 一行 warn(读回即证据,不清——清的责任归受控退);
|
|
25
|
-
// · uncaught 双钩:写遗言(version/pid/uptime/inflight/err 首行消毒)后仍按崩溃语义 exit(1),不吞崩溃;
|
|
26
|
-
// · 受控退:hardShutdown 一进来即 clearCrashLast(这次是有意的退)——若后续收尾半途真崩,
|
|
27
|
-
// uncaught 钩会重写遗言,判别不失真。
|
|
28
|
-
// dataRoot 类型上可选(config 解析恒回退 ~/.ai-agent ⇒ 实际恒真,/health dataRoot 同注);类型面
|
|
29
|
-
// 诚实处理:真缺席=遗言面整体不装(uncaught 钩仍装——崩溃语义不能因遗言面缺席而变)。
|
|
30
7
|
const dataRoot = config.localDataRoot;
|
|
31
8
|
if (dataRoot) {
|
|
32
9
|
const prior = readCrashLast(dataRoot);
|
|
@@ -39,7 +16,6 @@ export function installShutdownHandlers(ctx) {
|
|
|
39
16
|
};
|
|
40
17
|
process.on("uncaughtException", (err) => {
|
|
41
18
|
writeLastWords(err);
|
|
42
|
-
// 与 Node 缺省崩溃姿势对齐:stack 打 stderr(遗言只留首行),随后按崩溃码退。
|
|
43
19
|
console.error(err);
|
|
44
20
|
process.exit(1);
|
|
45
21
|
});
|
|
@@ -49,25 +25,15 @@ export function installShutdownHandlers(ctx) {
|
|
|
49
25
|
process.exit(1);
|
|
50
26
|
});
|
|
51
27
|
let closing = false;
|
|
52
|
-
/** #219:parent 监视腿(装配在本函数尾,`config.parentPid` 在场才建)。声明提前只为让 hardShutdown
|
|
53
|
-
* 能把它一并停掉 —— 文件头契约 3 同族(收尾期不再有后台 tick)。 */
|
|
54
28
|
let parentWatch;
|
|
55
29
|
const hardShutdown = () => {
|
|
56
30
|
if (closing)
|
|
57
31
|
return;
|
|
58
32
|
closing = true;
|
|
59
33
|
if (dataRoot)
|
|
60
|
-
clearCrashLast(dataRoot);
|
|
34
|
+
clearCrashLast(dataRoot);
|
|
61
35
|
clearInterval(reaper);
|
|
62
|
-
clearInterval(fleetReconcile.timer);
|
|
63
|
-
// #270 车2:契约 3 同族,且这一条最要紧——留存 tick 起飞的是跨十余张表的**破坏性**事务。
|
|
64
|
-
// 🔴 **四步的次序都是承重的**(codex 对抗复审 R1/R2 两轮各修一半,均验真后改):
|
|
65
|
-
// 停表 → 等在飞那一轮落地 → 让租 → `backend.close()`。
|
|
66
|
-
// · `clearInterval` 只挡新 tick,不等当前这一轮;停表之后紧跟着就让租 ⇒ 新副本立刻接租,而本副本
|
|
67
|
-
// 还在删同一个域的数据 —— 一个由**正常** SIGTERM/SIGINT 稳定触发的双执行者窗口(R1);
|
|
68
|
-
// · 而让租之后若不等就关池,这一轮会被从底下抽掉连接、半途而废(R2 —— 见下方 server.close 回调)。
|
|
69
|
-
// · 进程若在整条链跑完之前就退了,租约不会被让出 ⇒ 下一位等满 TTL —— 那是**安全**的那一侧。
|
|
70
|
-
// 三步的产物是一条**可等的** promise —— 下面 `server.close()` 的回调必须等它落地才 `backend.close()`。
|
|
36
|
+
clearInterval(fleetReconcile.timer);
|
|
71
37
|
const retentionTeardown = retentionLane === undefined
|
|
72
38
|
? Promise.resolve()
|
|
73
39
|
: retentionLane
|
|
@@ -75,27 +41,20 @@ export function installShutdownHandlers(ctx) {
|
|
|
75
41
|
.then(() => releaseRetentionLease?.())
|
|
76
42
|
.then(() => undefined)
|
|
77
43
|
.catch(() => undefined);
|
|
78
|
-
fleetReconcile.reconciler.stop();
|
|
79
|
-
parentWatch?.stop();
|
|
80
|
-
storeLiveProbe?.stop();
|
|
81
|
-
configCenter?.stopRefreshLoop();
|
|
44
|
+
fleetReconcile.reconciler.stop();
|
|
45
|
+
parentWatch?.stop();
|
|
46
|
+
storeLiveProbe?.stop();
|
|
47
|
+
configCenter?.stopRefreshLoop();
|
|
82
48
|
otelExporter?.stop();
|
|
83
49
|
breakerState?.stop();
|
|
84
50
|
if (costQuota && "stop" in costQuota)
|
|
85
|
-
costQuota.stop();
|
|
51
|
+
costQuota.stop();
|
|
86
52
|
if (rateLimiter && "stop" in rateLimiter)
|
|
87
53
|
rateLimiter.stop();
|
|
88
54
|
runner.sessions.dispose();
|
|
89
55
|
subRunner.sessions.dispose();
|
|
90
|
-
// TOC local LSP: NodeLspManager holds LOCAL child-process language servers — kill them on shutdown so a
|
|
91
|
-
// restart doesn't orphan a fleet of stdio servers. dispose() isn't on the LspServerManager interface (only the
|
|
92
|
-
// concrete managers have it), so probe it; the e2b/k8s bridge manager is a harmless no-op if it lacks one.
|
|
93
56
|
void lspManager?.dispose?.();
|
|
94
|
-
// SVC-1: release the File journal's append handle; the SQL twins hold no fd (probe — close isn't on the seam).
|
|
95
57
|
void workflowNotifyJournal?.close?.();
|
|
96
|
-
// core 1.270.1:hardShutdown 终点(drain 已完,in-flight 不再被破坏)全量收割 session 驻留
|
|
97
|
-
// bg bash(detached 进程组,引擎死后 reparent PID 1 残留=实测;retain-declared 例外在原语内)。
|
|
98
|
-
// TOB 容器形态同样调=无害且显式;SIGKILL 路径无钩=已知不可救记档。
|
|
99
58
|
try {
|
|
100
59
|
const reaped = defaultTaskRegistry.reapAllSessionBackground();
|
|
101
60
|
if (reaped > 0)
|
|
@@ -104,34 +63,16 @@ export function installShutdownHandlers(ctx) {
|
|
|
104
63
|
catch (err) {
|
|
105
64
|
logger.warn("shutdown_background_reap_failed", { err: String(err) });
|
|
106
65
|
}
|
|
107
|
-
void fleetClient?.stop();
|
|
66
|
+
void fleetClient?.stop();
|
|
108
67
|
server.close(() => {
|
|
109
|
-
// 🔴 `backend.close()` 必须排在留存收尾**之后**(codex 对抗复审 R2-[high],验真后修):
|
|
110
|
-
// 那一轮打的是跨十余张表的破坏性事务,把连接池从它底下抽掉 = 这一轮自己半途而废
|
|
111
|
-
// (比"让租早了"更重:让租早了只是**可能**双执行者,池被抽掉是必然的半成品)。
|
|
112
|
-
// 兜底仍在:下面那条 15s `finalExit` 无条件退出,一轮卡死的 sweep 拖不住停机。
|
|
113
68
|
void retentionTeardown.then(() => (backend ? backend.close() : Promise.resolve())).finally(() => process.exit(0));
|
|
114
69
|
});
|
|
115
|
-
// 对抗复查 B-4(HIGH):the exit above only fires after backend.close() SETTLES — a wedged pool teardown
|
|
116
|
-
// (dead DB, hung socket) kept the "force-terminated" process alive indefinitely (the 10s closeAllConnections
|
|
117
|
-
// below unblocks server.close(), but nothing bounded backend.close). FINAL deadline: whatever is still
|
|
118
|
-
// holding after 15s, exit anyway (durable state is already flushed by then — checkpoint/terminal writes
|
|
119
|
-
// happen before hardShutdown; the pool teardown is best-effort cleanup, not correctness).
|
|
120
70
|
const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
|
|
121
71
|
finalExit.unref?.();
|
|
122
|
-
// `server.close()` waits for every connection to end — an open SSE stream (/v1/tasks/stream, /events) would
|
|
123
|
-
// otherwise block it forever, so pool.end()/exit never run and the process hangs until SIGKILL. Drop idle
|
|
124
|
-
// keep-alives now, then force-terminate any lingering connections after a grace period so `close()` can
|
|
125
|
-
// resolve and shutdown completes. The timer is unref'd but still fires while the server holds connections.
|
|
126
72
|
server.closeIdleConnections?.();
|
|
127
73
|
const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
|
|
128
74
|
forceClose.unref?.();
|
|
129
75
|
};
|
|
130
|
-
// SIGTERM = graceful DRAIN — flip `draining` (new billable submits 503+Retry-After, /health carries
|
|
131
|
-
// draining:true for readiness摘流), then wait for this instance's in-flight legs (live streams + bg/resume) to
|
|
132
|
-
// finish before the hard shutdown. Bounded by DRAIN_GRACE_MS (default 10min — an interactive turn is minutes;
|
|
133
|
-
// the previous behavior was a 10s hard-cut that killed long turns). A SECOND SIGTERM or SIGINT (dev Ctrl-C)
|
|
134
|
-
// skips the wait — k8s sends SIGKILL after terminationGracePeriodSeconds regardless, so the escape hatch is free.
|
|
135
76
|
let draining = false;
|
|
136
77
|
const drainThenShutdown = () => {
|
|
137
78
|
if (closing)
|
|
@@ -144,17 +85,14 @@ export function installShutdownHandlers(ctx) {
|
|
|
144
85
|
draining = true;
|
|
145
86
|
drainState.draining = true;
|
|
146
87
|
drainState.since = Date.now();
|
|
147
|
-
void fleetClient?.announceNow();
|
|
88
|
+
void fleetClient?.announceNow();
|
|
148
89
|
const inflight = drainState.inflight?.() ?? 0;
|
|
149
90
|
logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
|
|
150
91
|
if (inflight === 0) {
|
|
151
92
|
hardShutdown();
|
|
152
93
|
return;
|
|
153
94
|
}
|
|
154
|
-
// 修3(三路复审 B3-2 时钟):drain elapsed/截止计算走 performance.now() 单调域(挂钟跳变不得吞掉/拉长
|
|
155
|
-
// drainGraceMs 窗);对外报告的 drainState.since(/health drainingSince)保留 epoch(Date.now)。
|
|
156
95
|
const t0 = performance.now();
|
|
157
|
-
// NOT unref'd on purpose — this timer IS the shutdown driver (the open connections keep the loop alive anyway).
|
|
158
96
|
const tick = setInterval(() => {
|
|
159
97
|
const n = drainState.inflight?.() ?? 0;
|
|
160
98
|
const elapsed = Math.round(performance.now() - t0);
|
|
@@ -167,16 +105,8 @@ export function installShutdownHandlers(ctx) {
|
|
|
167
105
|
};
|
|
168
106
|
process.on("SIGTERM", drainThenShutdown);
|
|
169
107
|
process.on("SIGINT", hardShutdown);
|
|
170
|
-
// SIGHUP(壳窗口关闭把信号打到整个进程组)≠「没人在用这只引擎」:共享引擎形态(同 config 多壳会话,
|
|
171
|
-
// 引擎按 T11 防孤儿拍骑在首壳进程组里)下 peer 会话还活着——无条件 drain 会把 peer mid-turn 斩掉(499)
|
|
172
|
-
// 且窗内新提交 503(B3,壳侧 [771]② 取证)。改 hup-pending 空闲窗:不翻 draining(peer 零感知),
|
|
173
|
-
// inflight()(bg/resume legs + live streams)连续 sighupIdleGraceMs 为 0 才 hardShutdown,任何在飞 leg
|
|
174
|
-
// 重置窗口。真孤儿(最后一窗关掉)有界自灭=T11 保留;第二个 SIGHUP 升级走既有 drain 路径(硬梯子)。
|
|
175
|
-
// session 驻留 bash 的全量收割仍挂 hardShutdown 终点(reapAllSessionBackground)。
|
|
176
108
|
process.on("SIGHUP", createSighupIdleHandler({
|
|
177
109
|
inflight: () => drainState.inflight?.() ?? 0,
|
|
178
|
-
// ⚠️ 时基契约(三路复审修3):lastActivityAt 由 server.ts 以 performance.now() 单调域打点,与
|
|
179
|
-
// sighup-idle.ts 内部的 `now` seam(缺省 performance.now)同域——不得混入 Date.now 值。
|
|
180
110
|
lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
|
|
181
111
|
isStopped: () => closing || draining,
|
|
182
112
|
escalate: drainThenShutdown,
|
|
@@ -184,10 +114,6 @@ export function installShutdownHandlers(ctx) {
|
|
|
184
114
|
idleGraceMs: config.sighupIdleGraceMs,
|
|
185
115
|
log: (event, fields) => logger.info(event, fields),
|
|
186
116
|
}));
|
|
187
|
-
// #219(黑板 [3617]):壳被 SIGKILL 后引擎被 reparent 到 init 而继续活着(孤儿引擎持端口/持库连接,
|
|
188
|
-
// 且再没有人会给它发 SIGTERM)。`SEMA_PARENT_PID` 设了才装配 —— 连续两拍 ESRCH 判死,出口**就是**
|
|
189
|
-
// 上面 SIGTERM 用的这条 `drainThenShutdown`(结算/park 语义与人工停机逐字一致,不是裸 exit)。
|
|
190
|
-
// 位置:与三个 process.on 同段、在它们之后 —— 它和信号腿共享 closing/draining 两个 latch。
|
|
191
117
|
if (config.parentPid !== undefined) {
|
|
192
118
|
parentWatch = createParentWatch({
|
|
193
119
|
pid: config.parentPid,
|
|
@@ -1,119 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #303([4610] test 报「/model 中途切换校验探针经 POST /v1/side-query 对有效 key 返回上游 401」)——
|
|
3
|
-
* side-query 面的 **per-model key plane** 装配。
|
|
4
|
-
*
|
|
5
|
-
* ## 病灶(#303 立案时亲验 core 5.43.0 dist 实现行)
|
|
6
|
-
*
|
|
7
|
-
* core 的 side-query verb 当时**结构性没有 key 座位**:
|
|
8
|
-
* · `core/side-query.d.ts` 的 `SideQuerySpec` 无 `apiKey` / `getApiKeyAndHeaders` 字段;
|
|
9
|
-
* · `SideQueryDeps` = `{ brain, models?, roles? }`;
|
|
10
|
-
* · `core/side-query.js` 铸 brain `options` 时只放 `signal/maxTokens/reasoning`,**不含 apiKey**。
|
|
11
|
-
* 而 openai brain 的 `buildRequest`(`brain/openai.js`)是这两行:
|
|
12
|
-
* `const apiKey = options?.apiKey ?? config.apiKey;` ← 缺席即回落 **brain 构造时的网关 key**
|
|
13
|
-
* `const root = (model.baseUrl || config.baseUrl || "")` ← 却**按模型**选路
|
|
14
|
-
* ⇒ 对带 per-model `baseUrl` + `apiKeyEnv`/`sealedApiKey` 的模型,side-query 把**主网关 key 发到
|
|
15
|
-
* 外部模型 URL**:用户看到上游 401,同时这是一次凭据外泄(安全轴)。
|
|
16
|
-
* 当时的窄修 = 一只 **key 注入 brain wrapper**(`createPerModelKeyBrain`),并在顶注写明「core 一旦
|
|
17
|
-
* 给 `SideQuerySpec` 补上 key 座位,本 wrapper 即可撤」,撤除信号挂机器钉。
|
|
18
|
-
*
|
|
19
|
-
* ## #341 换装(core 5.46.0 提货,[4615] 座位到货 —— 撤除条件已满足)
|
|
20
|
-
*
|
|
21
|
-
* core 5.46.0 的 `SideQuerySpec` 有了 `getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"]`,
|
|
22
|
-
* `runSideQuery` 每次调用按**解析后的** `Model` 调它,并把 `{apiKey, headers}` 铸进 brain options
|
|
23
|
-
* (`dist/core/side-query.js`:`const auth = await spec.getApiKeyAndHeaders?.(resolved.model);`)——
|
|
24
|
-
* 与主推理链逐字同一只座位形。于是 side-query 席**撤掉 brain wrapper**,key 供给改走 spec 座位:
|
|
25
|
-
* · 本席不再在 side-query 路上包 `ctx.brain`,`RunnerDeps.brain` 的零扰动从「wrapper 只包一只引用」
|
|
26
|
-
* 降级成**结构性事实**(这条路上根本没有 wrapper);
|
|
27
|
-
* · `headers` 半边从此有出路(wrapper 形只注得进 `apiKey`,core 的座位 `{apiKey, headers?}` 两半都收);
|
|
28
|
-
* · 判据(`gatewayBaseUrl`)与 resolver 一样**按调用现取** —— 见下面 `createSideQueryLane` 的注。
|
|
29
|
-
* `createPerModelKeyBrain` **不删**:它仍是 WebFetch 摘要面(`boot/webfetch-summarize-lane.ts`)的
|
|
30
|
-
* key plane —— core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`,那一面没有座位可换
|
|
31
|
-
* (亲验 core 5.46.0 `dist/tools/web.d.ts`)。
|
|
32
|
-
*
|
|
33
|
-
* 🔴 **poison(sealed key 不可解)上抛,绝不静默回落网关 key** —— `resolveModelApiKey` 抛
|
|
34
|
-
* `SealedKeyPoisonedError` 是「这个模型配了托管密钥但解不开」的响亮态;吞成 undefined 等于用共享
|
|
35
|
-
* 网关账号去打这只模型的上游,正是毒丸机制存在的理由(CLAUDE.md #157 安全轴 fail-closed)。换装后
|
|
36
|
-
* 这条语义由 core 承载得更彻底:座位抛 ⇒ `runSideQuery` 的 `await` 直接上抛,brain 一个字节都没发。
|
|
37
|
-
*
|
|
38
|
-
* ## 具名残余:**无 per-model key 的 off-route 模型仍收网关 key**(codex R1-F1,逐条评估后如实留)
|
|
39
|
-
*
|
|
40
|
-
* 判据本身没变(座位到货也没改它):`undefined` ⇒ 座位不给凭据 ⇒ core 用网关 key,即使这只模型的
|
|
41
|
-
* `baseUrl` 指向别家。为什么**不**在本批把它改成 fail-closed(逐条,而不是"先不管"):
|
|
42
|
-
* ① **与主推理链同语义**才是当前的正确性基准 —— 任务面(core 每次调 `getApiKeyAndHeaders`,返回
|
|
43
|
-
* `undefined` 即网关 key)在**同一个** catalog、**同一批**模型上就是这个姿势。只把 side-query 收严,
|
|
44
|
-
* 等于让同一只模型「跑任务能用、问一句就 401」,而任务面才是量大的那一面 —— 半边收严的覆盖比
|
|
45
|
-
* 统一的宽松更难排障。
|
|
46
|
-
* ② **谁被伤到**:`baseUrl` 是**运维**在 config-center 里自己写的(不是用户输入,用户只能在
|
|
47
|
-
* `atModelAllowlist` 里点名单内的模型)。"同一账号多网关主机/多区域"是既有且正当的部署形,
|
|
48
|
-
* 它今天靠网关 key 工作;无版本协商地收严 = 这类部署当场 401,而且没有旋钮可回退。
|
|
49
|
-
* ③ hook 面(`hooks/hook-llm.ts`)确实是 fail-closed 的,但它的辖域是 **hook roster**(运维给钩子挑的
|
|
50
|
-
* 模型),不是用户可点的整本目录;两者的收严代价不同量级。
|
|
51
|
-
* ⇒ 本批**不改行为**,把它作为**跨面语义问题**上报(要收严就三面一起收严、带旋钮、走表态制),
|
|
52
|
-
* 而不是在这一面偷偷收严。真要收严时的正解:按 `normUrl` 比对 `gatewayBaseUrl`(+fallback 列表)
|
|
53
|
-
* 与 anthropic 路由,off-route ∧ 无 per-model key ⇒ 拒发,并同批改任务面。
|
|
54
|
-
*/
|
|
55
1
|
import { expandTiers, runSideQuery } from "@sema-agent/core";
|
|
56
2
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
57
|
-
/**
|
|
58
|
-
* 「这只模型的 baseUrl 打的是**别家**主机吗」—— core 的选路规则是 `model.baseUrl || config.baseUrl`,
|
|
59
|
-
* 所以只有 `model.baseUrl` 非空时才可能离开本部署网关。比较按 URL 归一(`new URL` 小写 host、丢默认端口、
|
|
60
|
-
* 去尾斜杠),裸字符串比会在 host 大小写/默认端口写法上假阴。
|
|
61
|
-
*
|
|
62
|
-
* 参照系缺席(`gatewayBaseUrl` 未给)⇒ 回 `false`:判不了就不判,别猜。
|
|
63
|
-
*
|
|
64
|
-
* ⚠️ `hooks/hook-llm.ts` 有一只逐字同形的内联归一器(它用同一判据做 **fail-closed 拒发**,本函数只
|
|
65
|
-
* 做**计数**)。两处合一属跨面收严件 #309 的射程(那时三面同批换判据),本批刻意不动 hook 面的 fail-closed
|
|
66
|
-
* 门 —— 在只改可观测性的批里重写另一条安全轴的门,风险与收益不对等。
|
|
67
|
-
*/
|
|
68
3
|
export function isOffRouteBaseUrl(modelBaseUrl, gatewayBaseUrl) {
|
|
69
4
|
if (!modelBaseUrl || !gatewayBaseUrl)
|
|
70
5
|
return false;
|
|
71
|
-
// 解析不了(配歪的 baseUrl)⇒ 退到去尾斜杠的裸串比。用 `URL.canParse` 而不是 try/catch:静默降级
|
|
72
|
-
// 棘轮门(`test/silent-degradation-scan.test.ts` SHAPE C)对「catch 只 return 默认值」是零容忍的,
|
|
73
|
-
// 而这条降级恰好有一个不吞异常的写法(Node ≥20 / ES2023 均在射程)。方向:比不出同一家 ⇒ 算 off-route
|
|
74
|
-
// ⇒ 记债(宁可多记一条待查,不可把一次真外泄判成自家网关)。
|
|
75
6
|
const norm = (u) => URL.canParse(u) ? ((x) => `${x.protocol}//${x.host}${x.pathname.replace(/\/+$/, "")}`)(new URL(u)) : u.replace(/\/+$/, "");
|
|
76
7
|
return norm(modelBaseUrl) !== norm(gatewayBaseUrl);
|
|
77
8
|
}
|
|
78
|
-
/**
|
|
79
|
-
* per-model auth 座(#341,core 5.46.0 `SideQuerySpec.getApiKeyAndHeaders` 的 server 侧填充物)。
|
|
80
|
-
*
|
|
81
|
-
* 语义(逐条对齐主推理链 `getApiKeyAndHeaders`,也逐条对齐它取代的 wrapper 那三条臂):
|
|
82
|
-
* · `upstream`(spec 自带的座位)**解出凭据** ⇒ 原样透传,含 `headers`(上游/装饰器已经定了凭据,
|
|
83
|
-
* 本层不抢 —— wrapper 时代的「`options.apiKey` 已在场」臂);判别按**每次调用**的返回值,不是
|
|
84
|
-
* 「座位在场就整只让位」:上游对这只模型没意见时,本席照常按模型解 key;
|
|
85
|
-
* · resolver 缺席(部署根本没配 per-model key)⇒ 回 `undefined` ⇒ core 用网关 key(今日行为逐字不变);
|
|
86
|
-
* · resolver 返回 undefined(这只模型没有自己的 key)⇒ 回 `undefined` ⇒ 网关 key(additive 契约);
|
|
87
|
-
* · resolver 抛(sealed key 中毒)⇒ **上抛**,`runSideQuery` 的 `await` 直接把它扔给调用方,
|
|
88
|
-
* 本次调用整体失败,一个字节都不发。
|
|
89
|
-
*
|
|
90
|
-
* 🔴 A-057.59 [PARTIAL high](2026-08-19;#341 换装后**处置不变**)—— 「无 per-model key 的 off-route
|
|
91
|
-
* 模型仍收网关 key」这条具名残余**行为不变**(逐条理由见模块顶注「具名残余」段;core 的座位是
|
|
92
|
-
* 「absent ⇒ construction-time credentials apply」,与主推理链
|
|
93
|
-
* `dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂逐字同语义,座位到货并没有
|
|
94
|
-
* 改变这条残余,所以 `P-DEBT` 计数**不可销**),但**每次调用零留痕**这一半是要清偿的存量欠账:
|
|
95
|
-
* config-apply 期那条 `sema_registry_model_key_env_missing` warn 只覆盖「声明了 env 名却没设」,覆盖不到
|
|
96
|
-
* 「压根没配 per-model key 的 off-route 模型」,而后者才是真发生外泄的那一形。按 CLAUDE.md #157
|
|
97
|
-
* 「一时改不动的 P 类记 `P-DEBT`」补计数:命中条件 = **解不出 per-model key ∧ 该模型的 baseUrl 不是本
|
|
98
|
-
* 部署网关** ⇒ 这一次调用真把网关凭据发给了别家主机。`gatewayBaseUrl` 缺席(调用方没给参照系)⇒ 不计
|
|
99
|
-
* (不猜),与「宁可漏计不可错计」同向。
|
|
100
|
-
*/
|
|
101
9
|
export function createPerModelAuthSeat(getKeyResolver, opts = {}) {
|
|
102
10
|
return async (model) => {
|
|
103
|
-
// 🔴 **同代配对**(codex R1-[high],验真后采纳):resolver 引用必须在**第一个 await 之前**取。
|
|
104
|
-
// core 在同一个同步段里解析出 `model`(含它那一代的 `baseUrl`)后立刻调本席,所以本函数体的**第一条
|
|
105
|
-
// 同步语句**与模型解析同代。`await opts.upstream?.(model)` 是一道真 await 边界(`opts.upstream`
|
|
106
|
-
// 缺席时 `await undefined` 照样让出一个微任务),config-center 的 apply 提交段(#303 codex R1-F2 修成
|
|
107
|
-
// 「目录与密钥表零 await 窗成对换代」)可以整段落在那个缝里 ⇒ 缝之后再取 resolver 就会拿到**下一代**
|
|
108
|
-
// 的密钥表,配上**上一代**的模型/端点:新家的 key 发到旧家的 URL —— 正是本席存在要消灭的那个形。
|
|
109
|
-
// 取引用是纯读、无副作用,即使上游随后胜出也不多花什么。
|
|
110
11
|
const resolve = getKeyResolver();
|
|
111
12
|
const given = await opts.upstream?.(model);
|
|
112
13
|
if (given?.apiKey !== undefined)
|
|
113
|
-
return given;
|
|
114
|
-
// 🔴 不 try/catch:poison 必须上抛(静默回落 = 拿共享网关账号打这只模型的上游)。
|
|
115
|
-
// resolver 缺席(部署根本没配 per-model key 平面)与解析回 undefined(这只模型没有自己的 key)
|
|
116
|
-
// 两形都落到同一条缺席语义:回 undefined ⇒ core 用网关 key(additive 契约)。
|
|
14
|
+
return given;
|
|
117
15
|
const resolved = resolve ? await resolve({ name: model.name }) : undefined;
|
|
118
16
|
if (resolved === undefined && isOffRouteBaseUrl(model.baseUrl, opts.gatewayBaseUrl)) {
|
|
119
17
|
recordFailOpen("server.model-key.off-route-model-uses-gateway-key", model.name);
|
|
@@ -121,34 +19,17 @@ export function createPerModelAuthSeat(getKeyResolver, opts = {}) {
|
|
|
121
19
|
return resolved;
|
|
122
20
|
};
|
|
123
21
|
}
|
|
124
|
-
/**
|
|
125
|
-
* per-model key 注入 brain wrapper —— **#341 后只服务 WebFetch 摘要面**
|
|
126
|
-
* (`boot/webfetch-summarize-lane.ts`)。side-query 面已换到 core 的 spec 座位(见顶注「#341 换装」);
|
|
127
|
-
* core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`(亲验 5.46.0 `dist/tools/web.d.ts`),
|
|
128
|
-
* 那一面**没有座位可换**,只能继续在 brain 层补凭据。
|
|
129
|
-
*
|
|
130
|
-
* 语义(与 `createPerModelAuthSeat` 同一张表,只是注入位在 `options.apiKey`):
|
|
131
|
-
* · `options.apiKey` **已在场** ⇒ 原样透传(上游/装饰器已经定了凭据,本层不抢);
|
|
132
|
-
* · resolver 缺席 / 返回 undefined ⇒ 不注入 ⇒ 网关 key(additive 契约);
|
|
133
|
-
* · resolver 抛(sealed key 中毒)⇒ **上抛**,本次调用整体失败,一个字节都不发。
|
|
134
|
-
*
|
|
135
|
-
* `complete` 在场时同样包一层:core 的 `Brain.complete` 是 compaction/摘要腿的直调口(WebFetch 摘要器
|
|
136
|
-
* 正是**优先**走它),漏包等于给同一个病留第二条路。
|
|
137
|
-
*/
|
|
138
22
|
export function createPerModelKeyBrain(brain, getKeyResolver, opts = {}) {
|
|
139
23
|
const seat = createPerModelAuthSeat(getKeyResolver, opts);
|
|
140
24
|
const keyFor = async (model, given) => {
|
|
141
25
|
if (given !== undefined)
|
|
142
|
-
return given;
|
|
26
|
+
return given;
|
|
143
27
|
return (await seat(model))?.apiKey;
|
|
144
28
|
};
|
|
145
29
|
const stream = async (model, context, options) => {
|
|
146
30
|
const apiKey = await keyFor(model, options?.apiKey);
|
|
147
31
|
return brain.stream(model, context, apiKey === undefined ? options : { ...options, apiKey });
|
|
148
32
|
};
|
|
149
|
-
// 🔴 A-057.2:`.bind(brain)` **保留接收者** —— 裸摘引用(`const complete = brain.complete`)在 ESM
|
|
150
|
-
// 严格模式下让 `this` = undefined,任何读 `this` 的 Brain 实现经本 wrapper 后当场 TypeError,
|
|
151
|
-
// 而两行之上的 `stream` 臂是带接收者的 `brain.stream(…)`,同一只 wrapper 两条臂不该有两种调用形。
|
|
152
33
|
const complete = brain.complete?.bind(brain);
|
|
153
34
|
return {
|
|
154
35
|
stream,
|
|
@@ -162,22 +43,6 @@ export function createPerModelKeyBrain(brain, getKeyResolver, opts = {}) {
|
|
|
162
43
|
: {}),
|
|
163
44
|
};
|
|
164
45
|
}
|
|
165
|
-
/**
|
|
166
|
-
* 装配 side-query 执行席 = 真 `runSideQuery` + core 的 per-model auth 座,models/roles 与 Runner 同源。
|
|
167
|
-
*
|
|
168
|
-
* **按调用**取三件活值(`models` / `getKeyResolver` / `gatewayBaseUrl`),因为它们是**同一次** center
|
|
169
|
-
* 下发里成对换代的(#303 codex R1-F2:目录与密钥表零 await 窗成对换代):
|
|
170
|
-
* · `models` = `expandTiers(config.models, config.tiers)` —— 档位词 / CC 别名(`flash`/`opus`/…)在
|
|
171
|
-
* side-query 上照常可选路(core 的 `Runner` 构造时做同一件事),且 `Runner` 那次展开是**构造时快照**,
|
|
172
|
-
* 而本路由的模型白名单门(`routes/side-query.ts` 的 `isModelAllowlisted`)按请求现算同一张增广目录 ——
|
|
173
|
-
* 两边取同一份才不会出现「名单放行、选路说不认识」的分歧;
|
|
174
|
-
* · `getKeyResolver()` —— 热应用整个换 resolver 引用,活取才跟得上(装配时快照 = 旧 key 用到重启);
|
|
175
|
-
* · `config.gatewayBaseUrl` —— off-route 判据的参照系。装配时快照会让「换网关那一刻起,这次调用把网关
|
|
176
|
-
* 凭据发给了别家」在遥测里永久失明(钉:`test/side-query-key-plane.test.ts` 的活取钉)。
|
|
177
|
-
*
|
|
178
|
-
* `spec` 自带的 `getApiKeyAndHeaders`(嵌入方/装饰器)**不被覆盖** —— 它作为 `upstream` 折进本席,
|
|
179
|
-
* 每次调用先问它,它没意见时才由本席按模型解(见 `createPerModelAuthSeat` 顶注第一条臂)。
|
|
180
|
-
*/
|
|
181
46
|
export function createSideQueryLane(ctx) {
|
|
182
47
|
return (spec) => runSideQuery({
|
|
183
48
|
...spec,
|