claude-code-session-manager 0.38.4 → 0.38.6
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-Dz0cPmSN.js → TiptapBody-CTotrCub.js} +1 -1
- package/dist/assets/index-BGTU12rl.css +32 -0
- package/dist/assets/index-C54zva1x.js +3178 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +35 -9
- package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +16 -7
- package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +8 -5
- package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +7 -4
- package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +7 -3
- package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +23 -7
- package/src/main/__tests__/prdCreate.test.cjs +55 -1
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +65 -0
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +43 -0
- package/src/main/index.cjs +10 -10
- package/src/main/ipcSchemas.cjs +17 -16
- package/src/main/layoutStore.cjs +58 -0
- package/src/main/lib/prdCreate.cjs +66 -43
- package/src/main/pty.cjs +0 -5
- package/src/main/queueOps.cjs +58 -0
- package/src/main/scheduler.cjs +48 -3
- package/src/main/transcripts.cjs +2 -15
- package/src/preload/api.d.ts +39 -116
- package/src/preload/index.cjs +6 -27
- package/dist/assets/index-B45c4uKJ.js +0 -3504
- package/dist/assets/index-BZeC568j.css +0 -32
- package/src/main/hives.cjs +0 -287
- package/src/main/superagent.cjs +0 -212
- package/src/main/usageMatrix.cjs +0 -340
package/src/main/superagent.cjs
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SuperAgent — "boss" runner that dispatches specialist subagents.
|
|
3
|
-
*
|
|
4
|
-
* Ported (in MVP form) from ClaudeCodeUnleashed's SuperAgent feature.
|
|
5
|
-
* Differs significantly from upstream:
|
|
6
|
-
* - We don't call Groq/OpenAI. The boss IS Claude — we send a structured
|
|
7
|
-
* prompt to the active PTY tab asking Claude to act as the boss, pick
|
|
8
|
-
* specialists, and report progress in a parseable format.
|
|
9
|
-
* - No autonomous output-parsing / fast-path / WAIT logic. State machine
|
|
10
|
-
* is renderer-driven via transcript events (see live.ts pattern).
|
|
11
|
-
* - Per-tab run state lives here so the renderer can poll status across
|
|
12
|
-
* navigation, and so a future supervisor probe can act on stuck runs.
|
|
13
|
-
*
|
|
14
|
-
* Channels:
|
|
15
|
-
* superagent:start({ tabId, prompt, specialistCount, depth })
|
|
16
|
-
* → { ok, error? } and broadcasts the boss prompt to the tab's PTY.
|
|
17
|
-
* superagent:status(tabId) → SuperAgentRunState | null
|
|
18
|
-
* superagent:stop(tabId) → { ok }
|
|
19
|
-
*
|
|
20
|
-
* Broadcasts:
|
|
21
|
-
* superagent:state-changed → { tabId, state } whenever a run starts/stops.
|
|
22
|
-
*
|
|
23
|
-
* The PTY write is intentionally done via the existing ptyManager.write
|
|
24
|
-
* indirection, NOT a fresh spawn — SuperAgent runs inside the user's current
|
|
25
|
-
* Claude session.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
const { ipcMain } = require('electron');
|
|
29
|
-
const { schemas } = require('./ipcSchemas.cjs');
|
|
30
|
-
const { manager: ptyManager } = require('./pty.cjs');
|
|
31
|
-
const logs = require('./logs.cjs');
|
|
32
|
-
|
|
33
|
-
// Per-tab run state. Single live run per tab — starting a new run on a tab
|
|
34
|
-
// that's already running stops the existing one first.
|
|
35
|
-
//
|
|
36
|
-
// Shape (also exposed on the renderer via api.d.ts SuperAgentRunState):
|
|
37
|
-
// { status: 'idle' | 'running' | 'done' | 'error',
|
|
38
|
-
// prompt, specialistCount, depth,
|
|
39
|
-
// startedAt: number | null, finishedAt: number | null,
|
|
40
|
-
// error?: string }
|
|
41
|
-
const runs = new Map();
|
|
42
|
-
|
|
43
|
-
let mainWindow = null;
|
|
44
|
-
|
|
45
|
-
function attachWindow(win) {
|
|
46
|
-
mainWindow = win;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function broadcast(tabId) {
|
|
50
|
-
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
51
|
-
const state = runs.get(tabId) ?? null;
|
|
52
|
-
try {
|
|
53
|
-
mainWindow.webContents.send('superagent:state-changed', { tabId, state });
|
|
54
|
-
} catch { /* renderer gone */ }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build the prompt we send to Claude to act as the boss. Structured so the
|
|
59
|
-
* renderer can (in a follow-up) pattern-match progress markers off the
|
|
60
|
-
* transcript without us having to parse PTY output ourselves.
|
|
61
|
-
*
|
|
62
|
-
* O(1) string concat — the prompt is bounded by the zod max(8KiB).
|
|
63
|
-
*/
|
|
64
|
-
function buildBossPrompt({ prompt, specialistCount, depth }) {
|
|
65
|
-
const depthLine = depth === 'quick'
|
|
66
|
-
? 'Quick pass — surface-level analysis, single-shot specialist work.'
|
|
67
|
-
: depth === 'deep'
|
|
68
|
-
? 'Deep pass — multi-step plans per specialist, recurse on findings.'
|
|
69
|
-
: 'Standard pass — moderate detail, one or two iterations per specialist.';
|
|
70
|
-
|
|
71
|
-
return [
|
|
72
|
-
'## SuperAgent boss mode',
|
|
73
|
-
'',
|
|
74
|
-
`You are coordinating ${specialistCount} specialist subagent(s) for the user's task.`,
|
|
75
|
-
depthLine,
|
|
76
|
-
'',
|
|
77
|
-
'Workflow:',
|
|
78
|
-
` 1. Pick ${specialistCount} specialist subagent type(s) that best match the task.`,
|
|
79
|
-
' Report them as: [SUPERAGENT] specialists: <type1>, <type2>, ...',
|
|
80
|
-
' 2. Dispatch them via the Task tool, one at a time or in parallel as appropriate.',
|
|
81
|
-
' 3. After each specialist returns, report: [SUPERAGENT] progress: <specialist> done',
|
|
82
|
-
' 4. When the task is complete, summarize results and report: [SUPERAGENT] complete',
|
|
83
|
-
'',
|
|
84
|
-
'User task:',
|
|
85
|
-
prompt,
|
|
86
|
-
].join('\n');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function startRun(payload) {
|
|
90
|
-
const { tabId, prompt, specialistCount, depth } = payload;
|
|
91
|
-
|
|
92
|
-
// Stop any in-flight run on this tab — single run-per-tab invariant.
|
|
93
|
-
const existing = runs.get(tabId);
|
|
94
|
-
if (existing && existing.status === 'running') {
|
|
95
|
-
runs.set(tabId, {
|
|
96
|
-
...existing,
|
|
97
|
-
status: 'done',
|
|
98
|
-
finishedAt: Date.now(),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const fullPrompt = buildBossPrompt({ prompt, specialistCount, depth });
|
|
103
|
-
|
|
104
|
-
// PTY write — exactly the pattern docstring'd at the top of the task brief.
|
|
105
|
-
// The trailing carriage-return submits the prompt in Claude's TUI.
|
|
106
|
-
let writeErr = null;
|
|
107
|
-
try {
|
|
108
|
-
ptyManager.write({ tabId, data: fullPrompt + '\r' });
|
|
109
|
-
} catch (e) {
|
|
110
|
-
writeErr = e?.message ?? String(e);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (writeErr) {
|
|
114
|
-
const errState = {
|
|
115
|
-
status: 'error',
|
|
116
|
-
prompt,
|
|
117
|
-
specialistCount,
|
|
118
|
-
depth,
|
|
119
|
-
startedAt: Date.now(),
|
|
120
|
-
finishedAt: Date.now(),
|
|
121
|
-
error: writeErr,
|
|
122
|
-
};
|
|
123
|
-
runs.set(tabId, errState);
|
|
124
|
-
broadcast(tabId);
|
|
125
|
-
logs.writeLine({
|
|
126
|
-
scope: 'superagent',
|
|
127
|
-
level: 'error',
|
|
128
|
-
message: 'start failed',
|
|
129
|
-
meta: { tabId, error: writeErr },
|
|
130
|
-
});
|
|
131
|
-
return { ok: false, error: writeErr };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const state = {
|
|
135
|
-
status: 'running',
|
|
136
|
-
prompt,
|
|
137
|
-
specialistCount,
|
|
138
|
-
depth,
|
|
139
|
-
startedAt: Date.now(),
|
|
140
|
-
finishedAt: null,
|
|
141
|
-
};
|
|
142
|
-
runs.set(tabId, state);
|
|
143
|
-
broadcast(tabId);
|
|
144
|
-
|
|
145
|
-
logs.writeLine({
|
|
146
|
-
scope: 'superagent',
|
|
147
|
-
level: 'info',
|
|
148
|
-
message: 'start',
|
|
149
|
-
meta: { tabId, specialistCount, depth, promptBytes: prompt.length },
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return { ok: true };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function stopRun(tabId) {
|
|
156
|
-
const existing = runs.get(tabId);
|
|
157
|
-
if (!existing || existing.status !== 'running') {
|
|
158
|
-
return { ok: true };
|
|
159
|
-
}
|
|
160
|
-
runs.set(tabId, {
|
|
161
|
-
...existing,
|
|
162
|
-
status: 'done',
|
|
163
|
-
finishedAt: Date.now(),
|
|
164
|
-
});
|
|
165
|
-
broadcast(tabId);
|
|
166
|
-
logs.writeLine({
|
|
167
|
-
scope: 'superagent',
|
|
168
|
-
level: 'info',
|
|
169
|
-
message: 'stop',
|
|
170
|
-
meta: { tabId },
|
|
171
|
-
});
|
|
172
|
-
return { ok: true };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function getStatus(tabId) {
|
|
176
|
-
return runs.get(tabId) ?? null;
|
|
177
|
-
}
|
|
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
|
-
|
|
188
|
-
function registerSuperAgentHandlers() {
|
|
189
|
-
ipcMain.handle('superagent:start', (_e, payload) => {
|
|
190
|
-
const parsed = schemas.superagentStart.parse(payload);
|
|
191
|
-
return startRun(parsed);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
ipcMain.handle('superagent:status', (_e, payload) => {
|
|
195
|
-
const { tabId } = schemas.superagentTabId.parse(payload);
|
|
196
|
-
return getStatus(tabId);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
ipcMain.handle('superagent:stop', (_e, payload) => {
|
|
200
|
-
const { tabId } = schemas.superagentTabId.parse(payload);
|
|
201
|
-
return stopRun(tabId);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
module.exports = {
|
|
206
|
-
attachWindow,
|
|
207
|
-
registerSuperAgentHandlers,
|
|
208
|
-
dropTab,
|
|
209
|
-
// Exposed for tests.
|
|
210
|
-
buildBossPrompt,
|
|
211
|
-
_runs: runs,
|
|
212
|
-
};
|
package/src/main/usageMatrix.cjs
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
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, replay = false }) {
|
|
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
|
-
// Historical events must not enter the 5-min sliding window — doing so
|
|
113
|
-
// would make tokensPerMin spike to "critical" on every tab switch.
|
|
114
|
-
if (!replay) {
|
|
115
|
-
t.tokenWindow.push({ ts: now, tokens: inTok + outTok });
|
|
116
|
-
pruneWindow(t.tokenWindow, now);
|
|
117
|
-
}
|
|
118
|
-
dirty = true;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
case 'tool_use': {
|
|
122
|
-
const name = ev.data?.name;
|
|
123
|
-
if (name) {
|
|
124
|
-
t.toolSeries.push({ ts: now, name });
|
|
125
|
-
if (t.toolSeries.length > TOOL_RING) t.toolSeries.shift();
|
|
126
|
-
}
|
|
127
|
-
dirty = true;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
case 'agent_spawn': {
|
|
131
|
-
t.agentSpawns += 1;
|
|
132
|
-
const id = ev.data?.toolUseId;
|
|
133
|
-
if (id) t.agentsActive.set(id, { at: now });
|
|
134
|
-
dirty = true;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
case 'tool_result': {
|
|
138
|
-
const id = ev.data?.toolUseId;
|
|
139
|
-
if (id && t.agentsActive.has(id)) {
|
|
140
|
-
t.agentsActive.delete(id);
|
|
141
|
-
dirty = true;
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
case 'plan': {
|
|
146
|
-
t.lastPlanAt = now;
|
|
147
|
-
dirty = true;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
default:
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function pruneWindow(arr, now) {
|
|
156
|
-
const cutoff = now - TOKEN_WINDOW_MS;
|
|
157
|
-
while (arr.length && arr[0].ts < cutoff) arr.shift();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function removeTab(tabId) {
|
|
161
|
-
if (tabs.delete(tabId)) dirty = true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Build a derived snapshot. Heavy work happens here, not on the hot
|
|
166
|
-
* recordEvent path — recordEvent only appends to ring buffers and flips a
|
|
167
|
-
* dirty bit.
|
|
168
|
-
*/
|
|
169
|
-
function buildSnapshot() {
|
|
170
|
-
const now = Date.now();
|
|
171
|
-
const out = [];
|
|
172
|
-
|
|
173
|
-
let totalSubagentsActive = 0;
|
|
174
|
-
let totalSubagentsSpawned = 0;
|
|
175
|
-
let totalTokensToday = 0;
|
|
176
|
-
let combinedTokensPerMin = 0;
|
|
177
|
-
|
|
178
|
-
for (const t of tabs.values()) {
|
|
179
|
-
pruneWindow(t.tokenWindow, now);
|
|
180
|
-
const windowTokens = t.tokenWindow.reduce((s, e) => s + e.tokens, 0);
|
|
181
|
-
const windowMin = Math.min(TOKEN_WINDOW_MS, now - (t.tokenWindow[0]?.ts ?? now)) / 60_000;
|
|
182
|
-
const tokensPerMin = windowMin > 0.1 ? Math.round(windowTokens / windowMin) : 0;
|
|
183
|
-
combinedTokensPerMin += tokensPerMin;
|
|
184
|
-
|
|
185
|
-
const intensity = tokensPerMin >= BURN_CRITICAL
|
|
186
|
-
? 'critical'
|
|
187
|
-
: tokensPerMin >= BURN_LOW
|
|
188
|
-
? 'medium'
|
|
189
|
-
: tokensPerMin > 0
|
|
190
|
-
? 'low'
|
|
191
|
-
: 'idle';
|
|
192
|
-
|
|
193
|
-
const cacheAgeMs = t.lastAssistantAt ? now - t.lastAssistantAt : null;
|
|
194
|
-
const cacheState = cacheAgeMs == null
|
|
195
|
-
? 'cold'
|
|
196
|
-
: cacheAgeMs > CACHE_TTL_MS
|
|
197
|
-
? 'cold'
|
|
198
|
-
: cacheAgeMs > CACHE_WARN_MS
|
|
199
|
-
? 'expiring'
|
|
200
|
-
: 'warm';
|
|
201
|
-
|
|
202
|
-
const avgPerTurn = t.perTurnInputTokens.length > 0
|
|
203
|
-
? Math.round(t.perTurnInputTokens.reduce((s, n) => s + n, 0) / t.perTurnInputTokens.length)
|
|
204
|
-
: 0;
|
|
205
|
-
|
|
206
|
-
const geometricGrowth = detectGeometricGrowth(t.perTurnInputTokens);
|
|
207
|
-
|
|
208
|
-
// State derivation. Order matters — earlier branches win.
|
|
209
|
-
let state;
|
|
210
|
-
if (t.lastPlanAt > 0 && (now - t.lastPlanAt) < 60_000 && t.lastPlanAt >= t.lastAssistantAt) {
|
|
211
|
-
state = 'plan';
|
|
212
|
-
} else if (t.agentsActive.size > 0 && (now - t.lastAssistantAt) > 10_000) {
|
|
213
|
-
state = 'background';
|
|
214
|
-
} else if (t.lastEventAt > 0 && (now - t.lastEventAt) < 30_000) {
|
|
215
|
-
state = 'executing';
|
|
216
|
-
} else {
|
|
217
|
-
state = 'idle';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const subagentsActive = t.agentsActive.size;
|
|
221
|
-
totalSubagentsActive += subagentsActive;
|
|
222
|
-
totalSubagentsSpawned += t.agentSpawns;
|
|
223
|
-
totalTokensToday += t.cumulativeUsage.input + t.cumulativeUsage.output;
|
|
224
|
-
|
|
225
|
-
const alerts = [];
|
|
226
|
-
if (geometricGrowth) {
|
|
227
|
-
alerts.push({
|
|
228
|
-
level: 'warn',
|
|
229
|
-
code: 'geometric-growth',
|
|
230
|
-
message: `Per-turn input grew geometrically (avg ${(avgPerTurn / 1000).toFixed(0)}k). Run /compact.`,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
if (t.turns >= LONG_SESSION_TURNS) {
|
|
234
|
-
alerts.push({
|
|
235
|
-
level: 'warn',
|
|
236
|
-
code: 'long-session',
|
|
237
|
-
message: `Session is at turn ${t.turns}. Context resubmissions cost ${(avgPerTurn / 1000).toFixed(0)}k tokens each.`,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (cacheState === 'expiring' && state === 'idle') {
|
|
241
|
-
const minLeft = Math.max(0, Math.round((CACHE_TTL_MS - (cacheAgeMs ?? 0)) / 60_000));
|
|
242
|
-
alerts.push({
|
|
243
|
-
level: 'info',
|
|
244
|
-
code: 'cache-cold-start',
|
|
245
|
-
message: `Cache cold-start in ${minLeft}m. Next command will incur full cache_creation cost.`,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
if (cacheState === 'cold' && t.lastAssistantAt > 0) {
|
|
249
|
-
alerts.push({
|
|
250
|
-
level: 'info',
|
|
251
|
-
code: 'cache-cold',
|
|
252
|
-
message: 'Cache expired. Next command pays full cache_creation pricing.',
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
if (subagentsActive >= 4) {
|
|
256
|
-
alerts.push({
|
|
257
|
-
level: 'info',
|
|
258
|
-
code: 'subagent-fanout',
|
|
259
|
-
message: `${subagentsActive} subagents in flight. Consider waiting before fanning out more.`,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
out.push({
|
|
264
|
-
tabId: t.tabId,
|
|
265
|
-
workspace: t.workspace,
|
|
266
|
-
cwd: t.cwd,
|
|
267
|
-
state,
|
|
268
|
-
turns: t.turns,
|
|
269
|
-
lastEventAt: t.lastEventAt,
|
|
270
|
-
lastAssistantAt: t.lastAssistantAt,
|
|
271
|
-
cacheAgeMs,
|
|
272
|
-
cacheState,
|
|
273
|
-
tokensPerMin,
|
|
274
|
-
intensity,
|
|
275
|
-
avgTokensPerTurn: avgPerTurn,
|
|
276
|
-
cumulativeUsage: { ...t.cumulativeUsage },
|
|
277
|
-
subagentsActive,
|
|
278
|
-
subagentsSpawned: t.agentSpawns,
|
|
279
|
-
geometricGrowth,
|
|
280
|
-
alerts,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Most active first.
|
|
285
|
-
out.sort((a, b) => b.tokensPerMin - a.tokensPerMin || b.lastEventAt - a.lastEventAt);
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
generatedAt: now,
|
|
289
|
-
tabs: out,
|
|
290
|
-
totals: {
|
|
291
|
-
activeSessions: out.length,
|
|
292
|
-
subagentsActive: totalSubagentsActive,
|
|
293
|
-
subagentsSpawned: totalSubagentsSpawned,
|
|
294
|
-
tokensTotal: totalTokensToday,
|
|
295
|
-
combinedTokensPerMin,
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Geometric growth: avg of last 3 turns > 1.8 × avg of prior 3 turns,
|
|
302
|
-
* and last-3 avg above GEOMETRIC_MIN_TOKENS so trivial sessions don't flag.
|
|
303
|
-
*/
|
|
304
|
-
function detectGeometricGrowth(series) {
|
|
305
|
-
if (series.length < 6) return false;
|
|
306
|
-
const tail = series.slice(-3);
|
|
307
|
-
const prev = series.slice(-6, -3);
|
|
308
|
-
const tailAvg = tail.reduce((s, n) => s + n, 0) / 3;
|
|
309
|
-
const prevAvg = prev.reduce((s, n) => s + n, 0) / 3;
|
|
310
|
-
if (tailAvg < GEOMETRIC_MIN_TOKENS) return false;
|
|
311
|
-
if (prevAvg <= 0) return false;
|
|
312
|
-
return tailAvg / prevAvg >= 1.8;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function broadcastIfChanged() {
|
|
316
|
-
if (!dirty) return;
|
|
317
|
-
dirty = false;
|
|
318
|
-
if (!window) return;
|
|
319
|
-
sendIfAlive(window, 'usage:matrix:tick', buildSnapshot());
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function registerHandlers() {
|
|
323
|
-
ipcMain.handle('usage:matrix:snapshot', () => buildSnapshot());
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function closeAll() {
|
|
327
|
-
if (tickTimer) {
|
|
328
|
-
clearInterval(tickTimer);
|
|
329
|
-
tickTimer = null;
|
|
330
|
-
}
|
|
331
|
-
tabs.clear();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
module.exports = {
|
|
335
|
-
attachWindow,
|
|
336
|
-
recordEvent,
|
|
337
|
-
removeTab,
|
|
338
|
-
registerHandlers,
|
|
339
|
-
closeAll,
|
|
340
|
-
};
|