pan-wizard 3.19.0 → 3.21.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/bin/install-lib.cjs +14 -98
- package/hooks/dist/pan-cost-logger.js +134 -30
- package/hooks/dist/pan-trace-logger.js +160 -14
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +119 -0
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -1
- package/pan-wizard-core/bin/lib/config.cjs +3 -0
- package/pan-wizard-core/bin/lib/constants.cjs +8 -0
- package/pan-wizard-core/bin/lib/core.cjs +4 -0
- package/pan-wizard-core/bin/lib/cost.cjs +18 -4
- package/pan-wizard-core/bin/lib/focus.cjs +64 -5
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +253 -0
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +156 -0
- package/pan-wizard-core/bin/lib/optimize.cjs +192 -33
- package/pan-wizard-core/bin/lib/state.cjs +4 -0
- package/pan-wizard-core/bin/pan-tools.cjs +12 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PAN memory rebuild (A2) — regenerate DERIVED memory from source, idempotently.
|
|
5
|
+
*
|
|
6
|
+
* "Rebuild" is a projection, not a mutation: it re-emits only the regions PAN
|
|
7
|
+
* owns and can reproduce from source, and leaves everything the user wrote
|
|
8
|
+
* alone. Per the memory-management research, a rebuild must be an idempotent
|
|
9
|
+
* projection touching only derived regions — running it twice changes nothing.
|
|
10
|
+
*
|
|
11
|
+
* Three derived targets:
|
|
12
|
+
* 1. AGENTS.md — the universal, cross-runtime tools memory. PAN owns exactly
|
|
13
|
+
* the marker-fenced `<!-- BEGIN/END PAN WIZARD -->` section (every runtime,
|
|
14
|
+
* including Copilot/.github, reads AGENTS.md natively). User content
|
|
15
|
+
* outside the markers is preserved byte-for-byte.
|
|
16
|
+
* 2. CLAUDE.md — the Claude bridge (`@AGENTS.md` import), regenerated only
|
|
17
|
+
* when the Claude runtime is installed here.
|
|
18
|
+
* 3. state.md — its YAML frontmatter is re-derived from the body (phase
|
|
19
|
+
* progress, status). The body prose is never touched.
|
|
20
|
+
*
|
|
21
|
+
* Dry-run by default (`--apply` to write). Refuses to run inside the PAN source
|
|
22
|
+
* repository, mirroring the installer and experiment guards.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { output, safeReadFile } = require('./core.cjs');
|
|
28
|
+
const { planningPath } = require('./utils.cjs');
|
|
29
|
+
const { syncStateFrontmatter } = require('./state.cjs');
|
|
30
|
+
const {
|
|
31
|
+
buildAgentsMdSection,
|
|
32
|
+
upsertAgentsMdSection,
|
|
33
|
+
ensureClaudeMdImport,
|
|
34
|
+
} = require('./agents-md.cjs');
|
|
35
|
+
|
|
36
|
+
// Source repo root — mirrors experiment.cjs / install.js. __dirname is
|
|
37
|
+
// .../pan-wizard-core/bin/lib, so three levels up is the repo (or install) root.
|
|
38
|
+
const PAN_SOURCE_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
39
|
+
|
|
40
|
+
// Runtime → config directory. A runtime is "installed here" when its dir exists
|
|
41
|
+
// in the project. AGENTS.md is shared by all; only Claude gets a bridge file.
|
|
42
|
+
const RUNTIME_DIRS = {
|
|
43
|
+
claude: '.claude',
|
|
44
|
+
codex: '.codex',
|
|
45
|
+
gemini: '.gemini',
|
|
46
|
+
opencode: '.opencode',
|
|
47
|
+
copilot: '.github',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function normPath(p) {
|
|
51
|
+
return process.platform === 'win32' ? p.toLowerCase() : p;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* True only when `cwd` is the genuine PAN source repository — inside
|
|
56
|
+
* PAN_SOURCE_ROOT *and* that root actually looks like the source checkout
|
|
57
|
+
* (has bin/install.js). In an install layout PAN_SOURCE_ROOT resolves to the
|
|
58
|
+
* runtime config dir, which has no bin/install.js, so this stays false.
|
|
59
|
+
*/
|
|
60
|
+
function isInsideSourceRepo(cwd) {
|
|
61
|
+
const abs = normPath(path.resolve(cwd));
|
|
62
|
+
const src = normPath(PAN_SOURCE_ROOT);
|
|
63
|
+
const inside = abs === src || abs.startsWith(src + path.sep) || abs.startsWith(src + '/');
|
|
64
|
+
if (!inside) return false;
|
|
65
|
+
return fs.existsSync(path.join(PAN_SOURCE_ROOT, 'bin', 'install.js')) &&
|
|
66
|
+
fs.existsSync(path.join(PAN_SOURCE_ROOT, 'pan-wizard-core'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Which PAN runtimes are installed in this project (by config-dir presence). */
|
|
70
|
+
function detectRuntimes(cwd) {
|
|
71
|
+
return Object.entries(RUNTIME_DIRS)
|
|
72
|
+
.filter(([, dir]) => {
|
|
73
|
+
try { return fs.statSync(path.join(cwd, dir)).isDirectory(); }
|
|
74
|
+
catch { return false; }
|
|
75
|
+
})
|
|
76
|
+
.map(([name]) => name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rebuild one file: compute the desired content from `existing`, compare, and
|
|
81
|
+
* (on apply) write only when it differs. Returns a per-target status.
|
|
82
|
+
*/
|
|
83
|
+
function rebuildFile(filePath, existing, desired, apply) {
|
|
84
|
+
const absent = existing == null;
|
|
85
|
+
if (existing === desired) return { action: 'unchanged', wrote: false };
|
|
86
|
+
const action = absent ? 'create' : 'update';
|
|
87
|
+
if (apply) fs.writeFileSync(filePath, desired, 'utf-8');
|
|
88
|
+
return { action, wrote: apply };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `memory rebuild [--apply]` — regenerate derived tools-memory + state.md
|
|
93
|
+
* frontmatter. Dry-run by default: reports what WOULD change.
|
|
94
|
+
*/
|
|
95
|
+
function cmdMemoryRebuild(cwd, opts = {}, raw) {
|
|
96
|
+
const apply = !!opts.apply;
|
|
97
|
+
|
|
98
|
+
if (isInsideSourceRepo(cwd)) {
|
|
99
|
+
output(
|
|
100
|
+
{ error: 'source_repo', rebuilt: [] },
|
|
101
|
+
raw,
|
|
102
|
+
`refusing to rebuild memory inside the PAN source repository (${PAN_SOURCE_ROOT})`,
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const runtimes = detectRuntimes(cwd);
|
|
108
|
+
const targets = [];
|
|
109
|
+
|
|
110
|
+
// 1. AGENTS.md — universal PAN section (all runtimes read it natively).
|
|
111
|
+
{
|
|
112
|
+
const p = path.join(cwd, 'AGENTS.md');
|
|
113
|
+
const existing = safeReadFile(p);
|
|
114
|
+
const desired = upsertAgentsMdSection(existing, buildAgentsMdSection());
|
|
115
|
+
targets.push({ file: 'AGENTS.md', ...rebuildFile(p, existing, desired, apply) });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. CLAUDE.md — Claude bridge, only when the Claude runtime is installed.
|
|
119
|
+
if (runtimes.includes('claude')) {
|
|
120
|
+
const p = path.join(cwd, 'CLAUDE.md');
|
|
121
|
+
const existing = safeReadFile(p);
|
|
122
|
+
const desired = ensureClaudeMdImport(existing);
|
|
123
|
+
targets.push({ file: 'CLAUDE.md', ...rebuildFile(p, existing, desired, apply) });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. state.md — re-derive YAML frontmatter from the body (progress/status).
|
|
127
|
+
{
|
|
128
|
+
const p = path.join(planningPath(cwd), 'state.md');
|
|
129
|
+
const existing = safeReadFile(p);
|
|
130
|
+
if (existing != null) {
|
|
131
|
+
const desired = syncStateFrontmatter(existing, cwd);
|
|
132
|
+
targets.push({ file: '.planning/state.md', ...rebuildFile(p, existing, desired, apply) });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const changed = targets.filter((t) => t.action !== 'unchanged');
|
|
137
|
+
const result = {
|
|
138
|
+
apply,
|
|
139
|
+
runtimes,
|
|
140
|
+
rebuilt: targets,
|
|
141
|
+
changed_count: changed.length,
|
|
142
|
+
};
|
|
143
|
+
const summary = changed.length === 0
|
|
144
|
+
? `tools memory already current (${targets.map((t) => t.file).join(', ')}) — nothing to do`
|
|
145
|
+
: `${apply ? 'rebuilt' : 'would rebuild'} ${changed.map((t) => `${t.file} (${t.action})`).join(', ')}`;
|
|
146
|
+
output(result, raw, summary);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
cmdMemoryRebuild,
|
|
151
|
+
detectRuntimes,
|
|
152
|
+
isInsideSourceRepo,
|
|
153
|
+
rebuildFile,
|
|
154
|
+
PAN_SOURCE_ROOT,
|
|
155
|
+
RUNTIME_DIRS,
|
|
156
|
+
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { output, escapeRegex } = require('./core.cjs');
|
|
11
|
+
const { output, escapeRegex, execGit } = require('./core.cjs');
|
|
12
12
|
const { PLANNING_DIR } = require('./constants.cjs');
|
|
13
13
|
|
|
14
14
|
// ─── Storage layout ──────────────────────────────────────────────────────────
|
|
@@ -172,6 +172,65 @@ function logTraceEvent(cwd, event, sessionId) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Recompute a session's counters straight from its trace.jsonl — the single
|
|
177
|
+
* source of truth for event_count/agent_count/agents/type_counts, plus a count
|
|
178
|
+
* of malformed (unparseable) rows that would otherwise vanish silently. Pure
|
|
179
|
+
* read; never writes. Used by endTraceSession, the reconcile subcommand, and the
|
|
180
|
+
* reconcile-on-read overlay so the counting logic lives in exactly one place.
|
|
181
|
+
*/
|
|
182
|
+
function reconcileSessionMeta(cwd, sessionId) {
|
|
183
|
+
const sessionDir = path.join(getTracesDir(cwd), sessionId);
|
|
184
|
+
let eventCount = 0;
|
|
185
|
+
let malformed = 0;
|
|
186
|
+
const agentNames = new Set();
|
|
187
|
+
const typeCounts = {};
|
|
188
|
+
try {
|
|
189
|
+
const raw = fs.readFileSync(path.join(sessionDir, TRACE_EVENT_FILE), 'utf-8');
|
|
190
|
+
raw.trim().split('\n').filter(Boolean).forEach(line => {
|
|
191
|
+
let e;
|
|
192
|
+
try { e = JSON.parse(line); } catch { malformed++; return; }
|
|
193
|
+
eventCount++;
|
|
194
|
+
if (e.agent) agentNames.add(e.agent);
|
|
195
|
+
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
|
|
196
|
+
});
|
|
197
|
+
} catch { /* no trace.jsonl yet */ }
|
|
198
|
+
return {
|
|
199
|
+
event_count: eventCount,
|
|
200
|
+
agent_count: agentNames.size,
|
|
201
|
+
agents: Array.from(agentNames),
|
|
202
|
+
type_counts: typeCounts,
|
|
203
|
+
malformed_count: malformed,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Compute the measured dollar cost + commit count for a session's [started_at,
|
|
209
|
+
* ended_at||now] window, folding the authoritative per-agent cost ledger
|
|
210
|
+
* (tokens.jsonl, suspect rows already quarantined) into the session so the
|
|
211
|
+
* autonomous-overhead metrics work. cost_usd is null when the ledger has no
|
|
212
|
+
* in-window rows; commit_count is null when git is unavailable — never a
|
|
213
|
+
* fabricated 0 (0 would make minutes_per_commit Infinity). Best-effort.
|
|
214
|
+
*/
|
|
215
|
+
function computeSessionCostAndCommits(cwd, meta) {
|
|
216
|
+
const out = { cost_usd: null, commit_count: null };
|
|
217
|
+
if (!meta || !meta.started_at) return out;
|
|
218
|
+
const since = meta.started_at;
|
|
219
|
+
const until = meta.ended_at || new Date().toISOString();
|
|
220
|
+
try {
|
|
221
|
+
const cost = require('./cost.cjs');
|
|
222
|
+
const agg = cost.aggregate(cwd, { since, until });
|
|
223
|
+
if (agg && agg.totals && agg.totals.calls > 0) out.cost_usd = agg.totals.cost_usd;
|
|
224
|
+
} catch { /* cost is observability, never the critical path */ }
|
|
225
|
+
try {
|
|
226
|
+
const r = execGit(cwd, ['log', '--oneline', '--since', since, '--until', until]);
|
|
227
|
+
if (r && r.exitCode === 0) {
|
|
228
|
+
out.commit_count = r.stdout ? r.stdout.split('\n').filter(Boolean).length : 0;
|
|
229
|
+
}
|
|
230
|
+
} catch { /* non-repo / git absent → leave null */ }
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
175
234
|
function endTraceSession(cwd, sessionId) {
|
|
176
235
|
const sid = sessionId || getCurrentSessionId(cwd);
|
|
177
236
|
if (!sid) return { error: 'No active session' };
|
|
@@ -183,35 +242,39 @@ function endTraceSession(cwd, sessionId) {
|
|
|
183
242
|
let meta = {};
|
|
184
243
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {}
|
|
185
244
|
|
|
186
|
-
|
|
187
|
-
const agentNames = new Set();
|
|
188
|
-
const typeCounts = {};
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const raw = fs.readFileSync(path.join(sessionDir, TRACE_EVENT_FILE), 'utf-8');
|
|
192
|
-
raw.trim().split('\n').filter(Boolean).forEach(line => {
|
|
193
|
-
try {
|
|
194
|
-
const e = JSON.parse(line);
|
|
195
|
-
eventCount++;
|
|
196
|
-
if (e.agent) agentNames.add(e.agent);
|
|
197
|
-
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
|
|
198
|
-
} catch {}
|
|
199
|
-
});
|
|
200
|
-
} catch {}
|
|
201
|
-
|
|
245
|
+
const counts = reconcileSessionMeta(cwd, sid);
|
|
202
246
|
meta.ended_at = new Date().toISOString();
|
|
203
|
-
meta.event_count =
|
|
204
|
-
meta.agent_count =
|
|
205
|
-
meta.agents =
|
|
206
|
-
meta.type_counts =
|
|
247
|
+
meta.event_count = counts.event_count;
|
|
248
|
+
meta.agent_count = counts.agent_count;
|
|
249
|
+
meta.agents = counts.agents;
|
|
250
|
+
meta.type_counts = counts.type_counts;
|
|
251
|
+
if (counts.malformed_count) meta.malformed_count = counts.malformed_count;
|
|
252
|
+
|
|
253
|
+
// Fold measured cost + commit count into the session so overhead.* metrics
|
|
254
|
+
// and `optimize stats` carry real dollars, not perpetual nulls.
|
|
255
|
+
const cc = computeSessionCostAndCommits(cwd, meta);
|
|
256
|
+
if (cc.cost_usd != null) meta.cost_usd = cc.cost_usd;
|
|
257
|
+
if (cc.commit_count != null) meta.commit_count = cc.commit_count;
|
|
207
258
|
|
|
208
259
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
209
260
|
|
|
261
|
+
// Clear the active-session pointer so the next SubagentStop opens a fresh
|
|
262
|
+
// session — but ONLY when we ended the session it points at (ending session
|
|
263
|
+
// A explicitly must not orphan a different active session B).
|
|
264
|
+
try {
|
|
265
|
+
if (getCurrentSessionId(cwd) === sid) {
|
|
266
|
+
fs.unlinkSync(path.join(getOptimizeDir(cwd), CURRENT_SESSION_FILE));
|
|
267
|
+
}
|
|
268
|
+
} catch { /* best-effort */ }
|
|
269
|
+
|
|
210
270
|
return {
|
|
211
271
|
session_id: sid,
|
|
212
|
-
event_count:
|
|
213
|
-
agent_count:
|
|
214
|
-
type_counts:
|
|
272
|
+
event_count: counts.event_count,
|
|
273
|
+
agent_count: counts.agent_count,
|
|
274
|
+
type_counts: counts.type_counts,
|
|
275
|
+
malformed_count: counts.malformed_count,
|
|
276
|
+
cost_usd: meta.cost_usd != null ? meta.cost_usd : null,
|
|
277
|
+
commit_count: meta.commit_count != null ? meta.commit_count : null,
|
|
215
278
|
ended_at: meta.ended_at,
|
|
216
279
|
};
|
|
217
280
|
} catch (e) {
|
|
@@ -219,6 +282,43 @@ function endTraceSession(cwd, sessionId) {
|
|
|
219
282
|
}
|
|
220
283
|
}
|
|
221
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Rewrite session.json from trace.jsonl WITHOUT ending the session (ended_at is
|
|
287
|
+
* left untouched). Powers `optimize trace reconcile`, so hook-driven auto-sessions
|
|
288
|
+
* that never call `end` still get accurate counters.
|
|
289
|
+
*/
|
|
290
|
+
function reconcileTraceSession(cwd, sessionId) {
|
|
291
|
+
const sid = sessionId || getCurrentSessionId(cwd);
|
|
292
|
+
if (!sid) return { error: 'No session to reconcile' };
|
|
293
|
+
try {
|
|
294
|
+
const metaPath = path.join(getTracesDir(cwd), sid, OPT_SESSION_FILE);
|
|
295
|
+
let meta = {};
|
|
296
|
+
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {}
|
|
297
|
+
const counts = reconcileSessionMeta(cwd, sid);
|
|
298
|
+
meta.event_count = counts.event_count;
|
|
299
|
+
meta.agent_count = counts.agent_count;
|
|
300
|
+
meta.agents = counts.agents;
|
|
301
|
+
meta.type_counts = counts.type_counts;
|
|
302
|
+
if (counts.malformed_count) meta.malformed_count = counts.malformed_count;
|
|
303
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
304
|
+
return { session_id: sid, reconciled: true, event_count: counts.event_count, malformed_count: counts.malformed_count };
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return { error: e.message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Reconcile every session dir (used by `optimize trace reconcile --all`). */
|
|
311
|
+
function reconcileAllTraceSessions(cwd) {
|
|
312
|
+
const results = [];
|
|
313
|
+
try {
|
|
314
|
+
const tracesDir = getTracesDir(cwd);
|
|
315
|
+
for (const e of fs.readdirSync(tracesDir, { withFileTypes: true })) {
|
|
316
|
+
if (e.isDirectory() && e.name.startsWith('sess_')) results.push(reconcileTraceSession(cwd, e.name));
|
|
317
|
+
}
|
|
318
|
+
} catch { /* no traces dir */ }
|
|
319
|
+
return { reconciled: results.length, sessions: results };
|
|
320
|
+
}
|
|
321
|
+
|
|
222
322
|
function readTraceSession(cwd, sessionId) {
|
|
223
323
|
try {
|
|
224
324
|
const sessionDir = path.join(getTracesDir(cwd), sessionId);
|
|
@@ -229,14 +329,25 @@ function readTraceSession(cwd, sessionId) {
|
|
|
229
329
|
} catch {}
|
|
230
330
|
|
|
231
331
|
const events = [];
|
|
332
|
+
let malformed = 0;
|
|
232
333
|
try {
|
|
233
334
|
const raw = fs.readFileSync(path.join(sessionDir, TRACE_EVENT_FILE), 'utf-8');
|
|
234
335
|
raw.trim().split('\n').filter(Boolean).forEach(line => {
|
|
235
|
-
try { events.push(JSON.parse(line)); } catch {}
|
|
336
|
+
try { events.push(JSON.parse(line)); } catch { malformed++; }
|
|
236
337
|
});
|
|
237
338
|
} catch {}
|
|
238
339
|
|
|
239
|
-
|
|
340
|
+
// Reconcile-on-read: an unfinalized meta (no ended_at) or a stale zero
|
|
341
|
+
// event_count is overlaid with the live counts derived from the events just
|
|
342
|
+
// read, so consumers of metadata (e.g. optimize stats) never see a stale 0.
|
|
343
|
+
if (!metadata.ended_at || !metadata.event_count) {
|
|
344
|
+
const typeCounts = {};
|
|
345
|
+
const agents = new Set();
|
|
346
|
+
for (const e of events) { typeCounts[e.type] = (typeCounts[e.type] || 0) + 1; if (e.agent) agents.add(e.agent); }
|
|
347
|
+
metadata = { ...metadata, event_count: events.length, agent_count: agents.size, agents: Array.from(agents), type_counts: typeCounts };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { session_id: sessionId, metadata, events, event_count: events.length, malformed_count: malformed };
|
|
240
351
|
} catch (e) {
|
|
241
352
|
return { error: e.message };
|
|
242
353
|
}
|
|
@@ -256,6 +367,14 @@ function listTraceSessions(cwd) {
|
|
|
256
367
|
try {
|
|
257
368
|
meta = JSON.parse(fs.readFileSync(path.join(sessionDir, OPT_SESSION_FILE), 'utf-8'));
|
|
258
369
|
} catch {}
|
|
370
|
+
// Reconcile-on-read: unfinalized (no ended_at) or stale-zero sessions —
|
|
371
|
+
// every hook-driven auto-session — get live counts from trace.jsonl so
|
|
372
|
+
// getOptimizeStats doesn't sum perpetual zeros. Finalized sessions with a
|
|
373
|
+
// real count stay cheap (session.json read only).
|
|
374
|
+
if (!meta.ended_at || !meta.event_count) {
|
|
375
|
+
const counts = reconcileSessionMeta(cwd, e.name);
|
|
376
|
+
meta = { ...meta, event_count: counts.event_count, agent_count: counts.agent_count, agents: counts.agents, type_counts: counts.type_counts };
|
|
377
|
+
}
|
|
259
378
|
return meta;
|
|
260
379
|
}).sort((a, b) => (b.started_at || '').localeCompare(a.started_at || ''));
|
|
261
380
|
|
|
@@ -291,11 +410,18 @@ function analyzeEvents(events, sessionMeta) {
|
|
|
291
410
|
const agentStats = {};
|
|
292
411
|
events.forEach(e => {
|
|
293
412
|
if (!e.agent) return;
|
|
294
|
-
if (!agentStats[e.agent]) agentStats[e.agent] = { total: 0, errors: 0, gaps: 0, corrections: 0 };
|
|
413
|
+
if (!agentStats[e.agent]) agentStats[e.agent] = { total: 0, errors: 0, gaps: 0, corrections: 0, input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
295
414
|
agentStats[e.agent].total++;
|
|
296
415
|
if (e.type === 'error') agentStats[e.agent].errors++;
|
|
297
416
|
if (e.type === 'gap') agentStats[e.agent].gaps++;
|
|
298
417
|
if (e.type === 'correction') agentStats[e.agent].corrections++;
|
|
418
|
+
// Sum the per-call tokens the trace logger now writes — ONLY on completion
|
|
419
|
+
// events, so the redundancy event that mirrors output_tokens isn't counted twice.
|
|
420
|
+
if (e.category === 'agent_completion' && e.context) {
|
|
421
|
+
agentStats[e.agent].input_tokens += e.context.input_tokens || 0;
|
|
422
|
+
agentStats[e.agent].output_tokens += e.context.output_tokens || 0;
|
|
423
|
+
agentStats[e.agent].total_tokens += e.context.total_tokens || ((e.context.input_tokens || 0) + (e.context.output_tokens || 0));
|
|
424
|
+
}
|
|
299
425
|
});
|
|
300
426
|
|
|
301
427
|
Object.keys(agentStats).forEach(a => {
|
|
@@ -305,9 +431,20 @@ function analyzeEvents(events, sessionMeta) {
|
|
|
305
431
|
|
|
306
432
|
const wastedTokens = redundancies.reduce((sum, e) => sum + (e.tokens_wasted || 0), 0);
|
|
307
433
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
434
|
+
// Sum the authoritative per-call tokens (written by the SubagentStop hooks
|
|
435
|
+
// since v3.20.0) across completion events — the redundancy events mirror
|
|
436
|
+
// output_tokens, so restrict to agent_completion to avoid double-counting.
|
|
437
|
+
const completions = events.filter(e => e.category === 'agent_completion' && e.context);
|
|
438
|
+
const tokenTotals = completions.reduce((acc, e) => {
|
|
439
|
+
acc.input += e.context.input_tokens || 0;
|
|
440
|
+
acc.output += e.context.output_tokens || 0;
|
|
441
|
+
acc.cache_read += e.context.cache_read_tokens || 0;
|
|
442
|
+
return acc;
|
|
443
|
+
}, { input: 0, output: 0, cache_read: 0 });
|
|
444
|
+
|
|
445
|
+
// ── Timing analysis ───────────────────────────────────────────────────────
|
|
446
|
+
// Prefer measured per-agent duration_ms (hooks derive it from the transcript
|
|
447
|
+
// slice); fall back to the inter-event wall-clock gap when it's absent.
|
|
311
448
|
const timing = {};
|
|
312
449
|
|
|
313
450
|
// Session total duration
|
|
@@ -381,6 +518,10 @@ function analyzeEvents(events, sessionMeta) {
|
|
|
381
518
|
wasted_tokens: wastedTokens,
|
|
382
519
|
reviewer_corrections: reviewerCorrections.length,
|
|
383
520
|
memory_primed_count: memoryPrimed.length,
|
|
521
|
+
total_input_tokens: tokenTotals.input,
|
|
522
|
+
total_output_tokens: tokenTotals.output,
|
|
523
|
+
total_cache_read_tokens: tokenTotals.cache_read,
|
|
524
|
+
total_tokens: tokenTotals.input + tokenTotals.output,
|
|
384
525
|
},
|
|
385
526
|
timing,
|
|
386
527
|
overhead,
|
|
@@ -405,11 +546,21 @@ function generateLocalReport(cwd, sessionId) {
|
|
|
405
546
|
const session = readTraceSession(cwd, sessionId);
|
|
406
547
|
if (session.error) return session;
|
|
407
548
|
|
|
549
|
+
// Fold measured cost + commit count into the metadata before analysis so the
|
|
550
|
+
// autonomous-overhead metrics populate even for hook-driven auto-sessions that
|
|
551
|
+
// never call `optimize trace end`. Only fill fields a producer didn't set.
|
|
552
|
+
const metadata = session.metadata || {};
|
|
553
|
+
if (typeof metadata.cost_usd !== 'number' || typeof metadata.commit_count !== 'number') {
|
|
554
|
+
const cc = computeSessionCostAndCommits(cwd, metadata);
|
|
555
|
+
if (typeof metadata.cost_usd !== 'number' && cc.cost_usd != null) metadata.cost_usd = cc.cost_usd;
|
|
556
|
+
if (typeof metadata.commit_count !== 'number' && cc.commit_count != null) metadata.commit_count = cc.commit_count;
|
|
557
|
+
}
|
|
558
|
+
|
|
408
559
|
return {
|
|
409
560
|
session_id: sessionId,
|
|
410
561
|
generated_at: new Date().toISOString(),
|
|
411
|
-
metadata
|
|
412
|
-
...analyzeEvents(session.events,
|
|
562
|
+
metadata,
|
|
563
|
+
...analyzeEvents(session.events, metadata),
|
|
413
564
|
raw_events: session.events,
|
|
414
565
|
};
|
|
415
566
|
}
|
|
@@ -622,8 +773,12 @@ function cmdOptimizeTrace(cwd, sub, opts, raw) {
|
|
|
622
773
|
} else if (sub === 'show') {
|
|
623
774
|
if (!opts.sessionId) { output({ error: 'Session ID required (--session <id>)' }, raw); return; }
|
|
624
775
|
output(readTraceSession(cwd, opts.sessionId), raw);
|
|
776
|
+
} else if (sub === 'reconcile') {
|
|
777
|
+
// Rewrite session.json counters from trace.jsonl without ending the session,
|
|
778
|
+
// so hook-driven auto-sessions that never call `end` still report real numbers.
|
|
779
|
+
output(opts.all ? reconcileAllTraceSessions(cwd) : reconcileTraceSession(cwd, opts.sessionId), raw);
|
|
625
780
|
} else {
|
|
626
|
-
output({ error: 'Unknown trace subcommand. Available: init, log, end, current, list, show' }, raw);
|
|
781
|
+
output({ error: 'Unknown trace subcommand. Available: init, log, end, current, list, show, reconcile' }, raw);
|
|
627
782
|
}
|
|
628
783
|
}
|
|
629
784
|
|
|
@@ -1087,6 +1242,10 @@ module.exports = {
|
|
|
1087
1242
|
endTraceSession,
|
|
1088
1243
|
readTraceSession,
|
|
1089
1244
|
listTraceSessions,
|
|
1245
|
+
reconcileSessionMeta,
|
|
1246
|
+
reconcileTraceSession,
|
|
1247
|
+
reconcileAllTraceSessions,
|
|
1248
|
+
computeSessionCostAndCommits,
|
|
1090
1249
|
// Analysis
|
|
1091
1250
|
analyzeEvents,
|
|
1092
1251
|
generateLocalReport,
|
|
@@ -505,6 +505,9 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
505
505
|
|
|
506
506
|
if (updated.length > 0) {
|
|
507
507
|
writeStateMd(statePath, content, cwd);
|
|
508
|
+
// Normal-flow checkpoint: reconcile the always-loaded project memory once
|
|
509
|
+
// the session is recorded (no-op when already lean; never throws).
|
|
510
|
+
try { require('./memory-optimize.cjs').maybeAutoOptimizeMemory(cwd); } catch { /* best-effort */ }
|
|
508
511
|
output({ recorded: true, updated }, raw, 'true');
|
|
509
512
|
} else {
|
|
510
513
|
output({ recorded: false, reason: 'No session fields found in state.md' }, raw, 'false');
|
|
@@ -1021,6 +1024,7 @@ module.exports = {
|
|
|
1021
1024
|
stateExtractField,
|
|
1022
1025
|
stateReplaceField,
|
|
1023
1026
|
writeStateMd,
|
|
1027
|
+
syncStateFrontmatter,
|
|
1024
1028
|
cmdStateLoad,
|
|
1025
1029
|
cmdStateGet,
|
|
1026
1030
|
cmdStatePatch,
|
|
@@ -951,8 +951,18 @@ async function main() {
|
|
|
951
951
|
}, raw);
|
|
952
952
|
} else if (subcommand === 'budget') {
|
|
953
953
|
memory.cmdMemoryBudget(cwd, raw);
|
|
954
|
+
} else if (subcommand === 'optimize') {
|
|
955
|
+
const keepArg = getArgValue(args, '--keep');
|
|
956
|
+
require('./lib/memory-optimize.cjs').cmdMemoryOptimize(cwd, {
|
|
957
|
+
apply: args.includes('--apply'),
|
|
958
|
+
keep: keepArg ? Number(keepArg) : undefined,
|
|
959
|
+
}, raw);
|
|
960
|
+
} else if (subcommand === 'rebuild') {
|
|
961
|
+
require('./lib/memory-rebuild.cjs').cmdMemoryRebuild(cwd, {
|
|
962
|
+
apply: args.includes('--apply'),
|
|
963
|
+
}, raw);
|
|
954
964
|
} else {
|
|
955
|
-
error('Unknown memory subcommand. Available: read, append, list, compact, select, budget');
|
|
965
|
+
error('Unknown memory subcommand. Available: read, append, list, compact, select, budget, optimize, rebuild');
|
|
956
966
|
}
|
|
957
967
|
break;
|
|
958
968
|
}
|
|
@@ -1287,6 +1297,7 @@ async function main() {
|
|
|
1287
1297
|
const traceSub = args[2];
|
|
1288
1298
|
optimize.cmdOptimizeTrace(cwd, traceSub, {
|
|
1289
1299
|
sessionId: getArgValue(args, '--session'),
|
|
1300
|
+
all: args.includes('--all'),
|
|
1290
1301
|
description: getArgValue(args, '--description'),
|
|
1291
1302
|
command: getArgValue(args, '--command'),
|
|
1292
1303
|
phase: getArgValue(args, '--phase'),
|