ccc-notifier 0.3.0 → 0.5.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 +11 -7
- package/dist/{budget-KKTFYAXH.js → budget-TBWVHUKA.js} +1 -1
- package/dist/chunk-5PH7PPD6.js +1077 -0
- package/dist/{chunk-4RZ6OGTD.js → chunk-DDUK4EWQ.js} +1 -1
- package/dist/chunk-ENGUOLTD.js +92 -0
- package/dist/chunk-HLJAJS2W.js +55 -0
- package/dist/{chunk-LHKBGA5K.js → chunk-LVMKY6JB.js} +304 -29
- package/dist/chunk-O2AR4PXU.js +82 -0
- package/dist/chunk-O34L3NSI.js +172 -0
- package/dist/{chunk-QX5KIRSU.js → chunk-QWBQN4XY.js} +183 -54
- package/dist/{chunk-7KVOQ4UZ.js → chunk-S3XKO7MY.js} +235 -108
- package/dist/cli.js +382 -73
- package/dist/{dashboard-TH3NOVT3.js → dashboard-6F2QNUJT.js} +4 -2
- package/dist/history-7KN26LJH.js +162 -0
- package/dist/{mute-EPLYH67P.js → mute-MPX3K7QN.js} +2 -2
- package/dist/{setup-PTO5N46B.js → setup-IXPMVWOE.js} +2 -2
- package/dist/subagent-store-A6MIN22X.js +31 -0
- package/dist/{sweep-25EZAOGC.js → sweep-UIONM4UA.js} +143 -126
- package/dist/track-KEZON6KI.js +245 -0
- package/package.json +1 -1
- package/dist/chunk-DSV75EF7.js +0 -357
- package/dist/chunk-ECADO26T.js +0 -316
- package/dist/history-DLFYQJFM.js +0 -114
- package/dist/track-PXFSGMGL.js +0 -184
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/codex/subagent-store.ts
|
|
4
|
+
import {
|
|
5
|
+
existsSync as existsSync2,
|
|
6
|
+
linkSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync as mkdirSync2,
|
|
9
|
+
readFileSync as readFileSync2,
|
|
10
|
+
readdirSync,
|
|
11
|
+
renameSync as renameSync2,
|
|
12
|
+
rmSync as rmSync2,
|
|
13
|
+
statSync as statSync2,
|
|
14
|
+
writeFileSync as writeFileSync2
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
17
|
+
import { basename, dirname, join as join2 } from "path";
|
|
18
|
+
import { hostname } from "os";
|
|
19
|
+
|
|
20
|
+
// src/store.ts
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
appendFileSync,
|
|
27
|
+
renameSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
statSync
|
|
30
|
+
} from "fs";
|
|
31
|
+
import { join } from "path";
|
|
32
|
+
import { homedir } from "os";
|
|
33
|
+
|
|
34
|
+
// src/types.ts
|
|
35
|
+
var DEFAULT_CONFIG = {
|
|
36
|
+
notify: { os: true, slack: null },
|
|
37
|
+
minNotifyUSD: 0,
|
|
38
|
+
costLabel: "api_equivalent",
|
|
39
|
+
fx: { fallbackRate: 150, cacheHours: 12 },
|
|
40
|
+
includeDailyTotal: true,
|
|
41
|
+
monthlyBudgetUSD: 0,
|
|
42
|
+
dashboard: { autoRegenerate: true, autoReloadSec: 30, days: 30 }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/store.ts
|
|
46
|
+
var ERROR_LOG_MAX_BYTES = 1024 * 1024;
|
|
47
|
+
function dataHomePath() {
|
|
48
|
+
return process.env.CCCN_HOME || join(homedir(), ".ccc-notifier");
|
|
49
|
+
}
|
|
50
|
+
function configFilePath() {
|
|
51
|
+
return join(dataHomePath(), "config.json");
|
|
52
|
+
}
|
|
53
|
+
function paths() {
|
|
54
|
+
const home = dataHomePath();
|
|
55
|
+
const cacheDir = join(home, "cache");
|
|
56
|
+
mkdirSync(home, { recursive: true });
|
|
57
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
58
|
+
return {
|
|
59
|
+
home,
|
|
60
|
+
configFile: configFilePath(),
|
|
61
|
+
historyFile: join(home, "history.jsonl"),
|
|
62
|
+
cursorsFile: join(home, "cursors.json"),
|
|
63
|
+
cacheDir,
|
|
64
|
+
errorLog: join(home, "error.log"),
|
|
65
|
+
lastNotifyFile: join(home, "last-notify.json"),
|
|
66
|
+
muteFile: join(home, "muted.json"),
|
|
67
|
+
recentDashboardFile: join(home, "report.html"),
|
|
68
|
+
fullDashboardFile: join(home, "report-all.html"),
|
|
69
|
+
dashboardFullStateFile: join(cacheDir, "dashboard-full-state.json"),
|
|
70
|
+
dataLockDir: join(cacheDir, "data.lock"),
|
|
71
|
+
dataReclaimDir: join(cacheDir, "data.lock.reclaim")
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function isPlainObject(v) {
|
|
75
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
76
|
+
}
|
|
77
|
+
function mergeConfig(partial) {
|
|
78
|
+
const result = structuredClone(DEFAULT_CONFIG);
|
|
79
|
+
if (!isPlainObject(partial)) return result;
|
|
80
|
+
if (isPlainObject(partial.notify)) {
|
|
81
|
+
if ("os" in partial.notify) {
|
|
82
|
+
result.notify.os = partial.notify.os;
|
|
83
|
+
}
|
|
84
|
+
if ("slack" in partial.notify) {
|
|
85
|
+
result.notify.slack = partial.notify.slack;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if ("minNotifyUSD" in partial) {
|
|
89
|
+
result.minNotifyUSD = partial.minNotifyUSD;
|
|
90
|
+
}
|
|
91
|
+
if ("costLabel" in partial) {
|
|
92
|
+
result.costLabel = partial.costLabel;
|
|
93
|
+
}
|
|
94
|
+
if (isPlainObject(partial.fx)) {
|
|
95
|
+
if ("fallbackRate" in partial.fx) {
|
|
96
|
+
result.fx.fallbackRate = partial.fx.fallbackRate;
|
|
97
|
+
}
|
|
98
|
+
if ("cacheHours" in partial.fx) {
|
|
99
|
+
result.fx.cacheHours = partial.fx.cacheHours;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if ("includeDailyTotal" in partial) {
|
|
103
|
+
result.includeDailyTotal = partial.includeDailyTotal;
|
|
104
|
+
}
|
|
105
|
+
if ("monthlyBudgetUSD" in partial) {
|
|
106
|
+
const b = partial.monthlyBudgetUSD;
|
|
107
|
+
if (typeof b === "number" && Number.isFinite(b) && b >= 0) {
|
|
108
|
+
result.monthlyBudgetUSD = b;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (isPlainObject(partial.dashboard)) {
|
|
112
|
+
if ("autoRegenerate" in partial.dashboard) {
|
|
113
|
+
result.dashboard.autoRegenerate = partial.dashboard.autoRegenerate;
|
|
114
|
+
}
|
|
115
|
+
if ("autoReloadSec" in partial.dashboard) {
|
|
116
|
+
result.dashboard.autoReloadSec = partial.dashboard.autoReloadSec;
|
|
117
|
+
}
|
|
118
|
+
if ("days" in partial.dashboard) {
|
|
119
|
+
const days = partial.dashboard.days;
|
|
120
|
+
if (typeof days === "number" && Number.isFinite(days) && Number.isInteger(days) && days > 0) {
|
|
121
|
+
result.dashboard.days = days;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function readConfig() {
|
|
128
|
+
const p = paths();
|
|
129
|
+
if (!existsSync(p.configFile)) {
|
|
130
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
131
|
+
}
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = readFileSync(p.configFile, "utf8");
|
|
135
|
+
} catch (err) {
|
|
136
|
+
logError("readConfig", err);
|
|
137
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
138
|
+
}
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(raw);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logError("readConfig", err);
|
|
144
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
145
|
+
}
|
|
146
|
+
return mergeConfig(parsed);
|
|
147
|
+
}
|
|
148
|
+
function readMuteState() {
|
|
149
|
+
const p = paths();
|
|
150
|
+
if (!existsSync(p.muteFile)) return null;
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(readFileSync(p.muteFile, "utf8"));
|
|
153
|
+
if (!isPlainObject(parsed) || !("until" in parsed)) return null;
|
|
154
|
+
const until = parsed.until;
|
|
155
|
+
if (until === null) return { until: null };
|
|
156
|
+
if (typeof until === "string" && !Number.isNaN(new Date(until).getTime())) {
|
|
157
|
+
return { until };
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logError("readMuteState", err);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isMuted(now = /* @__PURE__ */ new Date()) {
|
|
166
|
+
const state = readMuteState();
|
|
167
|
+
if (state === null) return false;
|
|
168
|
+
if (state.until === null) return true;
|
|
169
|
+
return new Date(state.until).getTime() > now.getTime();
|
|
170
|
+
}
|
|
171
|
+
function writeMuteState(state) {
|
|
172
|
+
writeFileSync(paths().muteFile, `${JSON.stringify(state)}
|
|
173
|
+
`, "utf8");
|
|
174
|
+
}
|
|
175
|
+
function clearMuteState() {
|
|
176
|
+
rmSync(paths().muteFile, { force: true });
|
|
177
|
+
}
|
|
178
|
+
function loadCursor(transcriptPath) {
|
|
179
|
+
const p = paths();
|
|
180
|
+
if (!existsSync(p.cursorsFile)) return null;
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = readFileSync(p.cursorsFile, "utf8");
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logError("loadCursor", err);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
let parsed;
|
|
189
|
+
try {
|
|
190
|
+
parsed = JSON.parse(raw);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logError("loadCursor", err);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (!isPlainObject(parsed)) {
|
|
196
|
+
logError("loadCursor", new Error("cursors.json root is not an object"));
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const cursor = parsed[transcriptPath];
|
|
200
|
+
return cursor ?? null;
|
|
201
|
+
}
|
|
202
|
+
function sanitizeCursor(raw) {
|
|
203
|
+
if (!isPlainObject(raw)) return null;
|
|
204
|
+
const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals } = raw;
|
|
205
|
+
if (typeof offset !== "number" || !Number.isFinite(offset)) return null;
|
|
206
|
+
if (lastUuid !== null && typeof lastUuid !== "string") return null;
|
|
207
|
+
if (lastTs !== null && typeof lastTs !== "string") return null;
|
|
208
|
+
if (!Array.isArray(seenMessageKeys)) return null;
|
|
209
|
+
const keys = [];
|
|
210
|
+
for (const key of seenMessageKeys) {
|
|
211
|
+
if (typeof key !== "string") return null;
|
|
212
|
+
keys.push(key);
|
|
213
|
+
}
|
|
214
|
+
const cursor = { offset, lastUuid, lastTs, seenMessageKeys: keys };
|
|
215
|
+
if (isPlainObject(codexTotals)) {
|
|
216
|
+
const { input, cached, output } = codexTotals;
|
|
217
|
+
if (typeof input === "number" && Number.isFinite(input) && input >= 0 && typeof cached === "number" && Number.isFinite(cached) && cached >= 0 && typeof output === "number" && Number.isFinite(output) && output >= 0) {
|
|
218
|
+
cursor.codexTotals = { input, cached, output };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return cursor;
|
|
222
|
+
}
|
|
223
|
+
function saveCursor(transcriptPath, c) {
|
|
224
|
+
const p = paths();
|
|
225
|
+
let dict = {};
|
|
226
|
+
if (existsSync(p.cursorsFile)) {
|
|
227
|
+
try {
|
|
228
|
+
const raw = readFileSync(p.cursorsFile, "utf8");
|
|
229
|
+
const parsed = JSON.parse(raw);
|
|
230
|
+
if (isPlainObject(parsed)) {
|
|
231
|
+
dict = parsed;
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logError("saveCursor", err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
dict[transcriptPath] = c;
|
|
238
|
+
const tmpFile = `${p.cursorsFile}.tmp`;
|
|
239
|
+
writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
|
|
240
|
+
renameSync(tmpFile, p.cursorsFile);
|
|
241
|
+
}
|
|
242
|
+
function appendTurn(record) {
|
|
243
|
+
const p = paths();
|
|
244
|
+
const { subagentActivity: _runtimeActivity, ...persisted } = record;
|
|
245
|
+
appendFileSync(p.historyFile, JSON.stringify(persisted) + "\n", "utf8");
|
|
246
|
+
}
|
|
247
|
+
function readTurns(days) {
|
|
248
|
+
const p = paths();
|
|
249
|
+
if (!existsSync(p.historyFile)) return [];
|
|
250
|
+
let raw;
|
|
251
|
+
try {
|
|
252
|
+
raw = readFileSync(p.historyFile, "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
const cutoff = typeof days === "number" ? Date.now() - days * 864e5 : null;
|
|
257
|
+
const result = [];
|
|
258
|
+
for (const line of raw.split("\n")) {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
if (!trimmed) continue;
|
|
261
|
+
let rec;
|
|
262
|
+
try {
|
|
263
|
+
rec = JSON.parse(trimmed);
|
|
264
|
+
} catch {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (cutoff !== null) {
|
|
268
|
+
const ts = Date.parse(rec.ts);
|
|
269
|
+
if (!Number.isFinite(ts) || ts < cutoff) continue;
|
|
270
|
+
}
|
|
271
|
+
delete rec.subagentActivity;
|
|
272
|
+
result.push(rec);
|
|
273
|
+
}
|
|
274
|
+
const keyedRecords = result.filter(
|
|
275
|
+
(rec) => rec.source === "codex" && typeof rec.activityProjectionKey === "string" && /^[a-f0-9]{64}$/.test(rec.activityProjectionKey)
|
|
276
|
+
);
|
|
277
|
+
if (keyedRecords.length === 0) return result;
|
|
278
|
+
const byProjection = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const state of readCodexSubagentActivity()) {
|
|
280
|
+
const states = byProjection.get(state.projectionKey) ?? [];
|
|
281
|
+
states.push(state);
|
|
282
|
+
byProjection.set(state.projectionKey, states);
|
|
283
|
+
}
|
|
284
|
+
for (const rec of keyedRecords) {
|
|
285
|
+
const activity = projectCodexSubagentActivity(byProjection.get(rec.activityProjectionKey) ?? []);
|
|
286
|
+
if (activity !== void 0) rec.subagentActivity = activity;
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function todayTotalUSD() {
|
|
291
|
+
const now = /* @__PURE__ */ new Date();
|
|
292
|
+
const y = now.getFullYear();
|
|
293
|
+
const m = now.getMonth();
|
|
294
|
+
const d = now.getDate();
|
|
295
|
+
let total = 0;
|
|
296
|
+
for (const rec of readTurns()) {
|
|
297
|
+
const ts = new Date(rec.ts);
|
|
298
|
+
if (Number.isNaN(ts.getTime())) continue;
|
|
299
|
+
if (ts.getFullYear() === y && ts.getMonth() === m && ts.getDate() === d) {
|
|
300
|
+
total += rec.costUSD;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return total;
|
|
304
|
+
}
|
|
305
|
+
function currentMonthTotals() {
|
|
306
|
+
const now = /* @__PURE__ */ new Date();
|
|
307
|
+
const y = now.getFullYear();
|
|
308
|
+
const m = now.getMonth();
|
|
309
|
+
let usd = 0;
|
|
310
|
+
let jpy = 0;
|
|
311
|
+
let turns = 0;
|
|
312
|
+
for (const rec of readTurns()) {
|
|
313
|
+
const ts = new Date(rec.ts);
|
|
314
|
+
if (Number.isNaN(ts.getTime())) continue;
|
|
315
|
+
if (ts.getFullYear() === y && ts.getMonth() === m) {
|
|
316
|
+
const sa = rec.subagents?.costUSD ?? 0;
|
|
317
|
+
usd += rec.costUSD + sa;
|
|
318
|
+
jpy += rec.costJPY + sa * rec.fxRate;
|
|
319
|
+
turns += 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { usd, jpy, turns };
|
|
323
|
+
}
|
|
324
|
+
function logError(context, err) {
|
|
325
|
+
try {
|
|
326
|
+
const p = paths();
|
|
327
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
330
|
+
let entry = `[${iso}] [${context}] ${message}
|
|
331
|
+
`;
|
|
332
|
+
if (stack) {
|
|
333
|
+
entry += `${stack}
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const stat = statSync(p.errorLog);
|
|
338
|
+
if (stat.size > ERROR_LOG_MAX_BYTES) {
|
|
339
|
+
renameSync(p.errorLog, `${p.errorLog}.old`);
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
appendFileSync(p.errorLog, entry, "utf8");
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/codex/subagent-store.ts
|
|
349
|
+
var KNOWN_AGENT_TYPES = /* @__PURE__ */ new Map([
|
|
350
|
+
["default", "default"],
|
|
351
|
+
["explorer", "explorer"],
|
|
352
|
+
["worker", "worker"],
|
|
353
|
+
["reviewer", "reviewer"],
|
|
354
|
+
["researcher", "researcher"],
|
|
355
|
+
["general", "general"]
|
|
356
|
+
]);
|
|
357
|
+
var UNKNOWN_AGENT_TYPE = "unknown";
|
|
358
|
+
var MALFORMED_LOCK_GRACE_MS = 100;
|
|
359
|
+
var CRASH_STAGING_GRACE_MS = 6e4;
|
|
360
|
+
var KEY_CHECK_CONTEXT = "ccc-notifier:codex-subagent-key-check:v1";
|
|
361
|
+
var SAFE_AGENT_TYPES = /* @__PURE__ */ new Set([...KNOWN_AGENT_TYPES.values(), UNKNOWN_AGENT_TYPE]);
|
|
362
|
+
function isObject(value) {
|
|
363
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
364
|
+
}
|
|
365
|
+
function hasOnlyKeys(value, allowed) {
|
|
366
|
+
const allowlist = new Set(allowed);
|
|
367
|
+
return Object.keys(value).every((key) => allowlist.has(key));
|
|
368
|
+
}
|
|
369
|
+
function activityPaths() {
|
|
370
|
+
const home = paths().home;
|
|
371
|
+
return {
|
|
372
|
+
ledger: join2(home, "codex-subagent-activity.json"),
|
|
373
|
+
salt: join2(home, "codex-subagent-key"),
|
|
374
|
+
lock: join2(home, "codex-subagent-activity.lock"),
|
|
375
|
+
keyLock: join2(home, "codex-subagent-key.lock"),
|
|
376
|
+
legacyBackup: join2(home, "codex-subagent-activity.v1.json")
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function loadOrCreateSecret() {
|
|
380
|
+
const { salt: file, keyLock, ledger } = activityPaths();
|
|
381
|
+
const lock = acquireOwnedLock(keyLock, 2e3);
|
|
382
|
+
try {
|
|
383
|
+
cleanupStaleFileStaging(file, 16);
|
|
384
|
+
if (existsSync2(file)) {
|
|
385
|
+
const value2 = readFileSync2(file);
|
|
386
|
+
if (value2.length === 32) return value2;
|
|
387
|
+
if (existsSync2(ledger)) throw new Error("invalid activity key with existing ledger");
|
|
388
|
+
}
|
|
389
|
+
if (!existsSync2(file) && existsSync2(ledger)) {
|
|
390
|
+
throw new Error("missing activity key with existing ledger");
|
|
391
|
+
}
|
|
392
|
+
const value = randomBytes(32);
|
|
393
|
+
const staging = `${file}.${stagingHostTag()}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
394
|
+
try {
|
|
395
|
+
writeFileSync2(staging, value, { mode: 384 });
|
|
396
|
+
renameSync2(staging, file);
|
|
397
|
+
} finally {
|
|
398
|
+
rmSync2(staging, { force: true });
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
} finally {
|
|
402
|
+
lock.release();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function keyedV2(secret, domain, ...values) {
|
|
406
|
+
const hmac = createHmac("sha256", secret);
|
|
407
|
+
hmac.update(`ccc-notifier:${domain}\0`);
|
|
408
|
+
for (const value of values) {
|
|
409
|
+
hmac.update(String(Buffer.byteLength(value)));
|
|
410
|
+
hmac.update(":");
|
|
411
|
+
hmac.update(value);
|
|
412
|
+
hmac.update("\0");
|
|
413
|
+
}
|
|
414
|
+
return hmac.digest("hex");
|
|
415
|
+
}
|
|
416
|
+
function computeKeyCheck(secret) {
|
|
417
|
+
return createHmac("sha256", secret).update(KEY_CHECK_CONTEXT).digest("hex");
|
|
418
|
+
}
|
|
419
|
+
function keyCheckMatches(secret, persisted) {
|
|
420
|
+
if (!/^[a-f0-9]{64}$/.test(persisted)) return false;
|
|
421
|
+
const actual = Buffer.from(computeKeyCheck(secret), "hex");
|
|
422
|
+
const expected = Buffer.from(persisted, "hex");
|
|
423
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
424
|
+
}
|
|
425
|
+
function normalizeAgentType(value) {
|
|
426
|
+
if (typeof value !== "string") return UNKNOWN_AGENT_TYPE;
|
|
427
|
+
const cleaned = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim().slice(0, 64).toLowerCase();
|
|
428
|
+
return KNOWN_AGENT_TYPES.get(cleaned) ?? UNKNOWN_AGENT_TYPE;
|
|
429
|
+
}
|
|
430
|
+
function validateCodexSubagentPayload(payload, expectedKind) {
|
|
431
|
+
const raw = validateRawCodexSubagentPayload(payload, expectedKind);
|
|
432
|
+
if (raw === null) return null;
|
|
433
|
+
return deriveCodexSubagentEvent(loadVerifiedSecret(), raw);
|
|
434
|
+
}
|
|
435
|
+
function validateRawCodexSubagentPayload(payload, expectedKind) {
|
|
436
|
+
if (!isObject(payload)) return null;
|
|
437
|
+
const eventName = payload.hook_event_name;
|
|
438
|
+
const kind = eventName === "SubagentStart" ? "start" : eventName === "SubagentStop" ? "stop" : null;
|
|
439
|
+
if (kind === null || expectedKind !== void 0 && kind !== expectedKind) return null;
|
|
440
|
+
const { session_id: sessionId, turn_id: turnId, agent_id: agentId } = payload;
|
|
441
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 1024 || typeof turnId !== "string" || turnId.length === 0 || turnId.length > 1024 || typeof agentId !== "string" || agentId.length === 0 || agentId.length > 1024) return null;
|
|
442
|
+
return {
|
|
443
|
+
kind,
|
|
444
|
+
sessionId,
|
|
445
|
+
agentId,
|
|
446
|
+
agentTypeLabel: normalizeAgentType(payload.agent_type)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function deriveCodexSubagentEvent(secret, raw) {
|
|
450
|
+
return {
|
|
451
|
+
kind: raw.kind,
|
|
452
|
+
sessionKey: keyedV2(secret, "root-session-v2", raw.sessionId),
|
|
453
|
+
agentKey: keyedV2(secret, "agent-identity-v2", raw.sessionId, raw.agentId),
|
|
454
|
+
agentTypeLabel: raw.agentTypeLabel
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function validateCodexRootPayload(payload, expectedEvent) {
|
|
458
|
+
if (!isObject(payload)) return null;
|
|
459
|
+
if (payload.hook_event_name !== expectedEvent) return null;
|
|
460
|
+
const { session_id: sessionId, turn_id: turnId } = payload;
|
|
461
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 1024 || typeof turnId !== "string" || turnId.length === 0 || turnId.length > 1024) return null;
|
|
462
|
+
return { sessionId, turnId };
|
|
463
|
+
}
|
|
464
|
+
function deriveCodexRootEvent(secret, raw) {
|
|
465
|
+
return {
|
|
466
|
+
sessionKey: keyedV2(secret, "root-session-v2", raw.sessionId),
|
|
467
|
+
rootKey: keyedV2(secret, "root-turn-v2", raw.sessionId, raw.turnId)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function isoMin(a, b) {
|
|
471
|
+
return a <= b ? a : b;
|
|
472
|
+
}
|
|
473
|
+
function isoMax(a, b) {
|
|
474
|
+
return a >= b ? a : b;
|
|
475
|
+
}
|
|
476
|
+
function reduceCodexSubagentActivity(previous, event, observedAt, projectionKey = previous?.projectionKey) {
|
|
477
|
+
if (projectionKey === void 0) throw new Error("missing root assignment");
|
|
478
|
+
const incomingLabel = event.agentTypeLabel;
|
|
479
|
+
const label = previous === void 0 ? incomingLabel : previous.agentTypeLabel === UNKNOWN_AGENT_TYPE ? incomingLabel : incomingLabel === UNKNOWN_AGENT_TYPE ? previous.agentTypeLabel : [previous.agentTypeLabel, incomingLabel].sort()[0];
|
|
480
|
+
return {
|
|
481
|
+
schemaVersion: 1,
|
|
482
|
+
projectionKey,
|
|
483
|
+
agentKey: event.agentKey,
|
|
484
|
+
agentTypeLabel: label,
|
|
485
|
+
startObserved: (previous?.startObserved ?? false) || event.kind === "start",
|
|
486
|
+
stopObserved: (previous?.stopObserved ?? false) || event.kind === "stop",
|
|
487
|
+
firstObservedAt: previous ? isoMin(previous.firstObservedAt, observedAt) : observedAt,
|
|
488
|
+
lastObservedAt: previous ? isoMax(previous.lastObservedAt, observedAt) : observedAt
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function sleep(ms) {
|
|
492
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
493
|
+
}
|
|
494
|
+
function readLockOwner(lock) {
|
|
495
|
+
try {
|
|
496
|
+
const ownerPath = statSync2(lock).isDirectory() ? join2(lock, "owner.json") : lock;
|
|
497
|
+
const value = JSON.parse(readFileSync2(ownerPath, "utf8"));
|
|
498
|
+
if (!isObject(value) || typeof value.token !== "string" || !/^[a-f0-9]{32}$/.test(value.token) || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.hostname !== "string" || typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt))) return null;
|
|
499
|
+
return value;
|
|
500
|
+
} catch {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function processLiveness(pid) {
|
|
505
|
+
try {
|
|
506
|
+
process.kill(pid, 0);
|
|
507
|
+
return "alive";
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (!isObject(error)) return "unknown";
|
|
510
|
+
if (error.code === "EPERM") return "alive";
|
|
511
|
+
if (error.code === "ESRCH") return "dead";
|
|
512
|
+
return "unknown";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function escapeRegExp(value) {
|
|
516
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
517
|
+
}
|
|
518
|
+
function stagingHostTag() {
|
|
519
|
+
return createHash("sha256").update("ccc-notifier:staging-host:v1\0").update(hostname()).digest("hex").slice(0, 16);
|
|
520
|
+
}
|
|
521
|
+
function cleanupStaleFileStaging(file, randomHexLength) {
|
|
522
|
+
const parent = dirname(file);
|
|
523
|
+
const pattern = new RegExp(
|
|
524
|
+
`^${escapeRegExp(basename(file))}\\.([a-f0-9]{16})\\.(\\d+)\\.[a-f0-9]{${randomHexLength}}\\.tmp$`
|
|
525
|
+
);
|
|
526
|
+
const localHostTag = stagingHostTag();
|
|
527
|
+
let names;
|
|
528
|
+
try {
|
|
529
|
+
names = readdirSync(parent);
|
|
530
|
+
} catch {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const name of names) {
|
|
534
|
+
const match = pattern.exec(name);
|
|
535
|
+
if (match === null) continue;
|
|
536
|
+
if (match[1] !== localHostTag) continue;
|
|
537
|
+
const pid = Number(match[2]);
|
|
538
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || processLiveness(pid) !== "dead") continue;
|
|
539
|
+
const candidate = join2(parent, name);
|
|
540
|
+
try {
|
|
541
|
+
const stat = lstatSync(candidate);
|
|
542
|
+
if (!stat.isFile() || Date.now() - stat.mtimeMs < CRASH_STAGING_GRACE_MS) continue;
|
|
543
|
+
rmSync2(candidate);
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function releaseOwnedLock(lock, token) {
|
|
549
|
+
const owner = readLockOwner(lock);
|
|
550
|
+
if (owner?.token !== token) return;
|
|
551
|
+
const releasing = `${lock}.release-${token}`;
|
|
552
|
+
try {
|
|
553
|
+
renameSync2(lock, releasing);
|
|
554
|
+
} catch {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const movedOwner = readLockOwner(releasing);
|
|
558
|
+
if (movedOwner?.token === token) rmSync2(releasing, { recursive: true, force: true });
|
|
559
|
+
}
|
|
560
|
+
function claimAndRemove(lock) {
|
|
561
|
+
const recovery = `${lock}.recovery-${randomBytes(8).toString("hex")}`;
|
|
562
|
+
try {
|
|
563
|
+
renameSync2(lock, recovery);
|
|
564
|
+
} catch {
|
|
565
|
+
return !existsSync2(lock);
|
|
566
|
+
}
|
|
567
|
+
rmSync2(recovery, { recursive: true, force: true });
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
function tryRecoverExistingLock(lock, malformedGraceMs) {
|
|
571
|
+
let stat;
|
|
572
|
+
try {
|
|
573
|
+
stat = statSync2(lock);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
return isObject(error) && error.code === "ENOENT";
|
|
576
|
+
}
|
|
577
|
+
const owner = readLockOwner(lock);
|
|
578
|
+
if (owner !== null) {
|
|
579
|
+
if (owner.hostname !== hostname()) return false;
|
|
580
|
+
if (processLiveness(owner.pid) !== "dead") return false;
|
|
581
|
+
return claimAndRemove(lock);
|
|
582
|
+
}
|
|
583
|
+
if (!stat.isDirectory()) return false;
|
|
584
|
+
if (Date.now() - stat.mtimeMs < malformedGraceMs) return false;
|
|
585
|
+
const rechecked = readLockOwner(lock);
|
|
586
|
+
if (rechecked !== null) {
|
|
587
|
+
if (rechecked.hostname !== hostname() || processLiveness(rechecked.pid) !== "dead") return false;
|
|
588
|
+
}
|
|
589
|
+
return claimAndRemove(lock);
|
|
590
|
+
}
|
|
591
|
+
function publishOwnedLock(lock, owner) {
|
|
592
|
+
const staging = `${lock}.init-${owner.token}`;
|
|
593
|
+
const ownerTmp = join2(staging, "owner.json.tmp");
|
|
594
|
+
try {
|
|
595
|
+
mkdirSync2(staging);
|
|
596
|
+
writeFileSync2(ownerTmp, `${JSON.stringify(owner)}
|
|
597
|
+
`, { encoding: "utf8", mode: 384 });
|
|
598
|
+
const ownerPath = join2(staging, "owner.json");
|
|
599
|
+
renameSync2(ownerTmp, ownerPath);
|
|
600
|
+
try {
|
|
601
|
+
linkSync(ownerPath, lock);
|
|
602
|
+
return true;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
const errorCode = isObject(error) && typeof error.code === "string" ? error.code : null;
|
|
605
|
+
if (errorCode === "EEXIST" || existsSync2(lock)) return false;
|
|
606
|
+
const code = errorCode !== null && /^[A-Z0-9_]+$/.test(errorCode) ? ` (${errorCode})` : "";
|
|
607
|
+
throw new Error(`activity lock publication failed${code}`);
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function cleanupStaleLockInitializations(lock) {
|
|
614
|
+
const parent = dirname(lock);
|
|
615
|
+
const pattern = new RegExp(`^${escapeRegExp(basename(lock))}\\.init-([a-f0-9]{32})$`);
|
|
616
|
+
let names;
|
|
617
|
+
try {
|
|
618
|
+
names = readdirSync(parent);
|
|
619
|
+
} catch {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
for (const name of names) {
|
|
623
|
+
const match = pattern.exec(name);
|
|
624
|
+
if (match === null) continue;
|
|
625
|
+
const candidate = join2(parent, name);
|
|
626
|
+
try {
|
|
627
|
+
const stat = lstatSync(candidate);
|
|
628
|
+
const ownerStat = lstatSync(join2(candidate, "owner.json"));
|
|
629
|
+
const owner = readLockOwner(candidate);
|
|
630
|
+
if (!stat.isDirectory() || Date.now() - stat.mtimeMs < CRASH_STAGING_GRACE_MS || !ownerStat.isFile() || Date.now() - ownerStat.mtimeMs < CRASH_STAGING_GRACE_MS || owner === null || owner.token !== match[1] || owner.hostname !== hostname() || Date.now() - Date.parse(owner.createdAt) < CRASH_STAGING_GRACE_MS || processLiveness(owner.pid) !== "dead") continue;
|
|
631
|
+
const recovery = `${candidate}.recovery-${randomBytes(8).toString("hex")}`;
|
|
632
|
+
try {
|
|
633
|
+
renameSync2(candidate, recovery);
|
|
634
|
+
} catch {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const movedOwner = readLockOwner(recovery);
|
|
638
|
+
const movedStat = lstatSync(recovery);
|
|
639
|
+
const movedOwnerStat = lstatSync(join2(recovery, "owner.json"));
|
|
640
|
+
if (movedStat.isDirectory() && Date.now() - movedStat.mtimeMs >= CRASH_STAGING_GRACE_MS && movedOwnerStat.isFile() && Date.now() - movedOwnerStat.mtimeMs >= CRASH_STAGING_GRACE_MS && movedOwner?.token === owner.token && movedOwner.hostname === owner.hostname && movedOwner.pid === owner.pid && Date.now() - Date.parse(movedOwner.createdAt) >= CRASH_STAGING_GRACE_MS && processLiveness(movedOwner.pid) === "dead") {
|
|
641
|
+
rmSync2(recovery, { recursive: true, force: true });
|
|
642
|
+
} else if (!existsSync2(candidate)) {
|
|
643
|
+
try {
|
|
644
|
+
renameSync2(recovery, candidate);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function acquireOwnedLock(lock, timeoutMs, malformedGraceMs = MALFORMED_LOCK_GRACE_MS) {
|
|
653
|
+
const deadline = Date.now() + timeoutMs;
|
|
654
|
+
const token = randomBytes(16).toString("hex");
|
|
655
|
+
for (; ; ) {
|
|
656
|
+
if (Date.now() >= deadline) throw new Error("activity lock timeout");
|
|
657
|
+
const owner = { token, pid: process.pid, hostname: hostname(), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
658
|
+
if (publishOwnedLock(lock, owner)) {
|
|
659
|
+
cleanupStaleLockInitializations(lock);
|
|
660
|
+
return { token, release: () => releaseOwnedLock(lock, token) };
|
|
661
|
+
}
|
|
662
|
+
if (tryRecoverExistingLock(lock, malformedGraceMs)) continue;
|
|
663
|
+
if (Date.now() >= deadline) throw new Error("activity lock timeout");
|
|
664
|
+
sleep(10);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function acquireCodexActivityLock(timeoutMs = 2e3, options = {}) {
|
|
668
|
+
return acquireOwnedLock(activityPaths().lock, timeoutMs, options.staleMs ?? MALFORMED_LOCK_GRACE_MS);
|
|
669
|
+
}
|
|
670
|
+
function validState(value) {
|
|
671
|
+
return isObject(value) && hasOnlyKeys(value, [
|
|
672
|
+
"schemaVersion",
|
|
673
|
+
"projectionKey",
|
|
674
|
+
"agentKey",
|
|
675
|
+
"agentTypeLabel",
|
|
676
|
+
"startObserved",
|
|
677
|
+
"stopObserved",
|
|
678
|
+
"firstObservedAt",
|
|
679
|
+
"lastObservedAt"
|
|
680
|
+
]) && value.schemaVersion === 1 && typeof value.projectionKey === "string" && /^[a-f0-9]{64}$/.test(value.projectionKey) && typeof value.agentKey === "string" && /^[a-f0-9]{64}$/.test(value.agentKey) && typeof value.agentTypeLabel === "string" && SAFE_AGENT_TYPES.has(value.agentTypeLabel) && typeof value.startObserved === "boolean" && typeof value.stopObserved === "boolean" && typeof value.firstObservedAt === "string" && Number.isFinite(Date.parse(value.firstObservedAt)) && typeof value.lastObservedAt === "string" && Number.isFinite(Date.parse(value.lastObservedAt)) && value.firstObservedAt <= value.lastObservedAt;
|
|
681
|
+
}
|
|
682
|
+
function isHexKey(value) {
|
|
683
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
684
|
+
}
|
|
685
|
+
function validateV1(value) {
|
|
686
|
+
if (!isObject(value) || !hasOnlyKeys(value, ["schemaVersion", "keyCheck", "agents"]) || value.schemaVersion !== 1 || !isObject(value.agents)) {
|
|
687
|
+
throw new Error("invalid activity ledger");
|
|
688
|
+
}
|
|
689
|
+
if (value.keyCheck !== void 0 && !isHexKey(value.keyCheck)) {
|
|
690
|
+
throw new Error("invalid activity ledger keyCheck");
|
|
691
|
+
}
|
|
692
|
+
for (const [key, state] of Object.entries(value.agents)) {
|
|
693
|
+
if (!validState(state) || state.agentKey !== key) throw new Error("invalid activity ledger entry");
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
function validSessionState(value) {
|
|
698
|
+
return isObject(value) && hasOnlyKeys(value, ["activeRootKey", "latestRootKey"]) && (value.activeRootKey === null || isHexKey(value.activeRootKey)) && (value.latestRootKey === null || isHexKey(value.latestRootKey));
|
|
699
|
+
}
|
|
700
|
+
function validRootState(value) {
|
|
701
|
+
if (!isObject(value) || !hasOnlyKeys(value, [
|
|
702
|
+
"sessionKey",
|
|
703
|
+
"status",
|
|
704
|
+
"openedAt",
|
|
705
|
+
"openedSequence",
|
|
706
|
+
"closedAt",
|
|
707
|
+
"closedSequence",
|
|
708
|
+
"agents"
|
|
709
|
+
]) || !isHexKey(value.sessionKey) || value.status !== "open" && value.status !== "closed" && value.status !== "abandoned" || typeof value.openedAt !== "string" || !Number.isFinite(Date.parse(value.openedAt)) || typeof value.openedSequence !== "number" || !Number.isSafeInteger(value.openedSequence) || value.openedSequence < 1 || !isObject(value.agents)) return false;
|
|
710
|
+
if (value.closedAt !== void 0 && (typeof value.closedAt !== "string" || !Number.isFinite(Date.parse(value.closedAt)))) return false;
|
|
711
|
+
if (value.closedSequence !== void 0 && (typeof value.closedSequence !== "number" || !Number.isSafeInteger(value.closedSequence) || value.closedSequence < 1)) return false;
|
|
712
|
+
if (value.status === "closed" && (value.closedAt === void 0 || value.closedSequence === void 0)) return false;
|
|
713
|
+
if (value.status !== "closed" && (value.closedAt !== void 0 || value.closedSequence !== void 0)) return false;
|
|
714
|
+
for (const [key, state] of Object.entries(value.agents)) {
|
|
715
|
+
if (!validState(state) || state.agentKey !== key) return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
function validateV2(value) {
|
|
720
|
+
if (!isObject(value) || !hasOnlyKeys(value, [
|
|
721
|
+
"schemaVersion",
|
|
722
|
+
"keyCheck",
|
|
723
|
+
"sequence",
|
|
724
|
+
"sessions",
|
|
725
|
+
"roots",
|
|
726
|
+
"agentAssignments",
|
|
727
|
+
"conflictedAgents",
|
|
728
|
+
"legacyV1"
|
|
729
|
+
]) || value.schemaVersion !== 2 || !isHexKey(value.keyCheck) || typeof value.sequence !== "number" || !Number.isSafeInteger(value.sequence) || value.sequence < 0 || !isObject(value.sessions) || !isObject(value.roots) || !isObject(value.agentAssignments) || !isObject(value.conflictedAgents)) throw new Error("invalid activity ledger v2");
|
|
730
|
+
for (const [key, session] of Object.entries(value.sessions)) {
|
|
731
|
+
if (!isHexKey(key) || !validSessionState(session)) throw new Error("invalid activity session");
|
|
732
|
+
}
|
|
733
|
+
for (const [key, root] of Object.entries(value.roots)) {
|
|
734
|
+
if (!isHexKey(key) || !validRootState(root)) throw new Error("invalid activity root");
|
|
735
|
+
}
|
|
736
|
+
for (const [agentKey, rootKey] of Object.entries(value.agentAssignments)) {
|
|
737
|
+
if (!isHexKey(agentKey) || !isHexKey(rootKey) || !(rootKey in value.roots)) {
|
|
738
|
+
throw new Error("invalid activity assignment");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
for (const [agentKey, marker] of Object.entries(value.conflictedAgents)) {
|
|
742
|
+
if (!isHexKey(agentKey) || marker !== true) throw new Error("invalid activity conflict");
|
|
743
|
+
}
|
|
744
|
+
if (value.legacyV1 !== void 0) {
|
|
745
|
+
if (!isObject(value.legacyV1) || !hasOnlyKeys(value.legacyV1, ["agents"]) || !isObject(value.legacyV1.agents)) {
|
|
746
|
+
throw new Error("invalid legacy activity ledger");
|
|
747
|
+
}
|
|
748
|
+
for (const [key, state] of Object.entries(value.legacyV1.agents)) {
|
|
749
|
+
if (!validState(state) || state.agentKey !== key) throw new Error("invalid legacy activity entry");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const ledger = value;
|
|
753
|
+
for (const [agentKey, rootKey] of Object.entries(ledger.agentAssignments)) {
|
|
754
|
+
if (ledger.roots[rootKey]?.agents[agentKey] === void 0) {
|
|
755
|
+
throw new Error("invalid activity assignment state");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const openCount = /* @__PURE__ */ new Map();
|
|
759
|
+
const rootedAgents = /* @__PURE__ */ new Set();
|
|
760
|
+
for (const [rootKey, root] of Object.entries(ledger.roots)) {
|
|
761
|
+
if (ledger.sessions[root.sessionKey] === void 0) throw new Error("missing activity session");
|
|
762
|
+
if (root.status === "open") openCount.set(root.sessionKey, (openCount.get(root.sessionKey) ?? 0) + 1);
|
|
763
|
+
for (const [agentKey, state] of Object.entries(root.agents)) {
|
|
764
|
+
if (state.projectionKey !== rootKey) throw new Error("activity projection root mismatch");
|
|
765
|
+
if (rootedAgents.has(agentKey)) throw new Error("activity agent appears in multiple roots");
|
|
766
|
+
rootedAgents.add(agentKey);
|
|
767
|
+
if (ledger.agentAssignments[agentKey] !== rootKey) throw new Error("missing reverse activity assignment");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const [sessionKey, session] of Object.entries(ledger.sessions)) {
|
|
771
|
+
if ((openCount.get(sessionKey) ?? 0) > 1) throw new Error("ambiguous open root state");
|
|
772
|
+
if (session.activeRootKey !== null) {
|
|
773
|
+
const root = ledger.roots[session.activeRootKey];
|
|
774
|
+
if (root === void 0 || root.sessionKey !== sessionKey || root.status !== "open") {
|
|
775
|
+
throw new Error("invalid active root pointer");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (session.latestRootKey !== null) {
|
|
779
|
+
const root = ledger.roots[session.latestRootKey];
|
|
780
|
+
if (root === void 0 || root.sessionKey !== sessionKey) throw new Error("invalid latest root pointer");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return ledger;
|
|
784
|
+
}
|
|
785
|
+
function atomicWriteLedger(ledger) {
|
|
786
|
+
const file = activityPaths().ledger;
|
|
787
|
+
const tmp = `${file}.${stagingHostTag()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
788
|
+
try {
|
|
789
|
+
writeFileSync2(tmp, `${JSON.stringify(ledger)}
|
|
790
|
+
`, { encoding: "utf8", mode: 384 });
|
|
791
|
+
renameSync2(tmp, file);
|
|
792
|
+
} finally {
|
|
793
|
+
rmSync2(tmp, { force: true });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function emptyV2(secret) {
|
|
797
|
+
return {
|
|
798
|
+
schemaVersion: 2,
|
|
799
|
+
keyCheck: computeKeyCheck(secret),
|
|
800
|
+
sequence: 0,
|
|
801
|
+
sessions: {},
|
|
802
|
+
roots: {},
|
|
803
|
+
agentAssignments: {},
|
|
804
|
+
conflictedAgents: {}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function writeLegacyBackup(raw) {
|
|
808
|
+
const file = activityPaths().legacyBackup;
|
|
809
|
+
if (existsSync2(file)) {
|
|
810
|
+
if (readFileSync2(file, "utf8") !== raw) throw new Error("activity v1 backup mismatch");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const tmp = `${file}.${stagingHostTag()}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
814
|
+
try {
|
|
815
|
+
writeFileSync2(tmp, raw, { encoding: "utf8", mode: 384 });
|
|
816
|
+
renameSync2(tmp, file);
|
|
817
|
+
} finally {
|
|
818
|
+
rmSync2(tmp, { force: true });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function readLedgerForMutation(secret) {
|
|
822
|
+
const file = activityPaths().ledger;
|
|
823
|
+
if (!existsSync2(file)) return emptyV2(secret);
|
|
824
|
+
const raw = readFileSync2(file, "utf8");
|
|
825
|
+
const parsed = JSON.parse(raw);
|
|
826
|
+
if (isObject(parsed) && parsed.schemaVersion === 2) {
|
|
827
|
+
const ledger = validateV2(parsed);
|
|
828
|
+
if (!keyCheckMatches(secret, ledger.keyCheck)) {
|
|
829
|
+
throw new Error("activity key integrity mismatch; manual recovery required");
|
|
830
|
+
}
|
|
831
|
+
return ledger;
|
|
832
|
+
}
|
|
833
|
+
const legacy = validateV1(parsed);
|
|
834
|
+
if (legacy.keyCheck !== void 0 && !keyCheckMatches(secret, legacy.keyCheck)) {
|
|
835
|
+
throw new Error("activity key integrity mismatch; manual recovery required");
|
|
836
|
+
}
|
|
837
|
+
writeLegacyBackup(raw);
|
|
838
|
+
const migrated = emptyV2(secret);
|
|
839
|
+
migrated.legacyV1 = { agents: legacy.agents };
|
|
840
|
+
atomicWriteLedger(migrated);
|
|
841
|
+
return migrated;
|
|
842
|
+
}
|
|
843
|
+
function loadVerifiedSecret() {
|
|
844
|
+
const lock = acquireCodexActivityLock();
|
|
845
|
+
try {
|
|
846
|
+
cleanupStaleFileStaging(activityPaths().ledger, 12);
|
|
847
|
+
const secret = loadOrCreateSecret();
|
|
848
|
+
const ledger = readLedgerForMutation(secret);
|
|
849
|
+
if (!existsSync2(activityPaths().ledger)) atomicWriteLedger(ledger);
|
|
850
|
+
return secret;
|
|
851
|
+
} finally {
|
|
852
|
+
lock.release();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function recordCodexSubagentEventInLedger(ledger, event, observedAt) {
|
|
856
|
+
if (!Number.isFinite(Date.parse(observedAt))) throw new Error("invalid observation timestamp");
|
|
857
|
+
if (ledger.conflictedAgents[event.agentKey] === true) return null;
|
|
858
|
+
let rootKey = ledger.agentAssignments[event.agentKey];
|
|
859
|
+
const session = ledger.sessions[event.sessionKey];
|
|
860
|
+
const activeRootKey = session?.activeRootKey ?? null;
|
|
861
|
+
if (rootKey !== void 0) {
|
|
862
|
+
const assignedRoot = ledger.roots[rootKey];
|
|
863
|
+
if (assignedRoot === void 0 || assignedRoot.sessionKey !== event.sessionKey) {
|
|
864
|
+
throw new Error("invalid activity assignment integrity");
|
|
865
|
+
}
|
|
866
|
+
if (event.kind === "start" && activeRootKey !== null && activeRootKey !== rootKey) {
|
|
867
|
+
ledger.conflictedAgents[event.agentKey] = true;
|
|
868
|
+
atomicWriteLedger(ledger);
|
|
869
|
+
logError("codex-subagent:agent-conflict", new Error("agent identity conflict; event was not recorded"));
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
if (event.kind !== "start" || activeRootKey === null) return null;
|
|
874
|
+
const activeRoot = ledger.roots[activeRootKey];
|
|
875
|
+
if (activeRoot === void 0 || activeRoot.status !== "open" || activeRoot.sessionKey !== event.sessionKey) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
rootKey = activeRootKey;
|
|
879
|
+
ledger.agentAssignments[event.agentKey] = rootKey;
|
|
880
|
+
}
|
|
881
|
+
const root = ledger.roots[rootKey];
|
|
882
|
+
if (root === void 0) throw new Error("missing assigned activity root");
|
|
883
|
+
const prior = root.agents[event.agentKey];
|
|
884
|
+
const next = reduceCodexSubagentActivity(prior, event, observedAt, rootKey);
|
|
885
|
+
root.agents[event.agentKey] = next;
|
|
886
|
+
atomicWriteLedger(ledger);
|
|
887
|
+
return next;
|
|
888
|
+
}
|
|
889
|
+
function recordCodexSubagentEvent(event, options = {}) {
|
|
890
|
+
const lock = acquireCodexActivityLock(options.lockTimeoutMs ?? 2e3);
|
|
891
|
+
try {
|
|
892
|
+
cleanupStaleFileStaging(activityPaths().ledger, 12);
|
|
893
|
+
const secret = loadOrCreateSecret();
|
|
894
|
+
const ledger = readLedgerForMutation(secret);
|
|
895
|
+
const observedAt = options.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
896
|
+
return recordCodexSubagentEventInLedger(ledger, event, observedAt);
|
|
897
|
+
} finally {
|
|
898
|
+
lock.release();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function recordCodexSubagentPayload(payload, expectedKind, options = {}) {
|
|
902
|
+
const lock = acquireCodexActivityLock(options.lockTimeoutMs ?? 2e3);
|
|
903
|
+
try {
|
|
904
|
+
const raw = validateRawCodexSubagentPayload(payload, expectedKind);
|
|
905
|
+
if (raw === null) throw new Error("invalid hook payload");
|
|
906
|
+
cleanupStaleFileStaging(activityPaths().ledger, 12);
|
|
907
|
+
const secret = loadOrCreateSecret();
|
|
908
|
+
const ledger = readLedgerForMutation(secret);
|
|
909
|
+
const event = deriveCodexSubagentEvent(secret, raw);
|
|
910
|
+
return recordCodexSubagentEventInLedger(ledger, event, options.observedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
911
|
+
} finally {
|
|
912
|
+
lock.release();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function readCodexSubagentActivity(projectionKey) {
|
|
916
|
+
try {
|
|
917
|
+
const file = activityPaths().ledger;
|
|
918
|
+
if (!existsSync2(file)) return [];
|
|
919
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
920
|
+
let states;
|
|
921
|
+
if (isObject(parsed) && parsed.schemaVersion === 1) {
|
|
922
|
+
const ledger = validateV1(parsed);
|
|
923
|
+
if (ledger.keyCheck !== void 0) {
|
|
924
|
+
const secret = readFileSync2(activityPaths().salt);
|
|
925
|
+
if (secret.length !== 32 || !keyCheckMatches(secret, ledger.keyCheck)) {
|
|
926
|
+
throw new Error("activity key integrity mismatch; read was rejected");
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
states = Object.values(ledger.agents);
|
|
930
|
+
} else {
|
|
931
|
+
const ledger = validateV2(parsed);
|
|
932
|
+
const secret = readFileSync2(activityPaths().salt);
|
|
933
|
+
if (secret.length !== 32 || !keyCheckMatches(secret, ledger.keyCheck)) {
|
|
934
|
+
throw new Error("activity key integrity mismatch; read was rejected");
|
|
935
|
+
}
|
|
936
|
+
states = [
|
|
937
|
+
...Object.values(ledger.roots).flatMap((root) => Object.values(root.agents)),
|
|
938
|
+
...Object.values(ledger.legacyV1?.agents ?? {})
|
|
939
|
+
];
|
|
940
|
+
}
|
|
941
|
+
return states.filter(
|
|
942
|
+
(state) => projectionKey === void 0 || state.projectionKey === projectionKey
|
|
943
|
+
);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
logError("codex-subagent:read-ledger", error);
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function nextSequence(ledger) {
|
|
950
|
+
if (ledger.sequence >= Number.MAX_SAFE_INTEGER) throw new Error("activity sequence exhausted");
|
|
951
|
+
ledger.sequence += 1;
|
|
952
|
+
return ledger.sequence;
|
|
953
|
+
}
|
|
954
|
+
function openCodexRootContext(payload, options = {}) {
|
|
955
|
+
const lock = acquireCodexActivityLock(options.lockTimeoutMs ?? 2e3);
|
|
956
|
+
try {
|
|
957
|
+
const raw = validateCodexRootPayload(payload, "UserPromptSubmit");
|
|
958
|
+
if (raw === null) return null;
|
|
959
|
+
const secret = loadOrCreateSecret();
|
|
960
|
+
const ledger = readLedgerForMutation(secret);
|
|
961
|
+
const event = deriveCodexRootEvent(secret, raw);
|
|
962
|
+
const observedAt = options.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
963
|
+
if (!Number.isFinite(Date.parse(observedAt))) throw new Error("invalid observation timestamp");
|
|
964
|
+
const existing = ledger.roots[event.rootKey];
|
|
965
|
+
if (existing !== void 0) {
|
|
966
|
+
return event.rootKey;
|
|
967
|
+
}
|
|
968
|
+
const session = ledger.sessions[event.sessionKey] ?? { activeRootKey: null, latestRootKey: null };
|
|
969
|
+
if (session.activeRootKey !== null) {
|
|
970
|
+
const previous = ledger.roots[session.activeRootKey];
|
|
971
|
+
if (previous === void 0 || previous.status !== "open") throw new Error("invalid active root state");
|
|
972
|
+
previous.status = "abandoned";
|
|
973
|
+
}
|
|
974
|
+
const sequence = nextSequence(ledger);
|
|
975
|
+
ledger.roots[event.rootKey] = {
|
|
976
|
+
sessionKey: event.sessionKey,
|
|
977
|
+
status: "open",
|
|
978
|
+
openedAt: observedAt,
|
|
979
|
+
openedSequence: sequence,
|
|
980
|
+
agents: {}
|
|
981
|
+
};
|
|
982
|
+
session.activeRootKey = event.rootKey;
|
|
983
|
+
session.latestRootKey = event.rootKey;
|
|
984
|
+
ledger.sessions[event.sessionKey] = session;
|
|
985
|
+
atomicWriteLedger(ledger);
|
|
986
|
+
return event.rootKey;
|
|
987
|
+
} finally {
|
|
988
|
+
lock.release();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function closeCodexRootContext(payload, options = {}) {
|
|
992
|
+
const lock = acquireCodexActivityLock(options.lockTimeoutMs ?? 2e3);
|
|
993
|
+
try {
|
|
994
|
+
const raw = validateCodexRootPayload(payload, "Stop");
|
|
995
|
+
if (raw === null) return null;
|
|
996
|
+
const secret = loadOrCreateSecret();
|
|
997
|
+
const ledger = readLedgerForMutation(secret);
|
|
998
|
+
const event = deriveCodexRootEvent(secret, raw);
|
|
999
|
+
const root = ledger.roots[event.rootKey];
|
|
1000
|
+
if (root === void 0 || root.sessionKey !== event.sessionKey) return null;
|
|
1001
|
+
if (root.status === "closed") return event.rootKey;
|
|
1002
|
+
const observedAt = options.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
|
+
if (!Number.isFinite(Date.parse(observedAt))) throw new Error("invalid observation timestamp");
|
|
1004
|
+
root.status = "closed";
|
|
1005
|
+
root.closedAt = observedAt;
|
|
1006
|
+
root.closedSequence = nextSequence(ledger);
|
|
1007
|
+
const session = ledger.sessions[event.sessionKey];
|
|
1008
|
+
if (session?.activeRootKey === event.rootKey) session.activeRootKey = null;
|
|
1009
|
+
atomicWriteLedger(ledger);
|
|
1010
|
+
return event.rootKey;
|
|
1011
|
+
} finally {
|
|
1012
|
+
lock.release();
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function codexActivityProjectionKey(payload) {
|
|
1016
|
+
return closeCodexRootContext(payload);
|
|
1017
|
+
}
|
|
1018
|
+
function projectCodexSubagentActivity(states) {
|
|
1019
|
+
if (states.length === 0) return void 0;
|
|
1020
|
+
return {
|
|
1021
|
+
started: states.filter((state) => state.startObserved).length,
|
|
1022
|
+
stopped: states.filter((state) => state.stopObserved).length,
|
|
1023
|
+
agentTypes: [...new Set(states.map((state) => state.agentTypeLabel))].sort(),
|
|
1024
|
+
usageStatus: "unavailable"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function handleCodexSubagentHook(rawText, kind) {
|
|
1028
|
+
try {
|
|
1029
|
+
const parsed = JSON.parse(rawText);
|
|
1030
|
+
recordCodexSubagentPayload(parsed, kind);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
const integrityFailure = error instanceof Error && (error.message.includes("activity key integrity mismatch") || error.message.includes("activity key with existing ledger") || error.message.includes("activity ledger keyCheck"));
|
|
1033
|
+
logError(
|
|
1034
|
+
"codex-subagent-hook",
|
|
1035
|
+
new Error(integrityFailure ? "key integrity mismatch; manual recovery required; hook event was not recorded" : "hook event was not recorded")
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function handleCodexUserPromptSubmitHook(rawText) {
|
|
1040
|
+
try {
|
|
1041
|
+
const parsed = JSON.parse(rawText);
|
|
1042
|
+
if (openCodexRootContext(parsed) === null) throw new Error("invalid hook payload");
|
|
1043
|
+
} catch {
|
|
1044
|
+
logError("codex-root-hook", new Error("root context was not recorded"));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export {
|
|
1049
|
+
normalizeAgentType,
|
|
1050
|
+
validateCodexSubagentPayload,
|
|
1051
|
+
reduceCodexSubagentActivity,
|
|
1052
|
+
acquireCodexActivityLock,
|
|
1053
|
+
recordCodexSubagentEvent,
|
|
1054
|
+
recordCodexSubagentPayload,
|
|
1055
|
+
readCodexSubagentActivity,
|
|
1056
|
+
openCodexRootContext,
|
|
1057
|
+
closeCodexRootContext,
|
|
1058
|
+
codexActivityProjectionKey,
|
|
1059
|
+
projectCodexSubagentActivity,
|
|
1060
|
+
handleCodexSubagentHook,
|
|
1061
|
+
handleCodexUserPromptSubmitHook,
|
|
1062
|
+
configFilePath,
|
|
1063
|
+
paths,
|
|
1064
|
+
readConfig,
|
|
1065
|
+
readMuteState,
|
|
1066
|
+
isMuted,
|
|
1067
|
+
writeMuteState,
|
|
1068
|
+
clearMuteState,
|
|
1069
|
+
loadCursor,
|
|
1070
|
+
sanitizeCursor,
|
|
1071
|
+
saveCursor,
|
|
1072
|
+
appendTurn,
|
|
1073
|
+
readTurns,
|
|
1074
|
+
todayTotalUSD,
|
|
1075
|
+
currentMonthTotals,
|
|
1076
|
+
logError
|
|
1077
|
+
};
|