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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/outcomes/sync.ts — GitHub → work_items sync.
|
|
3
|
+
*
|
|
4
|
+
* Fetches PRs from GitHub and upserts into the work_items table.
|
|
5
|
+
* Watermark in user_config (key: "gh_watermark:<owner>/<repo>") prevents
|
|
6
|
+
* re-fetching already-processed PRs.
|
|
7
|
+
*
|
|
8
|
+
* Body and diff are parsed in-memory only — never stored (SEC-101).
|
|
9
|
+
* Skip workspaces without repo_owner/repo_name.
|
|
10
|
+
*/
|
|
11
|
+
import { DEFAULT_CONCURRENCY, mapWithConcurrency } from "./pool.js";
|
|
12
|
+
/**
|
|
13
|
+
* Sync PRs for one workspace into work_items.
|
|
14
|
+
* Idempotent — ON CONFLICT DO UPDATE safe to call repeatedly.
|
|
15
|
+
* Skips workspace silently if client is disabled.
|
|
16
|
+
*/
|
|
17
|
+
export async function syncWorkItems(db, client, workspace) {
|
|
18
|
+
if (!client.enabled)
|
|
19
|
+
return;
|
|
20
|
+
const { workspace_id, repo_owner, repo_name } = workspace;
|
|
21
|
+
const watermarkKey = `gh_watermark:${repo_owner}/${repo_name}`;
|
|
22
|
+
// Read watermark
|
|
23
|
+
const wmRow = db.prepare("SELECT value FROM user_config WHERE key = ?").get(watermarkKey);
|
|
24
|
+
const since = wmRow?.value ?? undefined;
|
|
25
|
+
const listResult = await client.listPRs(repo_owner, repo_name, since);
|
|
26
|
+
if (!listResult.ok) {
|
|
27
|
+
console.warn(`syncWorkItems: listPRs failed — ${listResult.reason}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const prs = listResult.data;
|
|
31
|
+
if (prs.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
const upsert = db.prepare(`
|
|
34
|
+
INSERT INTO work_items
|
|
35
|
+
(work_item_id, workspace_id, number, state, final_commit,
|
|
36
|
+
checks_conclusion, opened_at, merged_at, closed_at, synced_at)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
38
|
+
ON CONFLICT(work_item_id) DO UPDATE SET
|
|
39
|
+
state = excluded.state,
|
|
40
|
+
final_commit = excluded.final_commit,
|
|
41
|
+
checks_conclusion = COALESCE(excluded.checks_conclusion, work_items.checks_conclusion),
|
|
42
|
+
merged_at = excluded.merged_at,
|
|
43
|
+
closed_at = excluded.closed_at,
|
|
44
|
+
synced_at = excluded.synced_at
|
|
45
|
+
`);
|
|
46
|
+
const ensureConfig = db.prepare(`
|
|
47
|
+
INSERT OR IGNORE INTO user_config (key, value, updated_at) VALUES (?, ?, ?)
|
|
48
|
+
`);
|
|
49
|
+
const updateConfig = db.prepare("UPDATE user_config SET value = ?, updated_at = ? WHERE key = ?");
|
|
50
|
+
const upsertBranchKey = db.prepare(`
|
|
51
|
+
INSERT INTO work_item_branch_keys
|
|
52
|
+
(work_item_id, head_ref_key, normalization_version, synced_at)
|
|
53
|
+
VALUES (?, ?, 'branch-v1', ?)
|
|
54
|
+
ON CONFLICT(work_item_id) DO UPDATE SET
|
|
55
|
+
head_ref_key = excluded.head_ref_key,
|
|
56
|
+
normalization_version = excluded.normalization_version,
|
|
57
|
+
synced_at = excluded.synced_at
|
|
58
|
+
`);
|
|
59
|
+
const deleteBranchKey = db.prepare("DELETE FROM work_item_branch_keys WHERE work_item_id = ?");
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
let newestUpdated = since ?? "1970-01-01T00:00:00Z";
|
|
62
|
+
// Compute per-PR row fields synchronously, then fetch check conclusions with
|
|
63
|
+
// bounded concurrency (~0.77s per call; serially that was ~97s across a
|
|
64
|
+
// 126-PR page — pooled at DEFAULT_CONCURRENCY it drops proportionally).
|
|
65
|
+
const rows = prs.map((pr) => {
|
|
66
|
+
const workItemId = `gh:${repo_owner}/${repo_name}#${pr.number}`;
|
|
67
|
+
const state = pr.merged_at !== null ? "MERGED" : pr.state === "closed" ? "CLOSED" : "OPEN";
|
|
68
|
+
const finalCommit = pr.merge_commit_sha ?? pr.head.sha;
|
|
69
|
+
return { pr, workItemId, state, finalCommit };
|
|
70
|
+
});
|
|
71
|
+
const conclusions = await mapWithConcurrency(rows, DEFAULT_CONCURRENCY, (row) => client.getCheckConclusion(repo_owner, repo_name, row.pr.head.sha));
|
|
72
|
+
const writePR = db.transaction((row, checksConclusion, syncedAt) => {
|
|
73
|
+
upsert.run(row.workItemId, workspace_id, row.pr.number, row.state, row.finalCommit, checksConclusion, row.pr.created_at, row.pr.merged_at, row.pr.closed_at, syncedAt);
|
|
74
|
+
if (row.pr.head.refKey === null) {
|
|
75
|
+
deleteBranchKey.run(row.workItemId);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
upsertBranchKey.run(row.workItemId, row.pr.head.refKey, syncedAt);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
for (let i = 0; i < rows.length; i++) {
|
|
82
|
+
const row = rows[i];
|
|
83
|
+
// Get check conclusion for the HEAD SHA.
|
|
84
|
+
// If the call fails (rate-limit, truncation, transient error), pass null so the
|
|
85
|
+
// COALESCE in the upsert preserves any previously-correct value rather than
|
|
86
|
+
// overwriting it — preventing a false OBSERVED_SUCCESS from a transient failure.
|
|
87
|
+
const checkResult = conclusions[i];
|
|
88
|
+
const checksConclusion = checkResult?.ok === true ? checkResult.data : null;
|
|
89
|
+
// The work-item projection and privacy-safe branch key are one unit: a
|
|
90
|
+
// changed head SHA can never commit alongside a stale branch key.
|
|
91
|
+
writePR(row, checksConclusion, now);
|
|
92
|
+
// Track newest updated_at for watermark
|
|
93
|
+
const updatedAt = row.pr.merged_at ?? row.pr.closed_at ?? row.pr.created_at;
|
|
94
|
+
if (updatedAt > newestUpdated)
|
|
95
|
+
newestUpdated = updatedAt;
|
|
96
|
+
}
|
|
97
|
+
// Advance watermark
|
|
98
|
+
ensureConfig.run(watermarkKey, newestUpdated, now);
|
|
99
|
+
updateConfig.run(newestUpdated, now, watermarkKey);
|
|
100
|
+
}
|
|
101
|
+
const STRICT_UTC_RFC3339 = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/u;
|
|
102
|
+
const EVIDENCE_SCAN_CHUNK = 256;
|
|
103
|
+
function parseStrictUtcTimestamp(value, code) {
|
|
104
|
+
const match = STRICT_UTC_RFC3339.exec(value);
|
|
105
|
+
if (match === null)
|
|
106
|
+
throw new Error(code);
|
|
107
|
+
const milliseconds = (match[2] ?? "").padEnd(3, "0");
|
|
108
|
+
const canonical = `${match[1]}.${milliseconds}Z`;
|
|
109
|
+
const epoch = Date.parse(canonical);
|
|
110
|
+
if (!Number.isFinite(epoch) || new Date(epoch).toISOString() !== canonical)
|
|
111
|
+
throw new Error(code);
|
|
112
|
+
return epoch;
|
|
113
|
+
}
|
|
114
|
+
function assertEvidenceRepositories(db, evidence) {
|
|
115
|
+
if (evidence.repositories.length === 0 ||
|
|
116
|
+
evidence.asOf.length === 0 ||
|
|
117
|
+
evidence.syncedAt.length === 0 ||
|
|
118
|
+
!Number.isFinite(parseStrictUtcTimestamp(evidence.asOf, "branch_backfill_as_of_invalid")) ||
|
|
119
|
+
!Number.isFinite(parseStrictUtcTimestamp(evidence.syncedAt, "branch_backfill_synced_at_invalid"))) {
|
|
120
|
+
throw new Error("branch_backfill_evidence_options_invalid");
|
|
121
|
+
}
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const findWorkspace = db.prepare("SELECT repo_owner, repo_name FROM workspaces WHERE workspace_id = ?");
|
|
124
|
+
for (const repository of evidence.repositories) {
|
|
125
|
+
if (seen.has(repository.workspaceId))
|
|
126
|
+
throw new Error("branch_backfill_allowlist_duplicate");
|
|
127
|
+
seen.add(repository.workspaceId);
|
|
128
|
+
const row = findWorkspace.get(repository.workspaceId);
|
|
129
|
+
if (row === undefined ||
|
|
130
|
+
row.repo_owner !== repository.owner ||
|
|
131
|
+
row.repo_name !== repository.repo) {
|
|
132
|
+
throw new Error("branch_backfill_allowlist_mismatch");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Validation-only, bounded backfill for historical work items missed by the
|
|
138
|
+
* 100-row incremental listing. It is deliberately not wired into the daemon.
|
|
139
|
+
* Existing keys are never fetched or overwritten, and failed/ineligible rows
|
|
140
|
+
* remain missing so a later traversal from the start can retry them.
|
|
141
|
+
*/
|
|
142
|
+
export async function backfillMissingWorkItemBranchKeys(db, client, options = {}) {
|
|
143
|
+
const requestedLimit = options.limit ?? 100;
|
|
144
|
+
const limit = Math.max(1, Math.min(1000, Math.trunc(requestedLimit)));
|
|
145
|
+
const requestedConcurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
146
|
+
const concurrency = Math.max(1, Math.min(32, Math.trunc(requestedConcurrency)));
|
|
147
|
+
const evidence = options.evidence;
|
|
148
|
+
if (evidence !== undefined)
|
|
149
|
+
assertEvidenceRepositories(db, evidence);
|
|
150
|
+
const cursor = evidence?.resumeFromStart === true ? "" : (options.cursor ?? "");
|
|
151
|
+
const allowlistClause = evidence === undefined
|
|
152
|
+
? ""
|
|
153
|
+
: ` AND (${evidence.repositories
|
|
154
|
+
.map(() => "(wi.workspace_id = ? AND ws.repo_owner = ? AND ws.repo_name = ?)")
|
|
155
|
+
.join(" OR ")})`;
|
|
156
|
+
const queryArguments = [cursor];
|
|
157
|
+
if (evidence !== undefined) {
|
|
158
|
+
for (const repository of evidence.repositories) {
|
|
159
|
+
queryArguments.push(repository.workspaceId, repository.owner, repository.repo);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
queryArguments.push(evidence === undefined ? limit + 1 : EVIDENCE_SCAN_CHUNK);
|
|
163
|
+
const queriedCandidates = db
|
|
164
|
+
.prepare(`SELECT wi.work_item_id, wi.workspace_id, ws.repo_owner, ws.repo_name, wi.number,
|
|
165
|
+
wi.synced_at
|
|
166
|
+
FROM work_items wi
|
|
167
|
+
JOIN workspaces ws ON ws.workspace_id = wi.workspace_id
|
|
168
|
+
LEFT JOIN work_item_branch_keys bk ON bk.work_item_id = wi.work_item_id
|
|
169
|
+
WHERE bk.work_item_id IS NULL
|
|
170
|
+
AND ws.repo_owner IS NOT NULL
|
|
171
|
+
AND ws.repo_name IS NOT NULL
|
|
172
|
+
AND wi.work_item_id =
|
|
173
|
+
'gh:' || ws.repo_owner || '/' || ws.repo_name || '#' || wi.number
|
|
174
|
+
AND wi.work_item_id > ?
|
|
175
|
+
${allowlistClause}
|
|
176
|
+
ORDER BY wi.work_item_id
|
|
177
|
+
LIMIT ?`)
|
|
178
|
+
.all(...queryArguments);
|
|
179
|
+
const candidates = evidence === undefined
|
|
180
|
+
? queriedCandidates
|
|
181
|
+
: queriedCandidates
|
|
182
|
+
.filter((row) => parseStrictUtcTimestamp(row.synced_at, "branch_backfill_work_item_timestamp_invalid") <= parseStrictUtcTimestamp(evidence.asOf, "branch_backfill_as_of_invalid"))
|
|
183
|
+
.slice(0, limit + 1);
|
|
184
|
+
const eligibleOverflow = candidates.length > limit;
|
|
185
|
+
const page = eligibleOverflow ? candidates.slice(0, limit) : candidates;
|
|
186
|
+
const results = await mapWithConcurrency(page, concurrency, async (row) => {
|
|
187
|
+
try {
|
|
188
|
+
const result = await client.getPRHeadKey(row.repo_owner, row.repo_name, row.number);
|
|
189
|
+
return result.ok
|
|
190
|
+
? result
|
|
191
|
+
: { ok: false, failureReason: "GITHUB_READ_FAILED" };
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { ok: false, failureReason: "GITHUB_READ_THREW" };
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const insertKey = db.prepare(`
|
|
198
|
+
INSERT OR IGNORE INTO work_item_branch_keys
|
|
199
|
+
(work_item_id, head_ref_key, normalization_version, synced_at)
|
|
200
|
+
VALUES (?, ?, 'branch-v1', ?)
|
|
201
|
+
`);
|
|
202
|
+
let keyed = 0;
|
|
203
|
+
let ineligible = 0;
|
|
204
|
+
let failed = 0;
|
|
205
|
+
const classifications = {
|
|
206
|
+
KEYED: 0,
|
|
207
|
+
INELIGIBLE: 0,
|
|
208
|
+
FETCH_FAILED: 0,
|
|
209
|
+
};
|
|
210
|
+
const failureReasonCounts = {
|
|
211
|
+
GITHUB_READ_FAILED: 0,
|
|
212
|
+
GITHUB_READ_THREW: 0,
|
|
213
|
+
};
|
|
214
|
+
const commitClassifications = db.transaction(() => {
|
|
215
|
+
for (let i = 0; i < page.length; i++) {
|
|
216
|
+
const result = results[i];
|
|
217
|
+
if (result?.ok !== true) {
|
|
218
|
+
failed += 1;
|
|
219
|
+
classifications.FETCH_FAILED += 1;
|
|
220
|
+
const reason = result?.failureReason ?? "GITHUB_READ_THREW";
|
|
221
|
+
failureReasonCounts[reason] = (failureReasonCounts[reason] ?? 0) + 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (result.data === null) {
|
|
225
|
+
ineligible += 1;
|
|
226
|
+
classifications.INELIGIBLE += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const info = insertKey.run(page[i]?.work_item_id, result.data, evidence?.syncedAt ?? new Date().toISOString());
|
|
230
|
+
if (info.changes === 1) {
|
|
231
|
+
keyed += 1;
|
|
232
|
+
classifications.KEYED += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
commitClassifications();
|
|
237
|
+
const scanMayContinue = evidence !== undefined && queriedCandidates.length === EVIDENCE_SCAN_CHUNK;
|
|
238
|
+
const checkpointCursor = eligibleOverflow
|
|
239
|
+
? (page.at(-1)?.work_item_id ?? null)
|
|
240
|
+
: (queriedCandidates.at(-1)?.work_item_id ?? null);
|
|
241
|
+
const nextCursor = eligibleOverflow || scanMayContinue ? checkpointCursor : null;
|
|
242
|
+
if (evidence?.onPageCheckpoint !== undefined) {
|
|
243
|
+
await evidence.onPageCheckpoint({
|
|
244
|
+
afterWorkItemId: checkpointCursor,
|
|
245
|
+
scanned: queriedCandidates.length,
|
|
246
|
+
selected: page.length,
|
|
247
|
+
keyed,
|
|
248
|
+
ineligible,
|
|
249
|
+
failed,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
selected: page.length,
|
|
254
|
+
keyed,
|
|
255
|
+
ineligible,
|
|
256
|
+
failed,
|
|
257
|
+
missing: ineligible + failed,
|
|
258
|
+
nextCursor,
|
|
259
|
+
...(evidence === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: { scanned: queriedCandidates.length, classifications, failureReasonCounts }),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Sync all mapped workspaces (those with repo_owner and repo_name set).
|
|
266
|
+
*/
|
|
267
|
+
export async function syncAllWorkspaces(db, client) {
|
|
268
|
+
if (!client.enabled)
|
|
269
|
+
return;
|
|
270
|
+
const workspaces = db
|
|
271
|
+
.prepare("SELECT workspace_id, repo_owner, repo_name FROM workspaces WHERE repo_owner IS NOT NULL AND repo_name IS NOT NULL")
|
|
272
|
+
.all();
|
|
273
|
+
for (const ws of workspaces) {
|
|
274
|
+
await syncWorkItems(db, client, ws);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Live Claude Code agent measurements and confirm-gated session termination. */
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { capWeightExprSql, resolveCapReadCoeff } from "../cap-weighted.js";
|
|
4
|
+
import { getQueryDb } from "../db-context.js";
|
|
5
|
+
import { buildResponse } from "../envelope.js";
|
|
6
|
+
const LIVENESS_UNAVAILABLE = "liveness unknown: Claude Code CLI not found or too old";
|
|
7
|
+
const CACHE_MS = 30_000;
|
|
8
|
+
let cached = null;
|
|
9
|
+
function stringValue(value) {
|
|
10
|
+
return typeof value === "string" ? value : "";
|
|
11
|
+
}
|
|
12
|
+
function unavailable() {
|
|
13
|
+
const result = {
|
|
14
|
+
available: false,
|
|
15
|
+
reason: LIVENESS_UNAVAILABLE,
|
|
16
|
+
agents: [],
|
|
17
|
+
};
|
|
18
|
+
return buildResponse(result, { claim_kind: "OBS_PROXY", n: result.agents.length });
|
|
19
|
+
}
|
|
20
|
+
function runAgentsCommand() {
|
|
21
|
+
const childEnv = { ...process.env };
|
|
22
|
+
// biome-ignore lint/performance/noDelete: the child must not inherit Claude's nesting guard.
|
|
23
|
+
delete childEnv.CLAUDECODE;
|
|
24
|
+
for (const key of Object.keys(childEnv)) {
|
|
25
|
+
if (key.startsWith("CLAUDE_CODE_"))
|
|
26
|
+
delete childEnv[key];
|
|
27
|
+
}
|
|
28
|
+
let child;
|
|
29
|
+
try {
|
|
30
|
+
child = spawn("claude", ["agents", "--json"], {
|
|
31
|
+
env: childEnv,
|
|
32
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return Promise.resolve(null);
|
|
37
|
+
}
|
|
38
|
+
const stdoutStream = child.stdout;
|
|
39
|
+
if (stdoutStream === null)
|
|
40
|
+
return Promise.resolve(null);
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let stdout = "";
|
|
43
|
+
let settled = false;
|
|
44
|
+
const settle = (value) => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
resolve(value);
|
|
50
|
+
};
|
|
51
|
+
const timeout = setTimeout(() => {
|
|
52
|
+
child.kill();
|
|
53
|
+
settle(null);
|
|
54
|
+
}, 5_000);
|
|
55
|
+
stdoutStream.on("data", (chunk) => {
|
|
56
|
+
stdout += chunk.toString("utf8");
|
|
57
|
+
});
|
|
58
|
+
child.once("error", () => settle(null));
|
|
59
|
+
child.once("close", (code) => settle(code === 0 ? stdout : null));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function isAlive(pid) {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function isProcessNotFound(error) {
|
|
67
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
68
|
+
}
|
|
69
|
+
function pause(milliseconds) {
|
|
70
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
71
|
+
}
|
|
72
|
+
/** End an agent process only after an explicit user confirmation. */
|
|
73
|
+
export function endSession(pid, confirm) {
|
|
74
|
+
if (confirm !== true)
|
|
75
|
+
return { ok: false, reason: "confirmation required", status: 400 };
|
|
76
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
77
|
+
return { ok: false, reason: "invalid pid", status: 400 };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
isAlive(pid);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (isProcessNotFound(error))
|
|
84
|
+
return { ok: false, reason: "process not found", status: 404 };
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: error instanceof Error ? error.message : "unable to probe process",
|
|
88
|
+
status: 500,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (process.platform === "win32") {
|
|
93
|
+
const result = spawnSync("taskkill", ["/pid", String(pid), "/t", "/f"], {
|
|
94
|
+
stdio: "ignore",
|
|
95
|
+
});
|
|
96
|
+
if (result.error)
|
|
97
|
+
throw result.error;
|
|
98
|
+
if (result.status !== 0)
|
|
99
|
+
throw new Error("taskkill failed");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
process.kill(pid, "SIGTERM");
|
|
103
|
+
pause(100);
|
|
104
|
+
try {
|
|
105
|
+
isAlive(pid);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (isProcessNotFound(error))
|
|
109
|
+
return { ok: true, ended: pid, status: 200 };
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
process.kill(pid, "SIGKILL");
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, ended: pid, status: 200 };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: error instanceof Error ? error.message : "failed to end process",
|
|
120
|
+
status: 500,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Return active Claude Code agents enriched with local, read-only usage measurements. */
|
|
125
|
+
export async function getAgentsLiveness() {
|
|
126
|
+
if (cached !== null && Date.now() - cached.at < CACHE_MS)
|
|
127
|
+
return cached.result;
|
|
128
|
+
const stdout = await runAgentsCommand();
|
|
129
|
+
if (stdout === null)
|
|
130
|
+
return unavailable();
|
|
131
|
+
let rawAgents;
|
|
132
|
+
try {
|
|
133
|
+
rawAgents = JSON.parse(stdout);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return unavailable();
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(rawAgents))
|
|
139
|
+
return unavailable();
|
|
140
|
+
try {
|
|
141
|
+
const db = getQueryDb();
|
|
142
|
+
const expr = capWeightExprSql("turns", resolveCapReadCoeff(db));
|
|
143
|
+
const usage = db.prepare(`SELECT MAX(workspace_id) AS workspace_id,
|
|
144
|
+
MAX(ts) AS last_activity_ts,
|
|
145
|
+
COALESCE(SUM(${expr}), 0) AS cap_weighted_raw
|
|
146
|
+
FROM turns
|
|
147
|
+
WHERE session_id = ?`);
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const agents = [];
|
|
150
|
+
for (const rawAgent of rawAgents) {
|
|
151
|
+
if (typeof rawAgent !== "object" || rawAgent === null)
|
|
152
|
+
continue;
|
|
153
|
+
const agent = rawAgent;
|
|
154
|
+
const sessionId = stringValue(agent.sessionId);
|
|
155
|
+
if (sessionId.length === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const usageRow = usage.get(sessionId);
|
|
158
|
+
const lastActivityMs = Date.parse(usageRow.last_activity_ts ?? "");
|
|
159
|
+
agents.push({
|
|
160
|
+
session_id: sessionId,
|
|
161
|
+
pid: typeof agent.pid === "number" && Number.isInteger(agent.pid) ? agent.pid : null,
|
|
162
|
+
workspace_id: usageRow.workspace_id,
|
|
163
|
+
cwd: stringValue(agent.cwd),
|
|
164
|
+
kind: stringValue(agent.kind),
|
|
165
|
+
status: stringValue(agent.status) || stringValue(agent.state),
|
|
166
|
+
name: stringValue(agent.name),
|
|
167
|
+
started_at: stringValue(agent.startedAt),
|
|
168
|
+
idle_seconds: Number.isFinite(lastActivityMs)
|
|
169
|
+
? Math.floor(Math.max(0, now - lastActivityMs) / 1_000)
|
|
170
|
+
: 0,
|
|
171
|
+
cap_weighted_context_held: Math.round(usageRow.cap_weighted_raw ?? 0),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const result = { available: true, reason: null, agents };
|
|
175
|
+
const response = buildResponse(result, { claim_kind: "OBS_PROXY", n: result.agents.length });
|
|
176
|
+
cached = { at: now, result: response };
|
|
177
|
+
return response;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return unavailable();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** OAuth-backed Claude Code burn status for the local dashboard. */
|
|
2
|
+
import { fetchOAuthUsage } from "../../oauth/usage.js";
|
|
3
|
+
import { getQueryDb } from "../db-context.js";
|
|
4
|
+
import { buildResponse } from "../envelope.js";
|
|
5
|
+
/**
|
|
6
|
+
* Persist the latest per-model utilization snapshot to user_config so the D4
|
|
7
|
+
* detector can read it synchronously (async→sync bridge, R12 calibration
|
|
8
|
+
* pattern). The dashboard's recurring GET /api/burn-status poll IS the recurring
|
|
9
|
+
* write. Never throws — a persist failure must not fail the burn-status response,
|
|
10
|
+
* and we only overwrite when live per-model data is present (absence leaves the
|
|
11
|
+
* last-known snapshot in place; D4's 24h staleness bound handles the rest).
|
|
12
|
+
*/
|
|
13
|
+
function persistPerModelSnapshot(five_hour, seven_day, per_model) {
|
|
14
|
+
try {
|
|
15
|
+
const snapshot = JSON.stringify({
|
|
16
|
+
captured_at: new Date().toISOString(),
|
|
17
|
+
seven_day_util: seven_day.utilization,
|
|
18
|
+
five_hour_util: five_hour.utilization,
|
|
19
|
+
per_model,
|
|
20
|
+
});
|
|
21
|
+
getQueryDb()
|
|
22
|
+
.prepare(`INSERT INTO user_config (key, value, updated_at) VALUES ('per_model_snapshot', ?, ?)
|
|
23
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
24
|
+
.run(snapshot, new Date().toISOString());
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best-effort: a persist failure never fails the response.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function getBurnStatus(reader = fetchOAuthUsage) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await reader();
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
return buildResponse({ available: false, reason: result.reason }, { claim_kind: "OBS_PROXY", n: 0 });
|
|
35
|
+
}
|
|
36
|
+
const { five_hour, seven_day, per_model } = result.data;
|
|
37
|
+
if (per_model !== undefined && per_model.length > 0) {
|
|
38
|
+
persistPerModelSnapshot(five_hour, seven_day, per_model);
|
|
39
|
+
}
|
|
40
|
+
return buildResponse({
|
|
41
|
+
available: true,
|
|
42
|
+
five_hour,
|
|
43
|
+
seven_day,
|
|
44
|
+
...(per_model !== undefined && per_model.length > 0 ? { per_model } : {}),
|
|
45
|
+
}, { claim_kind: "OBS_PROXY", n: 1 });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return buildResponse({ available: false, reason: "OAuth usage is unavailable." }, { claim_kind: "OBS_PROXY", n: 0 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Fail-open context-budget measurement for the local hook. */
|
|
2
|
+
import { getQueryDb } from "../db-context.js";
|
|
3
|
+
import { buildResponse } from "../envelope.js";
|
|
4
|
+
import { DEFAULT_HOOK_CONFIG, readHookConfig } from "./hook-config.js";
|
|
5
|
+
const STANDARD_WINDOW = 200_000;
|
|
6
|
+
const LARGE_WINDOW = 1_000_000;
|
|
7
|
+
/**
|
|
8
|
+
* The transcript records only a base model id (e.g. `claude-opus-4-8`), never the
|
|
9
|
+
* context-window variant, so the true window is unknowable from the data alone. We take
|
|
10
|
+
* the user-declared window and floor it to a tier at least as large as the biggest context
|
|
11
|
+
* the session has actually reached — a 200k-window model can never exceed 200k tokens, so a
|
|
12
|
+
* session that has is provably a larger-window variant. This prevents false "near the limit"
|
|
13
|
+
* warnings when the declared window is left at the standard default on a large-window model.
|
|
14
|
+
*/
|
|
15
|
+
function effectiveWindow(declaredWindow, maxObserved) {
|
|
16
|
+
let floor;
|
|
17
|
+
if (maxObserved > LARGE_WINDOW)
|
|
18
|
+
floor = maxObserved;
|
|
19
|
+
else if (maxObserved > STANDARD_WINDOW)
|
|
20
|
+
floor = LARGE_WINDOW;
|
|
21
|
+
else
|
|
22
|
+
floor = STANDARD_WINDOW;
|
|
23
|
+
return Math.max(declaredWindow, floor);
|
|
24
|
+
}
|
|
25
|
+
function okBudget(sessionId, reason, config) {
|
|
26
|
+
const window = config.context_window;
|
|
27
|
+
return {
|
|
28
|
+
stage: "ok",
|
|
29
|
+
context_tokens: 0,
|
|
30
|
+
soft_at: Math.round(config.soft_pct * window),
|
|
31
|
+
hard_at: Math.round(config.hard_pct * window),
|
|
32
|
+
window,
|
|
33
|
+
usage_pct: 0,
|
|
34
|
+
model: null,
|
|
35
|
+
recommended_action: "compact",
|
|
36
|
+
reason,
|
|
37
|
+
ts: null,
|
|
38
|
+
stale_s: null,
|
|
39
|
+
session_id: sessionId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function response(data) {
|
|
43
|
+
return buildResponse(data, {
|
|
44
|
+
claim_kind: "OBS_PROXY",
|
|
45
|
+
n: data.ts === null ? 0 : 1,
|
|
46
|
+
drilldown_ids: { session_id: data.session_id },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Return a strictly fail-open budget result. This endpoint exposes only session
|
|
51
|
+
* identifiers and measured numbers; it never reads or returns transcript text.
|
|
52
|
+
*/
|
|
53
|
+
export function getContextBudget(sessionId) {
|
|
54
|
+
try {
|
|
55
|
+
const db = getQueryDb();
|
|
56
|
+
const config = readHookConfig(db);
|
|
57
|
+
const latest = db
|
|
58
|
+
.prepare(`SELECT ts, model, context_tokens
|
|
59
|
+
FROM turns
|
|
60
|
+
WHERE session_id = ?
|
|
61
|
+
ORDER BY ts DESC, rowid DESC
|
|
62
|
+
LIMIT 1`)
|
|
63
|
+
.get(sessionId);
|
|
64
|
+
if (latest === undefined) {
|
|
65
|
+
const session = db
|
|
66
|
+
.prepare("SELECT 1 AS present FROM sessions WHERE session_id = ? LIMIT 1")
|
|
67
|
+
.get(sessionId);
|
|
68
|
+
return response(okBudget(sessionId, session === undefined ? "unknown_session" : "no_turns", config));
|
|
69
|
+
}
|
|
70
|
+
const timestamp = Date.parse(latest.ts);
|
|
71
|
+
if (!Number.isFinite(timestamp)) {
|
|
72
|
+
return response(okBudget(sessionId, "invalid_timestamp", config));
|
|
73
|
+
}
|
|
74
|
+
const staleSeconds = Math.max(0, (Date.now() - timestamp) / 1_000);
|
|
75
|
+
const maxRow = db
|
|
76
|
+
.prepare(`SELECT COALESCE(MAX(context_tokens), 0) AS max_ctx
|
|
77
|
+
FROM turns
|
|
78
|
+
WHERE session_id = ?`)
|
|
79
|
+
.get(sessionId);
|
|
80
|
+
const maxObserved = Math.max(0, maxRow.max_ctx ?? 0);
|
|
81
|
+
const window = effectiveWindow(config.context_window, maxObserved);
|
|
82
|
+
const softAt = Math.round(config.soft_pct * window);
|
|
83
|
+
const hardAt = Math.round(config.hard_pct * window);
|
|
84
|
+
if (staleSeconds > config.stale_s) {
|
|
85
|
+
const stale = okBudget(sessionId, "stale", config);
|
|
86
|
+
stale.soft_at = softAt;
|
|
87
|
+
stale.hard_at = hardAt;
|
|
88
|
+
stale.window = window;
|
|
89
|
+
stale.ts = latest.ts;
|
|
90
|
+
stale.model = latest.model;
|
|
91
|
+
stale.stale_s = staleSeconds;
|
|
92
|
+
return response(stale);
|
|
93
|
+
}
|
|
94
|
+
const contextTokens = Math.max(0, latest.context_tokens ?? 0);
|
|
95
|
+
const stage = contextTokens >= hardAt ? "hard" : contextTokens >= softAt ? "soft" : "ok";
|
|
96
|
+
return response({
|
|
97
|
+
stage,
|
|
98
|
+
context_tokens: contextTokens,
|
|
99
|
+
soft_at: softAt,
|
|
100
|
+
hard_at: hardAt,
|
|
101
|
+
window,
|
|
102
|
+
usage_pct: contextTokens / window,
|
|
103
|
+
model: latest.model,
|
|
104
|
+
recommended_action: stage === "hard" ? "clear" : "compact",
|
|
105
|
+
reason: stage,
|
|
106
|
+
ts: latest.ts,
|
|
107
|
+
stale_s: staleSeconds,
|
|
108
|
+
session_id: sessionId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return response(okBudget(sessionId, "unavailable", DEFAULT_HOOK_CONFIG));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace context composition. v1 intentionally attributes only the
|
|
3
|
+
* always-loaded local files we can measure; tool output stays in the residual.
|
|
4
|
+
*/
|
|
5
|
+
import { getQueryDb } from "../db-context.js";
|
|
6
|
+
import { buildResponse } from "../envelope.js";
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Compare current CLAUDE.md/MEMORY inventory with the inclusive provisional
|
|
10
|
+
* seven-day context average. This deliberately never reads tool_result_bytes:
|
|
11
|
+
* it is byte-sized and v1 leaves tool output lumped in the residual.
|
|
12
|
+
*/
|
|
13
|
+
export function getContextComposition(workspaceId) {
|
|
14
|
+
const db = getQueryDb();
|
|
15
|
+
const to = new Date();
|
|
16
|
+
const from = new Date(to.getTime() - 7 * DAY_MS);
|
|
17
|
+
const fromIso = from.toISOString();
|
|
18
|
+
const toIso = to.toISOString();
|
|
19
|
+
const inventory = db
|
|
20
|
+
.prepare(`SELECT COUNT(*) AS inventory_rows, COALESCE(SUM(tokens), 0) AS always_loaded
|
|
21
|
+
FROM context_inventory
|
|
22
|
+
WHERE workspace_id IN (?, '__global__')
|
|
23
|
+
AND component IN ('CLAUDE_MD', 'MEMORY')`)
|
|
24
|
+
.get(workspaceId);
|
|
25
|
+
const observed = db
|
|
26
|
+
.prepare(`SELECT COUNT(*) AS observed_turns, AVG(context_tokens) AS observed_context_tokens
|
|
27
|
+
FROM turns
|
|
28
|
+
WHERE workspace_id = ? AND ts >= ? AND ts < ?`)
|
|
29
|
+
.get(workspaceId, fromIso, toIso);
|
|
30
|
+
const alwaysLoaded = Math.max(inventory.always_loaded, 0);
|
|
31
|
+
const observedContext = observed.observed_context_tokens;
|
|
32
|
+
const residual = observedContext === null ? 0 : Math.max(observedContext - alwaysLoaded, 0);
|
|
33
|
+
const total = observedContext === null ? 0 : alwaysLoaded + residual;
|
|
34
|
+
const share = (tokens) => (total > 0 ? tokens / total : null);
|
|
35
|
+
const rows = [
|
|
36
|
+
{
|
|
37
|
+
key: "always_loaded",
|
|
38
|
+
label: "always loaded",
|
|
39
|
+
tokens: alwaysLoaded,
|
|
40
|
+
share: share(alwaysLoaded),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "session_residual",
|
|
44
|
+
label: "session history + tool outputs (not itemized in v1)",
|
|
45
|
+
tokens: residual,
|
|
46
|
+
share: share(residual),
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
return buildResponse({
|
|
50
|
+
workspace_id: workspaceId,
|
|
51
|
+
observed_context_tokens: observedContext,
|
|
52
|
+
observed_turns: observed.observed_turns,
|
|
53
|
+
inventory_rows: inventory.inventory_rows,
|
|
54
|
+
rows,
|
|
55
|
+
}, {
|
|
56
|
+
claim_kind: "OBS_PROXY",
|
|
57
|
+
n: observed.observed_turns,
|
|
58
|
+
window: { from: fromIso, to: toIso, preset: "7d" },
|
|
59
|
+
qualification: {
|
|
60
|
+
provisional_excluded: false,
|
|
61
|
+
unpriced_turns: 0,
|
|
62
|
+
claim_kinds_count: 1,
|
|
63
|
+
note: "Provisional turns are included. v1 uses the current estimated CLAUDE.md and MEMORY inventory (±5–10% tokenizer error), excludes the system prompt and dynamic MCP schemas, and does not itemize the residual.",
|
|
64
|
+
},
|
|
65
|
+
drilldown_ids: { workspace_id: workspaceId },
|
|
66
|
+
});
|
|
67
|
+
}
|