ccc-notifier 0.6.3 → 0.8.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 (30) hide show
  1. package/README.md +5 -0
  2. package/dist/{budget-V7CCMJHF.js → budget-U37CCG4Y.js} +2 -2
  3. package/dist/chunk-3M5UQK76.js +92 -0
  4. package/dist/chunk-6IVSZ3S4.js +22 -0
  5. package/dist/{sweep-CONNNBK7.js → chunk-C7QOYVGT.js} +324 -120
  6. package/dist/{chunk-PYU7CAK2.js → chunk-DSTGXYSY.js} +44 -11
  7. package/dist/{chunk-D4D76RGQ.js → chunk-GAOGTALW.js} +361 -20
  8. package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
  9. package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
  10. package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
  11. package/dist/{chunk-OXXDZZPJ.js → chunk-RE72XBXB.js} +4 -6
  12. package/dist/{chunk-NV5UOHJA.js → chunk-TTD3BGOL.js} +5 -5
  13. package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
  14. package/dist/{chunk-J5QAYTFE.js → chunk-XOGYLFX3.js} +12 -1
  15. package/dist/chunk-YH45W7TA.js +479 -0
  16. package/dist/cli.js +287 -30
  17. package/dist/{dashboard-O7LJLYM3.js → dashboard-YG4H3GLA.js} +6 -4
  18. package/dist/{history-DL4SG3PG.js → history-W34EJOVV.js} +5 -3
  19. package/dist/{mute-UIR5P5N5.js → mute-23BWQXS3.js} +2 -2
  20. package/dist/reset-cursors-UVQXFN5Y.js +44 -0
  21. package/dist/scan-QB2N2O4Q.js +94 -0
  22. package/dist/{setup-I3JNGWZG.js → setup-2QDA3OWE.js} +5 -6
  23. package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
  24. package/dist/sweep-OLI2LIXJ.js +22 -0
  25. package/dist/track-TIINM4ZR.js +368 -0
  26. package/package.json +1 -1
  27. package/dist/chunk-HLJAJS2W.js +0 -55
  28. package/dist/chunk-HTYUYKFW.js +0 -21
  29. package/dist/chunk-OYD6H3ZZ.js +0 -124
  30. package/dist/track-RBLLR25M.js +0 -257
@@ -1,4 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ makeFullDashboardState,
4
+ writeFullDashboardStateAtomic
5
+ } from "./chunk-3M5UQK76.js";
6
+ import {
7
+ waitForDataLock
8
+ } from "./chunk-PXMXEFM7.js";
9
+ import {
10
+ effectiveSurface,
11
+ surfaceLabel
12
+ } from "./chunk-6IVSZ3S4.js";
2
13
  import {
3
14
  isWSL
4
15
  } from "./chunk-DGXUSPS4.js";
@@ -7,17 +18,12 @@ import {
7
18
  formatTokens,
8
19
  formatUSD,
9
20
  modelDisplayName
10
- } from "./chunk-J5QAYTFE.js";
11
- import {
12
- makeFullDashboardState,
13
- waitForDataLock,
14
- writeFullDashboardStateAtomic
15
- } from "./chunk-27SJELD2.js";
21
+ } from "./chunk-XOGYLFX3.js";
16
22
  import {
17
23
  paths,
18
24
  readConfig,
19
25
  readTurns
20
- } from "./chunk-OOAC5ULQ.js";
26
+ } from "./chunk-WONMZ466.js";
21
27
 
22
28
  // src/dashboard.ts
23
29
  import { spawn, spawnSync } from "child_process";
@@ -219,7 +225,7 @@ function buildTurnEmbed(rec, map) {
219
225
  const primary = rec.models[0] ?? "unknown";
220
226
  const extra = rec.models.length > 1 ? ` +${rec.models.length - 1}` : "";
221
227
  const saMark = rec.subagents ? " +SA" : "";
222
- const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\u672A\u96C6\u8A08)" : "";
228
+ const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\u691C\u51FA)" : "";
223
229
  let sa = null;
224
230
  if (rec.subagents) {
225
231
  const saModels = Object.keys(rec.subagents.costByModel).map((m) => modelDisplayName(m));
@@ -229,6 +235,12 @@ function buildTurnEmbed(rec, map) {
229
235
  models: saModels.join(", "),
230
236
  apiCalls: rec.subagents.apiCalls
231
237
  };
238
+ const b = rec.subagents.tokens;
239
+ const cache = b.cacheRead + b.cacheWrite5m + b.cacheWrite1h;
240
+ const total = b.input + b.output + cache;
241
+ if (total > 0) {
242
+ sa.tok = `\u8A08 ${formatTokens(total)} tokens(cache ${Math.round(cache / total * 100)}%)`;
243
+ }
232
244
  }
233
245
  const ms = Date.parse(rec.ts);
234
246
  const out = {
@@ -968,12 +980,12 @@ var APP_JS = `<script>
968
980
  dtd.appendChild(meta);
969
981
  if(t.sa){
970
982
  var saLine = document.createElement('div'); saLine.className = 'detail-meta';
971
- saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0);
983
+ saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0) + (t.sa.tok ? ' \xB7 ' + t.sa.tok : '');
972
984
  dtd.appendChild(saLine);
973
985
  }
974
986
  if(t.ca){
975
987
  var caLine = document.createElement('div'); caLine.className = 'detail-meta';
976
- caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A\u30FB\u6599\u91D1\u672A\u96C6\u8A08 \xB7 \u958B\u59CB ' +
988
+ caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A(\u6599\u91D1\u306F\u3053\u306E\u30BF\u30FC\u30F3\u306B\u542B\u3081\u306A\u3044)\xB7 \u958B\u59CB ' +
977
989
  (t.ca.started||0) + ' \xB7 \u7D42\u4E86 ' + (t.ca.stopped||0) + ' \xB7 \u7A2E\u5225 ' + ((t.ca.agentTypes||[]).join(', ')||'unknown');
978
990
  dtd.appendChild(caLine);
979
991
  }
@@ -1070,6 +1082,27 @@ var APP_JS = `<script>
1070
1082
  function statCard(label, sub, totals) {
1071
1083
  return `<div class="stat"><div class="stat-label">${esc(label)}<span class="stat-sub">${esc(sub)}</span></div><div class="stat-value">${esc(formatUSD(totals.usd))}</div><div class="stat-meta">${esc(formatJPY(totals.jpy))} \xB7 ${totals.turns} \u30BF\u30FC\u30F3</div></div>`;
1072
1084
  }
1085
+ function computeSurfaceBreakdown(turns) {
1086
+ const map = /* @__PURE__ */ new Map();
1087
+ for (const rec of turns) {
1088
+ const surface = effectiveSurface(rec);
1089
+ const agg = map.get(surface) ?? { surface, turns: 0, usd: 0, jpy: 0 };
1090
+ agg.turns += 1;
1091
+ agg.usd += turnTotalUSD(rec);
1092
+ agg.jpy += turnTotalJPY(rec);
1093
+ map.set(surface, agg);
1094
+ }
1095
+ return [...map.values()].sort((a, b) => b.usd - a.usd);
1096
+ }
1097
+ function surfaceSection(turns) {
1098
+ const rows = computeSurfaceBreakdown(turns);
1099
+ if (rows.length === 0) return "";
1100
+ if (rows.length === 1 && rows[0].surface === "cli") return "";
1101
+ const trs = rows.map(
1102
+ (r) => `<tr><td>${esc(surfaceLabel(r.surface))}</td><td class="c-num">${r.turns}</td><td class="c-num">${esc(formatUSD(r.usd))}</td><td class="c-num">${esc(formatJPY(r.jpy))}</td></tr>`
1103
+ ).join("");
1104
+ return `<section class="card"><h2>\u30B5\u30FC\u30D5\u30A7\u30B9\u5225\u5185\u8A33 / By surface</h2><p class="note">CLI \u4EE5\u5916(\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7\u30A2\u30D7\u30EA\u7B49)\u304B\u3089\u306E\u5229\u7528\u5206\u306E\u5185\u8A33(\u57CB\u3081\u8FBC\u307F\u5BFE\u8C61\u671F\u9593\u30FBSA \u8FBC\u307F\u7DCF\u984D)\u3002</p><div class="table-wrap"><table class="turns"><thead><tr><th>\u30B5\u30FC\u30D5\u30A7\u30B9</th><th class="c-num">\u30BF\u30FC\u30F3</th><th class="c-num">$</th><th class="c-num">\xA5</th></tr></thead><tbody>${trs}</tbody></table></div></section>`;
1105
+ }
1073
1106
  function renderLegend(slots) {
1074
1107
  if (slots.length < 2) return "";
1075
1108
  const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
@@ -1161,7 +1194,7 @@ function renderDashboard(turns, opts, fullHistory) {
1161
1194
  const capNote = "\u4EF6\u6570\u304C\u591A\u3044\u5834\u5408\u306F\u65B0\u3057\u3044\u9806\u306B\u5148\u982D200\u4EF6\u306E\u307F\u63CF\u753B\u3057\u307E\u3059(\u691C\u7D22\u3084\u3001\u4E0A\u306E\u30B0\u30E9\u30D5\u3067\u671F\u9593\u3092\u9078\u3076\u3068\u7D5E\u308A\u8FBC\u3081\u307E\u3059)\u3002";
1162
1195
  const truncNote = anyTruncated ? `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u30D7\u30ED\u30F3\u30D7\u30C8\u306F1\u4EF6\u3042\u305F\u308A\u6700\u5927 ${PROMPT_MAX.toLocaleString("en-US")} \u5B57\u307E\u3067(\u8D85\u904E\u5206\u306F\u300C${esc(PROMPT_TRUNC_MARK)}\u300D)\u3002\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u5168\u6587\u5C55\u958B\u3002</p>` : `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u5C55\u958B\u3057\u307E\u3059\u3002</p>`;
1163
1196
  const historySection = `<section class="card"><h2>\u30BF\u30FC\u30F3\u5C65\u6B74 / History</h2>` + truncNote + `<div class="toolbar"><input id="turn-search" class="search" type="search" placeholder="\u30D7\u30ED\u30F3\u30D7\u30C8\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30FB\u30E2\u30C7\u30EB\u3067\u691C\u7D22 / search\u2026" autocomplete="off"><span class="count"><span id="turn-count">0</span></span></div><div class="table-wrap"><table class="turns"><thead><tr><th>\u6642\u523B</th><th>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8</th><th>\u30E2\u30C7\u30EB</th><th class="c-num">in</th><th class="c-num">out</th><th class="c-num">$</th><th class="c-num">\xA5</th><th>\u30D7\u30ED\u30F3\u30D7\u30C8</th></tr></thead><tbody id="turn-body"></tbody></table></div></section>`;
1164
- return head + header + kpiSection + budgetSection + chartSection + breakdownSection + historySection + foot;
1197
+ return head + header + kpiSection + budgetSection + chartSection + breakdownSection + surfaceSection(turns) + historySection + foot;
1165
1198
  }
1166
1199
  function toWindowsPath(path) {
1167
1200
  try {
@@ -82,6 +82,205 @@ async function getUsdJpy(cfg, cacheDir) {
82
82
  return { rate: cfg.fx.fallbackRate, source: "fixed", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
83
83
  }
84
84
 
85
+ // src/claude-roots.ts
86
+ import { promises as fsp } from "fs";
87
+ import { homedir } from "os";
88
+ import * as nodePath from "path";
89
+ import { delimiter, join as join2 } from "path";
90
+ var DESKTOP_DISCOVERY_MAX_DEPTH = 8;
91
+ function defaultClaudeProjectsRoot(override) {
92
+ if (override) return override;
93
+ return process.env.CCCN_CLAUDE_PROJECTS || join2(homedir(), ".claude", "projects");
94
+ }
95
+ function defaultClaudeDesktopSessionsRoot() {
96
+ return join2(homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
97
+ }
98
+ async function isDirectory(path) {
99
+ try {
100
+ return (await fsp.stat(path)).isDirectory();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ async function discoverDesktopProjectRoots(sessionsRoot) {
106
+ const found = [];
107
+ const walk = async (dir, depth) => {
108
+ if (depth > DESKTOP_DISCOVERY_MAX_DEPTH) return;
109
+ let entries;
110
+ try {
111
+ entries = await fsp.readdir(dir, { withFileTypes: true });
112
+ } catch {
113
+ return;
114
+ }
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+ const full = join2(dir, entry.name);
118
+ if (entry.name === ".claude") {
119
+ const projects = join2(full, "projects");
120
+ if (await isDirectory(projects)) found.push(projects);
121
+ continue;
122
+ }
123
+ await walk(full, depth + 1);
124
+ }
125
+ };
126
+ await walk(sessionsRoot, 0);
127
+ return found;
128
+ }
129
+ async function claudeTranscriptRoots(opts = {}) {
130
+ const roots = [
131
+ { path: defaultClaudeProjectsRoot(opts.projectsOverride), surface: "cli" }
132
+ ];
133
+ const override = process.env.CCCN_CLAUDE_DESKTOP_ROOTS;
134
+ if (typeof override === "string" && override.length > 0) {
135
+ for (const raw of override.split(delimiter)) {
136
+ const p = raw.trim();
137
+ if (p.length > 0) roots.push({ path: p, surface: "desktop" });
138
+ }
139
+ return roots;
140
+ }
141
+ const explicitSessionsRoot = opts.desktopSessionsRoot;
142
+ const sessionsRoot = explicitSessionsRoot ?? (process.platform === "darwin" ? defaultClaudeDesktopSessionsRoot() : null);
143
+ if (sessionsRoot !== null && await isDirectory(sessionsRoot)) {
144
+ for (const p of await discoverDesktopProjectRoots(sessionsRoot)) {
145
+ roots.push({ path: p, surface: "desktop" });
146
+ }
147
+ }
148
+ return roots;
149
+ }
150
+ function surfaceForClaudePath(transcriptPath, roots) {
151
+ return rootForClaudePath(transcriptPath, roots)?.surface ?? "cli";
152
+ }
153
+ function rootForClaudePath(transcriptPath, roots) {
154
+ let best = null;
155
+ for (const root of roots) {
156
+ if (!isUnderRoot(transcriptPath, root.path)) continue;
157
+ if (best === null || root.path.length > best.path.length) best = root;
158
+ }
159
+ return best;
160
+ }
161
+ function isUnderRoot(target, root, flavor = nodePath) {
162
+ let rel;
163
+ try {
164
+ rel = flavor.relative(root, target);
165
+ } catch {
166
+ return false;
167
+ }
168
+ if (rel.length === 0) return true;
169
+ if (flavor.isAbsolute(rel)) return false;
170
+ return rel !== ".." && !rel.startsWith(`..${flavor.sep}`);
171
+ }
172
+ async function determineClaudeSurface(transcriptPath) {
173
+ if (process.env.CCCN_CLAUDE_DESKTOP_ROOTS === void 0) {
174
+ if (process.platform !== "darwin") return "cli";
175
+ const sessionsRoot = defaultClaudeDesktopSessionsRoot();
176
+ const withSep = sessionsRoot.endsWith("/") ? sessionsRoot : `${sessionsRoot}/`;
177
+ if (transcriptPath !== sessionsRoot && !transcriptPath.startsWith(withSep)) return "cli";
178
+ }
179
+ const roots = await claudeTranscriptRoots();
180
+ return surfaceForClaudePath(transcriptPath, roots);
181
+ }
182
+
183
+ // src/codex/sessions.ts
184
+ import { promises as fsp2 } from "fs";
185
+ import { join as join3, resolve } from "path";
186
+ var CODEX_MAX_DEPTH = 4;
187
+ async function listCodexRollouts(sessionsRoot) {
188
+ const rollouts = [];
189
+ let unreadableDirs = 0;
190
+ const root = resolve(sessionsRoot);
191
+ const rootStat = await fsp2.lstat(root).catch(() => null);
192
+ if (rootStat === null || !rootStat.isDirectory()) {
193
+ return { rollouts, unreadableDirs: 1 };
194
+ }
195
+ const walk = async (dir, depth) => {
196
+ const entries = await fsp2.readdir(dir, { withFileTypes: true }).catch(() => null);
197
+ if (entries === null) {
198
+ unreadableDirs += 1;
199
+ return;
200
+ }
201
+ for (const entry of entries) {
202
+ const full = resolve(join3(dir, entry.name));
203
+ if (entry.isFile()) {
204
+ if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) rollouts.push(full);
205
+ } else if (entry.isDirectory() && depth < CODEX_MAX_DEPTH) {
206
+ await walk(full, depth + 1);
207
+ }
208
+ }
209
+ };
210
+ await walk(root, 1);
211
+ return { rollouts, unreadableDirs };
212
+ }
213
+ async function findLatestCodexRollout(sessionsRoot) {
214
+ const discovery = await listCodexRollouts(sessionsRoot);
215
+ let latest = null;
216
+ let latestMtime = -Infinity;
217
+ let unreadableFiles = 0;
218
+ for (const path of discovery.rollouts) {
219
+ const stat = await fsp2.lstat(path).catch(() => null);
220
+ if (stat === null || !stat.isFile()) {
221
+ unreadableFiles += 1;
222
+ continue;
223
+ }
224
+ if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && (latest === null || path < latest)) {
225
+ latest = path;
226
+ latestMtime = stat.mtimeMs;
227
+ }
228
+ }
229
+ return { latest, unreadableDirs: discovery.unreadableDirs, unreadableFiles };
230
+ }
231
+
232
+ // src/counted-calls.ts
233
+ import { createHash } from "crypto";
234
+ var FINGERPRINT_HEX = 16;
235
+ function sha256Hex(material) {
236
+ return createHash("sha256").update(material).digest("hex");
237
+ }
238
+ function callFingerprint(messageKey) {
239
+ return sha256Hex(`claude-call|${messageKey}`).slice(0, FINGERPRINT_HEX);
240
+ }
241
+ function callFingerprints(messageKeys) {
242
+ const out = [];
243
+ for (const key of messageKeys) out.push(callFingerprint(key));
244
+ return out;
245
+ }
246
+ function codexEventFingerprint(rolloutFile, sessionId, byteOffset, totals) {
247
+ const material = [
248
+ "codex-event",
249
+ rolloutFile,
250
+ sessionId,
251
+ byteOffset,
252
+ totals.input,
253
+ totals.cached,
254
+ totals.output
255
+ ].join("|");
256
+ return sha256Hex(material).slice(0, FINGERPRINT_HEX);
257
+ }
258
+ function ingestKeyOf(fingerprints) {
259
+ if (fingerprints.length === 0) return void 0;
260
+ return sha256Hex([...fingerprints].sort().join(","));
261
+ }
262
+ function setCountedCalls(rec, fingerprints) {
263
+ const unique = [...new Set(fingerprints)].sort();
264
+ if (unique.length === 0) {
265
+ delete rec.countedCalls;
266
+ delete rec.ingestKey;
267
+ return rec;
268
+ }
269
+ rec.countedCalls = unique;
270
+ rec.ingestKey = ingestKeyOf(unique);
271
+ return rec;
272
+ }
273
+ function addCountedCalls(rec, fingerprints) {
274
+ return setCountedCalls(rec, [...rec.countedCalls ?? [], ...fingerprints]);
275
+ }
276
+ function messageKeyFilterOf(counted) {
277
+ return { has: (messageKey) => counted.has(callFingerprint(messageKey)) };
278
+ }
279
+ function anyOf(filters) {
280
+ const active = filters.filter((f) => f !== void 0);
281
+ return { has: (messageKey) => active.some((f) => f.has(messageKey)) };
282
+ }
283
+
85
284
  // src/codex/transcript.ts
86
285
  import { readFile as readFile2 } from "fs/promises";
87
286
  import { basename } from "path";
@@ -108,11 +307,14 @@ function addTotals(target, d) {
108
307
  }
109
308
  function readTotals(v) {
110
309
  if (!isRecord(v)) return null;
111
- return {
112
- input: numOf(v.input_tokens),
113
- cached: numOf(v.cached_input_tokens),
114
- output: numOf(v.output_tokens)
115
- };
310
+ const input = numOf(v.input_tokens);
311
+ const cached = numOf(v.cached_input_tokens);
312
+ const output = numOf(v.output_tokens);
313
+ if (input === 0 && cached === 0 && output === 0) {
314
+ const total = numOf(v.total_tokens);
315
+ if (total > 0) return { input: total, cached: total, output: 0 };
316
+ }
317
+ return { input, cached, output };
116
318
  }
117
319
  function totalsToBuckets(acc) {
118
320
  return {
@@ -136,6 +338,28 @@ async function readAll(path) {
136
338
  return null;
137
339
  }
138
340
  }
341
+ function readSessionMetaSeed(buffer) {
342
+ const nl = buffer.indexOf(NEWLINE);
343
+ if (nl <= 0) return null;
344
+ let obj;
345
+ try {
346
+ obj = JSON.parse(buffer.toString("utf8", 0, nl));
347
+ } catch {
348
+ return null;
349
+ }
350
+ if (!isRecord(obj) || obj.type !== "session_meta") return null;
351
+ const payload = isRecord(obj.payload) ? obj.payload : null;
352
+ if (payload === null) return null;
353
+ const source = payload.source;
354
+ return {
355
+ // session_meta.payload のキーは id。将来 session_id へ戻る可能性に備えて両方見る。
356
+ sessionId: strOrNull(payload.id) ?? strOrNull(payload.session_id),
357
+ cwd: strOrNull(payload.cwd),
358
+ originator: strOrNull(payload.originator),
359
+ isSubagentRollout: isRecord(source) && Object.hasOwn(source, "subagent"),
360
+ isForkedRollout: strOrNull(payload.forked_from_id) !== null
361
+ };
362
+ }
139
363
  function newSegmentBuf() {
140
364
  return {
141
365
  acc: zeroTotals(),
@@ -144,10 +368,11 @@ function newSegmentBuf() {
144
368
  turnCtxCwd: null,
145
369
  firstTs: null,
146
370
  endTs: null,
147
- hasLines: false
371
+ hasLines: false,
372
+ eventKeys: []
148
373
  };
149
374
  }
150
- async function scanWindow(rolloutPath, cursor) {
375
+ async function scanWindow(rolloutPath, cursor, opts = {}) {
151
376
  const buffer = await readAll(rolloutPath);
152
377
  if (buffer === null) return null;
153
378
  const fileSize = buffer.length;
@@ -165,16 +390,32 @@ async function scanWindow(rolloutPath, cursor) {
165
390
  let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
166
391
  const acc = zeroTotals();
167
392
  let apiCalls = 0;
168
- let lastModel = null;
393
+ let lastModel = cursor?.codexModel ?? null;
169
394
  let windowPrompt = null;
170
395
  let windowTurnCtxCwd = null;
171
396
  let sessionMetaCwd = null;
172
397
  let sessionMetaSid = null;
173
398
  let isSubagentRollout = false;
399
+ let isForkedRollout = false;
400
+ let originator = cursor?.codexOriginator ?? null;
401
+ if (startOffset > 0) {
402
+ const seed = readSessionMetaSeed(buffer);
403
+ if (seed !== null) {
404
+ sessionMetaSid = seed.sessionId;
405
+ sessionMetaCwd = seed.cwd;
406
+ if (seed.originator !== null) originator = seed.originator;
407
+ isSubagentRollout = seed.isSubagentRollout;
408
+ isForkedRollout = seed.isForkedRollout;
409
+ }
410
+ }
174
411
  let firstTs = null;
175
412
  let lastTs = null;
176
413
  const segments = [];
414
+ const windowEventKeys = [];
177
415
  let seg = newSegmentBuf();
416
+ const rolloutFile = basename(rolloutPath);
417
+ const filenameId = sessionIdFromFilename(rolloutPath);
418
+ const rolloutId = () => sessionMetaSid ?? filenameId;
178
419
  const snapshotSegment = (endOffset) => ({
179
420
  acc: seg.acc,
180
421
  apiCalls: seg.apiCalls,
@@ -185,9 +426,10 @@ async function scanWindow(rolloutPath, cursor) {
185
426
  endTs: seg.endTs,
186
427
  endOffset,
187
428
  prevAtEnd: { ...prev },
188
- lastTsAtEnd: lastTs
429
+ lastTsAtEnd: lastTs,
430
+ eventKeys: seg.eventKeys
189
431
  });
190
- const handleLine = (raw, endOffset) => {
432
+ const handleLine = (raw, lineOffset, endOffset) => {
191
433
  if (raw.trim().length === 0) return;
192
434
  let obj;
193
435
  try {
@@ -209,12 +451,15 @@ async function scanWindow(rolloutPath, cursor) {
209
451
  if (payload === null) return;
210
452
  const type = obj.type;
211
453
  if (type === "session_meta") {
212
- const sid = strOrNull(payload.session_id);
454
+ const sid = strOrNull(payload.id) ?? strOrNull(payload.session_id);
213
455
  if (sid !== null) sessionMetaSid = sid;
214
456
  const c = strOrNull(payload.cwd);
215
457
  if (c !== null) sessionMetaCwd = c;
216
458
  const source = payload.source;
217
459
  if (isRecord(source) && Object.hasOwn(source, "subagent")) isSubagentRollout = true;
460
+ if (strOrNull(payload.forked_from_id) !== null) isForkedRollout = true;
461
+ const orig = strOrNull(payload.originator);
462
+ if (orig !== null) originator = orig;
218
463
  return;
219
464
  }
220
465
  if (type === "turn_context") {
@@ -242,6 +487,11 @@ async function scanWindow(rolloutPath, cursor) {
242
487
  if (info === null) return;
243
488
  const total = readTotals(info.total_token_usage);
244
489
  if (total === null) return;
490
+ const eventKey = codexEventFingerprint(rolloutFile, rolloutId(), lineOffset, total);
491
+ if (opts.excludeEvents?.has(eventKey) === true) {
492
+ prev = total;
493
+ return;
494
+ }
245
495
  let step = {
246
496
  input: total.input - prev.input,
247
497
  cached: total.cached - prev.cached,
@@ -252,6 +502,8 @@ async function scanWindow(rolloutPath, cursor) {
252
502
  }
253
503
  addTotals(acc, step);
254
504
  addTotals(seg.acc, step);
505
+ seg.eventKeys.push(eventKey);
506
+ windowEventKeys.push(eventKey);
255
507
  prev = total;
256
508
  if (!isZeroTotals(step)) {
257
509
  apiCalls++;
@@ -267,7 +519,7 @@ async function scanWindow(rolloutPath, cursor) {
267
519
  let lineStart = startOffset;
268
520
  for (let pos = startOffset; pos < fileSize; pos++) {
269
521
  if (buffer[pos] !== NEWLINE) continue;
270
- handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
522
+ handleLine(buffer.toString("utf8", lineStart, pos), lineStart, pos + 1);
271
523
  lineStart = pos + 1;
272
524
  }
273
525
  const newOffset = lineStart;
@@ -281,8 +533,11 @@ async function scanWindow(rolloutPath, cursor) {
281
533
  model: lastModel,
282
534
  prompt: windowPrompt,
283
535
  cwd: windowTurnCtxCwd ?? sessionMetaCwd,
284
- sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
536
+ sessionId: rolloutId(),
537
+ eventKeys: windowEventKeys,
285
538
  isSubagentRollout,
539
+ isForkedRollout,
540
+ originator,
286
541
  firstTs,
287
542
  lastTs,
288
543
  newOffset
@@ -296,11 +551,13 @@ function windowCursor(scan) {
296
551
  lastTs: scan.lastTs,
297
552
  seenMessageKeys: [],
298
553
  // 去重は codexTotals の差分方式が担う
299
- codexTotals: { ...scan.prev }
554
+ codexTotals: { ...scan.prev },
555
+ codexOriginator: scan.originator,
556
+ codexModel: scan.model
300
557
  };
301
558
  }
302
- async function aggregateCodexTurn(rolloutPath, cursor) {
303
- const scan = await scanWindow(rolloutPath, cursor);
559
+ async function aggregateCodexTurn(rolloutPath, cursor, opts = {}) {
560
+ const scan = await scanWindow(rolloutPath, cursor, opts);
304
561
  if (scan === null || isZeroTotals(scan.acc)) return null;
305
562
  return {
306
563
  sessionId: scan.sessionId,
@@ -314,12 +571,74 @@ async function aggregateCodexTurn(rolloutPath, cursor) {
314
571
  // rollout に無い
315
572
  firstTs: scan.firstTs,
316
573
  lastTs: scan.lastTs,
317
- newCursor: windowCursor(scan)
574
+ newCursor: windowCursor(scan),
575
+ originator: scan.originator,
576
+ isSubagentRollout: scan.isSubagentRollout,
577
+ isForkedRollout: scan.isForkedRollout,
578
+ codexEventKeys: scan.eventKeys
579
+ };
580
+ }
581
+ async function codexConsumedCursor(rolloutPath, cursor, opts = {}) {
582
+ const scan = await scanWindow(rolloutPath, cursor, opts);
583
+ return scan === null ? null : windowCursor(scan);
584
+ }
585
+ async function scanCodexTurns(rolloutPath, cursor, opts = {}) {
586
+ const scan = await scanWindow(rolloutPath, cursor, opts);
587
+ if (scan === null) return null;
588
+ const newCursor = windowCursor(scan);
589
+ if (isZeroTotals(scan.acc)) return { drafts: [], newCursor };
590
+ return { drafts: draftsFromScan(scan), newCursor };
591
+ }
592
+ async function codexResumePointAtTs(rolloutPath, floorTs) {
593
+ const buffer = await readAll(rolloutPath);
594
+ if (buffer === null) return null;
595
+ const seed = readSessionMetaSeed(buffer);
596
+ let prev = zeroTotals();
597
+ let lastModel = null;
598
+ let consumedEnd = 0;
599
+ let lineStart = 0;
600
+ for (let pos = 0; pos < buffer.length; pos++) {
601
+ if (buffer[pos] !== NEWLINE) continue;
602
+ const raw = buffer.toString("utf8", lineStart, pos);
603
+ let obj = null;
604
+ try {
605
+ obj = JSON.parse(raw);
606
+ } catch {
607
+ obj = null;
608
+ }
609
+ if (isRecord(obj)) {
610
+ const ts = strOrNull(obj.timestamp);
611
+ if (ts !== null && ts > floorTs) break;
612
+ const payload = isRecord(obj.payload) ? obj.payload : null;
613
+ if (obj.type === "turn_context" && payload !== null) {
614
+ const m = strOrNull(payload.model);
615
+ if (m !== null) lastModel = m;
616
+ }
617
+ if (obj.type === "event_msg" && payload !== null && payload.type === "token_count") {
618
+ const info = isRecord(payload.info) ? payload.info : null;
619
+ const total = info === null ? null : readTotals(info.total_token_usage);
620
+ if (total !== null) prev = total;
621
+ }
622
+ }
623
+ consumedEnd = pos + 1;
624
+ lineStart = pos + 1;
625
+ }
626
+ return {
627
+ offset: consumedEnd,
628
+ lastUuid: null,
629
+ lastTs: floorTs,
630
+ seenMessageKeys: [],
631
+ codexTotals: prev,
632
+ codexOriginator: seed?.originator ?? null,
633
+ codexModel: lastModel
318
634
  };
319
635
  }
320
- async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
321
- const scan = await scanWindow(rolloutPath, cursor);
636
+ async function splitIntoCodexTurnDrafts(rolloutPath, cursor, opts = {}) {
637
+ const scan = await scanWindow(rolloutPath, cursor, opts);
322
638
  if (scan === null || isZeroTotals(scan.acc)) return null;
639
+ return draftsFromScan(scan);
640
+ }
641
+ function draftsFromScan(scan) {
323
642
  const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
324
643
  if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
325
644
  picked.push(scan.open);
@@ -327,6 +646,7 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
327
646
  const lastIndex = picked.length - 1;
328
647
  return picked.map((s, i) => ({
329
648
  isSubagentRollout: scan.isSubagentRollout,
649
+ isForkedRollout: scan.isForkedRollout,
330
650
  agg: {
331
651
  sessionId: scan.sessionId,
332
652
  // session_meta はファイル先頭にしか無いので全ドラフト共通
@@ -336,6 +656,11 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
336
656
  prompt: s.prompt,
337
657
  cwd: s.cwd,
338
658
  gitBranch: null,
659
+ originator: scan.originator,
660
+ // session_meta はファイル先頭にしか無いので全ドラフト共通
661
+ isSubagentRollout: scan.isSubagentRollout,
662
+ isForkedRollout: scan.isForkedRollout,
663
+ codexEventKeys: s.eventKeys,
339
664
  firstTs: s.firstTs,
340
665
  lastTs: s.endTs,
341
666
  // 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
@@ -346,7 +671,9 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
346
671
  lastUuid: null,
347
672
  lastTs: s.lastTsAtEnd,
348
673
  seenMessageKeys: [],
349
- codexTotals: { ...s.prevAtEnd }
674
+ codexTotals: { ...s.prevAtEnd },
675
+ codexOriginator: scan.originator,
676
+ codexModel: s.model
350
677
  }
351
678
  },
352
679
  endTs: s.endTs
@@ -539,7 +866,21 @@ async function aggregateNewTurn(transcriptPath, cursor, opts = {}) {
539
866
 
540
867
  export {
541
868
  getUsdJpy,
869
+ claudeTranscriptRoots,
870
+ surfaceForClaudePath,
871
+ rootForClaudePath,
872
+ determineClaudeSurface,
873
+ listCodexRollouts,
874
+ findLatestCodexRollout,
875
+ callFingerprints,
876
+ setCountedCalls,
877
+ addCountedCalls,
878
+ messageKeyFilterOf,
879
+ anyOf,
542
880
  aggregateCodexTurn,
881
+ codexConsumedCursor,
882
+ scanCodexTurns,
883
+ codexResumePointAtTs,
543
884
  splitIntoCodexTurnDrafts,
544
885
  extractBucket,
545
886
  promptCandidate,
@@ -4,7 +4,7 @@ import {
4
4
  isMuted,
5
5
  readMuteState,
6
6
  writeMuteState
7
- } from "./chunk-OOAC5ULQ.js";
7
+ } from "./chunk-WONMZ466.js";
8
8
 
9
9
  // src/mute.ts
10
10
  function parseDuration(arg) {