agentwrangler 0.1.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/LICENSE +191 -0
- package/README.md +116 -0
- package/dist/apply/jobs.js +429 -0
- package/dist/apply/open-terminal-child.mjs +98 -0
- package/dist/apply/open-terminal.js +221 -0
- package/dist/apply/settings-gen.js +35 -0
- package/dist/cli/agentwrangler.js +18 -0
- package/dist/daemon/config.js +51 -0
- package/dist/daemon/http.js +258 -0
- package/dist/daemon/index.js +372 -0
- package/dist/daemon/outcomes-pass.js +82 -0
- package/dist/daemon/readiness.js +15 -0
- package/dist/daemon/router.js +756 -0
- package/dist/daemon/static.js +146 -0
- package/dist/db/migrate.js +72 -0
- package/dist/db/migrations/001_observe.sql +196 -0
- package/dist/db/migrations/002_indexes.sql +6 -0
- package/dist/db/migrations/003_context_inventory_history.sql +20 -0
- package/dist/db/migrations/004_apply_jobs.sql +17 -0
- package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
- package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
- package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
- package/dist/db/migrations/008_thinking_tokens.sql +1 -0
- package/dist/db/migrations/009_user_turn_count.sql +1 -0
- package/dist/db/migrations/010_workspace_cwd.sql +1 -0
- package/dist/db/migrations/011_reports.sql +1 -0
- package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
- package/dist/db/migrations/013_friction_fields.sql +5 -0
- package/dist/db/migrations/014_session_churn.sql +11 -0
- package/dist/db/migrations/015_gap_aggregates.sql +6 -0
- package/dist/db/open.js +30 -0
- package/dist/detector/benchmark-anchors.js +36 -0
- package/dist/detector/calibration.js +302 -0
- package/dist/detector/context-history-retention.js +312 -0
- package/dist/detector/context-probe.js +574 -0
- package/dist/detector/d1-source-identity.js +25 -0
- package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
- package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
- package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
- package/dist/detector/detectors/d4_model_mismatch.js +258 -0
- package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
- package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
- package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
- package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
- package/dist/detector/detectors/d9_idle_background_session.js +101 -0
- package/dist/detector/engine.js +88 -0
- package/dist/detector/index.js +17 -0
- package/dist/detector/measurement.js +426 -0
- package/dist/detector/practice-registry.js +259 -0
- package/dist/detector/registry.js +32 -0
- package/dist/detector/savings.js +249 -0
- package/dist/detector/types.js +14 -0
- package/dist/evidence/common/approved-input.js +632 -0
- package/dist/evidence/common/boundary.js +84 -0
- package/dist/evidence/common/canonical.js +55 -0
- package/dist/evidence/common/redaction.js +321 -0
- package/dist/evidence/common/sqlite.js +25 -0
- package/dist/evidence/common/state.js +29 -0
- package/dist/evidence/cond1/cli.js +289 -0
- package/dist/evidence/cond1/packet.js +407 -0
- package/dist/evidence/cond1/prepare.js +295 -0
- package/dist/evidence/cond1/score.js +349 -0
- package/dist/evidence/cond1/types.js +1 -0
- package/dist/evidence/create-approval.js +365 -0
- package/dist/evidence/create-scratch.js +542 -0
- package/dist/evidence/d7/cli.js +113 -0
- package/dist/evidence/d7/measure.js +193 -0
- package/dist/evidence/d7/types.js +1 -0
- package/dist/evidence/discover-approval.js +492 -0
- package/dist/evidence/g2/adjudicate.js +20 -0
- package/dist/evidence/g2/cli.js +207 -0
- package/dist/evidence/g2/kappa.js +39 -0
- package/dist/evidence/g2/pipeline.js +92 -0
- package/dist/evidence/g2/store.js +14 -0
- package/dist/evidence/github/client.js +1 -0
- package/dist/evidence/github/gh-cli-client.js +301 -0
- package/dist/evidence/r3/cli.js +209 -0
- package/dist/evidence/r3/evaluate.js +417 -0
- package/dist/evidence/r3/packet.js +162 -0
- package/dist/evidence/r3/prepare.js +405 -0
- package/dist/evidence/r3/score.js +341 -0
- package/dist/evidence/r3/transcript.js +155 -0
- package/dist/evidence/r3/types.js +4 -0
- package/dist/hook/context-budget-hook.mjs +138 -0
- package/dist/hook/danger-guard-denylist.json +27 -0
- package/dist/hook/danger-guard-hook.mjs +167 -0
- package/dist/hook/install.js +0 -0
- package/dist/hook/limit-burn-hook.mjs +127 -0
- package/dist/hook/loop-guard-hook.mjs +104 -0
- package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
- package/dist/ingest/churn-collector.js +122 -0
- package/dist/ingest/detector-hook.js +52 -0
- package/dist/ingest/discovery.js +207 -0
- package/dist/ingest/health.js +43 -0
- package/dist/ingest/index.js +28 -0
- package/dist/ingest/ingestor.js +509 -0
- package/dist/ingest/parser.js +344 -0
- package/dist/ingest/pricing.js +153 -0
- package/dist/ingest/reconcile.js +52 -0
- package/dist/ingest/tail.js +152 -0
- package/dist/ingest/types.js +24 -0
- package/dist/ingest/workspace-mapping.js +114 -0
- package/dist/oauth/anthropic-api-key.js +88 -0
- package/dist/oauth/count-tokens.js +86 -0
- package/dist/oauth/credentials.js +171 -0
- package/dist/oauth/judge-g2-client.js +154 -0
- package/dist/oauth/usage.js +167 -0
- package/dist/outcomes/branch-key.js +49 -0
- package/dist/outcomes/conclusions.js +45 -0
- package/dist/outcomes/derive.js +94 -0
- package/dist/outcomes/finding-extractors.js +131 -0
- package/dist/outcomes/findings.js +237 -0
- package/dist/outcomes/github/client.js +367 -0
- package/dist/outcomes/github/credential.js +195 -0
- package/dist/outcomes/github/gh-cli-client.js +340 -0
- package/dist/outcomes/linker.js +486 -0
- package/dist/outcomes/pool.js +24 -0
- package/dist/outcomes/sync.js +276 -0
- package/dist/query/api/agents-liveness.js +182 -0
- package/dist/query/api/burn-status.js +50 -0
- package/dist/query/api/context-budget.js +114 -0
- package/dist/query/api/context-composition.js +67 -0
- package/dist/query/api/cost-per-success.js +104 -0
- package/dist/query/api/delivery.js +92 -0
- package/dist/query/api/effectiveness.js +254 -0
- package/dist/query/api/efficiency-headroom.js +74 -0
- package/dist/query/api/headroom-trend.js +105 -0
- package/dist/query/api/hook-config.js +75 -0
- package/dist/query/api/hook-install.js +8 -0
- package/dist/query/api/hot-sessions.js +17 -0
- package/dist/query/api/idle-sessions.js +52 -0
- package/dist/query/api/index.js +40 -0
- package/dist/query/api/loop-guard.js +90 -0
- package/dist/query/api/offload-share.js +41 -0
- package/dist/query/api/outcomes.js +218 -0
- package/dist/query/api/overview.js +535 -0
- package/dist/query/api/rec-prompt.js +138 -0
- package/dist/query/api/recommendations-ledger.js +111 -0
- package/dist/query/api/recommendations.js +514 -0
- package/dist/query/api/reports.js +78 -0
- package/dist/query/api/self-churn.js +77 -0
- package/dist/query/api/self-percentiles.js +109 -0
- package/dist/query/api/session-drivers.js +153 -0
- package/dist/query/api/settings.js +85 -0
- package/dist/query/api/spend-flavor.js +234 -0
- package/dist/query/api/trends.js +155 -0
- package/dist/query/cap-weighted.js +119 -0
- package/dist/query/db-context.js +42 -0
- package/dist/query/envelope.js +71 -0
- package/dist/query/forecast.js +191 -0
- package/dist/query/settings-store.js +441 -0
- package/dist/query/spend.js +171 -0
- package/dist/query/trends.js +194 -0
- package/dist/ui/assets/index-DnRKgc21.css +1 -0
- package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
- package/dist/ui/index.html +39 -0
- package/package.json +59 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D10 CATALOG_FOOTPRINT.
|
|
3
|
+
*
|
|
4
|
+
* This detector reports the derived MCP/plugin/skill catalog as an estimate.
|
|
5
|
+
* The ContextInventoryProbe records Tool-Search configuration facts separately
|
|
6
|
+
* in a zero-token SETTINGS_SYSTEM row, so D10 can distinguish deferred tools
|
|
7
|
+
* from server-level alwaysLoad exemptions without persisting catalog content.
|
|
8
|
+
* Per-turn loaded-tool attribution and economic savings remain gated on R11.
|
|
9
|
+
*/
|
|
10
|
+
import { GLOBAL_WORKSPACE_ID, parseToolSearchState, } from "../context-probe.js";
|
|
11
|
+
/** Retained as the unvalidated catalog-size review threshold. */
|
|
12
|
+
export const D10_CATALOG_FOOTPRINT_TARGET_TOKENS = 40_000;
|
|
13
|
+
function catalogStateLabel(state) {
|
|
14
|
+
switch (state.effective_catalog_state) {
|
|
15
|
+
case "alwaysLoad":
|
|
16
|
+
return `alwaysLoad keeps ${state.always_load_count} MCP server(s) upfront`;
|
|
17
|
+
case "deferred":
|
|
18
|
+
return "Tool Search defers MCP tool definitions until they are needed";
|
|
19
|
+
case "threshold":
|
|
20
|
+
return "Tool Search uses threshold loading, so some MCP definitions may be upfront";
|
|
21
|
+
case "upfront":
|
|
22
|
+
return "MCP tool definitions load upfront because Tool Search is disabled";
|
|
23
|
+
default:
|
|
24
|
+
return "MCP tool load behavior is unknown";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function statusNote(state, catalogTokens, withinTarget) {
|
|
28
|
+
const target = D10_CATALOG_FOOTPRINT_TARGET_TOKENS;
|
|
29
|
+
const comparison = withinTarget ? "is within" : "exceeds";
|
|
30
|
+
return `catalog estimate ${catalogTokens} token(s) ${comparison} ${target} target; ${catalogStateLabel(state)}; loaded-tool economics are not measured`;
|
|
31
|
+
}
|
|
32
|
+
function stateRow(db) {
|
|
33
|
+
const row = db
|
|
34
|
+
.prepare(`SELECT attribution_version
|
|
35
|
+
FROM context_inventory
|
|
36
|
+
WHERE workspace_id = ? AND component = 'SETTINGS_SYSTEM'
|
|
37
|
+
ORDER BY probed_at DESC
|
|
38
|
+
LIMIT 1`)
|
|
39
|
+
.get(GLOBAL_WORKSPACE_ID);
|
|
40
|
+
return row === undefined ? null : parseToolSearchState(row.attribution_version);
|
|
41
|
+
}
|
|
42
|
+
function catalogRows(db) {
|
|
43
|
+
return db
|
|
44
|
+
.prepare(`SELECT file_ref, file_hash, tokens, probed_at
|
|
45
|
+
FROM context_inventory
|
|
46
|
+
WHERE workspace_id = ? AND component = 'MCP_SCHEMAS'
|
|
47
|
+
ORDER BY file_ref`)
|
|
48
|
+
.all(GLOBAL_WORKSPACE_ID);
|
|
49
|
+
}
|
|
50
|
+
function configEvidence(state) {
|
|
51
|
+
return {
|
|
52
|
+
tool_search_mode: state.tool_search_mode,
|
|
53
|
+
effective_catalog_state: state.effective_catalog_state,
|
|
54
|
+
configured_value: state.configured_value,
|
|
55
|
+
always_load_flags: state.always_load_flags,
|
|
56
|
+
always_load_count: state.always_load_count,
|
|
57
|
+
always_load_flags_truncated: state.always_load_flags_truncated,
|
|
58
|
+
catalog_item_count: state.catalog_item_count,
|
|
59
|
+
catalog_item_count_truncated: state.catalog_item_count_truncated,
|
|
60
|
+
catalog_hash: state.catalog_hash,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export const d10Detector = {
|
|
64
|
+
id: "D10",
|
|
65
|
+
name: "CATALOG_FOOTPRINT",
|
|
66
|
+
evaluate(db, _ctx) {
|
|
67
|
+
const rows = catalogRows(db);
|
|
68
|
+
const state = stateRow(db);
|
|
69
|
+
if (rows.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
fired: [],
|
|
72
|
+
status: "NOT_EVALUATED",
|
|
73
|
+
note: state === null
|
|
74
|
+
? "catalog estimate unavailable; Tool-Search config state is not measured"
|
|
75
|
+
: `catalog estimate unavailable; ${catalogStateLabel(state)}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (state === null) {
|
|
79
|
+
return {
|
|
80
|
+
fired: [],
|
|
81
|
+
status: "NOT_EVALUATED",
|
|
82
|
+
note: "catalog inventory is estimated, but Tool-Search config state is not measured",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const catalogTokens = rows.reduce((sum, row) => sum + row.tokens, 0);
|
|
86
|
+
const withinTarget = catalogTokens <= D10_CATALOG_FOOTPRINT_TARGET_TOKENS;
|
|
87
|
+
if (withinTarget) {
|
|
88
|
+
return {
|
|
89
|
+
fired: [],
|
|
90
|
+
status: "INACTIVE",
|
|
91
|
+
note: statusNote(state, catalogTokens, true),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const delta = catalogTokens - D10_CATALOG_FOOTPRINT_TARGET_TOKENS;
|
|
95
|
+
const latestProbedAt = rows.reduce((latest, row) => (latest === null || row.probed_at > latest ? row.probed_at : latest), null);
|
|
96
|
+
const refs = rows.map((row) => row.file_ref);
|
|
97
|
+
const evidence = {
|
|
98
|
+
title: `Review ${state.effective_catalog_state} tool catalog: ${Math.round(catalogTokens / 1000)}K tokens`,
|
|
99
|
+
component: "MCP_SCHEMAS",
|
|
100
|
+
file_ref: refs.length === 1 ? refs[0] : null,
|
|
101
|
+
file_refs: refs,
|
|
102
|
+
source_count: rows.length,
|
|
103
|
+
catalog_tokens: catalogTokens,
|
|
104
|
+
catalog_target_tokens: D10_CATALOG_FOOTPRINT_TARGET_TOKENS,
|
|
105
|
+
delta_context_tokens: delta,
|
|
106
|
+
estimate: true,
|
|
107
|
+
priority: "down-ranked",
|
|
108
|
+
thresholds_unvalidated: true,
|
|
109
|
+
loaded_tool_telemetry: "unavailable",
|
|
110
|
+
probed_at: latestProbedAt,
|
|
111
|
+
...configEvidence(state),
|
|
112
|
+
steps: [
|
|
113
|
+
"Review enabled MCP servers, plugins, and skills in the local Claude catalog",
|
|
114
|
+
"Keep only the small set of MCP servers that need alwaysLoad on every turn",
|
|
115
|
+
"Disable rarely used plugins or leave infrequent tools deferred",
|
|
116
|
+
"Re-measure: ContextInventoryProbe refreshes catalog and config facts on the next daemon pass",
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
const fired = {
|
|
120
|
+
scopeKey: "D10|global|MCP_SCHEMAS",
|
|
121
|
+
category: "TOOLING",
|
|
122
|
+
scope_workspace_id: null,
|
|
123
|
+
lever: "Too many connected tools, plugins, and skills",
|
|
124
|
+
target_metric: "catalog_context_tokens",
|
|
125
|
+
// R11 is required before catalog size can become a freed-headroom claim.
|
|
126
|
+
modeled_savings_u_per_wk: null,
|
|
127
|
+
modeled_formula: {
|
|
128
|
+
model: "D10_CATALOG_FOOTPRINT_CONFIG_V1",
|
|
129
|
+
inputs: {
|
|
130
|
+
catalog_tokens: catalogTokens,
|
|
131
|
+
catalog_target_tokens: D10_CATALOG_FOOTPRINT_TARGET_TOKENS,
|
|
132
|
+
delta_context_tokens: delta,
|
|
133
|
+
catalog_item_count: state.catalog_item_count,
|
|
134
|
+
always_load_count: state.always_load_count,
|
|
135
|
+
},
|
|
136
|
+
kind: "estimate",
|
|
137
|
+
},
|
|
138
|
+
evidence,
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
fired: [fired],
|
|
142
|
+
status: "ACTIVE",
|
|
143
|
+
note: statusNote(state, catalogTokens, false),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/detector/detectors/d1_ctx_always_loaded.ts — D1 CTX_ALWAYS_LOADED_OVERSIZE.
|
|
3
|
+
*
|
|
4
|
+
* Fires PER-SOURCE against per-source token targets (UNVALIDATED defaults).
|
|
5
|
+
* For each context_inventory row:
|
|
6
|
+
* - fire when source_tokens > D1_SOURCE_TARGETS[component]
|
|
7
|
+
* - delta = source_tokens - target (trimmable portion)
|
|
8
|
+
* - weekly savings µUSD = delta × turns_per_week × blended_cache_read_price
|
|
9
|
+
*
|
|
10
|
+
* GLOBAL sources (workspace_id = '__global__'): multiply turns_per_week by the
|
|
11
|
+
* count of active (non-global) workspaces; set scope_workspace_id = null.
|
|
12
|
+
* PER-WORKSPACE sources: keep the real workspace_id as scope_workspace_id.
|
|
13
|
+
*
|
|
14
|
+
* scopeKey = `D1|<workspace_id_or_global>|<component>|<file_ref-basename>`
|
|
15
|
+
* so rec_id is stable and unique per source.
|
|
16
|
+
*
|
|
17
|
+
* When context_inventory is empty, returns NOT_EVALUATED (ContextInventoryProbe
|
|
18
|
+
* not yet run or no sources found).
|
|
19
|
+
*/
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { LIST_PRICES } from "../../ingest/pricing.js";
|
|
22
|
+
import { GLOBAL_WORKSPACE_ID } from "../context-probe.js";
|
|
23
|
+
import { D1_SOURCE_TARGETS, d1Savings } from "../savings.js";
|
|
24
|
+
/** Opus list cache-read price ($/MTok) — anchor when a workspace has no cache reads. */
|
|
25
|
+
const OPUS_CACHE_READ_PRICE = LIST_PRICES.opus?.[2] ?? 0.5;
|
|
26
|
+
/** Steps[] wording per component — numbered, source-specific action plan. */
|
|
27
|
+
function stepsFor(component, fileRef) {
|
|
28
|
+
switch (component) {
|
|
29
|
+
case "CLAUDE_MD":
|
|
30
|
+
return [
|
|
31
|
+
`Open ${fileRef}`,
|
|
32
|
+
"Move changelog/history/rationale prose to a linked doc",
|
|
33
|
+
"Keep current-state rules + pointers only",
|
|
34
|
+
"Re-measure: probe re-sizes on next daemon pass",
|
|
35
|
+
];
|
|
36
|
+
case "MEMORY":
|
|
37
|
+
return [
|
|
38
|
+
`Review memory files under ${fileRef}`,
|
|
39
|
+
"Delete stale or duplicate memories",
|
|
40
|
+
"Consolidate overlapping facts into concise entries",
|
|
41
|
+
"Re-measure: probe re-sizes on next daemon pass",
|
|
42
|
+
];
|
|
43
|
+
case "MCP_SCHEMAS":
|
|
44
|
+
return [
|
|
45
|
+
"List enabled plugins/skills in ~/.claude/plugins and ~/.claude/skills",
|
|
46
|
+
"Identify rarely-used skills from recent session activity",
|
|
47
|
+
"Extract to on-demand usage or disable the plugin",
|
|
48
|
+
"Re-measure: probe re-sizes on next daemon pass",
|
|
49
|
+
];
|
|
50
|
+
default:
|
|
51
|
+
return [`Trim ${fileRef} to reduce always-loaded context`];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Short imperative title per component — written into evidence.title (read by toCard). */
|
|
55
|
+
function titleFor(component, tokens, target) {
|
|
56
|
+
const label = component === "CLAUDE_MD" ? "CLAUDE.md" : component === "MEMORY" ? "memory" : "MCP schemas";
|
|
57
|
+
const fromK = Math.round(tokens / 1_000);
|
|
58
|
+
const toK = Math.round(target / 1_000);
|
|
59
|
+
return `Trim ${label}: ${fromK}K→${toK}K tokens`;
|
|
60
|
+
}
|
|
61
|
+
/** Lever label per component. */
|
|
62
|
+
function leverFor(component) {
|
|
63
|
+
switch (component) {
|
|
64
|
+
case "CLAUDE_MD":
|
|
65
|
+
return "Move changelog/history prose out of CLAUDE.md; keep current-state + pointers.";
|
|
66
|
+
case "MEMORY":
|
|
67
|
+
return "Prune stale/duplicate memories; consolidate overlapping facts.";
|
|
68
|
+
case "MCP_SCHEMAS":
|
|
69
|
+
return "Identify rarely-used skills/plugins; extract to on-demand or disable.";
|
|
70
|
+
default:
|
|
71
|
+
return "Trim always-loaded context source to the per-source target.";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export const d1Detector = {
|
|
75
|
+
id: "D1",
|
|
76
|
+
name: "CTX_ALWAYS_LOADED_OVERSIZE",
|
|
77
|
+
evaluate(db, ctx) {
|
|
78
|
+
// Early exit when there is nothing to evaluate.
|
|
79
|
+
const inventoryCount = db.prepare("SELECT COUNT(*) AS n FROM context_inventory").get().n;
|
|
80
|
+
if (inventoryCount === 0) {
|
|
81
|
+
return {
|
|
82
|
+
fired: [],
|
|
83
|
+
status: "NOT_EVALUATED",
|
|
84
|
+
note: "no context_inventory rows — run ContextInventoryProbe first",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Only sources with an active D1 contract are evaluable. MCP_SCHEMAS is a
|
|
88
|
+
// catalog estimate and is intentionally owned by the gated D10 detector.
|
|
89
|
+
const rows = db
|
|
90
|
+
.prepare(`SELECT probe_id, workspace_id, component, file_ref, tokens, probed_at
|
|
91
|
+
FROM context_inventory
|
|
92
|
+
WHERE component IN ('CLAUDE_MD', 'MEMORY')`)
|
|
93
|
+
.all();
|
|
94
|
+
if (rows.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
fired: [],
|
|
97
|
+
status: "NOT_EVALUATED",
|
|
98
|
+
note: inventoryCount === 0
|
|
99
|
+
? "no context_inventory rows — run ContextInventoryProbe first"
|
|
100
|
+
: "no evaluable context_inventory rows — catalog estimates are gated behind D10 load-state telemetry",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Count active (non-global) workspaces for the global multiplier.
|
|
104
|
+
const activeWorkspaceCount = db
|
|
105
|
+
.prepare("SELECT COUNT(*) AS n FROM workspaces WHERE workspace_id != ?")
|
|
106
|
+
.get(GLOBAL_WORKSPACE_ID).n;
|
|
107
|
+
// Turn metrics for ALL non-global workspaces (used for global sources).
|
|
108
|
+
const globalMetrics = db
|
|
109
|
+
.prepare(`SELECT COUNT(*) AS turns_per_week,
|
|
110
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
111
|
+
COALESCE(SUM(cache_read_tokens *
|
|
112
|
+
(SELECT CAST(json_extract(ps.unit_prices_json, '$[2]') AS REAL)
|
|
113
|
+
FROM pricing_snapshots ps
|
|
114
|
+
WHERE ps.snapshot_id = t.pricing_snapshot_id)), 0) AS cache_read_spend_u
|
|
115
|
+
FROM turns t
|
|
116
|
+
WHERE t.workspace_id != ? AND t.ts >= ? AND t.ts < ?`)
|
|
117
|
+
.get(GLOBAL_WORKSPACE_ID, ctx.fromIso, ctx.toIso);
|
|
118
|
+
const fired = [];
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const target = D1_SOURCE_TARGETS[row.component];
|
|
121
|
+
if (target === undefined)
|
|
122
|
+
continue; // unsupported component — skip
|
|
123
|
+
if (row.tokens <= target)
|
|
124
|
+
continue; // within target — no rec
|
|
125
|
+
const delta = row.tokens - target;
|
|
126
|
+
const isGlobal = row.workspace_id === GLOBAL_WORKSPACE_ID;
|
|
127
|
+
const scope = isGlobal ? "global" : "workspace";
|
|
128
|
+
// Turn metrics: global sources use all-workspace aggregate; per-workspace
|
|
129
|
+
// sources query only their own workspace.
|
|
130
|
+
let metrics;
|
|
131
|
+
if (isGlobal) {
|
|
132
|
+
metrics = globalMetrics;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
metrics = db
|
|
136
|
+
.prepare(`SELECT COUNT(*) AS turns_per_week,
|
|
137
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
138
|
+
COALESCE(SUM(cache_read_tokens *
|
|
139
|
+
(SELECT CAST(json_extract(ps.unit_prices_json, '$[2]') AS REAL)
|
|
140
|
+
FROM pricing_snapshots ps
|
|
141
|
+
WHERE ps.snapshot_id = t.pricing_snapshot_id)), 0) AS cache_read_spend_u
|
|
142
|
+
FROM turns t
|
|
143
|
+
WHERE t.workspace_id = ? AND t.ts >= ? AND t.ts < ?`)
|
|
144
|
+
.get(row.workspace_id, ctx.fromIso, ctx.toIso);
|
|
145
|
+
}
|
|
146
|
+
// Effective turns: for global, the total already spans all workspaces.
|
|
147
|
+
// workspace_multiplier is stored in evidence for display transparency.
|
|
148
|
+
const effectiveTurns = isGlobal ? metrics.turns_per_week : metrics.turns_per_week;
|
|
149
|
+
const blendedPrice = metrics.cache_read_tokens > 0
|
|
150
|
+
? metrics.cache_read_spend_u / metrics.cache_read_tokens
|
|
151
|
+
: OPUS_CACHE_READ_PRICE;
|
|
152
|
+
const { savingsU, formula } = d1Savings(delta, effectiveTurns, Number(blendedPrice.toFixed(6)));
|
|
153
|
+
// Skip dormant workspaces: no turns in-window ⇒ zero modeled savings this week,
|
|
154
|
+
// so the rec would be noise ("prune memory · 0 tokens/wk freed"). Latent bloat in
|
|
155
|
+
// an inactive workspace resurfaces on a later pass once it has activity again.
|
|
156
|
+
if (savingsU === 0)
|
|
157
|
+
continue;
|
|
158
|
+
const fileBasename = path.basename(row.file_ref);
|
|
159
|
+
// scopeKey uses workspace_id (or 'global' for __global__) to keep it unique
|
|
160
|
+
// across workspaces with the same component+basename.
|
|
161
|
+
const scopeSegment = isGlobal ? "global" : row.workspace_id;
|
|
162
|
+
const scopeKey = `D1|${scopeSegment}|${row.component}|${fileBasename}`;
|
|
163
|
+
const evidenceBase = {
|
|
164
|
+
title: titleFor(row.component, row.tokens, target),
|
|
165
|
+
component: row.component,
|
|
166
|
+
file_ref: row.file_ref,
|
|
167
|
+
source_tokens: row.tokens,
|
|
168
|
+
source_target: target,
|
|
169
|
+
delta_context_tokens: delta,
|
|
170
|
+
turns_per_week: effectiveTurns,
|
|
171
|
+
modeled_savings_basis: "LIST_EQUIV",
|
|
172
|
+
billed_cost_claim: "UNAVAILABLE",
|
|
173
|
+
scope,
|
|
174
|
+
steps: stepsFor(row.component, row.file_ref),
|
|
175
|
+
};
|
|
176
|
+
if (isGlobal) {
|
|
177
|
+
evidenceBase.workspace_multiplier = activeWorkspaceCount;
|
|
178
|
+
}
|
|
179
|
+
fired.push({
|
|
180
|
+
scopeKey,
|
|
181
|
+
category: "CONTEXT",
|
|
182
|
+
scope_workspace_id: isGlobal ? null : row.workspace_id,
|
|
183
|
+
lever: leverFor(row.component),
|
|
184
|
+
target_metric: "avg_context_per_turn",
|
|
185
|
+
modeled_savings_u_per_wk: savingsU,
|
|
186
|
+
modeled_formula: formula,
|
|
187
|
+
evidence: evidenceBase,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (fired.length === 0) {
|
|
191
|
+
return {
|
|
192
|
+
fired: [],
|
|
193
|
+
status: "INACTIVE",
|
|
194
|
+
note: "all context_inventory sources within per-source targets",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
fired,
|
|
199
|
+
status: "ACTIVE",
|
|
200
|
+
note: `${fired.length} source(s) exceed their per-source target (CLAUDE_MD=${D1_SOURCE_TARGETS.CLAUDE_MD}, MEMORY=${D1_SOURCE_TARGETS.MEMORY}); catalog estimates are gated behind D10 load-state telemetry`,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/detector/detectors/d2_session_long_full_context.ts — D2 SESSION_LONG_FULL_CONTEXT.
|
|
3
|
+
*
|
|
4
|
+
* Fires (global scope) when >= N qualifying long-context sessions appear in the
|
|
5
|
+
* trailing week. Qualifying = more than 150 reconciled turns in the same
|
|
6
|
+
* evaluation window AND AVG(context_tokens) > 180_000 over those same rows.
|
|
7
|
+
*
|
|
8
|
+
* Savings: cache-read spend the qualifying sessions generate × an (unvalidated)
|
|
9
|
+
* reduction fraction. The cache-read price is derived PER-MODEL from each turn's
|
|
10
|
+
* own pricing snapshot (unit_prices_json[2]), never a hard-coded scalar (§1 D2 (b)).
|
|
11
|
+
*/
|
|
12
|
+
import { capWeightedTokens, resolveCapReadCoeff } from "../../query/cap-weighted.js";
|
|
13
|
+
import { D2_AVG_CONTEXT_THRESHOLD, D2_MIN_QUALIFYING_SESSIONS, D2_REDUCTION_FRACTION, D2_TURN_COUNT_THRESHOLD, d2Savings, } from "../savings.js";
|
|
14
|
+
/** Cache-read price is index 2 of unit_prices_json ([in, out, cacheRead, cw5m, cw1h]). */
|
|
15
|
+
function cacheReadPrice(unitPricesJson) {
|
|
16
|
+
if (unitPricesJson === null)
|
|
17
|
+
return 0;
|
|
18
|
+
try {
|
|
19
|
+
const arr = JSON.parse(unitPricesJson);
|
|
20
|
+
return arr[2] ?? 0;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export const d2Detector = {
|
|
27
|
+
id: "D2",
|
|
28
|
+
name: "SESSION_LONG_FULL_CONTEXT",
|
|
29
|
+
evaluate(db, ctx) {
|
|
30
|
+
const qualifying = db
|
|
31
|
+
.prepare(`SELECT t.session_id AS session_id,
|
|
32
|
+
AVG(t.context_tokens) AS avg_context,
|
|
33
|
+
COUNT(*) AS qualifying_turns
|
|
34
|
+
FROM turns t
|
|
35
|
+
WHERE t.ts >= ? AND t.ts < ? AND t.provisional = 0
|
|
36
|
+
GROUP BY t.session_id
|
|
37
|
+
HAVING COUNT(*) > ? AND AVG(t.context_tokens) > ?
|
|
38
|
+
ORDER BY t.session_id ASC`)
|
|
39
|
+
.all(ctx.fromIso, ctx.toIso, D2_TURN_COUNT_THRESHOLD, D2_AVG_CONTEXT_THRESHOLD);
|
|
40
|
+
if (qualifying.length < D2_MIN_QUALIFYING_SESSIONS) {
|
|
41
|
+
return {
|
|
42
|
+
fired: [],
|
|
43
|
+
status: "INACTIVE",
|
|
44
|
+
note: `${qualifying.length} qualifying long-context session(s) in window (trigger ≥ ${D2_MIN_QUALIFYING_SESSIONS})`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const sessionIds = qualifying.map((q) => q.session_id).sort();
|
|
48
|
+
const placeholders = sessionIds.map(() => "?").join(",");
|
|
49
|
+
const qualifyingTurnCount = qualifying.reduce((sum, session) => sum + session.qualifying_turns, 0);
|
|
50
|
+
const rawContextAverageTokensPerTurn = qualifyingTurnCount > 0
|
|
51
|
+
? qualifying.reduce((sum, session) => sum + session.avg_context * session.qualifying_turns, 0) / qualifyingTurnCount
|
|
52
|
+
: 0;
|
|
53
|
+
// Cap-weighted burn is a separate annotation on the raw-context rec. It
|
|
54
|
+
// never participates in the qualifying-session gate above.
|
|
55
|
+
const capReadCoefficient = resolveCapReadCoeff(db);
|
|
56
|
+
const capBySession = new Map(capWeightedTokens(db, {
|
|
57
|
+
fromIso: ctx.fromIso,
|
|
58
|
+
toIso: ctx.toIso,
|
|
59
|
+
groupBy: "session_id",
|
|
60
|
+
coeff: capReadCoefficient,
|
|
61
|
+
}).map((row) => [row.group, row.cap_weighted_tokens]));
|
|
62
|
+
const capWeightedBurnTokensPerWeek = sessionIds.reduce((sum, sessionId) => sum + (capBySession.get(sessionId) ?? 0), 0);
|
|
63
|
+
// Cache-read spend over the qualifying sessions' turns in the window, priced
|
|
64
|
+
// per-model from each turn's own pricing snapshot.
|
|
65
|
+
const rows = db
|
|
66
|
+
.prepare(`SELECT t.cache_read_tokens AS cache_read_tokens, ps.unit_prices_json AS unit_prices_json
|
|
67
|
+
FROM turns t
|
|
68
|
+
LEFT JOIN pricing_snapshots ps ON ps.snapshot_id = t.pricing_snapshot_id
|
|
69
|
+
WHERE t.session_id IN (${placeholders})
|
|
70
|
+
AND t.ts >= ? AND t.ts < ? AND t.provisional = 0`)
|
|
71
|
+
.all(...sessionIds, ctx.fromIso, ctx.toIso);
|
|
72
|
+
let cacheReadTokensPerWeek = 0;
|
|
73
|
+
let cacheReadSpendU = 0;
|
|
74
|
+
for (const r of rows) {
|
|
75
|
+
cacheReadTokensPerWeek += r.cache_read_tokens;
|
|
76
|
+
cacheReadSpendU += r.cache_read_tokens * cacheReadPrice(r.unit_prices_json);
|
|
77
|
+
}
|
|
78
|
+
const { savingsU, formula } = d2Savings(cacheReadTokensPerWeek, cacheReadSpendU, D2_REDUCTION_FRACTION);
|
|
79
|
+
const n = qualifying.length;
|
|
80
|
+
const fired = {
|
|
81
|
+
scopeKey: `D2|global|${formula.model}`,
|
|
82
|
+
category: "CONTEXT",
|
|
83
|
+
scope_workspace_id: null,
|
|
84
|
+
lever: "/clear between unrelated tasks; split long work; avoid mid-task /compact.",
|
|
85
|
+
target_metric: "avg_context_per_turn",
|
|
86
|
+
modeled_savings_u_per_wk: savingsU,
|
|
87
|
+
modeled_formula: formula,
|
|
88
|
+
evidence: {
|
|
89
|
+
title: `Shorten sessions: ${n} long-context run${n === 1 ? "" : "s"} this week`,
|
|
90
|
+
qualifying_session_count: qualifying.length,
|
|
91
|
+
qualifying_turn_count: qualifyingTurnCount,
|
|
92
|
+
session_ids: sessionIds,
|
|
93
|
+
denominator: "reconciled turns in the evaluation window",
|
|
94
|
+
turn_count_threshold: D2_TURN_COUNT_THRESHOLD,
|
|
95
|
+
avg_context_threshold: D2_AVG_CONTEXT_THRESHOLD,
|
|
96
|
+
raw_context_average_tokens_per_turn: Number(rawContextAverageTokensPerTurn.toFixed(2)),
|
|
97
|
+
raw_context_basis: "RAW_USAGE",
|
|
98
|
+
cap_weighted_burn_tokens_per_week: capWeightedBurnTokensPerWeek,
|
|
99
|
+
cap_weighted_burn_basis: "CAP_PROXY",
|
|
100
|
+
cap_read_coefficient: capReadCoefficient,
|
|
101
|
+
cap_read_coefficient_unvalidated: true,
|
|
102
|
+
cache_read_tokens_per_week: cacheReadTokensPerWeek,
|
|
103
|
+
cache_read_spend_u_per_week: Math.round(cacheReadSpendU),
|
|
104
|
+
cache_read_exposure_tokens_per_week: cacheReadTokensPerWeek,
|
|
105
|
+
cache_read_exposure_spend_u_per_week: Math.round(cacheReadSpendU),
|
|
106
|
+
cache_read_exposure_spend_basis: "LIST_EQUIV",
|
|
107
|
+
modeled_savings_basis: "LIST_EQUIV",
|
|
108
|
+
billed_cost_claim: "UNAVAILABLE",
|
|
109
|
+
reduction_fraction: D2_REDUCTION_FRACTION,
|
|
110
|
+
thresholds_unvalidated: true,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
fired: [fired],
|
|
115
|
+
status: "ACTIVE",
|
|
116
|
+
note: `${qualifying.length} qualifying long-context sessions this week`,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|