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.
@@ -3,10 +3,12 @@ import {
3
3
  browserOpenPlan,
4
4
  runDashboard,
5
5
  writeDashboardHtml
6
- } from "./chunk-QX5KIRSU.js";
6
+ } from "./chunk-QWBQN4XY.js";
7
7
  import "./chunk-DGXUSPS4.js";
8
8
  import "./chunk-J5QAYTFE.js";
9
- import "./chunk-ECADO26T.js";
9
+ import "./chunk-ENGUOLTD.js";
10
+ import "./chunk-O34L3NSI.js";
11
+ import "./chunk-5PH7PPD6.js";
10
12
  export {
11
13
  browserOpenPlan,
12
14
  runDashboard,
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ invalidateCanonicalDashboards
4
+ } from "./chunk-ENGUOLTD.js";
5
+ import {
6
+ waitForDataLock
7
+ } from "./chunk-O34L3NSI.js";
8
+ import {
9
+ paths
10
+ } from "./chunk-5PH7PPD6.js";
11
+
12
+ // src/history.ts
13
+ import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from "fs";
14
+ import { createHash } from "crypto";
15
+ import * as p from "@clack/prompts";
16
+ function parseFlags(argv) {
17
+ let days = null;
18
+ let yes = false;
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a === "--yes" || a === "-y") {
22
+ yes = true;
23
+ } else if (a === "--days") {
24
+ const n = Number.parseInt(argv[i + 1] ?? "", 10);
25
+ if (Number.isFinite(n) && n > 0) days = n;
26
+ i++;
27
+ } else if (a.startsWith("--days=")) {
28
+ const n = Number.parseInt(a.slice("--days=".length), 10);
29
+ if (Number.isFinite(n) && n > 0) days = n;
30
+ }
31
+ }
32
+ return { days, yes };
33
+ }
34
+ function readLines(file) {
35
+ const raw = readFileSync(file, "utf8");
36
+ const lines = [];
37
+ for (const line of raw.split("\n")) {
38
+ if (!line.trim()) continue;
39
+ let rec = null;
40
+ try {
41
+ rec = JSON.parse(line);
42
+ } catch {
43
+ rec = null;
44
+ }
45
+ lines.push({ raw: line, rec });
46
+ }
47
+ return lines;
48
+ }
49
+ function isTargeted(rec, cutoffMs) {
50
+ if (cutoffMs === null) return true;
51
+ const ts = Date.parse(rec.ts);
52
+ if (!Number.isFinite(ts)) return false;
53
+ return ts < cutoffMs;
54
+ }
55
+ function atomicWrite(file, content) {
56
+ const tmp = `${file}.tmp`;
57
+ writeFileSync(tmp, content, "utf8");
58
+ renameSync(tmp, file);
59
+ }
60
+ function collectTargets(lines, sub, cutoff) {
61
+ const targetSet = /* @__PURE__ */ new Set();
62
+ for (let i = 0; i < lines.length; i++) {
63
+ const rec = lines[i].rec;
64
+ if (!rec || !isTargeted(rec, cutoff)) continue;
65
+ if (sub === "redact" && !(typeof rec.prompt === "string" && rec.prompt.length > 0)) continue;
66
+ targetSet.add(i);
67
+ }
68
+ return targetSet;
69
+ }
70
+ function targetFingerprint(lines, targets) {
71
+ const hash = createHash("sha256");
72
+ hash.update(`count:${targets.size}
73
+ `);
74
+ for (const i of [...targets].sort((a, b) => a - b)) {
75
+ hash.update(`${i}:${lines[i]?.raw ?? ""}
76
+ `);
77
+ }
78
+ return hash.digest("hex");
79
+ }
80
+ async function runHistory(argv, deps = {}) {
81
+ const [sub, ...rest] = argv;
82
+ if (sub !== "clear" && sub !== "redact") {
83
+ console.error(
84
+ "\u4F7F\u3044\u65B9 / Usage: ccc-notifier history <clear|redact> [--days N] [--yes]\n clear \u2026 \u30EC\u30B3\u30FC\u30C9\u3054\u3068\u524A\u9664(\u30C1\u30E3\u30FC\u30C8\u30FB\u96C6\u8A08\u304B\u3089\u3082\u6D88\u3048\u308B) / delete records\n redact \u2026 \u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3060\u3051\u6D88\u3059(\u30B3\u30B9\u30C8\u30FB\u30C1\u30E3\u30FC\u30C8\u306F\u6B8B\u3059) / strip prompts only"
85
+ );
86
+ return 1;
87
+ }
88
+ const flags = parseFlags(rest);
89
+ const file = paths().historyFile;
90
+ let lines = existsSync(file) ? readLines(file) : [];
91
+ const cutoff = flags.days !== null ? Date.now() - flags.days * 864e5 : null;
92
+ const scope = flags.days !== null ? `${flags.days}\u65E5\u3088\u308A\u524D` : "\u5168\u671F\u9593";
93
+ let targetSet = collectTargets(lines, sub, cutoff);
94
+ const initialFingerprint = targetFingerprint(lines, targetSet);
95
+ const action = sub === "clear" ? "\u30EC\u30B3\u30FC\u30C9\u3054\u3068\u524A\u9664" : "\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u6D88\u53BB";
96
+ if (targetSet.size > 0 && !flags.yes) {
97
+ const confirmed = await (deps.confirm ?? p.confirm)({
98
+ message: `${scope}\u306E\u5C65\u6B74 ${targetSet.size} \u4EF6\u3092${action}\u3057\u307E\u3059\u3002\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3002\u3088\u308D\u3057\u3044\u3067\u3059\u304B?`,
99
+ initialValue: false
100
+ });
101
+ if (p.isCancel(confirmed) || !confirmed) {
102
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
103
+ return 0;
104
+ }
105
+ }
106
+ const lock = await waitForDataLock();
107
+ if (lock === null) {
108
+ console.error("\u5C65\u6B74\u306E\u66F4\u65B0\u30ED\u30C3\u30AF\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u5F8C\u3067\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044 / history lock is busy");
109
+ return 1;
110
+ }
111
+ try {
112
+ lines = existsSync(file) ? readLines(file) : [];
113
+ targetSet = collectTargets(lines, sub, cutoff);
114
+ if (!flags.yes && targetFingerprint(lines, targetSet) !== initialFingerprint) {
115
+ console.error(
116
+ "\u78BA\u8A8D\u4E2D\u306B\u5C65\u6B74\u304C\u5909\u66F4\u3055\u308C\u305F\u305F\u3081\u51E6\u7406\u3057\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044 / history changed; retry"
117
+ );
118
+ return 1;
119
+ }
120
+ invalidateCanonicalDashboards();
121
+ if (!existsSync(file)) {
122
+ console.log("\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093(history.jsonl \u306F\u672A\u4F5C\u6210\u3067\u3059)\u3002");
123
+ return 0;
124
+ }
125
+ if (targetSet.size === 0) {
126
+ console.log(`\u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093(${scope})\u3002`);
127
+ return 0;
128
+ }
129
+ try {
130
+ if (sub === "clear") {
131
+ const kept = lines.filter((_, i) => !targetSet.has(i)).map((l) => l.raw);
132
+ if (kept.length === 0) {
133
+ rmSync(file, { force: true });
134
+ } else {
135
+ atomicWrite(file, kept.join("\n") + "\n");
136
+ }
137
+ console.log(`\u5C65\u6B74 ${targetSet.size} \u4EF6\u3092\u524A\u9664\u3057\u307E\u3057\u305F(${scope})\u3002`);
138
+ } else {
139
+ const out = lines.map((l, i) => {
140
+ if (!targetSet.has(i) || l.rec === null) return l.raw;
141
+ return JSON.stringify({ ...l.rec, prompt: "" });
142
+ });
143
+ atomicWrite(file, out.join("\n") + "\n");
144
+ console.log(
145
+ `\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092 ${targetSet.size} \u4EF6\u6D88\u53BB\u3057\u307E\u3057\u305F(${scope}\u3002\u30B3\u30B9\u30C8\u96C6\u8A08\u30FB\u30C1\u30E3\u30FC\u30C8\u306F\u4FDD\u6301\u3055\u308C\u307E\u3059)\u3002`
146
+ );
147
+ }
148
+ } catch (err) {
149
+ console.error(
150
+ `\u5C65\u6B74\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F / failed to update history: ${err instanceof Error ? err.message : String(err)}`
151
+ );
152
+ return 1;
153
+ }
154
+ invalidateCanonicalDashboards();
155
+ return 0;
156
+ } finally {
157
+ lock.release();
158
+ }
159
+ }
160
+ export {
161
+ runHistory
162
+ };
@@ -3,8 +3,8 @@ import {
3
3
  fmtMuteUntil,
4
4
  runMute,
5
5
  runUnmute
6
- } from "./chunk-4RZ6OGTD.js";
7
- import "./chunk-ECADO26T.js";
6
+ } from "./chunk-DDUK4EWQ.js";
7
+ import "./chunk-5PH7PPD6.js";
8
8
  export {
9
9
  fmtMuteUntil,
10
10
  runMute,
@@ -3,12 +3,12 @@ import {
3
3
  matchesMarker,
4
4
  runInit,
5
5
  runUninstall
6
- } from "./chunk-7KVOQ4UZ.js";
6
+ } from "./chunk-S3XKO7MY.js";
7
7
  import "./chunk-NV5UOHJA.js";
8
8
  import "./chunk-DGXUSPS4.js";
9
9
  import "./chunk-HTYUYKFW.js";
10
10
  import "./chunk-J5QAYTFE.js";
11
- import "./chunk-ECADO26T.js";
11
+ import "./chunk-5PH7PPD6.js";
12
12
  export {
13
13
  matchesMarker,
14
14
  runInit,
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ acquireCodexActivityLock,
4
+ closeCodexRootContext,
5
+ codexActivityProjectionKey,
6
+ handleCodexSubagentHook,
7
+ handleCodexUserPromptSubmitHook,
8
+ normalizeAgentType,
9
+ openCodexRootContext,
10
+ projectCodexSubagentActivity,
11
+ readCodexSubagentActivity,
12
+ recordCodexSubagentEvent,
13
+ recordCodexSubagentPayload,
14
+ reduceCodexSubagentActivity,
15
+ validateCodexSubagentPayload
16
+ } from "./chunk-5PH7PPD6.js";
17
+ export {
18
+ acquireCodexActivityLock,
19
+ closeCodexRootContext,
20
+ codexActivityProjectionKey,
21
+ handleCodexSubagentHook,
22
+ handleCodexUserPromptSubmitHook,
23
+ normalizeAgentType,
24
+ openCodexRootContext,
25
+ projectCodexSubagentActivity,
26
+ readCodexSubagentActivity,
27
+ recordCodexSubagentEvent,
28
+ recordCodexSubagentPayload,
29
+ reduceCodexSubagentActivity,
30
+ validateCodexSubagentPayload
31
+ };
@@ -1,24 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ listCodexRollouts
4
+ } from "./chunk-HLJAJS2W.js";
2
5
  import {
3
6
  codexHome,
4
7
  detectCodex
5
8
  } from "./chunk-HTYUYKFW.js";
6
9
  import {
7
- collectSubagentUsage,
8
- splitIntoCodexTurnDrafts
9
- } from "./chunk-DSV75EF7.js";
10
+ collectSubagentUsage
11
+ } from "./chunk-O2AR4PXU.js";
10
12
  import {
11
13
  computeCost,
12
14
  extractBucket,
13
15
  getUsdJpy,
14
16
  loadPriceTable,
15
- promptCandidate
16
- } from "./chunk-LHKBGA5K.js";
17
+ promptCandidate,
18
+ splitIntoCodexTurnDrafts
19
+ } from "./chunk-LVMKY6JB.js";
17
20
  import {
18
21
  formatJPY,
19
22
  formatUSD,
20
23
  modelDisplayName
21
24
  } from "./chunk-J5QAYTFE.js";
25
+ import {
26
+ waitForDataLock
27
+ } from "./chunk-O34L3NSI.js";
22
28
  import {
23
29
  appendTurn,
24
30
  loadCursor,
@@ -27,7 +33,7 @@ import {
27
33
  readConfig,
28
34
  sanitizeCursor,
29
35
  saveCursor
30
- } from "./chunk-ECADO26T.js";
36
+ } from "./chunk-5PH7PPD6.js";
31
37
 
32
38
  // src/sweep.ts
33
39
  import { readFile } from "fs/promises";
@@ -258,99 +264,91 @@ function mergeUnknownModels(rec, extra) {
258
264
  for (const m of extra) if (!merged.includes(m)) merged.push(m);
259
265
  rec.unknownModels = merged;
260
266
  }
261
- async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary) {
262
- const cursor = sanitizeCursor(loadCursor(mainPath));
263
- const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
264
- const records = [];
265
- for (const draft of drafts) {
266
- const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
267
- if (daysCutoff !== null) {
268
- const tsMs = Date.parse(ts);
269
- if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
270
- }
271
- records.push(draftToRecord(draft, ts, table, fx));
267
+ async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary, lockProvider) {
268
+ const lock = dryRun ? null : await lockProvider();
269
+ if (!dryRun && lock === null) {
270
+ logError("sweep:data-lock", new Error(`data lock timeout: ${mainPath}`));
271
+ summary.lockTimeouts += 1;
272
+ return;
272
273
  }
273
- let sa = null;
274
274
  try {
275
- sa = await collectSubagentUsage(mainPath);
276
- } catch (err) {
277
- logError("sweep:subagents", err);
278
- sa = null;
279
- }
280
- const saHasUsage = sa !== null && sa.apiCalls > 0;
281
- if (saHasUsage) {
282
- const saBreakdown = computeCost(sa.perModel, {}, table);
283
- summary.subagentsUSD += saBreakdown.usd;
284
- const saBlock = {
285
- costUSD: saBreakdown.usd,
286
- costByModel: saBreakdown.byModel,
287
- tokens: sumBuckets(sa.perModel),
288
- apiCalls: sa.apiCalls,
289
- agentFiles: sa.agentFiles
290
- };
291
- if (records.length > 0) {
292
- const last = records[records.length - 1];
293
- last.subagents = saBlock;
294
- mergeUnknownModels(last, saBreakdown.unknownModels);
295
- } else {
296
- const rec = {
297
- schemaVersion: 1,
298
- ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
299
- sessionId: "",
300
- project: "",
301
- gitBranch: null,
302
- models: collectModels(sa.perModel, {}),
303
- tokens: emptyBuckets(),
304
- sidechainTokens: null,
305
- apiCalls: 0,
306
- costUSD: 0,
307
- costByModel: {},
308
- costJPY: 0,
309
- fxRate: fx.rate,
310
- fxSource: fx.source,
311
- prompt: "",
312
- subagents: saBlock,
313
- ingest: "sweep"
314
- };
315
- mergeUnknownModels(rec, saBreakdown.unknownModels);
316
- records.push(rec);
317
- }
318
- summary.agentFiles += sa.agentFiles;
319
- }
320
- for (const rec of records) {
321
- summary.newRecords += 1;
322
- summary.totalUSD += rec.costUSD;
323
- if (rec.costByModel) {
324
- for (const [m, c] of Object.entries(rec.costByModel)) {
325
- summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
275
+ const cursor = sanitizeCursor(loadCursor(mainPath));
276
+ const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
277
+ const records = [];
278
+ for (const draft of drafts) {
279
+ const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
280
+ if (daysCutoff !== null) {
281
+ const tsMs = Date.parse(ts);
282
+ if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
326
283
  }
284
+ records.push(draftToRecord(draft, ts, table, fx));
327
285
  }
328
- }
329
- if (!dryRun) {
330
- for (const rec of records) appendTurn(rec);
331
- if (drafts.length > 0) saveCursor(mainPath, newCursor);
286
+ let sa = null;
287
+ try {
288
+ sa = await collectSubagentUsage(mainPath);
289
+ } catch (err) {
290
+ logError("sweep:subagents", err);
291
+ sa = null;
292
+ }
293
+ const saHasUsage = sa !== null && sa.apiCalls > 0;
332
294
  if (saHasUsage) {
333
- for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
295
+ const saBreakdown = computeCost(sa.perModel, {}, table);
296
+ summary.subagentsUSD += saBreakdown.usd;
297
+ const saBlock = {
298
+ costUSD: saBreakdown.usd,
299
+ costByModel: saBreakdown.byModel,
300
+ tokens: sumBuckets(sa.perModel),
301
+ apiCalls: sa.apiCalls,
302
+ agentFiles: sa.agentFiles
303
+ };
304
+ if (records.length > 0) {
305
+ const last = records[records.length - 1];
306
+ last.subagents = saBlock;
307
+ mergeUnknownModels(last, saBreakdown.unknownModels);
308
+ } else {
309
+ const rec = {
310
+ schemaVersion: 1,
311
+ ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
312
+ sessionId: "",
313
+ project: "",
314
+ gitBranch: null,
315
+ models: collectModels(sa.perModel, {}),
316
+ tokens: emptyBuckets(),
317
+ sidechainTokens: null,
318
+ apiCalls: 0,
319
+ costUSD: 0,
320
+ costByModel: {},
321
+ costJPY: 0,
322
+ fxRate: fx.rate,
323
+ fxSource: fx.source,
324
+ prompt: "",
325
+ subagents: saBlock,
326
+ ingest: "sweep"
327
+ };
328
+ mergeUnknownModels(rec, saBreakdown.unknownModels);
329
+ records.push(rec);
330
+ }
331
+ summary.agentFiles += sa.agentFiles;
334
332
  }
335
- }
336
- }
337
- var CODEX_MAX_DEPTH = 4;
338
- async function listCodexRollouts(sessionsRoot) {
339
- const found = [];
340
- const walk = async (dir, depth) => {
341
- const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
342
- if (entries === null) return;
343
- for (const e of entries) {
344
- const full = join(dir, e.name);
345
- if (e.isFile()) {
346
- if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) found.push(full);
347
- } else if (e.isDirectory() && depth < CODEX_MAX_DEPTH) {
348
- await walk(full, depth + 1);
333
+ for (const rec of records) {
334
+ summary.newRecords += 1;
335
+ summary.totalUSD += rec.costUSD;
336
+ if (rec.costByModel) {
337
+ for (const [m, c] of Object.entries(rec.costByModel)) {
338
+ summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
339
+ }
349
340
  }
350
341
  }
351
- };
352
- await walk(sessionsRoot, 1);
353
- return found;
342
+ if (!dryRun) {
343
+ for (const rec of records) appendTurn(rec);
344
+ if (drafts.length > 0) saveCursor(mainPath, newCursor);
345
+ if (saHasUsage) {
346
+ for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
347
+ }
348
+ }
349
+ } finally {
350
+ lock?.release();
351
+ }
354
352
  }
355
353
  function codexDraftToRecord(draft, ts, table, fx) {
356
354
  const main = draft.agg.main;
@@ -379,33 +377,43 @@ function codexDraftToRecord(draft, ts, table, fx) {
379
377
  if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
380
378
  return rec;
381
379
  }
382
- async function processCodexRollout(rolloutPath, table, fx, daysCutoff, dryRun, summary) {
383
- const cursor = sanitizeCursor(loadCursor(rolloutPath));
384
- const drafts = await splitIntoCodexTurnDrafts(rolloutPath, cursor);
385
- if (drafts === null || drafts.length === 0) return;
386
- const records = [];
387
- for (const draft of drafts) {
388
- const ts = draft.endTs ?? (/* @__PURE__ */ new Date()).toISOString();
389
- if (daysCutoff !== null) {
390
- const tsMs = Date.parse(ts);
391
- if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
392
- }
393
- records.push(codexDraftToRecord(draft, ts, table, fx));
380
+ async function processCodexRollout(rolloutPath, table, fx, daysCutoff, dryRun, summary, lockProvider) {
381
+ const lock = dryRun ? null : await lockProvider();
382
+ if (!dryRun && lock === null) {
383
+ logError("sweep:data-lock", new Error(`data lock timeout: ${rolloutPath}`));
384
+ summary.lockTimeouts += 1;
385
+ return;
394
386
  }
395
- for (const rec of records) {
396
- summary.newRecords += 1;
397
- summary.totalUSD += rec.costUSD;
398
- summary.codexRecords += 1;
399
- summary.codexUSD += rec.costUSD;
400
- if (rec.costByModel) {
401
- for (const [m, c] of Object.entries(rec.costByModel)) {
402
- summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
387
+ try {
388
+ const cursor = sanitizeCursor(loadCursor(rolloutPath));
389
+ const drafts = await splitIntoCodexTurnDrafts(rolloutPath, cursor);
390
+ if (drafts === null || drafts.length === 0) return;
391
+ const records = [];
392
+ for (const draft of drafts) {
393
+ const ts = draft.endTs ?? (/* @__PURE__ */ new Date()).toISOString();
394
+ if (daysCutoff !== null) {
395
+ const tsMs = Date.parse(ts);
396
+ if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
403
397
  }
398
+ records.push(codexDraftToRecord(draft, ts, table, fx));
404
399
  }
405
- }
406
- if (!dryRun) {
407
- for (const rec of records) appendTurn(rec);
408
- saveCursor(rolloutPath, drafts[drafts.length - 1].agg.newCursor);
400
+ for (const rec of records) {
401
+ summary.newRecords += 1;
402
+ summary.totalUSD += rec.costUSD;
403
+ summary.codexRecords += 1;
404
+ summary.codexUSD += rec.costUSD;
405
+ if (rec.costByModel) {
406
+ for (const [m, c] of Object.entries(rec.costByModel)) {
407
+ summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
408
+ }
409
+ }
410
+ }
411
+ if (!dryRun) {
412
+ for (const rec of records) appendTurn(rec);
413
+ saveCursor(rolloutPath, drafts[drafts.length - 1].agg.newCursor);
414
+ }
415
+ } finally {
416
+ lock?.release();
409
417
  }
410
418
  }
411
419
  async function codexSessionsRoot() {
@@ -414,17 +422,17 @@ async function codexSessionsRoot() {
414
422
  const isDir = await fsp.stat(sessionsRoot).then((st) => st.isDirectory()).catch(() => false);
415
423
  return isDir ? sessionsRoot : null;
416
424
  }
417
- async function sweepCodex(summary, table, fx, daysCutoff, flags) {
425
+ async function sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider) {
418
426
  const sessionsRoot = await codexSessionsRoot();
419
427
  if (sessionsRoot === null) return;
420
- const rollouts = await listCodexRollouts(sessionsRoot);
421
- for (const rolloutPath of rollouts) {
428
+ const discovery = await listCodexRollouts(sessionsRoot);
429
+ for (const rolloutPath of discovery.rollouts) {
422
430
  if (!flags.includeActive && await isRecentlyModified(rolloutPath)) {
423
431
  summary.skippedActive += 1;
424
432
  continue;
425
433
  }
426
434
  try {
427
- await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary);
435
+ await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
428
436
  } catch (err) {
429
437
  logError("sweep:codex", err);
430
438
  }
@@ -489,7 +497,12 @@ function printSweepSummary(summary, fx) {
489
497
  `\u30B9\u30AD\u30C3\u30D7: ${summary.skippedActive} transcript(\u76F4\u8FD1${ACTIVE_GUARD_MIN}\u5206\u4EE5\u5185\u306B\u66F4\u65B0 = \u9032\u884C\u4E2D\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u53EF\u80FD\u6027)\u3002\u30BB\u30C3\u30B7\u30E7\u30F3\u5B8C\u4E86\u5F8C\u306B\u518D\u5B9F\u884C\u3059\u308B\u304B\u3001\u5B8C\u4E86\u6E08\u307F\u3068\u5206\u304B\u3063\u3066\u3044\u308B\u5834\u5408\u306F --include-active \u3067\u53D6\u308A\u8FBC\u3081\u307E\u3059`
490
498
  );
491
499
  }
492
- if (summary.newRecords === 0) {
500
+ if (summary.lockTimeouts > 0) {
501
+ console.log(
502
+ `\u672A\u5B8C\u4E86: ${summary.lockTimeouts} \u4EF6\u306Fdata lock\u3092\u53D6\u5F97\u3067\u304D\u305A\u672A\u51E6\u7406\u3067\u3059\u3002\u30AB\u30FC\u30BD\u30EB\u306F\u9032\u3081\u3066\u3044\u306A\u3044\u305F\u3081\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044`
503
+ );
504
+ }
505
+ if (summary.newRecords === 0 && summary.lockTimeouts === 0) {
493
506
  console.log("\u65B0\u898F\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
494
507
  } else {
495
508
  console.log(
@@ -513,10 +526,12 @@ function printSweepSummary(summary, fx) {
513
526
  }
514
527
  }
515
528
  }
516
- console.log("\u65E2\u306B\u8A08\u4E0A\u6E08\u307F\u306E\u5206\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F(\u4E8C\u91CD\u8A08\u4E0A\u306A\u3057)");
529
+ if (summary.lockTimeouts === 0) {
530
+ console.log("\u65E2\u306B\u8A08\u4E0A\u6E08\u307F\u306E\u5206\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F(\u4E8C\u91CD\u8A08\u4E0A\u306A\u3057)");
531
+ }
517
532
  console.log(`\u5186\u63DB\u7B97\u30EC\u30FC\u30C8: 1USD = ${fx.rate}JPY(source=${fx.source})`);
518
533
  }
519
- async function runSweep(argv) {
534
+ async function runSweep(argv, deps = {}) {
520
535
  const flags = parseSweepFlags(argv);
521
536
  const root = projectsRoot(flags.projects);
522
537
  const projectDirs = await listProjectDirs(root);
@@ -543,9 +558,11 @@ async function runSweep(argv) {
543
558
  skippedActive: 0,
544
559
  codexRecords: 0,
545
560
  codexUSD: 0,
546
- dryRun: flags.dryRun
561
+ dryRun: flags.dryRun,
562
+ lockTimeouts: 0
547
563
  };
548
564
  const daysCutoff = flags.days !== null ? Date.now() - flags.days * DAY_MS : null;
565
+ const lockProvider = deps.lockProvider ?? (() => waitForDataLock());
549
566
  for (const projectDir of projectDirs ?? []) {
550
567
  const transcripts = await listTranscripts(projectDir);
551
568
  for (const mainPath of transcripts) {
@@ -555,16 +572,16 @@ async function runSweep(argv) {
555
572
  continue;
556
573
  }
557
574
  try {
558
- await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary);
575
+ await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
559
576
  } catch (err) {
560
577
  logError("sweep:transcript", err);
561
578
  }
562
579
  }
563
580
  }
564
- await sweepCodex(summary, table, fx, daysCutoff, flags);
581
+ await sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider);
565
582
  summary.totalJPY = summary.totalUSD * fx.rate;
566
583
  printSweepSummary(summary, fx);
567
- return 0;
584
+ return summary.lockTimeouts > 0 ? 1 : 0;
568
585
  }
569
586
  export {
570
587
  runSweep,