codex-copilot-dx 0.6.18 → 0.6.20

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/src/usage.mjs CHANGED
@@ -4,11 +4,17 @@ import path from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { cliOutputFormat, cliOutputWidth, formatResponsiveCliTable, terminalCell } from "./cli-table.mjs";
6
6
  import { parseByteLimit, rotateFileIfNeededSync, rotatedFilePath } from "./file-rotation.mjs";
7
+ import { withFileLock } from "./lock.mjs";
7
8
 
8
9
  const DEFAULT_USAGE_PATH = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "usage.jsonl");
9
10
  const DEFAULT_USAGE_MAX_BYTES = 32 * 1024 * 1024;
11
+ const USAGE_WRITE_LOCK_TIMEOUT_MS = 1_000;
12
+ const USAGE_WRITE_LOCK_STALE_MS = 1_000;
13
+ const USAGE_WRITE_LOCK_POLL_MS = 5;
14
+ const USAGE_FLUSH_TIMEOUT_MS = 1_500;
10
15
 
11
- let writeQueue = Promise.resolve();
16
+ const pendingWrites = [];
17
+ let writeDrain = null;
12
18
 
13
19
  export function usageLogPath() {
14
20
  return process.env.CCDX_USAGE_PATH || DEFAULT_USAGE_PATH;
@@ -119,19 +125,63 @@ export function buildAnthropicUsageRecord({ surface = "messages", mode, model, r
119
125
  });
120
126
  }
121
127
 
128
+ function takePendingWrites(filePath, maxBytes) {
129
+ const batch = [];
130
+ let retained = 0;
131
+ for (const entry of pendingWrites) {
132
+ if (entry.filePath === filePath && entry.maxBytes === maxBytes) batch.push(entry);
133
+ else pendingWrites[retained++] = entry;
134
+ }
135
+ pendingWrites.length = retained;
136
+ return batch;
137
+ }
138
+
139
+ async function writeUsageBatch(batch) {
140
+ const { filePath, maxBytes } = batch[0];
141
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
142
+ await withFileLock(`${path.resolve(filePath)}.lock`, async () => {
143
+ batch.push(...takePendingWrites(filePath, maxBytes));
144
+ for (const { line } of batch) {
145
+ rotateFileIfNeededSync(filePath, Buffer.byteLength(line), maxBytes);
146
+ await fs.promises.appendFile(filePath, line, { encoding: "utf8", mode: 0o600 });
147
+ }
148
+ }, {
149
+ timeoutMs: USAGE_WRITE_LOCK_TIMEOUT_MS,
150
+ staleMs: USAGE_WRITE_LOCK_STALE_MS,
151
+ pollMs: USAGE_WRITE_LOCK_POLL_MS,
152
+ });
153
+ }
154
+
155
+ function scheduleUsageWriteDrain() {
156
+ if (writeDrain) return;
157
+ writeDrain = Promise.resolve().then(async () => {
158
+ while (pendingWrites.length > 0) {
159
+ const first = pendingWrites.shift();
160
+ const batch = [first, ...takePendingWrites(first.filePath, first.maxBytes)];
161
+ try {
162
+ await writeUsageBatch(batch);
163
+ } catch (error) {
164
+ batch.push(...takePendingWrites(first.filePath, first.maxBytes));
165
+ console.error(`codex-copilot-dx usage log write failed: ${error.message}`);
166
+ } finally {
167
+ for (const entry of batch) entry.resolve();
168
+ }
169
+ }
170
+ }).finally(() => {
171
+ writeDrain = null;
172
+ if (pendingWrites.length > 0) scheduleUsageWriteDrain();
173
+ });
174
+ }
175
+
122
176
  export function recordUsage(record) {
123
177
  if (!record || process.env.CCDX_DISABLE_USAGE === "1") return Promise.resolve();
124
178
  const filePath = usageLogPath();
125
179
  const line = `${JSON.stringify(record)}\n`;
126
- writeQueue = writeQueue
127
- .catch(() => {})
128
- .then(async () => {
129
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
130
- rotateFileIfNeededSync(filePath, Buffer.byteLength(line), usageLogMaxBytes());
131
- await fs.promises.appendFile(filePath, line, { encoding: "utf8", mode: 0o600 });
132
- })
133
- .catch((e) => console.error(`codex-copilot-dx usage log write failed: ${e.message}`));
134
- return writeQueue;
180
+ const completed = new Promise((resolve) => {
181
+ pendingWrites.push({ filePath, line, maxBytes: usageLogMaxBytes(), resolve });
182
+ });
183
+ scheduleUsageWriteDrain();
184
+ return completed;
135
185
  }
136
186
 
137
187
  export function recordResponsesUsage(args) {
@@ -143,11 +193,28 @@ export function recordAnthropicUsage(args) {
143
193
  }
144
194
 
145
195
  export async function flushUsageWritesForTests() {
146
- await writeQueue;
196
+ while (writeDrain || pendingWrites.length > 0) {
197
+ if (writeDrain) await writeDrain;
198
+ else scheduleUsageWriteDrain();
199
+ }
147
200
  }
148
201
 
149
- export async function flushUsageWrites() {
150
- await writeQueue;
202
+ export async function flushUsageWrites({
203
+ timeoutMs = USAGE_FLUSH_TIMEOUT_MS,
204
+ warn = console.error,
205
+ } = {}) {
206
+ const timeout = Number.isFinite(timeoutMs) && timeoutMs >= 0
207
+ ? Math.floor(timeoutMs)
208
+ : USAGE_FLUSH_TIMEOUT_MS;
209
+ let timer;
210
+ const completed = await Promise.race([
211
+ flushUsageWritesForTests().then(() => true),
212
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeout); }),
213
+ ]);
214
+ if (timer) clearTimeout(timer);
215
+ if (!completed) {
216
+ try { warn(`codex-copilot-dx usage log flush timed out after ${timeout}ms`); } catch {}
217
+ }
151
218
  }
152
219
 
153
220
  async function* iterateUsageRecords(filePath, { warn = console.error } = {}) {