agentwrangler 0.1.0 → 0.1.1-next.2e8c141
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/README.md +152 -84
- package/dist/daemon/index.js +9 -3
- package/dist/daemon/readiness.js +26 -1
- package/dist/daemon/router.js +3 -1
- package/dist/db/migrations/016_ingest_offset_file_version.sql +8 -0
- package/dist/db/migrations/017_ingest_metric_events.sql +22 -0
- package/dist/detector/measurement.js +3 -2
- package/dist/hook/precompact-checkpoint-hook.mjs +51 -15
- package/dist/ingest/ingestor.js +151 -62
- package/dist/ingest/pricing.js +15 -0
- package/dist/ingest/tail.js +55 -11
- package/dist/query/api/outcomes.js +2 -1
- package/dist/query/api/overview.js +13 -3
- package/dist/query/settings-store.js +2 -0
- package/dist/ui/assets/BarChart-BwFZrcLn.js +3 -0
- package/dist/ui/assets/BriefsPage-CtJwHLOh.js +2 -0
- package/dist/ui/assets/CacheWriteSpikesChart-zJIJWO41.js +1 -0
- package/dist/ui/assets/CartesianChart-qApXl3Ak.js +84 -0
- package/dist/ui/assets/Chip-Cpt_fEc9.js +1 -0
- package/dist/ui/assets/ComposedChart-DYf1wymW.js +1 -0
- package/dist/ui/assets/EmptyState-DL8gAVnJ.js +1 -0
- package/dist/ui/assets/FlavorDecomposition-CvJW78M9.js +1 -0
- package/dist/ui/assets/FrictionCell-DciiSd3L.js +2 -0
- package/dist/ui/assets/GlossaryPage-BMkZGt2D.js +1 -0
- package/dist/ui/assets/HotSessionsPage-Ejn385bb.js +1 -0
- package/dist/ui/assets/InfoTip-Ckc9_LpJ.js +1 -0
- package/dist/ui/assets/Legend-C_YMp0Rv.js +5 -0
- package/dist/ui/assets/Line-DU4UvEBO.js +1 -0
- package/dist/ui/assets/OverviewPage-BcEKdWZj.js +2 -0
- package/dist/ui/assets/RecommendationsPage-CHfuNP_W.js +2 -0
- package/dist/ui/assets/Scatter-CNetVLuX.js +1 -0
- package/dist/ui/assets/SessionDetailPage-CYm5Gxo0.js +1 -0
- package/dist/ui/assets/SettingsPage-DjgGZj8N.js +25 -0
- package/dist/ui/assets/Skeleton-BO9mXDXU.js +1 -0
- package/dist/ui/assets/SpendPercentileChip-BvaZ8Lzi.js +1 -0
- package/dist/ui/assets/TrendChart-DKy9aH5G.js +1 -0
- package/dist/ui/assets/WorkspaceDetailPage-dutc4EpN.js +1 -0
- package/dist/ui/assets/WorkspacesPage-isSML2fh.js +1 -0
- package/dist/ui/assets/chart-theme-BPPMjbVX.js +1 -0
- package/dist/ui/assets/graphicalItemSelectors-DgKF1Dg2.js +1 -0
- package/dist/ui/assets/index-CUyiomzU.js +41 -0
- package/dist/ui/assets/index-SrfNUeBZ.css +1 -0
- package/dist/ui/assets/prompt-templates-DhR4Qoy9.js +13 -0
- package/dist/ui/assets/useExperimentalActions-BeDSmwBu.js +1 -0
- package/dist/ui/assets/workspace-label-CysPJNc0.js +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +21 -5
- package/dist/ui/assets/index-DnRKgc21.css +0 -1
- package/dist/ui/assets/index-h1Q1wWq5.js +0 -168
package/README.md
CHANGED
|
@@ -1,116 +1,184 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/logo.png" alt="AgentWrangler logo" width="260">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
actually merged or closed** — entirely on your machine — and ships **installable guardrails** (a
|
|
5
|
-
dangerous-command block, a pre-compaction checkpoint, a context-budget warning) you turn on from the
|
|
6
|
-
dashboard. See where your agent budget goes, whether the work shipped, and cut the waste.
|
|
5
|
+
<h1 align="center">AgentWrangler</h1>
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>See where your Claude Code tokens go — and whether the work actually shipped.</b><br>
|
|
9
|
+
Local-first observability for Claude Code: token spend, session outcomes, waste detection,
|
|
10
|
+
and installable guardrails. Runs entirely on your machine.
|
|
11
|
+
</p>
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
**Privacy exception (opt-in):** `npm run evidence:judge-g2 -- --execute` is the only path where PR
|
|
18
|
-
content leaves the machine. It calls the Claude API with your local Claude Code OAuth credential to
|
|
19
|
-
adjudicate G2 deferral findings, and runs only when `g2_claude_judge_opt_in` is enabled; otherwise it
|
|
20
|
-
refuses to run. No rationale text is persisted.
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://www.npmjs.com/package/agentwrangler"><img src="https://img.shields.io/npm/v/agentwrangler" alt="npm version"></a>
|
|
15
|
+
<a href="https://github.com/Doogit/AgentWrangler/actions/workflows/ci.yml"><img src="https://github.com/Doogit/AgentWrangler/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
<a href="https://img.shields.io/node/v/agentwrangler"><img src="https://img.shields.io/node/v/agentwrangler" alt="node version"></a>
|
|
17
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="license"></a>
|
|
18
|
+
</p>
|
|
21
19
|
|
|
22
20
|

|
|
23
21
|
|
|
24
22
|
<!--
|
|
25
|
-
|
|
26
|
-
live data) so no real workspace/repository
|
|
27
|
-
|
|
23
|
+
All screenshots and the demo GIF in this README are captured from a SANITIZED instance
|
|
24
|
+
(Vite test-mode fixtures — anonymized names, no live data) so no real workspace/repository
|
|
25
|
+
names or paths are committed (SEC-101). Regenerate against `npx vite --mode test`.
|
|
28
26
|
-->
|
|
29
27
|
|
|
30
|
-
##
|
|
28
|
+
## Why
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
Claude Code tells you almost nothing about where your token budget goes. Sessions balloon,
|
|
31
|
+
caches miss, background agents idle — and you find out when you hit the rate limit.
|
|
32
|
+
AgentWrangler reads the transcripts Claude Code already writes to your disk and answers three
|
|
33
|
+
questions:
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
npm run daemon # starts the daemon and opens your browser
|
|
39
|
-
```
|
|
35
|
+
- **Where did the tokens go?** Per-model, per-workspace, per-session spend with cache economics.
|
|
36
|
+
- **Did the work ship?** Sessions are linked to the pull requests they merged or closed.
|
|
37
|
+
- **What should I change?** Ranked waste-source detectors with modeled savings — and one-click
|
|
38
|
+
guardrail hooks that warn *inside* Claude Code before waste happens.
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
No cloud backend, no telemetry, no account. A daemon on `127.0.0.1`, a dashboard in your
|
|
41
|
+
browser, and a SQLite file in your home directory.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
## Quick start
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
|
|
47
|
-
npx agentwrangler # builds the UI if needed, then launches the daemon + browser
|
|
46
|
+
npx agentwrangler@latest
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
`~/.claude/projects
|
|
52
|
-
fills in as the scan completes
|
|
49
|
+
That's it — requires Node **22–24**. The daemon starts on `http://127.0.0.1:47821`, opens your
|
|
50
|
+
browser, and scans your `~/.claude/projects` transcripts in the background; the dashboard
|
|
51
|
+
appears immediately and fills in as the scan completes.
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
More options (install from source, GitHub outcomes sync, environment variables):
|
|
54
|
+
**[Getting started →](docs/getting-started.md)**
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
GitHub PAT via the `AW_GITHUB_TOKEN` environment variable (works on all platforms). On Windows you
|
|
58
|
-
may instead store it in Credential Manager as `AgentWrangler-GithubToken`. Without a token the
|
|
59
|
-
outcomes feature stays inert and Settings tells you so — nothing fails silently.
|
|
60
|
-
- **Usage reader** — reads your Claude Code OAuth credentials locally to calibrate the weekly limit
|
|
61
|
-
and burn forecast. Sign in through Claude Code as usual; Settings shows the reader status.
|
|
62
|
-
- **Context-budget hook** — an optional PreToolUse hook that warns before long sessions balloon.
|
|
63
|
-
Install it from the Settings page (or `npm run install-hook`).
|
|
56
|
+
## Features
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
### Overview — verdict first, details on demand
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
One screen answers "how bad is it this week": a spend verdict with trend, your top waste
|
|
61
|
+
source with a copyable fix prompt, live rate-limit gauges (5-hour and 7-day), a burn forecast
|
|
62
|
+
against your calibrated weekly limit, hot sessions, cache efficiency, and per-model
|
|
63
|
+
context-per-turn tiles.
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|---|---|---|
|
|
71
|
-
| `AW_PORT` | Daemon HTTP port | `47821` |
|
|
72
|
-
| `AW_DB_PATH` | SQLite database path | `~/.agentwrangler/db.sqlite` |
|
|
73
|
-
| `AW_SCAN_ROOT` | Transcript corpus to scan | `~/.claude/projects` |
|
|
74
|
-
| `AW_UI_ROOT` | Directory the built UI is served from | `<repo>/dist/ui` |
|
|
75
|
-
| `AW_GITHUB_TOKEN` | Read-only GitHub PAT for outcomes sync | *(unset)* |
|
|
76
|
-
| `AW_NO_OPEN` | Set to `1` to not auto-open the browser | *(unset)* |
|
|
65
|
+

|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
### Recommendations — waste-source detectors, ranked by impact
|
|
79
68
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
Ten detector families watch your sessions for the patterns that actually burn tokens: cache
|
|
70
|
+
misses (the biggest single lever), session hygiene, retry/redundant-read loops, tool-result
|
|
71
|
+
bloat, model routing, idle background sessions, and more. Each recommendation shows modeled
|
|
72
|
+
weekly savings, a confidence tier, and a concrete action — install a hook, copy a config
|
|
73
|
+
snippet, or copy a guided prompt straight into Claude Code. Adopted changes flow into an
|
|
74
|
+
**impact ledger** that tracks the measured effect, and modeled savings are never counted as
|
|
75
|
+
achieved.
|
|
85
76
|
|
|
86
|
-
|
|
77
|
+

|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
- **Transcript-format coupling.** AgentWrangler reads Claude Code's JSONL transcript format; a change
|
|
90
|
-
to that format upstream can require an ingestion update.
|
|
91
|
-
- **Outcomes credential sources.** The cross-platform path is `AW_GITHUB_TOKEN`; the OS credential
|
|
92
|
-
store integration currently covers Windows Credential Manager only (macOS/Linux keychain is a
|
|
93
|
-
follow-on).
|
|
94
|
-
- **Single local user.** It observes one machine's Claude Code history; there is no multi-user or
|
|
95
|
-
team-aggregation mode by design.
|
|
79
|
+
### Installable guardrails — local checks inside Claude Code, before the waste
|
|
96
80
|
|
|
97
|
-
|
|
81
|
+
Five small hooks you can install from the dashboard (directly, or via a copyable prompt that
|
|
82
|
+
Claude Code applies itself):
|
|
83
|
+
|
|
84
|
+
| Guardrail | What it does |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **Context-budget warning** | Warns when a session's context crosses your soft/hard thresholds |
|
|
87
|
+
| **Loop guard** | Flags repeated identical tool failures before they spiral |
|
|
88
|
+
| **Burn alert** | Catches idle sessions still burning tokens in the background |
|
|
89
|
+
| **Pre-compaction checkpoint** | Copies the raw local transcript before an automatic compaction, subject to a local retention cap |
|
|
90
|
+
| **Dangerous-command guard** | Asks before risky shell commands and denies a small catastrophe list |
|
|
91
|
+
|
|
92
|
+
The context-budget and burn hooks warn. The loop guard warns before it denies repeated identical
|
|
93
|
+
failures, and the dangerous-command guard can ask or deny. Direct install enables all five hooks;
|
|
94
|
+
the copied install prompt enables the context-budget, loop, and burn hooks only. Thresholds are
|
|
95
|
+
tunable from Settings, and direct uninstall removes every AgentWrangler hook.
|
|
96
|
+
|
|
97
|
+
### Sessions — who spent it, and on what
|
|
98
|
+
|
|
99
|
+
The highest-cost sessions ranked with their output-to-context split, model, friction band
|
|
100
|
+
(API errors, tool failures, compactions, interrupts), and a "top X% by spend" self-percentile
|
|
101
|
+
chip. Drill into any session for a turn-by-turn timeline, its cost drivers (which detectors
|
|
102
|
+
fired and how hard), and a guided fix prompt built only from measured numbers.
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary>Session detail view</summary>
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
### Workspaces — spend efficiency by repository
|
|
113
|
+
|
|
114
|
+
Every repo you run Claude Code in, with spend share, trend, context-per-turn, cache-write
|
|
115
|
+
share, Opus share, and $/turn. With a GitHub token configured, sessions are linked to the PRs
|
|
116
|
+
and commits they produced — so you can see cost-per-merged-PR, not just cost.
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+

|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
<details>
|
|
121
|
+
<summary>Workspace detail view</summary>
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+

|
|
124
|
+
</details>
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
### Weekly brief — one page, three decisions
|
|
127
|
+
|
|
128
|
+
The week in one screen: spend verdict, what changed vs. last week, the top actions to take —
|
|
129
|
+
with a **Copy as Markdown** button so the whole brief drops into a standup note or a message.
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
### Honest numbers, labeled as such
|
|
134
|
+
|
|
135
|
+
Every metric carries an honesty-tier chip — `EXACT`, `LIST_EQUIV`, `MODELED`, `PROXY`,
|
|
136
|
+
`DIRECTIONAL`, `EXPERIMENTAL` — so you always know what is measured versus estimated. Dollar
|
|
137
|
+
figures are list-price *equivalents* (subscription plans aren't billed per token; tokens drive
|
|
138
|
+
rate limits), and the built-in glossary ("How to read this dashboard") defines every
|
|
139
|
+
key metric in plain language. The full tour: **[Dashboard tour →](docs/dashboard-tour.md)**
|
|
140
|
+
|
|
141
|
+
## Privacy — local-only by design
|
|
142
|
+
|
|
143
|
+
- The daemon binds to **`127.0.0.1`** only. There is no cloud backend, telemetry, or account.
|
|
144
|
+
- Most dashboard data is **aggregates, ids, counts, and structural anchors**. Local command text
|
|
145
|
+
and filesystem paths can also be retained in SQLite; treat the database as sensitive. The optional
|
|
146
|
+
direct-install PreCompact hook can separately copy full raw transcripts locally.
|
|
147
|
+
- The optional GitHub token is read locally, never logged, never persisted to the DB.
|
|
148
|
+
- Usage refreshes can call Anthropic with an existing Claude Code sign-in. GitHub outcomes sync
|
|
149
|
+
calls GitHub only when a token is configured; calibration and G2 judging are separate opt-ins.
|
|
150
|
+
|
|
151
|
+
Full details, including exactly what is and isn't stored: **[Privacy model →](docs/privacy.md)**
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
Everything is optional with sensible defaults — port, DB path, scan roots, GitHub token, and
|
|
156
|
+
more are environment variables documented in [Getting started](docs/getting-started.md#configuration)
|
|
157
|
+
and [`.env.example`](.env.example).
|
|
158
|
+
|
|
159
|
+
## Documentation
|
|
160
|
+
|
|
161
|
+
| Page | What's in it |
|
|
107
162
|
|---|---|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
116
|
-
|
|
163
|
+
| [Getting started](docs/getting-started.md) | Install paths, optional setup, configuration, troubleshooting |
|
|
164
|
+
| [Dashboard tour](docs/dashboard-tour.md) | Every tab in depth, plus the metric vocabulary |
|
|
165
|
+
| [Privacy model](docs/privacy.md) | Local storage, raw-checkpoint exception, and network integrations |
|
|
166
|
+
| [Architecture](docs/planning/AgentWrangler_Technical_Architecture_v4_5_0.md) | Daemon, ingestion, detector, and query design |
|
|
167
|
+
| [Data model & metrics](docs/planning/AgentWrangler_Data_Model_and_Metrics_v2.md) | SQLite schema and metric definitions |
|
|
168
|
+
| [Contributing](.github/CONTRIBUTING.md) | Dev setup, checks, PR expectations |
|
|
169
|
+
| [Security policy](.github/SECURITY.md) | Threat model and how to report a vulnerability |
|
|
170
|
+
|
|
171
|
+
## Limitations
|
|
172
|
+
|
|
173
|
+
- Reads Claude Code's JSONL transcript format; an upstream format change can require an
|
|
174
|
+
ingestion update.
|
|
175
|
+
- Tested on Windows; macOS/Linux are believed working — reports welcome.
|
|
176
|
+
- Single local user by design — no multi-user or team-aggregation mode.
|
|
177
|
+
- Outcome linkage needs a read-only GitHub token; without one the feature stays inert (and
|
|
178
|
+
Settings says so — nothing fails silently).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[Apache 2.0](LICENSE) © 2026 AgentWrangler contributors.
|
|
183
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md), [SECURITY.md](.github/SECURITY.md), and
|
|
184
|
+
[CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md).
|
package/dist/daemon/index.js
CHANGED
|
@@ -35,11 +35,11 @@ import { linkSessions } from "../outcomes/linker.js";
|
|
|
35
35
|
import { syncAllWorkspaces } from "../outcomes/sync.js";
|
|
36
36
|
import { generateWeeklyReport } from "../query/api/reports.js";
|
|
37
37
|
import { setQueryDb } from "../query/db-context.js";
|
|
38
|
-
import { setHealthInstance, setRuntimeResetHook } from "../query/settings-store.js";
|
|
38
|
+
import { getSettingsData, setHealthInstance, setRuntimeResetHook, } from "../query/settings-store.js";
|
|
39
39
|
import { loadConfig } from "./config.js";
|
|
40
40
|
import { createServer } from "./http.js";
|
|
41
41
|
import { createOutcomesPassRunner } from "./outcomes-pass.js";
|
|
42
|
-
import { setReady } from "./readiness.js";
|
|
42
|
+
import { setReady, setScanRoots, setScanState } from "./readiness.js";
|
|
43
43
|
const VERSION = "0.1.0";
|
|
44
44
|
/** Parse a named CLI argument: --name value or --name=value. */
|
|
45
45
|
function getArg(name) {
|
|
@@ -110,6 +110,8 @@ if (isSmoke) {
|
|
|
110
110
|
"apply_jobs",
|
|
111
111
|
"analysis_runs",
|
|
112
112
|
"ingest_quarantine",
|
|
113
|
+
"ingest_metric_events",
|
|
114
|
+
"ingest_metric_baselines",
|
|
113
115
|
"ingest_offsets",
|
|
114
116
|
"schema_migrations",
|
|
115
117
|
"user_config",
|
|
@@ -208,7 +210,9 @@ function kickBootScan() {
|
|
|
208
210
|
}
|
|
209
211
|
async function runBootScan() {
|
|
210
212
|
try {
|
|
211
|
-
const
|
|
213
|
+
const scanRoots = getSettingsData(db).scan_roots;
|
|
214
|
+
setScanRoots(scanRoots);
|
|
215
|
+
const ingestor = new Ingestor(db, scanRoots, {
|
|
212
216
|
onNewMappings: (count) => {
|
|
213
217
|
console.log(`Discovery mapped ${count} new repo(s) — scheduling outcomes pass`);
|
|
214
218
|
runOutcomesPass().catch((e) => {
|
|
@@ -218,10 +222,12 @@ async function runBootScan() {
|
|
|
218
222
|
});
|
|
219
223
|
setHealthInstance(ingestor.health);
|
|
220
224
|
handle = await ingestor.startTailBatched();
|
|
225
|
+
setScanState("complete");
|
|
221
226
|
setRuntimeResetHook(() => ingestor.clearRuntimeState());
|
|
222
227
|
console.log(`Ingestion: initial scan complete — health ${JSON.stringify(ingestor.healthSnapshot())}`);
|
|
223
228
|
}
|
|
224
229
|
catch (e) {
|
|
230
|
+
setScanState("failed");
|
|
225
231
|
console.error(`Ingestion failed to start — serving dashboard in degraded mode: ${e instanceof Error ? e.message : String(e)}`);
|
|
226
232
|
}
|
|
227
233
|
// After the initial back-scan, workspaces are discovered — re-probe so per-workspace
|
package/dist/daemon/readiness.js
CHANGED
|
@@ -4,7 +4,32 @@
|
|
|
4
4
|
* Set to true by index.ts after the initial back-scan completes.
|
|
5
5
|
* Read by http.ts to decide whether to serve the loading page or the real SPA.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
const state = { ready: false, scan: "scanning", scanRoots: [] };
|
|
9
|
+
/** Snapshot the roots actually used by the running ingestor, not pending settings. */
|
|
10
|
+
export function setScanRoots(roots) {
|
|
11
|
+
state.scanRoots = [...roots];
|
|
12
|
+
}
|
|
13
|
+
export function setScanState(scan) {
|
|
14
|
+
state.scan = scan;
|
|
15
|
+
}
|
|
16
|
+
/** Aggregate-only diagnostics; never exposes a local path or filesystem error. */
|
|
17
|
+
export function getScanStatus() {
|
|
18
|
+
let invalid = 0;
|
|
19
|
+
for (const root of state.scanRoots) {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.statSync(root).isDirectory()) {
|
|
22
|
+
invalid += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
fs.accessSync(root, fs.constants.R_OK);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
invalid += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { scan_state: state.scan, invalid_scan_root_count: invalid };
|
|
32
|
+
}
|
|
8
33
|
/** Returns true after the initial back-scan + detector pass have finished. */
|
|
9
34
|
export function isReady() {
|
|
10
35
|
return state.ready;
|
package/dist/daemon/router.js
CHANGED
|
@@ -26,7 +26,7 @@ import { getSessionSpendPercentile, getWeeklySelfPercentile, } from "../query/ap
|
|
|
26
26
|
import { getFlavorDecomposition } from "../query/api/spend-flavor.js";
|
|
27
27
|
import { getCacheWriteTrend, getTrends } from "../query/api/trends.js";
|
|
28
28
|
import { getSettingsData } from "../query/settings-store.js";
|
|
29
|
-
import { isReady } from "./readiness.js";
|
|
29
|
+
import { getScanStatus, isReady } from "./readiness.js";
|
|
30
30
|
function sendJson(res, status, body) {
|
|
31
31
|
const payload = JSON.stringify(body);
|
|
32
32
|
res.writeHead(status, {
|
|
@@ -741,6 +741,8 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
741
741
|
sessions,
|
|
742
742
|
files_seen: parser_health.files_seen,
|
|
743
743
|
files_parsed: parser_health.files_parsed,
|
|
744
|
+
lines_quarantined: parser_health.lines_quarantined,
|
|
745
|
+
...getScanStatus(),
|
|
744
746
|
});
|
|
745
747
|
return;
|
|
746
748
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Nullable metadata lets existing offsets establish a baseline on their next read.
|
|
2
|
+
-- No transcript content is stored here. Identity detects path replacement; timestamps
|
|
3
|
+
-- detect equal-size in-place rewrites beyond the existing 256-byte head fingerprint.
|
|
4
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_size INTEGER;
|
|
5
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_dev TEXT;
|
|
6
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_ino TEXT;
|
|
7
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_mtime_ms REAL;
|
|
8
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_ctime_ms REAL;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Persist structural metric events so transcript replay cannot inflate session counters.
|
|
2
|
+
-- event_id is a SHA-256 digest computed in-process; transcript content is never stored.
|
|
3
|
+
CREATE TABLE ingest_metric_events (
|
|
4
|
+
event_id TEXT PRIMARY KEY,
|
|
5
|
+
session_id TEXT NOT NULL,
|
|
6
|
+
user_turn_ts TEXT,
|
|
7
|
+
is_user_turn INTEGER NOT NULL CHECK (is_user_turn IN (0, 1)),
|
|
8
|
+
is_compact_summary INTEGER NOT NULL CHECK (is_compact_summary IN (0, 1)),
|
|
9
|
+
is_api_error INTEGER NOT NULL CHECK (is_api_error IN (0, 1)),
|
|
10
|
+
is_interrupt INTEGER NOT NULL CHECK (is_interrupt IN (0, 1))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX idx_ingest_metric_events_session_user_ts
|
|
14
|
+
ON ingest_metric_events(session_id, user_turn_ts)
|
|
15
|
+
WHERE is_user_turn = 1;
|
|
16
|
+
|
|
17
|
+
-- Existing offsets predate the event ledger. Their consumed prefixes are seeded into
|
|
18
|
+
-- the ledger without incrementing counters, then this durable boundary is advanced.
|
|
19
|
+
CREATE TABLE ingest_metric_baselines (
|
|
20
|
+
file_path TEXT PRIMARY KEY,
|
|
21
|
+
seeded_offset INTEGER NOT NULL CHECK (seeded_offset >= 0)
|
|
22
|
+
);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Injected `now: Date` everywhere — never call new Date() in this module.
|
|
22
22
|
*/
|
|
23
|
+
import { PREMIUM_MODEL_SQL } from "../ingest/pricing.js";
|
|
23
24
|
import { GLOBAL_WORKSPACE_ID } from "./context-probe.js";
|
|
24
25
|
import { isD1SourceBackedRecommendation, parseD1SourceIdentity } from "./d1-source-identity.js";
|
|
25
26
|
/** Wait ≥1 day after adoption before entering MEASURING. */
|
|
@@ -82,14 +83,14 @@ function avgFloorContext(db, scopeWorkspaceId, fromIso, toIso) {
|
|
|
82
83
|
/**
|
|
83
84
|
* D4 signal: routing-adherence score = ROUND(100 × (1 − premium_share)) over
|
|
84
85
|
* non-sidechain reconciled turns in [fromIso, toIso), where premium = model
|
|
85
|
-
*
|
|
86
|
+
* matches PREMIUM_MODEL_SQL (Opus/Fable/Mythos). The data-model's "mechanical turn" classifier is not yet
|
|
86
87
|
* shipped, so all reconciled turns are the mechanical set (conservative proxy —
|
|
87
88
|
* documented in the W4 build digest).
|
|
88
89
|
*/
|
|
89
90
|
function routingAdherenceScore(db, scopeWorkspaceId, fromIso, toIso) {
|
|
90
91
|
const scoped = scopeWorkspaceId !== null;
|
|
91
92
|
const sql = `SELECT COUNT(*) AS n,
|
|
92
|
-
AVG(CASE WHEN
|
|
93
|
+
AVG(CASE WHEN ${PREMIUM_MODEL_SQL} THEN 1.0 ELSE 0.0 END) AS premium_share
|
|
93
94
|
FROM turns
|
|
94
95
|
WHERE is_sidechain = 0 AND provisional = 0 AND ts >= ? AND ts < ? ${scoped ? "AND workspace_id = ?" : ""}`;
|
|
95
96
|
const params = scoped ? [fromIso, toIso, scopeWorkspaceId] : [fromIso, toIso];
|
|
@@ -53,6 +53,32 @@ export function writeCheckpoint(transcriptPath, sessionId, dir, now = new Date()
|
|
|
53
53
|
return dest;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Return the timestamp encoded by a snapshot filename, or null for files this hook did
|
|
58
|
+
* not create. Malformed .jsonl files are deliberately left alone by retention.
|
|
59
|
+
*/
|
|
60
|
+
function snapshotTimestamp(name) {
|
|
61
|
+
const match = /-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-\d{6}\.jsonl$/.exec(
|
|
62
|
+
name,
|
|
63
|
+
);
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
const [, year, month, day, hour, minute, second, millisecond] = match;
|
|
66
|
+
const timestamp = Date.parse(
|
|
67
|
+
`${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z`,
|
|
68
|
+
);
|
|
69
|
+
if (Number.isNaN(timestamp)) return null;
|
|
70
|
+
const parsed = new Date(timestamp);
|
|
71
|
+
return parsed.getUTCFullYear() === Number(year) &&
|
|
72
|
+
parsed.getUTCMonth() + 1 === Number(month) &&
|
|
73
|
+
parsed.getUTCDate() === Number(day) &&
|
|
74
|
+
parsed.getUTCHours() === Number(hour) &&
|
|
75
|
+
parsed.getUTCMinutes() === Number(minute) &&
|
|
76
|
+
parsed.getUTCSeconds() === Number(second) &&
|
|
77
|
+
parsed.getUTCMilliseconds() === Number(millisecond)
|
|
78
|
+
? timestamp
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
/** Prune oldest snapshots beyond the count cap, then beyond the total-byte cap. */
|
|
57
83
|
export function enforceRetention(dir, maxCount = MAX_COUNT, maxBytes = MAX_BYTES) {
|
|
58
84
|
let entries;
|
|
@@ -60,30 +86,40 @@ export function enforceRetention(dir, maxCount = MAX_COUNT, maxBytes = MAX_BYTES
|
|
|
60
86
|
entries = fs
|
|
61
87
|
.readdirSync(dir)
|
|
62
88
|
.filter((name) => name.endsWith(".jsonl"))
|
|
63
|
-
.sort() // timestamp-derived names sort chronologically
|
|
64
89
|
.map((name) => {
|
|
65
90
|
const full = path.join(dir, name);
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
const timestamp = snapshotTimestamp(name);
|
|
92
|
+
if (timestamp === null) return null;
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(full);
|
|
95
|
+
if (!stat.isFile()) return null;
|
|
96
|
+
return { full, name, size: stat.size, timestamp, mtimeMs: stat.mtimeMs };
|
|
97
|
+
} catch {
|
|
98
|
+
// A vanished or unreadable entry must not prevent pruning known snapshots.
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.filter((entry) => entry !== null)
|
|
103
|
+
.sort(
|
|
104
|
+
(a, b) =>
|
|
105
|
+
a.timestamp - b.timestamp || a.mtimeMs - b.mtimeMs || a.name.localeCompare(b.name),
|
|
106
|
+
);
|
|
68
107
|
} catch {
|
|
69
108
|
return; // dir missing or unreadable — nothing to prune
|
|
70
109
|
}
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
let count = entries.length;
|
|
112
|
+
let total = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
113
|
+
for (const victim of entries) {
|
|
114
|
+
if (count <= maxCount && total <= maxBytes) break;
|
|
75
115
|
try {
|
|
76
116
|
fs.rmSync(victim.full);
|
|
77
|
-
} catch {
|
|
78
|
-
// A locked
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// A locked snapshot still counts. Try each later candidate at most once.
|
|
119
|
+
if (error?.code !== "ENOENT") continue;
|
|
79
120
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
while (entries.length > maxCount) removeOldest();
|
|
83
|
-
let total = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
84
|
-
while (entries.length > 0 && total > maxBytes) {
|
|
85
|
-
total -= entries[0].size;
|
|
86
|
-
removeOldest();
|
|
121
|
+
count -= 1;
|
|
122
|
+
total -= victim.size;
|
|
87
123
|
}
|
|
88
124
|
}
|
|
89
125
|
|