ccc-notifier 0.6.2 → 0.7.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/README.md +5 -0
- package/dist/{budget-V7CCMJHF.js → budget-O4VAP6RB.js} +2 -2
- package/dist/chunk-3M5UQK76.js +92 -0
- package/dist/{sweep-65D57PXU.js → chunk-6BXD6HU5.js} +275 -113
- package/dist/chunk-6IVSZ3S4.js +22 -0
- package/dist/{chunk-NV5UOHJA.js → chunk-JLDI4D5E.js} +5 -5
- package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
- package/dist/{chunk-6UZXXO5J.js → chunk-O2E4VTDR.js} +36 -9
- package/dist/{chunk-D4D76RGQ.js → chunk-OCKZZVOG.js} +353 -20
- package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
- package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
- package/dist/chunk-V4IS4FXH.js +454 -0
- package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
- package/dist/{chunk-OXXDZZPJ.js → chunk-X7U63BZG.js} +4 -6
- package/dist/{chunk-J5QAYTFE.js → chunk-XG73C6CC.js} +10 -1
- package/dist/cli.js +286 -29
- package/dist/{dashboard-VEKQ4TI3.js → dashboard-64XHDUVT.js} +6 -4
- package/dist/{history-DL4SG3PG.js → history-W34EJOVV.js} +5 -3
- package/dist/{mute-UIR5P5N5.js → mute-23BWQXS3.js} +2 -2
- package/dist/reset-cursors-5CCM6MQL.js +41 -0
- package/dist/scan-UB6ORRZK.js +87 -0
- package/dist/{setup-I3JNGWZG.js → setup-VLMW433R.js} +5 -6
- package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
- package/dist/sweep-DVOV4XIZ.js +20 -0
- package/dist/track-P6A5WDJZ.js +368 -0
- package/package.json +1 -1
- package/dist/chunk-HLJAJS2W.js +0 -55
- package/dist/chunk-HTYUYKFW.js +0 -21
- package/dist/chunk-OYD6H3ZZ.js +0 -124
- package/dist/track-WCP7446C.js +0 -257
|
@@ -1,100 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
paths
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WONMZ466.js";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
6
|
+
// src/data-lock.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import { hostname } from "os";
|
|
7
9
|
import {
|
|
8
|
-
closeSync,
|
|
9
10
|
existsSync,
|
|
10
|
-
|
|
11
|
+
mkdirSync,
|
|
11
12
|
readFileSync,
|
|
12
|
-
readSync,
|
|
13
13
|
renameSync,
|
|
14
14
|
rmSync,
|
|
15
15
|
writeFileSync
|
|
16
16
|
} from "fs";
|
|
17
|
-
import { randomUUID } from "crypto";
|
|
18
|
-
function localDate(now) {
|
|
19
|
-
const y = now.getFullYear();
|
|
20
|
-
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
21
|
-
const d = String(now.getDate()).padStart(2, "0");
|
|
22
|
-
return `${y}-${m}-${d}`;
|
|
23
|
-
}
|
|
24
|
-
function timeZone() {
|
|
25
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
|
|
26
|
-
}
|
|
27
|
-
function makeFullDashboardState(now = /* @__PURE__ */ new Date()) {
|
|
28
|
-
return { localDate: localDate(now), timeZone: timeZone(), generatedAt: now.toISOString() };
|
|
29
|
-
}
|
|
30
|
-
function readState() {
|
|
31
|
-
const file = paths().dashboardFullStateFile;
|
|
32
|
-
if (!existsSync(file)) return null;
|
|
33
|
-
try {
|
|
34
|
-
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
35
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
36
|
-
const v = value;
|
|
37
|
-
if (typeof v.localDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v.localDate) || typeof v.timeZone !== "string" || v.timeZone.length === 0 || typeof v.generatedAt !== "string" || !Number.isFinite(Date.parse(v.generatedAt))) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return v;
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function isFullDashboardDue(now = /* @__PURE__ */ new Date()) {
|
|
46
|
-
const p = paths();
|
|
47
|
-
if (!existsSync(p.fullDashboardFile)) return true;
|
|
48
|
-
try {
|
|
49
|
-
const fd = openSync(p.fullDashboardFile, "r");
|
|
50
|
-
try {
|
|
51
|
-
const head = Buffer.alloc(512);
|
|
52
|
-
const n = readSync(fd, head, 0, head.length, 0);
|
|
53
|
-
if (head.toString("utf8", 0, n).includes('name="cccn-placeholder"')) return true;
|
|
54
|
-
} finally {
|
|
55
|
-
closeSync(fd);
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
const state = readState();
|
|
61
|
-
if (state === null) return true;
|
|
62
|
-
const expected = makeFullDashboardState(now);
|
|
63
|
-
if (state.timeZone !== expected.timeZone) return true;
|
|
64
|
-
if (state.localDate !== expected.localDate) return true;
|
|
65
|
-
if (state.localDate > expected.localDate) return true;
|
|
66
|
-
if (Date.parse(state.generatedAt) > now.getTime()) return true;
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
function writeFullDashboardStateAtomic(state) {
|
|
70
|
-
const file = paths().dashboardFullStateFile;
|
|
71
|
-
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
72
|
-
try {
|
|
73
|
-
writeFileSync(tmp, `${JSON.stringify(state)}
|
|
74
|
-
`, "utf8");
|
|
75
|
-
renameSync(tmp, file);
|
|
76
|
-
} finally {
|
|
77
|
-
rmSync(tmp, { force: true });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function invalidateCanonicalDashboards() {
|
|
81
|
-
const p = paths();
|
|
82
|
-
for (const file of [p.recentDashboardFile, p.fullDashboardFile, p.dashboardFullStateFile]) {
|
|
83
|
-
rmSync(file, { force: true });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/data-lock.ts
|
|
88
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
89
|
-
import { hostname } from "os";
|
|
90
|
-
import {
|
|
91
|
-
existsSync as existsSync2,
|
|
92
|
-
mkdirSync,
|
|
93
|
-
readFileSync as readFileSync2,
|
|
94
|
-
renameSync as renameSync2,
|
|
95
|
-
rmSync as rmSync2,
|
|
96
|
-
writeFileSync as writeFileSync2
|
|
97
|
-
} from "fs";
|
|
98
17
|
import { join } from "path";
|
|
99
18
|
var DATA_LOCK_LEASE_MS = 3e4;
|
|
100
19
|
var HEARTBEAT_MS = 5e3;
|
|
@@ -103,7 +22,7 @@ function ownerFile(dir) {
|
|
|
103
22
|
}
|
|
104
23
|
function readOwner(dir) {
|
|
105
24
|
try {
|
|
106
|
-
const v = JSON.parse(
|
|
25
|
+
const v = JSON.parse(readFileSync(ownerFile(dir), "utf8"));
|
|
107
26
|
if (typeof v.token !== "string" || typeof v.pid !== "number" || typeof v.hostname !== "string" || typeof v.acquiredAt !== "string" || typeof v.heartbeatAt !== "string") return null;
|
|
108
27
|
return v;
|
|
109
28
|
} catch {
|
|
@@ -111,21 +30,21 @@ function readOwner(dir) {
|
|
|
111
30
|
}
|
|
112
31
|
}
|
|
113
32
|
function writeOwner(dir, owner) {
|
|
114
|
-
const tmp = join(dir, `owner.${owner.token}.${
|
|
33
|
+
const tmp = join(dir, `owner.${owner.token}.${randomUUID()}.tmp`);
|
|
115
34
|
try {
|
|
116
|
-
|
|
35
|
+
writeFileSync(tmp, `${JSON.stringify(owner)}
|
|
117
36
|
`, "utf8");
|
|
118
|
-
|
|
37
|
+
renameSync(tmp, ownerFile(dir));
|
|
119
38
|
} finally {
|
|
120
|
-
|
|
39
|
+
rmSync(tmp, { force: true });
|
|
121
40
|
}
|
|
122
41
|
}
|
|
123
42
|
function quarantineOwned(dir, token, label) {
|
|
124
43
|
const before = readOwner(dir);
|
|
125
44
|
if (before?.token !== token) return false;
|
|
126
|
-
const quarantine = `${dir}.${label}-${token}-${
|
|
45
|
+
const quarantine = `${dir}.${label}-${token}-${randomUUID()}`;
|
|
127
46
|
try {
|
|
128
|
-
|
|
47
|
+
renameSync(dir, quarantine);
|
|
129
48
|
} catch {
|
|
130
49
|
return false;
|
|
131
50
|
}
|
|
@@ -133,21 +52,21 @@ function quarantineOwned(dir, token, label) {
|
|
|
133
52
|
if (moved?.token !== token) {
|
|
134
53
|
return false;
|
|
135
54
|
}
|
|
136
|
-
|
|
55
|
+
rmSync(quarantine, { recursive: true, force: true });
|
|
137
56
|
return true;
|
|
138
57
|
}
|
|
139
58
|
function claimDir(fixed, label, now, metadataWriter = writeOwner) {
|
|
140
|
-
const token =
|
|
59
|
+
const token = randomUUID();
|
|
141
60
|
const staging = `${fixed}.${label}-${token}`;
|
|
142
61
|
const iso = now.toISOString();
|
|
143
62
|
const owner = { token, pid: process.pid, hostname: hostname(), acquiredAt: iso, heartbeatAt: iso };
|
|
144
63
|
try {
|
|
145
64
|
mkdirSync(staging);
|
|
146
65
|
metadataWriter(staging, owner);
|
|
147
|
-
|
|
66
|
+
renameSync(staging, fixed);
|
|
148
67
|
return { token, owner };
|
|
149
68
|
} catch {
|
|
150
|
-
|
|
69
|
+
rmSync(staging, { recursive: true, force: true });
|
|
151
70
|
return null;
|
|
152
71
|
}
|
|
153
72
|
}
|
|
@@ -180,15 +99,15 @@ function tryReclaim(now, leaseMs) {
|
|
|
180
99
|
if (!processDefinitelyDead(first.pid)) return false;
|
|
181
100
|
const second = readOwner(p.dataLockDir);
|
|
182
101
|
if (second === null || second.token !== first.token || second.heartbeatAt !== first.heartbeatAt) return false;
|
|
183
|
-
const orphan = `${p.dataLockDir}.orphan-${first.token}-${
|
|
102
|
+
const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID()}`;
|
|
184
103
|
try {
|
|
185
|
-
|
|
104
|
+
renameSync(p.dataLockDir, orphan);
|
|
186
105
|
} catch {
|
|
187
106
|
return false;
|
|
188
107
|
}
|
|
189
108
|
const moved = readOwner(orphan);
|
|
190
109
|
if (moved?.token !== first.token || moved.heartbeatAt !== first.heartbeatAt) return false;
|
|
191
|
-
|
|
110
|
+
rmSync(orphan, { recursive: true, force: true });
|
|
192
111
|
return true;
|
|
193
112
|
} finally {
|
|
194
113
|
quarantineOwned(p.dataReclaimDir, guard.token, "released");
|
|
@@ -198,15 +117,15 @@ function acquireDataLock(opts = {}) {
|
|
|
198
117
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
199
118
|
const leaseMs = opts.leaseMs ?? DATA_LOCK_LEASE_MS;
|
|
200
119
|
const p = paths();
|
|
201
|
-
if (
|
|
120
|
+
if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
|
|
202
121
|
let claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
|
|
203
122
|
if (claim === null) {
|
|
204
123
|
if (!tryReclaim(now, leaseMs)) return null;
|
|
205
|
-
if (
|
|
124
|
+
if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
|
|
206
125
|
claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
|
|
207
126
|
if (claim === null) return null;
|
|
208
127
|
}
|
|
209
|
-
if (
|
|
128
|
+
if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
|
|
210
129
|
quarantineOwned(p.dataLockDir, claim.token, "yielded");
|
|
211
130
|
return null;
|
|
212
131
|
}
|
|
@@ -249,9 +168,5 @@ async function waitForDataLock(timeoutMs, pollMs = 25) {
|
|
|
249
168
|
}
|
|
250
169
|
|
|
251
170
|
export {
|
|
252
|
-
makeFullDashboardState,
|
|
253
|
-
isFullDashboardDue,
|
|
254
|
-
writeFullDashboardStateAtomic,
|
|
255
|
-
invalidateCanonicalDashboards,
|
|
256
171
|
waitForDataLock
|
|
257
172
|
};
|
|
@@ -220,7 +220,24 @@ async function loadPriceTable(cacheDir, opts) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// src/codex/env.ts
|
|
224
|
+
import { statSync } from "fs";
|
|
225
|
+
import { homedir } from "os";
|
|
226
|
+
import { join } from "path";
|
|
227
|
+
function codexHome() {
|
|
228
|
+
return process.env.CCCN_CODEX_HOME || join(homedir(), ".codex");
|
|
229
|
+
}
|
|
230
|
+
function detectCodex() {
|
|
231
|
+
try {
|
|
232
|
+
return statSync(codexHome()).isDirectory();
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
export {
|
|
224
239
|
computeCost,
|
|
225
|
-
loadPriceTable
|
|
240
|
+
loadPriceTable,
|
|
241
|
+
codexHome,
|
|
242
|
+
detectCodex
|
|
226
243
|
};
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
notifyOS,
|
|
4
|
+
notifySlack
|
|
5
|
+
} from "./chunk-JLDI4D5E.js";
|
|
6
|
+
import {
|
|
7
|
+
attachSubagentGroups,
|
|
8
|
+
collectSubagentUsage,
|
|
9
|
+
normalizeCodexOriginator,
|
|
10
|
+
splitIntoTurnDrafts
|
|
11
|
+
} from "./chunk-6BXD6HU5.js";
|
|
12
|
+
import {
|
|
13
|
+
anyOf,
|
|
14
|
+
callFingerprints,
|
|
15
|
+
claudeTranscriptRoots,
|
|
16
|
+
codexResumePointAtTs,
|
|
17
|
+
getUsdJpy,
|
|
18
|
+
listCodexRollouts,
|
|
19
|
+
messageKeyFilterOf,
|
|
20
|
+
scanCodexTurns,
|
|
21
|
+
setCountedCalls,
|
|
22
|
+
surfaceForClaudePath
|
|
23
|
+
} from "./chunk-OCKZZVOG.js";
|
|
24
|
+
import {
|
|
25
|
+
codexHome,
|
|
26
|
+
computeCost,
|
|
27
|
+
loadPriceTable
|
|
28
|
+
} from "./chunk-Q45AAPJY.js";
|
|
29
|
+
import {
|
|
30
|
+
waitForDataLock
|
|
31
|
+
} from "./chunk-PXMXEFM7.js";
|
|
32
|
+
import {
|
|
33
|
+
formatIngestSummary
|
|
34
|
+
} from "./chunk-XG73C6CC.js";
|
|
35
|
+
import {
|
|
36
|
+
appendTurn,
|
|
37
|
+
floorKey,
|
|
38
|
+
isMuted,
|
|
39
|
+
loadAllCursors,
|
|
40
|
+
loadHistoryIndex,
|
|
41
|
+
logError,
|
|
42
|
+
paths,
|
|
43
|
+
readConfig,
|
|
44
|
+
readConfigReadOnly,
|
|
45
|
+
sanitizeCursor,
|
|
46
|
+
saveAllCursors
|
|
47
|
+
} from "./chunk-WONMZ466.js";
|
|
48
|
+
|
|
49
|
+
// src/ingest.ts
|
|
50
|
+
import { promises as fsp, readFileSync, renameSync, writeFileSync } from "fs";
|
|
51
|
+
import { basename, join } from "path";
|
|
52
|
+
function emptyBuckets() {
|
|
53
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
54
|
+
}
|
|
55
|
+
function sumBuckets(usage) {
|
|
56
|
+
const total = emptyBuckets();
|
|
57
|
+
for (const b of Object.values(usage)) {
|
|
58
|
+
total.input += b.input;
|
|
59
|
+
total.output += b.output;
|
|
60
|
+
total.cacheWrite5m += b.cacheWrite5m;
|
|
61
|
+
total.cacheWrite1h += b.cacheWrite1h;
|
|
62
|
+
total.cacheRead += b.cacheRead;
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
66
|
+
function collectModels(main, sidechain) {
|
|
67
|
+
const models = [];
|
|
68
|
+
for (const m of Object.keys(main)) if (!models.includes(m)) models.push(m);
|
|
69
|
+
for (const m of Object.keys(sidechain)) if (!models.includes(m)) models.push(m);
|
|
70
|
+
return models;
|
|
71
|
+
}
|
|
72
|
+
async function listProjectDirsBestEffort(root) {
|
|
73
|
+
try {
|
|
74
|
+
const entries = await fsp.readdir(root, { withFileTypes: true });
|
|
75
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function listTranscriptsBestEffort(projectDir) {
|
|
81
|
+
try {
|
|
82
|
+
const entries = await fsp.readdir(projectDir, { withFileTypes: true });
|
|
83
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(projectDir, e.name));
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function discoverClaudeFiles(roots) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (const root of roots) {
|
|
91
|
+
for (const dir of await listProjectDirsBestEffort(root.path)) {
|
|
92
|
+
files.push(...await listTranscriptsBestEffort(dir));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
97
|
+
async function discoverCodexFiles() {
|
|
98
|
+
const sessionsRoot = join(codexHome(), "sessions");
|
|
99
|
+
try {
|
|
100
|
+
const stat = await fsp.lstat(sessionsRoot);
|
|
101
|
+
if (!stat.isDirectory()) return [];
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const discovery = await listCodexRollouts(sessionsRoot);
|
|
107
|
+
return discovery.rollouts;
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function ingestMtimeCachePath() {
|
|
113
|
+
return join(paths().cacheDir, "ingest-mtimes.json");
|
|
114
|
+
}
|
|
115
|
+
function loadIngestMtimeCache() {
|
|
116
|
+
try {
|
|
117
|
+
const raw = readFileSync(ingestMtimeCachePath(), "utf8");
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
function saveIngestMtimeCache(cache) {
|
|
127
|
+
try {
|
|
128
|
+
const file = ingestMtimeCachePath();
|
|
129
|
+
const tmp = `${file}.tmp`;
|
|
130
|
+
writeFileSync(tmp, JSON.stringify(cache), "utf8");
|
|
131
|
+
renameSync(tmp, file);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function fileMtimeMs(filePath) {
|
|
136
|
+
return (await fsp.stat(filePath)).mtimeMs;
|
|
137
|
+
}
|
|
138
|
+
async function claudeMtimeSignature(filePath) {
|
|
139
|
+
let newest = (await fsp.stat(filePath)).mtimeMs;
|
|
140
|
+
const dir = join(
|
|
141
|
+
filePath.endsWith(".jsonl") ? filePath.slice(0, -".jsonl".length) : filePath,
|
|
142
|
+
"subagents"
|
|
143
|
+
);
|
|
144
|
+
let entries;
|
|
145
|
+
try {
|
|
146
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err?.code === "ENOENT") return newest;
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".jsonl")) continue;
|
|
153
|
+
try {
|
|
154
|
+
const m = (await fsp.stat(join(dir, entry.name))).mtimeMs;
|
|
155
|
+
if (m > newest) newest = m;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err?.code !== "ENOENT") throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return newest;
|
|
161
|
+
}
|
|
162
|
+
function recoveryCursor(floorTs) {
|
|
163
|
+
return { offset: 0, lastUuid: null, lastTs: floorTs, seenMessageKeys: [] };
|
|
164
|
+
}
|
|
165
|
+
function buildClaudeDraftRecord(draft, ts, table, fx, surface) {
|
|
166
|
+
const main = draft.mainPerModel;
|
|
167
|
+
const sidechain = draft.sidechainPerModel;
|
|
168
|
+
const breakdown = computeCost(main, sidechain, table);
|
|
169
|
+
const sidechainHasModels = Object.keys(sidechain).length > 0;
|
|
170
|
+
const rec = {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
ts,
|
|
173
|
+
sessionId: draft.sessionId,
|
|
174
|
+
project: draft.cwd ?? "",
|
|
175
|
+
gitBranch: draft.gitBranch,
|
|
176
|
+
models: collectModels(main, sidechain),
|
|
177
|
+
tokens: sumBuckets(main),
|
|
178
|
+
sidechainTokens: sidechainHasModels ? sumBuckets(sidechain) : null,
|
|
179
|
+
apiCalls: draft.apiCalls,
|
|
180
|
+
costUSD: breakdown.usd,
|
|
181
|
+
costByModel: breakdown.byModel,
|
|
182
|
+
costJPY: breakdown.usd * fx.rate,
|
|
183
|
+
fxRate: fx.rate,
|
|
184
|
+
fxSource: fx.source,
|
|
185
|
+
prompt: draft.prompt,
|
|
186
|
+
ingest: "scan",
|
|
187
|
+
surface
|
|
188
|
+
};
|
|
189
|
+
if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
|
|
190
|
+
setCountedCalls(rec, callFingerprints(draft.messageKeys));
|
|
191
|
+
return rec;
|
|
192
|
+
}
|
|
193
|
+
function sessionIdOfDrafts(drafts) {
|
|
194
|
+
for (let i = drafts.length - 1; i >= 0; i--) {
|
|
195
|
+
if (drafts[i].sessionId.length > 0) return drafts[i].sessionId;
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
function sessionIdFromTranscriptPath(filePath) {
|
|
200
|
+
const name = basename(filePath);
|
|
201
|
+
return name.endsWith(".jsonl") ? name.slice(0, -".jsonl".length) : "";
|
|
202
|
+
}
|
|
203
|
+
function buildCodexRecord(agg, table, fx) {
|
|
204
|
+
const main = agg.main;
|
|
205
|
+
const breakdown = computeCost(main, {}, table);
|
|
206
|
+
const surface = normalizeCodexOriginator(agg.originator);
|
|
207
|
+
const rec = {
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
210
|
+
sessionId: agg.sessionId,
|
|
211
|
+
project: agg.cwd ?? "",
|
|
212
|
+
gitBranch: null,
|
|
213
|
+
models: collectModels(main, {}),
|
|
214
|
+
tokens: sumBuckets(main),
|
|
215
|
+
sidechainTokens: null,
|
|
216
|
+
apiCalls: agg.apiCalls,
|
|
217
|
+
costUSD: breakdown.usd,
|
|
218
|
+
costByModel: breakdown.byModel,
|
|
219
|
+
costJPY: breakdown.usd * fx.rate,
|
|
220
|
+
fxRate: fx.rate,
|
|
221
|
+
fxSource: fx.source,
|
|
222
|
+
prompt: agg.prompt ?? "",
|
|
223
|
+
ingest: "scan",
|
|
224
|
+
source: "codex",
|
|
225
|
+
surface
|
|
226
|
+
};
|
|
227
|
+
if (typeof agg.originator === "string") rec.originator = agg.originator;
|
|
228
|
+
if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
|
|
229
|
+
setCountedCalls(rec, agg.codexEventKeys ?? []);
|
|
230
|
+
return rec;
|
|
231
|
+
}
|
|
232
|
+
function emptyResult(dryRun, fx, lockAcquired) {
|
|
233
|
+
return {
|
|
234
|
+
dryRun,
|
|
235
|
+
lockAcquired,
|
|
236
|
+
records: [],
|
|
237
|
+
scannedFiles: 0,
|
|
238
|
+
skippedByMtime: 0,
|
|
239
|
+
skippedDuplicates: 0,
|
|
240
|
+
failures: 0,
|
|
241
|
+
totalUSD: 0,
|
|
242
|
+
totalJPY: 0,
|
|
243
|
+
fxRate: fx.rate,
|
|
244
|
+
fxSource: fx.source,
|
|
245
|
+
bySurface: {}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function addRecord(result, rec) {
|
|
249
|
+
const usd = rec.costUSD + (rec.subagents?.costUSD ?? 0);
|
|
250
|
+
const jpy = rec.costJPY + (rec.subagents?.costUSD ?? 0) * rec.fxRate;
|
|
251
|
+
result.records.push(rec);
|
|
252
|
+
result.totalUSD += usd;
|
|
253
|
+
result.totalJPY += jpy;
|
|
254
|
+
const surface = rec.surface ?? "cli";
|
|
255
|
+
const cur = result.bySurface[surface] ?? { turns: 0, usd: 0 };
|
|
256
|
+
cur.turns += 1;
|
|
257
|
+
cur.usd += usd;
|
|
258
|
+
result.bySurface[surface] = cur;
|
|
259
|
+
}
|
|
260
|
+
async function runIngest(opts) {
|
|
261
|
+
const dryRun = opts.dryRun;
|
|
262
|
+
const cfg = opts.cfg ?? (dryRun ? readConfigReadOnly() : readConfig());
|
|
263
|
+
const cacheDir = paths().cacheDir;
|
|
264
|
+
const table = await loadPriceTable(cacheDir, { offline: opts.offlinePricing ?? true });
|
|
265
|
+
const fx = await getUsdJpy(cfg, cacheDir);
|
|
266
|
+
const claudeRoots = opts.claudeRoots ?? await claudeTranscriptRoots();
|
|
267
|
+
const includeCodex = opts.includeCodex ?? true;
|
|
268
|
+
const claudeFiles = await discoverClaudeFiles(claudeRoots);
|
|
269
|
+
const codexFiles = includeCodex ? await discoverCodexFiles() : [];
|
|
270
|
+
if (dryRun) {
|
|
271
|
+
return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, true);
|
|
272
|
+
}
|
|
273
|
+
const lock = await waitForDataLock(1e3);
|
|
274
|
+
if (lock === null) {
|
|
275
|
+
logError("ingest", new Error("data lock timeout; ingest skipped this cycle"));
|
|
276
|
+
return emptyResult(false, fx, false);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, false);
|
|
280
|
+
} finally {
|
|
281
|
+
lock.release();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, dryRun) {
|
|
285
|
+
const mtimeCache = loadIngestMtimeCache();
|
|
286
|
+
const nextMtimeCache = { ...mtimeCache };
|
|
287
|
+
const result = emptyResult(dryRun, fx, true);
|
|
288
|
+
const cursorsDict = loadAllCursors();
|
|
289
|
+
let cursorsChanged = false;
|
|
290
|
+
let index = null;
|
|
291
|
+
const historyIndex = () => index ??= loadHistoryIndex();
|
|
292
|
+
const recordedFloor = (scope, sessionId) => {
|
|
293
|
+
if (sessionId.length === 0) return null;
|
|
294
|
+
return historyIndex().legacyFloors.get(floorKey(scope, sessionId)) ?? null;
|
|
295
|
+
};
|
|
296
|
+
const countedFilter = () => messageKeyFilterOf(historyIndex().countedCalls);
|
|
297
|
+
const commit = (filePath, records, newCursor, subagentCursors = []) => {
|
|
298
|
+
const fresh = [];
|
|
299
|
+
for (const rec of records) {
|
|
300
|
+
const key = rec.ingestKey;
|
|
301
|
+
if (key !== void 0 && historyIndex().ingestKeys.has(key)) {
|
|
302
|
+
result.skippedDuplicates += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (key !== void 0) historyIndex().ingestKeys.add(key);
|
|
306
|
+
for (const fp of rec.countedCalls ?? []) historyIndex().countedCalls.add(fp);
|
|
307
|
+
fresh.push(rec);
|
|
308
|
+
}
|
|
309
|
+
if (!dryRun) {
|
|
310
|
+
for (const rec of fresh) appendTurn(rec);
|
|
311
|
+
cursorsDict[filePath] = newCursor;
|
|
312
|
+
for (const nc of subagentCursors) cursorsDict[nc.path] = nc.cursor;
|
|
313
|
+
cursorsChanged = true;
|
|
314
|
+
}
|
|
315
|
+
for (const rec of fresh) addRecord(result, rec);
|
|
316
|
+
};
|
|
317
|
+
const carryCursor = (next, prev, floor) => ({
|
|
318
|
+
...next,
|
|
319
|
+
lastUuid: next.lastUuid ?? prev?.lastUuid ?? null,
|
|
320
|
+
lastTs: next.lastTs ?? prev?.lastTs ?? floor
|
|
321
|
+
});
|
|
322
|
+
const processClaudeFile = async (filePath) => {
|
|
323
|
+
const cursor = sanitizeCursor(cursorsDict[filePath]);
|
|
324
|
+
const counted = countedFilter();
|
|
325
|
+
let split = await splitIntoTurnDrafts(filePath, cursor, { excludeMessageKeys: counted });
|
|
326
|
+
if (split.unreadable === true) throw new Error(`transcript \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
|
|
327
|
+
const sessionId = sessionIdOfDrafts(split.drafts) || sessionIdFromTranscriptPath(filePath);
|
|
328
|
+
let floor = null;
|
|
329
|
+
if (cursor === null) {
|
|
330
|
+
floor = recordedFloor("claude", sessionId);
|
|
331
|
+
if (floor !== null) {
|
|
332
|
+
split = await splitIntoTurnDrafts(filePath, recoveryCursor(floor), { excludeMessageKeys: counted });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const newCursor = carryCursor(split.newCursor, cursor, floor);
|
|
336
|
+
const surface = surfaceForClaudePath(filePath, claudeRoots);
|
|
337
|
+
const records = split.drafts.map(
|
|
338
|
+
(draft) => buildClaudeDraftRecord(draft, draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(), table, fx, surface)
|
|
339
|
+
);
|
|
340
|
+
const saFloor = recordedFloor("claude-sa", sessionId);
|
|
341
|
+
let sa = null;
|
|
342
|
+
try {
|
|
343
|
+
sa = await collectSubagentUsage(filePath, {
|
|
344
|
+
excludeMessageKeys: anyOf([new Set(split.messageKeys), counted]),
|
|
345
|
+
recovery: () => ({ minTimestampMs: saFloor === null ? null : Date.parse(saFloor) + 1 }),
|
|
346
|
+
readCursor: (path) => sanitizeCursor(cursorsDict[path])
|
|
347
|
+
});
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logError("ingest:subagents", err);
|
|
350
|
+
sa = null;
|
|
351
|
+
}
|
|
352
|
+
if (sa !== null && sa.apiCalls > 0) {
|
|
353
|
+
attachSubagentGroups(records, sa, table, fx, surface, "scan");
|
|
354
|
+
}
|
|
355
|
+
commit(filePath, records, newCursor, sa?.newCursors ?? []);
|
|
356
|
+
};
|
|
357
|
+
const processCodexFile = async (filePath) => {
|
|
358
|
+
const cursor = sanitizeCursor(cursorsDict[filePath]);
|
|
359
|
+
const scanOpts = { excludeEvents: historyIndex().countedCalls };
|
|
360
|
+
const scan = await scanCodexTurns(filePath, cursor, scanOpts);
|
|
361
|
+
if (scan === null) throw new Error(`rollout \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
|
|
362
|
+
const drafts = scan.drafts;
|
|
363
|
+
if (drafts.length === 0) {
|
|
364
|
+
commit(filePath, [], scan.newCursor);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const windowCursor = scan.newCursor;
|
|
368
|
+
if (drafts[0].isSubagentRollout) {
|
|
369
|
+
commit(filePath, [], windowCursor);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (cursor !== null) {
|
|
373
|
+
commit(filePath, drafts.map((draft) => buildCodexRecord(draft.agg, table, fx)), windowCursor);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const floor = recordedFloor("codex", drafts[0].agg.sessionId);
|
|
377
|
+
const target = floor === null ? drafts : (await scanCodexTurns(filePath, await codexResumePointAtTs(filePath, floor), scanOpts))?.drafts ?? [];
|
|
378
|
+
commit(filePath, target.map((draft) => buildCodexRecord(draft.agg, table, fx)), windowCursor);
|
|
379
|
+
};
|
|
380
|
+
const runOver = async (files, context, handle, signature = fileMtimeMs) => {
|
|
381
|
+
for (const filePath of files) {
|
|
382
|
+
let mtimeMs;
|
|
383
|
+
try {
|
|
384
|
+
mtimeMs = await signature(filePath);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (err?.code !== "ENOENT") {
|
|
387
|
+
result.failures += 1;
|
|
388
|
+
logError(context, err);
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const cached = mtimeCache[filePath];
|
|
393
|
+
if (cached !== void 0 && cached >= mtimeMs && sanitizeCursor(cursorsDict[filePath]) !== null) {
|
|
394
|
+
result.skippedByMtime += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
result.scannedFiles += 1;
|
|
398
|
+
try {
|
|
399
|
+
await handle(filePath);
|
|
400
|
+
nextMtimeCache[filePath] = mtimeMs;
|
|
401
|
+
} catch (err) {
|
|
402
|
+
result.failures += 1;
|
|
403
|
+
logError(context, err);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
await runOver(claudeFiles, "ingest:claude", processClaudeFile, claudeMtimeSignature);
|
|
408
|
+
await runOver(codexFiles, "ingest:codex", processCodexFile);
|
|
409
|
+
if (!dryRun) {
|
|
410
|
+
if (cursorsChanged) saveAllCursors(cursorsDict);
|
|
411
|
+
saveIngestMtimeCache(nextMtimeCache);
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
async function notifyIngestSummary(result, cfg) {
|
|
416
|
+
if (result.dryRun) return;
|
|
417
|
+
if (result.records.length === 0) return;
|
|
418
|
+
if (!(cfg.notify.os || cfg.notify.slack)) return;
|
|
419
|
+
if (result.totalUSD < cfg.minNotifyUSD) return;
|
|
420
|
+
if (isMuted()) return;
|
|
421
|
+
const bySurface = {};
|
|
422
|
+
for (const [surface, v] of Object.entries(result.bySurface)) {
|
|
423
|
+
if (v) bySurface[surface] = v;
|
|
424
|
+
}
|
|
425
|
+
const summary = formatIngestSummary(
|
|
426
|
+
{ recordCount: result.records.length, totalUSD: result.totalUSD, totalJPY: result.totalJPY, bySurface },
|
|
427
|
+
cfg
|
|
428
|
+
);
|
|
429
|
+
const record = {
|
|
430
|
+
schemaVersion: 1,
|
|
431
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
432
|
+
sessionId: "ingest-scan",
|
|
433
|
+
project: "",
|
|
434
|
+
gitBranch: null,
|
|
435
|
+
models: [],
|
|
436
|
+
tokens: emptyBuckets(),
|
|
437
|
+
sidechainTokens: null,
|
|
438
|
+
apiCalls: 0,
|
|
439
|
+
costUSD: result.totalUSD,
|
|
440
|
+
costJPY: result.totalJPY,
|
|
441
|
+
fxRate: result.fxRate,
|
|
442
|
+
fxSource: result.fxSource,
|
|
443
|
+
prompt: ""
|
|
444
|
+
};
|
|
445
|
+
await Promise.allSettled([
|
|
446
|
+
notifyOS(record, cfg, void 0, summary),
|
|
447
|
+
notifySlack(record, cfg, void 0, summary)
|
|
448
|
+
]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export {
|
|
452
|
+
runIngest,
|
|
453
|
+
notifyIngestSummary
|
|
454
|
+
};
|