@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
package/dist/model-select.js
CHANGED
|
@@ -1,43 +1,11 @@
|
|
|
1
1
|
import { parseModelMention } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
3
|
-
* Resolve the model ref for a task spec from the request (CLI `/model` picker + `run --model`).
|
|
4
|
-
*
|
|
5
|
-
* Priority (a deliberate selection beats incidental free text):
|
|
6
|
-
* 1) an explicit, allow-listed `body.model` (the picker / `--model <ref>` flag),
|
|
7
|
-
* 2) an inline `@name` mention in the objective (core's `parseModelMention`),
|
|
8
|
-
* 3) the `default` role.
|
|
9
|
-
*
|
|
10
|
-
* Security: BOTH the explicit field and the @mention are gated by the SAME catalog (`config.models` keys), so
|
|
11
|
-
* untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention
|
|
12
|
-
* only ever returns an allow-listed catalog name).
|
|
13
|
-
*
|
|
14
|
-
* [865]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个):
|
|
15
|
-
* - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**,
|
|
16
|
-
* 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的
|
|
17
|
-
* 上下文污染路径)。
|
|
18
|
-
* - 真未知不再无声:FRESH submit 在 HTTP 门 400 fail-loud(带 available 列表,http/server.ts);本函数保持
|
|
19
|
-
* 不 throw(RESUME 重放持久化 body 不过 400 门——模型可能事后被移出目录,砖死 resume 比降级更糟),但把
|
|
20
|
-
* 未知 ref 透出给调用方(`unknownExplicit`),resolveSpec 记 warn——降级永远可见,绝不静默。
|
|
21
|
-
*
|
|
22
|
-
* Returns the chosen model ref AND the objective with any `@mention` stripped (parseModelMention's cleanedText).
|
|
23
|
-
*
|
|
24
|
-
* 🔴 Was a real defect: resolveSpec previously read ONLY the inline @mention, so `body.model` (the picker + the
|
|
25
|
-
* `--model` flag) was SILENTLY DROPPED — the picker UX was a no-op server-side. This restores it.
|
|
26
|
-
*/
|
|
27
2
|
export function resolveTaskModel(bodyModel, objective, catalog, allowlist) {
|
|
28
3
|
const mention = parseModelMention(objective, Object.keys(catalog));
|
|
29
4
|
const explicit = typeof bodyModel === "string" ? matchCatalogModel(bodyModel, catalog) : undefined;
|
|
30
|
-
// 未匹配的「在场」ref 一律透出(codex L1:历史 checkpoint 里的空串/非字符串 model 在 RESUME 重放也不许
|
|
31
|
-
// 静默降级——非字符串给类型表示,不 stringify 值本体防日志泄内容)。
|
|
32
5
|
const present = bodyModel !== undefined && bodyModel !== null;
|
|
33
6
|
const unknown = present && explicit === undefined;
|
|
34
7
|
const repr = typeof bodyModel !== "string" ? `<non-string:${typeof bodyModel}>` : bodyModel === "" ? "<empty-string>" : bodyModel;
|
|
35
8
|
const model = explicit ?? mention.model ?? "default";
|
|
36
|
-
// #233 / A-002.8:名单只裁**用户逐 turn 选择通道**——显式 ref(body.model / settings.model)与 objective
|
|
37
|
-
// 内联 `@name`。两者都缺席时落 `default`(operator 配置通道),本门**不碰**:名单管"用户可点什么",
|
|
38
|
-
// 不管"operator 配什么"(registry-core 也只校验名单 ⊆ catalog,从不要求 default ∈ 名单)。
|
|
39
|
-
// 不 throw:resume 重放持久化 body 不过 HTTP 门,砖死 resume 比降级糟(本文件 [865]② 同判据)——
|
|
40
|
-
// 判别式拒因交给调用点按 leg 分流(fresh 400 / resume warn 续跑)。
|
|
41
9
|
const picked = explicit !== undefined ? { ref: explicit, source: "explicit", requested: repr } : mention.model !== undefined ? { ref: mention.model, source: "mention", requested: mention.model } : undefined;
|
|
42
10
|
const notAllowed = picked !== undefined && !isModelAllowlisted(picked.ref, catalog, allowlist)
|
|
43
11
|
? { requested: picked.requested, resolved: terminalCatalogName(picked.ref, catalog) ?? picked.ref, source: picked.source }
|
|
@@ -49,28 +17,6 @@ export function resolveTaskModel(bodyModel, objective, catalog, allowlist) {
|
|
|
49
17
|
...(notAllowed ? { notAllowed } : {}),
|
|
50
18
|
};
|
|
51
19
|
}
|
|
52
|
-
/**
|
|
53
|
-
* #233 / A-002.8 —— **归一条款**:membership 判定前把 ref 归一到**终端 catalog 条目**再比。
|
|
54
|
-
*
|
|
55
|
-
* 为什么不能拿字符串直接跟名单比:`expandTiers` 的增广目录里,档位词(`pro`)与 CC 别名(`sonnet`)
|
|
56
|
-
* 的值与 `config.models[<name>]` 是**同一个对象引用**——裸串比较会让 `@pro` 绕过一张按 name 写的名单
|
|
57
|
-
* (同一只模型,两个键)。所以两边(ref 与每个名单条目)都先经 {@link matchCatalogModel} 解到目录条目,
|
|
58
|
-
* 再按 **引用相等** 判同。id 形的 ref 不需要特别处理:`matchCatalogModel` 本身就是 name+id 双键索引,
|
|
59
|
-
* 它会把 id 解成目录键,引用相等接着就对上了。
|
|
60
|
-
*
|
|
61
|
-
* 🔴 **刻意没有「id 相等」退路**(codex 复审 R1 [high],已采纳):目录**允许两条不同 name 的条目共享
|
|
62
|
-
* 同一个上游 `Model.id`,却各自带不同的 `baseUrl` / `apiKeyEnv` / `cost` / provider** —— 那正是多网关、
|
|
63
|
-
* 多账号部署形。按 id 判同会把 `safe` 与 `premium` 合并,于是一张只含 `safe` 的名单会放行 `premium`,
|
|
64
|
-
* 而真正执行的是 `premium` 那份坐标与计费。判据只认**目录条目本身**:两条条目就是两只模型,哪怕上游
|
|
65
|
-
* id 撞车。(引用相等因此不是"优化",是唯一正确的判据;它天然覆盖 tier 词/别名/id 三形。)
|
|
66
|
-
*
|
|
67
|
-
* 缺席/空名单 ⇒ 恒 `true`(ship-dark:缺省 = 旧行为,全放行)。
|
|
68
|
-
* 目录里根本没有的 ref ⇒ 恒 `true`:那是「未知模型」那道门的辖区(fresh 400 / resume 降级留声),
|
|
69
|
-
* 本门只答「在目录里,但准不准点」——两道门同码同拒会让消费端分不清"没这模型"和"不让你点"。
|
|
70
|
-
*
|
|
71
|
-
* ⚠️ 射程:本门是**提交时**门。core 运行时路由(degrade 降级链 / cascade 梯 / roles 解析)按 operator
|
|
72
|
-
* 配置自己走,**不经本门**——那不是一次用户选择,如实声明,不假装是运行时执法。
|
|
73
|
-
*/
|
|
74
20
|
export function isModelAllowlisted(ref, catalog, allowlist) {
|
|
75
21
|
if (allowlist === undefined || allowlist.length === 0)
|
|
76
22
|
return true;
|
|
@@ -80,25 +26,16 @@ export function isModelAllowlisted(ref, catalog, allowlist) {
|
|
|
80
26
|
for (const name of allowlist) {
|
|
81
27
|
const allowed = catalogEntryOf(name, catalog);
|
|
82
28
|
if (allowed === undefined)
|
|
83
|
-
continue;
|
|
29
|
+
continue;
|
|
84
30
|
if (allowed === entry)
|
|
85
|
-
return true;
|
|
31
|
+
return true;
|
|
86
32
|
}
|
|
87
33
|
return false;
|
|
88
34
|
}
|
|
89
|
-
/** ref(name / 档位词 / 别名 / id)→ 它在目录里的**条目对象**;未知 ⇒ undefined。 */
|
|
90
35
|
function catalogEntryOf(ref, catalog) {
|
|
91
36
|
const name = matchCatalogModel(ref, catalog);
|
|
92
37
|
return name === undefined ? undefined : catalog[name];
|
|
93
38
|
}
|
|
94
|
-
/**
|
|
95
|
-
* #233 —— ref → **终端 catalog name**(拒因/日志的可读形)。
|
|
96
|
-
*
|
|
97
|
-
* 为什么不是 `matchCatalogModel` 的返回值:增广目录里 `pro`/`opus`/`default` 都是**真实存在的键**,
|
|
98
|
-
* 于是 `matchCatalogModel("pro")` 回的就是 `"pro"` —— 那是用户点的那个别名,不是他真会跑的模型。
|
|
99
|
-
* 取条目自己的 `Model.name`(applyEffective 建目录时 `models[m.name].name === m.name`,env lane 同)
|
|
100
|
-
* 才是终端名;拿不到时如实退回匹配键(诚实缺席,不猜)。
|
|
101
|
-
*/
|
|
102
39
|
export function terminalCatalogName(ref, catalog) {
|
|
103
40
|
const key = matchCatalogModel(ref, catalog);
|
|
104
41
|
if (key === undefined)
|
|
@@ -106,13 +43,6 @@ export function terminalCatalogName(ref, catalog) {
|
|
|
106
43
|
const name = catalog[key]?.name;
|
|
107
44
|
return typeof name === "string" && name.length > 0 ? name : key;
|
|
108
45
|
}
|
|
109
|
-
/**
|
|
110
|
-
* [865]② name+id 双键目录索引:返回 ref 命中的目录 **name 键**(spec.model 的规范形),未知 → undefined。
|
|
111
|
-
* - name 命中用 Object.hasOwn(🔴 不用真值索引:Object.prototype 继承键 "constructor"/"__proto__" 等在裸
|
|
112
|
-
* 索引下是真值,会绕过 allow-list——沿袭原实现的纪律)。
|
|
113
|
-
* - id 命中扫目录值(`Model.id`),跳过 "default" 别名键(它与其指向的真身共享 id,解析到别名会让 /model
|
|
114
|
-
* 面板与实际路由再度撕裂);同 id 多 name 时取目录序第一个(与 /v1/models 列表序一致,确定性)。
|
|
115
|
-
*/
|
|
116
46
|
export function matchCatalogModel(ref, catalog) {
|
|
117
47
|
if (Object.hasOwn(catalog, ref))
|
|
118
48
|
return ref;
|
|
@@ -124,16 +54,9 @@ export function matchCatalogModel(ref, catalog) {
|
|
|
124
54
|
}
|
|
125
55
|
return undefined;
|
|
126
56
|
}
|
|
127
|
-
/**
|
|
128
|
-
* vision precheck: a catalog model can read images iff its `input` modality list
|
|
129
|
-
* includes `"image"` (the SAME test as the vision capability on GET /v1/models — `server.ts`). FALSE-SAFE on
|
|
130
|
-
* missing/unknown modality info: returns `true` (do NOT block) — only an EXPLICIT text-only `input` triggers the
|
|
131
|
-
* precheck, so a model that never declared its modalities is never wrongly 422'd. Lets a request with images 422
|
|
132
|
-
* up-front instead of hitting the downstream gateway as an opaque deserialize 400 on a text-only model.
|
|
133
|
-
*/
|
|
134
57
|
export function modelSupportsImages(model) {
|
|
135
58
|
if (!model || !Array.isArray(model.input))
|
|
136
|
-
return true;
|
|
59
|
+
return true;
|
|
137
60
|
return model.input.includes("image");
|
|
138
61
|
}
|
|
139
62
|
//# sourceMappingURL=model-select.js.map
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
export class CostQuota {
|
|
2
2
|
now;
|
|
3
3
|
windows = new Map();
|
|
4
|
-
/** 🔴 不再 `readonly`:限额/窗长是比较参数,换代只动它们,窗内已累计的花费一个不动(design/322 §5)。 */
|
|
5
4
|
limitMicroUsd;
|
|
6
5
|
windowMs;
|
|
7
|
-
/** @param limitMicroUsd ceiling per window (integer micro-USD; `<=0` = OFF sentinel). @param windowMs rolling window. */
|
|
8
6
|
constructor(limitMicroUsd, windowMs, now = () => Date.now()) {
|
|
9
7
|
this.now = now;
|
|
10
8
|
this.limitMicroUsd = limitMicroUsd;
|
|
11
9
|
this.windowMs = windowMs;
|
|
12
10
|
}
|
|
13
|
-
/**
|
|
14
|
-
* 值换代。两件事**语义不同**,别混:
|
|
15
|
-
* · **限额**换代 = 纯比较参数 ⇒ 窗内已累计的花费一个不动(design/322 §5 定案:调小当场更严)。
|
|
16
|
-
* · **窗长**换代 = 换**记账周期** ⇒ 当代累计重开。
|
|
17
|
-
*
|
|
18
|
-
* 🔴 后者是 codex R1-F4(真 finding,红先):SQL 腿的窗长是**桶键的一部分**(`floor(now/windowMs)`),
|
|
19
|
-
* 换窗长后当代桶天然重算 ⇒ 已累计不跟着搬家;内存腿修前却保留原窗口累计。于是**同一条发布**在
|
|
20
|
-
* 内存部署上继续拒绝超额 principal、在 PG/TiDB 部署上重新放行一整个额度 —— 后端相关的成本 fail-open。
|
|
21
|
-
* 统一取 SQL 腿那一侧(唯一能在两条腿上都实现的语义:SQL 侧无法把旧桶累计搬进新桶)。
|
|
22
|
-
*/
|
|
23
11
|
setLimits(next) {
|
|
24
12
|
this.limitMicroUsd = next.limitMicroUsd;
|
|
25
13
|
if (next.windowMs !== this.windowMs) {
|
|
@@ -27,9 +15,7 @@ export class CostQuota {
|
|
|
27
15
|
this.windows.clear();
|
|
28
16
|
}
|
|
29
17
|
}
|
|
30
|
-
/** Is this principal under quota right now? (Reads only — does not consume.) */
|
|
31
18
|
check(key) {
|
|
32
|
-
// 关断哨兵:零窗表访问,如实报 `limitMicroUsd: 0`(消费方据此知道「本部署此刻没有 ceiling」)。
|
|
33
19
|
if (this.limitMicroUsd <= 0)
|
|
34
20
|
return { allowed: true, usedMicroUsd: 0, limitMicroUsd: 0, retryAfterSec: 0 };
|
|
35
21
|
const t = this.now();
|
|
@@ -40,10 +26,9 @@ export class CostQuota {
|
|
|
40
26
|
const retryAfterSec = !allowed && live ? Math.ceil((w.start + this.windowMs - t) / 1000) : 0;
|
|
41
27
|
return { allowed, usedMicroUsd: used, limitMicroUsd: this.limitMicroUsd, retryAfterSec };
|
|
42
28
|
}
|
|
43
|
-
/** Add spend for a principal, starting a fresh window if the prior one expired. */
|
|
44
29
|
add(key, micro) {
|
|
45
30
|
if (this.limitMicroUsd <= 0)
|
|
46
|
-
return;
|
|
31
|
+
return;
|
|
47
32
|
if (micro <= 0)
|
|
48
33
|
return;
|
|
49
34
|
const t = this.now();
|
|
@@ -53,7 +38,6 @@ export class CostQuota {
|
|
|
53
38
|
else
|
|
54
39
|
w.micro += micro;
|
|
55
40
|
}
|
|
56
|
-
/** Drop expired windows (called from the background reaper) to bound memory. */
|
|
57
41
|
reap() {
|
|
58
42
|
const t = this.now();
|
|
59
43
|
let dropped = 0;
|
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/80 b (item 3) — the INFRA cost axes the SERVICE owns, composed with core's LLM-family `costBreakdown`
|
|
3
|
-
* for the supervisor cost surface.
|
|
4
|
-
*
|
|
5
|
-
* (design/158 N24: moved here from the single-file `src/finance/` directory — lens4 §4.4 flagged the "cost"
|
|
6
|
-
* concept as split across `finance/cost-taxonomy.ts` + `observability/cost-quota.ts` + `budget.ts`. Chose
|
|
7
|
-
* "dissolve finance/, fold into observability/" over "collect all three into finance/": this module + the
|
|
8
|
-
* quota tracker are both cost REPORT/GUARD layers that feed metrics/decisions (observability's existing
|
|
9
|
-
* job), and budget.ts stays put — it has ~19 import sites and was already the N18 move target this same
|
|
10
|
-
* batch; stacking a second wide-fanout move on it in one session was the higher-risk option for no added
|
|
11
|
-
* clarity.)
|
|
12
|
-
*
|
|
13
|
-
* The engine prices ONLY LLM tokens (core's `Stats.costBreakdown` = llmRoot / nestedSubagent / memoryConsolidation
|
|
14
|
-
* / compaction). It has NO $ for tool calls, sandbox wall-clock, or network egress — those are INFRA/service costs
|
|
15
|
-
* the service owns the data + rates for. This module is the thin report layer: it turns a
|
|
16
|
-
* per-run infra USAGE + the deployment's RATES into an infra breakdown, then composes it with the core LLM
|
|
17
|
-
* breakdown into one supervisor-facing total. Pure + rate-driven (rates default 0 ⇒ infra reads $0, exactly like
|
|
18
|
-
* MODEL_COST_* — opt-in pricing). The per-run usage extraction (sandbox wall-clock from the run's remote-env
|
|
19
|
-
* spans, tool-call count from the trace) is the data-source wiring fed in by the caller; this layer is the math.
|
|
20
|
-
*/
|
|
21
1
|
const nonNeg = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
22
|
-
/** Price a run's infra usage. Every term clamps negatives to 0 (a bad usage/rate never produces a negative cost). */
|
|
23
2
|
export function infraCost(usage, rates) {
|
|
24
3
|
const toolCallMicroUsd = nonNeg(usage.toolCalls) * nonNeg(rates.toolCallMicroUsd);
|
|
25
4
|
const sandboxWalltimeMicroUsd = (nonNeg(usage.sandboxWalltimeMs) / 1000) * nonNeg(rates.sandboxSecMicroUsd);
|
|
@@ -31,25 +10,12 @@ export function infraCost(usage, rates) {
|
|
|
31
10
|
totalMicroUsd: toolCallMicroUsd + sandboxWalltimeMicroUsd + egressMicroUsd,
|
|
32
11
|
};
|
|
33
12
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Compose the supervisor cost surface from core's LLM breakdown + total and the service's infra breakdown.
|
|
36
|
-
* `llmTotalMicroUsd` is core's authoritative priced-LLM total (`stats.costMicroUsd`) — the grand total is that +
|
|
37
|
-
* infra, NOT a re-sum of the LLM sub-categories (so a breakdown category bug can't move the grand total).
|
|
38
|
-
*/
|
|
39
13
|
export function composeSupervisorCost(llm, llmTotalMicroUsd, infra) {
|
|
40
14
|
return { llm, infra, totalMicroUsd: nonNeg(llmTotalMicroUsd) + infra.totalMicroUsd };
|
|
41
15
|
}
|
|
42
|
-
/** True iff ANY infra axis is priced — the gate for surfacing the composed supervisor cost (when all rates are 0
|
|
43
|
-
* the infra contribution is always $0, so the client just reads core's `stats.costBreakdown` directly). */
|
|
44
16
|
export function hasInfraPricing(r) {
|
|
45
17
|
return r.toolCallMicroUsd > 0 || r.sandboxSecMicroUsd > 0 || r.egressGbMicroUsd > 0;
|
|
46
18
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Extract a run's infra USAGE from its event log + wall-clock span. `toolCalls` = the `tool_start` events (one per
|
|
49
|
-
* tool invocation). `sandboxWalltimeMs` ≈ the run's wall-clock duration (the remote-env is connected for ~the
|
|
50
|
-
* run's active life; the precise per-span sum lives in logs, not the event trace — the run duration is the
|
|
51
|
-
* faithful per-run proxy). `egressBytes` = 0 (not yet metered per run).
|
|
52
|
-
*/
|
|
53
19
|
export function infraUsageFromEvents(events, runDurationMs) {
|
|
54
20
|
return {
|
|
55
21
|
toolCalls: events.reduce((n, e) => (e.type === "tool_start" ? n + 1 : n), 0),
|
|
@@ -1,36 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #157 fail-open 治理专项 —— 兜底路径的**响亮化**工具(server 半场;跨仓方案见 sema-comms
|
|
3
|
-
* `audits/failopen-governance-176.md` §2,三仓语义对齐、实现各自)。
|
|
4
|
-
*
|
|
5
|
-
* ## 它解决的问题
|
|
6
|
-
* 「走了兜底」与「一切正常」在外部**同形**——成功回执一样、日志一样、指标一样。于是一个静默降级
|
|
7
|
-
* 能带病运行数周,只能靠"该发生的事没发生"这种间接症状发现。本模块给每条合法兜底臂一个**登记过的
|
|
8
|
-
* tag**,走到即留痕三件套:
|
|
9
|
-
*
|
|
10
|
-
* 1. **计数(逐次)** —— `fail_open_total{tag,cls}`;
|
|
11
|
-
* 2. **探针文件(逐次)** —— `SEMA_FAILOPEN_PROBE=<path>` 在场时逐次追加 `fail-open <tag> <detail>`。
|
|
12
|
-
* 逐次是承重的:同进程多腿时"第二腿起无行"会让「无行=没走兜底」的黑盒断言变成假绿;
|
|
13
|
-
* 3. **一次性 warn(每进程每 tag 一次)** —— 防刷屏。**只有这一件是一次性的**,计数与探针都逐次。
|
|
14
|
-
*
|
|
15
|
-
* ## 它**不**解决的问题(封装不是合法化通道)
|
|
16
|
-
* 词表的 `cls` 只有 `F` 与 `P-DEBT` 两种,**结构性**地把 P/Z 类挡在外面:
|
|
17
|
-
* - **P 保护型**(权限门/判定词表/凭证/车道选择):正解是 fail-closed,不是"留痕后继续放行"。
|
|
18
|
-
* 确实一时改不动的,才允许以 `P-DEBT` 登记——让债在遥测里显形,而不是让它消失。
|
|
19
|
-
* - **Z 计量型**(重试/预算/超时/清理范围):正解是归零 + 自己的节流告警
|
|
20
|
-
* (先例:`boot/reapers.ts` 的 `createThrottledReaperCatch`),不走本封装。
|
|
21
|
-
*
|
|
22
|
-
* ## 词表是闭集
|
|
23
|
-
* {@link FailOpenTag} 由 {@link FAIL_OPEN_TAGS} 的键推导,未登记的 tag 是**编译期错误**——
|
|
24
|
-
* "新 tag 须登记"因此是机器执法的,不靠自觉。新增 tag 同时要在 `docs/FAIL-OPEN-CENSUS.md` 补行。
|
|
25
|
-
*
|
|
26
|
-
* ## 进程级而非注入级
|
|
27
|
-
* 三件套里有两件(一次性 warn、进程计数)天然是**进程**范围的,且真正的兜底臂往往在没有 logger/
|
|
28
|
-
* metrics 句柄的深处(`question.ts` / `elicitation.ts` 全文零观测通道)。所以对外主口是自由函数
|
|
29
|
-
* {@link recordFailOpen},装配层用 {@link installFailOpenRecorder} 把 logger/metrics 接上即可;
|
|
30
|
-
* 装配之前的调用不会丢——探针与 stderr 契约行不依赖注入,计数也照常累加并在装配时继承过去。
|
|
31
|
-
*/
|
|
32
1
|
import { appendFileSync } from "node:fs";
|
|
33
|
-
/** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
|
|
34
2
|
export const FAIL_OPEN_TAGS = {
|
|
35
3
|
"server.run-store.file-hydrate-unreadable": {
|
|
36
4
|
cls: "P-DEBT",
|
|
@@ -149,26 +117,12 @@ export const FAIL_OPEN_TAGS = {
|
|
|
149
117
|
note: "A-057.59:一只 **baseUrl 指向别家主机**(非本部署 `gatewayBaseUrl`)的目录模型**没有** per-model key ⇒ `createPerModelAuthSeat` 回 `undefined`(side-query 面 = core 5.46.0 的 `SideQuerySpec.getApiKeyAndHeaders` 座位;WebFetch 摘要面 = 同一只座位包在 `createPerModelKeyBrain` 里,那面 core 至今无座位)⇒ core 的 `options?.apiKey ?? config.apiKey` 回落**主网关 key**,而同一行下面的 `model.baseUrl || config.baseUrl` 仍把请求路由到那台外部主机 —— 这一次调用真把共享网关凭据发给了运维在 config-center 里指定的另一家。方向明知不对(hook 面对逐字同形的场景是 TRUE fail-closed:「refusing to send the prompt off-gateway」),之所以记债而不是当场收严:该缺席语义与**主推理链**(`dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂)逐字同语义,只收严 wrapper 这两面会造成「同一只模型跑任务能用、问一句 401」,且「同账号多网关主机/多区域」是既有且正当的部署形、无旋钮可退。计数覆盖两面(side-query / WebFetch 摘要),`detail` = 模型名。⚠️ 判据需要参照系:调用方没给 `gatewayBaseUrl` ⇒ **不计**(宁可漏计不可错计),故计数是**下限**。终局收口 = 跨面收严件 #309(三面同批 + 旋钮 + 表态制),届时本 tag 同批销。",
|
|
150
118
|
},
|
|
151
119
|
};
|
|
152
|
-
/**
|
|
153
|
-
* 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
|
|
154
|
-
*
|
|
155
|
-
* 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
|
|
156
|
-
* 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
|
|
157
|
-
* 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
|
|
158
|
-
*
|
|
159
|
-
* ⚠️ **两条 question 臂在生产上已不可达**(#173 之后,2026-08-07):本函数唯一的生产调用点是
|
|
160
|
-
* `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
|
|
161
|
-
* `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
|
|
162
|
-
* question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
|
|
163
|
-
* 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
|
|
164
|
-
* 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
|
|
165
|
-
*/
|
|
166
120
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
167
121
|
switch (frameType) {
|
|
168
|
-
case "question":
|
|
122
|
+
case "question":
|
|
169
123
|
case "elicitation":
|
|
170
124
|
return "server.hitl.frame-undelivered-stream-closed";
|
|
171
|
-
case "question_complete":
|
|
125
|
+
case "question_complete":
|
|
172
126
|
return "server.question.complete-breadcrumb-dropped";
|
|
173
127
|
case "elicitation_complete":
|
|
174
128
|
return "server.elicitation.complete-breadcrumb-dropped";
|
|
@@ -176,9 +130,7 @@ export function failOpenTagForDroppedFrame(frameType) {
|
|
|
176
130
|
return "server.stream.frame-dropped-stream-closed";
|
|
177
131
|
}
|
|
178
132
|
}
|
|
179
|
-
/** 探针文件的 env 名(跨仓同名同形:`audits/failopen-governance-176.md` §2-r2,壳/引擎同认)。 */
|
|
180
133
|
export const FAIL_OPEN_PROBE_ENV = "SEMA_FAILOPEN_PROBE";
|
|
181
|
-
/** 计数器名。`cls` 进标签:一眼分得出"合法兜底"与"保护型欠账"两种曲线,后者应当只降不升。 */
|
|
182
134
|
const FAIL_OPEN_COUNTER = "fail_open_total";
|
|
183
135
|
export function createFailOpenRecorder(deps = {}) {
|
|
184
136
|
const counts = new Map(deps.seedCounts ?? []);
|
|
@@ -189,52 +141,40 @@ export function createFailOpenRecorder(deps = {}) {
|
|
|
189
141
|
const writeStderr = deps.writeStderr ?? ((line) => void process.stderr.write(line));
|
|
190
142
|
const appendProbe = deps.appendProbe ?? ((path, line) => appendFileSync(path, line));
|
|
191
143
|
deps.metrics?.counter(FAIL_OPEN_COUNTER, "fallback (fail-open) arms traversed, by registered tag — see docs/FAIL-OPEN-CENSUS.md");
|
|
192
|
-
// 补账:装配前(无 metrics/无 logger)命中的那几次不能凭空消失——否则 `fail_open_total` 永久
|
|
193
|
-
// 少算,少的还正是 boot 期兜底。metrics 到场即把欠账一次性灌进去并清零(所以再 install 一次不会重放),
|
|
194
|
-
// 结构化 warn 同理按 tag 补一条。stderr 契约行**不补**:它在装配前就已经喊过了,一 tag 一次是它的全部价值。
|
|
195
|
-
// 🔴 记账**后于**投递(codex R2 复审 MEDIUM):先清 backlog / 先 `logged.add` 再投递,等于把一次
|
|
196
|
-
// 投递失败记成"已送达"——欠账从此无法重放,而它保的正是最需要补的那批 boot 期兜底。
|
|
197
144
|
if (deps.metrics !== undefined && backlog.size > 0) {
|
|
198
145
|
for (const [tag, n] of [...backlog]) {
|
|
199
146
|
try {
|
|
200
147
|
deps.metrics.inc(FAIL_OPEN_COUNTER, { tag, cls: FAIL_OPEN_TAGS[tag].cls }, n);
|
|
201
|
-
backlog.delete(tag);
|
|
148
|
+
backlog.delete(tag);
|
|
202
149
|
}
|
|
203
150
|
catch {
|
|
204
|
-
/* 留在 backlog 里,下一次装配再补 */
|
|
205
151
|
}
|
|
206
152
|
}
|
|
207
153
|
}
|
|
208
|
-
// 补痕的起算集=**观测到的 tag 全集**(`counts`),不是"stderr 喊成功过的那批"(`warned`)——
|
|
209
|
-
// 后者在"装配前命中 + stderr 正断裂"这条组合路上恒空,那次兜底的结构化痕迹就永久没了(codex R3)。
|
|
210
154
|
if (deps.logger !== undefined) {
|
|
211
155
|
for (const tag of counts.keys()) {
|
|
212
156
|
if (logged.has(tag))
|
|
213
157
|
continue;
|
|
214
158
|
try {
|
|
215
159
|
deps.logger.warn("fail_open", { tag, cls: FAIL_OPEN_TAGS[tag].cls, note: FAIL_OPEN_TAGS[tag].note, backfilled: true });
|
|
216
|
-
logged.add(tag);
|
|
160
|
+
logged.add(tag);
|
|
217
161
|
}
|
|
218
162
|
catch {
|
|
219
|
-
/* 不记 logged,下一次装配/下一次命中再补 */
|
|
220
163
|
}
|
|
221
164
|
}
|
|
222
165
|
}
|
|
223
166
|
return {
|
|
224
167
|
record: (tag, detail) => {
|
|
225
|
-
// 三件套逐件独立 try:一件的失败(坏路径的探针文件、抛错的 logger 传输)绝不能让**另外两件**
|
|
226
|
-
// 也丢,更不能让调用方的兜底臂变成抛错——那等于把"降级"换成"故障",正是本模块要消灭的方向。
|
|
227
168
|
const entry = FAIL_OPEN_TAGS[tag];
|
|
228
169
|
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
229
170
|
if (deps.metrics === undefined) {
|
|
230
|
-
backlog.set(tag, (backlog.get(tag) ?? 0) + 1);
|
|
171
|
+
backlog.set(tag, (backlog.get(tag) ?? 0) + 1);
|
|
231
172
|
}
|
|
232
173
|
else {
|
|
233
174
|
try {
|
|
234
175
|
deps.metrics.inc(FAIL_OPEN_COUNTER, { tag, cls: entry.cls });
|
|
235
176
|
}
|
|
236
177
|
catch {
|
|
237
|
-
// 汇在场但这一次没进去 ⇒ 同样记欠账(而不是蒸发),等下一次装配补。
|
|
238
178
|
backlog.set(tag, (backlog.get(tag) ?? 0) + 1);
|
|
239
179
|
}
|
|
240
180
|
}
|
|
@@ -243,30 +183,23 @@ export function createFailOpenRecorder(deps = {}) {
|
|
|
243
183
|
appendProbe(probePath, `fail-open ${tag}${detail !== undefined && detail !== "" ? ` ${detail}` : ""}\n`);
|
|
244
184
|
}
|
|
245
185
|
catch {
|
|
246
|
-
/* 探针路径不可写(只读盘/权限)⇒ 静默放弃,不牵连本次兜底 */
|
|
247
186
|
}
|
|
248
187
|
}
|
|
249
|
-
// 只有**真发出去**才记进 logged —— 没有 logger 时记了,会让日后 install 的补账把这条当成
|
|
250
|
-
// "已经报过"而跳过,那正是本轮复审要修的漏账形。
|
|
251
188
|
if (deps.logger !== undefined && !logged.has(tag)) {
|
|
252
189
|
try {
|
|
253
190
|
deps.logger.warn("fail_open", { tag, cls: entry.cls, note: entry.note, ...(detail !== undefined ? { detail } : {}) });
|
|
254
|
-
logged.add(tag);
|
|
191
|
+
logged.add(tag);
|
|
255
192
|
}
|
|
256
193
|
catch {
|
|
257
|
-
/* 观测不得成为故障 */
|
|
258
194
|
}
|
|
259
195
|
}
|
|
260
196
|
if (warned.has(tag))
|
|
261
197
|
return;
|
|
262
198
|
try {
|
|
263
|
-
// 跨仓约定的黑盒锚,逐字 `[sema] fail-open: <tag>`。**不依赖** logger 是否装配上——
|
|
264
|
-
// 评测线的断言打的就是这一行,它在 boot 之前的兜底上也必须成立。
|
|
265
199
|
writeStderr(`[sema] fail-open: ${tag}\n`);
|
|
266
|
-
warned.add(tag);
|
|
200
|
+
warned.add(tag);
|
|
267
201
|
}
|
|
268
202
|
catch {
|
|
269
|
-
/* 断裂的 stderr(EPIPE 族)⇒ 丢这行,不丢进程 */
|
|
270
203
|
}
|
|
271
204
|
},
|
|
272
205
|
counts: () => counts,
|
|
@@ -275,16 +208,7 @@ export function createFailOpenRecorder(deps = {}) {
|
|
|
275
208
|
loggedTags: () => logged,
|
|
276
209
|
};
|
|
277
210
|
}
|
|
278
|
-
/** 进程单例。装配前也可用(探针+stderr 契约行照常,计数照常;只是没有结构化 warn/metric)。 */
|
|
279
211
|
let current = createFailOpenRecorder();
|
|
280
|
-
/**
|
|
281
|
-
* 装配层接线:把 logger/metrics 接到进程单例上。搬四份状态过去,各有各的理由:
|
|
282
|
-
* - `seedCounts` —— 逻辑总数续算,不从 0 重来;
|
|
283
|
-
* - `seedWarned` —— boot 期已喊过的 stderr 契约行不再喊(「每进程每 tag 一次」是它的全部价值);
|
|
284
|
-
* - `seedBacklog` —— 装配**前**命中的次数还没进任何 metrics 汇,交给新实例补账(补完清零,
|
|
285
|
-
* 所以第二次 install 不会重放);
|
|
286
|
-
* - `seedLogged` —— 已真发出去的结构化 warn 不重发;没发过的由新 logger 补一条。
|
|
287
|
-
*/
|
|
288
212
|
export function installFailOpenRecorder(deps) {
|
|
289
213
|
current = createFailOpenRecorder({
|
|
290
214
|
...deps,
|
|
@@ -295,15 +219,12 @@ export function installFailOpenRecorder(deps) {
|
|
|
295
219
|
});
|
|
296
220
|
return current;
|
|
297
221
|
}
|
|
298
|
-
/** 兜底臂的调用口。`tag` 必须已登记(闭集类型),`detail` 是可选的一行现场(路径/原因,不放密钥)。 */
|
|
299
222
|
export function recordFailOpen(tag, detail) {
|
|
300
223
|
current.record(tag, detail);
|
|
301
224
|
}
|
|
302
|
-
/** 本进程各 tag 累计次数——`/metrics` 之外的进程内读数(测试与自检面用)。 */
|
|
303
225
|
export function failOpenCounts() {
|
|
304
226
|
return current.counts();
|
|
305
227
|
}
|
|
306
|
-
/** test-only:换一个全新单例(计数与 warn 集合清零),让每个用例都从"第一次"看起。 */
|
|
307
228
|
export function resetFailOpenRecorderForTest(deps = {}) {
|
|
308
229
|
current = createFailOpenRecorder(deps);
|
|
309
230
|
return current;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
const ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2
|
-
// EPIPE hardening (live catch): a host that feeds this engine's stdout through a
|
|
3
|
-
// PIPE and exits first leaves the fd broken — the NEXT log write raises EPIPE. Without a listener
|
|
4
|
-
// that async 'error' event on process.stdout is fatal, which killed drainThenShutdown ON ITS FIRST
|
|
5
|
-
// LOG LINE (the SIGHUP reap never ran). Logs are droppable; the process is not — swallow both the
|
|
6
|
-
// sync throw (try/catch below) and the async 'error' event (no-op listener, attached once).
|
|
7
2
|
let stdioErrorsSwallowed = false;
|
|
8
3
|
function swallowStdioErrors() {
|
|
9
4
|
if (stdioErrorsSwallowed)
|
|
@@ -28,7 +23,6 @@ export function createLogger(minLevel = "info") {
|
|
|
28
23
|
(level === "error" || level === "warn" ? process.stderr : process.stdout).write(line + "\n");
|
|
29
24
|
}
|
|
30
25
|
catch {
|
|
31
|
-
// Broken/destroyed stream (EPIPE family): drop the line, never the process.
|
|
32
26
|
}
|
|
33
27
|
};
|
|
34
28
|
return {
|