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.
Files changed (44) hide show
  1. package/dist/assets/{TiptapBody-Btu-_mZq.js → TiptapBody-D0tfDVZb.js} +1 -1
  2. package/dist/assets/{cssMode-EkBJI3CN.js → cssMode-C3tZkaJ9.js} +1 -1
  3. package/dist/assets/{freemarker2-DFbgmGC_.js → freemarker2-DEh8tC5X.js} +1 -1
  4. package/dist/assets/{handlebars-CUy9oTnL.js → handlebars-D_6KmsOK.js} +1 -1
  5. package/dist/assets/{html-CUFKxIqK.js → html-DF1KVjzv.js} +1 -1
  6. package/dist/assets/{htmlMode-hBSk7Fab.js → htmlMode-DEakPokt.js} +1 -1
  7. package/dist/assets/{index-D4dlTF2R.css → index-D-kX3T0V.css} +1 -1
  8. package/dist/assets/{index-BGIAQ9_i.js → index-Zg61GP50.js} +1416 -1115
  9. package/dist/assets/{javascript-CAH2ooMO.js → javascript-Du7a359D.js} +1 -1
  10. package/dist/assets/{jsonMode-BSMCRMaw.js → jsonMode-W3BJwbUD.js} +1 -1
  11. package/dist/assets/{liquid-CSMs05cs.js → liquid-DDj1fqca.js} +1 -1
  12. package/dist/assets/{lspLanguageFeatures-DEyYbu0m.js → lspLanguageFeatures-uyEbiR-d.js} +1 -1
  13. package/dist/assets/{mdx-BocaqTLI.js → mdx-DUqSETvC.js} +1 -1
  14. package/dist/assets/{python-Dj6u2CWq.js → python-D7S2lUAn.js} +1 -1
  15. package/dist/assets/{razor-D5OWmIwh.js → razor-19nfZNaZ.js} +1 -1
  16. package/dist/assets/{tsMode-Cy6xJd5e.js → tsMode-CQke5zpL.js} +1 -1
  17. package/dist/assets/{typescript-CsWWOqVn.js → typescript-D4ge0PUF.js} +1 -1
  18. package/dist/assets/{xml-DiTZK7ii.js → xml-D42QDS7q.js} +1 -1
  19. package/dist/assets/{yaml-pdLhZQr9.js → yaml-CEiz9NyU.js} +1 -1
  20. package/dist/index.html +2 -2
  21. package/package.json +2 -1
  22. package/src/main/__tests__/runVerify.test.cjs +388 -0
  23. package/src/main/config.cjs +1 -7
  24. package/src/main/files.cjs +4 -37
  25. package/src/main/historyAggregator.cjs +37 -14
  26. package/src/main/index.cjs +57 -134
  27. package/src/main/ipcSchemas.cjs +4 -0
  28. package/src/main/lib/childWithLog.cjs +218 -0
  29. package/src/main/lib/expandHome.cjs +21 -0
  30. package/src/main/lib/insideHome.cjs +74 -21
  31. package/src/main/lib/openExternalApp.cjs +141 -0
  32. package/src/main/pluginInstall.cjs +39 -1
  33. package/src/main/pty.cjs +11 -4
  34. package/src/main/queueOps.cjs +2 -2
  35. package/src/main/runVerify.cjs +527 -0
  36. package/src/main/scheduler.cjs +335 -286
  37. package/src/main/search.cjs +1 -6
  38. package/src/main/superagent.cjs +10 -0
  39. package/src/main/supervisor.cjs +16 -8
  40. package/src/main/transcripts.cjs +18 -0
  41. package/src/main/usageMatrix.cjs +336 -0
  42. package/src/main/watchers.cjs +2 -2
  43. package/src/preload/api.d.ts +68 -7
  44. package/src/preload/index.cjs +9 -0
@@ -53,12 +53,7 @@ function probeRipgrep() {
53
53
  }
54
54
 
55
55
  // ── helpers ───────────────────────────────────────────────────────────────
56
- function expandHome(p) {
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.
@@ -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,
@@ -154,15 +154,23 @@ function appendSupervisorLog(entry) {
154
154
  }
155
155
 
156
156
  function readSupervisorLog(n) {
157
- try {
158
- const text = fs.readFileSync(SUPERVISOR_LOG_PATH, 'utf8');
159
- const lines = text.split('\n').filter((l) => l.trim());
160
- return lines.slice(-n).map((l) => {
161
- try { return JSON.parse(l); } catch { return null; }
162
- }).filter(Boolean).reverse();
163
- } catch {
164
- return [];
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 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 ──────────────────────────────────────────────────────────────────
@@ -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
+ };
@@ -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 { assertCwdInsideHome } = require('./lib/insideHome.cjs');
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 = assertCwdInsideHome(resolvedCwd);
64
+ const r = checkInsideHome(resolvedCwd);
65
65
  if (!r.ok) throw new Error(`watcher ${r.error}`);
66
66
 
67
67
  const watcherId = crypto.randomUUID();
@@ -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; editor?: string; error?: string }>;
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; editor?: string; error?: string }>;
754
- openInFinder: (cwd: string) => Promise<{ ok: boolean; error?: string }>;
755
- openInTerminal: (cwd: string) => Promise<{ ok: boolean; terminal?: string; error?: string }>;
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>;
@@ -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);