modelstat 0.4.1 → 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/dist/cli.mjs CHANGED
@@ -34544,7 +34544,8 @@ function load() {
34544
34544
  apiUrl: obj.apiUrl ?? DEFAULTS.apiUrl,
34545
34545
  cursor: obj.cursor ?? {},
34546
34546
  segmentsSent: obj.segmentsSent ?? 0,
34547
- processingVersion: obj.processingVersion ?? null
34547
+ processingVersion: obj.processingVersion ?? null,
34548
+ reconcileCache: obj.reconcileCache ?? {}
34548
34549
  };
34549
34550
  } catch {
34550
34551
  cache2 = { ...DEFAULTS, cursor: {} };
@@ -34566,7 +34567,8 @@ var init_runtime_state = __esm({
34566
34567
  apiUrl: "",
34567
34568
  cursor: {},
34568
34569
  segmentsSent: 0,
34569
- processingVersion: null
34570
+ processingVersion: null,
34571
+ reconcileCache: {}
34570
34572
  };
34571
34573
  cache2 = null;
34572
34574
  runtimeState = {
@@ -34593,6 +34595,29 @@ var init_runtime_state = __esm({
34593
34595
  s.cursor = {};
34594
34596
  persist(s);
34595
34597
  },
34598
+ /** Drop ONE file's cursor so the next scan re-reads it — the precise lever the
34599
+ * self-healing reconcile pulls for the files of sessions the server is missing. */
34600
+ clearCursor(path) {
34601
+ const s = load();
34602
+ if (path in s.cursor) {
34603
+ delete s.cursor[path];
34604
+ persist(s);
34605
+ }
34606
+ },
34607
+ /** Drop cursors for files no longer present so the map tracks the CURRENT file
34608
+ * set, not every file ever seen. Returns how many were pruned. */
34609
+ pruneCursors(present) {
34610
+ const s = load();
34611
+ let removed = 0;
34612
+ for (const p of Object.keys(s.cursor)) {
34613
+ if (!present.has(p)) {
34614
+ delete s.cursor[p];
34615
+ removed += 1;
34616
+ }
34617
+ }
34618
+ if (removed) persist(s);
34619
+ return removed;
34620
+ },
34596
34621
  getSegmentsSent() {
34597
34622
  return load().segmentsSent;
34598
34623
  },
@@ -34610,6 +34635,15 @@ var init_runtime_state = __esm({
34610
34635
  s.processingVersion = v;
34611
34636
  persist(s);
34612
34637
  },
34638
+ /** Self-healing reconcile cache (see {@link RuntimeState.reconcileCache}). */
34639
+ getReconcileCache() {
34640
+ return load().reconcileCache;
34641
+ },
34642
+ setReconcileCache(c) {
34643
+ const s = load();
34644
+ s.reconcileCache = c;
34645
+ persist(s);
34646
+ },
34613
34647
  /** Test-only: drop the in-memory cache so the next read hits disk. */
34614
34648
  _resetCacheForTests() {
34615
34649
  cache2 = null;
@@ -34884,6 +34918,25 @@ async function uploadBatch(batch) {
34884
34918
  batch_id: result2.response.batch_id
34885
34919
  };
34886
34920
  }
34921
+ async function backfillGet(query) {
34922
+ const bearer = state.bearer;
34923
+ if (!bearer) return null;
34924
+ const res = await (0, import_undici.request)(`${state.apiUrl}/v1/backfill/digests${query}`, {
34925
+ method: "GET",
34926
+ headers: { authorization: `Bearer ${bearer}` }
34927
+ });
34928
+ if (res.statusCode >= 300) {
34929
+ await res.body.dump();
34930
+ return null;
34931
+ }
34932
+ return await res.body.json();
34933
+ }
34934
+ function fetchBackfillDays() {
34935
+ return backfillGet("");
34936
+ }
34937
+ function fetchBackfillDaySessions(day) {
34938
+ return backfillGet(`?day=${encodeURIComponent(day)}`);
34939
+ }
34887
34940
  var import_undici, DeviceMeUnauthorized, _ingest;
34888
34941
  var init_api = __esm({
34889
34942
  "src/api.ts"() {
@@ -36836,9 +36889,7 @@ import { join as join7 } from "path";
36836
36889
  function withNonNullTokens(e) {
36837
36890
  return e.tokens ? e : { ...e, tokens: { ...ZERO_TOKENS } };
36838
36891
  }
36839
- async function scanAll(cb = {}) {
36840
- const deviceId = state.deviceId;
36841
- if (!deviceId) throw new Error("daemon not enrolled \u2014 run `register` first");
36892
+ async function discoverJobs(deviceId) {
36842
36893
  const jobs = [];
36843
36894
  try {
36844
36895
  const base = join7(homedir5(), ".claude/projects");
@@ -36853,8 +36904,8 @@ async function scanAll(cb = {}) {
36853
36904
  const full = join7(dir, f);
36854
36905
  jobs.push({
36855
36906
  path: full,
36856
- parse: async (sink2) => {
36857
- const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full, onEvents: sink2 });
36907
+ parse: async (sink) => {
36908
+ const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full, onEvents: sink });
36858
36909
  return { toolCalls: r.toolCalls ?? [], scriptContexts: r.scriptContexts ?? [] };
36859
36910
  }
36860
36911
  });
@@ -36877,8 +36928,8 @@ async function scanAll(cb = {}) {
36877
36928
  const full = join7(base, y, m, d, f);
36878
36929
  jobs.push({
36879
36930
  path: full,
36880
- parse: async (sink2) => {
36881
- const r = await parseCodexRollout({ deviceId, sourceFile: full, onEvents: sink2 });
36931
+ parse: async (sink) => {
36932
+ const r = await parseCodexRollout({ deviceId, sourceFile: full, onEvents: sink });
36882
36933
  return { toolCalls: r.toolCalls ?? [], scriptContexts: r.scriptContexts ?? [] };
36883
36934
  }
36884
36935
  });
@@ -36889,6 +36940,12 @@ async function scanAll(cb = {}) {
36889
36940
  } catch (e) {
36890
36941
  console.warn("codex scan skipped:", e.message);
36891
36942
  }
36943
+ return jobs;
36944
+ }
36945
+ async function scanAll(cb = {}) {
36946
+ const deviceId = state.deviceId;
36947
+ if (!deviceId) throw new Error("daemon not enrolled \u2014 run `register` first");
36948
+ const jobs = await discoverJobs(deviceId);
36892
36949
  const ordered = (await Promise.all(
36893
36950
  jobs.map(async (j) => ({
36894
36951
  job: j,
@@ -37029,7 +37086,7 @@ var init_scan = __esm({
37029
37086
  init_api();
37030
37087
  init_config2();
37031
37088
  init_pipeline2();
37032
- DAEMON_VERSION = true ? "daemon-0.4.1" : "daemon-dev";
37089
+ DAEMON_VERSION = true ? "daemon-0.5.0" : "daemon-dev";
37033
37090
  BATCH_MAX_EVENTS = INGEST_BATCH_MAX_EVENTS;
37034
37091
  BATCH_MAX_TOOL_CALLS = 2e4;
37035
37092
  BATCH_BUFFER_HARD_CAP = BATCH_MAX_EVENTS * 2;
@@ -37182,6 +37239,117 @@ var init_lock = __esm({
37182
37239
  }
37183
37240
  });
37184
37241
 
37242
+ // src/reconcile.ts
37243
+ import { stat as stat3 } from "fs/promises";
37244
+ function utcDay(ts) {
37245
+ const d = new Date(ts);
37246
+ return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
37247
+ }
37248
+ async function reconcileBackfill(requestScan2) {
37249
+ const deviceId = state.deviceId;
37250
+ if (!deviceId) return null;
37251
+ const jobs = await discoverJobs(deviceId);
37252
+ const present = new Set(jobs.map((j) => j.path));
37253
+ const cache3 = runtimeState.getReconcileCache();
37254
+ let filesParsed = 0;
37255
+ for (const job of jobs) {
37256
+ const mtime = (await stat3(job.path).catch(() => null))?.mtimeMs ?? 0;
37257
+ const hit = cache3[job.path];
37258
+ if (hit && hit.mtime === mtime) continue;
37259
+ const perDaySession = {};
37260
+ try {
37261
+ await job.parse(async (chunk) => {
37262
+ for (const e of chunk) {
37263
+ const bySession = perDaySession[utcDay(e.ts)] ??= {};
37264
+ bySession[e.session_id] = (bySession[e.session_id] ?? 0) + 1;
37265
+ }
37266
+ });
37267
+ cache3[job.path] = { mtime, perDaySession };
37268
+ filesParsed += 1;
37269
+ } catch (e) {
37270
+ console.warn(` ! reconcile parse failed for ${job.path}:`, e.message);
37271
+ }
37272
+ }
37273
+ for (const p of Object.keys(cache3)) if (!present.has(p)) delete cache3[p];
37274
+ runtimeState.setReconcileCache(cache3);
37275
+ runtimeState.pruneCursors(present);
37276
+ const localDay = /* @__PURE__ */ new Map();
37277
+ const localDaySession = /* @__PURE__ */ new Map();
37278
+ const filesOf = /* @__PURE__ */ new Map();
37279
+ let localEvents = 0;
37280
+ for (const [path, entry] of Object.entries(cache3)) {
37281
+ for (const [day, sessions] of Object.entries(entry.perDaySession)) {
37282
+ let ds = localDaySession.get(day);
37283
+ if (!ds) {
37284
+ ds = /* @__PURE__ */ new Map();
37285
+ localDaySession.set(day, ds);
37286
+ }
37287
+ for (const [sid, n] of Object.entries(sessions)) {
37288
+ localEvents += n;
37289
+ localDay.set(day, (localDay.get(day) ?? 0) + n);
37290
+ ds.set(sid, (ds.get(sid) ?? 0) + n);
37291
+ const key = `${day}\0${sid}`;
37292
+ let fs2 = filesOf.get(key);
37293
+ if (!fs2) {
37294
+ fs2 = /* @__PURE__ */ new Set();
37295
+ filesOf.set(key, fs2);
37296
+ }
37297
+ fs2.add(path);
37298
+ }
37299
+ }
37300
+ }
37301
+ const serverDays = await fetchBackfillDays();
37302
+ if (!serverDays) return null;
37303
+ const base = {
37304
+ inSync: true,
37305
+ localEvents,
37306
+ serverEvents: serverDays.total_events,
37307
+ filesParsed,
37308
+ daysChecked: 0,
37309
+ sessionsShort: 0,
37310
+ filesInvalidated: 0
37311
+ };
37312
+ if (localEvents <= serverDays.total_events) return base;
37313
+ const serverDayMap = new Map(serverDays.days.map((d) => [d.day, d.events]));
37314
+ const filesToReship = /* @__PURE__ */ new Set();
37315
+ let sessionsShort = 0;
37316
+ let daysChecked = 0;
37317
+ for (const [day, localCount] of localDay) {
37318
+ if (localCount <= (serverDayMap.get(day) ?? 0)) continue;
37319
+ daysChecked += 1;
37320
+ const serverSessions = await fetchBackfillDaySessions(day);
37321
+ const have = new Map((serverSessions?.sessions ?? []).map((s) => [s.session_id, s.events]));
37322
+ for (const [sid, n] of localDaySession.get(day) ?? []) {
37323
+ if (n > (have.get(sid) ?? 0)) {
37324
+ sessionsShort += 1;
37325
+ for (const f of filesOf.get(`${day}\0${sid}`) ?? []) filesToReship.add(f);
37326
+ }
37327
+ }
37328
+ }
37329
+ if (filesToReship.size === 0) return { ...base, daysChecked };
37330
+ for (const f of filesToReship) runtimeState.clearCursor(f);
37331
+ console.log(
37332
+ `[modelstat] self-heal: server short ${sessionsShort} session(s) across ${daysChecked} day(s); re-shipping ${filesToReship.size} file(s) from local logs`
37333
+ );
37334
+ await requestScan2("self-heal");
37335
+ return {
37336
+ ...base,
37337
+ inSync: false,
37338
+ daysChecked,
37339
+ sessionsShort,
37340
+ filesInvalidated: filesToReship.size
37341
+ };
37342
+ }
37343
+ var init_reconcile = __esm({
37344
+ "src/reconcile.ts"() {
37345
+ "use strict";
37346
+ init_api();
37347
+ init_config2();
37348
+ init_runtime_state();
37349
+ init_scan();
37350
+ }
37351
+ });
37352
+
37185
37353
  // src/single-flight.ts
37186
37354
  function createCoalescingRunner(task) {
37187
37355
  let running = false;
@@ -37491,7 +37659,7 @@ var init_policies2 = __esm({
37491
37659
  });
37492
37660
 
37493
37661
  // ../../node_modules/.pnpm/readdirp@4.1.2/node_modules/readdirp/esm/index.js
37494
- import { stat as stat3, lstat, readdir as readdir2, realpath } from "fs/promises";
37662
+ import { stat as stat4, lstat, readdir as readdir2, realpath } from "fs/promises";
37495
37663
  import { Readable } from "stream";
37496
37664
  import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
37497
37665
  function readdirp(root, options = {}) {
@@ -37578,7 +37746,7 @@ var init_esm = __esm({
37578
37746
  const { root, type } = opts;
37579
37747
  this._fileFilter = normalizeFilter(opts.fileFilter);
37580
37748
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
37581
- const statMethod = opts.lstat ? lstat : stat3;
37749
+ const statMethod = opts.lstat ? lstat : stat4;
37582
37750
  if (wantBigintFsStats) {
37583
37751
  this._stat = (path) => statMethod(path, { bigint: true });
37584
37752
  } else {
@@ -37717,7 +37885,7 @@ var init_esm = __esm({
37717
37885
 
37718
37886
  // ../../node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/handler.js
37719
37887
  import { watchFile, unwatchFile, watch as fs_watch } from "fs";
37720
- import { open, stat as stat4, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
37888
+ import { open, stat as stat5, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
37721
37889
  import * as sysPath from "path";
37722
37890
  import { type as osType } from "os";
37723
37891
  function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
@@ -37765,7 +37933,7 @@ var init_handler = __esm({
37765
37933
  };
37766
37934
  EV = EVENTS;
37767
37935
  THROTTLE_MODE_WATCH = "watch";
37768
- statMethods = { lstat: lstat2, stat: stat4 };
37936
+ statMethods = { lstat: lstat2, stat: stat5 };
37769
37937
  KEY_LISTENERS = "listeners";
37770
37938
  KEY_ERR = "errHandlers";
37771
37939
  KEY_RAW = "rawEmitters";
@@ -38234,7 +38402,7 @@ var init_handler = __esm({
38234
38402
  return;
38235
38403
  if (!newStats || newStats.mtimeMs === 0) {
38236
38404
  try {
38237
- const newStats2 = await stat4(file);
38405
+ const newStats2 = await stat5(file);
38238
38406
  if (this.fsw.closed)
38239
38407
  return;
38240
38408
  const at = newStats2.atimeMs;
@@ -38486,7 +38654,7 @@ __export(esm_exports, {
38486
38654
  watch: () => watch
38487
38655
  });
38488
38656
  import { stat as statcb } from "fs";
38489
- import { stat as stat5, readdir as readdir3 } from "fs/promises";
38657
+ import { stat as stat6, readdir as readdir3 } from "fs/promises";
38490
38658
  import { EventEmitter } from "events";
38491
38659
  import * as sysPath2 from "path";
38492
38660
  function arrify(item) {
@@ -38957,7 +39125,7 @@ var init_esm2 = __esm({
38957
39125
  const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path) : path;
38958
39126
  let stats2;
38959
39127
  try {
38960
- stats2 = await stat5(fullPath);
39128
+ stats2 = await stat6(fullPath);
38961
39129
  } catch (err) {
38962
39130
  }
38963
39131
  if (!stats2 || this.closed)
@@ -39084,8 +39252,8 @@ var init_esm2 = __esm({
39084
39252
  }
39085
39253
  return this._userIgnored(path, stats);
39086
39254
  }
39087
- _isntIgnored(path, stat6) {
39088
- return !this._isIgnored(path, stat6);
39255
+ _isntIgnored(path, stat7) {
39256
+ return !this._isIgnored(path, stat7);
39089
39257
  }
39090
39258
  /**
39091
39259
  * Provides a set of common helpers and properties relating to symlink handling.
@@ -39300,12 +39468,12 @@ async function sendHeartbeat() {
39300
39468
  async function rotateRunawayLogs() {
39301
39469
  const { homedir: homedir9 } = await import("os");
39302
39470
  const { join: join14 } = await import("path");
39303
- const { open: open2, stat: stat6, truncate, writeFile: writeFile2 } = await import("fs/promises");
39471
+ const { open: open2, stat: stat7, truncate, writeFile: writeFile2 } = await import("fs/promises");
39304
39472
  const dir = join14(homedir9(), ".modelstat", "logs");
39305
39473
  for (const name of ["out.log", "err.log"]) {
39306
39474
  const p = join14(dir, name);
39307
39475
  try {
39308
- const st = await stat6(p);
39476
+ const st = await stat7(p);
39309
39477
  if (st.size <= LOG_MAX_BYTES) continue;
39310
39478
  const keep = Math.min(LOG_TAIL_KEEP_BYTES, st.size);
39311
39479
  const fh = await open2(p, "r");
@@ -39510,6 +39678,13 @@ async function runDaemon(opts = {}) {
39510
39678
  backstop.unref();
39511
39679
  const discoveryTimer = setInterval(() => void runDiscovery(), DISCOVERY_INTERVAL_MS);
39512
39680
  discoveryTimer.unref();
39681
+ const RECONCILE_INTERVAL_MS = 30 * 6e4;
39682
+ const reconcileTimer = setInterval(
39683
+ () => void reconcileBackfill(requestScan),
39684
+ RECONCILE_INTERVAL_MS
39685
+ );
39686
+ reconcileTimer.unref();
39687
+ setTimeout(() => void reconcileBackfill(requestScan), 6e4).unref();
39513
39688
  const shutdown = async () => {
39514
39689
  setPhase("offline", "Shutting down");
39515
39690
  await sendHeartbeat();
@@ -39537,9 +39712,10 @@ var init_daemon = __esm({
39537
39712
  init_config2();
39538
39713
  init_lock();
39539
39714
  init_machine_key();
39715
+ init_reconcile();
39540
39716
  init_scan();
39541
39717
  init_single_flight();
39542
- DAEMON_VERSION2 = true ? "daemon-0.4.1" : "daemon-dev";
39718
+ DAEMON_VERSION2 = true ? "daemon-0.5.0" : "daemon-dev";
39543
39719
  HEARTBEAT_INTERVAL_MS = 1e4;
39544
39720
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
39545
39721
  DISCOVERY_INTERVAL_MS = 6e4;
@@ -40140,7 +40316,7 @@ function tryOpenBrowser(url) {
40140
40316
  return false;
40141
40317
  }
40142
40318
  }
40143
- var DAEMON_VERSION3 = true ? "daemon-0.4.1" : "daemon-dev";
40319
+ var DAEMON_VERSION3 = true ? "daemon-0.5.0" : "daemon-dev";
40144
40320
  function osFamily() {
40145
40321
  const p = platform5();
40146
40322
  if (p === "darwin") return "macos";