@primitive.ai/prim 0.1.0-alpha.91 → 0.1.0-alpha.93

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.
Files changed (40) hide show
  1. package/README.md +34 -2
  2. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  3. package/dist/{chunk-VW3FDYRT.js → chunk-36TWVNBE.js} +6 -6
  4. package/dist/{chunk-HU4PW6I6.js → chunk-C3VAA5CG.js} +1 -1
  5. package/dist/{chunk-FOLKHXLJ.js → chunk-DCJN6LZH.js} +37 -5
  6. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  7. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  8. package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
  9. package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
  10. package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  11. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  12. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  13. package/dist/chunk-OYHYCK4K.js +13 -0
  14. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  15. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  16. package/dist/{chunk-EWZMDD44.js → chunk-X64HTHUF.js} +22 -3
  17. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  18. package/dist/daemon/server.js +382 -124
  19. package/dist/hook-runtime/daemon/server.js +382 -124
  20. package/dist/hook-runtime/hooks/post-commit.js +57 -39
  21. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  22. package/dist/hook-runtime/hooks/post-tool-use.js +111 -78
  23. package/dist/hook-runtime/hooks/pre-tool-use.js +48 -26
  24. package/dist/hook-runtime/hooks/prim-hook.js +157 -117
  25. package/dist/hook-runtime/hooks/session-end.js +1 -1
  26. package/dist/hook-runtime/hooks/session-start.js +38 -4
  27. package/dist/hook-runtime/index.js +1293 -526
  28. package/dist/hook-runtime/statusline-main.js +23 -1
  29. package/dist/hooks/post-commit.js +11 -8
  30. package/dist/hooks/post-rewrite.js +11 -8
  31. package/dist/hooks/post-tool-use.js +13 -15
  32. package/dist/hooks/pre-commit.js +4 -4
  33. package/dist/hooks/pre-tool-use.js +10 -12
  34. package/dist/hooks/prim-hook.js +17 -14
  35. package/dist/hooks/session-end.js +4 -4
  36. package/dist/hooks/session-start.js +12 -36
  37. package/dist/index.js +867 -291
  38. package/dist/statusline-main.js +23 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-BTNNEH2W.js +0 -79
@@ -52,9 +52,9 @@ var require_ucs2length = __commonJS({
52
52
  });
53
53
 
54
54
  // src/daemon/server.ts
55
- import { chmodSync as chmodSync6, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
55
+ import { chmodSync as chmodSync7, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync3 } from "fs";
56
56
  import { createServer } from "net";
57
- import { join as join13 } from "path";
57
+ import { join as join14 } from "path";
58
58
  import { fileURLToPath } from "url";
59
59
 
60
60
  // src/client.ts
@@ -1112,8 +1112,8 @@ async function getPinnedClient(options = {}) {
1112
1112
  }
1113
1113
 
1114
1114
  // src/flusher.ts
1115
- import { createReadStream, renameSync as renameSync3, unlinkSync } from "fs";
1116
- import { dirname as dirname7, join as join8 } from "path";
1115
+ import { createReadStream, renameSync as renameSync3, statSync as statSync2, unlinkSync } from "fs";
1116
+ import { dirname as dirname8, join as join9 } from "path";
1117
1117
 
1118
1118
  // src/dead-letter.ts
1119
1119
  import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
@@ -1213,6 +1213,95 @@ function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {})
1213
1213
  return record;
1214
1214
  }
1215
1215
 
1216
+ // src/drain-progress.ts
1217
+ import {
1218
+ chmodSync as chmodSync3,
1219
+ existsSync as existsSync4,
1220
+ readFileSync as readFileSync4,
1221
+ readdirSync,
1222
+ rmSync as rmSync5,
1223
+ rmdirSync
1224
+ } from "fs";
1225
+ import { basename, dirname as dirname5, join as join6 } from "path";
1226
+ var DIRECTORY_MODE3 = 448;
1227
+ var FILE_MODE3 = 384;
1228
+ var DRAIN_PROGRESS_DIRNAME = "drain-progress";
1229
+ var CHECKPOINT_NAME = /^(.+)\.json(?:\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp)?$/;
1230
+ function rotationIdentity(stat) {
1231
+ return { size: stat.size, ino: stat.ino, dev: stat.dev };
1232
+ }
1233
+ function drainProgressDirectory(flushingPath) {
1234
+ return join6(dirname5(flushingPath), DRAIN_PROGRESS_DIRNAME);
1235
+ }
1236
+ function drainProgressPath(flushingPath) {
1237
+ return join6(drainProgressDirectory(flushingPath), `${basename(flushingPath)}.json`);
1238
+ }
1239
+ function isByteCount(value) {
1240
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1241
+ }
1242
+ function drainResumeOffset(flushingPath, rotation) {
1243
+ let parsed;
1244
+ try {
1245
+ parsed = JSON.parse(readFileSync4(drainProgressPath(flushingPath), "utf8"));
1246
+ } catch {
1247
+ return 0;
1248
+ }
1249
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1250
+ return 0;
1251
+ }
1252
+ const { v, offset, size, ino, dev } = parsed;
1253
+ if (v !== 2 || ino !== rotation.ino || dev !== rotation.dev || size !== rotation.size || !isByteCount(offset) || offset > rotation.size) {
1254
+ return 0;
1255
+ }
1256
+ return offset;
1257
+ }
1258
+ function writeDrainCheckpoint(flushingPath, checkpoint) {
1259
+ const directory = drainProgressDirectory(flushingPath);
1260
+ ensureDurableDirectory(directory, DIRECTORY_MODE3);
1261
+ chmodSync3(directory, DIRECTORY_MODE3);
1262
+ atomicWriteFile(drainProgressPath(flushingPath), `${JSON.stringify(checkpoint)}
1263
+ `, {
1264
+ mode: FILE_MODE3
1265
+ });
1266
+ }
1267
+ function removeEmptyDirectory(directory) {
1268
+ try {
1269
+ rmdirSync(directory);
1270
+ } catch (error) {
1271
+ const code = error.code;
1272
+ if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST") {
1273
+ throw error;
1274
+ }
1275
+ }
1276
+ }
1277
+ function removeDrainCheckpoint(flushingPath) {
1278
+ rmSync5(drainProgressPath(flushingPath), { force: true });
1279
+ removeEmptyDirectory(drainProgressDirectory(flushingPath));
1280
+ }
1281
+ function sweepOrphanedDrainCheckpoints(bucketDirectory) {
1282
+ const directory = join6(bucketDirectory, DRAIN_PROGRESS_DIRNAME);
1283
+ let names;
1284
+ try {
1285
+ names = readdirSync(directory);
1286
+ } catch {
1287
+ return;
1288
+ }
1289
+ for (const name of names) {
1290
+ const rotation = CHECKPOINT_NAME.exec(name)?.[1];
1291
+ if (rotation === void 0 || existsSync4(join6(bucketDirectory, rotation))) {
1292
+ continue;
1293
+ }
1294
+ try {
1295
+ rmSync5(join6(directory, name), { force: true });
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ try {
1300
+ removeEmptyDirectory(directory);
1301
+ } catch {
1302
+ }
1303
+ }
1304
+
1216
1305
  // src/generated/cli-http-v1.validators.js
1217
1306
  var import_ucs2length = __toESM(require_ucs2length(), 1);
1218
1307
  var schema33 = { "type": "object", "properties": { "authenticated": { "type": "boolean", "const": true }, "organizationBindingVersion": { "type": "number", "const": 1 }, "captureAuthorityKind": { "type": "string", "enum": ["workos", "service_token"] }, "organizationId": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, "workosOrganizationId": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] } }, "required": ["authenticated", "organizationBindingVersion", "captureAuthorityKind", "organizationId", "workosOrganizationId"], "additionalProperties": false };
@@ -19003,15 +19092,15 @@ validate68.evaluated = { "props": true, "dynamicProps": false, "dynamicItems": f
19003
19092
 
19004
19093
  // src/lib/activation.ts
19005
19094
  import { execFileSync as execFileSync2 } from "child_process";
19006
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
19095
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
19007
19096
  import { homedir as homedir2 } from "os";
19008
- import { join as join6 } from "path";
19097
+ import { join as join7 } from "path";
19009
19098
 
19010
19099
  // src/lib/git.ts
19011
19100
  import { execFileSync } from "child_process";
19012
19101
  import { createHash as createHash4 } from "crypto";
19013
19102
  import { lstatSync, realpathSync } from "fs";
19014
- import { basename, dirname as dirname5, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
19103
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
19015
19104
  var GIT_TIMEOUT_MS = 1e3;
19016
19105
  var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
19017
19106
  var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
@@ -19121,10 +19210,10 @@ function hasProjectPrimInstall(cwd) {
19121
19210
  const root = gitToplevel(cwd);
19122
19211
  if (root === null || root === homedir2()) return false;
19123
19212
  for (const rel of PROJECT_INSTALL_FILES) {
19124
- const path = join6(root, rel);
19125
- if (existsSync4(path)) {
19213
+ const path = join7(root, rel);
19214
+ if (existsSync5(path)) {
19126
19215
  try {
19127
- const content = readFileSync4(path, "utf-8");
19216
+ const content = readFileSync5(path, "utf-8");
19128
19217
  if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
19129
19218
  } catch {
19130
19219
  }
@@ -19393,6 +19482,13 @@ function parseOrganizationBinding(value) {
19393
19482
  }
19394
19483
  };
19395
19484
  }
19485
+ function summarizeRetainedBuckets(retained) {
19486
+ const counts = /* @__PURE__ */ new Map();
19487
+ for (const item of retained) {
19488
+ counts.set(item.reason, (counts.get(item.reason) ?? 0) + 1);
19489
+ }
19490
+ return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([reason, count]) => `${reason}:${String(count)}`).join(", ");
19491
+ }
19396
19492
  function classifyJournalBuckets(buckets, binding) {
19397
19493
  const deliverableBuckets = /* @__PURE__ */ new Set();
19398
19494
  const retainedBuckets = [];
@@ -19467,17 +19563,17 @@ async function inspectJournalDelivery(buckets, options = {}) {
19467
19563
  import {
19468
19564
  appendFileSync,
19469
19565
  closeSync as closeSync3,
19470
- existsSync as existsSync5,
19566
+ existsSync as existsSync6,
19471
19567
  fstatSync,
19472
19568
  mkdirSync as mkdirSync5,
19473
19569
  openSync as openSync3,
19474
19570
  opendirSync,
19475
- readFileSync as readFileSync5,
19571
+ readFileSync as readFileSync6,
19476
19572
  readSync,
19477
- readdirSync
19573
+ readdirSync as readdirSync2
19478
19574
  } from "fs";
19479
- import { dirname as dirname6, join as join7 } from "path";
19480
- var JOURNAL_DIR = join7(primConfigDirectory(), "moves");
19575
+ import { dirname as dirname7, join as join8 } from "path";
19576
+ var JOURNAL_DIR = join8(primConfigDirectory(), "moves");
19481
19577
  var JOURNAL_BASENAME = "journal.ndjson";
19482
19578
  var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
19483
19579
  var PENDING_STATS_MAX_FILES = 128;
@@ -19489,7 +19585,7 @@ function envSlug(apiUrl) {
19489
19585
  return slug;
19490
19586
  }
19491
19587
  function currentEnvDir() {
19492
- return join7(JOURNAL_DIR, envSlug(getSiteUrl()));
19588
+ return join8(JOURNAL_DIR, envSlug(getSiteUrl()));
19493
19589
  }
19494
19590
  function parseMoves(content) {
19495
19591
  const moves = [];
@@ -19507,15 +19603,15 @@ function parseMoves(content) {
19507
19603
  function listBuckets() {
19508
19604
  const out = [];
19509
19605
  const envDir = currentEnvDir();
19510
- if (!existsSync5(envDir)) {
19606
+ if (!existsSync6(envDir)) {
19511
19607
  return out;
19512
19608
  }
19513
- for (const entry of readdirSync(envDir, { withFileTypes: true })) {
19609
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
19514
19610
  if (!entry.isDirectory()) {
19515
19611
  continue;
19516
19612
  }
19517
- const path = join7(envDir, entry.name, JOURNAL_BASENAME);
19518
- if (existsSync5(path)) {
19613
+ const path = join8(envDir, entry.name, JOURNAL_BASENAME);
19614
+ if (existsSync6(path)) {
19519
19615
  out.push({ bucket: entry.name, path });
19520
19616
  }
19521
19617
  }
@@ -19532,22 +19628,23 @@ function oldestCapturedAt(moves) {
19532
19628
  }
19533
19629
  return oldest;
19534
19630
  }
19535
- function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
19631
+ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES, options = {}) {
19536
19632
  const fd = openSync3(path, "r");
19537
19633
  try {
19538
19634
  const initial = fstatSync(fd);
19539
- const target = Math.min(initial.size, Math.max(0, maxBytes));
19635
+ const start = options.fromDrainCheckpoint ? drainResumeOffset(path, rotationIdentity(initial)) : 0;
19636
+ const target = Math.min(initial.size - start, Math.max(0, maxBytes));
19540
19637
  const buffer = Buffer.allocUnsafe(target);
19541
19638
  let sampledBytes = 0;
19542
19639
  while (sampledBytes < target) {
19543
- const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, sampledBytes);
19640
+ const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, start + sampledBytes);
19544
19641
  if (count === 0) {
19545
19642
  break;
19546
19643
  }
19547
19644
  sampledBytes += count;
19548
19645
  }
19549
19646
  const stat = fstatSync(fd);
19550
- const sampled = stat.size > sampledBytes;
19647
+ const sampled = stat.size - start > sampledBytes;
19551
19648
  let content = buffer.subarray(0, sampledBytes).toString("utf-8");
19552
19649
  if (sampled) {
19553
19650
  const lastNewline = content.lastIndexOf("\n");
@@ -19557,6 +19654,7 @@ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
19557
19654
  const moves = parseMoves(content);
19558
19655
  return {
19559
19656
  sizeBytes: stat.size,
19657
+ pendingBytes: Math.max(0, stat.size - start),
19560
19658
  mtimeMs: stat.mtimeMs,
19561
19659
  lineCount: sampled && stat.size > 0 ? Math.max(1, lines.length) : lines.length,
19562
19660
  oldestCapturedAt: oldestCapturedAt(moves),
@@ -19575,7 +19673,7 @@ function parseFlushingPid(name) {
19575
19673
  return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
19576
19674
  }
19577
19675
  function listFlushingInDir(dir, bucket, options = {}) {
19578
- if (!existsSync5(dir)) {
19676
+ if (!existsSync6(dir)) {
19579
19677
  return [];
19580
19678
  }
19581
19679
  const out = [];
@@ -19589,10 +19687,10 @@ function listFlushingInDir(dir, bucket, options = {}) {
19589
19687
  entry = entries.readSync();
19590
19688
  continue;
19591
19689
  }
19592
- const path = join7(dir, entry.name);
19690
+ const path = join8(dir, entry.name);
19593
19691
  let sample;
19594
19692
  try {
19595
- sample = sampleJournalFile(path, remainingBytes);
19693
+ sample = sampleJournalFile(path, remainingBytes, { fromDrainCheckpoint: true });
19596
19694
  } catch (err) {
19597
19695
  if (err.code === "ENOENT") {
19598
19696
  entry = entries.readSync();
@@ -19616,18 +19714,18 @@ function listFlushingInDir(dir, bucket, options = {}) {
19616
19714
  }
19617
19715
  function listFlushing(options = {}) {
19618
19716
  const envDir = currentEnvDir();
19619
- if (!existsSync5(envDir)) {
19717
+ if (!existsSync6(envDir)) {
19620
19718
  return [];
19621
19719
  }
19622
19720
  const out = [];
19623
19721
  let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
19624
19722
  const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
19625
- for (const entry of readdirSync(envDir, { withFileTypes: true })) {
19723
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
19626
19724
  if (out.length >= maxFiles) {
19627
19725
  break;
19628
19726
  }
19629
19727
  if (entry.isDirectory()) {
19630
- const found = listFlushingInDir(join7(envDir, entry.name), entry.name, {
19728
+ const found = listFlushingInDir(join8(envDir, entry.name), entry.name, {
19631
19729
  sampleBytes: remainingBytes,
19632
19730
  maxFiles: maxFiles - out.length
19633
19731
  });
@@ -19640,6 +19738,17 @@ function listFlushing(options = {}) {
19640
19738
  }
19641
19739
  return out;
19642
19740
  }
19741
+ function sweepOrphanedDrainProgress() {
19742
+ const envDir = currentEnvDir();
19743
+ if (!existsSync6(envDir)) {
19744
+ return;
19745
+ }
19746
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
19747
+ if (entry.isDirectory()) {
19748
+ sweepOrphanedDrainCheckpoints(join8(envDir, entry.name));
19749
+ }
19750
+ }
19751
+ }
19643
19752
  function bucketStats(options = {}) {
19644
19753
  const out = [];
19645
19754
  let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
@@ -19686,8 +19795,12 @@ function pendingJournalStats() {
19686
19795
 
19687
19796
  // src/flusher.ts
19688
19797
  var BATCH_SIZE = 500;
19798
+ var BATCH_MAX_BYTES = 1048576;
19689
19799
  var HTTP_TIMEOUT_MS = 1e4;
19690
19800
  var ORPHAN_QUARANTINE_MS = 6e4;
19801
+ function exceedsBatchBytes(count, bytes, nextBytes, maxBytes) {
19802
+ return count > 0 && bytes + nextBytes > maxBytes;
19803
+ }
19691
19804
  function addDrainCounts(left, right) {
19692
19805
  return {
19693
19806
  flushed: left.flushed + right.flushed,
@@ -19731,23 +19844,31 @@ function deadLetterReason(error) {
19731
19844
  }
19732
19845
  return void 0;
19733
19846
  }
19734
- async function* rawJournalLines(input) {
19847
+ function mayBeBatchSizeFailure(error) {
19848
+ if (error instanceof HttpError) {
19849
+ return error.status >= 500 && error.status !== 503;
19850
+ }
19851
+ return error instanceof Error && error.name === "TimeoutError";
19852
+ }
19853
+ async function* rawJournalLines(input, offset) {
19735
19854
  let pending = [];
19736
19855
  let pendingBytes = 0;
19856
+ let end = offset;
19737
19857
  for await (const value of input) {
19738
19858
  const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
19739
19859
  let start = 0;
19740
19860
  let newline = chunk.indexOf(10, start);
19741
19861
  while (newline !== -1) {
19742
19862
  const tail = chunk.subarray(start, newline + 1);
19743
- if (pending.length === 0) {
19744
- yield tail;
19745
- } else {
19863
+ let line = tail;
19864
+ if (pending.length > 0) {
19746
19865
  pending.push(tail);
19747
- yield Buffer.concat(pending, pendingBytes + tail.length);
19866
+ line = Buffer.concat(pending, pendingBytes + tail.length);
19748
19867
  pending = [];
19749
19868
  pendingBytes = 0;
19750
19869
  }
19870
+ end += line.length;
19871
+ yield { bytes: line, end };
19751
19872
  start = newline + 1;
19752
19873
  newline = chunk.indexOf(10, start);
19753
19874
  }
@@ -19758,21 +19879,46 @@ async function* rawJournalLines(input) {
19758
19879
  }
19759
19880
  }
19760
19881
  if (pendingBytes > 0) {
19761
- yield pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
19882
+ const bytes = pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
19883
+ yield { bytes, end: end + pendingBytes };
19762
19884
  }
19763
19885
  }
19886
+ var drainProgressWarned = false;
19764
19887
  async function drainFlushingPath(flushingPath, client2, binding, options = {}) {
19888
+ const rotation = rotationIdentity(statSync2(flushingPath));
19889
+ const resumeAt = drainResumeOffset(flushingPath, rotation);
19890
+ const recordProgress = (end) => {
19891
+ if (end === void 0 || end >= rotation.size) {
19892
+ return;
19893
+ }
19894
+ try {
19895
+ writeDrainCheckpoint(flushingPath, { v: 2, offset: end, ...rotation });
19896
+ drainProgressWarned = false;
19897
+ } catch (error) {
19898
+ if (!drainProgressWarned) {
19899
+ drainProgressWarned = true;
19900
+ process.stderr.write(
19901
+ `[prim] could not record drain progress: ${error instanceof Error ? error.message : String(error)}
19902
+ `
19903
+ );
19904
+ }
19905
+ }
19906
+ };
19907
+ const reportProgress = (delta) => {
19908
+ options.onProgress?.(delta);
19909
+ return delta;
19910
+ };
19765
19911
  const postBatch = async (batch2) => {
19912
+ const moves = batch2.map((entry) => entry.move);
19766
19913
  try {
19767
- const request2 = buildOrganizationBoundMoveRequest(batch2, binding);
19914
+ const request2 = buildOrganizationBoundMoveRequest(moves, binding);
19768
19915
  const response = await client2.post("/api/cli/moves/ingest", request2, {
19769
19916
  signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
19770
19917
  });
19771
- requireDurableIngestAcknowledgement(response, batch2.length, collectionScopeRoots(batch2));
19772
- return { flushed: batch2.length, quarantined: 0 };
19918
+ requireDurableIngestAcknowledgement(response, moves.length, collectionScopeRoots(moves));
19773
19919
  } catch (error) {
19774
19920
  const reason = deadLetterReason(error);
19775
- if (!reason) {
19921
+ if (!(reason || mayBeBatchSizeFailure(error))) {
19776
19922
  throw error;
19777
19923
  }
19778
19924
  if (batch2.length > 1) {
@@ -19781,33 +19927,46 @@ async function drainFlushingPath(flushingPath, client2, binding, options = {}) {
19781
19927
  const right = await postBatch(batch2.slice(midpoint));
19782
19928
  return addDrainCounts(left, right);
19783
19929
  }
19784
- const [move] = batch2;
19930
+ if (!reason) {
19931
+ throw error;
19932
+ }
19933
+ const [entry] = batch2;
19785
19934
  const quarantined = quarantineMove(
19786
19935
  flushingPath,
19787
- move,
19936
+ entry.move,
19788
19937
  reason,
19789
19938
  Date.now,
19790
19939
  options.deadLetterPersistence
19791
19940
  );
19941
+ recordProgress(entry.end);
19792
19942
  process.stderr.write(
19793
19943
  `[prim] quarantined rejected move ${quarantined.quarantineId.slice(0, 12)} (${reason})
19794
19944
  `
19795
19945
  );
19796
- return { flushed: 0, quarantined: 1 };
19946
+ return reportProgress({ flushed: 0, quarantined: 1 });
19797
19947
  }
19948
+ recordProgress(batch2.at(-1)?.end);
19949
+ return reportProgress({ flushed: batch2.length, quarantined: 0 });
19798
19950
  };
19799
- const input = createReadStream(flushingPath);
19951
+ const input = createReadStream(flushingPath, { start: resumeAt });
19800
19952
  const decoder = new TextDecoder("utf-8", { fatal: true });
19801
19953
  let batch = [];
19954
+ let batchBytes = 0;
19802
19955
  let counts = { flushed: 0, quarantined: 0 };
19956
+ const postPending = async () => {
19957
+ counts = addDrainCounts(counts, await postBatch(batch));
19958
+ batch = [];
19959
+ batchBytes = 0;
19960
+ };
19803
19961
  try {
19804
- for await (const rawLine of rawJournalLines(input)) {
19962
+ for await (const { bytes: rawLine, end } of rawJournalLines(input, resumeAt)) {
19805
19963
  if (rawLine.length === 1 && rawLine[0] === 10 || rawLine.length === 2 && rawLine[0] === 13 && rawLine[1] === 10) {
19806
19964
  continue;
19807
19965
  }
19966
+ let move;
19808
19967
  try {
19809
19968
  const line = decoder.decode(rawLine);
19810
- batch.push(JSON.parse(line));
19969
+ move = JSON.parse(line);
19811
19970
  } catch {
19812
19971
  const quarantined = quarantineRawLine(
19813
19972
  flushingPath,
@@ -19819,24 +19978,35 @@ async function drainFlushingPath(flushingPath, client2, binding, options = {}) {
19819
19978
  `[prim] quarantined invalid journal line ${quarantined.quarantineId.slice(0, 12)} (invalid_move)
19820
19979
  `
19821
19980
  );
19822
- counts.quarantined += 1;
19981
+ counts = addDrainCounts(counts, reportProgress({ flushed: 0, quarantined: 1 }));
19982
+ if (batch.length === 0) {
19983
+ recordProgress(end);
19984
+ }
19823
19985
  continue;
19824
19986
  }
19825
- if (batch.length === BATCH_SIZE) {
19826
- counts = addDrainCounts(counts, await postBatch(batch));
19827
- batch = [];
19987
+ if (exceedsBatchBytes(batch.length, batchBytes, rawLine.length, BATCH_MAX_BYTES)) {
19988
+ await postPending();
19989
+ }
19990
+ batch.push({ move, end });
19991
+ batchBytes += rawLine.length;
19992
+ if (batch.length >= BATCH_SIZE) {
19993
+ await postPending();
19828
19994
  }
19829
19995
  }
19830
19996
  if (batch.length > 0) {
19831
- counts = addDrainCounts(counts, await postBatch(batch));
19997
+ await postPending();
19832
19998
  }
19833
19999
  } finally {
19834
20000
  input.destroy();
19835
20001
  }
19836
20002
  unlinkSync(flushingPath);
20003
+ try {
20004
+ removeDrainCheckpoint(flushingPath);
20005
+ } catch {
20006
+ }
19837
20007
  return counts;
19838
20008
  }
19839
- async function drainPath(path, client2, binding) {
20009
+ async function drainPath(path, client2, binding, onProgress) {
19840
20010
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
19841
20011
  try {
19842
20012
  renameSync3(path, tmpPath);
@@ -19846,7 +20016,19 @@ async function drainPath(path, client2, binding) {
19846
20016
  }
19847
20017
  throw err;
19848
20018
  }
19849
- return drainFlushingPath(tmpPath, client2, binding);
20019
+ return drainFlushingPath(tmpPath, client2, binding, { onProgress });
20020
+ }
20021
+ async function settleDrain(drain) {
20022
+ let progress = { flushed: 0, quarantined: 0 };
20023
+ try {
20024
+ return {
20025
+ counts: await drain((delta) => {
20026
+ progress = addDrainCounts(progress, delta);
20027
+ })
20028
+ };
20029
+ } catch (error) {
20030
+ return { counts: progress, failure: { error } };
20031
+ }
19850
20032
  }
19851
20033
  function selectRecoverable(files, now, opts = {}) {
19852
20034
  const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
@@ -19863,7 +20045,8 @@ async function recoverOrphans(candidates, options) {
19863
20045
  flushed: 0,
19864
20046
  quarantined: 0,
19865
20047
  errors: [],
19866
- failedBuckets: /* @__PURE__ */ new Set()
20048
+ failedBuckets: /* @__PURE__ */ new Set(),
20049
+ failedDrainFlushed: 0
19867
20050
  };
19868
20051
  const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
19869
20052
  ownerPid: options.ownerPid ?? process.pid,
@@ -19874,28 +20057,41 @@ async function recoverOrphans(candidates, options) {
19874
20057
  if (summary.failedBuckets.has(file.bucket)) {
19875
20058
  continue;
19876
20059
  }
19877
- try {
19878
- const counts = await drain(file.path);
19879
- summary.flushed += counts.flushed;
19880
- summary.quarantined += counts.quarantined;
19881
- } catch (err) {
19882
- summary.errors.push(err);
20060
+ const { counts, failure } = await settleDrain((onProgress) => drain(file.path, onProgress));
20061
+ summary.flushed += counts.flushed;
20062
+ summary.quarantined += counts.quarantined;
20063
+ if (failure) {
20064
+ summary.failedDrainFlushed += counts.flushed;
20065
+ summary.errors.push(failure.error);
19883
20066
  summary.failedBuckets.add(file.bucket);
19884
20067
  }
19885
20068
  }
19886
20069
  return summary;
19887
20070
  }
19888
20071
  var FlushError = class extends Error {
20072
+ /** Moves the whole sweep acknowledged, across every drain it ran. */
19889
20073
  flushed;
19890
20074
  quarantined;
19891
- constructor(cause, flushed, quarantined) {
20075
+ /** Buckets the failed sweep held back without sending, as on success. */
20076
+ retained;
20077
+ /**
20078
+ * Of `flushed`, the Moves acknowledged by the drains that failed, before
20079
+ * they failed. Only this shows the failure itself is advancing: `flushed`
20080
+ * also counts other buckets' drains, so a bucket whose drain fails every
20081
+ * time would read as progress while a sibling bucket kept delivering.
20082
+ */
20083
+ failedDrainFlushed;
20084
+ constructor(cause, flushed, quarantined, retained = [], failedDrainFlushed = 0) {
19892
20085
  super(cause instanceof Error ? cause.message : String(cause), { cause });
19893
20086
  this.name = "FlushError";
19894
20087
  this.flushed = flushed;
19895
20088
  this.quarantined = quarantined;
20089
+ this.retained = retained;
20090
+ this.failedDrainFlushed = failedDrainFlushed;
19896
20091
  }
19897
20092
  };
19898
20093
  async function flushOnce() {
20094
+ sweepOrphanedDrainProgress();
19899
20095
  const orphanCandidates = listFlushing({ sampleBytes: 0 });
19900
20096
  const liveBuckets = listBuckets();
19901
20097
  if (orphanCandidates.length === 0 && liveBuckets.length === 0) {
@@ -19912,10 +20108,11 @@ async function flushOnce() {
19912
20108
  const binding = inspection.binding;
19913
20109
  const recovered = await recoverOrphans(
19914
20110
  orphanCandidates.filter((file) => inspection.deliverableBuckets.has(file.bucket)),
19915
- { drain: (path) => drainFlushingPath(path, client2, binding) }
20111
+ { drain: (path, onProgress) => drainFlushingPath(path, client2, binding, { onProgress }) }
19916
20112
  );
19917
20113
  let total = recovered.flushed;
19918
20114
  let quarantined = recovered.quarantined;
20115
+ let failedDrainFlushed = recovered.failedDrainFlushed;
19919
20116
  const errors = recovered.errors;
19920
20117
  for (const { bucket, path } of liveBuckets) {
19921
20118
  if (!inspection.deliverableBuckets.has(bucket)) {
@@ -19924,21 +20121,29 @@ async function flushOnce() {
19924
20121
  if (recovered.failedBuckets.has(bucket)) {
19925
20122
  continue;
19926
20123
  }
19927
- try {
19928
- const counts = await drainPath(path, client2, binding);
19929
- total += counts.flushed;
19930
- quarantined += counts.quarantined;
19931
- } catch (err) {
19932
- errors.push(err);
20124
+ const { counts, failure } = await settleDrain(
20125
+ (onProgress) => drainPath(path, client2, binding, onProgress)
20126
+ );
20127
+ total += counts.flushed;
20128
+ quarantined += counts.quarantined;
20129
+ if (failure) {
20130
+ failedDrainFlushed += counts.flushed;
20131
+ errors.push(failure.error);
19933
20132
  }
19934
20133
  }
19935
20134
  if (errors.length > 0) {
19936
- throw new FlushError(errors[0], total, quarantined);
20135
+ throw new FlushError(
20136
+ errors[0],
20137
+ total,
20138
+ quarantined,
20139
+ inspection.retainedBuckets,
20140
+ failedDrainFlushed
20141
+ );
19937
20142
  }
19938
20143
  return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
19939
20144
  }
19940
20145
  var flushInFlight;
19941
- var FLUSH_LOCK_PATH = join8(dirname7(JOURNAL_DIR), ".flush.lock");
20146
+ var FLUSH_LOCK_PATH = join9(dirname8(JOURNAL_DIR), ".flush.lock");
19942
20147
  var FLUSH_LOCK_TIMEOUT_MS = 250;
19943
20148
  function isFlushLockContended(error) {
19944
20149
  return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
@@ -19962,6 +20167,30 @@ function flush() {
19962
20167
  flushInFlight = attempt;
19963
20168
  return attempt;
19964
20169
  }
20170
+ function hasPendingDrainWork(stats) {
20171
+ return stats.pendingCount > 0 || stats.sampled || stats.strandedFileCount > 0;
20172
+ }
20173
+
20174
+ // src/lib/delivery-backlog.ts
20175
+ var MS_PER_SECOND = 1e3;
20176
+ var MS_PER_MINUTE = 60 * MS_PER_SECOND;
20177
+ var MS_PER_HOUR = 60 * MS_PER_MINUTE;
20178
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
20179
+ function isNonNegativeInteger(value) {
20180
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
20181
+ }
20182
+ function isPositiveInteger(value) {
20183
+ return isNonNegativeInteger(value) && value > 0;
20184
+ }
20185
+ function deliveryBacklogState(ingestion) {
20186
+ if (ingestion?.healthy !== false) return void 0;
20187
+ const failures = ingestion.consecutiveFailures;
20188
+ const failedWithoutProgress = failures !== 0 && !(isPositiveInteger(failures) && isPositiveInteger(ingestion.lastFailedDrainAcknowledgedCount));
20189
+ if (failedWithoutProgress) return "failing";
20190
+ const retained = ingestion.lastRetainedBucketCount;
20191
+ if (!isNonNegativeInteger(retained)) return "failing";
20192
+ return retained > 0 ? "retained" : "draining";
20193
+ }
19965
20194
 
19966
20195
  // src/lib/presence.ts
19967
20196
  var DECISION_ORIGIN = "https://app.getprimitive.ai";
@@ -20037,7 +20266,8 @@ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {
20037
20266
  if (snapshot.ingestion?.healthy === false) {
20038
20267
  const pending = snapshot.ingestion.pendingCount;
20039
20268
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
20040
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
20269
+ const delivery = deliveryBacklogState(snapshot.ingestion) === "draining" && snapshot.heartbeat?.healthy === true ? "draining" : "stalled";
20270
+ return `primitive ${version} (daemon: degraded \xB7 delivery: ${delivery}${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
20041
20271
  }
20042
20272
  if (snapshot.heartbeat?.healthy === false) {
20043
20273
  return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
@@ -20115,8 +20345,8 @@ var StatuslineIngestionCache = class {
20115
20345
 
20116
20346
  // src/daemon/client-instance-id.ts
20117
20347
  import { randomBytes as randomBytes3 } from "crypto";
20118
- import { chmodSync as chmodSync3, lstatSync as lstatSync2, readFileSync as readFileSync6 } from "fs";
20119
- import { join as join9 } from "path";
20348
+ import { chmodSync as chmodSync4, lstatSync as lstatSync2, readFileSync as readFileSync7 } from "fs";
20349
+ import { join as join10 } from "path";
20120
20350
  var CONFIG_DIRECTORY_MODE = 448;
20121
20351
  var INSTANCE_FILE_MODE = 384;
20122
20352
  var INSTANCE_ENTROPY_BYTES = 32;
@@ -20133,7 +20363,7 @@ function ensurePrivateConfigDirectory(configDir) {
20133
20363
  if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
20134
20364
  throw new Error("prim config path must be a private directory");
20135
20365
  }
20136
- chmodSync3(configDir, CONFIG_DIRECTORY_MODE);
20366
+ chmodSync4(configDir, CONFIG_DIRECTORY_MODE);
20137
20367
  syncDirectory(configDir);
20138
20368
  }
20139
20369
  function readExisting(path) {
@@ -20149,11 +20379,11 @@ function readExisting(path) {
20149
20379
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
20150
20380
  throw new Error("stored client instance identity is invalid");
20151
20381
  }
20152
- chmodSync3(path, INSTANCE_FILE_MODE);
20382
+ chmodSync4(path, INSTANCE_FILE_MODE);
20153
20383
  if (metadata.size > MAX_STORED_BYTES) {
20154
20384
  throw new Error("stored client instance identity is invalid");
20155
20385
  }
20156
- const raw = readFileSync6(path, "utf8");
20386
+ const raw = readFileSync7(path, "utf8");
20157
20387
  const value = raw.endsWith("\n") ? raw.slice(0, -1) : raw;
20158
20388
  if (raw !== value && raw !== `${value}
20159
20389
  ` || !isClientInstanceId(value)) {
@@ -20167,8 +20397,8 @@ function atomicWrite2(path, value) {
20167
20397
  }
20168
20398
  async function getOrCreateClientInstanceId(options) {
20169
20399
  const configDir = options.configDir;
20170
- const path = join9(configDir, "client_instance_id");
20171
- const lockPath = join9(configDir, "client-instance.lock");
20400
+ const path = join10(configDir, "client_instance_id");
20401
+ const lockPath = join10(configDir, "client-instance.lock");
20172
20402
  ensurePrivateConfigDirectory(configDir);
20173
20403
  return withFileLock(
20174
20404
  lockPath,
@@ -20192,7 +20422,7 @@ async function getOrCreateClientInstanceId(options) {
20192
20422
 
20193
20423
  // src/daemon/client.ts
20194
20424
  import { createConnection } from "net";
20195
- import { join as join10 } from "path";
20425
+ import { join as join11 } from "path";
20196
20426
 
20197
20427
  // src/daemon/principal.ts
20198
20428
  import { createHash as createHash5 } from "crypto";
@@ -20269,7 +20499,7 @@ function parseDaemonResponseEnvelope(value) {
20269
20499
  }
20270
20500
 
20271
20501
  // src/daemon/client.ts
20272
- var SOCK_PATH = join10(primConfigDirectory(), "sock");
20502
+ var SOCK_PATH = join11(primConfigDirectory(), "sock");
20273
20503
  var DEFAULT_TIMEOUT_MS2 = 250;
20274
20504
  var nextRequestId = 1;
20275
20505
  function daemonRequest(method, params = {}, opts = {}) {
@@ -20538,8 +20768,8 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
20538
20768
  }
20539
20769
 
20540
20770
  // src/daemon/health.ts
20541
- import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync6, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
20542
- import { dirname as dirname8, join as join11 } from "path";
20771
+ import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync6, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
20772
+ import { dirname as dirname9, join as join12 } from "path";
20543
20773
  var CONFIG_DIR_MODE2 = 448;
20544
20774
  var HEALTH_FILE_MODE = 384;
20545
20775
  var INGESTION_SLA_MS = 3e4;
@@ -20547,12 +20777,13 @@ var HEARTBEAT_FRESH_MS = 9e4;
20547
20777
  var HEARTBEAT_RETRY_CAP_MS = 3e4;
20548
20778
  var INGESTION_RETRY_BASE_MS = 5e3;
20549
20779
  var INGESTION_RETRY_CAP_MS = 15e3;
20550
- var DAEMON_HEALTH_PATH = join11(primConfigDirectory(), "daemon-health.json");
20551
- function createDaemonHealthState(version, pid, startedAt2) {
20780
+ var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
20781
+ function createDaemonHealthState(version, pid, startedAt2, siteUrl) {
20552
20782
  return {
20553
20783
  schemaVersion: 1,
20554
20784
  version,
20555
20785
  pid,
20786
+ siteUrl,
20556
20787
  startedAt: startedAt2,
20557
20788
  updatedAt: startedAt2,
20558
20789
  healthy: false,
@@ -20563,7 +20794,9 @@ function createDaemonHealthState(version, pid, startedAt2) {
20563
20794
  pendingCount: 0,
20564
20795
  pendingSampled: false,
20565
20796
  strandedCount: 0,
20566
- lastAcknowledgedCount: 0
20797
+ lastAcknowledgedCount: 0,
20798
+ lastFailedDrainAcknowledgedCount: 0,
20799
+ lastRetainedBucketCount: 0
20567
20800
  }
20568
20801
  };
20569
20802
  }
@@ -20585,37 +20818,37 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
20585
20818
  return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
20586
20819
  }
20587
20820
  function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
20588
- const dir = dirname8(path);
20589
- if (!existsSync6(dir)) {
20821
+ const dir = dirname9(path);
20822
+ if (!existsSync7(dir)) {
20590
20823
  mkdirSync6(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
20591
20824
  }
20592
- const tmp = join11(dir, `.${process.pid}.daemon-health.tmp`);
20825
+ const tmp = join12(dir, `.${process.pid}.daemon-health.tmp`);
20593
20826
  writeFileSync4(tmp, `${JSON.stringify(state, null, 2)}
20594
20827
  `, { mode: HEALTH_FILE_MODE });
20595
- chmodSync4(tmp, HEALTH_FILE_MODE);
20828
+ chmodSync5(tmp, HEALTH_FILE_MODE);
20596
20829
  renameSync4(tmp, path);
20597
20830
  }
20598
20831
 
20599
20832
  // src/daemon/instance-lock.ts
20600
20833
  import { randomUUID as randomUUID2 } from "crypto";
20601
20834
  import {
20602
- chmodSync as chmodSync5,
20603
- existsSync as existsSync7,
20835
+ chmodSync as chmodSync6,
20836
+ existsSync as existsSync8,
20604
20837
  mkdirSync as mkdirSync7,
20605
- readFileSync as readFileSync7,
20838
+ readFileSync as readFileSync8,
20606
20839
  renameSync as renameSync5,
20607
- rmSync as rmSync5,
20608
- statSync as statSync2,
20840
+ rmSync as rmSync6,
20841
+ statSync as statSync3,
20609
20842
  unlinkSync as unlinkSync2,
20610
20843
  writeFileSync as writeFileSync5
20611
20844
  } from "fs";
20612
- import { join as join12 } from "path";
20845
+ import { join as join13 } from "path";
20613
20846
  var DIR_MODE = 448;
20614
- var FILE_MODE3 = 384;
20847
+ var FILE_MODE4 = 384;
20615
20848
  var DAEMON_STARTUP_GRACE_MS = 5e3;
20616
20849
  function readLockRecord(path) {
20617
20850
  try {
20618
- const value = JSON.parse(readFileSync7(path, "utf-8"));
20851
+ const value = JSON.parse(readFileSync8(path, "utf-8"));
20619
20852
  if (Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
20620
20853
  return value;
20621
20854
  }
@@ -20625,23 +20858,23 @@ function readLockRecord(path) {
20625
20858
  }
20626
20859
  function numericPid(path) {
20627
20860
  try {
20628
- const pid = Number(readFileSync7(path, "utf-8").trim());
20861
+ const pid = Number(readFileSync8(path, "utf-8").trim());
20629
20862
  return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
20630
20863
  } catch {
20631
20864
  return;
20632
20865
  }
20633
20866
  }
20634
20867
  function readDaemonOwner(configDir) {
20635
- return readLockRecord(join12(configDir, "daemon.lock", "owner.json"));
20868
+ return readLockRecord(join13(configDir, "daemon.lock", "owner.json"));
20636
20869
  }
20637
20870
  function readDaemonPid(configDir) {
20638
- const path = join12(configDir, "daemon.pid");
20871
+ const path = join13(configDir, "daemon.pid");
20639
20872
  const pid = numericPid(path);
20640
20873
  if (pid === void 0) {
20641
20874
  return;
20642
20875
  }
20643
20876
  try {
20644
- return { pid, mtimeMs: statSync2(path).mtimeMs };
20877
+ return { pid, mtimeMs: statSync3(path).mtimeMs };
20645
20878
  } catch {
20646
20879
  return;
20647
20880
  }
@@ -20650,11 +20883,11 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20650
20883
  const pid = opts.pid ?? process.pid;
20651
20884
  const now = opts.now ?? Date.now();
20652
20885
  const isAlive = opts.isAlive ?? processIsAlive;
20653
- const lockDir = join12(configDir, "daemon.lock");
20654
- const ownerPath2 = join12(lockDir, "owner.json");
20655
- const pidPath = join12(configDir, "daemon.pid");
20886
+ const lockDir = join13(configDir, "daemon.lock");
20887
+ const ownerPath2 = join13(lockDir, "owner.json");
20888
+ const pidPath = join13(configDir, "daemon.pid");
20656
20889
  const instanceId = randomUUID2();
20657
- if (!existsSync7(configDir)) {
20890
+ if (!existsSync8(configDir)) {
20658
20891
  mkdirSync7(configDir, { recursive: true, mode: DIR_MODE });
20659
20892
  }
20660
20893
  for (; ; ) {
@@ -20672,7 +20905,7 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20672
20905
  if (!existing) {
20673
20906
  let mtimeMs;
20674
20907
  try {
20675
- mtimeMs = statSync2(lockDir).mtimeMs;
20908
+ mtimeMs = statSync3(lockDir).mtimeMs;
20676
20909
  } catch (statErr) {
20677
20910
  if (statErr.code === "ENOENT") {
20678
20911
  continue;
@@ -20686,7 +20919,7 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20686
20919
  const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
20687
20920
  try {
20688
20921
  renameSync5(lockDir, stale);
20689
- rmSync5(stale, { recursive: true, force: true });
20922
+ rmSync6(stale, { recursive: true, force: true });
20690
20923
  } catch (renameErr) {
20691
20924
  if (renameErr.code !== "ENOENT") {
20692
20925
  throw renameErr;
@@ -20704,15 +20937,15 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20704
20937
  };
20705
20938
  const record = { pid, instanceId, startedAt: now };
20706
20939
  writeFileSync5(ownerPath2, `${JSON.stringify(record)}
20707
- `, { mode: FILE_MODE3 });
20708
- chmodSync5(ownerPath2, FILE_MODE3);
20940
+ `, { mode: FILE_MODE4 });
20941
+ chmodSync6(ownerPath2, FILE_MODE4);
20709
20942
  const legacyPid = numericPid(pidPath);
20710
20943
  if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
20711
- rmSync5(lockDir, { recursive: true, force: true });
20944
+ rmSync6(lockDir, { recursive: true, force: true });
20712
20945
  throw new Error(`daemon already running (pid=${String(legacyPid)})`);
20713
20946
  }
20714
- writeFileSync5(pidPath, String(pid), { mode: FILE_MODE3 });
20715
- chmodSync5(pidPath, FILE_MODE3);
20947
+ writeFileSync5(pidPath, String(pid), { mode: FILE_MODE4 });
20948
+ chmodSync6(pidPath, FILE_MODE4);
20716
20949
  return ownership2;
20717
20950
  }
20718
20951
  function ownsDaemonInstance(ownership2) {
@@ -20742,7 +20975,7 @@ function releaseDaemonOwnership(ownership2, socketPath) {
20742
20975
  }
20743
20976
  }
20744
20977
  }
20745
- rmSync5(ownership2.lockDir, { recursive: true, force: true });
20978
+ rmSync6(ownership2.lockDir, { recursive: true, force: true });
20746
20979
  if (cleanupError) {
20747
20980
  throw cleanupError;
20748
20981
  }
@@ -20837,7 +21070,7 @@ function parseStatuslineRequest(buffer) {
20837
21070
 
20838
21071
  // src/daemon/server.ts
20839
21072
  var CONFIG_DIR = primConfigDirectory();
20840
- var SOCK_PATH2 = join13(CONFIG_DIR, "sock");
21073
+ var SOCK_PATH2 = join14(CONFIG_DIR, "sock");
20841
21074
  var CONFIG_DIR_MODE3 = 448;
20842
21075
  var SOCKET_MODE = 384;
20843
21076
  var MAX_SOCKET_CONNECTIONS = 64;
@@ -20854,7 +21087,7 @@ var EXIT_CRASH = 1;
20854
21087
  var startedAt = Date.now();
20855
21088
  var client = getClient();
20856
21089
  var runtimeVersion = resolveRuntimeVersion();
20857
- var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt);
21090
+ var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt, getSiteUrl());
20858
21091
  var statuslineIngestionCache = new StatuslineIngestionCache(decisionIngestionStatus, {
20859
21092
  resolveRepositoryBindingState: repositoryBindingState
20860
21093
  });
@@ -20906,7 +21139,7 @@ function resolveRuntimeVersion() {
20906
21139
  }
20907
21140
  try {
20908
21141
  const here = fileURLToPath(new URL(".", import.meta.url));
20909
- const pkg = JSON.parse(readFileSync8(join13(here, "..", "..", "package.json"), "utf-8"));
21142
+ const pkg = JSON.parse(readFileSync9(join14(here, "..", "..", "package.json"), "utf-8"));
20910
21143
  return pkg.version ?? "unknown";
20911
21144
  } catch {
20912
21145
  return "unknown";
@@ -20924,6 +21157,7 @@ function purgePrincipalScopedState() {
20924
21157
  lastOnlineNames = void 0;
20925
21158
  lastOnlineTeammates = void 0;
20926
21159
  lastOkAtLocal = void 0;
21160
+ clearSweepRecord();
20927
21161
  }
20928
21162
  function synchronizeDaemonCredential() {
20929
21163
  const token = resolveAuthCredential()?.token;
@@ -20931,6 +21165,7 @@ function synchronizeDaemonCredential() {
20931
21165
  if (credentialKey !== activeCredentialKey) {
20932
21166
  activeCredentialKey = credentialKey;
20933
21167
  purgePrincipalScopedState();
21168
+ persistHealth();
20934
21169
  }
20935
21170
  return resolveDaemonPrincipal(token);
20936
21171
  }
@@ -20944,7 +21179,7 @@ function assertCallerPrincipalMatches(caller) {
20944
21179
  }
20945
21180
  function ensureDaemonDirectoryPermissions() {
20946
21181
  mkdirSync8(CONFIG_DIR, { recursive: true, mode: CONFIG_DIR_MODE3 });
20947
- chmodSync6(CONFIG_DIR, CONFIG_DIR_MODE3);
21182
+ chmodSync7(CONFIG_DIR, CONFIG_DIR_MODE3);
20948
21183
  }
20949
21184
  function persistHealth() {
20950
21185
  refreshDaemonHealth(daemonHealth, Date.now());
@@ -20962,6 +21197,15 @@ function updatePendingHealth() {
20962
21197
  daemonHealth.ingestion.pendingSampled = pending.sampled;
20963
21198
  daemonHealth.ingestion.oldestPendingAt = pending.oldestPendingAt;
20964
21199
  daemonHealth.ingestion.strandedCount = pending.strandedCount;
21200
+ return pending;
21201
+ }
21202
+ function recordRetainedBuckets(retained) {
21203
+ daemonHealth.ingestion.lastRetainedBucketCount = retained.length;
21204
+ daemonHealth.ingestion.lastRetainedReasons = retained.length > 0 ? summarizeRetainedBuckets(retained) : void 0;
21205
+ }
21206
+ function clearSweepRecord() {
21207
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = 0;
21208
+ recordRetainedBuckets([]);
20965
21209
  }
20966
21210
  function enterReauthHold() {
20967
21211
  if (reauthHold) {
@@ -20997,6 +21241,8 @@ function exitReauthHold() {
20997
21241
  daemonHealth.ingestion.consecutiveFailures = 0;
20998
21242
  daemonHealth.heartbeat.lastError = void 0;
20999
21243
  daemonHealth.ingestion.lastError = void 0;
21244
+ clearSweepRecord();
21245
+ daemonHealth.heartbeat.lastSuccessAt = void 0;
21000
21246
  persistHealth();
21001
21247
  process.stderr.write(
21002
21248
  "[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion + Decision cache\n"
@@ -21005,6 +21251,11 @@ function exitReauthHold() {
21005
21251
  void runIngestionLoop();
21006
21252
  void runDecisionDigestLoop();
21007
21253
  }
21254
+ function resumeIfReauthenticated() {
21255
+ if (reauthHold && !isSessionEnded() && resolveAuthCredential()) {
21256
+ exitReauthHold();
21257
+ }
21258
+ }
21008
21259
  async function takeOwnership() {
21009
21260
  const now = Date.now();
21010
21261
  const recordedOwner = readDaemonOwner(CONFIG_DIR);
@@ -21014,7 +21265,7 @@ async function takeOwnership() {
21014
21265
  (pid) => pid !== void 0 && processIsAlive(pid)
21015
21266
  )
21016
21267
  );
21017
- if (existsSync8(SOCK_PATH2) || candidatePids.size > 0) {
21268
+ if (existsSync9(SOCK_PATH2) || candidatePids.size > 0) {
21018
21269
  const snapshot = await daemonRequest(
21019
21270
  "status_snapshot",
21020
21271
  {},
@@ -21169,22 +21420,26 @@ function scheduleIngestion(delayMs) {
21169
21420
  }
21170
21421
  async function runIngestionLoop() {
21171
21422
  synchronizeDaemonCredential();
21423
+ let pending;
21172
21424
  try {
21173
- updatePendingHealth();
21425
+ pending = updatePendingHealth();
21174
21426
  } catch (err) {
21175
21427
  const failures = daemonHealth.ingestion.consecutiveFailures + 1;
21176
21428
  const delay = ingestionRetryDelayMs(failures);
21177
21429
  daemonHealth.ingestion.consecutiveFailures = failures;
21178
21430
  daemonHealth.ingestion.lastError = `journal scan failed: ${errorMessage2(err)}`;
21179
21431
  daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
21432
+ daemonHealth.ingestion.lastAcknowledgedCount = 0;
21433
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = 0;
21180
21434
  persistHealth();
21181
21435
  scheduleIngestion(delay);
21182
21436
  return;
21183
21437
  }
21184
- if (daemonHealth.ingestion.pendingCount === 0 && !daemonHealth.ingestion.pendingSampled) {
21438
+ if (!hasPendingDrainWork(pending)) {
21185
21439
  daemonHealth.ingestion.consecutiveFailures = 0;
21186
21440
  daemonHealth.ingestion.lastError = void 0;
21187
21441
  daemonHealth.ingestion.nextRetryAt = void 0;
21442
+ clearSweepRecord();
21188
21443
  persistHealth();
21189
21444
  scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
21190
21445
  return;
@@ -21200,8 +21455,10 @@ async function runIngestionLoop() {
21200
21455
  }
21201
21456
  daemonHealth.ingestion.lastSuccessAt = Date.now();
21202
21457
  daemonHealth.ingestion.lastAcknowledgedCount = result.flushed;
21458
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = 0;
21203
21459
  daemonHealth.ingestion.consecutiveFailures = 0;
21204
21460
  daemonHealth.ingestion.lastError = void 0;
21461
+ recordRetainedBuckets(result.retained ?? []);
21205
21462
  updatePendingHealth();
21206
21463
  persistHealth();
21207
21464
  scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
@@ -21209,6 +21466,8 @@ async function runIngestionLoop() {
21209
21466
  const failures = daemonHealth.ingestion.consecutiveFailures + 1;
21210
21467
  const delay = ingestionRetryDelayMs(failures);
21211
21468
  daemonHealth.ingestion.lastAcknowledgedCount = err instanceof FlushError ? err.flushed : 0;
21469
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = err instanceof FlushError ? err.failedDrainFlushed : 0;
21470
+ if (err instanceof FlushError) recordRetainedBuckets(err.retained);
21212
21471
  daemonHealth.ingestion.consecutiveFailures = failures;
21213
21472
  daemonHealth.ingestion.lastError = errorMessage2(err);
21214
21473
  daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
@@ -21267,6 +21526,7 @@ async function proxyGet(params, allowedPrefix, caller) {
21267
21526
  return result;
21268
21527
  }
21269
21528
  function handleStatusSnapshot(params, caller, enforcePrincipal = false) {
21529
+ resumeIfReauthenticated();
21270
21530
  const daemonPrincipal = synchronizeDaemonCredential();
21271
21531
  const wasHealthy = daemonHealth.healthy;
21272
21532
  const heartbeatWasHealthy = daemonHealth.heartbeat.healthy;
@@ -21543,7 +21803,7 @@ function startSocketServer() {
21543
21803
  });
21544
21804
  socketServer.listen(SOCK_PATH2, () => {
21545
21805
  try {
21546
- chmodSync6(SOCK_PATH2, SOCKET_MODE);
21806
+ chmodSync7(SOCK_PATH2, SOCKET_MODE);
21547
21807
  } catch (err) {
21548
21808
  shutdown(EXIT_CRASH, `[prim-daemon] failed to secure socket: ${errorMessage2(err)}`);
21549
21809
  return;
@@ -21570,9 +21830,7 @@ function startTimers() {
21570
21830
  }
21571
21831
  async function runTokenCheckLoop() {
21572
21832
  if (reauthHold) {
21573
- if (!isSessionEnded() && resolveAuthCredential()) {
21574
- exitReauthHold();
21575
- }
21833
+ resumeIfReauthenticated();
21576
21834
  } else {
21577
21835
  await ensureTokenFresh();
21578
21836
  synchronizeDaemonCredential();