pi-mega-compact 0.8.16 → 0.8.18
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 +1 -1
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +12 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +19 -6
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/server.js +83 -0
- package/dist/extensions/mega-compact-s38.test.js +33 -0
- package/dist/extensions/mega-events/agent-handlers.js +51 -3
- package/dist/extensions/mega-events/compact-handlers.js +10 -4
- package/dist/extensions/mega-events/error-classifier.js +24 -1
- package/dist/extensions/mega-events/send-safe.js +12 -0
- package/dist/extensions/mega-events.js +1 -0
- package/dist/extensions/mega-runtime/state.js +17 -1
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/global-index.js +199 -1
- package/dist/src/store/sqlite/global-index.test.js +136 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js → AchievementTiles-CNWZ5NjP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js.map → AchievementTiles-CNWZ5NjP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js → AchievementsTab-BKPifNKj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js.map → AchievementsTab-BKPifNKj.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js → CacheTab--epzrQHD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js.map → CacheTab--epzrQHD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DXD7zbqy.js → ConfigTab-D3ralwSj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/ConfigTab-D3ralwSj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js → GameTab-CgUvITYy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js.map → GameTab-CgUvITYy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js → MetricsTab-DrlJqXGL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js.map → MetricsTab-DrlJqXGL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js → OverviewTab-_gD756wJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js.map → OverviewTab-_gD756wJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js → ReposTab-DjEZmqUb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js.map → ReposTab-DjEZmqUb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js +70 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js → TimeSavedCard-BbAXT1wO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js.map → TimeSavedCard-BbAXT1wO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Cfxniu2R.css → index-BTft6DM_.css} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js → index-CnRzusfi.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js.map → index-CnRzusfi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js +2 -0
- package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js.map +1 -0
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/package-lock.json +374 -2
- package/extensions/dashboard-client/package.json +2 -1
- package/extensions/dashboard-client/src/App.tsx +5 -1
- package/extensions/dashboard-client/src/api/client.ts +12 -0
- package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +185 -0
- package/extensions/dashboard-client/src/components/EventStream.tsx +3 -0
- package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +199 -0
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/sessions.css +193 -0
- package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +6 -3
- package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +193 -0
- package/extensions/dashboard-server/api-contracts/core.ts +25 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +102 -0
- package/extensions/dashboard-server/api-contracts/index.ts +10 -1
- package/extensions/dashboard-server/api-contracts.test.ts +20 -7
- package/extensions/dashboard-server/server.ts +100 -0
- package/extensions/mega-compact-s38.test.ts +63 -0
- package/extensions/mega-events/agent-handlers.ts +47 -3
- package/extensions/mega-events/compact-handlers.ts +11 -4
- package/extensions/mega-events/error-classifier.ts +25 -1
- package/extensions/mega-events/send-safe.ts +37 -0
- package/extensions/mega-events.ts +1 -0
- package/extensions/mega-runtime/state.ts +31 -0
- package/package.json +1 -1
- package/src/store/sqlite/global-index.test.ts +222 -0
- package/src/store/sqlite/global-index.ts +316 -1
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
- package/extensions/dashboard-client/dist/assets/ConfigTab-DXD7zbqy.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js +0 -2
- package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js.map +0 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline CSS for the dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from the original monolithic `html-template.ts`.
|
|
5
|
+
* Kept as a single cohesive stylesheet — splitting CSS into per-tab files
|
|
6
|
+
* would fragment the design system (shared tokens, e.g. `.card`, `.stat-grid`,
|
|
7
|
+
* `.meter-*` are reused across all tabs).
|
|
8
|
+
*/
|
|
9
|
+
export function styles() {
|
|
10
|
+
return `<style>
|
|
11
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
|
|
13
|
+
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
|
|
14
|
+
h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
|
|
15
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
|
16
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
17
|
+
.card.safe { border-color: #238636; }
|
|
18
|
+
.card.safe h2 { color: #3fb950; }
|
|
19
|
+
.safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
|
|
20
|
+
.value.ok { color: #3fb950; }
|
|
21
|
+
.label {
|
|
22
|
+
cursor: help;
|
|
23
|
+
border-bottom: 1px dotted #484f58;
|
|
24
|
+
}
|
|
25
|
+
.card.legend { grid-column: 1 / -1; }
|
|
26
|
+
.legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
|
|
27
|
+
.legend-list li { margin-bottom: 8px; line-height: 1.5; }
|
|
28
|
+
.legend-list b { color: #f0f6fc; }
|
|
29
|
+
.legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
|
|
30
|
+
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
|
|
31
|
+
.meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
|
|
32
|
+
.meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
|
|
33
|
+
.meter-green { background: #238636; }
|
|
34
|
+
.meter-yellow { background: #d29922; }
|
|
35
|
+
.meter-red { background: #f85149; }
|
|
36
|
+
.meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
37
|
+
.meter-sub { font-size: 12px; color: #8b949e; }
|
|
38
|
+
.status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
|
|
39
|
+
.status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
40
|
+
.bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
|
|
41
|
+
.bullet-off { background: #484f58; }
|
|
42
|
+
.bullet-na { background: #d29922; }
|
|
43
|
+
.state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
|
|
44
|
+
.stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
45
|
+
.stat-grid .label { color: #8b949e; }
|
|
46
|
+
.stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
|
|
47
|
+
.conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
48
|
+
.conf-grid .label { color: #8b949e; }
|
|
49
|
+
.conf-grid .value { color: #f0f6fc; font-family: monospace; }
|
|
50
|
+
.events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
51
|
+
.events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
|
|
52
|
+
.events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
|
|
53
|
+
.ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
|
|
54
|
+
.ev:last-child { border-bottom: none; }
|
|
55
|
+
.ev-type { font-weight: 700; min-width: 70px; text-align: right; }
|
|
56
|
+
.ev-type-compact { color: #3fb950; }
|
|
57
|
+
.ev-type-recall { color: #a371f7; }
|
|
58
|
+
.ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
|
|
59
|
+
.ev-detail { color: #8b949e; flex: 1; }
|
|
60
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
61
|
+
.empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
62
|
+
.offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
|
|
63
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
64
|
+
.tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
|
|
65
|
+
.tab:hover { color: #c9d1d9; border-color: #484f58; }
|
|
66
|
+
.tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
|
|
67
|
+
.tab.future { opacity: .7; }
|
|
68
|
+
.tab .soon { font-size: 9px; font-weight: 700; background: #6e40c9; color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 6px; text-transform: uppercase; letter-spacing: .5px; }
|
|
69
|
+
.tab-panel { display: none; }
|
|
70
|
+
.tab-panel.active { display: block; }
|
|
71
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
72
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
73
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
74
|
+
.summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
75
|
+
table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
|
|
76
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
77
|
+
table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
|
|
78
|
+
table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
|
|
79
|
+
table.repos tr:last-child td { border-bottom: none; }
|
|
80
|
+
table.repos tr:hover td { background: #1c2128; }
|
|
81
|
+
.repo-model { color: #a371f7; }
|
|
82
|
+
.repo-none { color: #484f58; font-style: italic; }
|
|
83
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
84
|
+
.model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
|
|
85
|
+
.card.cost h2 { color: #a371f7; }
|
|
86
|
+
.cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
|
|
87
|
+
.cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
|
|
88
|
+
.repo-link { cursor: pointer; }
|
|
89
|
+
.repo-link:hover td { color: #58a6ff; }
|
|
90
|
+
.repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
91
|
+
.repo-detail.open { display: flex; }
|
|
92
|
+
.repo-detail-box { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
|
|
93
|
+
.repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
|
|
94
|
+
.repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
|
|
95
|
+
.repo-close:hover { color: #f0f6fc; }
|
|
96
|
+
.repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
|
|
97
|
+
.placeholder { text-align: center; padding: 48px 16px; color: #8b949e; }
|
|
98
|
+
.placeholder .em { font-size: 40px; margin-bottom: 12px; }
|
|
99
|
+
.placeholder h2 { font-size: 16px; color: #f0f6fc; margin-bottom: 8px; }
|
|
100
|
+
.placeholder p { font-size: 13px; line-height: 1.6; max-width: 420px; margin: 0 auto; }
|
|
101
|
+
</style>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Summary" tab panel — aggregate counts across all registered repos.
|
|
3
|
+
*
|
|
4
|
+
* Populated by the index poller in `script.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export function summaryTab() {
|
|
7
|
+
return `<!-- Summary (aggregate across all repos) -->
|
|
8
|
+
<div class="tab-panel" id="panel-summary">
|
|
9
|
+
<div class="summary-grid">
|
|
10
|
+
<div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
|
|
11
|
+
<div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
|
|
12
|
+
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
13
|
+
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="updated" id="sm-updated"></div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML template for the dashboard server — composer.
|
|
3
|
+
*
|
|
4
|
+
* Originally a single 518-line monolithic file returning one large HTML page
|
|
5
|
+
* template. Split into per-concern fragments under `./html/` (each tab is its
|
|
6
|
+
* own file; plus shared styles, header chrome, the repo-detail modal, and the
|
|
7
|
+
* client script). This file composes them back into the full document.
|
|
8
|
+
*
|
|
9
|
+
* `tierName` is threaded into the heading badge and the Configuration card.
|
|
10
|
+
*
|
|
11
|
+
* Fragment layout:
|
|
12
|
+
* headOpen() — <!DOCTYPE html>…<title>
|
|
13
|
+
* styles() — <style>…</style> (shared CSS, design-system level)
|
|
14
|
+
* bodyOpen(tier) — </head><body>…<h1>…</nav> (incl. High Score future tab)
|
|
15
|
+
* currentRepoTab(tier) — panel-current (cards + event stream + in-panel table)
|
|
16
|
+
* allReposTab() — panel-all (machine-wide repo table)
|
|
17
|
+
* summaryTab() — panel-summary (aggregate cards)
|
|
18
|
+
* highScoreTab() — panel-highscore (FUTURE placeholder, next milestone)
|
|
19
|
+
* repoDetailModal() — per-repo overlay (position-independent)
|
|
20
|
+
* script() — <script>…</script></body></html>
|
|
21
|
+
*/
|
|
22
|
+
import { headOpen } from "./html/head-open.js";
|
|
23
|
+
import { styles } from "./html/styles.js";
|
|
24
|
+
import { bodyOpen } from "./html/body-open.js";
|
|
25
|
+
import { currentRepoTab } from "./html/current-repo-tab.js";
|
|
26
|
+
import { allReposTab } from "./html/all-repos-tab.js";
|
|
27
|
+
import { summaryTab } from "./html/summary-tab.js";
|
|
28
|
+
import { highScoreTab } from "./html/high-score-tab.js";
|
|
29
|
+
import { repoDetailModal } from "./html/repo-detail-modal.js";
|
|
30
|
+
import { script } from "./html/script.js";
|
|
31
|
+
export function dashboardHtml(tierName) {
|
|
32
|
+
return (headOpen() +
|
|
33
|
+
styles() +
|
|
34
|
+
bodyOpen(tierName) +
|
|
35
|
+
currentRepoTab(tierName) +
|
|
36
|
+
repoDetailModal() +
|
|
37
|
+
allReposTab() +
|
|
38
|
+
summaryTab() +
|
|
39
|
+
highScoreTab() +
|
|
40
|
+
script());
|
|
41
|
+
}
|
|
@@ -691,6 +691,89 @@ export async function launchDashboardServer(stateDir) {
|
|
|
691
691
|
}
|
|
692
692
|
return;
|
|
693
693
|
}
|
|
694
|
+
// /api/sessions — S39: active pi sessions with latest token usage + heartbeat.
|
|
695
|
+
// GET returns {updatedAt, pruned, sessions[]} after pruning stale heartbeats.
|
|
696
|
+
// The dashboard server is a detached child with no MegaRuntime ref, so it
|
|
697
|
+
// reads session_heartbeats via a require()'d sqlite helper (same pattern as
|
|
698
|
+
// /api/achievements, /api/perf). Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
699
|
+
if (req.url?.startsWith("/api/sessions")) {
|
|
700
|
+
// Guard: /api/sessions/timeseries handled separately below.
|
|
701
|
+
if (req.url.startsWith("/api/sessions/timeseries")) {
|
|
702
|
+
// Fall through to the timeseries handler below.
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
const sReq = createRequire(import.meta.url);
|
|
706
|
+
const { readActiveSessions, pruneStaleSessions, listRepoRegistry, } = sReq("../../src/store/sqlite.js");
|
|
707
|
+
if (req.method !== "GET") {
|
|
708
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
709
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const pruned = pruneStaleSessions(); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
714
|
+
const active = readActiveSessions();
|
|
715
|
+
const repos = listRepoRegistry();
|
|
716
|
+
const repoMap = new Map(repos.map((r) => [r.repoRoot, r]));
|
|
717
|
+
const sessions = active.map((s) => ({
|
|
718
|
+
pid: s.pid,
|
|
719
|
+
sessionId: s.sessionId,
|
|
720
|
+
repoRoot: s.repoRoot,
|
|
721
|
+
displayName: s.repoRoot
|
|
722
|
+
? (s.repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? s.repoRoot)
|
|
723
|
+
: (s.stateDir?.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"),
|
|
724
|
+
model: s.repoRoot ? (repoMap.get(s.repoRoot)?.modelName ?? null) : null,
|
|
725
|
+
tokens: s.tokens,
|
|
726
|
+
percent: s.percent,
|
|
727
|
+
ctxWindow: s.ctxWindow,
|
|
728
|
+
lastSeen: s.lastSeen,
|
|
729
|
+
stateDir: s.stateDir,
|
|
730
|
+
}));
|
|
731
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
732
|
+
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), pruned, sessions }));
|
|
733
|
+
}
|
|
734
|
+
catch (e) {
|
|
735
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
736
|
+
res.end(JSON.stringify({ error: "sessions_unavailable", detail: String(e) }));
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// /api/sessions/timeseries — S39: stacked per-session token timeseries for
|
|
742
|
+
// the recharts memory graph. GET ?minutes=N (clamped [1,1440]) returns
|
|
743
|
+
// {updatedAt, windowMinutes, series[], totals[]} in recharts-ready shape.
|
|
744
|
+
// Non-GET -> 405. PREVENT-PI-004: loopback.
|
|
745
|
+
if (req.url?.startsWith("/api/sessions/timeseries")) {
|
|
746
|
+
const tsReq = createRequire(import.meta.url);
|
|
747
|
+
const { readSessionTimeseries, pruneTokenSamples, } = tsReq("../../src/store/sqlite.js");
|
|
748
|
+
if (req.method !== "GET") {
|
|
749
|
+
res.writeHead(405, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
750
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const url = new URL(req.url, "http://x"); // guardrails-allow PREVENT-PI-004: localhost dashboard URL base (loopback-only)
|
|
755
|
+
let minutes = Number(url.searchParams.get("minutes") ?? "30");
|
|
756
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
757
|
+
minutes = 30;
|
|
758
|
+
minutes = Math.min(Math.max(minutes, 1), 1440);
|
|
759
|
+
const sinceTs = Date.now() - minutes * 60_000;
|
|
760
|
+
const pruneMs = Math.max(minutes * 60_000, 1_800_000);
|
|
761
|
+
pruneTokenSamples(pruneMs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
762
|
+
const result = readSessionTimeseries(sinceTs); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
763
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
764
|
+
res.end(JSON.stringify({
|
|
765
|
+
updatedAt: new Date().toISOString(),
|
|
766
|
+
windowMinutes: minutes,
|
|
767
|
+
series: result.series,
|
|
768
|
+
totals: result.totals,
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
773
|
+
res.end(JSON.stringify({ error: "timeseries_unavailable", detail: String(e) }));
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
694
777
|
// Fallback — serve the React client build (SPA route) or legacy dashboard.
|
|
695
778
|
// Non-/api/* GETs hit here: serve client assets if built, else inline HTML.
|
|
696
779
|
if (req.method === "GET" &&
|
|
@@ -179,6 +179,39 @@ test("S38: classifyError returns 'compaction-noop' for 'Auto compaction failed'
|
|
|
179
179
|
assert.equal(classifyErrorFn("Auto compaction failed"), "compaction-noop");
|
|
180
180
|
assert.equal(classifyErrorFn("Auto-compaction failed"), "compaction-noop");
|
|
181
181
|
});
|
|
182
|
+
// ---- context-overflow classifier (S38.8: 400 "too long... even after compaction") ----
|
|
183
|
+
test("S38: classifyError returns 'context-overflow' for the literal user 400 string", () => {
|
|
184
|
+
assert.equal(classifyErrorFn("Your conversation is too long for this model's context window even after compaction. Reduce the conversation length or enable/allow compaction."), "context-overflow");
|
|
185
|
+
});
|
|
186
|
+
test("S38: classifyError returns 'context-overflow' for 'All targets failed' wrapper", () => {
|
|
187
|
+
assert.equal(classifyErrorFn("All targets failed: ... Last error: ... too long ... context window even after compaction"), "context-overflow");
|
|
188
|
+
});
|
|
189
|
+
test("S38: classifyError returns 'context-overflow' for invalid_request_error JSON shape", () => {
|
|
190
|
+
assert.equal(classifyErrorFn('{"type":"invalid_request_error","message":"... too long for context window ..."}'), "context-overflow");
|
|
191
|
+
});
|
|
192
|
+
test("S38: classifyError returns 'context-overflow' for OpenRouter 'All targets failed' max-context 400 (FAIL-20260725)", () => {
|
|
193
|
+
// Regression: the OpenAI/OpenRouter provider-side phrasing
|
|
194
|
+
// "maximum context length is N tokens ... requires at least M tokens ...
|
|
195
|
+
// reduce your input or max_tokens" does NOT contain "too long" / "context
|
|
196
|
+
// window" / "reduce the conversation", so it slipped past the original S38.8
|
|
197
|
+
// regex and fell through to the generic transient branch, firing 5 blind
|
|
198
|
+
// retry nudges that re-submitted the same oversized prompt -> re-400 ->
|
|
199
|
+
// busy-loop. The exact user-facing wrapper string from the router:
|
|
200
|
+
assert.equal(classifyErrorFn('Error: 400: {"message":"All targets failed: neuralwatt/glm-5.2-short. Last error: This model\'s maximum context length is 200000 tokens. Your request requires at least 201070 tokens (201070 prompt + 0 max_tokens). Please reduce your input or max_tokens.","type":"invalid_request_error"}'), "context-overflow");
|
|
201
|
+
});
|
|
202
|
+
test("S38: classifyError returns 'context-overflow' for bare 'maximum context length' phrasing", () => {
|
|
203
|
+
assert.equal(classifyErrorFn("This model's maximum context length is 128000 tokens. Your request requires at least 130000 tokens. Please reduce your input or max_tokens."), "context-overflow");
|
|
204
|
+
});
|
|
205
|
+
test("S38: classifyError returns 'context-overflow' for 'context length exceeded' phrasing", () => {
|
|
206
|
+
assert.equal(classifyErrorFn("This model's context length exceeded: 200000 tokens"), "context-overflow");
|
|
207
|
+
});
|
|
208
|
+
test("S38: context-overflow fires NO blind retry nudge and logs 'context_overflow'", async () => {
|
|
209
|
+
const h = harness();
|
|
210
|
+
await s38TurnEnd(h, "error", "Your conversation is too long for this model's context window even after compaction. Reduce the conversation length or enable/allow compaction.");
|
|
211
|
+
const ev = eventTypes(h.stateDir);
|
|
212
|
+
assert.equal(h.sendUserMessages.length, 0, "context-overflow: NO blind retry nudge fired");
|
|
213
|
+
assert.ok(ev.includes("context_overflow"), "context-overflow: 'context_overflow' event logged");
|
|
214
|
+
});
|
|
182
215
|
// ---- integration tests (fire turn_end through the real extension) ----
|
|
183
216
|
test("S38: compaction-noop logs 'compaction_noop_diagnostic' + resets counter + no retry fired", async () => {
|
|
184
217
|
const h = harness();
|
|
@@ -5,6 +5,7 @@ import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achie
|
|
|
5
5
|
import { isMegaCache } from "../../src/game/scoring.js";
|
|
6
6
|
import { resolveRepoRoot } from "../mega-config.js";
|
|
7
7
|
import { classifyError } from "./error-classifier.js";
|
|
8
|
+
import { safeSendUserMessage } from "./send-safe.js";
|
|
8
9
|
/** Register agent/turn tracking event handlers. */
|
|
9
10
|
export function registerAgentHandlers(pi, runtime, config) {
|
|
10
11
|
// ---- Agent tracking for real-time widget + status-line updates ---------
|
|
@@ -188,7 +189,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
188
189
|
const nudgeMsg = lengthStop && !didDurableTrim
|
|
189
190
|
? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
|
|
190
191
|
: "[mega-compact] continue from the compacted context above.";
|
|
191
|
-
pi
|
|
192
|
+
await safeSendUserMessage(pi, nudgeMsg);
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
catch {
|
|
@@ -312,6 +313,53 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
312
313
|
turnIndex: event.turnIndex,
|
|
313
314
|
});
|
|
314
315
|
}
|
|
316
|
+
else if (category === 'context-overflow') {
|
|
317
|
+
// (4b) context-window overflow 400 ("too long... even after compaction").
|
|
318
|
+
// NOT a blind retry: re-submitting the same oversized prompt would just
|
|
319
|
+
// re-400 and busy-loop. Reset the counters (this turn is terminal, not
|
|
320
|
+
// retryable in the S38 sense) and force ONE best-effort re-compact with
|
|
321
|
+
// the debounce bypassed + the same race-guard (lastNativeCompactAt
|
|
322
|
+
// cooldown + deferred setTimeout re-check) as the agent_end durable
|
|
323
|
+
// trim. Resume after that forced compact is handled by the existing
|
|
324
|
+
// nudgeResume() inside session_before_compact (it fires after
|
|
325
|
+
// driveNativeCompaction supplies the compaction) — do NOT add a
|
|
326
|
+
// separate nudge here.
|
|
327
|
+
runtime.rt.errorRetryCount = 0;
|
|
328
|
+
runtime.rt.consecutiveErrors = 0;
|
|
329
|
+
runtime.dashboard.event('context_overflow', {
|
|
330
|
+
turnIndex: event.turnIndex,
|
|
331
|
+
sessionId: runtime.rt.sessionId,
|
|
332
|
+
});
|
|
333
|
+
runtime.logger.warn('context-overflow', {
|
|
334
|
+
sessionId: runtime.rt.sessionId,
|
|
335
|
+
turnIndex: event.turnIndex,
|
|
336
|
+
});
|
|
337
|
+
if (config.auto) {
|
|
338
|
+
const now2 = Date.now();
|
|
339
|
+
const cooldownMs2 = config.raceGuardStrict ? 30_000 : 10_000;
|
|
340
|
+
const sinceCompact2 = now2 - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
341
|
+
if (sinceCompact2 >= cooldownMs2 && !piCompactWouldNoop(ctx)) {
|
|
342
|
+
runtime.debounceUntil = now2 + 0;
|
|
343
|
+
const stamp2 = runtime.rt.lastNativeCompactAt;
|
|
344
|
+
const liveSid2 = runtime.rt.sessionId;
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
try {
|
|
347
|
+
if (runtime.rt.sessionId !== liveSid2)
|
|
348
|
+
return; // session reset
|
|
349
|
+
const since3 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
350
|
+
if (runtime.rt.lastNativeCompactAt !== stamp2 && since3 < cooldownMs2)
|
|
351
|
+
return;
|
|
352
|
+
if (piCompactWouldNoop(ctx))
|
|
353
|
+
return;
|
|
354
|
+
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. Forced re-compact after a context-overflow 400.
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
/* non-fatal */
|
|
358
|
+
}
|
|
359
|
+
}, 500);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
315
363
|
else {
|
|
316
364
|
// (5) transient or permanent — retry with exponential backoff.
|
|
317
365
|
// S38.7: hard-stop switch — bypass ALL retry logic when set.
|
|
@@ -388,8 +436,8 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
388
436
|
count: runtime.rt.errorRetryCount,
|
|
389
437
|
max,
|
|
390
438
|
});
|
|
391
|
-
// PREVENT-PI-003: user-role sendUserMessage only.
|
|
392
|
-
pi
|
|
439
|
+
// PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
|
|
440
|
+
await safeSendUserMessage(pi, '[mega-compact] the last turn ended with an error; please retry.');
|
|
393
441
|
}
|
|
394
442
|
}
|
|
395
443
|
}
|
|
@@ -3,6 +3,7 @@ import { estimateBlockTokens } from "../../src/tokens.js";
|
|
|
3
3
|
import { recordScore, getDedupStats } from "../../src/store/sqlite.js";
|
|
4
4
|
import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
|
|
5
5
|
import { resolveRepoRoot } from "../mega-config.js";
|
|
6
|
+
import { safeSendUserMessage } from "./send-safe.js";
|
|
6
7
|
/**
|
|
7
8
|
* Build a minimal fallback compaction so pi never runs its throwing compact().
|
|
8
9
|
*
|
|
@@ -34,13 +35,18 @@ function fallbackCompaction(event) {
|
|
|
34
35
|
/**
|
|
35
36
|
* Debounced resume-nudge: restart the agent loop after a compaction (which
|
|
36
37
|
* may have stopped it). Idempotent — one nudge per 30s, never blocks.
|
|
38
|
+
*
|
|
39
|
+
* Uses safeSendUserMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
|
|
40
|
+
* nudge fired during session_before_compact (which is mid-prompt-submission,
|
|
41
|
+
* so the agent can be busy) QUEUES instead of throwing
|
|
42
|
+
* "Agent is already processing. Specify streamingBehavior (steer or followUp)".
|
|
37
43
|
*/
|
|
38
|
-
function nudgeResume(pi, runtime) {
|
|
44
|
+
async function nudgeResume(pi, runtime) {
|
|
39
45
|
try {
|
|
40
46
|
const now = Date.now();
|
|
41
47
|
if (now >= runtime.resumeNudgeUntil) {
|
|
42
48
|
runtime.resumeNudgeUntil = now + 30_000;
|
|
43
|
-
pi
|
|
49
|
+
await safeSendUserMessage(pi, "[mega-compact] continue from the compacted context above.");
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
catch {
|
|
@@ -83,7 +89,7 @@ export function registerCompactHandlers(pi, runtime, config) {
|
|
|
83
89
|
tokensBefore: result.compaction.tokensBefore,
|
|
84
90
|
summaryTokens: result.compaction.estimatedTokensAfter,
|
|
85
91
|
});
|
|
86
|
-
nudgeResume(pi, runtime);
|
|
92
|
+
await nudgeResume(pi, runtime);
|
|
87
93
|
return { compaction: result.compaction };
|
|
88
94
|
}
|
|
89
95
|
// FIX "compacts but doesn't resume" + "Nothing to compact" regression:
|
|
@@ -104,7 +110,7 @@ export function registerCompactHandlers(pi, runtime, config) {
|
|
|
104
110
|
tokensBefore: fb.compaction.tokensBefore,
|
|
105
111
|
reason: event.reason,
|
|
106
112
|
});
|
|
107
|
-
nudgeResume(pi, runtime);
|
|
113
|
+
await nudgeResume(pi, runtime);
|
|
108
114
|
return { compaction: fb.compaction };
|
|
109
115
|
}
|
|
110
116
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* exclusively (its agent_end nudge path is separate and must not be doubled).
|
|
12
12
|
*
|
|
13
13
|
* @param message the event.message (a pi AgentMessage) or an error string
|
|
14
|
-
* @returns 'transient' | 'permanent' | 'compaction-noop' | null (success/unknown)
|
|
14
|
+
* @returns 'transient' | 'permanent' | 'compaction-noop' | 'context-overflow' | null (success/unknown)
|
|
15
15
|
*/
|
|
16
16
|
export function classifyError(message) {
|
|
17
17
|
// Resolve a searchable text blob from a pi AgentMessage or raw string.
|
|
@@ -84,6 +84,29 @@ export function classifyError(message) {
|
|
|
84
84
|
return 'compaction-noop';
|
|
85
85
|
if (/auto[\s-]?compaction failed/.test(s))
|
|
86
86
|
return 'compaction-noop';
|
|
87
|
+
// --- context-overflow (ORDER BEFORE generic transient!) ---
|
|
88
|
+
// A 400 from the model meaning "the prompt is bigger than the context window."
|
|
89
|
+
// Catches BOTH provider phrasings so the classification does not depend on
|
|
90
|
+
// which backend the router landed on:
|
|
91
|
+
// - pi/Anthropic wrapper: "too long for this model's context window even
|
|
92
|
+
// after compaction. Reduce the conversation length..." ->
|
|
93
|
+
// too long | context window | even after compaction | reduce the conversation
|
|
94
|
+
// - OpenAI/OpenRouter provider-side (often wrapped in "All targets failed:
|
|
95
|
+
// <model>. Last error: ..."): "This model's maximum context length is N
|
|
96
|
+
// tokens. Your request requires at least M tokens. Please reduce your
|
|
97
|
+
// input or max_tokens." -> maximum context length | context length
|
|
98
|
+
// exceeded | requires at least N tokens | reduce your input
|
|
99
|
+
// All of these carry `invalid_request_error` (UNDERSCORE, not 'invalid
|
|
100
|
+
// request' space) and would otherwise fall through to the generic
|
|
101
|
+
// `s.includes('error')` transient branch below, misclassifying as
|
|
102
|
+
// 'transient' and firing up to 5 blind retry nudges that re-submit the same
|
|
103
|
+
// oversized prompt -> re-400 -> busy-loop. 'context-overflow' instead forces
|
|
104
|
+
// ONE deferred re-compact (debounce-bypassed, race-guarded) and fires NO
|
|
105
|
+
// blind retry nudge. The forced re-compact is shaped by the existing
|
|
106
|
+
// session_before_compact durable trim (it cannot lower pi's firstKeptEntryId).
|
|
107
|
+
if (/too long|context window|maximum context length|context length exceeded|requires at least \d+ tokens|even after compaction|reduce the conversation|reduce your input/.test(s)) {
|
|
108
|
+
return 'context-overflow';
|
|
109
|
+
}
|
|
87
110
|
// --- transient (retryable) ---
|
|
88
111
|
if (s.includes('error') && !/\b(permanent|invalid request|malformed|bad request|auth|unauthorized|invalid (api )?key|permission)\b/.test(s)) {
|
|
89
112
|
return 'transient'; // generic pi stopReason 'error' / 'aborted'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safeSendUserMessage — await + catch-guard + queue-safe wrapper for
|
|
3
|
+
* pi.sendUserMessage. Never throws; never produces an unhandled rejection.
|
|
4
|
+
*/
|
|
5
|
+
export async function safeSendUserMessage(pi, content) {
|
|
6
|
+
try {
|
|
7
|
+
await pi.sendUserMessage(content, { deliverAs: "followUp" });
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
/* non-fatal: a failed/queued nudge never blocks the agent loop */
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -15,7 +15,7 @@ import { VectorStore } from "../../src/vectorStore.js";
|
|
|
15
15
|
import { toEngineMessages } from "../../src/adapt.js";
|
|
16
16
|
import { normalizeSessionId } from "../../src/store.js";
|
|
17
17
|
import { Logger } from "../../src/log.js";
|
|
18
|
-
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, getDedupStats, getCompactCount, getRecallInjected, getCacheHitTokensSaved, getGameState, recordPerfSample, } from "../../src/store/sqlite.js";
|
|
18
|
+
import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, getDedupStats, getCompactCount, getRecallInjected, getCacheHitTokensSaved, getGameState, recordPerfSample, recordSessionHeartbeat, appendTokenSample, } from "../../src/store/sqlite.js";
|
|
19
19
|
import { detectCrossRepoDrift } from "../../src/driftDetection.js";
|
|
20
20
|
import { repoStateDir, resolveRepoRoot, pressureRatio, pressureFromPct, pressureBand, effectiveThresholdTokens, } from "../mega-config.js";
|
|
21
21
|
import { Dashboard } from "../mega-dashboard.js";
|
|
@@ -513,6 +513,22 @@ export class MegaRuntime {
|
|
|
513
513
|
},
|
|
514
514
|
});
|
|
515
515
|
const perfDiskMs = this.dashboard.lastWriteMs;
|
|
516
|
+
// S39: record a session heartbeat + token sample into the shared
|
|
517
|
+
// machine-wide index.sqlite so the dashboard can show a real-time
|
|
518
|
+
// stacked-memory graph across all active pi processes. Behind the
|
|
519
|
+
// material-change gate (this code only runs when sig changed). Non-fatal
|
|
520
|
+
// try/catch mirrors the recordPerfSample pattern below. Skip the token
|
|
521
|
+
// sample when lastCtxTokens is null (no context data yet).
|
|
522
|
+
try {
|
|
523
|
+
const repo = resolveRepoRoot(ctx?.cwd ?? this.currentStateDir) ?? this.currentStateDir;
|
|
524
|
+
recordSessionHeartbeat(process.pid, this.rt.sessionId, repo, this.currentStateDir, this.lastCtxWindow || 0);
|
|
525
|
+
if (this.lastCtxTokens != null) {
|
|
526
|
+
appendTokenSample(this.rt.sessionId, repo, this.lastCtxTokens, this.lastCtxPercent ?? 0, this.lastCtxWindow || 0, join(this.currentStateDir, "events.log"));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
/* non-fatal: S39 monitoring must never block the snapshot path */
|
|
531
|
+
}
|
|
516
532
|
// Live stats widget above the editor
|
|
517
533
|
if (ctx) {
|
|
518
534
|
// ── gather widget data (computed per snapshot, rendered per frame) ────
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repo SQLite connection management (open / reuse / close).
|
|
3
|
+
*
|
|
4
|
+
* In-process cache so the same stateDir reuses one connection (and so a fresh
|
|
5
|
+
* VectorStore over the same dir shares the open DB). Cross-process durability
|
|
6
|
+
* comes from reopening the same file path — proven by the integration test.
|
|
7
|
+
*/
|
|
8
|
+
import { DatabaseSync } from "node:sqlite";
|
|
9
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getStateDir } from "../../store.js";
|
|
12
|
+
import { initSchema } from "./schema.js";
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
/** Open (or reuse) the SQLite store for a state dir. */
|
|
15
|
+
export function openStore(stateDir = getStateDir()) {
|
|
16
|
+
const existing = cache.get(stateDir);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
if (!existsSync(stateDir))
|
|
20
|
+
mkdirSync(stateDir, { recursive: true });
|
|
21
|
+
const db = new DatabaseSync(join(stateDir, "sqlite.db"));
|
|
22
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
24
|
+
initSchema(db);
|
|
25
|
+
cache.set(stateDir, db);
|
|
26
|
+
return db;
|
|
27
|
+
}
|
|
28
|
+
/** Close and evict a cached connection (test teardown only). */
|
|
29
|
+
export function closeStore(stateDir) {
|
|
30
|
+
const db = cache.get(stateDir);
|
|
31
|
+
if (db) {
|
|
32
|
+
db.close();
|
|
33
|
+
cache.delete(stateDir);
|
|
34
|
+
}
|
|
35
|
+
}
|