@bitkyc08/opencodex 2.7.28 → 2.7.29

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.
@@ -11,11 +11,13 @@ import type { OcxUsage } from "../types";
11
11
  import { redactSecretString } from "../lib/redact";
12
12
  import {
13
13
  appendUsageEntry,
14
+ readRecentUsageEntries,
14
15
  usageForFinalLog,
15
16
  usageStatusForFinalLog,
16
17
  usageTotalTokens,
17
18
  type AttemptRecoveryKind,
18
19
  type PersistedUsageAttempt,
20
+ type PersistedUsageEntry,
19
21
  type UsageStatus,
20
22
  } from "../usage/log";
21
23
  import {
@@ -95,6 +97,92 @@ export interface RequestLogEntry {
95
97
  const requestLog: RequestLogEntry[] = [];
96
98
  const MAX_LOG_SIZE = 200;
97
99
  let requestLogSeq = 0;
100
+ /** True after hydrateRequestLogsFromDisk ran once in this process. */
101
+ let requestLogsHydratedFromDisk = false;
102
+
103
+ function asTerminalStatus(value: string | undefined): ResponsesTerminalStatus | undefined {
104
+ if (value === "completed" || value === "failed" || value === "incomplete") return value;
105
+ return undefined;
106
+ }
107
+
108
+ function asCloseReason(value: string | undefined): RequestLogEntry["closeReason"] | undefined {
109
+ switch (value) {
110
+ case "terminal":
111
+ case "client_cancel":
112
+ case "non_stream":
113
+ case "body_stall":
114
+ case "body_overflow":
115
+ return value;
116
+ default:
117
+ return undefined;
118
+ }
119
+ }
120
+
121
+ /** Project a persisted usage.jsonl row back into the in-memory /api/logs shape. */
122
+ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): RequestLogEntry {
123
+ const terminalStatus = asTerminalStatus(entry.terminalStatus);
124
+ const closeReason = asCloseReason(entry.closeReason);
125
+ return {
126
+ requestId: entry.requestId,
127
+ timestamp: entry.timestamp,
128
+ model: entry.model,
129
+ provider: entry.provider,
130
+ ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
131
+ ...(entry.surface === "claude" ? { surface: entry.surface } : {}),
132
+ ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
133
+ ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
134
+ ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
135
+ ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
136
+ ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
137
+ ...(entry.configuredSpeedLabel ? { configuredSpeedLabel: entry.configuredSpeedLabel } : {}),
138
+ ...(entry.modelSupportsServiceTier !== undefined
139
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
140
+ : {}),
141
+ ...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}),
142
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
143
+ status: entry.status,
144
+ durationMs: entry.durationMs,
145
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
146
+ ...(terminalStatus ? { terminalStatus } : {}),
147
+ ...(closeReason ? { closeReason } : {}),
148
+ ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
149
+ usageStatus: entry.usageStatus,
150
+ ...(entry.usage ? { usage: entry.usage } : {}),
151
+ ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
152
+ ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Seed the in-memory Logs ring buffer from usage.jsonl so GUI /api/logs survives
158
+ * `ocx stop` / `ocx start` (process restart). Idempotent per process; no-ops when
159
+ * the buffer already has live entries. Read failures are non-fatal (same as /api/usage).
160
+ */
161
+ export function hydrateRequestLogsFromDisk(
162
+ reader: () => PersistedUsageEntry[] = () => readRecentUsageEntries(MAX_LOG_SIZE),
163
+ ): number {
164
+ if (requestLogsHydratedFromDisk) return 0;
165
+ if (requestLog.length > 0) {
166
+ requestLogsHydratedFromDisk = true;
167
+ return 0;
168
+ }
169
+ try {
170
+ const persisted = reader();
171
+ requestLogsHydratedFromDisk = true;
172
+ if (persisted.length === 0) return 0;
173
+ const slice = persisted.length > MAX_LOG_SIZE
174
+ ? persisted.slice(persisted.length - MAX_LOG_SIZE)
175
+ : persisted;
176
+ for (const entry of slice) requestLog.push(requestLogEntryFromPersistedUsage(entry));
177
+ return slice.length;
178
+ } catch (err) {
179
+ requestLogsHydratedFromDisk = true;
180
+ console.warn(
181
+ `[request-log] failed to hydrate from usage.jsonl: ${err instanceof Error ? err.message : String(err)}`,
182
+ );
183
+ return 0;
184
+ }
185
+ }
98
186
 
99
187
  export function addRequestLog(entry: RequestLogEntry) {
100
188
  requestLog.push(entry);
@@ -119,6 +207,15 @@ export function addRequestLog(entry: RequestLogEntry) {
119
207
  ...(entry.surface === "claude" ? { surface: entry.surface } : {}),
120
208
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
121
209
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
210
+ ...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
211
+ ...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
212
+ ...(entry.requestedSpeedLabel ? { requestedSpeedLabel: entry.requestedSpeedLabel } : {}),
213
+ ...(entry.configuredServiceTier ? { configuredServiceTier: entry.configuredServiceTier } : {}),
214
+ ...(entry.configuredSpeedLabel ? { configuredSpeedLabel: entry.configuredSpeedLabel } : {}),
215
+ ...(entry.modelSupportsServiceTier !== undefined
216
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
217
+ : {}),
218
+ ...(entry.responseServiceTier ? { responseServiceTier: entry.responseServiceTier } : {}),
122
219
  status: entry.status,
123
220
  durationMs: entry.durationMs,
124
221
  ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
@@ -687,4 +784,5 @@ export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
687
784
  export function clearRequestLogsForTests(): void {
688
785
  requestLog.length = 0;
689
786
  requestLogSeq = 0;
787
+ requestLogsHydratedFromDisk = false;
690
788
  }
package/src/service.ts CHANGED
@@ -242,9 +242,29 @@ function shellQuote(value: string): string {
242
242
  return `'${value.replace(/'/g, "'\\''")}'`;
243
243
  }
244
244
 
245
- function buildServiceShellCommand(bun: string, cli: string): string {
245
+ /**
246
+ * Listen port baked into service wrappers / WinSW XML.
247
+ * Priority: explicit override → OCX_BAKE_PORT (update restart) → config.port → 10100.
248
+ * `config.port === 0` means ephemeral for interactive start; services need a stable pin,
249
+ * so treat 0 / invalid like unset (default 10100) instead of baking `--port 0`.
250
+ */
251
+ export function resolveServiceListenPort(override?: number): number {
252
+ if (typeof override === "number" && Number.isFinite(override) && override > 0 && override <= 65535) {
253
+ return Math.trunc(override);
254
+ }
255
+ const baked = process.env.OCX_BAKE_PORT?.trim();
256
+ if (baked && /^\d+$/.test(baked)) {
257
+ const n = Number(baked);
258
+ if (n > 0 && n <= 65535) return n;
259
+ }
260
+ const configured = loadConfig().port;
261
+ if (typeof configured === "number" && configured > 0 && configured <= 65535) return configured;
262
+ return 10100;
263
+ }
264
+
265
+ function buildServiceShellCommand(bun: string, cli: string, port = resolveServiceListenPort()): string {
246
266
  const tokenFile = serviceApiTokenFilePath();
247
- return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${shellQuote(cli)} start`;
267
+ return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${shellQuote(cli)} start --port ${port}`;
248
268
  }
249
269
 
250
270
  function systemdQuote(value: string): string {
@@ -316,7 +336,7 @@ function taskXmlString(value: string): string {
316
336
  .replace(/'/g, "&apos;");
317
337
  }
318
338
 
319
- export function buildWindowsServiceScript(entry = cliEntry()): string {
339
+ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
320
340
  const { bun, cli } = entry;
321
341
  const bunRuntime = durableBunRuntime();
322
342
  const path = process.env.PATH ?? "";
@@ -345,7 +365,7 @@ export function buildWindowsServiceScript(entry = cliEntry()): string {
345
365
  '>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"',
346
366
  '>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
347
367
  '>>"%OCX_SERVICE_LOG%" echo token_file="%OCX_API_TOKEN_FILE%"',
348
- '"%OCX_BUN%" "%OCX_CLI%" start >>"%OCX_SERVICE_LOG%" 2>&1',
368
+ `"%OCX_BUN%" "%OCX_CLI%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1`,
349
369
  "if %ERRORLEVEL% NEQ 0 (",
350
370
  ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] child exited with code %ERRORLEVEL%; restarting in 5s',
351
371
  // `timeout` needs console stdin and dies with "Input redirection is not supported"
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readFileSync, readdirSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, join } from "node:path";
5
- import { getConfigDir, readPid, readRuntimePort } from "../config";
5
+ import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
6
6
 
7
7
  /**
8
8
  * A `codex-history-backup-*.json` surviving a stop means the native-history restore was
@@ -171,6 +171,20 @@ export async function runUpdate(): Promise<void> {
171
171
  serviceWasInstalled = isServiceInstalled();
172
172
  } catch { /* best-effort */ }
173
173
 
174
+ // Capture listen target before stop clears runtime state (same contract as GUI update worker).
175
+ // Prefer a live runtime record; a stale crashed leftover must not override config.port.
176
+ const preUpdateConfig = loadConfig();
177
+ const preUpdateRt = readRuntimePort();
178
+ const livePid = readPid();
179
+ const runtimeTrusted = !!(preUpdateRt && livePid && preUpdateRt.pid === livePid);
180
+ const configPort = typeof preUpdateConfig.port === "number" && preUpdateConfig.port > 0
181
+ ? preUpdateConfig.port
182
+ : 10100;
183
+ const capturedListen = {
184
+ port: runtimeTrusted ? preUpdateRt.port : configPort,
185
+ hostname: (runtimeTrusted ? preUpdateRt.hostname : undefined) ?? preUpdateConfig.hostname ?? "127.0.0.1",
186
+ };
187
+
174
188
  // Never replace package files under a live proxy: the running server dynamic-imports
175
189
  // modules after startup, so an in-place update leaves it executing mixed old/new code.
176
190
  // Gate on the service and the runtime-port record too, not just the pid file — a
@@ -230,14 +244,27 @@ export async function runUpdate(): Promise<void> {
230
244
  if (serviceWasInstalled) {
231
245
  console.log("🔁 Reinstalling the background service with the updated files...");
232
246
  const { serviceReinstallArgs } = await import("../service");
233
- const svcStdio = updateChildStdio();
234
- const svc = spawnSync(process.execPath, [process.argv[1], ...serviceReinstallArgs()], {
235
- stdio: svcStdio,
236
- encoding: svcStdio === "pipe" ? "utf8" : undefined,
237
- windowsHide: true,
247
+ const { waitForPortAvailable } = await import("../server/ports");
248
+ const freed = await waitForPortAvailable(capturedListen.port, capturedListen.hostname, {
249
+ timeoutMs: 5_000,
250
+ intervalMs: 25,
238
251
  });
239
- if (svcStdio === "pipe") logSpawnOutput("", svc);
240
- if (svc.status !== 0) console.warn("⚠️ Service refresh failed — run 'ocx service install' manually.");
252
+ if (!freed) console.warn(`⚠️ Port ${capturedListen.port} still busy; reinstalling with pinned --port anyway.`);
253
+ const prevBake = process.env.OCX_BAKE_PORT;
254
+ process.env.OCX_BAKE_PORT = String(capturedListen.port);
255
+ try {
256
+ const svcStdio = updateChildStdio();
257
+ const svc = spawnSync(process.execPath, [process.argv[1], ...serviceReinstallArgs()], {
258
+ stdio: svcStdio,
259
+ encoding: svcStdio === "pipe" ? "utf8" : undefined,
260
+ windowsHide: true,
261
+ });
262
+ if (svcStdio === "pipe") logSpawnOutput("", svc);
263
+ if (svc.status !== 0) console.warn("⚠️ Service refresh failed — run 'ocx service install' manually.");
264
+ } finally {
265
+ if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
266
+ else process.env.OCX_BAKE_PORT = prevBake;
267
+ }
241
268
  } else {
242
269
  console.log("Restart the proxy: ocx start");
243
270
  }
package/src/update/job.ts CHANGED
@@ -291,6 +291,12 @@ export interface RestartIo {
291
291
  waitForPort?: typeof waitForPortAvailable;
292
292
  spawnStart?: (job: UpdateJobState, installer: Installer, port?: number) => void;
293
293
  serviceInstalledFn?: () => boolean;
294
+ /** Service-mode install/reinstall command (defaults to spawnSync via runLoggedCommand). */
295
+ runService?: (
296
+ job: UpdateJobState,
297
+ bin: string,
298
+ args: string[],
299
+ ) => { status: number | null; signal?: NodeJS.Signals | null };
294
300
  }
295
301
 
296
302
  async function restartAfterUpdate(
@@ -313,10 +319,26 @@ async function restartAfterUpdate(
313
319
  } catch { /* fallback to default service install */ }
314
320
  }
315
321
  const cmd = restartCommand(serviceInstalled, job.installer, packageLauncherPath(), port, svcArgs);
322
+ const waitFn = io.waitForPort ?? waitForPortAvailable;
323
+
316
324
  if (serviceInstalled) {
317
- const result = runLoggedCommand(job, cmd.bin, cmd.args, RESTART_TIMEOUT_MS);
318
- if (result.status !== 0) {
319
- throw new Error(`service restart failed (${cmd.display}, exit ${result.status ?? "?"})`);
325
+ // Stop-first update already unloaded the service; wait for the socket to drain,
326
+ // then reinstall wrappers that bake `--port` via OCX_BAKE_PORT (PR #152 gap).
327
+ const freed = await waitFn(port, hostname, { timeoutMs: 5_000, intervalMs: 25 });
328
+ if (!freed) {
329
+ updateJob(job, {}, `Port ${port} still busy after stop; reinstalling service with pinned --port ${port} anyway.`);
330
+ }
331
+ const prevBake = process.env.OCX_BAKE_PORT;
332
+ process.env.OCX_BAKE_PORT = String(Math.trunc(port));
333
+ try {
334
+ const run = io.runService ?? ((j, bin, args) => runLoggedCommand(j, bin, args, RESTART_TIMEOUT_MS));
335
+ const result = run(job, cmd.bin, cmd.args);
336
+ if (result.status !== 0) {
337
+ throw new Error(`service restart failed (${cmd.display}, exit ${result.status ?? "?"})`);
338
+ }
339
+ } finally {
340
+ if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
341
+ else process.env.OCX_BAKE_PORT = prevBake;
320
342
  }
321
343
  return;
322
344
  }
@@ -329,8 +351,7 @@ async function restartAfterUpdate(
329
351
  // The old socket can stay busy briefly after stop (Windows taskkill drain, or the
330
352
  // stop-first update path that already killed the proxy before we got here) — wait
331
353
  // unconditionally on the captured port so the pinned start does not race the drain.
332
- const waitFn = io.waitForPort ?? waitForPortAvailable;
333
- const freed = await waitFn(port, hostname, { timeoutMs: 2000, intervalMs: 25 });
354
+ const freed = await waitFn(port, hostname, { timeoutMs: 2_000, intervalMs: 25 });
334
355
  if (!freed) {
335
356
  updateJob(job, {}, `Port ${port} still busy after stop; starting with --port ${port} anyway.`);
336
357
  }
@@ -352,11 +373,17 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
352
373
  const now = new Date().toISOString();
353
374
  // Capture the live listen target BEFORE the update command runs: the stop-first update
354
375
  // flow clears pid/runtime state, so this is the last moment the real port is knowable.
376
+ // Only trust runtime-port.json when its pid matches the live pidfile process.
355
377
  const rt = readRuntimePort();
378
+ const livePid = readPid();
356
379
  const preUpdateConfig = loadConfig();
380
+ const runtimeTrusted = !!(rt && livePid && rt.pid === livePid);
381
+ const configPort = typeof preUpdateConfig.port === "number" && preUpdateConfig.port > 0
382
+ ? preUpdateConfig.port
383
+ : 10100;
357
384
  const captured = {
358
- port: rt?.port ?? preUpdateConfig.port ?? 10100,
359
- hostname: rt?.hostname ?? preUpdateConfig.hostname ?? "127.0.0.1",
385
+ port: runtimeTrusted ? rt.port : configPort,
386
+ hostname: (runtimeTrusted ? rt.hostname : undefined) ?? preUpdateConfig.hostname ?? "127.0.0.1",
360
387
  };
361
388
  if (!job) {
362
389
  job = {
package/src/usage/log.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
1
+ import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getConfigDir } from "../config";
4
4
  import { usageDisplayTotalTokens } from "./totals";
@@ -39,6 +39,14 @@ export interface PersistedUsageEntry {
39
39
  surface?: "claude";
40
40
  resolvedModel?: string;
41
41
  requestedModel?: string;
42
+ /** Reasoning effort / service-tier metadata for GUI Logs after restart. */
43
+ requestedEffort?: string;
44
+ requestedServiceTier?: string;
45
+ requestedSpeedLabel?: string;
46
+ configuredServiceTier?: string;
47
+ configuredSpeedLabel?: string;
48
+ modelSupportsServiceTier?: boolean;
49
+ responseServiceTier?: string;
42
50
  status: number;
43
51
  durationMs: number;
44
52
  /** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
@@ -197,6 +205,11 @@ function normalizedAttempts(raw: unknown): PersistedUsageAttempt[] {
197
205
  .filter((attempt): attempt is PersistedUsageAttempt => attempt !== null);
198
206
  }
199
207
 
208
+ const MAX_METADATA_STRING_LEN = 64;
209
+ function capMetadataString(s: string): string {
210
+ return s.length > MAX_METADATA_STRING_LEN ? s.slice(0, MAX_METADATA_STRING_LEN) : s;
211
+ }
212
+
200
213
  function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
201
214
  const attempts = normalizedAttempts(entry.attempts);
202
215
  return {
@@ -207,6 +220,27 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
207
220
  ...(entry.surface === "claude" ? { surface: entry.surface } : {}),
208
221
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
209
222
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
223
+ ...(typeof entry.requestedEffort === "string" && entry.requestedEffort
224
+ ? { requestedEffort: capMetadataString(entry.requestedEffort) }
225
+ : {}),
226
+ ...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
227
+ ? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
228
+ : {}),
229
+ ...(typeof entry.requestedSpeedLabel === "string" && entry.requestedSpeedLabel
230
+ ? { requestedSpeedLabel: capMetadataString(entry.requestedSpeedLabel) }
231
+ : {}),
232
+ ...(typeof entry.configuredServiceTier === "string" && entry.configuredServiceTier
233
+ ? { configuredServiceTier: capMetadataString(entry.configuredServiceTier) }
234
+ : {}),
235
+ ...(typeof entry.configuredSpeedLabel === "string" && entry.configuredSpeedLabel
236
+ ? { configuredSpeedLabel: capMetadataString(entry.configuredSpeedLabel) }
237
+ : {}),
238
+ ...(typeof entry.modelSupportsServiceTier === "boolean"
239
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
240
+ : {}),
241
+ ...(typeof entry.responseServiceTier === "string" && entry.responseServiceTier
242
+ ? { responseServiceTier: capMetadataString(entry.responseServiceTier) }
243
+ : {}),
210
244
  status: entry.status,
211
245
  durationMs: entry.durationMs,
212
246
  ...(isNonNegativeFiniteNumber(entry.firstOutputMs)
@@ -254,3 +288,66 @@ export function readUsageEntries(): PersistedUsageEntry[] {
254
288
  }
255
289
  return entries;
256
290
  }
291
+
292
+ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
293
+ const entries: PersistedUsageEntry[] = [];
294
+ for (const line of lines) {
295
+ if (!line.trim()) continue;
296
+ try {
297
+ const parsed = JSON.parse(line) as PersistedUsageEntry;
298
+ if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") {
299
+ entries.push(normalizeUsageEntry(parsed));
300
+ }
301
+ } catch {
302
+ /* skip partial / hand-edited lines */
303
+ }
304
+ }
305
+ return entries;
306
+ }
307
+
308
+ /**
309
+ * Read only the newest `limit` usage.jsonl rows without loading the whole append-only
310
+ * file into memory. Used by request-log hydration on `ocx start`.
311
+ */
312
+ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
313
+ if (!Number.isFinite(limit) || limit <= 0) return [];
314
+ const path = usageLogPath();
315
+ if (!existsSync(path)) return [];
316
+ let fd: number | undefined;
317
+ try {
318
+ fd = openSync(path, "r");
319
+ const size = fstatSync(fd).size;
320
+ if (size <= 0) return [];
321
+ // ~4 KiB/row budget with a floor; expand once if the window yields too few lines.
322
+ let windowBytes = Math.min(size, Math.max(64 * 1024, Math.ceil(limit) * 4 * 1024));
323
+ for (let attempt = 0; attempt < 2; attempt++) {
324
+ const start = Math.max(0, size - windowBytes);
325
+ const buf = Buffer.alloc(size - start);
326
+ readSync(fd, buf, 0, buf.length, start);
327
+ let text = buf.toString("utf-8");
328
+ if (start > 0) {
329
+ const nl = text.indexOf("\n");
330
+ if (nl < 0) {
331
+ if (start === 0) break;
332
+ windowBytes = Math.min(size, windowBytes * 4);
333
+ continue;
334
+ }
335
+ text = text.slice(nl + 1);
336
+ }
337
+ const lines = text.split(/\r?\n/).filter(line => line.trim());
338
+ // Parse ALL lines first, then take the last N valid entries. This way corrupt
339
+ // or partial lines are filtered out during parsing and we always return the
340
+ // most recent N valid rows (not N physical lines minus corrupt ones).
341
+ const entries = parseUsageLines(lines);
342
+ if (entries.length >= limit || start === 0 || windowBytes >= size) return entries.slice(-limit);
343
+ windowBytes = Math.min(size, windowBytes * 4);
344
+ }
345
+ return [];
346
+ } catch {
347
+ return [];
348
+ } finally {
349
+ if (fd !== undefined) {
350
+ try { closeSync(fd); } catch { /* ignore */ }
351
+ }
352
+ }
353
+ }