@primitive.ai/prim 0.1.0-alpha.92 → 0.1.0-alpha.94

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 (42) hide show
  1. package/README.md +84 -10
  2. package/SKILL.md +5 -2
  3. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  4. package/dist/{chunk-26P57LAR.js → chunk-AXQQMX2U.js} +203 -9
  5. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  6. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  7. package/dist/chunk-FXF4JWK2.js +162 -0
  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-5HRR4AOR.js → chunk-IMK4JKTA.js} +1 -1
  11. package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  12. package/dist/{chunk-ADUTW66W.js → chunk-O7YAW7G6.js} +6 -6
  13. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  14. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  15. package/dist/chunk-OYHYCK4K.js +13 -0
  16. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  17. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  18. package/dist/{chunk-LGGMJYY4.js → chunk-WUXFLE7X.js} +720 -369
  19. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  20. package/dist/daemon/server.js +372 -123
  21. package/dist/hook-runtime/daemon/server.js +372 -123
  22. package/dist/hook-runtime/hooks/post-commit.js +192 -58
  23. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  24. package/dist/hook-runtime/hooks/post-tool-use.js +150 -67
  25. package/dist/hook-runtime/hooks/pre-tool-use.js +87 -15
  26. package/dist/hook-runtime/hooks/prim-hook.js +197 -107
  27. package/dist/hook-runtime/hooks/session-end.js +53 -5
  28. package/dist/hook-runtime/hooks/session-start.js +880 -280
  29. package/dist/hook-runtime/index.js +4008 -2438
  30. package/dist/hook-runtime/statusline-main.js +23 -1
  31. package/dist/hooks/post-commit.js +15 -8
  32. package/dist/hooks/post-rewrite.js +11 -8
  33. package/dist/hooks/post-tool-use.js +13 -15
  34. package/dist/hooks/pre-commit.js +4 -4
  35. package/dist/hooks/pre-tool-use.js +10 -12
  36. package/dist/hooks/prim-hook.js +17 -14
  37. package/dist/hooks/session-end.js +4 -4
  38. package/dist/hooks/session-start.js +35 -45
  39. package/dist/index.js +1401 -620
  40. package/dist/statusline-main.js +23 -1
  41. package/package.json +1 -1
  42. 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,
@@ -19737,23 +19850,25 @@ function mayBeBatchSizeFailure(error) {
19737
19850
  }
19738
19851
  return error instanceof Error && error.name === "TimeoutError";
19739
19852
  }
19740
- async function* rawJournalLines(input) {
19853
+ async function* rawJournalLines(input, offset) {
19741
19854
  let pending = [];
19742
19855
  let pendingBytes = 0;
19856
+ let end = offset;
19743
19857
  for await (const value of input) {
19744
19858
  const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
19745
19859
  let start = 0;
19746
19860
  let newline = chunk.indexOf(10, start);
19747
19861
  while (newline !== -1) {
19748
19862
  const tail = chunk.subarray(start, newline + 1);
19749
- if (pending.length === 0) {
19750
- yield tail;
19751
- } else {
19863
+ let line = tail;
19864
+ if (pending.length > 0) {
19752
19865
  pending.push(tail);
19753
- yield Buffer.concat(pending, pendingBytes + tail.length);
19866
+ line = Buffer.concat(pending, pendingBytes + tail.length);
19754
19867
  pending = [];
19755
19868
  pendingBytes = 0;
19756
19869
  }
19870
+ end += line.length;
19871
+ yield { bytes: line, end };
19757
19872
  start = newline + 1;
19758
19873
  newline = chunk.indexOf(10, start);
19759
19874
  }
@@ -19764,18 +19879,43 @@ async function* rawJournalLines(input) {
19764
19879
  }
19765
19880
  }
19766
19881
  if (pendingBytes > 0) {
19767
- 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 };
19768
19884
  }
19769
19885
  }
19886
+ var drainProgressWarned = false;
19770
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
+ };
19771
19911
  const postBatch = async (batch2) => {
19912
+ const moves = batch2.map((entry) => entry.move);
19772
19913
  try {
19773
- const request2 = buildOrganizationBoundMoveRequest(batch2, binding);
19914
+ const request2 = buildOrganizationBoundMoveRequest(moves, binding);
19774
19915
  const response = await client2.post("/api/cli/moves/ingest", request2, {
19775
19916
  signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
19776
19917
  });
19777
- requireDurableIngestAcknowledgement(response, batch2.length, collectionScopeRoots(batch2));
19778
- return { flushed: batch2.length, quarantined: 0 };
19918
+ requireDurableIngestAcknowledgement(response, moves.length, collectionScopeRoots(moves));
19779
19919
  } catch (error) {
19780
19920
  const reason = deadLetterReason(error);
19781
19921
  if (!(reason || mayBeBatchSizeFailure(error))) {
@@ -19790,33 +19930,43 @@ async function drainFlushingPath(flushingPath, client2, binding, options = {}) {
19790
19930
  if (!reason) {
19791
19931
  throw error;
19792
19932
  }
19793
- const [move] = batch2;
19933
+ const [entry] = batch2;
19794
19934
  const quarantined = quarantineMove(
19795
19935
  flushingPath,
19796
- move,
19936
+ entry.move,
19797
19937
  reason,
19798
19938
  Date.now,
19799
19939
  options.deadLetterPersistence
19800
19940
  );
19941
+ recordProgress(entry.end);
19801
19942
  process.stderr.write(
19802
19943
  `[prim] quarantined rejected move ${quarantined.quarantineId.slice(0, 12)} (${reason})
19803
19944
  `
19804
19945
  );
19805
- return { flushed: 0, quarantined: 1 };
19946
+ return reportProgress({ flushed: 0, quarantined: 1 });
19806
19947
  }
19948
+ recordProgress(batch2.at(-1)?.end);
19949
+ return reportProgress({ flushed: batch2.length, quarantined: 0 });
19807
19950
  };
19808
- const input = createReadStream(flushingPath);
19951
+ const input = createReadStream(flushingPath, { start: resumeAt });
19809
19952
  const decoder = new TextDecoder("utf-8", { fatal: true });
19810
19953
  let batch = [];
19954
+ let batchBytes = 0;
19811
19955
  let counts = { flushed: 0, quarantined: 0 };
19956
+ const postPending = async () => {
19957
+ counts = addDrainCounts(counts, await postBatch(batch));
19958
+ batch = [];
19959
+ batchBytes = 0;
19960
+ };
19812
19961
  try {
19813
- for await (const rawLine of rawJournalLines(input)) {
19962
+ for await (const { bytes: rawLine, end } of rawJournalLines(input, resumeAt)) {
19814
19963
  if (rawLine.length === 1 && rawLine[0] === 10 || rawLine.length === 2 && rawLine[0] === 13 && rawLine[1] === 10) {
19815
19964
  continue;
19816
19965
  }
19966
+ let move;
19817
19967
  try {
19818
19968
  const line = decoder.decode(rawLine);
19819
- batch.push(JSON.parse(line));
19969
+ move = JSON.parse(line);
19820
19970
  } catch {
19821
19971
  const quarantined = quarantineRawLine(
19822
19972
  flushingPath,
@@ -19828,24 +19978,35 @@ async function drainFlushingPath(flushingPath, client2, binding, options = {}) {
19828
19978
  `[prim] quarantined invalid journal line ${quarantined.quarantineId.slice(0, 12)} (invalid_move)
19829
19979
  `
19830
19980
  );
19831
- counts.quarantined += 1;
19981
+ counts = addDrainCounts(counts, reportProgress({ flushed: 0, quarantined: 1 }));
19982
+ if (batch.length === 0) {
19983
+ recordProgress(end);
19984
+ }
19832
19985
  continue;
19833
19986
  }
19834
- if (batch.length === BATCH_SIZE) {
19835
- counts = addDrainCounts(counts, await postBatch(batch));
19836
- 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();
19837
19994
  }
19838
19995
  }
19839
19996
  if (batch.length > 0) {
19840
- counts = addDrainCounts(counts, await postBatch(batch));
19997
+ await postPending();
19841
19998
  }
19842
19999
  } finally {
19843
20000
  input.destroy();
19844
20001
  }
19845
20002
  unlinkSync(flushingPath);
20003
+ try {
20004
+ removeDrainCheckpoint(flushingPath);
20005
+ } catch {
20006
+ }
19846
20007
  return counts;
19847
20008
  }
19848
- async function drainPath(path, client2, binding) {
20009
+ async function drainPath(path, client2, binding, onProgress) {
19849
20010
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
19850
20011
  try {
19851
20012
  renameSync3(path, tmpPath);
@@ -19855,7 +20016,19 @@ async function drainPath(path, client2, binding) {
19855
20016
  }
19856
20017
  throw err;
19857
20018
  }
19858
- 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
+ }
19859
20032
  }
19860
20033
  function selectRecoverable(files, now, opts = {}) {
19861
20034
  const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
@@ -19872,7 +20045,8 @@ async function recoverOrphans(candidates, options) {
19872
20045
  flushed: 0,
19873
20046
  quarantined: 0,
19874
20047
  errors: [],
19875
- failedBuckets: /* @__PURE__ */ new Set()
20048
+ failedBuckets: /* @__PURE__ */ new Set(),
20049
+ failedDrainFlushed: 0
19876
20050
  };
19877
20051
  const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
19878
20052
  ownerPid: options.ownerPid ?? process.pid,
@@ -19883,28 +20057,41 @@ async function recoverOrphans(candidates, options) {
19883
20057
  if (summary.failedBuckets.has(file.bucket)) {
19884
20058
  continue;
19885
20059
  }
19886
- try {
19887
- const counts = await drain(file.path);
19888
- summary.flushed += counts.flushed;
19889
- summary.quarantined += counts.quarantined;
19890
- } catch (err) {
19891
- 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);
19892
20066
  summary.failedBuckets.add(file.bucket);
19893
20067
  }
19894
20068
  }
19895
20069
  return summary;
19896
20070
  }
19897
20071
  var FlushError = class extends Error {
20072
+ /** Moves the whole sweep acknowledged, across every drain it ran. */
19898
20073
  flushed;
19899
20074
  quarantined;
19900
- 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) {
19901
20085
  super(cause instanceof Error ? cause.message : String(cause), { cause });
19902
20086
  this.name = "FlushError";
19903
20087
  this.flushed = flushed;
19904
20088
  this.quarantined = quarantined;
20089
+ this.retained = retained;
20090
+ this.failedDrainFlushed = failedDrainFlushed;
19905
20091
  }
19906
20092
  };
19907
20093
  async function flushOnce() {
20094
+ sweepOrphanedDrainProgress();
19908
20095
  const orphanCandidates = listFlushing({ sampleBytes: 0 });
19909
20096
  const liveBuckets = listBuckets();
19910
20097
  if (orphanCandidates.length === 0 && liveBuckets.length === 0) {
@@ -19921,10 +20108,11 @@ async function flushOnce() {
19921
20108
  const binding = inspection.binding;
19922
20109
  const recovered = await recoverOrphans(
19923
20110
  orphanCandidates.filter((file) => inspection.deliverableBuckets.has(file.bucket)),
19924
- { drain: (path) => drainFlushingPath(path, client2, binding) }
20111
+ { drain: (path, onProgress) => drainFlushingPath(path, client2, binding, { onProgress }) }
19925
20112
  );
19926
20113
  let total = recovered.flushed;
19927
20114
  let quarantined = recovered.quarantined;
20115
+ let failedDrainFlushed = recovered.failedDrainFlushed;
19928
20116
  const errors = recovered.errors;
19929
20117
  for (const { bucket, path } of liveBuckets) {
19930
20118
  if (!inspection.deliverableBuckets.has(bucket)) {
@@ -19933,21 +20121,29 @@ async function flushOnce() {
19933
20121
  if (recovered.failedBuckets.has(bucket)) {
19934
20122
  continue;
19935
20123
  }
19936
- try {
19937
- const counts = await drainPath(path, client2, binding);
19938
- total += counts.flushed;
19939
- quarantined += counts.quarantined;
19940
- } catch (err) {
19941
- 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);
19942
20132
  }
19943
20133
  }
19944
20134
  if (errors.length > 0) {
19945
- throw new FlushError(errors[0], total, quarantined);
20135
+ throw new FlushError(
20136
+ errors[0],
20137
+ total,
20138
+ quarantined,
20139
+ inspection.retainedBuckets,
20140
+ failedDrainFlushed
20141
+ );
19946
20142
  }
19947
20143
  return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
19948
20144
  }
19949
20145
  var flushInFlight;
19950
- var FLUSH_LOCK_PATH = join8(dirname7(JOURNAL_DIR), ".flush.lock");
20146
+ var FLUSH_LOCK_PATH = join9(dirname8(JOURNAL_DIR), ".flush.lock");
19951
20147
  var FLUSH_LOCK_TIMEOUT_MS = 250;
19952
20148
  function isFlushLockContended(error) {
19953
20149
  return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
@@ -19971,6 +20167,30 @@ function flush() {
19971
20167
  flushInFlight = attempt;
19972
20168
  return attempt;
19973
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
+ }
19974
20194
 
19975
20195
  // src/lib/presence.ts
19976
20196
  var DECISION_ORIGIN = "https://app.getprimitive.ai";
@@ -20046,7 +20266,8 @@ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {
20046
20266
  if (snapshot.ingestion?.healthy === false) {
20047
20267
  const pending = snapshot.ingestion.pendingCount;
20048
20268
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
20049
- 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)})`;
20050
20271
  }
20051
20272
  if (snapshot.heartbeat?.healthy === false) {
20052
20273
  return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
@@ -20124,8 +20345,8 @@ var StatuslineIngestionCache = class {
20124
20345
 
20125
20346
  // src/daemon/client-instance-id.ts
20126
20347
  import { randomBytes as randomBytes3 } from "crypto";
20127
- import { chmodSync as chmodSync3, lstatSync as lstatSync2, readFileSync as readFileSync6 } from "fs";
20128
- 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";
20129
20350
  var CONFIG_DIRECTORY_MODE = 448;
20130
20351
  var INSTANCE_FILE_MODE = 384;
20131
20352
  var INSTANCE_ENTROPY_BYTES = 32;
@@ -20142,7 +20363,7 @@ function ensurePrivateConfigDirectory(configDir) {
20142
20363
  if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
20143
20364
  throw new Error("prim config path must be a private directory");
20144
20365
  }
20145
- chmodSync3(configDir, CONFIG_DIRECTORY_MODE);
20366
+ chmodSync4(configDir, CONFIG_DIRECTORY_MODE);
20146
20367
  syncDirectory(configDir);
20147
20368
  }
20148
20369
  function readExisting(path) {
@@ -20158,11 +20379,11 @@ function readExisting(path) {
20158
20379
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
20159
20380
  throw new Error("stored client instance identity is invalid");
20160
20381
  }
20161
- chmodSync3(path, INSTANCE_FILE_MODE);
20382
+ chmodSync4(path, INSTANCE_FILE_MODE);
20162
20383
  if (metadata.size > MAX_STORED_BYTES) {
20163
20384
  throw new Error("stored client instance identity is invalid");
20164
20385
  }
20165
- const raw = readFileSync6(path, "utf8");
20386
+ const raw = readFileSync7(path, "utf8");
20166
20387
  const value = raw.endsWith("\n") ? raw.slice(0, -1) : raw;
20167
20388
  if (raw !== value && raw !== `${value}
20168
20389
  ` || !isClientInstanceId(value)) {
@@ -20176,8 +20397,8 @@ function atomicWrite2(path, value) {
20176
20397
  }
20177
20398
  async function getOrCreateClientInstanceId(options) {
20178
20399
  const configDir = options.configDir;
20179
- const path = join9(configDir, "client_instance_id");
20180
- const lockPath = join9(configDir, "client-instance.lock");
20400
+ const path = join10(configDir, "client_instance_id");
20401
+ const lockPath = join10(configDir, "client-instance.lock");
20181
20402
  ensurePrivateConfigDirectory(configDir);
20182
20403
  return withFileLock(
20183
20404
  lockPath,
@@ -20201,7 +20422,7 @@ async function getOrCreateClientInstanceId(options) {
20201
20422
 
20202
20423
  // src/daemon/client.ts
20203
20424
  import { createConnection } from "net";
20204
- import { join as join10 } from "path";
20425
+ import { join as join11 } from "path";
20205
20426
 
20206
20427
  // src/daemon/principal.ts
20207
20428
  import { createHash as createHash5 } from "crypto";
@@ -20278,7 +20499,7 @@ function parseDaemonResponseEnvelope(value) {
20278
20499
  }
20279
20500
 
20280
20501
  // src/daemon/client.ts
20281
- var SOCK_PATH = join10(primConfigDirectory(), "sock");
20502
+ var SOCK_PATH = join11(primConfigDirectory(), "sock");
20282
20503
  var DEFAULT_TIMEOUT_MS2 = 250;
20283
20504
  var nextRequestId = 1;
20284
20505
  function daemonRequest(method, params = {}, opts = {}) {
@@ -20547,8 +20768,8 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
20547
20768
  }
20548
20769
 
20549
20770
  // src/daemon/health.ts
20550
- import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync6, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
20551
- 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";
20552
20773
  var CONFIG_DIR_MODE2 = 448;
20553
20774
  var HEALTH_FILE_MODE = 384;
20554
20775
  var INGESTION_SLA_MS = 3e4;
@@ -20556,12 +20777,13 @@ var HEARTBEAT_FRESH_MS = 9e4;
20556
20777
  var HEARTBEAT_RETRY_CAP_MS = 3e4;
20557
20778
  var INGESTION_RETRY_BASE_MS = 5e3;
20558
20779
  var INGESTION_RETRY_CAP_MS = 15e3;
20559
- var DAEMON_HEALTH_PATH = join11(primConfigDirectory(), "daemon-health.json");
20560
- function createDaemonHealthState(version, pid, startedAt2) {
20780
+ var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
20781
+ function createDaemonHealthState(version, pid, startedAt2, siteUrl) {
20561
20782
  return {
20562
20783
  schemaVersion: 1,
20563
20784
  version,
20564
20785
  pid,
20786
+ siteUrl,
20565
20787
  startedAt: startedAt2,
20566
20788
  updatedAt: startedAt2,
20567
20789
  healthy: false,
@@ -20572,7 +20794,9 @@ function createDaemonHealthState(version, pid, startedAt2) {
20572
20794
  pendingCount: 0,
20573
20795
  pendingSampled: false,
20574
20796
  strandedCount: 0,
20575
- lastAcknowledgedCount: 0
20797
+ lastAcknowledgedCount: 0,
20798
+ lastFailedDrainAcknowledgedCount: 0,
20799
+ lastRetainedBucketCount: 0
20576
20800
  }
20577
20801
  };
20578
20802
  }
@@ -20594,37 +20818,37 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
20594
20818
  return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
20595
20819
  }
20596
20820
  function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
20597
- const dir = dirname8(path);
20598
- if (!existsSync6(dir)) {
20821
+ const dir = dirname9(path);
20822
+ if (!existsSync7(dir)) {
20599
20823
  mkdirSync6(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
20600
20824
  }
20601
- const tmp = join11(dir, `.${process.pid}.daemon-health.tmp`);
20825
+ const tmp = join12(dir, `.${process.pid}.daemon-health.tmp`);
20602
20826
  writeFileSync4(tmp, `${JSON.stringify(state, null, 2)}
20603
20827
  `, { mode: HEALTH_FILE_MODE });
20604
- chmodSync4(tmp, HEALTH_FILE_MODE);
20828
+ chmodSync5(tmp, HEALTH_FILE_MODE);
20605
20829
  renameSync4(tmp, path);
20606
20830
  }
20607
20831
 
20608
20832
  // src/daemon/instance-lock.ts
20609
20833
  import { randomUUID as randomUUID2 } from "crypto";
20610
20834
  import {
20611
- chmodSync as chmodSync5,
20612
- existsSync as existsSync7,
20835
+ chmodSync as chmodSync6,
20836
+ existsSync as existsSync8,
20613
20837
  mkdirSync as mkdirSync7,
20614
- readFileSync as readFileSync7,
20838
+ readFileSync as readFileSync8,
20615
20839
  renameSync as renameSync5,
20616
- rmSync as rmSync5,
20617
- statSync as statSync2,
20840
+ rmSync as rmSync6,
20841
+ statSync as statSync3,
20618
20842
  unlinkSync as unlinkSync2,
20619
20843
  writeFileSync as writeFileSync5
20620
20844
  } from "fs";
20621
- import { join as join12 } from "path";
20845
+ import { join as join13 } from "path";
20622
20846
  var DIR_MODE = 448;
20623
- var FILE_MODE3 = 384;
20847
+ var FILE_MODE4 = 384;
20624
20848
  var DAEMON_STARTUP_GRACE_MS = 5e3;
20625
20849
  function readLockRecord(path) {
20626
20850
  try {
20627
- const value = JSON.parse(readFileSync7(path, "utf-8"));
20851
+ const value = JSON.parse(readFileSync8(path, "utf-8"));
20628
20852
  if (Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
20629
20853
  return value;
20630
20854
  }
@@ -20634,23 +20858,23 @@ function readLockRecord(path) {
20634
20858
  }
20635
20859
  function numericPid(path) {
20636
20860
  try {
20637
- const pid = Number(readFileSync7(path, "utf-8").trim());
20861
+ const pid = Number(readFileSync8(path, "utf-8").trim());
20638
20862
  return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
20639
20863
  } catch {
20640
20864
  return;
20641
20865
  }
20642
20866
  }
20643
20867
  function readDaemonOwner(configDir) {
20644
- return readLockRecord(join12(configDir, "daemon.lock", "owner.json"));
20868
+ return readLockRecord(join13(configDir, "daemon.lock", "owner.json"));
20645
20869
  }
20646
20870
  function readDaemonPid(configDir) {
20647
- const path = join12(configDir, "daemon.pid");
20871
+ const path = join13(configDir, "daemon.pid");
20648
20872
  const pid = numericPid(path);
20649
20873
  if (pid === void 0) {
20650
20874
  return;
20651
20875
  }
20652
20876
  try {
20653
- return { pid, mtimeMs: statSync2(path).mtimeMs };
20877
+ return { pid, mtimeMs: statSync3(path).mtimeMs };
20654
20878
  } catch {
20655
20879
  return;
20656
20880
  }
@@ -20659,11 +20883,11 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20659
20883
  const pid = opts.pid ?? process.pid;
20660
20884
  const now = opts.now ?? Date.now();
20661
20885
  const isAlive = opts.isAlive ?? processIsAlive;
20662
- const lockDir = join12(configDir, "daemon.lock");
20663
- const ownerPath2 = join12(lockDir, "owner.json");
20664
- 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");
20665
20889
  const instanceId = randomUUID2();
20666
- if (!existsSync7(configDir)) {
20890
+ if (!existsSync8(configDir)) {
20667
20891
  mkdirSync7(configDir, { recursive: true, mode: DIR_MODE });
20668
20892
  }
20669
20893
  for (; ; ) {
@@ -20681,7 +20905,7 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20681
20905
  if (!existing) {
20682
20906
  let mtimeMs;
20683
20907
  try {
20684
- mtimeMs = statSync2(lockDir).mtimeMs;
20908
+ mtimeMs = statSync3(lockDir).mtimeMs;
20685
20909
  } catch (statErr) {
20686
20910
  if (statErr.code === "ENOENT") {
20687
20911
  continue;
@@ -20695,7 +20919,7 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20695
20919
  const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
20696
20920
  try {
20697
20921
  renameSync5(lockDir, stale);
20698
- rmSync5(stale, { recursive: true, force: true });
20922
+ rmSync6(stale, { recursive: true, force: true });
20699
20923
  } catch (renameErr) {
20700
20924
  if (renameErr.code !== "ENOENT") {
20701
20925
  throw renameErr;
@@ -20713,15 +20937,15 @@ function acquireDaemonOwnership(configDir, opts = {}) {
20713
20937
  };
20714
20938
  const record = { pid, instanceId, startedAt: now };
20715
20939
  writeFileSync5(ownerPath2, `${JSON.stringify(record)}
20716
- `, { mode: FILE_MODE3 });
20717
- chmodSync5(ownerPath2, FILE_MODE3);
20940
+ `, { mode: FILE_MODE4 });
20941
+ chmodSync6(ownerPath2, FILE_MODE4);
20718
20942
  const legacyPid = numericPid(pidPath);
20719
20943
  if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
20720
- rmSync5(lockDir, { recursive: true, force: true });
20944
+ rmSync6(lockDir, { recursive: true, force: true });
20721
20945
  throw new Error(`daemon already running (pid=${String(legacyPid)})`);
20722
20946
  }
20723
- writeFileSync5(pidPath, String(pid), { mode: FILE_MODE3 });
20724
- chmodSync5(pidPath, FILE_MODE3);
20947
+ writeFileSync5(pidPath, String(pid), { mode: FILE_MODE4 });
20948
+ chmodSync6(pidPath, FILE_MODE4);
20725
20949
  return ownership2;
20726
20950
  }
20727
20951
  function ownsDaemonInstance(ownership2) {
@@ -20751,7 +20975,7 @@ function releaseDaemonOwnership(ownership2, socketPath) {
20751
20975
  }
20752
20976
  }
20753
20977
  }
20754
- rmSync5(ownership2.lockDir, { recursive: true, force: true });
20978
+ rmSync6(ownership2.lockDir, { recursive: true, force: true });
20755
20979
  if (cleanupError) {
20756
20980
  throw cleanupError;
20757
20981
  }
@@ -20846,7 +21070,7 @@ function parseStatuslineRequest(buffer) {
20846
21070
 
20847
21071
  // src/daemon/server.ts
20848
21072
  var CONFIG_DIR = primConfigDirectory();
20849
- var SOCK_PATH2 = join13(CONFIG_DIR, "sock");
21073
+ var SOCK_PATH2 = join14(CONFIG_DIR, "sock");
20850
21074
  var CONFIG_DIR_MODE3 = 448;
20851
21075
  var SOCKET_MODE = 384;
20852
21076
  var MAX_SOCKET_CONNECTIONS = 64;
@@ -20863,7 +21087,7 @@ var EXIT_CRASH = 1;
20863
21087
  var startedAt = Date.now();
20864
21088
  var client = getClient();
20865
21089
  var runtimeVersion = resolveRuntimeVersion();
20866
- var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt);
21090
+ var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt, getSiteUrl());
20867
21091
  var statuslineIngestionCache = new StatuslineIngestionCache(decisionIngestionStatus, {
20868
21092
  resolveRepositoryBindingState: repositoryBindingState
20869
21093
  });
@@ -20915,7 +21139,7 @@ function resolveRuntimeVersion() {
20915
21139
  }
20916
21140
  try {
20917
21141
  const here = fileURLToPath(new URL(".", import.meta.url));
20918
- const pkg = JSON.parse(readFileSync8(join13(here, "..", "..", "package.json"), "utf-8"));
21142
+ const pkg = JSON.parse(readFileSync9(join14(here, "..", "..", "package.json"), "utf-8"));
20919
21143
  return pkg.version ?? "unknown";
20920
21144
  } catch {
20921
21145
  return "unknown";
@@ -20933,6 +21157,7 @@ function purgePrincipalScopedState() {
20933
21157
  lastOnlineNames = void 0;
20934
21158
  lastOnlineTeammates = void 0;
20935
21159
  lastOkAtLocal = void 0;
21160
+ clearSweepRecord();
20936
21161
  }
20937
21162
  function synchronizeDaemonCredential() {
20938
21163
  const token = resolveAuthCredential()?.token;
@@ -20940,6 +21165,7 @@ function synchronizeDaemonCredential() {
20940
21165
  if (credentialKey !== activeCredentialKey) {
20941
21166
  activeCredentialKey = credentialKey;
20942
21167
  purgePrincipalScopedState();
21168
+ persistHealth();
20943
21169
  }
20944
21170
  return resolveDaemonPrincipal(token);
20945
21171
  }
@@ -20953,7 +21179,7 @@ function assertCallerPrincipalMatches(caller) {
20953
21179
  }
20954
21180
  function ensureDaemonDirectoryPermissions() {
20955
21181
  mkdirSync8(CONFIG_DIR, { recursive: true, mode: CONFIG_DIR_MODE3 });
20956
- chmodSync6(CONFIG_DIR, CONFIG_DIR_MODE3);
21182
+ chmodSync7(CONFIG_DIR, CONFIG_DIR_MODE3);
20957
21183
  }
20958
21184
  function persistHealth() {
20959
21185
  refreshDaemonHealth(daemonHealth, Date.now());
@@ -20971,6 +21197,15 @@ function updatePendingHealth() {
20971
21197
  daemonHealth.ingestion.pendingSampled = pending.sampled;
20972
21198
  daemonHealth.ingestion.oldestPendingAt = pending.oldestPendingAt;
20973
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([]);
20974
21209
  }
20975
21210
  function enterReauthHold() {
20976
21211
  if (reauthHold) {
@@ -21006,6 +21241,8 @@ function exitReauthHold() {
21006
21241
  daemonHealth.ingestion.consecutiveFailures = 0;
21007
21242
  daemonHealth.heartbeat.lastError = void 0;
21008
21243
  daemonHealth.ingestion.lastError = void 0;
21244
+ clearSweepRecord();
21245
+ daemonHealth.heartbeat.lastSuccessAt = void 0;
21009
21246
  persistHealth();
21010
21247
  process.stderr.write(
21011
21248
  "[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion + Decision cache\n"
@@ -21014,6 +21251,11 @@ function exitReauthHold() {
21014
21251
  void runIngestionLoop();
21015
21252
  void runDecisionDigestLoop();
21016
21253
  }
21254
+ function resumeIfReauthenticated() {
21255
+ if (reauthHold && !isSessionEnded() && resolveAuthCredential()) {
21256
+ exitReauthHold();
21257
+ }
21258
+ }
21017
21259
  async function takeOwnership() {
21018
21260
  const now = Date.now();
21019
21261
  const recordedOwner = readDaemonOwner(CONFIG_DIR);
@@ -21023,7 +21265,7 @@ async function takeOwnership() {
21023
21265
  (pid) => pid !== void 0 && processIsAlive(pid)
21024
21266
  )
21025
21267
  );
21026
- if (existsSync8(SOCK_PATH2) || candidatePids.size > 0) {
21268
+ if (existsSync9(SOCK_PATH2) || candidatePids.size > 0) {
21027
21269
  const snapshot = await daemonRequest(
21028
21270
  "status_snapshot",
21029
21271
  {},
@@ -21178,22 +21420,26 @@ function scheduleIngestion(delayMs) {
21178
21420
  }
21179
21421
  async function runIngestionLoop() {
21180
21422
  synchronizeDaemonCredential();
21423
+ let pending;
21181
21424
  try {
21182
- updatePendingHealth();
21425
+ pending = updatePendingHealth();
21183
21426
  } catch (err) {
21184
21427
  const failures = daemonHealth.ingestion.consecutiveFailures + 1;
21185
21428
  const delay = ingestionRetryDelayMs(failures);
21186
21429
  daemonHealth.ingestion.consecutiveFailures = failures;
21187
21430
  daemonHealth.ingestion.lastError = `journal scan failed: ${errorMessage2(err)}`;
21188
21431
  daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
21432
+ daemonHealth.ingestion.lastAcknowledgedCount = 0;
21433
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = 0;
21189
21434
  persistHealth();
21190
21435
  scheduleIngestion(delay);
21191
21436
  return;
21192
21437
  }
21193
- if (daemonHealth.ingestion.pendingCount === 0 && !daemonHealth.ingestion.pendingSampled) {
21438
+ if (!hasPendingDrainWork(pending)) {
21194
21439
  daemonHealth.ingestion.consecutiveFailures = 0;
21195
21440
  daemonHealth.ingestion.lastError = void 0;
21196
21441
  daemonHealth.ingestion.nextRetryAt = void 0;
21442
+ clearSweepRecord();
21197
21443
  persistHealth();
21198
21444
  scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
21199
21445
  return;
@@ -21209,8 +21455,10 @@ async function runIngestionLoop() {
21209
21455
  }
21210
21456
  daemonHealth.ingestion.lastSuccessAt = Date.now();
21211
21457
  daemonHealth.ingestion.lastAcknowledgedCount = result.flushed;
21458
+ daemonHealth.ingestion.lastFailedDrainAcknowledgedCount = 0;
21212
21459
  daemonHealth.ingestion.consecutiveFailures = 0;
21213
21460
  daemonHealth.ingestion.lastError = void 0;
21461
+ recordRetainedBuckets(result.retained ?? []);
21214
21462
  updatePendingHealth();
21215
21463
  persistHealth();
21216
21464
  scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
@@ -21218,6 +21466,8 @@ async function runIngestionLoop() {
21218
21466
  const failures = daemonHealth.ingestion.consecutiveFailures + 1;
21219
21467
  const delay = ingestionRetryDelayMs(failures);
21220
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);
21221
21471
  daemonHealth.ingestion.consecutiveFailures = failures;
21222
21472
  daemonHealth.ingestion.lastError = errorMessage2(err);
21223
21473
  daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
@@ -21276,6 +21526,7 @@ async function proxyGet(params, allowedPrefix, caller) {
21276
21526
  return result;
21277
21527
  }
21278
21528
  function handleStatusSnapshot(params, caller, enforcePrincipal = false) {
21529
+ resumeIfReauthenticated();
21279
21530
  const daemonPrincipal = synchronizeDaemonCredential();
21280
21531
  const wasHealthy = daemonHealth.healthy;
21281
21532
  const heartbeatWasHealthy = daemonHealth.heartbeat.healthy;
@@ -21552,7 +21803,7 @@ function startSocketServer() {
21552
21803
  });
21553
21804
  socketServer.listen(SOCK_PATH2, () => {
21554
21805
  try {
21555
- chmodSync6(SOCK_PATH2, SOCKET_MODE);
21806
+ chmodSync7(SOCK_PATH2, SOCKET_MODE);
21556
21807
  } catch (err) {
21557
21808
  shutdown(EXIT_CRASH, `[prim-daemon] failed to secure socket: ${errorMessage2(err)}`);
21558
21809
  return;
@@ -21579,9 +21830,7 @@ function startTimers() {
21579
21830
  }
21580
21831
  async function runTokenCheckLoop() {
21581
21832
  if (reauthHold) {
21582
- if (!isSessionEnded() && resolveAuthCredential()) {
21583
- exitReauthHold();
21584
- }
21833
+ resumeIfReauthenticated();
21585
21834
  } else {
21586
21835
  await ensureTokenFresh();
21587
21836
  synchronizeDaemonCredential();