@plinth-music/cli 0.23.2 → 0.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4671,7 +4671,7 @@ var require_omap = __commonJS((exports, module) => {
4671
4671
  function resolveYamlOmap(data) {
4672
4672
  if (data === null)
4673
4673
  return true;
4674
- var objectKeys = [], index, length, pair, pairKey, pairHasKey, object = data;
4674
+ var objectKeys = {}, index, length, pair, pairKey, pairHasKey, object = data;
4675
4675
  for (index = 0, length = object.length;index < length; index += 1) {
4676
4676
  pair = object[index];
4677
4677
  pairHasKey = false;
@@ -4687,10 +4687,9 @@ var require_omap = __commonJS((exports, module) => {
4687
4687
  }
4688
4688
  if (!pairHasKey)
4689
4689
  return false;
4690
- if (objectKeys.indexOf(pairKey) === -1)
4691
- objectKeys.push(pairKey);
4692
- else
4690
+ if (_hasOwnProperty.call(objectKeys, pairKey))
4693
4691
  return false;
4692
+ Object.defineProperty(objectKeys, pairKey, { value: true });
4694
4693
  }
4695
4694
  return true;
4696
4695
  }
@@ -5033,6 +5032,7 @@ var require_loader = __commonJS((exports, module) => {
5033
5032
  this.legacy = options2["legacy"] || false;
5034
5033
  this.json = options2["json"] || false;
5035
5034
  this.listener = options2["listener"] || null;
5035
+ this.maxTotalMergeKeys = typeof options2["maxTotalMergeKeys"] === "number" ? options2["maxTotalMergeKeys"] : 1e4;
5036
5036
  this.implicitTypes = this.schema.compiledImplicit;
5037
5037
  this.typeMap = this.schema.compiledTypeMap;
5038
5038
  this.length = input.length;
@@ -5040,6 +5040,7 @@ var require_loader = __commonJS((exports, module) => {
5040
5040
  this.line = 0;
5041
5041
  this.lineStart = 0;
5042
5042
  this.lineIndent = 0;
5043
+ this.totalMergeKeys = 0;
5043
5044
  this.documents = [];
5044
5045
  }
5045
5046
  function generateError(state, message) {
@@ -5113,14 +5114,22 @@ var require_loader = __commonJS((exports, module) => {
5113
5114
  state.result += _result;
5114
5115
  }
5115
5116
  }
5117
+ function chargeMergeWork(state) {
5118
+ state.totalMergeKeys += 1;
5119
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
5120
+ throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
5121
+ }
5122
+ }
5116
5123
  function mergeMappings(state, destination, source, overridableKeys) {
5117
5124
  var sourceKeys, key, index, quantity;
5118
5125
  if (!common.isObject(source)) {
5119
5126
  throwError(state, "cannot merge mappings; the provided source object is unacceptable");
5120
5127
  }
5128
+ chargeMergeWork(state);
5121
5129
  sourceKeys = Object.keys(source);
5122
5130
  for (index = 0, quantity = sourceKeys.length;index < quantity; index += 1) {
5123
5131
  key = sourceKeys[index];
5132
+ chargeMergeWork(state);
5124
5133
  if (!_hasOwnProperty.call(destination, key)) {
5125
5134
  setProperty(destination, key, source[key]);
5126
5135
  overridableKeys[key] = true;
@@ -5149,6 +5158,9 @@ var require_loader = __commonJS((exports, module) => {
5149
5158
  }
5150
5159
  if (keyTag === "tag:yaml.org,2002:merge") {
5151
5160
  if (Array.isArray(valueNode)) {
5161
+ if (valueNode.length > 100) {
5162
+ throwError(state, "abnormal merge sequence size");
5163
+ }
5152
5164
  for (index = 0, quantity = valueNode.length;index < quantity; index += 1) {
5153
5165
  mergeMappings(state, _result, valueNode[index], overridableKeys);
5154
5166
  }
@@ -7584,7 +7596,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
7584
7596
  // package.json
7585
7597
  var package_default = {
7586
7598
  name: "@plinth-music/cli",
7587
- version: "0.23.2",
7599
+ version: "0.23.3",
7588
7600
  description: "Plinth capstone sync daemon and CLI",
7589
7601
  type: "module",
7590
7602
  private: false,
@@ -7628,6 +7640,9 @@ var package_default = {
7628
7640
  "gray-matter": "^4.0.3",
7629
7641
  ora: "^9.4.0",
7630
7642
  zod: "^4.4.3"
7643
+ },
7644
+ overrides: {
7645
+ "js-yaml": "3.15.2"
7631
7646
  }
7632
7647
  };
7633
7648
 
@@ -26404,6 +26419,23 @@ var RejectedWriteSchema = exports_external.object({
26404
26419
  max_bytes: exports_external.number().int().nonnegative().optional(),
26405
26420
  reported_at: exports_external.string().datetime({ offset: true }).optional()
26406
26421
  });
26422
+ var FilePullHoldSchema = exports_external.object({
26423
+ target: exports_external.string().min(1),
26424
+ path: exports_external.string().min(1),
26425
+ kind: exports_external.string().min(1),
26426
+ row_id: exports_external.string().min(1),
26427
+ row_updated_at: exports_external.string().min(1),
26428
+ row_hash: exports_external.string().min(1),
26429
+ reason: exports_external.string().optional(),
26430
+ code: exports_external.string().optional(),
26431
+ local: exports_external.object({
26432
+ size: exports_external.number().int().nonnegative(),
26433
+ mtime_ms: exports_external.number(),
26434
+ ino: exports_external.number().int().nonnegative()
26435
+ }).nullable().optional(),
26436
+ attempts: exports_external.number().int().positive(),
26437
+ next_retry_at: exports_external.string().optional()
26438
+ });
26407
26439
  var TeamFileRejectionSchema = exports_external.object({
26408
26440
  path: exports_external.string().min(1),
26409
26441
  reason: exports_external.string().min(1),
@@ -26431,6 +26463,7 @@ var WorkspaceStateV2Schema = exports_external.object({
26431
26463
  files: FileEntriesSchema.optional(),
26432
26464
  unsynced: exports_external.array(UnsyncedOpSchema).optional(),
26433
26465
  rejected: exports_external.array(RejectedWriteSchema).optional(),
26466
+ file_pull_holds: exports_external.array(FilePullHoldSchema).optional(),
26434
26467
  workspace_rules: WorkspaceRulesStateSchema.optional(),
26435
26468
  workspace_memory: WorkspaceMemoryStateSchema.optional(),
26436
26469
  user_memory: UserMemoryStateSchema.optional(),
@@ -26526,25 +26559,50 @@ function isLaterVersion(a, b) {
26526
26559
  return ta > tb;
26527
26560
  }
26528
26561
  function committedEntryIfNewer(workspaceSlug, entityType, entry) {
26562
+ const committed = readCommittedEntry(workspaceSlug, entityType, entry.id);
26563
+ return isLaterVersion(committed, entry) ? committed : undefined;
26564
+ }
26565
+ function readCommittedEntry(workspaceSlug, entityType, id) {
26566
+ const parsed = readCommittedJson(workspaceSlug);
26567
+ const entries = parsed?.entries;
26568
+ const committed = entries?.[entityType]?.[id];
26569
+ if (committed === undefined)
26570
+ return;
26571
+ const valid = StateEntrySchema.safeParse(committed);
26572
+ return valid.success ? valid.data : undefined;
26573
+ }
26574
+ function readCommittedFileEntry(workspaceSlug, relPath) {
26575
+ const read = readCommittedFileAck(workspaceSlug, relPath);
26576
+ return read.kind === "entry" ? read.entry : undefined;
26577
+ }
26578
+ function readCommittedFileAck(workspaceSlug, relPath) {
26579
+ const json2 = readCommittedJson(workspaceSlug);
26580
+ if (!json2)
26581
+ return { kind: "unknown" };
26582
+ const files = json2.files;
26583
+ if (files === undefined)
26584
+ return { kind: "absent" };
26585
+ if (files === null || typeof files !== "object")
26586
+ return { kind: "unknown" };
26587
+ const committed = files[relPath];
26588
+ if (committed === undefined)
26589
+ return { kind: "absent" };
26590
+ const valid = FileStateEntrySchema.safeParse(committed);
26591
+ return valid.success ? { kind: "entry", entry: valid.data } : { kind: "unknown" };
26592
+ }
26593
+ function readCommittedJson(workspaceSlug) {
26529
26594
  let raw;
26530
26595
  try {
26531
26596
  raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
26532
26597
  } catch {
26533
26598
  return;
26534
26599
  }
26535
- let committed;
26536
26600
  try {
26537
26601
  const parsed = JSON.parse(raw);
26538
- committed = parsed.entries?.[entityType]?.[entry.id];
26602
+ return parsed !== null && typeof parsed === "object" ? parsed : undefined;
26539
26603
  } catch {
26540
26604
  return;
26541
26605
  }
26542
- if (committed === undefined)
26543
- return;
26544
- const valid = StateEntrySchema.safeParse(committed);
26545
- if (!valid.success)
26546
- return;
26547
- return isLaterVersion(valid.data, entry) ? valid.data : undefined;
26548
26606
  }
26549
26607
  function mergeKey(fresh, before, after) {
26550
26608
  if (before === after)
@@ -26651,6 +26709,7 @@ function mergeWorkspaceState(fresh, before, after, wantCorrected = true) {
26651
26709
  files: mergedFiles,
26652
26710
  unsynced: mergedList(fresh.unsynced, before.unsynced, after.unsynced),
26653
26711
  rejected: mergedList(fresh.rejected, before.rejected, after.rejected),
26712
+ file_pull_holds: mergedList(fresh.file_pull_holds, before.file_pull_holds, after.file_pull_holds),
26654
26713
  workspace_rules: rules.value,
26655
26714
  workspace_memory: memory.value,
26656
26715
  user_memory: userMemory.value,
@@ -26788,6 +26847,27 @@ function removeFileEntry(state, path) {
26788
26847
  delete nextFiles[path];
26789
26848
  return { state: { ...state, files: nextFiles }, changed: true };
26790
26849
  }
26850
+ function listFilePullHolds(state) {
26851
+ return (state.file_pull_holds ?? []).filter((h) => h.target === "file");
26852
+ }
26853
+ function getFilePullHold(state, path) {
26854
+ return state.file_pull_holds?.find((h) => h.target === "file" && h.path === path);
26855
+ }
26856
+ function upsertFilePullHold(state, hold) {
26857
+ const list = state.file_pull_holds ?? [];
26858
+ const at = list.findIndex((h) => h.target === hold.target && h.path === hold.path);
26859
+ if (at !== -1 && stableHash(list[at]) === stableHash(hold))
26860
+ return state;
26861
+ const next = at === -1 ? [...list, hold] : list.map((h, i) => i === at ? hold : h);
26862
+ return { ...state, file_pull_holds: next };
26863
+ }
26864
+ function removeFilePullHold(state, path, kind) {
26865
+ const list = state.file_pull_holds;
26866
+ const hit = (h) => h.target === "file" && h.path === path && (kind === undefined || h.kind === kind);
26867
+ if (!list?.some(hit))
26868
+ return state;
26869
+ return { ...state, file_pull_holds: list.filter((h) => !hit(h)) };
26870
+ }
26791
26871
  function fileEntryCount(state) {
26792
26872
  return Object.keys(state.files ?? {}).length;
26793
26873
  }
@@ -30588,6 +30668,9 @@ function entityPushHeldLine(f) {
30588
30668
  function entityRestoreDeferredLine(f) {
30589
30669
  return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
30590
30670
  }
30671
+ function entityPullDeferredLine(f) {
30672
+ return `[PULL_DEFERRED entityType=${f.entityType} slug=${f.slug} id=${f.id} reason=${f.reason}]`;
30673
+ }
30591
30674
  function fileRestoreDeferredLine(f) {
30592
30675
  return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
30593
30676
  }
@@ -30606,6 +30689,16 @@ function fileStashFailedLine(f) {
30606
30689
  const codePart = f.code ? ` code=${f.code}` : "";
30607
30690
  return `[FILE_STASH_FAILED path=${f.path} reason=${f.reason}${codePart}]`;
30608
30691
  }
30692
+ function fileRestoreSkippedLine(f) {
30693
+ return `[FILE_RESTORE_SKIPPED path=${f.path} row_id=${f.rowId} reason=${f.reason}]`;
30694
+ }
30695
+ function filePullSkippedLine(f) {
30696
+ return `[FILE_PULL_SKIPPED path=${f.path} row_id=${f.rowId} reason=${f.reason}]`;
30697
+ }
30698
+ function filePullHeldLine(f) {
30699
+ const retryPart = f.retryInSeconds !== undefined ? ` retry_s=${f.retryInSeconds}` : "";
30700
+ return `[FILE_PULL_HELD path=${f.path} kind=${f.kind}${retryPart}]`;
30701
+ }
30609
30702
  function fileRejectionSyncFailedLine(f) {
30610
30703
  const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
30611
30704
  if (f.path !== undefined)
@@ -34387,7 +34480,7 @@ var sessionCommand = defineCommand({
34387
34480
  });
34388
34481
 
34389
34482
  // src/commands/start.ts
34390
- import { existsSync as existsSync10, readFileSync as readFileSync31, statSync as statSync18 } from "node:fs";
34483
+ import { existsSync as existsSync10, readFileSync as readFileSync33, statSync as statSync18 } from "node:fs";
34391
34484
 
34392
34485
  // src/lib/daemon-log.ts
34393
34486
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -34460,7 +34553,20 @@ import { basename as basename4 } from "node:path";
34460
34553
  import { readFileSync as readFileSync15 } from "node:fs";
34461
34554
 
34462
34555
  // src/lib/conflicts.ts
34463
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync6, statSync as statSync5, writeFileSync as writeFileSync7 } from "node:fs";
34556
+ import { createHash as createHash3, randomBytes } from "node:crypto";
34557
+ import {
34558
+ closeSync as closeSync4,
34559
+ constants as fsConstants2,
34560
+ fstatSync as fstatSync2,
34561
+ mkdirSync as mkdirSync10,
34562
+ openSync as openSync4,
34563
+ readFileSync as readFileSync14,
34564
+ readdirSync as readdirSync6,
34565
+ renameSync as renameSync8,
34566
+ statSync as statSync5,
34567
+ unlinkSync as unlinkSync8,
34568
+ writeFileSync as writeFileSync7
34569
+ } from "node:fs";
34464
34570
  import { dirname as dirname9, join as join12 } from "node:path";
34465
34571
  function conflictsRoot(workspaceSlug) {
34466
34572
  return join12(plinthHome(), "_conflicts", workspaceSlug);
@@ -34502,13 +34608,95 @@ function fileConflictPath(args) {
34502
34608
  return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34503
34609
  }
34504
34610
  function fileConflictStem(relPath) {
34505
- return `files.${relPath.replace(/[/\\]/g, "__")}`;
34611
+ const flat = `files.${relPath.replace(/[/\\]/g, "__")}`;
34612
+ if (Buffer.byteLength(flat) + STAMP_AND_SIDE_BYTES <= NAME_MAX_BYTES && !LONG_STEM_RE.test(flat)) {
34613
+ return flat;
34614
+ }
34615
+ return `${truncateUtf8(flat, LONG_PREFIX_BYTES)}~${pathHash(relPath)}`;
34616
+ }
34617
+ var NAME_MAX_BYTES = 255;
34618
+ var STAMP_AND_SIDE_BYTES = 1 + 24 + 1 + 5;
34619
+ var PATH_HASH_HEX = 16;
34620
+ var LONG_PREFIX_BYTES = NAME_MAX_BYTES - STAMP_AND_SIDE_BYTES - 1 - PATH_HASH_HEX;
34621
+ var LONG_STEM_RE = /^(files\..*)~([0-9a-f]{16})$/;
34622
+ function pathHash(relPath) {
34623
+ return createHash3("sha256").update(relPath, "utf8").digest("hex").slice(0, PATH_HASH_HEX);
34624
+ }
34625
+ function truncateUtf8(s, maxBytes) {
34626
+ let bytes = 0;
34627
+ let out = "";
34628
+ for (const ch of s) {
34629
+ const n = Buffer.byteLength(ch);
34630
+ if (bytes + n > maxBytes)
34631
+ break;
34632
+ bytes += n;
34633
+ out += ch;
34634
+ }
34635
+ return out;
34636
+ }
34637
+ function pathRecordFile(workspaceSlug, hash2) {
34638
+ return join12(conflictsRoot(workspaceSlug), `.${hash2}.path`);
34639
+ }
34640
+ function ensurePathRecord(workspaceSlug, relPath) {
34641
+ const target = pathRecordFile(workspaceSlug, pathHash(relPath));
34642
+ if (readPathRecord(target) === relPath)
34643
+ return;
34644
+ const tmp = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
34645
+ writeFileSync7(tmp, relPath, { mode: 384 });
34646
+ try {
34647
+ renameSync8(tmp, target);
34648
+ } catch (err) {
34649
+ try {
34650
+ unlinkSync8(tmp);
34651
+ } catch {}
34652
+ throw err;
34653
+ }
34654
+ }
34655
+ function readStashFolderFile(target) {
34656
+ let fd;
34657
+ try {
34658
+ fd = openSync4(target, fsConstants2.O_RDONLY | fsConstants2.O_NONBLOCK | fsConstants2.O_NOFOLLOW);
34659
+ } catch {
34660
+ return null;
34661
+ }
34662
+ try {
34663
+ return fstatSync2(fd).isFile() ? readFileSync14(fd) : null;
34664
+ } catch {
34665
+ return null;
34666
+ } finally {
34667
+ closeSync4(fd);
34668
+ }
34669
+ }
34670
+ function readPathRecord(target) {
34671
+ return readStashFolderFile(target)?.toString("utf8") ?? null;
34672
+ }
34673
+ function prepareFileStash(workspaceSlug, relPath) {
34674
+ mkdirSync10(conflictsRoot(workspaceSlug), { recursive: true, mode: 448 });
34675
+ if (LONG_STEM_RE.test(fileConflictStem(relPath)))
34676
+ ensurePathRecord(workspaceSlug, relPath);
34677
+ }
34678
+ function resolveLongStem(workspaceSlug, head) {
34679
+ const m = LONG_STEM_RE.exec(head);
34680
+ if (!m)
34681
+ return null;
34682
+ const recorded = readPathRecord(pathRecordFile(workspaceSlug, m[2]));
34683
+ return recorded !== null && fileConflictStem(recorded) === head ? recorded : null;
34506
34684
  }
34507
34685
  function stashFileConflict(args) {
34508
- const target = fileConflictPath(args);
34509
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34510
- writeFileSync7(target, args.bytes, { mode: 384 });
34511
- return target;
34686
+ prepareFileStash(args.workspaceSlug, args.relPath);
34687
+ let stamp = args.timestamp;
34688
+ for (let attempt = 0;attempt < 1000; attempt++) {
34689
+ const target = fileConflictPath({ ...args, timestamp: stamp });
34690
+ try {
34691
+ writeFileSync7(target, args.bytes, { mode: 384, flag: "wx" });
34692
+ return target;
34693
+ } catch (err) {
34694
+ if (err?.code !== "EEXIST")
34695
+ throw err;
34696
+ stamp = new Date(stamp.getTime() + 1);
34697
+ }
34698
+ }
34699
+ throw new Error(`no free recovery-copy name for files/${args.relPath} near ${args.timestamp.toISOString()}`);
34512
34700
  }
34513
34701
  function stashFileConflictOnce(args) {
34514
34702
  const dir = conflictsRoot(args.workspaceSlug);
@@ -34523,10 +34711,13 @@ function stashFileConflictOnce(args) {
34523
34711
  const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34524
34712
  if (newest !== undefined) {
34525
34713
  const path5 = join12(dir, newest);
34526
- try {
34527
- if (Buffer.compare(args.bytes, readFileSync14(path5)) === 0)
34528
- return { path: path5, reused: true };
34529
- } catch {}
34714
+ const existing = readStashFolderFile(path5);
34715
+ if (existing !== null && Buffer.compare(args.bytes, existing) === 0) {
34716
+ try {
34717
+ prepareFileStash(args.workspaceSlug, args.relPath);
34718
+ } catch {}
34719
+ return { path: path5, reused: true };
34720
+ }
34530
34721
  }
34531
34722
  return { path: stashFileConflict(args), reused: false };
34532
34723
  }
@@ -34536,18 +34727,28 @@ function parseStamp(stamp) {
34536
34727
  const d = new Date(iso);
34537
34728
  return Number.isNaN(d.getTime()) ? null : d;
34538
34729
  }
34539
- function labelFor(file2) {
34730
+ function labelFor(file2, workspaceSlug) {
34540
34731
  const m = STAMP_RE2.exec(file2);
34541
34732
  if (!m)
34542
- return file2;
34733
+ return { label: file2 };
34543
34734
  const head = file2.slice(0, m.index);
34735
+ const exact = LONG_STEM_RE.test(head) ? resolveLongStem(workspaceSlug, head) : null;
34736
+ if (exact !== null)
34737
+ return { label: `files/${exact}`, pathRecorded: true };
34738
+ if (LONG_STEM_RE.test(head)) {
34739
+ const cut = LONG_STEM_RE.exec(head)[1];
34740
+ return {
34741
+ label: `files/${cut.slice("files.".length).replace(/__/g, "/")}…`,
34742
+ pathUnrecorded: true
34743
+ };
34744
+ }
34544
34745
  if (head.startsWith("files.")) {
34545
- return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
34746
+ return { label: `files/${head.slice("files.".length).replace(/__/g, "/")}` };
34546
34747
  }
34547
34748
  const dot = head.indexOf(".");
34548
34749
  if (dot === -1)
34549
- return head;
34550
- return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
34750
+ return { label: head };
34751
+ return { label: `${head.slice(0, dot)}/${head.slice(dot + 1)}` };
34551
34752
  }
34552
34753
  function listConflictStashes(workspaceSlug) {
34553
34754
  const dir = conflictsRoot(workspaceSlug);
@@ -34572,10 +34773,13 @@ function listConflictStashes(workspaceSlug) {
34572
34773
  continue;
34573
34774
  const m = STAMP_RE2.exec(file2);
34574
34775
  const side = m?.[2];
34776
+ const labelled = labelFor(file2, workspaceSlug);
34575
34777
  out.push({
34576
34778
  file: file2,
34577
34779
  absPath,
34578
- label: labelFor(file2),
34780
+ label: labelled.label,
34781
+ ...labelled.pathUnrecorded ? { pathUnrecorded: true } : {},
34782
+ ...labelled.pathRecorded ? { pathRecorded: true } : {},
34579
34783
  stashedAt: m ? parseStamp(m[1]) : null,
34580
34784
  side: side === "local" || side === "cloud" ? side : null,
34581
34785
  mtimeMs: st.mtimeMs
@@ -34944,26 +35148,26 @@ class DestructiveGate {
34944
35148
  }
34945
35149
 
34946
35150
  // src/lib/resolve-pending.ts
34947
- import { readFileSync as readFileSync21 } from "node:fs";
35151
+ import { readFileSync as readFileSync22 } from "node:fs";
34948
35152
 
34949
35153
  // src/lib/files/push.ts
34950
- import { readFileSync as readFileSync18, statSync as statSync7 } from "node:fs";
35154
+ import { readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
34951
35155
 
34952
35156
  // src/lib/permanent-deletions.ts
34953
- import { createHash as createHash3, randomUUID } from "node:crypto";
35157
+ import { createHash as createHash4, randomUUID } from "node:crypto";
34954
35158
  import {
34955
35159
  chmodSync,
34956
- closeSync as closeSync4,
35160
+ closeSync as closeSync5,
34957
35161
  constants as constants2,
34958
35162
  existsSync as existsSync4,
34959
35163
  fsyncSync as fsyncSync2,
34960
- fstatSync as fstatSync2,
35164
+ fstatSync as fstatSync3,
34961
35165
  lstatSync as lstatSync4,
34962
35166
  mkdirSync as mkdirSync11,
34963
- openSync as openSync4,
35167
+ openSync as openSync5,
34964
35168
  readFileSync as readFileSync17,
34965
- renameSync as renameSync8,
34966
- unlinkSync as unlinkSync8,
35169
+ renameSync as renameSync9,
35170
+ unlinkSync as unlinkSync9,
34967
35171
  writeSync as writeSync2
34968
35172
  } from "node:fs";
34969
35173
  import { dirname as dirname10, join as join13 } from "node:path";
@@ -35061,13 +35265,13 @@ function canonical(value) {
35061
35265
  return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
35062
35266
  }
35063
35267
  function canonicalPayloadDigest(value) {
35064
- return createHash3("sha256").update(canonical(value), "utf8").digest("hex");
35268
+ return createHash4("sha256").update(canonical(value), "utf8").digest("hex");
35065
35269
  }
35066
35270
  function fileCreatePayloadDigest(contentHash, mimeType) {
35067
35271
  return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
35068
35272
  }
35069
35273
  function checksum(body) {
35070
- return createHash3("sha256").update(canonical(body), "utf8").digest("hex");
35274
+ return createHash4("sha256").update(canonical(body), "utf8").digest("hex");
35071
35275
  }
35072
35276
  function emptyJournal(workspaceSlug) {
35073
35277
  return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
@@ -35139,11 +35343,11 @@ function parseJournal(workspaceSlug, raw) {
35139
35343
  return body;
35140
35344
  }
35141
35345
  function fsyncDirectory(path5) {
35142
- const fd = openSync4(path5, constants2.O_RDONLY);
35346
+ const fd = openSync5(path5, constants2.O_RDONLY);
35143
35347
  try {
35144
35348
  fsyncSync2(fd);
35145
35349
  } finally {
35146
- closeSync4(fd);
35350
+ closeSync5(fd);
35147
35351
  }
35148
35352
  }
35149
35353
  function loadPermanentDeletionJournal(workspaceSlug) {
@@ -35187,8 +35391,8 @@ function writePermanentDeletionJournal(prior, next) {
35187
35391
  `;
35188
35392
  let fd;
35189
35393
  try {
35190
- fd = openSync4(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
35191
- const stat = fstatSync2(fd);
35394
+ fd = openSync5(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
35395
+ const stat = fstatSync3(fd);
35192
35396
  if (!stat.isFile() || (stat.mode & 511) !== 384) {
35193
35397
  throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
35194
35398
  }
@@ -35197,16 +35401,16 @@ function writePermanentDeletionJournal(prior, next) {
35197
35401
  while (offset < bytes.length)
35198
35402
  offset += writeSync2(fd, bytes, offset, bytes.length - offset);
35199
35403
  fsyncSync2(fd);
35200
- closeSync4(fd);
35404
+ closeSync5(fd);
35201
35405
  fd = undefined;
35202
35406
  } catch (error52) {
35203
35407
  if (fd !== undefined) {
35204
35408
  try {
35205
- closeSync4(fd);
35409
+ closeSync5(fd);
35206
35410
  } catch {}
35207
35411
  }
35208
35412
  try {
35209
- unlinkSync8(tmp);
35413
+ unlinkSync9(tmp);
35210
35414
  } catch {}
35211
35415
  if (error52 instanceof PermanentDeletionJournalError)
35212
35416
  throw error52;
@@ -35214,7 +35418,7 @@ function writePermanentDeletionJournal(prior, next) {
35214
35418
  throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
35215
35419
  }
35216
35420
  try {
35217
- renameSync8(tmp, target);
35421
+ renameSync9(tmp, target);
35218
35422
  } catch (error52) {
35219
35423
  let installed2 = false;
35220
35424
  try {
@@ -35225,7 +35429,7 @@ function writePermanentDeletionJournal(prior, next) {
35225
35429
  installed2 = false;
35226
35430
  }
35227
35431
  try {
35228
- unlinkSync8(tmp);
35432
+ unlinkSync9(tmp);
35229
35433
  } catch {}
35230
35434
  if (!installed2) {
35231
35435
  const detail = error52 instanceof Error ? error52.message : String(error52);
@@ -35584,9 +35788,109 @@ async function prepareOrResolveGenerationCreateLocked(args) {
35584
35788
  }
35585
35789
 
35586
35790
  // src/lib/files/content-hash.ts
35587
- import { createHash as createHash4 } from "node:crypto";
35791
+ import { createHash as createHash5 } from "node:crypto";
35588
35792
  function sha256Bytes(bytes) {
35589
- return createHash4("sha256").update(bytes).digest("hex");
35793
+ return createHash5("sha256").update(bytes).digest("hex");
35794
+ }
35795
+
35796
+ // src/lib/files/keep-local.ts
35797
+ import { randomBytes as randomBytes2 } from "node:crypto";
35798
+ import { lstatSync as lstatSync5, readFileSync as readFileSync18, unlinkSync as unlinkSync10, writeFileSync as writeFileSync8 } from "node:fs";
35799
+ import { dirname as dirname11, join as join14 } from "node:path";
35800
+ function keepUnsentLocalCopy(args) {
35801
+ const read = readKeepable(args.workspaceSlug, args.abs);
35802
+ if (read.kind === "failed")
35803
+ return read.failure;
35804
+ if (read.kind === "absent")
35805
+ return null;
35806
+ const localHash = sha256Bytes(read.bytes);
35807
+ if (args.safeHashes.includes(localHash))
35808
+ return null;
35809
+ let stashed;
35810
+ try {
35811
+ stashed = stashFileConflictOnce({
35812
+ workspaceSlug: args.workspaceSlug,
35813
+ relPath: args.relPath,
35814
+ timestamp: args.now(),
35815
+ bytes: read.bytes,
35816
+ side: "local"
35817
+ });
35818
+ } catch (err) {
35819
+ return { reason: "stash_io", code: errCode(err) };
35820
+ }
35821
+ const key = `reused:${stashed.path}:${args.versionKey}`;
35822
+ if (stashed.reused && args.loggedStashNotices?.has(key))
35823
+ return null;
35824
+ if (stashed.reused)
35825
+ args.loggedStashNotices?.add(key);
35826
+ args.output?.log(fileConflictLine({ path: args.relPath, stash: stashed.path, via: args.via, reused: stashed.reused }));
35827
+ args.output?.warn(stashed.reused ? `Conflict on files/${args.relPath} → your local edit is already kept at ${stashed.path}.` : `Conflict on files/${args.relPath} → stashed local edit to ${stashed.path} (cloud-wins${args.onBoot ? " on boot" : ""}).`);
35828
+ return null;
35829
+ }
35830
+ function probeKeep(args) {
35831
+ const read = readKeepable(args.workspaceSlug, args.abs);
35832
+ if (read.kind === "failed")
35833
+ return read.failure;
35834
+ if (read.kind === "absent")
35835
+ return null;
35836
+ if (args.safeHashes.includes(sha256Bytes(read.bytes)))
35837
+ return null;
35838
+ const dir = conflictsRoot(args.workspaceSlug);
35839
+ const probe2 = join14(dir, `.keep-probe-${process.pid}-${randomBytes2(6).toString("hex")}`);
35840
+ try {
35841
+ prepareFileStash(args.workspaceSlug, args.relPath);
35842
+ writeFileSync8(probe2, read.bytes, { mode: 384 });
35843
+ } catch (err) {
35844
+ return { reason: "stash_io", code: errCode(err) };
35845
+ } finally {
35846
+ try {
35847
+ unlinkSync10(probe2);
35848
+ } catch {}
35849
+ }
35850
+ return null;
35851
+ }
35852
+ function unsentBytesAt(args) {
35853
+ const read = readKeepable(args.workspaceSlug, args.abs);
35854
+ if (read.kind === "failed")
35855
+ return read;
35856
+ if (read.kind === "absent")
35857
+ return { kind: "none" };
35858
+ return args.safeHashes.includes(sha256Bytes(read.bytes)) ? { kind: "none" } : { kind: "unsent" };
35859
+ }
35860
+ function readLocalForKeep(workspaceSlug, abs) {
35861
+ return readKeepable(workspaceSlug, abs);
35862
+ }
35863
+ function localFingerprint(abs) {
35864
+ try {
35865
+ const st = lstatSync5(abs);
35866
+ return { size: st.size, mtime_ms: st.mtimeMs, ino: st.ino };
35867
+ } catch (err) {
35868
+ return err?.code === "ENOENT" ? null : undefined;
35869
+ }
35870
+ }
35871
+ function readKeepable(workspaceSlug, abs) {
35872
+ if (hasSymlinkedComponent(workspaceSlug, dirname11(abs))) {
35873
+ return { kind: "failed", failure: { reason: "symlink" } };
35874
+ }
35875
+ let st;
35876
+ try {
35877
+ st = lstatSync5(abs);
35878
+ } catch (err) {
35879
+ if (err?.code === "ENOENT")
35880
+ return { kind: "absent" };
35881
+ return { kind: "failed", failure: { reason: "unreadable", code: errCode(err) } };
35882
+ }
35883
+ if (!st.isFile())
35884
+ return { kind: "failed", failure: { reason: "not_a_file" } };
35885
+ try {
35886
+ return { kind: "read", bytes: readFileSync18(abs) };
35887
+ } catch (err) {
35888
+ return { kind: "failed", failure: { reason: "unreadable", code: errCode(err) } };
35889
+ }
35890
+ }
35891
+ function errCode(err) {
35892
+ const code = err?.code;
35893
+ return typeof code === "string" ? code : undefined;
35590
35894
  }
35591
35895
 
35592
35896
  // src/lib/files/mime.ts
@@ -35658,23 +35962,63 @@ async function restoreFileFromCloud(args) {
35658
35962
  const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor, output } = args;
35659
35963
  const got = args.prefetched ?? await getFile({ baseUrl, pat, id, fetchImpl });
35660
35964
  const abs = filesSubtreePath(workspaceSlug, relPath);
35965
+ const ack = getFileEntry(state, relPath);
35966
+ const keptHash = args.kept ? sha256Bytes(args.kept) : undefined;
35967
+ const blocked = (cloud) => {
35968
+ const read = readCommittedFileAck(workspaceSlug, relPath);
35969
+ if (read.kind === "unknown")
35970
+ return null;
35971
+ if (read.kind === "absent")
35972
+ return ack ? "removed" : null;
35973
+ const committed = read.entry;
35974
+ if (committed.id !== id)
35975
+ return "replaced";
35976
+ const against = cloud ?? ack;
35977
+ return against && isLaterVersion(committed, against) ? "newer_ack" : null;
35978
+ };
35979
+ const unsent = (cloudHash) => {
35980
+ const committed = readCommittedFileEntry(workspaceSlug, relPath);
35981
+ const disk = unsentBytesAt({
35982
+ workspaceSlug,
35983
+ abs,
35984
+ safeHashes: [
35985
+ cloudHash,
35986
+ ack?.content_hash,
35987
+ keptHash,
35988
+ committed?.id === id ? committed.content_hash : undefined
35989
+ ]
35990
+ });
35991
+ if (disk.kind === "failed")
35992
+ return disk.failure.reason;
35993
+ return disk.kind === "unsent" ? "newer_save" : null;
35994
+ };
35995
+ const skipped = (reason2) => {
35996
+ output?.log(fileRestoreSkippedLine({ path: relPath, rowId: id, reason: reason2 }));
35997
+ return { state, outcome: "skipped", reason: reason2 };
35998
+ };
35661
35999
  if (!got.ok) {
36000
+ const reason2 = blocked(null) ?? unsent(undefined);
36001
+ if (reason2)
36002
+ return skipped(reason2);
35662
36003
  suppressor?.registerDelete(abs);
35663
36004
  deleteFilesFile(workspaceSlug, relPath);
35664
- return dropFiles(state, relPath);
36005
+ return { state: dropFiles(state, relPath), outcome: "removed" };
35665
36006
  }
35666
36007
  if (got.row.download_url === null) {
35667
36008
  output?.warn(`files/${relPath}: Plinth holds no bytes for this file, so there is nothing to restore from cloud — the local copy and its state were left exactly as they are.`);
35668
- return state;
36009
+ return { state, outcome: "no_bytes" };
35669
36010
  }
35670
36011
  const bytes = await downloadBytes(got.row.download_url, fetchImpl);
36012
+ const reason = blocked(got.row) ?? unsent(got.row.content_hash);
36013
+ if (reason)
36014
+ return skipped(reason);
35671
36015
  suppressor?.registerWrite(abs, {
35672
36016
  bodyHash: got.row.content_hash,
35673
36017
  frontmatterHash: ""
35674
36018
  });
35675
36019
  writeFilesBytes(workspaceSlug, relPath, bytes);
35676
36020
  deleteExtractedText(workspaceSlug, relPath);
35677
- return persistFiles(state, {
36021
+ const restored = persistFiles(state, {
35678
36022
  id: got.row.id,
35679
36023
  path: relPath,
35680
36024
  content_hash: got.row.content_hash,
@@ -35683,6 +36027,7 @@ async function restoreFileFromCloud(args) {
35683
36027
  updated_at: got.row.updated_at,
35684
36028
  inode: safeInode(abs)
35685
36029
  });
36030
+ return { state: restored, outcome: "restored" };
35686
36031
  }
35687
36032
  async function pushFilesUpsert(args) {
35688
36033
  return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
@@ -35740,7 +36085,7 @@ async function pushFilesUpsertLocked(args) {
35740
36085
  state: next
35741
36086
  };
35742
36087
  }
35743
- bytes = readFileSync18(absolutePath);
36088
+ bytes = readFileSync19(absolutePath);
35744
36089
  hash2 = sha256Bytes(bytes);
35745
36090
  inode = st.ino;
35746
36091
  }
@@ -35947,8 +36292,9 @@ async function pushFilesUpsertLocked(args) {
35947
36292
  side: "local"
35948
36293
  });
35949
36294
  output?.log(fileConflictLine({ path: relPath, stash: stashPath, reused }));
36295
+ let restore;
35950
36296
  if (existing) {
35951
- nextState = await restoreFileFromCloud({
36297
+ const restored = await restoreFileFromCloud({
35952
36298
  workspaceSlug,
35953
36299
  baseUrl,
35954
36300
  pat,
@@ -35958,10 +36304,16 @@ async function pushFilesUpsertLocked(args) {
35958
36304
  fetchImpl,
35959
36305
  suppressor,
35960
36306
  output,
35961
- prefetched: cloudRow
36307
+ prefetched: cloudRow,
36308
+ kept: bytes
35962
36309
  });
36310
+ nextState = restored.state;
36311
+ restore = restored.outcome;
35963
36312
  }
35964
- return { outcome: { kind: "conflict", relPath, stashPath }, state: nextState };
36313
+ return {
36314
+ outcome: { kind: "conflict", relPath, stashPath, ...restore ? { restore } : {} },
36315
+ state: nextState
36316
+ };
35965
36317
  }
35966
36318
  async function pushFilesDelete(args) {
35967
36319
  const { workspaceSlug, baseUrl, pat, relPath, state, deps } = args;
@@ -35987,7 +36339,7 @@ async function pushFilesDelete(args) {
35987
36339
  output?.log(fileDeleteLine({ path: relPath, id: existing.id, note: "already_absent" }));
35988
36340
  return { outcome: { kind: "deleted", relPath, id: existing.id }, state: dropFiles(state, relPath) };
35989
36341
  }
35990
- const nextState = await restoreFileFromCloud({
36342
+ const restore = await restoreFileFromCloud({
35991
36343
  workspaceSlug,
35992
36344
  baseUrl,
35993
36345
  pat,
@@ -35998,15 +36350,18 @@ async function pushFilesDelete(args) {
35998
36350
  suppressor,
35999
36351
  output
36000
36352
  });
36001
- return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
36353
+ return {
36354
+ outcome: { kind: "conflict", relPath, stashPath: "", restore: restore.outcome },
36355
+ state: restore.state
36356
+ };
36002
36357
  }
36003
36358
 
36004
36359
  // src/lib/push.ts
36005
- import { existsSync as existsSync5, readFileSync as readFileSync20, statSync as statSync8 } from "node:fs";
36360
+ import { existsSync as existsSync5, readFileSync as readFileSync21, statSync as statSync8 } from "node:fs";
36006
36361
  import { basename as basename5, relative as relative5 } from "node:path";
36007
36362
 
36008
36363
  // src/lib/frontmatter-held.ts
36009
- import { lstatSync as lstatSync5, readFileSync as readFileSync19 } from "node:fs";
36364
+ import { lstatSync as lstatSync6, readFileSync as readFileSync20 } from "node:fs";
36010
36365
  import { resolve as resolve4, sep as sep5 } from "node:path";
36011
36366
  function holdUnreadableFrontmatter(args) {
36012
36367
  const next = recordEntityFrontmatterUnreadable(args);
@@ -36049,9 +36404,9 @@ function stillHeld(workspaceSlug, root, recordPath) {
36049
36404
  return true;
36050
36405
  let raw;
36051
36406
  try {
36052
- if (!lstatSync5(absolutePath).isFile())
36407
+ if (!lstatSync6(absolutePath).isFile())
36053
36408
  return true;
36054
- raw = readFileSync19(absolutePath, "utf8");
36409
+ raw = readFileSync20(absolutePath, "utf8");
36055
36410
  } catch (err) {
36056
36411
  const code = err.code;
36057
36412
  return code !== "ENOENT" && code !== "ENOTDIR";
@@ -36214,7 +36569,7 @@ function stampStateFromMirror(args) {
36214
36569
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
36215
36570
  const targetSlug = args.pathSlug ?? doc2.slug;
36216
36571
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
36217
- const mirrorRaw = readFileSync20(mirrorPath, "utf8");
36572
+ const mirrorRaw = readFileSync21(mirrorPath, "utf8");
36218
36573
  const mirrorParsed = matter(mirrorRaw);
36219
36574
  const mirrorFm = mirrorParsed.data ?? {};
36220
36575
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
@@ -36367,7 +36722,7 @@ async function pushFile(args) {
36367
36722
  }
36368
36723
  let raw;
36369
36724
  try {
36370
- raw = readFileSync20(absolutePath, "utf8");
36725
+ raw = readFileSync21(absolutePath, "utf8");
36371
36726
  } catch (err) {
36372
36727
  if (err?.code === "ENOENT") {
36373
36728
  return runPushDelete({
@@ -36726,7 +37081,7 @@ async function runPushUpsert(args) {
36726
37081
  if (liveEntry !== snapshotEntry) {
36727
37082
  let onDisk = null;
36728
37083
  try {
36729
- onDisk = readFileSync20(args.absolutePath, "utf8");
37084
+ onDisk = readFileSync21(args.absolutePath, "utf8");
36730
37085
  } catch {
36731
37086
  onDisk = null;
36732
37087
  }
@@ -37188,7 +37543,7 @@ function writeCreatedIdBack(args) {
37188
37543
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
37189
37544
  return;
37190
37545
  }
37191
- const readBack = args.readBack ?? ((p) => readFileSync20(p, "utf8"));
37546
+ const readBack = args.readBack ?? ((p) => readFileSync21(p, "utf8"));
37192
37547
  if (readBack(args.absolutePath) !== args.raw)
37193
37548
  return;
37194
37549
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -37660,7 +38015,7 @@ async function pushRename(args) {
37660
38015
  };
37661
38016
  if (!updated.body_hash || !updated.frontmatter_hash) {
37662
38017
  try {
37663
- const mirrorRaw = readFileSync20(toPath, "utf8");
38018
+ const mirrorRaw = readFileSync21(toPath, "utf8");
37664
38019
  const mirrorParsed = matter(mirrorRaw);
37665
38020
  const renameFm = mirrorParsed.data ?? {};
37666
38021
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
@@ -37697,7 +38052,7 @@ async function pushRename(args) {
37697
38052
  if (patchResult.reason === "slug_conflict") {
37698
38053
  let contents2 = "";
37699
38054
  try {
37700
- contents2 = readFileSync20(toPath, "utf8");
38055
+ contents2 = readFileSync21(toPath, "utf8");
37701
38056
  } catch {}
37702
38057
  const stashPath2 = stashConflict({
37703
38058
  workspaceSlug,
@@ -37745,7 +38100,7 @@ async function pushRename(args) {
37745
38100
  }
37746
38101
  let contents = "";
37747
38102
  try {
37748
- contents = readFileSync20(toPath, "utf8");
38103
+ contents = readFileSync21(toPath, "utf8");
37749
38104
  } catch {}
37750
38105
  const stashPath = stashConflict({
37751
38106
  workspaceSlug,
@@ -37793,7 +38148,7 @@ async function drainResolvedBatches(deps) {
37793
38148
  deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
37794
38149
  applied++;
37795
38150
  } else {
37796
- await discardBatchOps(batch, deps);
38151
+ await discardBatchOps(batch, deps, mutex2);
37797
38152
  deps.output?.log(`[DESTRUCTIVE_DISCARDED batch=${batch.batch_id} ops=${batch.ops.length}]`);
37798
38153
  discarded++;
37799
38154
  }
@@ -37866,11 +38221,11 @@ async function applyFileOp(op, deps) {
37866
38221
  });
37867
38222
  deps.applyState(base2, result.state);
37868
38223
  }
37869
- async function discardBatchOps(batch, deps) {
38224
+ async function discardBatchOps(batch, deps, mutex2) {
37870
38225
  for (const op of batch.ops) {
37871
38226
  try {
37872
38227
  if (op.target === "file") {
37873
- await discardFileOp(op, deps);
38228
+ await discardFileOp(op, deps, mutex2);
37874
38229
  continue;
37875
38230
  }
37876
38231
  const strategy = requireStrategy(op.entityType);
@@ -37878,7 +38233,7 @@ async function discardBatchOps(batch, deps) {
37878
38233
  const path5 = opPath(deps.workspaceSlug, op);
37879
38234
  let local = null;
37880
38235
  try {
37881
- local = readFileSync21(path5, "utf8");
38236
+ local = readFileSync22(path5, "utf8");
37882
38237
  } catch {
37883
38238
  local = null;
37884
38239
  }
@@ -37913,35 +38268,50 @@ async function discardBatchOps(batch, deps) {
37913
38268
  }
37914
38269
  }
37915
38270
  }
37916
- async function discardFileOp(op, deps) {
37917
- const abs = filesSubtreePath(deps.workspaceSlug, op.path);
37918
- let local = null;
37919
- try {
37920
- local = readFileSync21(abs);
37921
- } catch {
37922
- local = null;
37923
- }
37924
- if (local !== null) {
37925
- stashFileConflict({
38271
+ async function discardFileOp(op, deps, mutex2) {
38272
+ await mutex2.run(`files:${op.path}`, async () => {
38273
+ const abs = filesSubtreePath(deps.workspaceSlug, op.path);
38274
+ const skip = (reason, why) => {
38275
+ deps.output?.log(fileRestoreSkippedLine({ path: op.path, rowId: op.id, reason }));
38276
+ deps.output?.warn(`Discard of files/${op.path} left the file as it is: ${why}.`);
38277
+ };
38278
+ const committed = readCommittedFileEntry(deps.workspaceSlug, op.path);
38279
+ if (committed && committed.id !== op.id) {
38280
+ skip("replaced", "another file now has that path");
38281
+ return;
38282
+ }
38283
+ const read = readLocalForKeep(deps.workspaceSlug, abs);
38284
+ if (read.kind === "failed") {
38285
+ skip(read.failure.reason, "the file there could not be read safely");
38286
+ return;
38287
+ }
38288
+ const local = read.kind === "read" ? read.bytes : null;
38289
+ if (local !== null) {
38290
+ stashFileConflict({
38291
+ workspaceSlug: deps.workspaceSlug,
38292
+ relPath: op.path,
38293
+ timestamp: (deps.now ?? (() => new Date))(),
38294
+ bytes: local,
38295
+ side: "local"
38296
+ });
38297
+ }
38298
+ const base2 = deps.getState();
38299
+ const restore = await restoreFileFromCloud({
37926
38300
  workspaceSlug: deps.workspaceSlug,
38301
+ baseUrl: deps.baseUrl,
38302
+ pat: deps.pat,
38303
+ id: op.id,
37927
38304
  relPath: op.path,
37928
- timestamp: (deps.now ?? (() => new Date))(),
37929
- bytes: local,
37930
- side: "local"
38305
+ state: base2,
38306
+ fetchImpl: deps.fetchImpl,
38307
+ output: deps.output,
38308
+ kept: local
37931
38309
  });
37932
- }
37933
- const base2 = deps.getState();
37934
- const next = await restoreFileFromCloud({
37935
- workspaceSlug: deps.workspaceSlug,
37936
- baseUrl: deps.baseUrl,
37937
- pat: deps.pat,
37938
- id: op.id,
37939
- relPath: op.path,
37940
- state: base2,
37941
- fetchImpl: deps.fetchImpl,
37942
- output: deps.output
38310
+ if (restore.outcome === "skipped") {
38311
+ deps.output?.warn(`Discard of files/${op.path} left the file as it is (${restore.reason}); daemon.log has the line.`);
38312
+ }
38313
+ deps.applyState(base2, restore.state);
37943
38314
  });
37944
- deps.applyState(base2, next);
37945
38315
  }
37946
38316
  function opPath(workspaceSlug, op) {
37947
38317
  try {
@@ -38889,10 +39259,10 @@ class FileDestructiveGate {
38889
39259
  }
38890
39260
 
38891
39261
  // src/lib/files/reconcile.ts
38892
- import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
39262
+ import { readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
38893
39263
 
38894
39264
  // src/lib/unsynced.ts
38895
- import { join as join14, relative as relative8 } from "node:path";
39265
+ import { join as join15, relative as relative8 } from "node:path";
38896
39266
  var RECORDABLE_REASONS = new Set([
38897
39267
  "rate_limit",
38898
39268
  "server",
@@ -38954,7 +39324,7 @@ async function drainUnsyncedOps(args) {
38954
39324
  attempted += 1;
38955
39325
  try {
38956
39326
  if (op.target === "entity") {
38957
- const absolutePath = join14(workspaceMirrorPath(workspaceSlug), op.path);
39327
+ const absolutePath = join15(workspaceMirrorPath(workspaceSlug), op.path);
38958
39328
  const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
38959
39329
  if (!classified) {
38960
39330
  clear("entity", op.path);
@@ -39767,9 +40137,9 @@ class NodeFsHandler {
39767
40137
  if (this.fsw.closed) {
39768
40138
  return;
39769
40139
  }
39770
- const dirname12 = sp.dirname(file2);
40140
+ const dirname13 = sp.dirname(file2);
39771
40141
  const basename7 = sp.basename(file2);
39772
- const parent = this.fsw._getWatchedDir(dirname12);
40142
+ const parent = this.fsw._getWatchedDir(dirname13);
39773
40143
  let prevStats = stats;
39774
40144
  if (parent.has(basename7))
39775
40145
  return;
@@ -39796,7 +40166,7 @@ class NodeFsHandler {
39796
40166
  prevStats = newStats2;
39797
40167
  }
39798
40168
  } catch (error52) {
39799
- this.fsw._remove(dirname12, basename7);
40169
+ this.fsw._remove(dirname13, basename7);
39800
40170
  }
39801
40171
  } else if (parent.has(basename7)) {
39802
40172
  const at = newStats.atimeMs;
@@ -40834,6 +41204,12 @@ class DebounceMap {
40834
41204
  this.pending.delete(oldestKey);
40835
41205
  this.handler(entry2.event);
40836
41206
  }
41207
+ nudge(absolutePath) {
41208
+ if (this.pending.has(absolutePath))
41209
+ return false;
41210
+ this.schedule({ kind: "upsert", absolutePath });
41211
+ return true;
41212
+ }
40837
41213
  async drain() {
40838
41214
  const entries = Array.from(this.pending.entries());
40839
41215
  this.pending.clear();
@@ -40898,6 +41274,9 @@ function startWatcher(opts, handler) {
40898
41274
  drainPending() {
40899
41275
  return debounce.drain();
40900
41276
  },
41277
+ nudge(absolutePath) {
41278
+ return debounce.nudge(absolutePath);
41279
+ },
40901
41280
  pendingCount() {
40902
41281
  return debounce.size();
40903
41282
  }
@@ -40928,7 +41307,7 @@ function readBlobForPush(args) {
40928
41307
  }
40929
41308
  let bytes;
40930
41309
  try {
40931
- bytes = readFileSync22(absolutePath);
41310
+ bytes = readFileSync23(absolutePath);
40932
41311
  } catch (err) {
40933
41312
  if (err?.code === "ENOENT")
40934
41313
  return { kind: "gone" };
@@ -41989,10 +42368,10 @@ function anyUnknown(state) {
41989
42368
  }
41990
42369
 
41991
42370
  // src/lib/memory/write.ts
41992
- import { readFileSync as readFileSync23, statSync as statSync11 } from "node:fs";
42371
+ import { readFileSync as readFileSync24, statSync as statSync11 } from "node:fs";
41993
42372
 
41994
42373
  // src/lib/files/extracted-text.ts
41995
- import { createHash as createHash5 } from "node:crypto";
42374
+ import { createHash as createHash6 } from "node:crypto";
41996
42375
  var DERIVED_TEXT_VERSION = 2;
41997
42376
  function yamlString3(value) {
41998
42377
  return JSON.stringify(value);
@@ -42066,7 +42445,7 @@ ${doc2.text}
42066
42445
  `;
42067
42446
  }
42068
42447
  function extractedTextHash(rendered) {
42069
- return createHash5("sha256").update(rendered, "utf8").digest("hex");
42448
+ return createHash6("sha256").update(rendered, "utf8").digest("hex");
42070
42449
  }
42071
42450
 
42072
42451
  // src/lib/memory/splice.ts
@@ -42183,7 +42562,7 @@ function writeDocumentAnnotation(args) {
42183
42562
  }
42184
42563
  let current;
42185
42564
  try {
42186
- current = readFileSync23(target, "utf8");
42565
+ current = readFileSync24(target, "utf8");
42187
42566
  } catch {
42188
42567
  return { kind: "absent" };
42189
42568
  }
@@ -44142,7 +44521,7 @@ class PullSuppressor {
44142
44521
  }
44143
44522
 
44144
44523
  // src/lib/discover-entities.ts
44145
- import { readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
44524
+ import { readFileSync as readFileSync25, statSync as statSync12 } from "node:fs";
44146
44525
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
44147
44526
  function discoveryWriteCap() {
44148
44527
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -44246,7 +44625,7 @@ function planDiscoveryWrites(args) {
44246
44625
  counts.discovered++;
44247
44626
  let raw;
44248
44627
  try {
44249
- raw = readFileSync24(absolutePath, "utf8");
44628
+ raw = readFileSync25(absolutePath, "utf8");
44250
44629
  } catch {
44251
44630
  counts.discovered--;
44252
44631
  continue;
@@ -44494,7 +44873,7 @@ function recordDiscoveryFailure(args) {
44494
44873
  }
44495
44874
 
44496
44875
  // src/lib/frontmatter-id-backfill.ts
44497
- import { readFileSync as readFileSync25, statSync as statSync13 } from "node:fs";
44876
+ import { readFileSync as readFileSync26, statSync as statSync13 } from "node:fs";
44498
44877
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
44499
44878
  function backfillWorthReporting(r) {
44500
44879
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -44537,7 +44916,7 @@ function safeInode3(path5) {
44537
44916
  function backfillFrontmatterIds(args) {
44538
44917
  const { workspaceSlug, state } = args;
44539
44918
  const compose = args.compose ?? withFrontmatterId;
44540
- const readRaw = args.readFile ?? ((p) => readFileSync25(p, "utf8"));
44919
+ const readRaw = args.readFile ?? ((p) => readFileSync26(p, "utf8"));
44541
44920
  const result = emptyResult();
44542
44921
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
44543
44922
  return result;
@@ -44665,7 +45044,7 @@ function backfillFrontmatterIds(args) {
44665
45044
  }
44666
45045
 
44667
45046
  // src/lib/reconcile.ts
44668
- import { readFileSync as readFileSync26, statSync as statSync14 } from "node:fs";
45047
+ import { readFileSync as readFileSync27, statSync as statSync14 } from "node:fs";
44669
45048
  async function reconcileDivergedRows(args) {
44670
45049
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
44671
45050
  const now = args.now ?? (() => new Date);
@@ -44777,7 +45156,7 @@ async function reconcileDivergedRows(args) {
44777
45156
  }
44778
45157
  let content;
44779
45158
  try {
44780
- content = readFileSync26(absolutePath, "utf8");
45159
+ content = readFileSync27(absolutePath, "utf8");
44781
45160
  } catch (err) {
44782
45161
  if (err?.code === "ENOENT") {
44783
45162
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -46142,17 +46521,17 @@ function createThrottledFetch(baseFetch, opts = {}) {
46142
46521
  // src/commands/sync.ts
46143
46522
  import {
46144
46523
  existsSync as existsSync9,
46145
- lstatSync as lstatSync7,
46524
+ lstatSync as lstatSync8,
46146
46525
  readdirSync as readdirSync7,
46147
- readFileSync as readFileSync30,
46526
+ readFileSync as readFileSync32,
46148
46527
  statSync as statSync17,
46149
- unlinkSync as unlinkSync9
46528
+ unlinkSync as unlinkSync11
46150
46529
  } from "node:fs";
46151
- import { join as join18 } from "node:path";
46530
+ import { join as join19 } from "node:path";
46152
46531
 
46153
46532
  // src/lib/conflicts-view.ts
46154
46533
  import { existsSync as existsSync7 } from "node:fs";
46155
- import { join as join17 } from "node:path";
46534
+ import { join as join18 } from "node:path";
46156
46535
  var NO_GENERATED_MEMORY = {
46157
46536
  artistMemoryGenerated: () => false,
46158
46537
  workspaceMemoryGenerated: () => false,
@@ -46194,11 +46573,13 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
46194
46573
  if (root === null || !stash.label.startsWith("files/")) {
46195
46574
  return { kind: "entity", stash };
46196
46575
  }
46576
+ if (stash.pathUnrecorded)
46577
+ return { kind: "unsafe-path", stash };
46197
46578
  const rel = stash.label.slice("files/".length);
46198
46579
  if (validateFilesRelPath(rel) === null) {
46199
46580
  return { kind: "unsafe-path", stash };
46200
46581
  }
46201
- const abs = join17(root, "files", rel);
46582
+ const abs = join18(root, "files", rel);
46202
46583
  if (hasSymlinkedComponent(workspaceSlug, abs)) {
46203
46584
  return { kind: "unsafe-path", stash };
46204
46585
  }
@@ -46265,7 +46646,9 @@ function renderRow(row) {
46265
46646
  `);
46266
46647
  }
46267
46648
  out.push("To put your version back, run this and then delete the stash:", "", "```sh", `cp ${shellQuote2(stash.absPath)} ${shellQuote2(row.mirrorPath)}`, "```");
46268
- if (!row.mirrorPathExists) {
46649
+ if (!row.mirrorPathExists && stash.pathRecorded) {
46650
+ out.push("", "**Nothing is at that path right now**, so the cloud copy was probably", "deleted. Open the stash and check before running it.");
46651
+ } else if (!row.mirrorPathExists) {
46269
46652
  out.push("", "**Nothing is at that path right now.** It is derived from the stash", "filename, which cannot represent a `/` and a `__` differently, so either", "the cloud copy was deleted, or the original path contained `__` and the", "line above names the wrong file. Open the stash and check before running it.");
46270
46653
  }
46271
46654
  return out.join(`
@@ -46328,17 +46711,16 @@ function refreshConflictsView(workspaceSlug) {
46328
46711
  }
46329
46712
 
46330
46713
  // src/lib/files/pull.ts
46331
- import { existsSync as existsSync8, lstatSync as lstatSync6, readFileSync as readFileSync28, statSync as statSync15 } from "node:fs";
46332
- import { dirname as dirname13 } from "node:path";
46714
+ import { existsSync as existsSync8, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
46333
46715
 
46334
46716
  // src/lib/permanent-disposition.ts
46335
- import { createHash as createHash6 } from "node:crypto";
46336
- import { readFileSync as readFileSync27 } from "node:fs";
46717
+ import { createHash as createHash7 } from "node:crypto";
46718
+ import { readFileSync as readFileSync28 } from "node:fs";
46337
46719
  function sha256(value) {
46338
- return createHash6("sha256").update(value).digest("hex");
46720
+ return createHash7("sha256").update(value).digest("hex");
46339
46721
  }
46340
46722
  function verifyStash(path5, expectedHash) {
46341
- const actual = sha256(readFileSync27(path5));
46723
+ const actual = sha256(readFileSync28(path5));
46342
46724
  if (actual !== expectedHash) {
46343
46725
  throw new Error(`permanent deletion stash verification failed for ${path5}`);
46344
46726
  }
@@ -46382,7 +46764,7 @@ function disposePermanentDocumentLocked(args) {
46382
46764
  const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
46383
46765
  let raw;
46384
46766
  try {
46385
- raw = readFileSync27(target, "utf8");
46767
+ raw = readFileSync28(target, "utf8");
46386
46768
  } catch (error52) {
46387
46769
  if (error52?.code !== "ENOENT")
46388
46770
  throw error52;
@@ -46480,7 +46862,7 @@ function disposePermanentFileLocked(args) {
46480
46862
  const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
46481
46863
  let bytes;
46482
46864
  try {
46483
- bytes = readFileSync27(target);
46865
+ bytes = readFileSync28(target);
46484
46866
  } catch (error52) {
46485
46867
  if (error52?.code !== "ENOENT")
46486
46868
  throw error52;
@@ -46527,6 +46909,34 @@ function disposePermanentFileLocked(args) {
46527
46909
  return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
46528
46910
  }
46529
46911
 
46912
+ // src/lib/files/pull-ack.ts
46913
+ function currentFileAck(args) {
46914
+ const read = readCommittedFileAck(args.workspaceSlug, args.relPath);
46915
+ if (read.kind === "unknown")
46916
+ return args.snapshot;
46917
+ if (read.kind === "absent") {
46918
+ const retired = args.committedBase !== undefined && sameFileVersion(args.snapshot, args.committedBase);
46919
+ return retired ? undefined : args.snapshot;
46920
+ }
46921
+ const committed = read.entry;
46922
+ const known = args.snapshot ?? args.committedBase;
46923
+ if (!known)
46924
+ return committed;
46925
+ if (committed.id !== known.id) {
46926
+ if (isLaterVersion(known, committed))
46927
+ return args.snapshot;
46928
+ if (isLaterVersion(committed, known))
46929
+ return committed;
46930
+ return args.snapshot !== undefined && !sameFileVersion(args.snapshot, args.committedBase) ? args.snapshot : committed;
46931
+ }
46932
+ return isLaterVersion(committed, known) ? committed : args.snapshot;
46933
+ }
46934
+ function sameFileVersion(a, b) {
46935
+ if (!a || !b)
46936
+ return a === b;
46937
+ return a.id === b.id && a.updated_at === b.updated_at && a.content_hash === b.content_hash;
46938
+ }
46939
+
46530
46940
  // src/lib/files/pull.ts
46531
46941
  function dueFileRefreshMarkers(workspaceSlug, now) {
46532
46942
  try {
@@ -46653,7 +47063,9 @@ async function replayPermanentFiles(args, markers) {
46653
47063
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46654
47064
  forceTextRewalk: args.forceTextRewalk,
46655
47065
  loggedStashNotices: args.loggedStashNotices,
46656
- recentUnlinks: args.recentUnlinks
47066
+ recentUnlinks: args.recentUnlinks,
47067
+ committedBase: lastCommittedState,
47068
+ commit: args.commit
46657
47069
  });
46658
47070
  state = outcome.state;
46659
47071
  if (outcome.action === "written")
@@ -46726,6 +47138,7 @@ async function pullFiles(args) {
46726
47138
  let finalHighWater = null;
46727
47139
  let markRefused = false;
46728
47140
  let cursorBlocked = permanentJournalBlocked;
47141
+ const deliveredPaths = new Set;
46729
47142
  while (true) {
46730
47143
  let page;
46731
47144
  try {
@@ -46743,6 +47156,7 @@ async function pullFiles(args) {
46743
47156
  let pageTextFailed = false;
46744
47157
  for (const row of page.rows) {
46745
47158
  fetched++;
47159
+ deliveredPaths.add(row.path);
46746
47160
  if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
46747
47161
  if (permanentJournalBlocked) {
46748
47162
  pageTextFailed = true;
@@ -46773,7 +47187,9 @@ async function pullFiles(args) {
46773
47187
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46774
47188
  forceTextRewalk: args.forceTextRewalk,
46775
47189
  loggedStashNotices: args.loggedStashNotices,
46776
- recentUnlinks: args.recentUnlinks
47190
+ recentUnlinks: args.recentUnlinks,
47191
+ committedBase: pageBase,
47192
+ commit: args.commit
46777
47193
  });
46778
47194
  state = outcome.state;
46779
47195
  if (!outcome.symlinkRefused) {
@@ -46827,6 +47243,14 @@ async function pullFiles(args) {
46827
47243
  return replayPermanentFiles({ ...args, state }, markers);
46828
47244
  }
46829
47245
  }
47246
+ const pruned = pruneFilePullHolds({
47247
+ state,
47248
+ workspaceSlug,
47249
+ deliveredPaths,
47250
+ cursorPass: markRefused || args.cursor === baseCursor
47251
+ });
47252
+ if (pruned !== state)
47253
+ state = args.commit ? args.commit(state, pruned) : pruned;
46830
47254
  if (textUnknown > 0) {
46831
47255
  args.output?.warn(`${textUnknown} file ${textUnknown === 1 ? "row" : "rows"} arrived without a readable \`has_extracted_text\` — this server predates the extracted-text contract, or the field was dropped from the payload. Derived text under views/extracted/ was left untouched rather than deleted, and is not being updated either.`);
46832
47256
  }
@@ -46899,6 +47323,9 @@ async function materialiseExtractedText(args) {
46899
47323
  });
46900
47324
  } else {
46901
47325
  const got = await getFileText({ baseUrl, pat, id: row.id, fetchImpl });
47326
+ if (args.stillCurrent && !args.stillCurrent()) {
47327
+ return { textHash: undefined, written: 0, deleted: 0, stale: true };
47328
+ }
46902
47329
  if (!got.ok)
46903
47330
  return { textHash: undefined, written: 0, deleted: 0 };
46904
47331
  if (got.row.text === null) {
@@ -46960,7 +47387,10 @@ async function applyFileRow(args) {
46960
47387
  symlinkRefused: true
46961
47388
  };
46962
47389
  }
46963
- const existing = getFileEntry(state, relPath);
47390
+ const snapshotEntry = getFileEntry(state, relPath);
47391
+ const baseEntry = getFileEntry(args.committedBase, relPath);
47392
+ const ackNow = () => currentFileAck({ workspaceSlug, relPath, snapshot: snapshotEntry, committedBase: baseEntry });
47393
+ const existing = ackNow();
46964
47394
  if (row.deleted_at !== null) {
46965
47395
  if (existing && existing.id !== row.id) {
46966
47396
  const key = `${row.id}:${existing.id}:${relPath}`;
@@ -46968,40 +47398,63 @@ async function applyFileRow(args) {
46968
47398
  args.loggedTombstoneSkips?.add(key);
46969
47399
  output?.log(`[FILE_TOMBSTONE_SKIP path=${relPath} row_id=${row.id} tracked_id=${existing.id} reason=id_mismatch]`);
46970
47400
  }
46971
- return { state, action: "skipped" };
47401
+ return { state: clearHoldsOfRow(state, relPath, row.id), action: "skipped" };
46972
47402
  }
46973
47403
  let localBytes;
46974
47404
  try {
46975
- localBytes = readFileSync28(abs);
47405
+ localBytes = readFileSync29(abs);
46976
47406
  } catch {
46977
47407
  localBytes = undefined;
46978
47408
  }
46979
47409
  const isOurs = localBytes === undefined || existing !== undefined && sha256Bytes(localBytes) === existing.content_hash;
46980
47410
  if (!isOurs) {
46981
47411
  output?.log(`[FILE_TOMBSTONE_KEEP path=${relPath} row_id=${row.id} reason=${existing === undefined ? "untracked" : "diverged"}]`);
46982
- const next2 = existing ? removeFileEntry(state, relPath).state : state;
46983
- return { state: next2, action: "skipped" };
47412
+ if (!existing)
47413
+ return { state: clearHoldsOfRow(state, relPath, row.id), action: "skipped" };
47414
+ const kept = upsertFilePullHold(clearRowHolds(dropAck(state, relPath, existing, snapshotEntry, args.commit), relPath), {
47415
+ target: "file",
47416
+ path: relPath,
47417
+ kind: "kept_after_delete",
47418
+ row_id: row.id,
47419
+ row_updated_at: row.deleted_at,
47420
+ row_hash: existing.content_hash,
47421
+ local: localFingerprint(abs) ?? null,
47422
+ attempts: 1
47423
+ });
47424
+ return { state: kept, action: "skipped" };
46984
47425
  }
46985
47426
  suppressor?.registerDelete(abs);
46986
47427
  const didUnlink = deleteFilesFile(workspaceSlug, relPath);
46987
47428
  const removedText = safeDeleteExtractedText(workspaceSlug, relPath, output);
46988
47429
  const textDeleted = removedText.removed ? 1 : 0;
46989
47430
  if (existing) {
46990
- const { state: next2 } = removeFileEntry(state, relPath);
46991
47431
  return {
46992
- state: next2,
47432
+ state: clearRowHolds(dropAck(state, relPath, existing, snapshotEntry, args.commit), relPath),
46993
47433
  action: "deleted",
46994
47434
  textDeleted,
46995
47435
  textFailed: removedText.failed
46996
47436
  };
46997
47437
  }
46998
47438
  return {
46999
- state,
47439
+ state: clearRowHolds(state, relPath),
47000
47440
  action: didUnlink ? "deleted" : "skipped",
47001
47441
  textDeleted,
47002
47442
  textFailed: removedText.failed
47003
47443
  };
47004
47444
  }
47445
+ if (existing && isLaterVersion(existing, row)) {
47446
+ const key = `skip:${relPath}:${row.id}:${row.updated_at}`;
47447
+ if (!args.loggedStashNotices?.has(key)) {
47448
+ args.loggedStashNotices?.add(key);
47449
+ output?.log(filePullSkippedLine({
47450
+ path: relPath,
47451
+ rowId: row.id,
47452
+ reason: existing.id === row.id ? "older" : "replaced"
47453
+ }));
47454
+ }
47455
+ return { state: clearRowHolds(state, relPath), action: "skipped" };
47456
+ }
47457
+ const stillAt = (decided) => () => sameFileVersion(ackNow(), decided);
47005
47458
  const textSignal = extractedTextSignal(row);
47006
47459
  const textUnknown = textSignal === "unknown";
47007
47460
  if (storedBytesSignal(row) === "absent") {
@@ -47015,7 +47468,8 @@ async function applyFileRow(args) {
47015
47468
  existing,
47016
47469
  textSignal,
47017
47470
  fetchImpl,
47018
- output
47471
+ output,
47472
+ stillCurrent: stillAt(existing)
47019
47473
  });
47020
47474
  }
47021
47475
  if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
@@ -47029,9 +47483,9 @@ async function applyFileRow(args) {
47029
47483
  } else {
47030
47484
  presentAgain(args.recentUnlinks, abs);
47031
47485
  }
47032
- if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
47486
+ if (existing && existing.id === row.id && existing.content_hash === row.content_hash && existsSync8(abs)) {
47033
47487
  if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
47034
- return { state, action: "noop", textUnknown };
47488
+ return { state: clearRowHolds(state, relPath), action: "noop", textUnknown };
47035
47489
  }
47036
47490
  const text2 = await applyExtractedText({
47037
47491
  workspaceSlug,
@@ -47041,9 +47495,13 @@ async function applyFileRow(args) {
47041
47495
  relPath,
47042
47496
  bytesInMirror: true,
47043
47497
  fetchImpl,
47044
- output
47498
+ output,
47499
+ stillCurrent: stillAt(existing)
47045
47500
  });
47046
- const { state: next2 } = upsertFileEntry(state, {
47501
+ if (text2.stale) {
47502
+ return { state, action: "noop", textUnknown };
47503
+ }
47504
+ const { state: next2 } = upsertFileEntry(clearRowHolds(state, relPath), {
47047
47505
  ...existing,
47048
47506
  updated_at: row.updated_at,
47049
47507
  text_hash: textUnknown ? existing.text_hash : text2.textHash
@@ -47057,6 +47515,23 @@ async function applyFileRow(args) {
47057
47515
  textUnknown
47058
47516
  };
47059
47517
  }
47518
+ const held = stillHeld2({
47519
+ hold: getFilePullHold(state, relPath),
47520
+ row,
47521
+ abs,
47522
+ relPath,
47523
+ workspaceSlug,
47524
+ ackHash: existing?.content_hash,
47525
+ now
47526
+ });
47527
+ if (held) {
47528
+ const key = `held:${relPath}:${row.updated_at}:${held.kind}`;
47529
+ if (!args.loggedStashNotices?.has(key)) {
47530
+ args.loggedStashNotices?.add(key);
47531
+ output?.log(filePullHeldLine({ path: relPath, kind: held.kind, retryInSeconds: held.retryInSeconds }));
47532
+ }
47533
+ return held.kind === "stash_failed" ? { state, action: "skipped", stashFailed: true } : { state, action: "skipped", integrityFailed: true };
47534
+ }
47060
47535
  const got = await getFile({ baseUrl, pat, id: row.id, fetchImpl });
47061
47536
  if (!got.ok) {
47062
47537
  return { state, action: "skipped" };
@@ -47075,22 +47550,51 @@ async function applyFileRow(args) {
47075
47550
  existing,
47076
47551
  textSignal,
47077
47552
  fetchImpl,
47078
- output
47553
+ output,
47554
+ stillCurrent: stillAt(existing)
47079
47555
  });
47080
47556
  }
47081
47557
  const bytes = await downloadBytes(got.row.download_url, fetchImpl);
47082
47558
  if (sha256Bytes(bytes) !== row.content_hash) {
47083
47559
  output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
47084
- return { state, action: "skipped", integrityFailed: true };
47560
+ return {
47561
+ state: holdRow(state, getFilePullHold(state, relPath), {
47562
+ row,
47563
+ relPath,
47564
+ kind: "integrity_failed",
47565
+ abs,
47566
+ now
47567
+ }),
47568
+ action: "skipped",
47569
+ integrityFailed: true
47570
+ };
47571
+ }
47572
+ const ackAtWrite = ackNow();
47573
+ if (ackAtWrite && isLaterVersion(ackAtWrite, row)) {
47574
+ const key = `skip:${relPath}:${row.id}:${row.updated_at}`;
47575
+ if (!args.loggedStashNotices?.has(key)) {
47576
+ args.loggedStashNotices?.add(key);
47577
+ output?.log(filePullSkippedLine({
47578
+ path: relPath,
47579
+ rowId: row.id,
47580
+ reason: ackAtWrite.id === row.id ? "older" : "replaced"
47581
+ }));
47582
+ }
47583
+ return { state: clearRowHolds(state, relPath), action: "skipped" };
47584
+ }
47585
+ const ackIsRow = ackAtWrite !== undefined && ackAtWrite.id === row.id && ackAtWrite.updated_at === row.updated_at && ackAtWrite.content_hash === row.content_hash;
47586
+ if (ackIsRow && (!sameFileVersion(ackAtWrite, existing) || unsentBytesAt({ workspaceSlug, abs, safeHashes: [row.content_hash] }).kind === "unsent")) {
47587
+ return { state: clearRowHolds(state, relPath), action: "noop", textUnknown };
47085
47588
  }
47086
- const stashFailure = stashUnpushedLocalEdit({
47589
+ const stashFailure = keepUnsentLocalCopy({
47087
47590
  workspaceSlug,
47088
47591
  relPath,
47089
47592
  abs,
47090
- existing,
47091
- rowHash: row.content_hash,
47092
- ambient: suppressor !== undefined,
47593
+ safeHashes: [row.content_hash, ackAtWrite?.content_hash],
47093
47594
  now,
47595
+ via: "pull",
47596
+ versionKey: row.content_hash,
47597
+ onBoot: suppressor === undefined,
47094
47598
  output,
47095
47599
  loggedStashNotices: args.loggedStashNotices
47096
47600
  });
@@ -47100,7 +47604,18 @@ async function applyFileRow(args) {
47100
47604
  args.loggedStashNotices?.add(key);
47101
47605
  output?.log(fileStashFailedLine({ path: relPath, ...stashFailure }));
47102
47606
  }
47103
- return { state, action: "skipped", stashFailed: true };
47607
+ return {
47608
+ state: holdRow(state, getFilePullHold(state, relPath), {
47609
+ row,
47610
+ relPath,
47611
+ kind: "stash_failed",
47612
+ abs,
47613
+ now,
47614
+ failure: stashFailure
47615
+ }),
47616
+ action: "skipped",
47617
+ stashFailed: true
47618
+ };
47104
47619
  }
47105
47620
  suppressor?.registerWrite(abs, { bodyHash: row.content_hash, frontmatterHash: "" });
47106
47621
  writeFilesBytes(workspaceSlug, relPath, bytes);
@@ -47126,7 +47641,8 @@ async function applyFileRow(args) {
47126
47641
  relPath,
47127
47642
  bytesInMirror: true,
47128
47643
  fetchImpl,
47129
- output
47644
+ output,
47645
+ stillCurrent: stillAt(ackAtWrite)
47130
47646
  });
47131
47647
  }
47132
47648
  let inode;
@@ -47135,7 +47651,7 @@ async function applyFileRow(args) {
47135
47651
  } catch {
47136
47652
  inode = undefined;
47137
47653
  }
47138
- const { state: next } = upsertFileEntry(state, {
47654
+ const { state: next } = upsertFileEntry(clearRowHolds(state, relPath), {
47139
47655
  id: row.id,
47140
47656
  path: relPath,
47141
47657
  content_hash: row.content_hash,
@@ -47171,7 +47687,8 @@ async function applyPointerRow(args) {
47171
47687
  relPath,
47172
47688
  bytesInMirror,
47173
47689
  fetchImpl: args.fetchImpl,
47174
- output: args.output
47690
+ output: args.output,
47691
+ stillCurrent: args.stillCurrent
47175
47692
  });
47176
47693
  return {
47177
47694
  state,
@@ -47183,53 +47700,87 @@ async function applyPointerRow(args) {
47183
47700
  pointer: true
47184
47701
  };
47185
47702
  }
47186
- function stashUnpushedLocalEdit(args) {
47187
- const { workspaceSlug, relPath, abs, existing, output } = args;
47188
- if (hasSymlinkedComponent(workspaceSlug, dirname13(abs)))
47189
- return { reason: "symlink" };
47190
- let st;
47191
- try {
47192
- st = lstatSync6(abs);
47193
- } catch (err) {
47194
- if (err?.code === "ENOENT")
47195
- return null;
47196
- return { reason: "unreadable", code: errCode(err) };
47197
- }
47198
- if (!st.isFile())
47199
- return { reason: "not_a_file" };
47200
- let localBytes;
47201
- try {
47202
- localBytes = readFileSync28(abs);
47203
- } catch (err) {
47204
- return { reason: "unreadable", code: errCode(err) };
47703
+ function clearRowHolds(state, relPath) {
47704
+ return removeFilePullHold(removeFilePullHold(state, relPath, "stash_failed"), relPath, "integrity_failed");
47705
+ }
47706
+ function clearHoldsOfRow(state, relPath, rowId) {
47707
+ const hold = getFilePullHold(state, relPath);
47708
+ return hold && hold.row_id === rowId ? clearRowHolds(state, relPath) : state;
47709
+ }
47710
+ function dropAck(state, relPath, ack, snapshotEntry, commit2) {
47711
+ const next = removeFileEntry(state, relPath).state;
47712
+ if (commit2 && !sameFileVersion(snapshotEntry, ack)) {
47713
+ commit2(upsertFileEntry(state, ack).state, next);
47205
47714
  }
47206
- const localHash = sha256Bytes(localBytes);
47207
- if (localHash === args.rowHash || localHash === existing?.content_hash)
47715
+ return next;
47716
+ }
47717
+ function holdRow(state, prior, args) {
47718
+ const { row, relPath, kind, now } = args;
47719
+ const sameVersion = prior?.kind === kind && prior.row_id === row.id && prior.row_updated_at === row.updated_at && prior.row_hash === row.content_hash;
47720
+ const attempts = sameVersion ? prior.attempts + 1 : 1;
47721
+ const at = now();
47722
+ return upsertFilePullHold(clearRowHolds(state, relPath), {
47723
+ target: "file",
47724
+ path: relPath,
47725
+ kind,
47726
+ row_id: row.id,
47727
+ row_updated_at: row.updated_at,
47728
+ row_hash: row.content_hash,
47729
+ ...args.failure ? { reason: args.failure.reason } : {},
47730
+ ...args.failure?.code ? { code: args.failure.code } : {},
47731
+ local: localFingerprint(args.abs) ?? null,
47732
+ attempts,
47733
+ ...kind === "integrity_failed" ? { next_retry_at: new Date(at.getTime() + integrityBackoffMs(attempts)).toISOString() } : {}
47734
+ });
47735
+ }
47736
+ var INTEGRITY_BACKOFF_MS = 60000;
47737
+ var INTEGRITY_BACKOFF_CAP_MS = 3600000;
47738
+ function integrityBackoffMs(attempts) {
47739
+ return Math.min(INTEGRITY_BACKOFF_MS * 2 ** Math.max(0, attempts - 1), INTEGRITY_BACKOFF_CAP_MS);
47740
+ }
47741
+ function stillHeld2(args) {
47742
+ const { hold, row } = args;
47743
+ if (!hold || hold.row_id !== row.id || hold.row_updated_at !== row.updated_at || hold.row_hash !== row.content_hash) {
47208
47744
  return null;
47209
- let stashed;
47210
- try {
47211
- stashed = stashFileConflictOnce({
47212
- workspaceSlug,
47213
- relPath,
47214
- timestamp: args.now(),
47215
- bytes: localBytes,
47216
- side: "local"
47217
- });
47218
- } catch (err) {
47219
- return { reason: "stash_io", code: errCode(err) };
47220
47745
  }
47221
- const key = `reused:${stashed.path}:${args.rowHash}`;
47222
- if (stashed.reused && args.loggedStashNotices?.has(key))
47746
+ if (hold.kind === "integrity_failed") {
47747
+ const due = hold.next_retry_at ? Date.parse(hold.next_retry_at) : Number.NaN;
47748
+ const wait = due - args.now().getTime();
47749
+ return Number.isFinite(wait) && wait > 0 ? { kind: "integrity_failed", retryInSeconds: Math.ceil(wait / 1000) } : null;
47750
+ }
47751
+ if (hold.kind !== "stash_failed")
47223
47752
  return null;
47224
- if (stashed.reused)
47225
- args.loggedStashNotices?.add(key);
47226
- output?.log(fileConflictLine({ path: relPath, stash: stashed.path, via: "pull", reused: stashed.reused }));
47227
- output?.warn(stashed.reused ? `Conflict on files/${relPath} → your local edit is already kept at ${stashed.path}.` : `Conflict on files/${relPath} → stashed local edit to ${stashed.path} (cloud-wins${args.ambient ? "" : " on boot"}).`);
47228
- return null;
47753
+ const local = localFingerprint(args.abs);
47754
+ const unchanged = local !== undefined && (local === null ? hold.local === null : hold.local != null && hold.local.size === local.size && hold.local.mtime_ms === local.mtime_ms && hold.local.ino === local.ino);
47755
+ if (!unchanged)
47756
+ return null;
47757
+ const failure = probeKeep({
47758
+ workspaceSlug: args.workspaceSlug,
47759
+ relPath: args.relPath,
47760
+ abs: args.abs,
47761
+ safeHashes: [row.content_hash, args.ackHash]
47762
+ });
47763
+ return failure ? { kind: "stash_failed" } : null;
47229
47764
  }
47230
- function errCode(err) {
47231
- const code = err?.code;
47232
- return typeof code === "string" ? code : undefined;
47765
+ function pruneFilePullHolds(args) {
47766
+ let state = args.state;
47767
+ for (const hold of listFilePullHolds(state)) {
47768
+ if (hold.kind === "kept_after_delete") {
47769
+ let gone = getFileEntry(state, hold.path) !== undefined;
47770
+ if (!gone) {
47771
+ try {
47772
+ gone = localFingerprint(filesSubtreePath(args.workspaceSlug, hold.path)) === null;
47773
+ } catch {
47774
+ gone = false;
47775
+ }
47776
+ }
47777
+ if (gone)
47778
+ state = removeFilePullHold(state, hold.path, hold.kind);
47779
+ } else if ((hold.kind === "stash_failed" || hold.kind === "integrity_failed") && args.cursorPass && !args.deliveredPaths.has(hold.path)) {
47780
+ state = removeFilePullHold(state, hold.path, hold.kind);
47781
+ }
47782
+ }
47783
+ return state;
47233
47784
  }
47234
47785
  function textIsCurrent(existing, row, workspaceSlug, relPath) {
47235
47786
  const signal = extractedTextSignal(row);
@@ -47249,7 +47800,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
47249
47800
  }
47250
47801
 
47251
47802
  // src/lib/files/rejection-sync.ts
47252
- import { readFileSync as readFileSync29, statSync as statSync16 } from "node:fs";
47803
+ import { readFileSync as readFileSync30, statSync as statSync16 } from "node:fs";
47253
47804
 
47254
47805
  // src/lib/files/rejections-api.ts
47255
47806
  var RejectionsFeedSchema = exports_external.object({
@@ -47382,7 +47933,7 @@ function settledDisposition(args) {
47382
47933
  return null;
47383
47934
  let bytes;
47384
47935
  try {
47385
- bytes = readFileSync29(absolute2);
47936
+ bytes = readFileSync30(absolute2);
47386
47937
  } catch {
47387
47938
  return null;
47388
47939
  }
@@ -47447,6 +47998,49 @@ function rearmMissingReports(state) {
47447
47998
 
47448
47999
  // src/commands/sync.ts
47449
48000
  init_output();
48001
+
48002
+ // src/lib/pull-live-gate.ts
48003
+ import { lstatSync as lstatSync7, readFileSync as readFileSync31 } from "node:fs";
48004
+ function decideLivePull(args) {
48005
+ const { workspaceSlug, strategy, live, snapshotEntry, writeSlug } = args;
48006
+ if (strategy.preserveLocalBodyOnPull)
48007
+ return { kind: "write" };
48008
+ if (strategy.type === "artists")
48009
+ return { kind: "write" };
48010
+ const absolutePath = entityFilePath(workspaceSlug, strategy.type, writeSlug);
48011
+ const ack = snapshotEntry ? committedEntryIfNewer(workspaceSlug, strategy.type, snapshotEntry) ?? snapshotEntry : readCommittedEntry(workspaceSlug, strategy.type, live.id);
48012
+ if (!ack)
48013
+ return { kind: "write" };
48014
+ const older = isLaterVersion(ack, live);
48015
+ if (ack.slug !== writeSlug)
48016
+ return older ? { kind: "skip" } : { kind: "write" };
48017
+ const newer = !older && live.updated_at !== ack.updated_at;
48018
+ let raw = null;
48019
+ try {
48020
+ raw = lstatSync7(absolutePath).isFile() ? readFileSync31(absolutePath, "utf8") : null;
48021
+ } catch {
48022
+ raw = null;
48023
+ }
48024
+ if (raw === null)
48025
+ return older ? { kind: "skip" } : { kind: "write" };
48026
+ if (frontmatterParseError(raw) !== null) {
48027
+ return { kind: "defer", reason: "unreadable", absolutePath };
48028
+ }
48029
+ const disk = computeMirrorHashes(strategy, raw);
48030
+ const matchesAck = disk.bodyHash === ack.body_hash && disk.frontmatterHash === ack.frontmatter_hash;
48031
+ if (!newer) {
48032
+ return matchesAck ? { kind: "skip" } : { kind: "defer", reason: "local_edit", absolutePath };
48033
+ }
48034
+ if (matchesAck)
48035
+ return { kind: "write" };
48036
+ const incoming = computeMirrorHashes(strategy, renderMirrorWithBodyPreserve(workspaceSlug, strategy, live, writeSlug));
48037
+ if (disk.bodyHash === incoming.bodyHash && disk.frontmatterHash === incoming.frontmatterHash) {
48038
+ return { kind: "adopt", raw };
48039
+ }
48040
+ return { kind: "defer", reason: "local_edit", absolutePath };
48041
+ }
48042
+
48043
+ // src/commands/sync.ts
47450
48044
  var memberRefreshFailedAt = new Map;
47451
48045
  var MEMBER_REFRESH_RETRY_AFTER_MS = 60 * 60 * 1000;
47452
48046
  function classifyApiError(err) {
@@ -47468,7 +48062,7 @@ function hasExistingContent(workspacePath) {
47468
48062
  if (!e.isDirectory())
47469
48063
  return true;
47470
48064
  try {
47471
- return readdirSync7(join18(workspacePath, e.name)).length > 0;
48065
+ return readdirSync7(join19(workspacePath, e.name)).length > 0;
47472
48066
  } catch {
47473
48067
  return true;
47474
48068
  }
@@ -47498,7 +48092,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
47498
48092
  }
47499
48093
  let raw;
47500
48094
  try {
47501
- raw = readFileSync30(target, "utf8");
48095
+ raw = readFileSync32(target, "utf8");
47502
48096
  } catch (err) {
47503
48097
  if (err?.code === "ENOENT") {
47504
48098
  return { didUnlink: false };
@@ -47516,7 +48110,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
47516
48110
  }
47517
48111
  try {
47518
48112
  suppressor?.registerDelete(target);
47519
- unlinkSync9(target);
48113
+ unlinkSync11(target);
47520
48114
  return { didUnlink: true };
47521
48115
  } catch (err) {
47522
48116
  if (err?.code === "ENOENT") {
@@ -47623,7 +48217,7 @@ async function replayPermanentDocuments(args) {
47623
48217
  for (const row of rows) {
47624
48218
  if (row.deleted_at !== null && row.permanent_delete === true)
47625
48219
  continue;
47626
- const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks);
48220
+ const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks, args.deferral);
47627
48221
  state = outcome.state;
47628
48222
  if (outcome.action === "written")
47629
48223
  counters.written++;
@@ -47760,6 +48354,10 @@ async function runSync(workspaceSlug, deps = {}) {
47760
48354
  committed = state;
47761
48355
  };
47762
48356
  const counters = {};
48357
+ const deferral = {
48358
+ onPushDeferred: deps.onPushDeferred,
48359
+ logged: deps.loggedPullDeferrals
48360
+ };
47763
48361
  let restoresDeferred = 0;
47764
48362
  try {
47765
48363
  for (const entityType of allEntityTypes()) {
@@ -47783,6 +48381,7 @@ async function runSync(workspaceSlug, deps = {}) {
47783
48381
  isFrozen: deps.isFrozen,
47784
48382
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47785
48383
  recentUnlinks: deps.recentUnlinks,
48384
+ deferral,
47786
48385
  commit: (before, after) => {
47787
48386
  const next = commitState(before, after);
47788
48387
  committed = next;
@@ -47868,7 +48467,7 @@ async function runSync(workspaceSlug, deps = {}) {
47868
48467
  }
47869
48468
  let outcome;
47870
48469
  try {
47871
- outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks);
48470
+ outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks, deferral);
47872
48471
  } catch (err) {
47873
48472
  if (!(err instanceof MirrorSymlinkError))
47874
48473
  throw err;
@@ -47928,6 +48527,7 @@ async function runSync(workspaceSlug, deps = {}) {
47928
48527
  isFrozen: deps.isFrozen,
47929
48528
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47930
48529
  recentUnlinks: deps.recentUnlinks,
48530
+ deferral,
47931
48531
  commit: (before, after) => {
47932
48532
  const next = commitState(before, after);
47933
48533
  committed = next;
@@ -48221,7 +48821,7 @@ async function runSync(workspaceSlug, deps = {}) {
48221
48821
  out.success(`Done in ${elapsed}s.`);
48222
48822
  return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
48223
48823
  }
48224
- function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks) {
48824
+ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks, deferral) {
48225
48825
  if (isFrozen?.(strategy.type, row.id)) {
48226
48826
  return { state, action: "skipped" };
48227
48827
  }
@@ -48372,10 +48972,38 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48372
48972
  return { state, action: "noop_overwrite" };
48373
48973
  }
48374
48974
  }
48975
+ let adoptRaw;
48976
+ if (suppressor && relocation.kind !== "moved") {
48977
+ const gate = decideLivePull({
48978
+ workspaceSlug,
48979
+ strategy,
48980
+ live,
48981
+ snapshotEntry: existingEntry,
48982
+ writeSlug
48983
+ });
48984
+ if (gate.kind === "skip")
48985
+ return { state, action: "noop_overwrite" };
48986
+ if (gate.kind === "defer") {
48987
+ const key = `${strategy.type}:${live.id}:${live.updated_at}`;
48988
+ if (!deferral?.logged?.has(key)) {
48989
+ deferral?.logged?.add(key);
48990
+ out.log(entityPullDeferredLine({
48991
+ entityType: strategy.type,
48992
+ slug: writeSlug,
48993
+ id: live.id,
48994
+ reason: gate.reason
48995
+ }));
48996
+ deferral?.onPushDeferred?.(gate.absolutePath);
48997
+ }
48998
+ return { state, action: "deferred" };
48999
+ }
49000
+ if (gate.kind === "adopt")
49001
+ adoptRaw = gate.raw;
49002
+ }
48375
49003
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
48376
49004
  let localRaw = null;
48377
49005
  try {
48378
- localRaw = readFileSync30(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
49006
+ localRaw = readFileSync32(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
48379
49007
  } catch {
48380
49008
  localRaw = null;
48381
49009
  }
@@ -48402,7 +49030,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48402
49030
  const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
48403
49031
  let localRaw = null;
48404
49032
  try {
48405
- localRaw = lstatSync7(target).isFile() ? readFileSync30(target, "utf8") : null;
49033
+ localRaw = lstatSync8(target).isFile() ? readFileSync32(target, "utf8") : null;
48406
49034
  } catch {
48407
49035
  localRaw = null;
48408
49036
  }
@@ -48430,7 +49058,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48430
49058
  }
48431
49059
  }
48432
49060
  }
48433
- const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
49061
+ const content = adoptRaw ?? writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
48434
49062
  const hashes = computeMirrorHashes(strategy, content);
48435
49063
  let inode;
48436
49064
  try {
@@ -48467,7 +49095,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48467
49095
  memory_hash: memoryHash,
48468
49096
  memory_generated: memoryGenerated
48469
49097
  });
48470
- const action = relocation.kind === "moved" ? "moved" : "written";
49098
+ const action = adoptRaw !== undefined ? "noop_overwrite" : relocation.kind === "moved" ? "moved" : "written";
48471
49099
  return { state: nextState, action };
48472
49100
  }
48473
49101
  var syncCommand = defineCommand({
@@ -48568,7 +49196,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
48568
49196
  return { state, changed: false };
48569
49197
  return { state: { ...state, entries: nextEntries }, changed: true };
48570
49198
  }
48571
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync31(p, "utf8")) {
49199
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync33(p, "utf8")) {
48572
49200
  let changed = false;
48573
49201
  const nextEntries = {};
48574
49202
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -48600,7 +49228,7 @@ function isOwnWriteEcho(suppressor, absolutePath, strategy, raw) {
48600
49228
  }
48601
49229
  function destinationIsUnreadable(absolutePath) {
48602
49230
  try {
48603
- return frontmatterParseError(readFileSync31(absolutePath, "utf8")) !== null;
49231
+ return frontmatterParseError(readFileSync33(absolutePath, "utf8")) !== null;
48604
49232
  } catch {
48605
49233
  return false;
48606
49234
  }
@@ -48721,7 +49349,7 @@ function makeWatchHandler(deps) {
48721
49349
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
48722
49350
  return;
48723
49351
  }
48724
- const bytes = readFileSync31(absolutePath);
49352
+ const bytes = readFileSync33(absolutePath);
48725
49353
  const hash2 = sha256Bytes(bytes);
48726
49354
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
48727
49355
  bodyHash: hash2,
@@ -48785,7 +49413,7 @@ function makeWatchHandler(deps) {
48785
49413
  if (suppressor) {
48786
49414
  let raw = null;
48787
49415
  try {
48788
- raw = readFileSync31(absolutePath, "utf8");
49416
+ raw = readFileSync33(absolutePath, "utf8");
48789
49417
  } catch {
48790
49418
  raw = null;
48791
49419
  }
@@ -48824,7 +49452,7 @@ function makeWatchHandler(deps) {
48824
49452
  if (suppressor) {
48825
49453
  let raw = null;
48826
49454
  try {
48827
- raw = readFileSync31(absolutePath, "utf8");
49455
+ raw = readFileSync33(absolutePath, "utf8");
48828
49456
  } catch {
48829
49457
  raw = null;
48830
49458
  }
@@ -48863,7 +49491,7 @@ function makeWatchHandler(deps) {
48863
49491
  if (suppressor) {
48864
49492
  let raw = null;
48865
49493
  try {
48866
- raw = readFileSync31(absolutePath, "utf8");
49494
+ raw = readFileSync33(absolutePath, "utf8");
48867
49495
  } catch {
48868
49496
  raw = null;
48869
49497
  }
@@ -48902,7 +49530,7 @@ function makeWatchHandler(deps) {
48902
49530
  if (suppressor) {
48903
49531
  let raw = null;
48904
49532
  try {
48905
- raw = readFileSync31(absolutePath, "utf8");
49533
+ raw = readFileSync33(absolutePath, "utf8");
48906
49534
  } catch {
48907
49535
  raw = null;
48908
49536
  }
@@ -48941,7 +49569,7 @@ function makeWatchHandler(deps) {
48941
49569
  if (suppressor) {
48942
49570
  let raw = null;
48943
49571
  try {
48944
- raw = readFileSync31(absolutePath, "utf8");
49572
+ raw = readFileSync33(absolutePath, "utf8");
48945
49573
  } catch {
48946
49574
  raw = null;
48947
49575
  }
@@ -48987,7 +49615,7 @@ function makeWatchHandler(deps) {
48987
49615
  const strategy = getStrategy(classifiedFrom.entityType);
48988
49616
  let toRaw = null;
48989
49617
  try {
48990
- toRaw = readFileSync31(event.toPath, "utf8");
49618
+ toRaw = readFileSync33(event.toPath, "utf8");
48991
49619
  } catch {
48992
49620
  toRaw = null;
48993
49621
  }
@@ -49059,7 +49687,7 @@ function makeWatchHandler(deps) {
49059
49687
  if (event.kind === "upsert") {
49060
49688
  try {
49061
49689
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
49062
- bodyHash: stableHash(readFileSync31(absolutePath, "utf8")),
49690
+ bodyHash: stableHash(readFileSync33(absolutePath, "utf8")),
49063
49691
  frontmatterHash: ""
49064
49692
  }) === true;
49065
49693
  } catch {
@@ -49114,7 +49742,7 @@ function makeWatchHandler(deps) {
49114
49742
  if (strategy) {
49115
49743
  let raw = null;
49116
49744
  try {
49117
- raw = readFileSync31(absolutePath, "utf8");
49745
+ raw = readFileSync33(absolutePath, "utf8");
49118
49746
  } catch {
49119
49747
  raw = null;
49120
49748
  }
@@ -49561,6 +50189,7 @@ async function runStart(workspaceSlug, deps = {}) {
49561
50189
  }
49562
50190
  const loggedTombstoneSkips = new Set;
49563
50191
  const loggedStashNotices = new Set;
50192
+ const loggedPullDeferrals = new Set;
49564
50193
  const initial = await runSync(workspaceSlug, {
49565
50194
  fetchImpl,
49566
50195
  keychain,
@@ -49826,7 +50455,11 @@ async function runStart(workspaceSlug, deps = {}) {
49826
50455
  isFrozen,
49827
50456
  isFileFrozen,
49828
50457
  loggedTombstoneSkips,
49829
- loggedStashNotices
50458
+ loggedStashNotices,
50459
+ onPushDeferred: (absolutePath) => {
50460
+ watcher.nudge(absolutePath);
50461
+ },
50462
+ loggedPullDeferrals
49830
50463
  }));
49831
50464
  if (result.exitCode === 0) {
49832
50465
  const applied = result.applied ?? 0;
@@ -50191,6 +50824,21 @@ function reportOutcome(spinner, slug, state, outcome) {
50191
50824
  return;
50192
50825
  }
50193
50826
  }
50827
+ function fileConflictMessage(outcome) {
50828
+ const head = `Conflict on files/${outcome.relPath}${outcome.stashPath ? ` → ${outcome.stashPath}` : ""}`;
50829
+ switch (outcome.restore) {
50830
+ case "skipped":
50831
+ return `${head} (Plinth's version was not applied here; daemon.log says why)`;
50832
+ case "removed":
50833
+ return outcome.stashPath ? `${head} (deleted in Plinth, so removed here; your copy is stashed)` : `${head} (deleted in Plinth too, and gone here)`;
50834
+ case "no_bytes":
50835
+ return `${head} (Plinth holds no bytes for it; the local file was left as it is)`;
50836
+ case "restored":
50837
+ return outcome.stashPath ? `${head} (cloud-wins; local copy stashed)` : `${head} (cloud-wins; cloud copy restored)`;
50838
+ default:
50839
+ return `${head} (already in Plinth; your copy is stashed)`;
50840
+ }
50841
+ }
50194
50842
  function reportFileOutcome(spinner, slug, state, outcome) {
50195
50843
  const summary = watchingSummary(state);
50196
50844
  switch (outcome.kind) {
@@ -50207,7 +50855,7 @@ function reportFileOutcome(spinner, slug, state, outcome) {
50207
50855
  spinner.update(`Watching ${slug} · ${summary}`);
50208
50856
  return;
50209
50857
  case "conflict":
50210
- spinner.warn(outcome.stashPath ? `Conflict on files/${outcome.relPath} → ${outcome.stashPath} (cloud-wins; local copy stashed)` : `Conflict on files/${outcome.relPath} (cloud-wins; cloud copy restored)`);
50858
+ spinner.warn(fileConflictMessage(outcome));
50211
50859
  spinner.update(`Watching ${slug} · ${summary}`);
50212
50860
  return;
50213
50861
  case "over_cap":
@@ -50347,7 +50995,7 @@ var startCommand = defineCommand({
50347
50995
  });
50348
50996
 
50349
50997
  // src/commands/status.ts
50350
- import { statSync as statSync20 } from "node:fs";
50998
+ import { existsSync as existsSync12, statSync as statSync20 } from "node:fs";
50351
50999
 
50352
51000
  // src/lib/context/context-walls.ts
50353
51001
  function prepare(raw) {
@@ -50648,14 +51296,69 @@ function assessContextWalls(workspaceSlug, deps = {}) {
50648
51296
  return { walls, played, asOf, unreadable };
50649
51297
  }
50650
51298
 
51299
+ // src/commands/status-holds.ts
51300
+ function filePullHoldLines(args) {
51301
+ const details = [];
51302
+ for (const hold of args.holds) {
51303
+ const line = holdLine(hold, args);
51304
+ if (line !== null)
51305
+ details.push(line);
51306
+ }
51307
+ const n = details.length;
51308
+ if (n === 0)
51309
+ return { headline: "No files held.", details };
51310
+ return {
51311
+ headline: `${n} file${n === 1 ? "" : "s"} held - this folder and Plinth differ. Each line says why and what clears it.`,
51312
+ details
51313
+ };
51314
+ }
51315
+ function holdLine(hold, args) {
51316
+ const name = `files/${hold.path}`;
51317
+ if (hold.kind === "kept_after_delete") {
51318
+ if (!args.fileExists(hold.path) || args.isTracked(hold.path))
51319
+ return null;
51320
+ return `${name}: deleted in Plinth, but your copy here had changes that were never uploaded, so it was kept. ` + `It goes back into Plinth as a new file the next time the daemon starts; delete it here to accept the deletion.`;
51321
+ }
51322
+ if (hold.kind === "stash_failed") {
51323
+ return `${name}: Plinth has a newer version, but your copy here could not be kept first, so it was not replaced. ` + `${keepRemedy(hold, args.conflictsDir)} It updates on the next sync after that.`;
51324
+ }
51325
+ if (hold.kind === "integrity_failed") {
51326
+ return `${name}: Plinth's copy did not match its checksum when it was downloaded, so it was not written. ` + `${retryPhrase(hold, args.now)}`;
51327
+ }
51328
+ return `${name}: held by a newer version of Plinth (${hold.kind}); update Plinth on this Mac to see why.`;
51329
+ }
51330
+ function keepRemedy(hold, conflictsDir) {
51331
+ const code = hold.code ? ` (${hold.code})` : "";
51332
+ switch (hold.reason) {
51333
+ case "symlink":
51334
+ return "A folder on the way to it is a link; replace the link with a real folder.";
51335
+ case "not_a_file":
51336
+ return "Something other than a file is at that path; move it aside.";
51337
+ case "unreadable":
51338
+ return `Your copy could not be read${code}; check its permissions.`;
51339
+ case "stash_io":
51340
+ return `A recovery copy could not be written to ${conflictsDir}${code}; free some disk space or check that folder's permissions.`;
51341
+ default:
51342
+ return `Your copy could not be kept (${hold.reason ?? "no reason recorded"}${code}); update Plinth on this Mac to see why.`;
51343
+ }
51344
+ }
51345
+ function retryPhrase(hold, now) {
51346
+ const due = hold.next_retry_at ? Date.parse(hold.next_retry_at) : Number.NaN;
51347
+ const wait = due - now.getTime();
51348
+ if (!Number.isFinite(wait) || wait <= 0)
51349
+ return "It is tried again on the next sync.";
51350
+ const minutes = Math.max(1, Math.round(wait / 60000));
51351
+ return `It is tried again in about ${minutes} minute${minutes === 1 ? "" : "s"}, or at once if the file changes in Plinth.`;
51352
+ }
51353
+
50651
51354
  // src/lib/build-stamp.ts
50652
51355
  import { execFileSync as execFileSync4 } from "node:child_process";
50653
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync32, writeFileSync as writeFileSync8 } from "node:fs";
50654
- import { dirname as dirname14, join as join19 } from "node:path";
51356
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync34, writeFileSync as writeFileSync9 } from "node:fs";
51357
+ import { dirname as dirname14, join as join20 } from "node:path";
50655
51358
  import { fileURLToPath } from "node:url";
50656
51359
  var BUILD_STAMP_FILENAME = "build-stamp.json";
50657
51360
  function stampPath(distDir) {
50658
- return join19(distDir, BUILD_STAMP_FILENAME);
51361
+ return join20(distDir, BUILD_STAMP_FILENAME);
50659
51362
  }
50660
51363
  function readHead(repoDir) {
50661
51364
  try {
@@ -50674,7 +51377,7 @@ function readHead(repoDir) {
50674
51377
  }
50675
51378
  function readBuildStamp(distDir) {
50676
51379
  try {
50677
- const parsed = JSON.parse(readFileSync32(stampPath(distDir), "utf8"));
51380
+ const parsed = JSON.parse(readFileSync34(stampPath(distDir), "utf8"));
50678
51381
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
50679
51382
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
50680
51383
  }
@@ -50686,7 +51389,7 @@ function readBuildStamp(distDir) {
50686
51389
  function findRepoRoot(fromDir) {
50687
51390
  let dir = fromDir;
50688
51391
  for (;; ) {
50689
- if (existsSync11(join19(dir, ".git")))
51392
+ if (existsSync11(join20(dir, ".git")))
50690
51393
  return dir;
50691
51394
  const parent = dirname14(dir);
50692
51395
  if (parent === dir)
@@ -50735,12 +51438,12 @@ function checkBuildStaleness(deps = {}) {
50735
51438
  }
50736
51439
 
50737
51440
  // src/lib/memory-links.ts
50738
- import { readFileSync as readFileSync33 } from "node:fs";
51441
+ import { readFileSync as readFileSync35 } from "node:fs";
50739
51442
  import { relative as relative15, sep as sep9 } from "node:path";
50740
51443
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
50741
51444
  function scanMemoryLinks(deps) {
50742
51445
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
50743
- const readFile = deps.readFile ?? ((p) => readFileSync33(p, "utf8"));
51446
+ const readFile = deps.readFile ?? ((p) => readFileSync35(p, "utf8"));
50744
51447
  let root;
50745
51448
  let absPaths;
50746
51449
  try {
@@ -50784,7 +51487,7 @@ function scanMemoryLinks(deps) {
50784
51487
  init_output();
50785
51488
 
50786
51489
  // src/lib/pending-writes.ts
50787
- import { readFileSync as readFileSync34, statSync as statSync19 } from "node:fs";
51490
+ import { readFileSync as readFileSync36, statSync as statSync19 } from "node:fs";
50788
51491
  import { relative as relative16 } from "node:path";
50789
51492
  var EMPTY3 = { writes: [], count: 0 };
50790
51493
  function reportedElsewhere(state) {
@@ -50831,7 +51534,7 @@ function scanPendingWrites(args) {
50831
51534
  continue;
50832
51535
  let hash2;
50833
51536
  try {
50834
- hash2 = sha256Bytes(readFileSync34(absolutePath));
51537
+ hash2 = sha256Bytes(readFileSync36(absolutePath));
50835
51538
  } catch {
50836
51539
  continue;
50837
51540
  }
@@ -50862,7 +51565,7 @@ function scanPendingWrites(args) {
50862
51565
  continue;
50863
51566
  let content;
50864
51567
  try {
50865
- content = readFileSync34(absolutePath, "utf8");
51568
+ content = readFileSync36(absolutePath, "utf8");
50866
51569
  } catch {
50867
51570
  continue;
50868
51571
  }
@@ -51305,6 +52008,23 @@ async function runStatus(args, deps = {}) {
51305
52008
  }
51306
52009
  out.info("");
51307
52010
  }
52011
+ const pullHolds = filePullHoldLines({
52012
+ holds: listFilePullHolds(localState),
52013
+ conflictsDir: conflictsRoot(slug),
52014
+ now,
52015
+ fileExists: (rel) => {
52016
+ try {
52017
+ return existsSync12(filesSubtreePath(slug, rel));
52018
+ } catch {
52019
+ return true;
52020
+ }
52021
+ },
52022
+ isTracked: (rel) => getFileEntry(localState, rel) !== undefined
52023
+ });
52024
+ out.info(row("Held", pullHolds.headline));
52025
+ for (const line of pullHolds.details)
52026
+ out.info(cont(line));
52027
+ out.info("");
51308
52028
  const rejected = listRejected(localState);
51309
52029
  const freshness = Object.values(localState.memory_sources?.unwritten ?? {}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
51310
52030
  const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
@@ -51771,7 +52491,7 @@ var versionCommand = defineCommand({
51771
52491
  });
51772
52492
 
51773
52493
  // src/commands/voice-gate.ts
51774
- import { readFileSync as readFileSync37 } from "node:fs";
52494
+ import { readFileSync as readFileSync39 } from "node:fs";
51775
52495
 
51776
52496
  // src/lib/voice-gate/api.ts
51777
52497
  var VoiceGateSurfaces = [
@@ -51873,15 +52593,15 @@ async function callVoiceGate(req) {
51873
52593
  }
51874
52594
 
51875
52595
  // src/lib/voice-gate/attempts.ts
51876
- import { createHash as createHash7 } from "node:crypto";
52596
+ import { createHash as createHash8 } from "node:crypto";
51877
52597
  import {
51878
52598
  mkdirSync as mkdirSync13,
51879
- readFileSync as readFileSync35,
51880
- renameSync as renameSync9,
51881
- unlinkSync as unlinkSync10,
51882
- writeFileSync as writeFileSync9
52599
+ readFileSync as readFileSync37,
52600
+ renameSync as renameSync10,
52601
+ unlinkSync as unlinkSync12,
52602
+ writeFileSync as writeFileSync10
51883
52603
  } from "node:fs";
51884
- import { join as join20 } from "node:path";
52604
+ import { join as join21 } from "node:path";
51885
52605
  var MAX_REGENS = 2;
51886
52606
  var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
51887
52607
  var AttemptEntrySchema = exports_external.object({
@@ -51894,20 +52614,20 @@ var AttemptsFileSchema = exports_external.object({
51894
52614
  entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
51895
52615
  });
51896
52616
  function attemptsDir(workspaceSlug) {
51897
- return join20(plinthHome(), "_voice_gate", workspaceSlug);
52617
+ return join21(plinthHome(), "_voice_gate", workspaceSlug);
51898
52618
  }
51899
52619
  function attemptsPath(workspaceSlug) {
51900
- return join20(attemptsDir(workspaceSlug), "attempts.json");
52620
+ return join21(attemptsDir(workspaceSlug), "attempts.json");
51901
52621
  }
51902
52622
  function attemptKey(identity) {
51903
- return createHash7("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
52623
+ return createHash8("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
51904
52624
  }
51905
52625
  function emptyFile() {
51906
52626
  return { version: 1, entries: {} };
51907
52627
  }
51908
52628
  function load2(workspaceSlug) {
51909
52629
  try {
51910
- const raw = readFileSync35(attemptsPath(workspaceSlug), "utf8");
52630
+ const raw = readFileSync37(attemptsPath(workspaceSlug), "utf8");
51911
52631
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
51912
52632
  return parsed.success ? parsed.data : emptyFile();
51913
52633
  } catch {
@@ -51928,13 +52648,13 @@ function save(workspaceSlug, file2) {
51928
52648
  const target = attemptsPath(workspaceSlug);
51929
52649
  const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
51930
52650
  mkdirSync13(dir, { recursive: true, mode: 448 });
51931
- writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
52651
+ writeFileSync10(tmp, `${JSON.stringify(file2, null, 2)}
51932
52652
  `, { mode: 384 });
51933
52653
  try {
51934
- renameSync9(tmp, target);
52654
+ renameSync10(tmp, target);
51935
52655
  } catch (err) {
51936
52656
  try {
51937
- unlinkSync10(tmp);
52657
+ unlinkSync12(tmp);
51938
52658
  } catch {}
51939
52659
  throw err;
51940
52660
  }
@@ -52070,7 +52790,7 @@ function denyUngated(detail) {
52070
52790
  }
52071
52791
 
52072
52792
  // src/lib/voice-gate/payload.ts
52073
- import { readFileSync as readFileSync36, statSync as statSync21 } from "node:fs";
52793
+ import { readFileSync as readFileSync38, statSync as statSync21 } from "node:fs";
52074
52794
  import { homedir as homedir2 } from "node:os";
52075
52795
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
52076
52796
  var MAX_BODY_BYTES = 256 * 1024;
@@ -52231,7 +52951,7 @@ function defaultReadBodyFile(path5) {
52231
52951
  if (stat4.size > MAX_BODY_BYTES) {
52232
52952
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
52233
52953
  }
52234
- return readFileSync36(path5, "utf8");
52954
+ return readFileSync38(path5, "utf8");
52235
52955
  }
52236
52956
  function parseRecipients(...raw) {
52237
52957
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -52591,7 +53311,7 @@ async function runVoiceGateDirect(args, deps = {}) {
52591
53311
  let text;
52592
53312
  if (args.file) {
52593
53313
  try {
52594
- text = readFileSync37(args.file, "utf8");
53314
+ text = readFileSync39(args.file, "utf8");
52595
53315
  } catch (err) {
52596
53316
  const msg = err instanceof Error ? err.message : "unknown error";
52597
53317
  out.error(`Could not read ${args.file}: ${msg}`);