claude-code-session-manager 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{TiptapBody-Btu-_mZq.js → TiptapBody-D0tfDVZb.js} +1 -1
- package/dist/assets/{cssMode-EkBJI3CN.js → cssMode-C3tZkaJ9.js} +1 -1
- package/dist/assets/{freemarker2-DFbgmGC_.js → freemarker2-DEh8tC5X.js} +1 -1
- package/dist/assets/{handlebars-CUy9oTnL.js → handlebars-D_6KmsOK.js} +1 -1
- package/dist/assets/{html-CUFKxIqK.js → html-DF1KVjzv.js} +1 -1
- package/dist/assets/{htmlMode-hBSk7Fab.js → htmlMode-DEakPokt.js} +1 -1
- package/dist/assets/{index-D4dlTF2R.css → index-D-kX3T0V.css} +1 -1
- package/dist/assets/{index-BGIAQ9_i.js → index-Zg61GP50.js} +1416 -1115
- package/dist/assets/{javascript-CAH2ooMO.js → javascript-Du7a359D.js} +1 -1
- package/dist/assets/{jsonMode-BSMCRMaw.js → jsonMode-W3BJwbUD.js} +1 -1
- package/dist/assets/{liquid-CSMs05cs.js → liquid-DDj1fqca.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DEyYbu0m.js → lspLanguageFeatures-uyEbiR-d.js} +1 -1
- package/dist/assets/{mdx-BocaqTLI.js → mdx-DUqSETvC.js} +1 -1
- package/dist/assets/{python-Dj6u2CWq.js → python-D7S2lUAn.js} +1 -1
- package/dist/assets/{razor-D5OWmIwh.js → razor-19nfZNaZ.js} +1 -1
- package/dist/assets/{tsMode-Cy6xJd5e.js → tsMode-CQke5zpL.js} +1 -1
- package/dist/assets/{typescript-CsWWOqVn.js → typescript-D4ge0PUF.js} +1 -1
- package/dist/assets/{xml-DiTZK7ii.js → xml-D42QDS7q.js} +1 -1
- package/dist/assets/{yaml-pdLhZQr9.js → yaml-CEiz9NyU.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/main/__tests__/runVerify.test.cjs +388 -0
- package/src/main/config.cjs +1 -7
- package/src/main/files.cjs +4 -37
- package/src/main/historyAggregator.cjs +37 -14
- package/src/main/index.cjs +57 -134
- package/src/main/ipcSchemas.cjs +4 -0
- package/src/main/lib/childWithLog.cjs +218 -0
- package/src/main/lib/expandHome.cjs +21 -0
- package/src/main/lib/insideHome.cjs +74 -21
- package/src/main/lib/openExternalApp.cjs +141 -0
- package/src/main/pluginInstall.cjs +39 -1
- package/src/main/pty.cjs +11 -4
- package/src/main/queueOps.cjs +2 -2
- package/src/main/runVerify.cjs +527 -0
- package/src/main/scheduler.cjs +335 -286
- package/src/main/search.cjs +1 -6
- package/src/main/superagent.cjs +10 -0
- package/src/main/supervisor.cjs +16 -8
- package/src/main/transcripts.cjs +18 -0
- package/src/main/usageMatrix.cjs +336 -0
- package/src/main/watchers.cjs +2 -2
- package/src/preload/api.d.ts +68 -7
- package/src/preload/index.cjs +9 -0
package/src/main/search.cjs
CHANGED
|
@@ -53,12 +53,7 @@ function probeRipgrep() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// ── helpers ───────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
if (!p) return p;
|
|
58
|
-
if (p === '~') return os.homedir();
|
|
59
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
60
|
-
return p;
|
|
61
|
-
}
|
|
56
|
+
const { expandHome } = require('./lib/expandHome.cjs');
|
|
62
57
|
|
|
63
58
|
/**
|
|
64
59
|
* Spawn rg with argv (no shell), capture stdout/stderr, enforce TIMEOUT_MS.
|
package/src/main/superagent.cjs
CHANGED
|
@@ -176,6 +176,15 @@ function getStatus(tabId) {
|
|
|
176
176
|
return runs.get(tabId) ?? null;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function dropTab(tabId) {
|
|
180
|
+
if (runs.has(tabId)) {
|
|
181
|
+
runs.delete(tabId);
|
|
182
|
+
if (process.env.SM_DEBUG_LEAKS === '1') {
|
|
183
|
+
console.log('[superagent] dropTab', tabId, 'runs.size=', runs.size);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
179
188
|
function registerSuperAgentHandlers() {
|
|
180
189
|
ipcMain.handle('superagent:start', (_e, payload) => {
|
|
181
190
|
const parsed = schemas.superagentStart.parse(payload);
|
|
@@ -196,6 +205,7 @@ function registerSuperAgentHandlers() {
|
|
|
196
205
|
module.exports = {
|
|
197
206
|
attachWindow,
|
|
198
207
|
registerSuperAgentHandlers,
|
|
208
|
+
dropTab,
|
|
199
209
|
// Exposed for tests.
|
|
200
210
|
buildBossPrompt,
|
|
201
211
|
_runs: runs,
|
package/src/main/supervisor.cjs
CHANGED
|
@@ -154,15 +154,23 @@ function appendSupervisorLog(entry) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function readSupervisorLog(n) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
const lines = [];
|
|
158
|
+
for (const p of [SUPERVISOR_LOG_PATH + '.1', SUPERVISOR_LOG_PATH]) {
|
|
159
|
+
let stat;
|
|
160
|
+
try { stat = fs.statSync(p); } catch { continue; }
|
|
161
|
+
// Sanity-cap: skip any file larger than 2× the rotation ceiling.
|
|
162
|
+
// The rotated file is bounded to SUPERVISOR_LOG_MAX_BYTES by policy;
|
|
163
|
+
// anything larger indicates corruption or a hand-edit — skip it.
|
|
164
|
+
if (stat.size > 2 * SUPERVISOR_LOG_MAX_BYTES) continue;
|
|
165
|
+
try {
|
|
166
|
+
const t = fs.readFileSync(p, 'utf8');
|
|
167
|
+
for (const l of t.split('\n')) if (l.trim()) lines.push(l);
|
|
168
|
+
} catch { /* file vanished between stat and read — skip */ }
|
|
165
169
|
}
|
|
170
|
+
return lines.slice(-n)
|
|
171
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.reverse();
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
// ─── Probe ──────────────────────────────────────────────────────────────────
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -25,6 +25,7 @@ const os = require('node:os');
|
|
|
25
25
|
const chokidar = require('chokidar');
|
|
26
26
|
const otel = require('./otel.cjs');
|
|
27
27
|
const logs = require('./logs.cjs');
|
|
28
|
+
const usageMatrix = require('./usageMatrix.cjs');
|
|
28
29
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
29
30
|
|
|
30
31
|
let window = null;
|
|
@@ -142,6 +143,14 @@ async function flush(sub, { emit = true } = {}) {
|
|
|
142
143
|
// Ring buffer (cap at 500 entries to bound memory).
|
|
143
144
|
sub.buffer.push(ev);
|
|
144
145
|
if (sub.buffer.length > 500) sub.buffer.shift();
|
|
146
|
+
// Feed the AgOps aggregator on every event — both replay and live, so
|
|
147
|
+
// freshly-attached tabs land with full history reflected in the matrix.
|
|
148
|
+
usageMatrix.recordEvent({
|
|
149
|
+
tabId: sub.tabId,
|
|
150
|
+
cwd: sub.cwd,
|
|
151
|
+
sessionUuid: sub.sessionUuid,
|
|
152
|
+
ev,
|
|
153
|
+
});
|
|
145
154
|
if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, ev);
|
|
146
155
|
// Mirror to OTEL — no-op when disabled. We emit on the initial drain too
|
|
147
156
|
// so backfilled transcripts show up in the trace store.
|
|
@@ -160,6 +169,12 @@ const MAX_TRANSCRIPT_SUBS = 20;
|
|
|
160
169
|
async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
161
170
|
if (subs.has(tabId)) return { ok: true, path: subs.get(tabId).filePath };
|
|
162
171
|
if (subs.size >= MAX_TRANSCRIPT_SUBS) {
|
|
172
|
+
logs.writeLine({
|
|
173
|
+
level: 'warn',
|
|
174
|
+
scope: 'transcripts',
|
|
175
|
+
message: 'subscribe rejected: at cap',
|
|
176
|
+
meta: { tabId, cap: MAX_TRANSCRIPT_SUBS, cwd },
|
|
177
|
+
});
|
|
163
178
|
return { ok: false, path: null, error: 'too many active subscriptions' };
|
|
164
179
|
}
|
|
165
180
|
const filePath = transcriptPath(cwd, sessionUuid);
|
|
@@ -200,6 +215,8 @@ function unsubscribe(tabId) {
|
|
|
200
215
|
if (!sub) return;
|
|
201
216
|
sub.watcher?.close().catch(() => {});
|
|
202
217
|
subs.delete(tabId);
|
|
218
|
+
// Drop the tab from the AgOps matrix — "active sessions" only.
|
|
219
|
+
usageMatrix.removeTab(tabId);
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
function getBuffer(tabId) {
|
|
@@ -210,6 +227,7 @@ function getBuffer(tabId) {
|
|
|
210
227
|
function closeAll() {
|
|
211
228
|
for (const sub of subs.values()) sub.watcher?.close().catch(() => {});
|
|
212
229
|
subs.clear();
|
|
230
|
+
usageMatrix.closeAll();
|
|
213
231
|
}
|
|
214
232
|
|
|
215
233
|
function registerTranscriptHandlers() {
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usageMatrix.cjs — per-tab agentic-ops aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Watches the same classified transcript events that transcripts.cjs streams
|
|
5
|
+
* to the renderer and maintains a small rolling state per tab: turn count,
|
|
6
|
+
* token velocity (5-min window), cache age, subagent fan-out, and behavioral
|
|
7
|
+
* flags (geometric context growth, cache cold-start risk, long session).
|
|
8
|
+
*
|
|
9
|
+
* Lives in main rather than the renderer so a renderer reload does not lose
|
|
10
|
+
* the rolling counters — multi-hour sessions are the primary use case and
|
|
11
|
+
* losing the velocity series at a reload would make the dashboard useless
|
|
12
|
+
* exactly when it matters most.
|
|
13
|
+
*
|
|
14
|
+
* Broadcasts `usage:matrix:tick` on a 5-second cadence (and once on demand
|
|
15
|
+
* via the `usage:matrix:snapshot` handler at mount).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { ipcMain } = require('electron');
|
|
22
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
23
|
+
|
|
24
|
+
const TICK_MS = 5_000;
|
|
25
|
+
const TOKEN_WINDOW_MS = 5 * 60_000; // 5 min rolling window for tokens/min
|
|
26
|
+
const TURN_RING = 20; // per-turn input-token series cap
|
|
27
|
+
const TOOL_RING = 100; // recent tool-name series cap
|
|
28
|
+
const CACHE_TTL_MS = 60 * 60_000; // Anthropic prefix cache ~60 min
|
|
29
|
+
const CACHE_WARN_MS = 50 * 60_000; // start warning at 50 min
|
|
30
|
+
|
|
31
|
+
const BURN_LOW = 5_000; // tokens/min
|
|
32
|
+
const BURN_CRITICAL = 30_000; // tokens/min
|
|
33
|
+
const LONG_SESSION_TURNS = 70;
|
|
34
|
+
const GEOMETRIC_MIN_TOKENS = 50_000; // don't flag tiny series
|
|
35
|
+
|
|
36
|
+
/** Map<tabId, TabState> */
|
|
37
|
+
const tabs = new Map();
|
|
38
|
+
let window = null;
|
|
39
|
+
let tickTimer = null;
|
|
40
|
+
let dirty = false;
|
|
41
|
+
|
|
42
|
+
function attachWindow(w) {
|
|
43
|
+
window = w;
|
|
44
|
+
if (!tickTimer) {
|
|
45
|
+
tickTimer = setInterval(broadcastIfChanged, TICK_MS);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function workspaceName(cwd) {
|
|
50
|
+
if (!cwd) return '(unknown)';
|
|
51
|
+
return path.basename(cwd) || cwd;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function blankTab(tabId, cwd, sessionUuid) {
|
|
55
|
+
return {
|
|
56
|
+
tabId,
|
|
57
|
+
cwd,
|
|
58
|
+
sessionUuid,
|
|
59
|
+
workspace: workspaceName(cwd),
|
|
60
|
+
startedAt: Date.now(),
|
|
61
|
+
lastEventAt: 0,
|
|
62
|
+
lastAssistantAt: 0,
|
|
63
|
+
lastUserAt: 0,
|
|
64
|
+
lastPlanAt: 0,
|
|
65
|
+
turns: 0,
|
|
66
|
+
cumulativeUsage: { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 },
|
|
67
|
+
perTurnInputTokens: [],
|
|
68
|
+
tokenWindow: [], // [{ts, tokens}]
|
|
69
|
+
toolSeries: [], // [{ts, name}]
|
|
70
|
+
agentSpawns: 0,
|
|
71
|
+
agentsActive: new Map(), // toolUseId -> {at}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureTab(tabId, cwd, sessionUuid) {
|
|
76
|
+
let t = tabs.get(tabId);
|
|
77
|
+
if (!t) {
|
|
78
|
+
t = blankTab(tabId, cwd, sessionUuid);
|
|
79
|
+
tabs.set(tabId, t);
|
|
80
|
+
}
|
|
81
|
+
return t;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Feed one classified transcript event into the per-tab aggregator. Called
|
|
86
|
+
* from transcripts.cjs for every event, both during replay and live.
|
|
87
|
+
*/
|
|
88
|
+
function recordEvent({ tabId, cwd, sessionUuid, ev }) {
|
|
89
|
+
if (!tabId || !ev) return;
|
|
90
|
+
const t = ensureTab(tabId, cwd, sessionUuid);
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
t.lastEventAt = now;
|
|
93
|
+
|
|
94
|
+
switch (ev.kind) {
|
|
95
|
+
case 'usage': {
|
|
96
|
+
// Each usage rollup = one assistant turn in Claude Code's transcript.
|
|
97
|
+
const u = ev.data || {};
|
|
98
|
+
const inTok = (u.input_tokens || 0)
|
|
99
|
+
+ (u.cache_creation_input_tokens || 0)
|
|
100
|
+
+ (u.cache_read_input_tokens || 0);
|
|
101
|
+
const outTok = u.output_tokens || 0;
|
|
102
|
+
t.cumulativeUsage.input += u.input_tokens || 0;
|
|
103
|
+
t.cumulativeUsage.output += outTok;
|
|
104
|
+
t.cumulativeUsage.cacheRead += u.cache_read_input_tokens || 0;
|
|
105
|
+
t.cumulativeUsage.cacheCreate += u.cache_creation_input_tokens || 0;
|
|
106
|
+
t.turns += 1;
|
|
107
|
+
t.lastAssistantAt = now;
|
|
108
|
+
|
|
109
|
+
t.perTurnInputTokens.push(inTok);
|
|
110
|
+
if (t.perTurnInputTokens.length > TURN_RING) t.perTurnInputTokens.shift();
|
|
111
|
+
|
|
112
|
+
t.tokenWindow.push({ ts: now, tokens: inTok + outTok });
|
|
113
|
+
pruneWindow(t.tokenWindow, now);
|
|
114
|
+
dirty = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'tool_use': {
|
|
118
|
+
const name = ev.data?.name;
|
|
119
|
+
if (name) {
|
|
120
|
+
t.toolSeries.push({ ts: now, name });
|
|
121
|
+
if (t.toolSeries.length > TOOL_RING) t.toolSeries.shift();
|
|
122
|
+
}
|
|
123
|
+
dirty = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'agent_spawn': {
|
|
127
|
+
t.agentSpawns += 1;
|
|
128
|
+
const id = ev.data?.toolUseId;
|
|
129
|
+
if (id) t.agentsActive.set(id, { at: now });
|
|
130
|
+
dirty = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'tool_result': {
|
|
134
|
+
const id = ev.data?.toolUseId;
|
|
135
|
+
if (id && t.agentsActive.has(id)) {
|
|
136
|
+
t.agentsActive.delete(id);
|
|
137
|
+
dirty = true;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'plan': {
|
|
142
|
+
t.lastPlanAt = now;
|
|
143
|
+
dirty = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pruneWindow(arr, now) {
|
|
152
|
+
const cutoff = now - TOKEN_WINDOW_MS;
|
|
153
|
+
while (arr.length && arr[0].ts < cutoff) arr.shift();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function removeTab(tabId) {
|
|
157
|
+
if (tabs.delete(tabId)) dirty = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a derived snapshot. Heavy work happens here, not on the hot
|
|
162
|
+
* recordEvent path — recordEvent only appends to ring buffers and flips a
|
|
163
|
+
* dirty bit.
|
|
164
|
+
*/
|
|
165
|
+
function buildSnapshot() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const out = [];
|
|
168
|
+
|
|
169
|
+
let totalSubagentsActive = 0;
|
|
170
|
+
let totalSubagentsSpawned = 0;
|
|
171
|
+
let totalTokensToday = 0;
|
|
172
|
+
let combinedTokensPerMin = 0;
|
|
173
|
+
|
|
174
|
+
for (const t of tabs.values()) {
|
|
175
|
+
pruneWindow(t.tokenWindow, now);
|
|
176
|
+
const windowTokens = t.tokenWindow.reduce((s, e) => s + e.tokens, 0);
|
|
177
|
+
const windowMin = Math.min(TOKEN_WINDOW_MS, now - (t.tokenWindow[0]?.ts ?? now)) / 60_000;
|
|
178
|
+
const tokensPerMin = windowMin > 0.1 ? Math.round(windowTokens / windowMin) : 0;
|
|
179
|
+
combinedTokensPerMin += tokensPerMin;
|
|
180
|
+
|
|
181
|
+
const intensity = tokensPerMin >= BURN_CRITICAL
|
|
182
|
+
? 'critical'
|
|
183
|
+
: tokensPerMin >= BURN_LOW
|
|
184
|
+
? 'medium'
|
|
185
|
+
: tokensPerMin > 0
|
|
186
|
+
? 'low'
|
|
187
|
+
: 'idle';
|
|
188
|
+
|
|
189
|
+
const cacheAgeMs = t.lastAssistantAt ? now - t.lastAssistantAt : null;
|
|
190
|
+
const cacheState = cacheAgeMs == null
|
|
191
|
+
? 'cold'
|
|
192
|
+
: cacheAgeMs > CACHE_TTL_MS
|
|
193
|
+
? 'cold'
|
|
194
|
+
: cacheAgeMs > CACHE_WARN_MS
|
|
195
|
+
? 'expiring'
|
|
196
|
+
: 'warm';
|
|
197
|
+
|
|
198
|
+
const avgPerTurn = t.perTurnInputTokens.length > 0
|
|
199
|
+
? Math.round(t.perTurnInputTokens.reduce((s, n) => s + n, 0) / t.perTurnInputTokens.length)
|
|
200
|
+
: 0;
|
|
201
|
+
|
|
202
|
+
const geometricGrowth = detectGeometricGrowth(t.perTurnInputTokens);
|
|
203
|
+
|
|
204
|
+
// State derivation. Order matters — earlier branches win.
|
|
205
|
+
let state;
|
|
206
|
+
if (t.lastPlanAt > 0 && (now - t.lastPlanAt) < 60_000 && t.lastPlanAt >= t.lastAssistantAt) {
|
|
207
|
+
state = 'plan';
|
|
208
|
+
} else if (t.agentsActive.size > 0 && (now - t.lastAssistantAt) > 10_000) {
|
|
209
|
+
state = 'background';
|
|
210
|
+
} else if (t.lastEventAt > 0 && (now - t.lastEventAt) < 30_000) {
|
|
211
|
+
state = 'executing';
|
|
212
|
+
} else {
|
|
213
|
+
state = 'idle';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const subagentsActive = t.agentsActive.size;
|
|
217
|
+
totalSubagentsActive += subagentsActive;
|
|
218
|
+
totalSubagentsSpawned += t.agentSpawns;
|
|
219
|
+
totalTokensToday += t.cumulativeUsage.input + t.cumulativeUsage.output;
|
|
220
|
+
|
|
221
|
+
const alerts = [];
|
|
222
|
+
if (geometricGrowth) {
|
|
223
|
+
alerts.push({
|
|
224
|
+
level: 'warn',
|
|
225
|
+
code: 'geometric-growth',
|
|
226
|
+
message: `Per-turn input grew geometrically (avg ${(avgPerTurn / 1000).toFixed(0)}k). Run /compact.`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (t.turns >= LONG_SESSION_TURNS) {
|
|
230
|
+
alerts.push({
|
|
231
|
+
level: 'warn',
|
|
232
|
+
code: 'long-session',
|
|
233
|
+
message: `Session is at turn ${t.turns}. Context resubmissions cost ${(avgPerTurn / 1000).toFixed(0)}k tokens each.`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (cacheState === 'expiring' && state === 'idle') {
|
|
237
|
+
const minLeft = Math.max(0, Math.round((CACHE_TTL_MS - (cacheAgeMs ?? 0)) / 60_000));
|
|
238
|
+
alerts.push({
|
|
239
|
+
level: 'info',
|
|
240
|
+
code: 'cache-cold-start',
|
|
241
|
+
message: `Cache cold-start in ${minLeft}m. Next command will incur full cache_creation cost.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (cacheState === 'cold' && t.lastAssistantAt > 0) {
|
|
245
|
+
alerts.push({
|
|
246
|
+
level: 'info',
|
|
247
|
+
code: 'cache-cold',
|
|
248
|
+
message: 'Cache expired. Next command pays full cache_creation pricing.',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (subagentsActive >= 4) {
|
|
252
|
+
alerts.push({
|
|
253
|
+
level: 'info',
|
|
254
|
+
code: 'subagent-fanout',
|
|
255
|
+
message: `${subagentsActive} subagents in flight. Consider waiting before fanning out more.`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
out.push({
|
|
260
|
+
tabId: t.tabId,
|
|
261
|
+
workspace: t.workspace,
|
|
262
|
+
cwd: t.cwd,
|
|
263
|
+
state,
|
|
264
|
+
turns: t.turns,
|
|
265
|
+
lastEventAt: t.lastEventAt,
|
|
266
|
+
lastAssistantAt: t.lastAssistantAt,
|
|
267
|
+
cacheAgeMs,
|
|
268
|
+
cacheState,
|
|
269
|
+
tokensPerMin,
|
|
270
|
+
intensity,
|
|
271
|
+
avgTokensPerTurn: avgPerTurn,
|
|
272
|
+
cumulativeUsage: { ...t.cumulativeUsage },
|
|
273
|
+
subagentsActive,
|
|
274
|
+
subagentsSpawned: t.agentSpawns,
|
|
275
|
+
geometricGrowth,
|
|
276
|
+
alerts,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Most active first.
|
|
281
|
+
out.sort((a, b) => b.tokensPerMin - a.tokensPerMin || b.lastEventAt - a.lastEventAt);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
generatedAt: now,
|
|
285
|
+
tabs: out,
|
|
286
|
+
totals: {
|
|
287
|
+
activeSessions: out.length,
|
|
288
|
+
subagentsActive: totalSubagentsActive,
|
|
289
|
+
subagentsSpawned: totalSubagentsSpawned,
|
|
290
|
+
tokensTotal: totalTokensToday,
|
|
291
|
+
combinedTokensPerMin,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Geometric growth: avg of last 3 turns > 1.8 × avg of prior 3 turns,
|
|
298
|
+
* and last-3 avg above GEOMETRIC_MIN_TOKENS so trivial sessions don't flag.
|
|
299
|
+
*/
|
|
300
|
+
function detectGeometricGrowth(series) {
|
|
301
|
+
if (series.length < 6) return false;
|
|
302
|
+
const tail = series.slice(-3);
|
|
303
|
+
const prev = series.slice(-6, -3);
|
|
304
|
+
const tailAvg = tail.reduce((s, n) => s + n, 0) / 3;
|
|
305
|
+
const prevAvg = prev.reduce((s, n) => s + n, 0) / 3;
|
|
306
|
+
if (tailAvg < GEOMETRIC_MIN_TOKENS) return false;
|
|
307
|
+
if (prevAvg <= 0) return false;
|
|
308
|
+
return tailAvg / prevAvg >= 1.8;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function broadcastIfChanged() {
|
|
312
|
+
if (!dirty) return;
|
|
313
|
+
dirty = false;
|
|
314
|
+
if (!window) return;
|
|
315
|
+
sendIfAlive(window, 'usage:matrix:tick', buildSnapshot());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function registerHandlers() {
|
|
319
|
+
ipcMain.handle('usage:matrix:snapshot', () => buildSnapshot());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function closeAll() {
|
|
323
|
+
if (tickTimer) {
|
|
324
|
+
clearInterval(tickTimer);
|
|
325
|
+
tickTimer = null;
|
|
326
|
+
}
|
|
327
|
+
tabs.clear();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
attachWindow,
|
|
332
|
+
recordEvent,
|
|
333
|
+
removeTab,
|
|
334
|
+
registerHandlers,
|
|
335
|
+
closeAll,
|
|
336
|
+
};
|
package/src/main/watchers.cjs
CHANGED
|
@@ -15,7 +15,7 @@ const path = require('node:path');
|
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const fs = require('node:fs');
|
|
17
17
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
18
|
-
const {
|
|
18
|
+
const { checkInsideHome } = require('./lib/insideHome.cjs');
|
|
19
19
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
20
20
|
|
|
21
21
|
// Splits a readable stream into lines capped at maxLineBytes. Prevents OOM
|
|
@@ -61,7 +61,7 @@ class WatcherManager {
|
|
|
61
61
|
add({ tabId, label, command, cwd }) {
|
|
62
62
|
const resolvedCwd = cwd || process.cwd();
|
|
63
63
|
|
|
64
|
-
const r =
|
|
64
|
+
const r = checkInsideHome(resolvedCwd);
|
|
65
65
|
if (!r.ok) throw new Error(`watcher ${r.error}`);
|
|
66
66
|
|
|
67
67
|
const watcherId = crypto.randomUUID();
|
package/src/preload/api.d.ts
CHANGED
|
@@ -75,7 +75,8 @@ export interface TranscriptEvent {
|
|
|
75
75
|
|
|
76
76
|
export interface SubscribeResult {
|
|
77
77
|
ok: boolean;
|
|
78
|
-
path: string;
|
|
78
|
+
path: string | null;
|
|
79
|
+
error?: string;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export interface PersistedTab {
|
|
@@ -129,6 +130,54 @@ export type BillingFetchResult =
|
|
|
129
130
|
| { kind: 'transient'; message: string; httpStatus: number | null }
|
|
130
131
|
| { kind: 'config'; message: string };
|
|
131
132
|
|
|
133
|
+
// ── Usage matrix (AgOps dashboard, main-side aggregator)
|
|
134
|
+
export type UsageMatrixState = 'idle' | 'executing' | 'plan' | 'background';
|
|
135
|
+
export type UsageMatrixIntensity = 'idle' | 'low' | 'medium' | 'critical';
|
|
136
|
+
export type UsageMatrixCacheState = 'warm' | 'expiring' | 'cold';
|
|
137
|
+
|
|
138
|
+
export interface UsageMatrixAlert {
|
|
139
|
+
level: 'info' | 'warn';
|
|
140
|
+
code:
|
|
141
|
+
| 'geometric-growth'
|
|
142
|
+
| 'long-session'
|
|
143
|
+
| 'cache-cold-start'
|
|
144
|
+
| 'cache-cold'
|
|
145
|
+
| 'subagent-fanout';
|
|
146
|
+
message: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface UsageMatrixTab {
|
|
150
|
+
tabId: string;
|
|
151
|
+
workspace: string;
|
|
152
|
+
cwd: string;
|
|
153
|
+
state: UsageMatrixState;
|
|
154
|
+
turns: number;
|
|
155
|
+
lastEventAt: number;
|
|
156
|
+
lastAssistantAt: number;
|
|
157
|
+
cacheAgeMs: number | null;
|
|
158
|
+
cacheState: UsageMatrixCacheState;
|
|
159
|
+
tokensPerMin: number;
|
|
160
|
+
intensity: UsageMatrixIntensity;
|
|
161
|
+
avgTokensPerTurn: number;
|
|
162
|
+
cumulativeUsage: { input: number; output: number; cacheRead: number; cacheCreate: number };
|
|
163
|
+
subagentsActive: number;
|
|
164
|
+
subagentsSpawned: number;
|
|
165
|
+
geometricGrowth: boolean;
|
|
166
|
+
alerts: UsageMatrixAlert[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface UsageMatrixSnapshot {
|
|
170
|
+
generatedAt: number;
|
|
171
|
+
tabs: UsageMatrixTab[];
|
|
172
|
+
totals: {
|
|
173
|
+
activeSessions: number;
|
|
174
|
+
subagentsActive: number;
|
|
175
|
+
subagentsSpawned: number;
|
|
176
|
+
tokensTotal: number;
|
|
177
|
+
combinedTokensPerMin: number;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
132
181
|
export interface VoiceHotkeyConfig {
|
|
133
182
|
accelerator: string;
|
|
134
183
|
mode: 'hold' | 'toggle';
|
|
@@ -245,7 +294,7 @@ export interface ScheduleConfig {
|
|
|
245
294
|
schemaVersion: 1;
|
|
246
295
|
}
|
|
247
296
|
|
|
248
|
-
export type ScheduleJobStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
297
|
+
export type ScheduleJobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'needs_review';
|
|
249
298
|
|
|
250
299
|
export interface ScheduleJobRuntime {
|
|
251
300
|
pid: number;
|
|
@@ -275,6 +324,14 @@ export interface ScheduleJob {
|
|
|
275
324
|
* transcript even after restart. */
|
|
276
325
|
sessionId?: string;
|
|
277
326
|
runtime?: ScheduleJobRuntime;
|
|
327
|
+
/**
|
|
328
|
+
* Set when the post-run verifier downgrades the job.
|
|
329
|
+
* Values: 'halt' | 'deps_unmet' | 'transcript_errors' | 'verify_unavailable'
|
|
330
|
+
* Cleared when the job is reset to 'pending'.
|
|
331
|
+
*/
|
|
332
|
+
verifierVerdict?: string;
|
|
333
|
+
/** Per-job values carried in queue.json for dependency checking. */
|
|
334
|
+
dependsOn?: string[];
|
|
278
335
|
}
|
|
279
336
|
|
|
280
337
|
export interface SchedulePaths {
|
|
@@ -743,16 +800,16 @@ export interface SessionManagerAPI {
|
|
|
743
800
|
homeSelfCheck: () => Promise<{ ok: boolean; error?: string; realCwd?: string }>;
|
|
744
801
|
onNewSession: (handler: () => void) => () => void;
|
|
745
802
|
onRebootSession: (handler: () => void) => () => void;
|
|
746
|
-
openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean;
|
|
803
|
+
openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean; opener?: string; error?: string }>;
|
|
747
804
|
/** Open an http/https URL in the OS default browser. file://, javascript:,
|
|
748
805
|
* and other schemes are rejected with `ok:false` to prevent abuse. */
|
|
749
806
|
openExternal: (url: string) => Promise<{ ok: boolean; error?: string }>;
|
|
750
807
|
/** Open a specific file at line:col in the user's editor. Editors with
|
|
751
808
|
* goto-line support (code/cursor/subl) get the `-g file:line:col` form;
|
|
752
|
-
* others open the file alone. */
|
|
753
|
-
openFileInEditor: (filePath: string, line?: number, col?: number, editor?: string | null) => Promise<{ ok: boolean;
|
|
754
|
-
openInFinder: (cwd: string) => Promise<{ ok: boolean; error?: string }>;
|
|
755
|
-
openInTerminal: (cwd: string) => Promise<{ ok: boolean;
|
|
809
|
+
* others open the file alone. Image files are routed to the OS default viewer. */
|
|
810
|
+
openFileInEditor: (filePath: string, line?: number, col?: number, editor?: string | null) => Promise<{ ok: boolean; opener?: string; error?: string }>;
|
|
811
|
+
openInFinder: (cwd: string) => Promise<{ ok: boolean; opener?: string; error?: string }>;
|
|
812
|
+
openInTerminal: (cwd: string) => Promise<{ ok: boolean; opener?: string; error?: string }>;
|
|
756
813
|
archiveProject: (encoded: string) => Promise<{ ok: boolean; error?: string }>;
|
|
757
814
|
};
|
|
758
815
|
pty: {
|
|
@@ -778,6 +835,10 @@ export interface SessionManagerAPI {
|
|
|
778
835
|
billing: {
|
|
779
836
|
fetch: () => Promise<BillingFetchResult>;
|
|
780
837
|
};
|
|
838
|
+
usageMatrix: {
|
|
839
|
+
snapshot: () => Promise<UsageMatrixSnapshot>;
|
|
840
|
+
onTick: (handler: (snap: UsageMatrixSnapshot) => void) => () => void;
|
|
841
|
+
};
|
|
781
842
|
logs: {
|
|
782
843
|
write: (scope: string, level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: unknown) => void;
|
|
783
844
|
dir: () => Promise<string>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -76,6 +76,14 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
76
76
|
billing: {
|
|
77
77
|
fetch: () => ipcRenderer.invoke('billing:fetch'),
|
|
78
78
|
},
|
|
79
|
+
usageMatrix: {
|
|
80
|
+
snapshot: () => ipcRenderer.invoke('usage:matrix:snapshot'),
|
|
81
|
+
onTick: (handler) => {
|
|
82
|
+
const listener = (_e, payload) => handler(payload);
|
|
83
|
+
ipcRenderer.on('usage:matrix:tick', listener);
|
|
84
|
+
return () => ipcRenderer.removeListener('usage:matrix:tick', listener);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
79
87
|
logs: {
|
|
80
88
|
write: (scope, level, message, meta) =>
|
|
81
89
|
ipcRenderer.send('log:write', { scope, level, message, meta }),
|
|
@@ -211,6 +219,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
211
219
|
},
|
|
212
220
|
plugins: {
|
|
213
221
|
install: (payload) => ipcRenderer.invoke('plugins:install', payload),
|
|
222
|
+
abort: (slug) => ipcRenderer.invoke('plugins:abort', slug),
|
|
214
223
|
onInstallProgress: (handler) => {
|
|
215
224
|
const listener = (_e, payload) => handler(payload);
|
|
216
225
|
ipcRenderer.on('plugins:install-progress', listener);
|