@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/budget.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cost / budget / trace wiring for the Runner (core 5.x) — pure, testable helpers kept out of the
|
|
3
|
-
* auto-running main.ts:
|
|
4
|
-
* ③ buildPricing — Model.cost catalog → injected ModelPricing (authoritative costMicroUsd).
|
|
5
|
-
* ④ createTracer — core's content-free TraceEvents → /metrics (spend, first-token, tool calls).
|
|
6
|
-
* ⑤ cappedCeiling — clamp a caller's requested per-task budget DOWN to the operator ceiling.
|
|
7
|
-
*/
|
|
8
1
|
import { modelCostToPricing } from "@sema-agent/core";
|
|
9
2
|
import { currentPrincipal } from "./observability/principal-context.js";
|
|
10
|
-
import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
|
|
11
|
-
/** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent). */
|
|
3
|
+
import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
|
|
12
4
|
export function buildPricing(models) {
|
|
13
5
|
const pricing = {};
|
|
14
6
|
for (const m of Object.values(models))
|
|
@@ -16,28 +8,9 @@ export function buildPricing(models) {
|
|
|
16
8
|
pricing[m.id] = modelCostToPricing(m.cost);
|
|
17
9
|
return pricing;
|
|
18
10
|
}
|
|
19
|
-
/**
|
|
20
|
-
* 缝合审 M3:`capabilities.pricingConfigured` 的判据 —— 提成函数放在 pricing 自己的文件里,因为
|
|
21
|
-
* **这个表是热变的**(`boot/config-center.ts` 的 `mutateInPlace(pricing, buildPricing(config.models))`,
|
|
22
|
-
* 那行注释自己写着 `hot: cost/model changes`)。旧码在 `main.ts` 的 capabilities 字面量里把它算成一个
|
|
23
|
-
* **boot 时的布尔快照**,于是运维在 config center 里加上/撤掉价目表之后,`/v1/capabilities` 还报旧值 ——
|
|
24
|
-
* 而这个键的**全部用途**就是给消费端消歧「costMicroUsd=0 是免费跑,还是根本没配价目表」,报错了就是
|
|
25
|
-
* 把消歧信号变成误导信号。main.ts 侧改用 getter(读时求值)。
|
|
26
|
-
*
|
|
27
|
-
* 同一条论证在同一个字面量里**已经执行过一次**:紧邻的 `restartState: () => configCenter.restartState()`
|
|
28
|
-
* 带着注释「A live getter (not a snapshot) so /health always reads current」—— 兄弟键漏了。本仓第 7 例同形。
|
|
29
|
-
*/
|
|
30
11
|
export function isPricingConfigured(pricing) {
|
|
31
12
|
return Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0));
|
|
32
13
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Bound the cardinality of a free-form metric label value (BL-11/BL-21). A label fed by untrusted/unbounded
|
|
35
|
-
* input — MCP tool names (a misconfigured/malicious server can emit thousands of dynamic names) or center
|
|
36
|
-
* model ids (a malformed roster) — would otherwise mint an unbounded number of metric series: a slow memory
|
|
37
|
-
* leak in the registry + a /metrics scrape blow-up. We track the DISTINCT values seen; once the cap is hit,
|
|
38
|
-
* further NEW values collapse into one `__other__` bucket (already-seen values still report exactly). The
|
|
39
|
-
* cap is made observable (no silent truncation) via `onOverflow`, fired once when the cap is first crossed.
|
|
40
|
-
*/
|
|
41
14
|
export function cardinalityGuard(cap, onOverflow) {
|
|
42
15
|
const seen = new Set();
|
|
43
16
|
let overflowed = false;
|
|
@@ -55,27 +28,11 @@ export function cardinalityGuard(cap, onOverflow) {
|
|
|
55
28
|
return value;
|
|
56
29
|
};
|
|
57
30
|
}
|
|
58
|
-
/**
|
|
59
|
-
* E8 (shell-host contract): per-task × per-model usage accumulator, fed (synchronously) by the `brain.call`
|
|
60
|
-
* tracer. The `TracerHook` contract is SYNC / no-IO, so we cannot persist from the hook; instead we accumulate
|
|
61
|
-
* here in memory and the run lifecycle DRAINS us at each turn boundary, appending the delta to the durable run
|
|
62
|
-
* event log. The **append-only delta** model is what makes it resume-safe: a task that suspends and resumes in a
|
|
63
|
-
* different process appends only its own post-resume deltas; the result echo SUMS all `model_usage` events across
|
|
64
|
-
* the whole (possibly multi-process) run. Memory is bounded — a task's entry is dropped on {@link drain}/{@link clear}.
|
|
65
|
-
*/
|
|
66
31
|
export class ModelUsageTracker {
|
|
67
32
|
byTask = new Map();
|
|
68
33
|
active = new Set();
|
|
69
|
-
/** Mark a TOP-LEVEL run active so its `brain.call` usage is tracked. **Required** — `record` ignores any taskId
|
|
70
|
-
* that isn't registered. This is the leak fence: delegated SUB-TASKS (team members,
|
|
71
|
-
* adversarial verifiers) run on throwaway core-minted sessionIds that the run lifecycle never registers/drains,
|
|
72
|
-
* so without this gate their usage would accumulate under ids nothing ever clears → an unbounded process-lifetime
|
|
73
|
-
* leak. Sub-task spend is intentionally NOT in the per-run `modelUsage` echo (it lands in `TaskStats.nested`);
|
|
74
|
-
* `modelUsage` is the TOP-LEVEL run's own per-model usage. Call at run start; {@link clear} unregisters. */
|
|
75
34
|
register(taskId) { if (taskId)
|
|
76
35
|
this.active.add(taskId); }
|
|
77
|
-
/** Accumulate one `brain.call`'s usage — ONLY for a registered top-level run (else a no-op; see {@link register}).
|
|
78
|
-
* Sync, never throws (TracerHook contract). */
|
|
79
36
|
record(taskId, model, d) {
|
|
80
37
|
if (!taskId || !model || !this.active.has(taskId))
|
|
81
38
|
return;
|
|
@@ -90,22 +47,16 @@ export class ModelUsageTracker {
|
|
|
90
47
|
cur.outputTokens += d.outputTokens;
|
|
91
48
|
cur.cacheReadTokens += d.cacheReadTokens;
|
|
92
49
|
cur.cacheWriteTokens += d.cacheWriteTokens;
|
|
93
|
-
// 🔴 RB-368 未知传染:已知 + 未知 = **未知**(和里含一个不知道的项,这个和就不知道)⇒ 删键,不折 0。
|
|
94
|
-
// 顺序无关(先未知后已知同样保持未知),因为 `cur.costMicroUsd` 已是 undefined 时 `+` 也不成立。
|
|
95
50
|
if (d.costMicroUsd === undefined || cur.costMicroUsd === undefined)
|
|
96
51
|
delete cur.costMicroUsd;
|
|
97
52
|
else
|
|
98
53
|
cur.costMicroUsd += d.costMicroUsd;
|
|
99
54
|
}
|
|
100
55
|
else {
|
|
101
|
-
// 显式重建而非 `{...d}`:调用方可能传 `{costMicroUsd: undefined}`(显式 undefined 键),
|
|
102
|
-
// 那样 `Object.hasOwn` 为真、JSON.stringify 又把它抹掉 ⇒ 「缺席」在内存与落库两处形状不一致。
|
|
103
56
|
const { costMicroUsd, ...tokens } = d;
|
|
104
57
|
perModel.set(model, { ...tokens, ...(costMicroUsd !== undefined ? { costMicroUsd } : {}) });
|
|
105
58
|
}
|
|
106
59
|
}
|
|
107
|
-
/** Return the usage accumulated for this task SINCE the last drain (per neutral model id), and RESET it.
|
|
108
|
-
* `undefined` when nothing accrued — callers skip the durable append then. */
|
|
109
60
|
drain(taskId) {
|
|
110
61
|
const perModel = this.byTask.get(taskId);
|
|
111
62
|
if (!perModel || perModel.size === 0)
|
|
@@ -116,251 +67,70 @@ export class ModelUsageTracker {
|
|
|
116
67
|
this.byTask.delete(taskId);
|
|
117
68
|
return out;
|
|
118
69
|
}
|
|
119
|
-
/** Drop a task's pending accumulation AND unregister it (cleanup at run end / on an abandoned/errored leg). After
|
|
120
|
-
* this, further `record(taskId,…)` no-ops until a fresh {@link register}. `drain` deliberately does NOT unregister
|
|
121
|
-
* (the run continues across turns); only `clear` ends the run's tracking. */
|
|
122
70
|
clear(taskId) { this.byTask.delete(taskId); this.active.delete(taskId); }
|
|
123
71
|
}
|
|
124
|
-
// design/158 N18: PromptManifestRecord / promptManifestRecordOf / configAssembledRecordOf / PromptManifestTracker
|
|
125
|
-
// moved VERBATIM to ./observability/prompt-manifest.js (budget.ts's name says cost/budget; this face is
|
|
126
|
-
// wire-observability projection of prompt.assembled/config.assembled, unrelated). Re-exported here so existing
|
|
127
|
-
// importers (main.ts, runs.ts, http/server.ts, trace/project.ts, tests) keep working untouched — createTracer
|
|
128
|
-
// below still consumes promptManifestRecordOf/configAssembledRecordOf/PromptManifestTracker.
|
|
129
72
|
export { promptManifestRecordOf, configAssembledRecordOf, PromptManifestTracker, } from "./observability/prompt-manifest.js";
|
|
130
|
-
/**
|
|
131
|
-
* ④ Bridge core's fire-and-forget trace events to /metrics (+ the E8 per-task model-usage accumulator). The hook
|
|
132
|
-
* is void / never throws / does no sync IO (in-memory counters only), per the TracerHook contract. `brain.call`
|
|
133
|
-
* fires for EVERY call (top-level + async + council sub-tasks) → `model_cost_micro_usd_total` is the authoritative
|
|
134
|
-
* spend, and (when a {@link ModelUsageTracker} is supplied) the per-task `modelUsage` echo's source.
|
|
135
|
-
*/
|
|
136
73
|
export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLease, quotaWeightFor, promptManifests) {
|
|
137
|
-
// Per-process cardinality guards for the two unbounded-input labels (BL-11/BL-21). 256 is generous for any
|
|
138
|
-
// real tool/model catalog; overflow bumps a counter so an over-cap deploy is observable, not silent.
|
|
139
74
|
const toolName = cardinalityGuard(256, () => metrics.inc("metric_label_overflow_total", { label: "tool_name" }));
|
|
140
75
|
const modelId = cardinalityGuard(256, () => metrics.inc("metric_label_overflow_total", { label: "model" }));
|
|
141
|
-
// provider flows from env/registry config (not a bounded enum) — guard it like modelId (1.127.0):
|
|
142
|
-
// a misconfigured registry entry must not explode Prometheus label cardinality. 64 is generous for real providers.
|
|
143
76
|
const providerName = cardinalityGuard(64, () => metrics.inc("metric_label_overflow_total", { label: "provider" }));
|
|
144
77
|
return (e) => {
|
|
145
78
|
if (e.kind === "brain.call") {
|
|
146
|
-
// S18 (SILENT-FALLBACK P1): count every call by provider+model. provider is optional on the trace event —
|
|
147
|
-
// "unknown" = the routing layer fell through without a recognized provider (previously unobservable).
|
|
148
79
|
metrics.inc("brain_calls_total", { provider: providerName(e.provider ?? "unknown"), model: modelId(e.model) });
|
|
149
80
|
if (e.costMicroUsd) {
|
|
150
81
|
metrics.inc("model_cost_micro_usd_total", { model: modelId(e.model) }, e.costMicroUsd);
|
|
151
|
-
// ⑤b attribute spend to the originating principal (ALS) — reaches council/team sub-tasks too.
|
|
152
82
|
const principal = currentPrincipal();
|
|
153
83
|
if (principal)
|
|
154
84
|
costQuota?.add(principal, e.costMicroUsd);
|
|
155
85
|
}
|
|
156
86
|
else if (e.costMicroUsd === undefined) {
|
|
157
|
-
// D5:cost 整键缺席 = 未知(RB-368)——计数供 /metrics/summary 的 costUnknown/unpricedCalls
|
|
158
|
-
// 信号(costUsd 在 unpriced 部署恒 0,没有这个信号消费端会读成「零花费」)。显式 0 =
|
|
159
|
-
// declared free,走上面 truthy 为假的静默臂,不计入 unpriced。
|
|
160
87
|
metrics.inc("brain_calls_unpriced_total", { model: modelId(e.model) });
|
|
161
88
|
}
|
|
162
|
-
|
|
163
|
-
// 病灶报案:下面三处曾是 `e.costMicroUsd ?? 0`(账本 + fleetUsage + fleetLease),把「成本未知」记成
|
|
164
|
-
// 「免费」,与 `pricingConfigured=false` 的部署组合 ⇒ 全部账目假 0。
|
|
165
|
-
//
|
|
166
|
-
// **第一轮裁定(core ≤2.2.0,已被上游修复取代,留档见证判断链)**:亲读上游后纠偏——core 当时在
|
|
167
|
-
// brain.call 帧上**恒发数字、从不缺席**(`costMicroUsd: turnCostMicroUsd` 是无条件键;无价目表时
|
|
168
|
-
// `modelCostToPricing(undefined)` 产出全零单价 ⇒ 如实算出 0)。歧义**产生在 core**,server 收到的
|
|
169
|
-
// 信息里根本不含这个区分 ⇒ 当时改透传治不了根(缺席永不到达),`?? 0` 定性为**类型面守卫**。
|
|
170
|
-
// 结论是把根因工单挂回 core,而不是在本层做样子。
|
|
171
|
-
//
|
|
172
|
-
// 🔴 **第二轮 = 现行(core ≥2.3.0,RB-368 到货)**:core 已在源头消歧——unpriced 部署下
|
|
173
|
-
// brain.call(`runtask.js:831`)/ task.end(`:2851`)/ `stats.costMicroUsd`+`costBreakdown`
|
|
174
|
-
// (`assemble-result.js:39,144`)**整键缺席**;显式全零 `cost` = 声明免费 ⇒ 照发 0。于是三处
|
|
175
|
-
// `?? 0` 全部改为**条件展开透传**:键在 ⇒ 原样传(含真 0);键不在 ⇒ 三路都不带该键。
|
|
176
|
-
// 事实钉在 test/budget.test.ts(真 Runner 跑一趟取证),floor 已钉 ^2.3.0——装回旧 core 这三条
|
|
177
|
-
// 透传臂会变成永不到达的死枝而全量照绿,那正是 floor 门存在的理由。
|
|
178
|
-
//
|
|
179
|
-
// 三问(第二轮):
|
|
180
|
-
// · 谁需要 —— 消费端要区分真零/未知(cli 在 CC 对照里正是靠「诚实缺席不编造」赢的那一点;
|
|
181
|
-
// cli #68 的两信号分渲 = 部署级 pricingConfigured + 行级缺席,行级这一半由本改动供给)。
|
|
182
|
-
// · 谁受伤 —— (a) 聚合面遇缺席:usage-analytics 与 aggregateModelUsage 此前把 DB JSON 当全必填做裸
|
|
183
|
-
// 算术,一行缺键就 NaN/字符串毒掉整窗(#59 同批已修,红先复现在
|
|
184
|
-
// test/usage-analytics.test.ts + test/model-usage.test.ts);(b) 历史行:已按 0 记账的行无法回溯
|
|
185
|
-
// 区分,只能靠部署位读回——**本改动不回溯**,分界线 = 升到 core 2.3.0 那一刻。
|
|
186
|
-
// · 补偿 —— 第一级 = `capabilities.pricingConfigured`(main.ts,已在):false ⇒ 消费端把任何 0
|
|
187
|
-
// 渲染成「未知」而非「免费」;第二级 = 行级缺席(本改动),沿账本→durable `model_usage`→
|
|
188
|
-
// `aggregateModelUsage`→`TaskStats.modelUsage` 回声→usage-analytics 的 `estimated` 标记,一路
|
|
189
|
-
// 保持缺席(聚合面的**未知传染**语义见 trace/project.ts)。
|
|
190
|
-
// · fleet 批报腿(下面 fleetUsage)自 registry-core 0.11.0/[2103] 起**同样接住了**:wire 有
|
|
191
|
-
// 缺席位(UsageEntry.costMicroUsd optional),accumulator cell 级传染透传缺席,center 聚合面
|
|
192
|
-
// 「下界 + costUnknown」——两层语义与本处的关系见 fleet-client.ts 的旁注(3.7.0 前这里曾写
|
|
193
|
-
// 「wire 表达不了缺席、center 只见低估」,已被该车推翻;doc-rot 修 2026-07-30)。
|
|
194
|
-
// └──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
195
|
-
// [2153]③ 侦察钉(core 2.6.0):`brain.call` 帧新增可选 `turn?: number`(trace.d.ts 确认;此前只有
|
|
196
|
-
// `turn.end`/`repetition.detected`/`task.degraded` 三帧带 turn)。**刻意不接**——`modelUsage.record`
|
|
197
|
-
// 喂的是 E8 durable `model_usage` echo,累加键=`taskId → model`(见 `ModelUsageTracker.byTask`,
|
|
198
|
-
// 上方 class 定义),drain 边界=**per-turn**(run lifecycle 在每个 turn 边界 drain 一次、append 一条
|
|
199
|
-
// durable 事件行)。也就是说 turn 粒度已经由「何时 drain」这个外层动作表达,一次 drain 窗口内本就
|
|
200
|
-
// 只可能有一个 turn 的 delta——帧上逐行再带一份 `turn` 是重复信息,对已经 per-turn 聚合的行没有
|
|
201
|
-
// 增量语义(不是「丢了信息」,是「信息已经在结构里」)。将来若变盘:若聚合粒度改粗(比如攒够 N
|
|
202
|
-
// turn 才 drain 一次,单行 delta 跨多个 turn),或 usage-analytics 想要「同一 drain 窗口内哪个 turn
|
|
203
|
-
// 花了多少」的细分,才值得把 `e.turn` 塞进 `ModelUsageDelta`(那时 byTask 需要再加一层 turn 键)。
|
|
204
|
-
// E8 (shell-host): per-task × per-model usage for the `TaskStats.modelUsage` echo. `record` is a NO-OP unless
|
|
205
|
-
// e.taskId is a REGISTERED top-level run (the leak fence — sub-task/throwaway taskIds are never registered, so
|
|
206
|
-
// they neither leak nor enter the echo; their spend is in TaskStats.nested). Drained at turn boundaries into the
|
|
207
|
-
// durable log (resume-safe); the registered entry is dropped at run end (clear). NOT cardinality-guarded by
|
|
208
|
-
// model: bounded by a run's model set, and by the active-run set.
|
|
209
|
-
// core 3.0.0([2296] S1):this lane's `inputTokens` stays `e.promptTokens` — DELIBERATELY zero-changed.
|
|
210
|
-
// The E8 ledger's semantics are COMPONENT-wise (MISS + cacheRead/cacheWrite listed separately;
|
|
211
|
-
// usage-analytics' read side SUMS them back into a total), unlike the weightedTokens/fleetUsage lane just
|
|
212
|
-
// below (which wants the pre-summed normalized total and migrates to `e.totalInputTokens`). Pre-3.0.0 the
|
|
213
|
-
// gateway family's `promptTokens` already included cache, so this same read-side sum double-counted for
|
|
214
|
-
// that family — an existing accounting gap in historical rows that this version does not retroactively fix
|
|
215
|
-
// (declared, not silently carried forward): see CHANGELOG's "存量口径断层" note.
|
|
216
|
-
modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
|
|
217
|
-
// weight-at-burn:加权 tokens = (prompt+completion)×模型配额倍率,在记账点固化(此后改倍率
|
|
218
|
-
// 不回算历史,registry ModelEntry.quotaWeight 同句契约)。weight 源=config.modelQuotaWeights(registry
|
|
219
|
-
// hot-apply;env lane 空表 ⇒ 1 兜底);非法回调值防御性归 1(tracer 契约 never-throws,不让坏表毒记账)。
|
|
220
|
-
// core 3.0.0([2296] S1):`e.promptTokens` → `e.totalInputTokens`. This axis's contract (line ~365 below)
|
|
221
|
-
// has always been "normalized total input INCLUDING cache" — pre-3.0.0 that's what `promptTokens` meant for
|
|
222
|
-
// the gateway family; 3.0.0 flips `promptTokens` to mean cache-MISS-only, so staying on it here would
|
|
223
|
-
// massively under-count the quota/lease axis for any cache-heavy load (a lease could be walked through by
|
|
224
|
-
// cache-heavy traffic — the exact same failure class as the side-query R4 finding below).
|
|
89
|
+
modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) });
|
|
225
90
|
const qwRaw = quotaWeightFor?.(e.model);
|
|
226
91
|
const qw = typeof qwRaw === "number" && Number.isFinite(qwRaw) && qwRaw > 0 ? qwRaw : 1;
|
|
227
92
|
const weightedTokens = Math.round((e.totalInputTokens + e.completionTokens) * qw);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// core 3.0.0:同上迁移理由,inputTokens=归一化总输入含 cache(totalInputTokens),与 weightedTokens 同轴。
|
|
231
|
-
fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.totalInputTokens, outputTokens: e.completionTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}), weightedTokens, quotaWeightAtUse: qw }); // RB-368 透传缺席
|
|
232
|
-
// lease 消费(D4 AP):同一 ALS 归因点做 lease 本地扣减 —— 只对当前持 lease 的 principal
|
|
233
|
-
// 生效(FleetLeaseManager 内部判),sync + never throws。双轴=weightedTokens(配额轴)+costMicroUsd($ 轴)。
|
|
234
|
-
fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
|
|
93
|
+
fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.totalInputTokens, outputTokens: e.completionTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}), weightedTokens, quotaWeightAtUse: qw });
|
|
94
|
+
fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) });
|
|
235
95
|
if (e.firstTokenMs !== undefined)
|
|
236
96
|
metrics.observe("brain_first_token_ms", e.firstTokenMs);
|
|
237
97
|
metrics.observe("brain_call_latency_ms", e.latencyMs);
|
|
238
98
|
}
|
|
239
99
|
else if (e.kind === "tool.call") {
|
|
240
|
-
// [2153]③ 侦察钉(core 2.6.0):`tool.call` 帧新增可选 `toolCallId?: string` / `turn?: number`(trace.d.ts
|
|
241
|
-
// 确认)。**刻意不接进 `tool_calls_total` 的标签**:`toolCallId` 逐调用唯一 → 直接当标签会让这个
|
|
242
|
-
// 计数器的基数=调用总数(等于没聚合),是本文件 `cardinalityGuard` 存在的理由要防的那类爆炸,
|
|
243
|
-
// 连 guard 都救不了(不是「用户输入无界」那类可折叠 __other__ 的情形,是键本身设计上就唯一)。
|
|
244
|
-
// `turn` 本身是低基数(有界于单任务的 turn 数),但作为**跨任务**的 Prometheus 标签会随长任务/
|
|
245
|
-
// 多任务叠加成事实上无界的序列(同 model/tool_name 已用 guard 的理由,turn 目前没有等价上限)——
|
|
246
|
-
// 且当前没有「按 turn 分桶看 tool 调用量」的仪表盘/告警消费者在等这个切面,先不加标签。这两个键
|
|
247
|
-
// 已经在别的通道有名分:`toolCallId` 走 wire 的 `tool_start`/`tool_end` TaskEvent(trace/project.ts
|
|
248
|
-
// toolStartEventData/toolEndEventData,逐调用可关联、不进 metrics 基数);`turn` 走 `turn.end`/
|
|
249
|
-
// `task.end.turns`。本臂(createTracer→/metrics)只做计数,不是这两键的合适去处。将来若要接:
|
|
250
|
-
// (a) 出现「per-turn tool 调用密度」的真实消费者(如异常 turn 侦测),`turn` 分桶价值才成立——
|
|
251
|
-
// 用有界桶(如 `turn <= 20 ? String(turn) : "20+"`)而非原始整数,避免上面同款无界问题;
|
|
252
|
-
// (b) `toolCallId` 除非改造成非标签的关联字段(如结构化日志行,不是 Prometheus label),否则永远
|
|
253
|
-
// 不该进这个计数器。
|
|
254
100
|
metrics.inc("tool_calls_total", { name: toolName(e.name), ok: String(e.ok) });
|
|
255
101
|
}
|
|
256
102
|
else if (e.kind === "task.degraded") {
|
|
257
|
-
// 1.40: a task was switched to a cheaper model (near-budget / rate_limit / breaker-open). Quality
|
|
258
|
-
// dropped → alert-worthy operational signal, by reason.
|
|
259
103
|
metrics.inc("degraded_total", { reason: e.reason });
|
|
260
104
|
}
|
|
261
105
|
else if (e.kind === "prompt.assembled") {
|
|
262
|
-
// core 1.243(提示词主权批): who owned the constitution layer, per task. The
|
|
263
|
-
// steady state for EVERY service scenario is "core" (structural constitution — our providers all
|
|
264
|
-
// return role bases only). Anything else is worth eyes: "provider-assembled" = a pre-1.243-shaped
|
|
265
|
-
// provider tripped the migration guard (upgrade it); "replaced" = an explicit replaceAll opt-out
|
|
266
|
-
// (none exist in this repo today); "legacy" = a deprecated free-form `system` provider. Alert on
|
|
267
|
-
// `prompt_constitution_total{mode!="core"} > 0` — the "TOC lost its safety blocks and called it a
|
|
268
|
-
// config issue" failure class becomes a one-glance dashboard check.
|
|
269
106
|
metrics.inc("prompt_constitution_total", { mode: e.constitution });
|
|
270
|
-
// [998]② durable half: record the whitelisted manifest for the run lifecycle to drain into a
|
|
271
|
-
// `prompt_assembled` event (turns/stream). register-gated (top-level runs only), sync, never persists here.
|
|
272
107
|
promptManifests?.record(e.taskId, promptManifestRecordOf(e));
|
|
273
108
|
}
|
|
274
109
|
else if (e.kind === "config.assembled") {
|
|
275
|
-
// [1301]③:config catalog 观测半场——生效配置逐字段来源(default/spec/derived/host-declared+
|
|
276
|
-
// overrideReasons)随任务进 durable 事件日志(config_assembled),「谁设的 80」读帧不考古。
|
|
277
110
|
const rec = configAssembledRecordOf(e);
|
|
278
111
|
if (rec)
|
|
279
112
|
promptManifests?.recordConfig(e.taskId, rec);
|
|
280
113
|
}
|
|
281
114
|
else if (e.kind === "compaction.phase_timings") {
|
|
282
|
-
// codex F6: core 1.289's `compaction.phase_timings` fires on SUCCESS (per-phase timing telemetry) — it is
|
|
283
|
-
// NOT an outcome, so counting it into compaction_events_total would add a fake "phase_timings" failure
|
|
284
|
-
// outcome for every successful compaction (poisoning the "non-success outcomes" dashboard semantics
|
|
285
|
-
// documented below). Record the total duration on the existing observe face instead (the
|
|
286
|
-
// brain_call_latency_ms precedent); non-finite values are dropped (tracer never throws, never poisons).
|
|
287
|
-
// 三路复审修5 核查(workflow W5 报「histogram never」):duration 观测出口就在下一行(F6 起已在),
|
|
288
|
-
// event 契约(core trace.d.ts)带 `durationMs: number`,budget.test.ts 咬合断言 observe 只进
|
|
289
|
-
// compaction_duration_ms 不进失败计数器 —— W5 判 STALE,零改。per-phase 明细走 compacted 事件的
|
|
290
|
-
// phaseDurations(trace/project.ts compactedEventData)上 wire,不在 /metrics 重复开面(有界纪律)。
|
|
291
115
|
const d = e.durationMs;
|
|
292
116
|
if (typeof d === "number" && Number.isFinite(d))
|
|
293
117
|
metrics.observe("compaction_duration_ms", d);
|
|
294
118
|
}
|
|
295
119
|
else if (e.kind.startsWith("compaction.")) {
|
|
296
|
-
// core 1.257(压缩健康面): every NON-SUCCESS compaction outcome
|
|
297
|
-
// now emits a `compaction.*` trace frame (failed / mooted / blocked / noop / disabled / suppressed +
|
|
298
|
-
// the input_truncated disclosure and announce_dropped). Before these frames the only evidence of a
|
|
299
|
-
// failing compactor was `onError(phase:"compaction")` log lines — no aggregate, so "3 silent auto
|
|
300
|
-
// failures opened the breaker and manual verbs drain mooted" (the exact failure fingerprint)
|
|
301
|
-
// was invisible on a dashboard. Count by outcome+trigger: `compaction_events_total{outcome="failed"}`
|
|
302
|
-
// rising = the summarizer is failing; sustained `mooted`/`blocked` explains "202 → no compacted
|
|
303
|
-
// event" without log spelunking. Labels are bounded: 8 kinds × 3 triggers (frames without a trigger
|
|
304
|
-
// — noop/disabled/mooted/input_truncated/announce_dropped — label "none"). SUCCESS is deliberately
|
|
305
|
-
// not counted here: `compacted` is a TaskEvent riding the run stream, not a trace frame.
|
|
306
120
|
metrics.inc("compaction_events_total", { outcome: e.kind.slice("compaction.".length), trigger: e.trigger ?? "none" });
|
|
307
121
|
}
|
|
308
122
|
else if (e.kind === "permission.persisted_rule_allowed" || e.kind === "permission.rule_store_unreadable") {
|
|
309
|
-
// 🔴 [3372] core 5.23.0 提货批,codex 复审 round2 [medium](验真后修)。首版把这两帧登记成
|
|
310
|
-
// 「丢弃,已有等价信号」——**那条理由是假的**(亲验:`plugins/permission-rule-store-sql.ts` 里既没有
|
|
311
|
-
// `noteStoreError` 也没有 `onCorruptRead`),而它们又是**真可达**的(#154 车二把规则店接上了)。
|
|
312
|
-
// 一条假理由比没有理由更坏:台账的全部价值就在于它写的话是真的。
|
|
313
|
-
//
|
|
314
|
-
// 两帧各自答一个**别处答不出来**的问题,而且都在安全轴上:
|
|
315
|
-
// · `persisted_rule_allowed` —— design/179 明写它是 allow 侧的**归因通道**:「这次为什么没问我?」
|
|
316
|
-
// `permission_denied_total` 的补集答不了(它连「有没有走规则」都不知道),`tool_trace` 是默认关
|
|
317
|
-
// 的诊断旋钮、也不带规则归因。持久规则店是本装配里**唯一的放宽缝**,它每放行一次都该有账。
|
|
318
|
-
// · `rule_store_unreadable` —— 规则店读不出 ⇒ 该次调用按**零规则**裁决(诚实地问,而不是假装空店)。
|
|
319
|
-
// 后果是一场**审批风暴**:运维看到的是「卡突然变多」,而病因在店上。没有这个计数就只能靠猜。
|
|
320
|
-
// 标签有界:`event` 二取一(闭集,由上面的 `===` 判别式本身封闭)。规则原文/店的错误句**不进标签**
|
|
321
|
-
// (前者是用户数据、后者无界)——它们该去结构化日志,不是 Prometheus 基数。
|
|
322
123
|
metrics.inc("permission_rule_events_total", { event: e.kind.slice("permission.".length) });
|
|
323
124
|
}
|
|
324
|
-
// ── 未命中 = 该帧不进 /metrics ────────────────────────────────────────────────────────────────
|
|
325
|
-
// 这条 if/else 链没有 else:没命中的 `TraceEvent` 就地丢弃。对多数 kind 这是对的(计量面不该给每个
|
|
326
|
-
// 诊断帧开计数器),但「丢」必须是**裁定**而不是漏接。[3372](core 5.23.0 一次给安全轴加了三个
|
|
327
|
-
// `permission.*` 帧)之后,该族的逐条处置记在 `trace/core-keyset-guard.ts` 的 ⑦ 面并由 tsc 守着:
|
|
328
|
-
// core 再加一个 `permission.*` 帧,那里编译期先红,逼人裁「接还是不接」。其余族(compaction/brain/
|
|
329
|
-
// task/…)仍按本链的显式分支处置,无门。
|
|
330
125
|
};
|
|
331
126
|
}
|
|
332
|
-
|
|
333
|
-
* [1469] side-query 记账 seam(codex R3 high:sideQuery 不走 core tracer——brain.stream 直调不发
|
|
334
|
-
* brain.call——createTracer 的四路 sink(costQuota/model_cost 指标/fleetUsage 批报/fleetLease 本地扣减)
|
|
335
|
-
* 全部旁路)。本 seam 与 createTracer 的 brain.call 臂**同语义**喂同一组 sink:weightedTokens 同式
|
|
336
|
-
* (tokens×quotaWeight,weight-at-burn 固化)、micro-USD 同轴。装配点与 createTracer 同一处(main.ts),
|
|
337
|
-
* 传同一组实例——两臂永不分家。
|
|
338
|
-
*
|
|
339
|
-
* 已知残差(codex R3 medium,记档):cost 源=core SideQueryResult.usage.cost.total(brain 层计价),
|
|
340
|
-
* OpenAI cache-heavy 输入按全价入账(tracer 的 e.costMicroUsd 是 cache-family-aware 价)——方向=多收
|
|
341
|
-
* (保守,绝不少收);根修=core 在 SideQueryResult 上回权威 costMicroUsd(已提货 core)。model 标签/
|
|
342
|
-
* weight 键用**路由身份 r.model**(池配置有界)非 servedModel(provider 上报,无界)。
|
|
343
|
-
*/
|
|
344
|
-
export function createSideQueryAccountant(metrics, costQuota,
|
|
345
|
-
// 依赖面收窄到真消费的方法(§X 同 ModelKeyRef 一式):全形对象照传(结构超集),测试桩零铸型。
|
|
346
|
-
fleetUsage, fleetLease, quotaWeightFor) {
|
|
127
|
+
export function createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, quotaWeightFor) {
|
|
347
128
|
return (principal, r) => {
|
|
348
129
|
const input = typeof r.usage?.input === "number" ? r.usage.input : 0;
|
|
349
130
|
const output = typeof r.usage?.output === "number" ? r.usage.output : 0;
|
|
350
131
|
const cacheRead = typeof r.usage?.cacheRead === "number" ? r.usage.cacheRead : 0;
|
|
351
132
|
const cacheWrite = typeof r.usage?.cacheWrite === "number" ? r.usage.cacheWrite : 0;
|
|
352
|
-
// #257(codex #256-R3 high 定谳):core 在 side-query 出口已把 `usage.input` **归一为 uncached 量**
|
|
353
|
-
// ——对每个 provider 族(side-query.d.ts:「`input` NORMALIZED to the uncached prompt quantity …
|
|
354
|
-
// for EVERY provider family」,与 run loop 的 usage-accounting 同一道门)。所以配额轴折算恒为
|
|
355
|
-
// input + cacheRead + cacheWrite,**不再按 cache family 分臂**:曾经的
|
|
356
|
-
// `family === "input-includes-cached" ? input : …` 分支建立在「OpenAI 族 input 仍含 cache」这个
|
|
357
|
-
// 已被 core 出口归一推翻的前提上,每次 cache 命中都把 OpenAI 族配额/lease 少收 cacheRead+cacheWrite
|
|
358
|
-
// (红先钉 = budget.test「#257 OpenAI 族 cache 命中不许少计」格)。family 判定链
|
|
359
|
-
// (cacheFamilyOfMirror + catalog 快照)随本修整体退役——LEDGER 的 cacheFamilyOf/CacheFamily
|
|
360
|
-
// 两行同步改 skip(消费点消失,理由=core 出口归一)。
|
|
361
133
|
const promptTokens = input + cacheRead + cacheWrite;
|
|
362
|
-
// RB-368/#59 同判据(createTracer 上方注释同款纪律):`usage.cost.total` 整键缺席 = 未知(unpriced 部署的
|
|
363
|
-
// side query 真形),不是「花了 $0」——折 0 就是把「不知道」编造成「免费」。已知(哪怕显式 0)才带键。
|
|
364
134
|
const costTotal = typeof r.usage?.cost?.total === "number" ? r.usage.cost.total : undefined;
|
|
365
135
|
const micro = costTotal !== undefined ? Math.round(costTotal * 1_000_000) : undefined;
|
|
366
136
|
if (micro !== undefined && micro > 0) {
|
|
@@ -372,20 +142,15 @@ fleetUsage, fleetLease, quotaWeightFor) {
|
|
|
372
142
|
const qw = typeof qwRaw === "number" && Number.isFinite(qwRaw) && qwRaw > 0 ? qwRaw : 1;
|
|
373
143
|
const weightedTokens = Math.round((promptTokens + output) * qw);
|
|
374
144
|
if (weightedTokens > 0 || (micro !== undefined && micro > 0)) {
|
|
375
|
-
fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, ...(micro !== undefined ? { costMicroUsd: micro } : {}), weightedTokens, quotaWeightAtUse: qw });
|
|
376
|
-
fleetLease?.recordSpend(principal, { weightedTokens, ...(micro !== undefined ? { costMicroUsd: micro } : {}) });
|
|
145
|
+
fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, ...(micro !== undefined ? { costMicroUsd: micro } : {}), weightedTokens, quotaWeightAtUse: qw });
|
|
146
|
+
fleetLease?.recordSpend(principal, { weightedTokens, ...(micro !== undefined ? { costMicroUsd: micro } : {}) });
|
|
377
147
|
}
|
|
378
148
|
};
|
|
379
149
|
}
|
|
380
|
-
/**
|
|
381
|
-
* ⑤ Resolve a per-task budget: honor the caller's requested value but never let it EXCEED the operator
|
|
382
|
-
* ceiling. `0`/unset ceiling = no limit (returns the caller's value, or undefined). A non-positive /
|
|
383
|
-
* non-numeric request is ignored (falls back to the ceiling). Returns undefined = "no budget gate".
|
|
384
|
-
*/
|
|
385
150
|
export function cappedCeiling(requested, ceiling) {
|
|
386
151
|
const req = typeof requested === "number" && requested > 0 ? requested : undefined;
|
|
387
152
|
if (!ceiling || ceiling <= 0)
|
|
388
|
-
return req;
|
|
389
|
-
return req === undefined ? ceiling : Math.min(req, ceiling);
|
|
153
|
+
return req;
|
|
154
|
+
return req === undefined ? ceiling : Math.min(req, ceiling);
|
|
390
155
|
}
|
|
391
156
|
//# sourceMappingURL=budget.js.map
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
/** Current time — the model has no clock; OA/most tasks need it. No side effects. CC-parity tool name `Now`
|
|
3
|
-
* (PascalCase, core 1.176 first-party rename wrap-up — the SERVICE owns this one; `nowTool` is not a core tool).
|
|
4
|
-
* effect:"read" so it never suspends → no durable-resume rename strand (no pending-tool checkpoint to canonicalize). */
|
|
5
2
|
export function nowTool() {
|
|
6
3
|
return {
|
|
7
4
|
name: "Now",
|
|
@@ -1,41 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* plugins 域 worker 消费半场(契约冻结 board [1361]②/[1362] 四收紧/[1366]①;2026-07-28 开工车)。
|
|
3
|
-
*
|
|
4
|
-
* center 只存"插件从哪来"(PluginSpec 引用,永不承载可执行字节);worker 半场 = 把引用物化成本地
|
|
5
|
-
* 目录并把其中的 **skills** 装进 baseline(v1 装载面;commands 由 cli 壳消费——board [1363] 即 cli
|
|
6
|
-
* 答的命令契约稿,worker 侧本无命令面,这是分工不是降级)。挂 applyCenterSkills 同 reconcile 周期
|
|
7
|
-
* (boot 期,buildScenarios 之前)。
|
|
8
|
-
*
|
|
9
|
-
* 契约条款落点(与 test/center-plugins.test.ts 逐条对应):
|
|
10
|
-
* - https-only + `PLUGINS_ALLOW_HOSTS`(缺省 github.com):{@link validatePluginDecl}。registry-core
|
|
11
|
-
* 0.10.23 已在 schema 层单真源拒非 https,worker 侧**仍复验**——纵深:wire 可能来自旧 center。
|
|
12
|
-
* - sha 全 40hex + checkout 强制:{@link gitClonePlan}(有 sha = init+fetch-by-sha+checkout,不信
|
|
13
|
-
* 分支名也不拉全史;无 sha = `--depth 1` HEAD)。
|
|
14
|
-
* - clone 姿势隔离:GIT_CONFIG_GLOBAL/SYSTEM=/dev/null + GIT_TERMINAL_PROMPT=0(无凭据仓 fail 而非
|
|
15
|
-
* 挂交互 prompt)。私仓凭据走各机自己的 credential helper,永不入声明。
|
|
16
|
-
* - manifest 路径逃逸(../ /绝对路径/symlink 出根)= 整条拒装:{@link resolvePluginSkillsRoot}
|
|
17
|
-
* (realpath 后前缀判定)。
|
|
18
|
-
* - 撞名让位(收紧④):plugin 的 skill 与 baseline(builtin/center 直发)同名 ⇒ **plugin 让位**
|
|
19
|
-
* (跳过+letGo 留痕)。⚠️ 方向与 applyCenterSkills 的 center-wins 相反——center 直发 skills 过了
|
|
20
|
-
* publisher RBAC+发布审批,plugin 内容只是 git 仓字节,信任级不同,让位方向因此相反。
|
|
21
|
-
* - fail-loud 单条不连坐 + LKG(上次成功物化留盘,拉取失败沿用):{@link applyCenterPlugins}。
|
|
22
|
-
*/
|
|
23
1
|
import { execFile } from "node:child_process";
|
|
24
2
|
import { promisify } from "node:util";
|
|
25
3
|
import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, statSync } from "node:fs";
|
|
26
4
|
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
27
5
|
import { assertConfined, loadSkills } from "./skills.js";
|
|
28
6
|
const execFileP = promisify(execFile);
|
|
29
|
-
/** 缺省 clone host 白名单(契约条款 1;国内镜像走本名单加 https 镜像域,不开 http 口)。 */
|
|
30
7
|
export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
|
|
31
|
-
/** 条款 1/2/4:https-only、host 全等白名单、sha 40hex。纯函数,worker 侧纵深复验(不信任 wire 已验过)。 */
|
|
32
8
|
export function validatePluginDecl(decl, allowHosts) {
|
|
33
9
|
const name = typeof decl.name === "string" && decl.name !== "" ? decl.name : "(unnamed)";
|
|
34
10
|
if (decl.enabled === false)
|
|
35
11
|
return { ok: false, skip: true, name, reason: "enabled:false" };
|
|
36
12
|
const kind = decl.source?.kind;
|
|
37
13
|
if (kind === "marketplace") {
|
|
38
|
-
// v1 暂缓:marketplace 物化(add-by-url + install-by-name)是 v2 面;skip 不连坐,warn 留痕。
|
|
39
14
|
return { ok: false, skip: true, name, reason: "marketplace source: not materialized in v1 (skills lane ships git-form first)" };
|
|
40
15
|
}
|
|
41
16
|
if (kind !== "git")
|
|
@@ -51,7 +26,6 @@ export function validatePluginDecl(decl, allowHosts) {
|
|
|
51
26
|
catch {
|
|
52
27
|
return { ok: false, name, reason: "https url does not parse" };
|
|
53
28
|
}
|
|
54
|
-
// host 全等匹配——evil-github.com / github.com.evil.io 都不隐含放行。
|
|
55
29
|
if (!allowHosts.some((h) => h.toLowerCase() === host)) {
|
|
56
30
|
return { ok: false, name, reason: `host ${host} not in PLUGINS_ALLOW_HOSTS (default github.com; extend with a comma-separated https mirror domain list)` };
|
|
57
31
|
}
|
|
@@ -63,17 +37,15 @@ export function validatePluginDecl(decl, allowHosts) {
|
|
|
63
37
|
}
|
|
64
38
|
return { ok: true, name, url };
|
|
65
39
|
}
|
|
66
|
-
/** 条款 3/5:clone 计划(argv 序列 + 隔离 env)。纯数据,让"姿势"可被钉死。 */
|
|
67
40
|
export function gitClonePlan(o) {
|
|
68
41
|
const env = {
|
|
69
|
-
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
42
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
70
43
|
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
71
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
44
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
72
45
|
};
|
|
73
46
|
if (o.sha === undefined) {
|
|
74
47
|
return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]], env };
|
|
75
48
|
}
|
|
76
|
-
// 有 sha:init + fetch-by-sha + checkout —— 不信分支名、不拉全史;fetch 不到该 sha = 该条失败。
|
|
77
49
|
return {
|
|
78
50
|
steps: [
|
|
79
51
|
["init", "--quiet", o.dest],
|
|
@@ -83,25 +55,17 @@ export function gitClonePlan(o) {
|
|
|
83
55
|
env,
|
|
84
56
|
};
|
|
85
57
|
}
|
|
86
|
-
/** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
|
|
87
|
-
* 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
|
|
88
58
|
export const PLUGIN_MANIFEST_MAX_BYTES = 64 * 1024;
|
|
89
|
-
/** 条款 5 的 manifest 半场:`.claude-plugin/plugin.json` 的 `skillsPath`(缺省 `skills/`;cli plugin
|
|
90
|
-
* 布局同族)。返回 ok:true+dir:undefined = 插件没有 skills 目录(可以只有 commands——那是 cli 壳的
|
|
91
|
-
* 半场,worker 装 0 个 skill 不算失败);ok:false = 路径逃逸/manifest 本体畸形,整条拒装。
|
|
92
|
-
* manifest 臂分立(V2-⑦,原 broad-catch 是 fail-open):**不存在**=可选面,缺省 skills/;**存在但**
|
|
93
|
-
* 本体逃逸根外/非普通文件/超界/读失败/JSON 坏 = ok:false 走既有 warning 面。 */
|
|
94
59
|
export function resolvePluginSkillsRoot(cloneRoot) {
|
|
95
60
|
let skillsPath = "skills";
|
|
96
61
|
const rootReal = realpathSync(cloneRoot);
|
|
97
62
|
const manifestPath = join(cloneRoot, ".claude-plugin", "plugin.json");
|
|
98
63
|
let manifestPresent = false;
|
|
99
64
|
try {
|
|
100
|
-
lstatSync(manifestPath);
|
|
65
|
+
lstatSync(manifestPath);
|
|
101
66
|
manifestPresent = true;
|
|
102
67
|
}
|
|
103
68
|
catch {
|
|
104
|
-
/* 无 manifest ⇒ 缺省 skills/(manifest 是可选面) */
|
|
105
69
|
}
|
|
106
70
|
if (manifestPresent) {
|
|
107
71
|
try {
|
|
@@ -123,21 +87,17 @@ export function resolvePluginSkillsRoot(cloneRoot) {
|
|
|
123
87
|
if (isAbsolute(skillsPath))
|
|
124
88
|
return { ok: false, reason: `skillsPath must be relative (got ${skillsPath})` };
|
|
125
89
|
const candidate = resolve(rootReal, skillsPath);
|
|
126
|
-
// 先做词法前缀判定(../ 逃逸在 realpath 之前就拒——目标可能不存在)
|
|
127
90
|
if (candidate !== rootReal && !candidate.startsWith(rootReal + sep)) {
|
|
128
91
|
return { ok: false, reason: `skillsPath escapes the plugin root (${skillsPath})` };
|
|
129
92
|
}
|
|
130
93
|
if (!existsSync(candidate))
|
|
131
94
|
return { ok: true };
|
|
132
|
-
// symlink 出根:对存在的目标 realpath 再判一次
|
|
133
95
|
const real = realpathSync(candidate);
|
|
134
96
|
if (real !== rootReal && !real.startsWith(rootReal + sep)) {
|
|
135
97
|
return { ok: false, reason: `skillsPath resolves outside the plugin root via symlink (${skillsPath})` };
|
|
136
98
|
}
|
|
137
99
|
return { ok: true, dir: candidate };
|
|
138
100
|
}
|
|
139
|
-
/** 总入口(条款 3/7/8):逐条物化 + 装载 + 让位合并。materialize 盘位 = `<cacheRoot>/<name>/live`
|
|
140
|
-
* (成功后原子 rename 替换,失败保留旧盘位 = LKG)。 */
|
|
141
101
|
export async function applyCenterPlugins(baseline, eff, opts) {
|
|
142
102
|
const declared = Array.isArray(eff.plugins)
|
|
143
103
|
? (eff.plugins)
|
|
@@ -150,7 +110,7 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
150
110
|
const runGit = opts.runGit ?? (async (args, env) => {
|
|
151
111
|
await execFileP("git", args, { env: { ...process.env, ...env }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
152
112
|
});
|
|
153
|
-
const taken = new Set(baseline.map((s) => s.spec.name));
|
|
113
|
+
const taken = new Set(baseline.map((s) => s.spec.name));
|
|
154
114
|
for (const decl of declared) {
|
|
155
115
|
const v = opts.skipValidate
|
|
156
116
|
? { ok: true, name: String(decl.name), url: String(decl.source?.url), ...(typeof decl.sha === "string" ? { sha: decl.sha } : {}) }
|
|
@@ -165,14 +125,12 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
165
125
|
continue;
|
|
166
126
|
}
|
|
167
127
|
const liveDir = join(opts.cacheRoot, v.name, "live");
|
|
168
|
-
// ── 物化:clone 到 staging,成功后原子替换 live;失败保留旧 live = LKG ──
|
|
169
128
|
const staging = join(opts.cacheRoot, v.name, `staging-${process.pid}`);
|
|
170
129
|
let materialized = false;
|
|
171
130
|
try {
|
|
172
131
|
await fsp.rm(staging, { recursive: true, force: true });
|
|
173
132
|
await fsp.mkdir(staging, { recursive: true });
|
|
174
133
|
const plan = gitClonePlan({ url: v.url, dest: staging, sha: v.sha });
|
|
175
|
-
// clone 目标目录已建:单步 clone 允许空目录;init 形对空目录同样成立。
|
|
176
134
|
for (const step of plan.steps)
|
|
177
135
|
await runGit(step, plan.env);
|
|
178
136
|
await fsp.rm(liveDir, { recursive: true, force: true });
|
|
@@ -183,7 +141,6 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
183
141
|
catch (err) {
|
|
184
142
|
await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
185
143
|
if (existsSync(liveDir)) {
|
|
186
|
-
// LKG:拉取失败沿用上次成功物化(applyCenterSkills 同款语义)。
|
|
187
144
|
opts.logger?.warn("center_plugin_fetch_failed_lkg", { plugin: v.name, error: err instanceof Error ? err.message : String(err) });
|
|
188
145
|
out.lkgUsed.push(v.name);
|
|
189
146
|
}
|
|
@@ -193,7 +150,6 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
193
150
|
continue;
|
|
194
151
|
}
|
|
195
152
|
}
|
|
196
|
-
// ── 装载:skills 目录(路径逃逸=整条拒装;fail-loud 单条不连坐)──
|
|
197
153
|
try {
|
|
198
154
|
const rootCheck = resolvePluginSkillsRoot(liveDir);
|
|
199
155
|
if (!rootCheck.ok) {
|
|
@@ -202,11 +158,10 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
202
158
|
continue;
|
|
203
159
|
}
|
|
204
160
|
if (!rootCheck.dir)
|
|
205
|
-
continue;
|
|
206
|
-
const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir });
|
|
161
|
+
continue;
|
|
162
|
+
const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir });
|
|
207
163
|
for (const skill of loaded) {
|
|
208
164
|
if (taken.has(skill.spec.name)) {
|
|
209
|
-
// 收紧④:plugin 让位(方向与 center-wins 相反,理由见模块顶注)。
|
|
210
165
|
out.letGo.push({ plugin: v.name, skill: skill.spec.name });
|
|
211
166
|
opts.logger?.warn("center_plugin_skill_shadowed", { plugin: v.name, skill: skill.spec.name, note: "name taken by builtin/center skill — plugin yields (contract tightening-4)" });
|
|
212
167
|
continue;
|