agentwrangler 0.1.1-next.bca85ea → 0.1.1-next.bdce596
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 +5 -3
- package/dist/daemon/http.js +175 -34
- package/dist/daemon/index.js +44 -81
- package/dist/daemon/lifecycle.js +122 -0
- package/dist/daemon/router.js +339 -9
- package/dist/db/migrations/019_work_records.sql +291 -0
- package/dist/db/migrations/020_local_command_retention.sql +50 -0
- package/dist/db/migrations/021_recommendation_feedback.sql +25 -0
- package/dist/detector/d7-loop-analysis.js +142 -0
- package/dist/detector/detectors/d7_loop_retry_waste.js +14 -127
- package/dist/effects/engine.js +27 -10
- package/dist/effects/index.js +1 -0
- package/dist/effects/registry.js +87 -6
- package/dist/effects/scope-identity.js +19 -0
- package/dist/effects/sql-observer.js +322 -8
- package/dist/ingest/boot-progress.js +76 -0
- package/dist/ingest/ingestor.js +352 -82
- package/dist/ingest/parser.js +19 -3
- package/dist/ingest/tail.js +130 -20
- package/dist/ingest/types.js +1 -1
- package/dist/query/api/decision-ranker-types.js +6 -0
- package/dist/query/api/decision-ranker.js +393 -0
- package/dist/query/api/effect-service.js +2 -0
- package/dist/query/api/esf-observations.js +117 -62
- package/dist/query/api/evidence-packet-types.js +3 -0
- package/dist/query/api/evidence-packet.js +316 -0
- package/dist/query/api/feedback-store.js +286 -0
- package/dist/query/api/index.js +1 -1
- package/dist/query/api/opportunity-composer-types.js +3 -0
- package/dist/query/api/opportunity-composer.js +303 -0
- package/dist/query/api/overview.js +42 -1
- package/dist/query/api/reported-work.js +46 -0
- package/dist/query/api/session-drivers.js +1 -1
- package/dist/query/api/spend-flavor.js +22 -15
- package/dist/query/api/trends.js +86 -6
- package/dist/query/api/work-records.js +85 -0
- package/dist/query/settings-store.js +11 -0
- package/dist/query/spend.js +27 -13
- package/dist/query/trends.js +27 -0
- package/dist/ui/assets/{BarChart-CIjCNfbI.js → BarChart-C9vtAcml.js} +1 -1
- package/dist/ui/assets/{BriefsPage-DIU8iFjV.js → BriefsPage-DLp1pInz.js} +2 -2
- package/dist/ui/assets/{CacheWriteSpikesChart-1Nfnf7OX.js → CacheWriteSpikesChart-BlflM-xW.js} +1 -1
- package/dist/ui/assets/{CartesianChart-DEnDOGKj.js → CartesianChart-0_RUWlzc.js} +1 -1
- package/dist/ui/assets/{Chip-s-32Dh3o.js → Chip-BQ4UlaAL.js} +1 -1
- package/dist/ui/assets/{ComposedChart-DUw8Y79v.js → ComposedChart-CIG9CS0j.js} +1 -1
- package/dist/ui/assets/{EmptyState-B8vpug3_.js → EmptyState-CjBh_Y06.js} +1 -1
- package/dist/ui/assets/FlavorDecomposition-Cb1Et9e_.js +1 -0
- package/dist/ui/assets/{FrictionCell-X75fKRBK.js → FrictionCell-BAcKJbGq.js} +1 -1
- package/dist/ui/assets/{GlossaryPage-D1tYumqL.js → GlossaryPage-C0nFEAdc.js} +1 -1
- package/dist/ui/assets/HotSessionsPage-C6obf1m9.js +1 -0
- package/dist/ui/assets/InfoTip-Dc4F2ylc.js +1 -0
- package/dist/ui/assets/{Legend-CB7sqqJH.js → Legend-Ck2QXclg.js} +1 -1
- package/dist/ui/assets/{Line-B9XY30uT.js → Line-CSuEmMnn.js} +1 -1
- package/dist/ui/assets/Modal-C5VeGRjW.js +1 -0
- package/dist/ui/assets/ObservationEvidence-Bhnk-xkX.css +1 -0
- package/dist/ui/assets/ObservationEvidence-CP8cvr_5.js +1 -0
- package/dist/ui/assets/OverviewPage-CCyQ2Oxn.js +2 -0
- package/dist/ui/assets/RecommendationsPage-Bs1B1AJ0.js +2 -0
- package/dist/ui/assets/{Scatter-BSWm_yIZ.js → Scatter-BiTrTZ1e.js} +1 -1
- package/dist/ui/assets/SectionHeader-BcQQCuZA.js +1 -0
- package/dist/ui/assets/SessionDetailPage-CcPL36Cv.js +1 -0
- package/dist/ui/assets/SettingsPage-CzAe6Xf1.js +25 -0
- package/dist/ui/assets/Skeleton-D1ey4h0q.js +1 -0
- package/dist/ui/assets/SpendPercentileChip-DiTZ1xoI.js +1 -0
- package/dist/ui/assets/{TrendChart-BSLwlBiq.js → TrendChart-B51LSzds.js} +1 -1
- package/dist/ui/assets/WorkspaceDetailPage-B4LWopuo.js +1 -0
- package/dist/ui/assets/WorkspacesPage-m4Km-zOE.js +1 -0
- package/dist/ui/assets/chart-theme-BAFdtBab.js +1 -0
- package/dist/ui/assets/{graphicalItemSelectors-0-Ue_y1f.js → graphicalItemSelectors-yTOJ8llA.js} +1 -1
- package/dist/ui/assets/index-DTmPINeX.js +41 -0
- package/dist/ui/assets/index-qkEcvt6k.css +1 -0
- package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-CVAAu0IZ.js} +1 -1
- package/dist/ui/assets/short-id-C12NbqLn.js +1 -0
- package/dist/ui/assets/{useExperimentalActions-aI00AFZv.js → useExperimentalActions-BVXB2b0W.js} +1 -1
- package/dist/ui/assets/workspace-names-DW0HTwW6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/work-records/allocation.js +242 -0
- package/dist/work-records/ids.js +80 -0
- package/dist/work-records/index.js +4 -0
- package/dist/work-records/store.js +376 -0
- package/dist/work-records/types.js +29 -0
- package/dist/work-records/validation.js +71 -0
- package/package.json +4 -3
- package/dist/ui/assets/FlavorDecomposition-CkHX3M-U.js +0 -1
- package/dist/ui/assets/HotSessionsPage-8HlRe0VB.js +0 -1
- package/dist/ui/assets/InfoTip-CL8RC4vz.js +0 -1
- package/dist/ui/assets/OverviewPage-DL4joBBE.js +0 -2
- package/dist/ui/assets/RecommendationsPage-NkfH17eB.js +0 -2
- package/dist/ui/assets/SessionDetailPage-Ceqt7poO.js +0 -1
- package/dist/ui/assets/SettingsPage-ZK4ii5h_.js +0 -25
- package/dist/ui/assets/Skeleton-Dp_m79B8.js +0 -1
- package/dist/ui/assets/SpendPercentileChip-CzY6h1gP.js +0 -1
- package/dist/ui/assets/WorkspaceDetailPage-DFOdEB39.js +0 -1
- package/dist/ui/assets/WorkspacesPage-Doct6meK.js +0 -1
- package/dist/ui/assets/chart-theme-DYgw_Tac.js +0 -1
- package/dist/ui/assets/index-D_IJsvkJ.css +0 -1
- package/dist/ui/assets/index-DhFiA8Ch.js +0 -41
- package/dist/ui/assets/workspace-label-CysPJNc0.js +0 -1
package/README.md
CHANGED
|
@@ -84,9 +84,11 @@ five hooks; **Copy install prompt** prepares instructions for three. Copying alo
|
|
|
84
84
|
## Privacy and limits
|
|
85
85
|
|
|
86
86
|
- The daemon binds to **127.0.0.1**. There is no telemetry or hosted product backend.
|
|
87
|
-
- SQLite contains aggregates and structural data,
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
- SQLite contains aggregates and structural data, plus filesystem paths. Command rows keep
|
|
88
|
+
only `/compact`, `/clear`, or an unclassified marker — other commands and arguments are
|
|
89
|
+
discarded, though databases or backups from before the upgrade can still hold raw command
|
|
90
|
+
text (sanitation is logical, not physical erasure). Treat it as sensitive. The optional
|
|
91
|
+
PreCompact hook makes separate raw transcript copies on your machine.
|
|
90
92
|
- Usage refresh can contact Anthropic using your existing Claude Code sign-in. GitHub
|
|
91
93
|
outcomes sync requires a configured token. [Privacy details](https://github.com/Doogit/AgentWrangler/blob/main/docs/privacy.md).
|
|
92
94
|
- Claude Code format changes can require parser updates. This is a single-user tool.
|
package/dist/daemon/http.js
CHANGED
|
@@ -27,53 +27,178 @@ import * as http from "node:http";
|
|
|
27
27
|
import { isReady } from "./readiness.js";
|
|
28
28
|
import { handleApiRequest } from "./router.js";
|
|
29
29
|
import { createStaticHandler } from "./static.js";
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Inline loading page served while the daemon's initial back-scan is running
|
|
32
|
+
* (BOOT-2, Option E): docked terminal panel streaming boot_scan_events on top,
|
|
33
|
+
* three live counters + a determinate progress bar below, fade into the app on
|
|
34
|
+
* ready (no white-flash hard reload). Self-contained by design — the SPA's
|
|
35
|
+
* assets are not servable pre-ready, so design-token VALUES are copied from
|
|
36
|
+
* src/ui/styles.css (dark theme) with that file as the source of truth.
|
|
37
|
+
* Falls back to the plain "Scanning transcripts — N of M files" line when the
|
|
38
|
+
* BOOT-1 fields are absent (older daemon mid-upgrade).
|
|
39
|
+
*/
|
|
31
40
|
const LOADING_HTML = `<!doctype html>
|
|
32
41
|
<html lang="en">
|
|
33
42
|
<head>
|
|
34
43
|
<meta charset="utf-8">
|
|
35
44
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
45
|
+
<meta name="color-scheme" content="dark">
|
|
36
46
|
<title>AgentWrangler — Starting up</title>
|
|
37
47
|
<style>
|
|
48
|
+
/* Token values copied from src/ui/styles.css (dark theme) — keep in sync. */
|
|
49
|
+
:root{
|
|
50
|
+
--bg:#0b0f1a;--panel:#111827;--panel2:#1a2233;--line:rgba(255,255,255,.07);
|
|
51
|
+
--text:#e2e8f0;--text2:#94a3b8;--muted:#9aa9bd;
|
|
52
|
+
--cyan:#22d3ee;--violet:#a78bfa;--green:#34d399;
|
|
53
|
+
--sans:system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
54
|
+
--mono:ui-monospace,Consolas,Menlo,monospace;
|
|
55
|
+
}
|
|
38
56
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
57
|
+
html{background:var(--bg)}
|
|
58
|
+
body{
|
|
59
|
+
background:var(--bg);
|
|
60
|
+
color:var(--text);font-family:var(--sans);min-height:100vh;
|
|
61
|
+
display:grid;place-content:center;gap:28px;padding:24px 0;
|
|
62
|
+
}
|
|
63
|
+
body.leaving .log,body.leaving .hero{opacity:0}
|
|
64
|
+
.log,.hero{opacity:1;transition:opacity .4s ease}
|
|
65
|
+
.log{
|
|
66
|
+
background:#0a0e16;margin:0 auto;width:min(480px,calc(100vw - 36px));
|
|
67
|
+
border:1px solid var(--line);border-radius:8px;overflow:hidden;
|
|
68
|
+
display:grid;grid-template-rows:26px 1fr;height:158px;
|
|
69
|
+
}
|
|
70
|
+
.log-head{
|
|
71
|
+
display:flex;align-items:center;gap:8px;padding:0 12px;
|
|
72
|
+
background:var(--panel);border-bottom:1px solid var(--line);
|
|
73
|
+
font-family:var(--mono);font-size:10px;color:var(--muted);
|
|
74
|
+
letter-spacing:.08em;text-transform:uppercase;
|
|
75
|
+
}
|
|
76
|
+
.log-head .dot{width:7px;height:7px;border-radius:50%;background:var(--green)}
|
|
77
|
+
.log-body{
|
|
78
|
+
padding:8px 12px;font-family:var(--mono);font-size:10.5px;line-height:1.7;
|
|
79
|
+
color:var(--text2);overflow:hidden;display:flex;flex-direction:column;justify-content:flex-end;
|
|
80
|
+
}
|
|
81
|
+
.log-body .ln{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
82
|
+
.log-body .ok{color:var(--green)}
|
|
83
|
+
.log-body .hl{color:var(--cyan)}
|
|
84
|
+
.hero{display:grid;place-content:center;gap:16px;text-align:center;padding:0 16px}
|
|
85
|
+
.headline{font-size:15px;font-weight:600}
|
|
86
|
+
.headline span{color:var(--cyan)}
|
|
87
|
+
.counters{display:flex;gap:34px;justify-content:center;flex-wrap:wrap}
|
|
88
|
+
.counter .cv{font-family:var(--mono);font-size:30px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
|
|
89
|
+
.counter:nth-child(1) .cv{color:var(--cyan)}
|
|
90
|
+
.counter:nth-child(2) .cv{color:var(--violet)}
|
|
91
|
+
.counter:nth-child(3) .cv{color:var(--green)}
|
|
92
|
+
.counter .cl{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-top:2px}
|
|
93
|
+
.pbar{width:300px;max-width:80vw;height:6px;background:var(--panel2);border-radius:99px;overflow:hidden;margin:0 auto}
|
|
94
|
+
.pbar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--violet));border-radius:99px;transition:width .25s ease-out}
|
|
95
|
+
.status{font-family:var(--mono);font-size:11.5px;color:var(--muted);min-height:1.2em;font-variant-numeric:tabular-nums}
|
|
96
|
+
@media (prefers-reduced-motion:reduce){
|
|
97
|
+
.log,.hero,.pbar i{transition:none}
|
|
98
|
+
}
|
|
47
99
|
</style>
|
|
48
100
|
</head>
|
|
49
101
|
<body>
|
|
50
|
-
<div class="
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
102
|
+
<div class="log" id="log" hidden>
|
|
103
|
+
<div class="log-head"><span class="dot"></span>boot scan · <span id="rate">— files/s</span></div>
|
|
104
|
+
<div class="log-body" id="logbody"></div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="hero">
|
|
107
|
+
<div class="headline">Preparing your <span>Claude Code</span> data…</div>
|
|
108
|
+
<div class="counters">
|
|
109
|
+
<div class="counter"><div class="cv" id="c-files">0</div><div class="cl">files scanned</div></div>
|
|
110
|
+
<div class="counter"><div class="cv" id="c-sess">0</div><div class="cl">sessions found</div></div>
|
|
111
|
+
<div class="counter"><div class="cv" id="c-tok">0</div><div class="cl">tokens counted</div></div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="pbar"><i id="fill"></i></div>
|
|
114
|
+
<div class="status" id="status" aria-live="polite">Scanning transcript roots…</div>
|
|
56
115
|
</div>
|
|
57
116
|
<script>
|
|
58
|
-
(function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
117
|
+
(function(){
|
|
118
|
+
var reduced=false;
|
|
119
|
+
try{reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;}catch(e){}
|
|
120
|
+
var lastSeq=0,prevFiles=null,prevT=null,rateEma=null,leaving=false;
|
|
121
|
+
function $(id){return document.getElementById(id);}
|
|
122
|
+
function fmt(n){return Number(n||0).toLocaleString('en-US');}
|
|
123
|
+
function fmtTok(n){
|
|
124
|
+
n=Number(n||0);
|
|
125
|
+
if(n>=1e9)return (n/1e9).toFixed(1)+'B';
|
|
126
|
+
if(n>=1e6)return (n/1e6).toFixed(1)+'M';
|
|
127
|
+
if(n>=1e3)return (n/1e3).toFixed(1)+'K';
|
|
128
|
+
return String(n);
|
|
129
|
+
}
|
|
130
|
+
function finish(){
|
|
131
|
+
if(leaving)return;
|
|
132
|
+
leaving=true;
|
|
133
|
+
// Fade out, then swap to the app behind the transition — no white flash.
|
|
134
|
+
document.body.className='leaving';
|
|
135
|
+
setTimeout(function(){location.reload();},reduced?0:450);
|
|
136
|
+
}
|
|
137
|
+
function renderLog(events){
|
|
138
|
+
var body=$('logbody');
|
|
139
|
+
for(var i=0;i<events.length;i++){
|
|
140
|
+
var ev=events[i];
|
|
141
|
+
if(!ev||typeof ev.seq!=='number'||ev.seq<=lastSeq)continue;
|
|
142
|
+
lastSeq=ev.seq;
|
|
143
|
+
var el=document.createElement('div');
|
|
144
|
+
el.className='ln'+(ev.kind==='stage'?' hl':ev.kind==='root'?' ok':'');
|
|
145
|
+
el.textContent=String(ev.text||'');
|
|
146
|
+
body.appendChild(el);
|
|
147
|
+
while(body.children.length>6)body.removeChild(body.firstChild);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function render(s){
|
|
151
|
+
if(!s)return;
|
|
152
|
+
var files=typeof s.files_parsed==='number'?s.files_parsed:0;
|
|
153
|
+
var seen=typeof s.files_seen==='number'?s.files_seen:0;
|
|
154
|
+
var hasBoot=Array.isArray(s.boot_scan_events);
|
|
155
|
+
// Progress denominator: discovery's up-front total. files_seen only counts
|
|
156
|
+
// files visited so far, which would pin a determinate bar at ~100%.
|
|
157
|
+
var total=hasBoot&&typeof s.boot_files_total==='number'&&s.boot_files_total>0?s.boot_files_total:seen;
|
|
158
|
+
$('c-files').textContent=fmt(files);
|
|
159
|
+
$('c-sess').textContent=fmt(typeof s.boot_sessions_found==='number'?s.boot_sessions_found:(s.sessions||0));
|
|
160
|
+
$('c-tok').textContent=hasBoot?fmtTok(s.boot_tokens_counted):'—';
|
|
161
|
+
var pct=total>0?Math.min(100,Math.round(100*files/total)):0;
|
|
162
|
+
$('fill').style.width=pct+'%';
|
|
163
|
+
// Client-side ETA from the parse-rate delta between polls (EMA-smoothed).
|
|
164
|
+
var now=Date.now();
|
|
165
|
+
if(prevFiles!==null&&now>prevT){
|
|
166
|
+
var r=(files-prevFiles)/((now-prevT)/1000);
|
|
167
|
+
if(r>0)rateEma=rateEma===null?r:(rateEma*0.7+r*0.3);
|
|
168
|
+
}
|
|
169
|
+
prevFiles=files;prevT=now;
|
|
170
|
+
if(hasBoot){
|
|
171
|
+
$('log').hidden=false;
|
|
172
|
+
renderLog(s.boot_scan_events);
|
|
173
|
+
var rr=typeof s.boot_files_per_sec==='number'?s.boot_files_per_sec:0;
|
|
174
|
+
$('rate').textContent=(rr>0?rr:'—')+' files/s';
|
|
175
|
+
if(total===0){
|
|
176
|
+
$('status').textContent='Scanning transcript roots…';
|
|
177
|
+
}else if(pct<100){
|
|
178
|
+
var eta=rateEma&&rateEma>0?Math.max(1,Math.round((total-files)/rateEma)):null;
|
|
179
|
+
$('status').textContent='Parsing sessions'+(eta!==null?' — '+eta+'s remaining':'…');
|
|
180
|
+
}else{
|
|
181
|
+
$('status').textContent='Opening your dashboard…';
|
|
182
|
+
}
|
|
183
|
+
}else if(seen>0){
|
|
184
|
+
// Fallback for an older daemon without BOOT-1 fields: keep the plain
|
|
185
|
+
// first-run counter line ("N of M files"), never a blank page.
|
|
186
|
+
$('status').textContent='Scanning transcripts — '+files+' of '+seen+' files';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
(function poll(){
|
|
190
|
+
fetch('/api/ready')
|
|
191
|
+
.then(function(r){return r.json();})
|
|
192
|
+
.then(function(d){
|
|
193
|
+
if(d.ready){finish();return;}
|
|
194
|
+
fetch('/api/status')
|
|
195
|
+
.then(function(r){return r.json();})
|
|
196
|
+
.then(render)
|
|
197
|
+
.catch(function(){})
|
|
198
|
+
.then(function(){setTimeout(poll,1000);});
|
|
199
|
+
})
|
|
200
|
+
.catch(function(){setTimeout(poll,1000);});
|
|
201
|
+
})();
|
|
77
202
|
})();
|
|
78
203
|
</script>
|
|
79
204
|
</body>
|
|
@@ -132,6 +257,14 @@ function requiresSessionToken(pathname) {
|
|
|
132
257
|
if (pathname === "/api/recommendations/adopt" || pathname === "/api/recommendations/dismiss") {
|
|
133
258
|
return true;
|
|
134
259
|
}
|
|
260
|
+
if (pathname === "/api/recommendations/feedback")
|
|
261
|
+
return true;
|
|
262
|
+
if (pathname === "/api/recommendations/feedback/undo")
|
|
263
|
+
return true;
|
|
264
|
+
if (pathname === "/api/recommendations/goal")
|
|
265
|
+
return true;
|
|
266
|
+
if (pathname === "/api/recommendations/goal/reset")
|
|
267
|
+
return true;
|
|
135
268
|
if (/^\/api\/recommendations\/[^/]+\/apply$/.test(pathname))
|
|
136
269
|
return true;
|
|
137
270
|
if (/^\/api\/recommendations\/[^/]+\/open-terminal$/.test(pathname))
|
|
@@ -140,6 +273,14 @@ function requiresSessionToken(pathname) {
|
|
|
140
273
|
return true;
|
|
141
274
|
if (/^\/api\/esf\/effects\/(track|stop|close|rollback|attest-rollback)$/.test(pathname))
|
|
142
275
|
return true;
|
|
276
|
+
if (pathname === "/api/work-records" || pathname === "/api/work-records/ids")
|
|
277
|
+
return true;
|
|
278
|
+
if (/^\/api\/work-records\/[^/]+\/(edit|closeout|archive|reopen|sessions|contexts|delete)$/.test(pathname))
|
|
279
|
+
return true;
|
|
280
|
+
if (/^\/api\/work-records\/[^/]+\/(sessions|contexts)\/[^/]+\/detach$/.test(pathname))
|
|
281
|
+
return true;
|
|
282
|
+
if (pathname === "/api/work-records/allocations/recompute")
|
|
283
|
+
return true;
|
|
143
284
|
return false;
|
|
144
285
|
}
|
|
145
286
|
/**
|
package/dist/daemon/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { configGet as bptConfigGet, calibrateBytesPerToken } from "../detector/c
|
|
|
23
23
|
import { runContextProbe } from "../detector/context-probe.js";
|
|
24
24
|
import { runDetectors } from "../detector/index.js";
|
|
25
25
|
import { installHook, uninstallHook } from "../hook/install.js";
|
|
26
|
+
import { getBootProgress, pushBootEvent } from "../ingest/boot-progress.js";
|
|
26
27
|
import { collectSessionChurn } from "../ingest/churn-collector.js";
|
|
27
28
|
import { runPostProbeHook, setPostIngestHook, setPostProbeHook } from "../ingest/detector-hook.js";
|
|
28
29
|
import { Ingestor } from "../ingest/index.js";
|
|
@@ -38,6 +39,7 @@ import { getSettingsData, setHealthInstance, setRuntimeResetHook, } from "../que
|
|
|
38
39
|
import { loadConfig } from "./config.js";
|
|
39
40
|
import { runEffectMeasurementPass } from "./effect-pass.js";
|
|
40
41
|
import { createServer } from "./http.js";
|
|
42
|
+
import { createDaemonLifecycle } from "./lifecycle.js";
|
|
41
43
|
import { createOutcomesPassRunner } from "./outcomes-pass.js";
|
|
42
44
|
import { setReady, setScanRoots, setScanState } from "./readiness.js";
|
|
43
45
|
const VERSION = "0.1.0";
|
|
@@ -171,10 +173,6 @@ runProbePass("boot");
|
|
|
171
173
|
// The runner (src/daemon/outcomes-pass.ts) adds a hard 15-min per-pass deadline:
|
|
172
174
|
// a hung pass can no longer wedge the poll cadence forever — the running flag
|
|
173
175
|
// always clears and the next poll resumes.
|
|
174
|
-
let outcomesTimer = null;
|
|
175
|
-
let reportsTimer = null;
|
|
176
|
-
let reportsRunning = false;
|
|
177
|
-
let bptCalibrationTimer = null;
|
|
178
176
|
const outcomesRunner = createOutcomesPassRunner({
|
|
179
177
|
db,
|
|
180
178
|
probe: () => runProbePass("poll"),
|
|
@@ -188,29 +186,31 @@ const outcomesRunner = createOutcomesPassRunner({
|
|
|
188
186
|
function runOutcomesPass() {
|
|
189
187
|
return outcomesRunner.run();
|
|
190
188
|
}
|
|
189
|
+
// R12 bytes→token calibration — best-effort, never crash the daemon.
|
|
190
|
+
// Runs on boot (deferred) and weekly when: opt-in enabled AND ratio is absent
|
|
191
|
+
// or older than 30 days. Mirrors the outcomes bootstrap pattern.
|
|
192
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
193
|
+
function runBptCalibrationIfStale() {
|
|
194
|
+
const enabled = bptConfigGet(db, "bytes_per_token_calibration_enabled");
|
|
195
|
+
if (enabled !== "true")
|
|
196
|
+
return;
|
|
197
|
+
const measuredAt = bptConfigGet(db, "bytes_per_token_measured_at");
|
|
198
|
+
if (measuredAt !== null && Date.now() - new Date(measuredAt).getTime() < THIRTY_DAYS_MS)
|
|
199
|
+
return;
|
|
200
|
+
calibrateBytesPerToken(db).catch((e) => {
|
|
201
|
+
console.error(`Bytes-per-token calibration failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
191
204
|
// ── Deferred boot scan (runs AFTER the HTTP port is bound) ───────────────────
|
|
192
205
|
// startTailBatched() performs a yielding initial back-scan (ingestAllKnown +
|
|
193
206
|
// reconcile), then arms the tail/discovery timers. We do NOT
|
|
194
207
|
// also call runBackscan — that would spin up a second Ingestor whose separate
|
|
195
208
|
// Health instance is the one surfaced in Settings. One Ingestor, one Health.
|
|
196
209
|
// Bounded: if ingestion fails to start, still serve the dashboard (degraded).
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
// requests or timers fire kickBootScan concurrently.
|
|
200
|
-
let scanKicked = false;
|
|
201
|
-
function kickBootScan() {
|
|
202
|
-
if (scanKicked)
|
|
203
|
-
return;
|
|
204
|
-
scanKicked = true;
|
|
205
|
-
// Use setImmediate so the triggering HTTP response flushes before boot work
|
|
206
|
-
// starts. The scan itself also yields between bounded batches.
|
|
207
|
-
setImmediate(() => {
|
|
208
|
-
runBootScan().catch((e) => {
|
|
209
|
-
console.error(`Boot scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
}
|
|
210
|
+
// The one-shot kick guard, the daemon-owned intervals, and shutdown ordering
|
|
211
|
+
// live in the lifecycle harness (src/daemon/lifecycle.ts).
|
|
213
212
|
async function runBootScan() {
|
|
213
|
+
let handle = null;
|
|
214
214
|
try {
|
|
215
215
|
const scanRoots = getSettingsData(db).scan_roots;
|
|
216
216
|
setScanRoots(scanRoots);
|
|
@@ -225,6 +225,7 @@ async function runBootScan() {
|
|
|
225
225
|
setHealthInstance(ingestor.health);
|
|
226
226
|
handle = await ingestor.startTailBatched();
|
|
227
227
|
setScanState("complete");
|
|
228
|
+
pushBootEvent("stage", `parse complete — ${getBootProgress().boot_sessions_found} sessions found`);
|
|
228
229
|
setRuntimeResetHook(() => ingestor.clearRuntimeState());
|
|
229
230
|
console.log(`Ingestion: initial scan complete — health ${JSON.stringify(ingestor.healthSnapshot())}`);
|
|
230
231
|
}
|
|
@@ -237,6 +238,7 @@ async function runBootScan() {
|
|
|
237
238
|
// boot probe ran before any workspace existed and could only size the global sources.
|
|
238
239
|
// Then re-evaluate detectors so D1 surfaces its per-source context recs on the first run
|
|
239
240
|
// instead of waiting for the next 10-minute poll.
|
|
241
|
+
pushBootEvent("stage", "building aggregates…");
|
|
240
242
|
runProbePass("post-scan");
|
|
241
243
|
try {
|
|
242
244
|
runDetectors(db, { now: new Date() });
|
|
@@ -246,6 +248,7 @@ async function runBootScan() {
|
|
|
246
248
|
}
|
|
247
249
|
// Signal readiness BEFORE the outcomes pass — outcomes is a slow ~97s I/O
|
|
248
250
|
// pass that yields on `gh` subprocess I/O; the loading page must not wait on it.
|
|
251
|
+
pushBootEvent("stage", "ready — opening dashboard");
|
|
249
252
|
setReady();
|
|
250
253
|
// Outcomes bootstrap — deferred until after the back-scan so the event loop
|
|
251
254
|
// is free and readGithubToken() execFile (10 s timeout) cannot be starved.
|
|
@@ -263,21 +266,8 @@ async function runBootScan() {
|
|
|
263
266
|
console.error(`Self-churn collection failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
264
267
|
}
|
|
265
268
|
});
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// or older than 30 days. Mirrors the outcomes bootstrap pattern.
|
|
269
|
-
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
270
|
-
function runBptCalibrationIfStale() {
|
|
271
|
-
const enabled = bptConfigGet(db, "bytes_per_token_calibration_enabled");
|
|
272
|
-
if (enabled !== "true")
|
|
273
|
-
return;
|
|
274
|
-
const measuredAt = bptConfigGet(db, "bytes_per_token_measured_at");
|
|
275
|
-
if (measuredAt !== null && Date.now() - new Date(measuredAt).getTime() < THIRTY_DAYS_MS)
|
|
276
|
-
return;
|
|
277
|
-
calibrateBytesPerToken(db).catch((e) => {
|
|
278
|
-
console.error(`Bytes-per-token calibration failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
279
|
-
});
|
|
280
|
-
}
|
|
269
|
+
// Deferred boot calibration check (the weekly interval is armed by the
|
|
270
|
+
// lifecycle harness after this scan resolves).
|
|
281
271
|
setImmediate(() => {
|
|
282
272
|
try {
|
|
283
273
|
runBptCalibrationIfStale();
|
|
@@ -286,42 +276,31 @@ async function runBootScan() {
|
|
|
286
276
|
console.error(`Bytes-per-token calibration boot failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
287
277
|
}
|
|
288
278
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
outcomesTimer = setInterval(() => {
|
|
279
|
+
return handle;
|
|
280
|
+
}
|
|
281
|
+
// ── Lifecycle harness: boot-kick guard, daemon intervals, shutdown ordering ──
|
|
282
|
+
// closeServer closes over `server` (declared below); shutdown only ever runs
|
|
283
|
+
// after listen, so the reference is initialized by then.
|
|
284
|
+
const lifecycle = createDaemonLifecycle({
|
|
285
|
+
bootScan: runBootScan,
|
|
286
|
+
calibrationTick: runBptCalibrationIfStale,
|
|
287
|
+
outcomesTick: () => {
|
|
299
288
|
runOutcomesPass().catch((e) => {
|
|
300
289
|
console.error(`Outcomes poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
301
290
|
});
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
await Promise.resolve(generateWeeklyReport(db, new Date()));
|
|
309
|
-
}
|
|
310
|
-
catch (e) {
|
|
311
|
-
console.error(`Weekly report poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
312
|
-
}
|
|
313
|
-
finally {
|
|
314
|
-
reportsRunning = false;
|
|
315
|
-
}
|
|
316
|
-
}, 7 * 24 * 60 * 60 * 1000);
|
|
317
|
-
}
|
|
291
|
+
},
|
|
292
|
+
reportsTick: () => generateWeeklyReport(db, new Date()),
|
|
293
|
+
closeServer: (onClosed) => server.close(onClosed),
|
|
294
|
+
closeDb: () => db.close(),
|
|
295
|
+
onClosed: () => process.exit(0),
|
|
296
|
+
});
|
|
318
297
|
// ── 4. Start HTTP server ──────────────────────────────────────────────────────
|
|
319
298
|
// Bind the port BEFORE the back-scan so the loading page is delivered
|
|
320
299
|
// immediately while the initial scan yields between file batches.
|
|
321
300
|
// Generate a random in-memory session token for the CSRF token gate.
|
|
322
301
|
// NEVER log it, NEVER persist it — it lives only in process memory.
|
|
323
302
|
const sessionToken = randomUUID();
|
|
324
|
-
const server = createServer(db, config.port, config.uiRoot, sessionToken, kickBootScan);
|
|
303
|
+
const server = createServer(db, config.port, config.uiRoot, sessionToken, lifecycle.kickBootScan);
|
|
325
304
|
server.listen(config.port, "127.0.0.1", () => {
|
|
326
305
|
const url = `http://127.0.0.1:${config.port}`;
|
|
327
306
|
console.log(`Listening on ${url}`);
|
|
@@ -334,7 +313,7 @@ server.listen(config.port, "127.0.0.1", () => {
|
|
|
334
313
|
// Fallback: kick the back-scan after 3 s even if no browser request arrives
|
|
335
314
|
// (headless mode, --no-open, CI). The one-shot guard in kickBootScan() makes
|
|
336
315
|
// whichever trigger fires first the only one that runs the scan.
|
|
337
|
-
|
|
316
|
+
lifecycle.armBootFallback();
|
|
338
317
|
});
|
|
339
318
|
/** Open `url` in the system default browser (best-effort; never crashes the daemon). */
|
|
340
319
|
function openBrowser(url) {
|
|
@@ -357,24 +336,8 @@ function openBrowser(url) {
|
|
|
357
336
|
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
358
337
|
process.on("SIGINT", () => {
|
|
359
338
|
console.log("Shutting down…");
|
|
360
|
-
|
|
361
|
-
if (outcomesTimer !== null)
|
|
362
|
-
clearInterval(outcomesTimer);
|
|
363
|
-
if (reportsTimer !== null)
|
|
364
|
-
clearInterval(reportsTimer);
|
|
365
|
-
server.close(() => {
|
|
366
|
-
db.close();
|
|
367
|
-
process.exit(0);
|
|
368
|
-
});
|
|
339
|
+
lifecycle.shutdown();
|
|
369
340
|
});
|
|
370
341
|
process.on("SIGTERM", () => {
|
|
371
|
-
|
|
372
|
-
if (outcomesTimer !== null)
|
|
373
|
-
clearInterval(outcomesTimer);
|
|
374
|
-
if (reportsTimer !== null)
|
|
375
|
-
clearInterval(reportsTimer);
|
|
376
|
-
server.close(() => {
|
|
377
|
-
db.close();
|
|
378
|
-
process.exit(0);
|
|
379
|
-
});
|
|
342
|
+
lifecycle.shutdown();
|
|
380
343
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/daemon/lifecycle.ts — daemon timer registration and graceful shutdown.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from daemon/index.ts (PERF5) so the boot-kick guard, the three
|
|
5
|
+
* daemon-owned intervals (calibration / outcomes / weekly reports), and the
|
|
6
|
+
* shutdown ordering are deterministically unit-testable without booting the
|
|
7
|
+
* daemon. The signal handlers in index.ts stay thin: both call shutdown().
|
|
8
|
+
*
|
|
9
|
+
* Shutdown contract:
|
|
10
|
+
* - idempotent — a second signal is a no-op
|
|
11
|
+
* - clears the boot-fallback timeout so a shutdown during the 3 s window
|
|
12
|
+
* cannot start a scan afterwards
|
|
13
|
+
* - a pending boot-kick setImmediate checks the flag and never starts the
|
|
14
|
+
* scan after shutdown
|
|
15
|
+
* - when shutdown lands while the boot scan is still in flight, the timers
|
|
16
|
+
* are never armed and the tail handle is stopped as soon as the scan
|
|
17
|
+
* resolves (previously both leaked past db.close())
|
|
18
|
+
* - stopping the tail does NOT drain a suspended tail pass; the Ingestor's
|
|
19
|
+
* stopped flag makes it quit at the next file boundary (its contract)
|
|
20
|
+
*/
|
|
21
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
|
+
export function createDaemonLifecycle(deps) {
|
|
23
|
+
const error = deps.error ?? ((msg) => console.error(msg));
|
|
24
|
+
const calibrationIntervalMs = deps.calibrationIntervalMs ?? WEEK_MS;
|
|
25
|
+
const outcomesIntervalMs = deps.outcomesIntervalMs ?? 10 * 60 * 1000;
|
|
26
|
+
const reportsIntervalMs = deps.reportsIntervalMs ?? WEEK_MS;
|
|
27
|
+
const bootFallbackMs = deps.bootFallbackMs ?? 3000;
|
|
28
|
+
let scanKicked = false;
|
|
29
|
+
let shutDown = false;
|
|
30
|
+
let handle = null;
|
|
31
|
+
let calibrationTimer = null;
|
|
32
|
+
let outcomesTimer = null;
|
|
33
|
+
let reportsTimer = null;
|
|
34
|
+
let fallbackTimer = null;
|
|
35
|
+
let reportsRunning = false;
|
|
36
|
+
function armTimers() {
|
|
37
|
+
calibrationTimer = setInterval(() => {
|
|
38
|
+
try {
|
|
39
|
+
deps.calibrationTick();
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
error(`Bytes-per-token calibration poll failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
43
|
+
}
|
|
44
|
+
}, calibrationIntervalMs);
|
|
45
|
+
outcomesTimer = setInterval(() => {
|
|
46
|
+
try {
|
|
47
|
+
deps.outcomesTick();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
error(`Outcomes poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
51
|
+
}
|
|
52
|
+
}, outcomesIntervalMs);
|
|
53
|
+
reportsTimer = setInterval(() => {
|
|
54
|
+
if (reportsRunning)
|
|
55
|
+
return;
|
|
56
|
+
reportsRunning = true;
|
|
57
|
+
Promise.resolve()
|
|
58
|
+
.then(() => deps.reportsTick())
|
|
59
|
+
.catch((e) => {
|
|
60
|
+
error(`Weekly report poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
61
|
+
})
|
|
62
|
+
.finally(() => {
|
|
63
|
+
reportsRunning = false;
|
|
64
|
+
});
|
|
65
|
+
}, reportsIntervalMs);
|
|
66
|
+
}
|
|
67
|
+
function kickBootScan() {
|
|
68
|
+
if (scanKicked || shutDown)
|
|
69
|
+
return;
|
|
70
|
+
scanKicked = true;
|
|
71
|
+
// setImmediate so the triggering HTTP response flushes before boot work
|
|
72
|
+
// starts. The scan itself also yields between bounded batches.
|
|
73
|
+
setImmediate(() => {
|
|
74
|
+
if (shutDown)
|
|
75
|
+
return;
|
|
76
|
+
deps
|
|
77
|
+
.bootScan()
|
|
78
|
+
.then((h) => {
|
|
79
|
+
handle = h;
|
|
80
|
+
if (shutDown) {
|
|
81
|
+
// Shutdown landed mid-scan: never arm timers; stop the tail now.
|
|
82
|
+
handle?.stop();
|
|
83
|
+
handle = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
armTimers();
|
|
87
|
+
})
|
|
88
|
+
.catch((e) => {
|
|
89
|
+
error(`Boot scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function shutdown() {
|
|
94
|
+
if (shutDown)
|
|
95
|
+
return;
|
|
96
|
+
shutDown = true;
|
|
97
|
+
if (fallbackTimer !== null)
|
|
98
|
+
clearTimeout(fallbackTimer);
|
|
99
|
+
handle?.stop();
|
|
100
|
+
handle = null;
|
|
101
|
+
if (calibrationTimer !== null)
|
|
102
|
+
clearInterval(calibrationTimer);
|
|
103
|
+
if (outcomesTimer !== null)
|
|
104
|
+
clearInterval(outcomesTimer);
|
|
105
|
+
if (reportsTimer !== null)
|
|
106
|
+
clearInterval(reportsTimer);
|
|
107
|
+
deps.closeServer(() => {
|
|
108
|
+
deps.closeDb();
|
|
109
|
+
deps.onClosed?.();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
kickBootScan,
|
|
114
|
+
armBootFallback() {
|
|
115
|
+
fallbackTimer = setTimeout(kickBootScan, bootFallbackMs);
|
|
116
|
+
},
|
|
117
|
+
shutdown,
|
|
118
|
+
get isShutDown() {
|
|
119
|
+
return shutDown;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|