@plinth-music/cli 0.23.1 → 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.1",
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
 
@@ -7843,8 +7858,48 @@ function emptyCorpusCounts() {
7843
7858
  return { nonMarkdown: 0, unreadable: 0 };
7844
7859
  }
7845
7860
 
7861
+ // src/lib/matter.ts
7862
+ var import_gray_matter = __toESM(require_gray_matter(), 1);
7863
+ var FRONTMATTER_IS_CODE = "it is marked as code, which Plinth never runs";
7864
+ var PATCHED = Symbol.for("plinth.matter.engines-before-patch");
7865
+ var marked = (table) => table[PATCHED];
7866
+ var refuse = {
7867
+ parse() {
7868
+ throw new Error(FRONTMATTER_IS_CODE);
7869
+ },
7870
+ stringify() {
7871
+ throw new Error(FRONTMATTER_IS_CODE);
7872
+ }
7873
+ };
7874
+ var engines = import_gray_matter.default.engines;
7875
+ if (marked(engines) === undefined) {
7876
+ const before = Object.keys(engines).sort();
7877
+ const safeLoad = engines.yaml.parse;
7878
+ Object.assign(engines, {
7879
+ javascript: refuse,
7880
+ coffee: refuse,
7881
+ yaml: { parse: (input) => safeLoad(input), stringify: engines.yaml.stringify }
7882
+ });
7883
+ Object.defineProperty(engines, PATCHED, { value: before });
7884
+ }
7885
+ var ENGINES_BEFORE_PATCH = marked(engines);
7886
+ function matter(input, read) {
7887
+ return read === undefined ? import_gray_matter.default(input) : import_gray_matter.default(input, { cache: false });
7888
+ }
7889
+ function stringifyFrontmatter(body, data) {
7890
+ const out = import_gray_matter.default.stringify({ content: body }, data);
7891
+ const bodyOnly = body.endsWith(`
7892
+ `) ? body : `${body}
7893
+ `;
7894
+ if (out === bodyOnly && import_gray_matter.default.test(body.replace(/^/, ""))) {
7895
+ return `---
7896
+ ---
7897
+ ${bodyOnly}`;
7898
+ }
7899
+ return out;
7900
+ }
7901
+
7846
7902
  // src/lib/backlinks/scan.ts
7847
- var import_gray_matter4 = __toESM(require_gray_matter(), 1);
7848
7903
  import { readFileSync as readFileSync2 } from "node:fs";
7849
7904
  import { relative, sep as sep2 } from "node:path";
7850
7905
 
@@ -7860,9 +7915,6 @@ import {
7860
7915
  } from "node:fs";
7861
7916
  import { dirname, join as join2, resolve, sep } from "node:path";
7862
7917
 
7863
- // src/lib/entities/_helpers.ts
7864
- var import_gray_matter = __toESM(require_gray_matter(), 1);
7865
-
7866
7918
  // node_modules/zod/v4/classic/external.js
7867
7919
  var exports_external = {};
7868
7920
  __export(exports_external, {
@@ -22189,7 +22241,7 @@ function buildFrontmatter(doc2, projectionKeys) {
22189
22241
  return { ...doc2.frontmatter, ...projection };
22190
22242
  }
22191
22243
  function serialiseRow(doc2, projectionKeys, bodyAccessor) {
22192
- const out = import_gray_matter.default.stringify(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22244
+ const out = stringifyFrontmatter(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22193
22245
  return out.endsWith(`
22194
22246
  `) ? out : out + `
22195
22247
  `;
@@ -22829,7 +22881,6 @@ var documentsStrategy = {
22829
22881
  };
22830
22882
 
22831
22883
  // src/lib/entities/meetings.ts
22832
- var import_gray_matter2 = __toESM(require_gray_matter(), 1);
22833
22884
  var MeetingLiveSchema = exports_external.object({
22834
22885
  ...BASE_ROW,
22835
22886
  deleted_at: exports_external.null(),
@@ -22984,7 +23035,7 @@ function serialise(doc2) {
22984
23035
  decisions: doc2.decisions,
22985
23036
  follow_up_summary: doc2.follow_up_summary
22986
23037
  });
22987
- const out = import_gray_matter2.default.stringify(body, fm);
23038
+ const out = stringifyFrontmatter(body, fm);
22988
23039
  return out.endsWith(`
22989
23040
  `) ? out : out + `
22990
23041
  `;
@@ -24475,7 +24526,6 @@ function readContextFile(workspaceSlug, relativePath) {
24475
24526
  }
24476
24527
 
24477
24528
  // src/lib/backlinks/parse.ts
24478
- var import_gray_matter3 = __toESM(require_gray_matter(), 1);
24479
24529
  var RENDERED_BLOCKS = [
24480
24530
  { name: "tasks", begin: TASKS_BLOCK_BEGIN, end: TASKS_BLOCK_END },
24481
24531
  { name: "comments", begin: COMMENTS_BLOCK_BEGIN, end: COMMENTS_BLOCK_END },
@@ -24557,7 +24607,7 @@ function frontmatterLinks(body) {
24557
24607
  return null;
24558
24608
  let data;
24559
24609
  try {
24560
- data = import_gray_matter3.default(body).data;
24610
+ data = matter(body).data;
24561
24611
  } catch {
24562
24612
  return null;
24563
24613
  }
@@ -24825,7 +24875,7 @@ function buildArtistIndex(relPaths, root, readFile) {
24825
24875
  const slug = m[1];
24826
24876
  let name = null;
24827
24877
  try {
24828
- const raw = import_gray_matter4.default(readFile(`${root}${sep2}${rel}`)).data;
24878
+ const raw = matter(readFile(`${root}${sep2}${rel}`)).data;
24829
24879
  if (typeof raw.name === "string" && raw.name.trim().length > 0) {
24830
24880
  name = raw.name.trim();
24831
24881
  }
@@ -25634,7 +25684,8 @@ var TripReasonSchema = exports_external.object({
25634
25684
  entityType: exports_external.string().optional(),
25635
25685
  typeCount: exports_external.number().int().nonnegative().optional(),
25636
25686
  typePopulation: exports_external.number().int().nonnegative().optional(),
25637
- pct: exports_external.number().optional()
25687
+ pct: exports_external.number().optional(),
25688
+ loneRemoved: exports_external.number().int().nonnegative().optional()
25638
25689
  });
25639
25690
  var PendingBatchFileSchema = exports_external.object({
25640
25691
  version: exports_external.literal(1),
@@ -25743,6 +25794,9 @@ function describeTrip(reason) {
25743
25794
  const pct = reason.pct !== undefined ? ` (${Math.round(reason.pct * 100)}%)` : "";
25744
25795
  return `${reason.typeCount}/${reason.typePopulation} ${reason.entityType}${pct} — proportion threshold`;
25745
25796
  }
25797
+ if (reason.loneRemoved !== undefined) {
25798
+ return reason.count === 1 ? "1 large overwrite — size threshold" : `${reason.count} destructive ops, including a large overwrite — size threshold`;
25799
+ }
25746
25800
  return `${reason.count} destructive ops — count threshold`;
25747
25801
  }
25748
25802
  function formatBytes(n) {
@@ -26365,6 +26419,23 @@ var RejectedWriteSchema = exports_external.object({
26365
26419
  max_bytes: exports_external.number().int().nonnegative().optional(),
26366
26420
  reported_at: exports_external.string().datetime({ offset: true }).optional()
26367
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
+ });
26368
26439
  var TeamFileRejectionSchema = exports_external.object({
26369
26440
  path: exports_external.string().min(1),
26370
26441
  reason: exports_external.string().min(1),
@@ -26392,6 +26463,7 @@ var WorkspaceStateV2Schema = exports_external.object({
26392
26463
  files: FileEntriesSchema.optional(),
26393
26464
  unsynced: exports_external.array(UnsyncedOpSchema).optional(),
26394
26465
  rejected: exports_external.array(RejectedWriteSchema).optional(),
26466
+ file_pull_holds: exports_external.array(FilePullHoldSchema).optional(),
26395
26467
  workspace_rules: WorkspaceRulesStateSchema.optional(),
26396
26468
  workspace_memory: WorkspaceMemoryStateSchema.optional(),
26397
26469
  user_memory: UserMemoryStateSchema.optional(),
@@ -26487,25 +26559,50 @@ function isLaterVersion(a, b) {
26487
26559
  return ta > tb;
26488
26560
  }
26489
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) {
26490
26594
  let raw;
26491
26595
  try {
26492
26596
  raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
26493
26597
  } catch {
26494
26598
  return;
26495
26599
  }
26496
- let committed;
26497
26600
  try {
26498
26601
  const parsed = JSON.parse(raw);
26499
- committed = parsed.entries?.[entityType]?.[entry.id];
26602
+ return parsed !== null && typeof parsed === "object" ? parsed : undefined;
26500
26603
  } catch {
26501
26604
  return;
26502
26605
  }
26503
- if (committed === undefined)
26504
- return;
26505
- const valid = StateEntrySchema.safeParse(committed);
26506
- if (!valid.success)
26507
- return;
26508
- return isLaterVersion(valid.data, entry) ? valid.data : undefined;
26509
26606
  }
26510
26607
  function mergeKey(fresh, before, after) {
26511
26608
  if (before === after)
@@ -26612,6 +26709,7 @@ function mergeWorkspaceState(fresh, before, after, wantCorrected = true) {
26612
26709
  files: mergedFiles,
26613
26710
  unsynced: mergedList(fresh.unsynced, before.unsynced, after.unsynced),
26614
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),
26615
26713
  workspace_rules: rules.value,
26616
26714
  workspace_memory: memory.value,
26617
26715
  user_memory: userMemory.value,
@@ -26749,6 +26847,27 @@ function removeFileEntry(state, path) {
26749
26847
  delete nextFiles[path];
26750
26848
  return { state: { ...state, files: nextFiles }, changed: true };
26751
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
+ }
26752
26871
  function fileEntryCount(state) {
26753
26872
  return Object.keys(state.files ?? {}).length;
26754
26873
  }
@@ -27630,7 +27749,6 @@ function decideRegen(input) {
27630
27749
  }
27631
27750
 
27632
27751
  // src/lib/context/roster.ts
27633
- var import_gray_matter5 = __toESM(require_gray_matter(), 1);
27634
27752
  import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
27635
27753
  import { join as join7 } from "node:path";
27636
27754
  function readRoster(workspaceSlug) {
@@ -27655,7 +27773,7 @@ function readRoster(workspaceSlug) {
27655
27773
  continue;
27656
27774
  }
27657
27775
  try {
27658
- const fm = import_gray_matter5.default(raw).data;
27776
+ const fm = matter(raw).data;
27659
27777
  const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
27660
27778
  const status = typeof fm.status === "string" ? fm.status : null;
27661
27779
  roster.push({ slug, name, status });
@@ -29045,7 +29163,6 @@ var SKILL_LOCATIONS = [
29045
29163
  var SECURITY_NOTE = "Scoped to a single Plinth workspace: do not read or write across " + "workspace folders; each has its own token and isolation boundary.";
29046
29164
 
29047
29165
  // src/lib/context/artist-data.ts
29048
- var import_gray_matter6 = __toESM(require_gray_matter(), 1);
29049
29166
  import { readFileSync as readFileSync10 } from "node:fs";
29050
29167
  function artistUuidBySlug(state) {
29051
29168
  const map2 = new Map;
@@ -29172,7 +29289,7 @@ function readEntityFrontmatter(workspaceSlug, entityType, slug) {
29172
29289
  return null;
29173
29290
  }
29174
29291
  try {
29175
- return import_gray_matter6.default(raw).data;
29292
+ return matter(raw).data;
29176
29293
  } catch {
29177
29294
  return null;
29178
29295
  }
@@ -30545,9 +30662,15 @@ function fileDeleteSuppressedLine(f) {
30545
30662
  function entityDeleteSuppressedLine(f) {
30546
30663
  return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30547
30664
  }
30665
+ function entityPushHeldLine(f) {
30666
+ return `[PUSH_HELD entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30667
+ }
30548
30668
  function entityRestoreDeferredLine(f) {
30549
30669
  return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
30550
30670
  }
30671
+ function entityPullDeferredLine(f) {
30672
+ return `[PULL_DEFERRED entityType=${f.entityType} slug=${f.slug} id=${f.id} reason=${f.reason}]`;
30673
+ }
30551
30674
  function fileRestoreDeferredLine(f) {
30552
30675
  return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
30553
30676
  }
@@ -30566,6 +30689,16 @@ function fileStashFailedLine(f) {
30566
30689
  const codePart = f.code ? ` code=${f.code}` : "";
30567
30690
  return `[FILE_STASH_FAILED path=${f.path} reason=${f.reason}${codePart}]`;
30568
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
+ }
30569
30702
  function fileRejectionSyncFailedLine(f) {
30570
30703
  const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
30571
30704
  if (f.path !== undefined)
@@ -30739,6 +30872,34 @@ function recordEntityRejected(args) {
30739
30872
  rejected_at: args.now().toISOString()
30740
30873
  }));
30741
30874
  }
30875
+ var FRONTMATTER_UNREADABLE_REASON = "frontmatter_unreadable";
30876
+ function frontmatterUnreadableSentence(why) {
30877
+ const reason = why.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
30878
+ return `not synced, because its frontmatter could not be read (${reason}). ` + `Fix the frontmatter and it syncs.`;
30879
+ }
30880
+ function recordEntityFrontmatterUnreadable(args) {
30881
+ let path;
30882
+ try {
30883
+ path = relative4(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
30884
+ } catch {
30885
+ path = args.absolutePath;
30886
+ }
30887
+ const message = frontmatterUnreadableSentence(args.why);
30888
+ const withoutStale = removeUnsynced(args.state, "entity", path);
30889
+ const existing = listRejected(args.state).find((o) => o.target === "entity" && o.path === path);
30890
+ if (!withoutStale.changed && existing?.reason === FRONTMATTER_UNREADABLE_REASON && existing.message === message) {
30891
+ return args.state;
30892
+ }
30893
+ return commitState(args.state, upsertRejected(withoutStale.state, {
30894
+ target: "entity",
30895
+ op: "upsert",
30896
+ path,
30897
+ status: 0,
30898
+ reason: FRONTMATTER_UNREADABLE_REASON,
30899
+ message,
30900
+ rejected_at: args.now().toISOString()
30901
+ }).state);
30902
+ }
30742
30903
  function recordFileRejected(args) {
30743
30904
  const cat = categorizeError(args.err);
30744
30905
  const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
@@ -34319,8 +34480,7 @@ var sessionCommand = defineCommand({
34319
34480
  });
34320
34481
 
34321
34482
  // src/commands/start.ts
34322
- var import_gray_matter14 = __toESM(require_gray_matter(), 1);
34323
- import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
34483
+ import { existsSync as existsSync10, readFileSync as readFileSync33, statSync as statSync18 } from "node:fs";
34324
34484
 
34325
34485
  // src/lib/daemon-log.ts
34326
34486
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -34386,188 +34546,27 @@ class DaemonLog {
34386
34546
  }
34387
34547
 
34388
34548
  // src/lib/destructive-gate.ts
34389
- var import_gray_matter7 = __toESM(require_gray_matter(), 1);
34390
- import { readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
34549
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "node:fs";
34391
34550
  import { basename as basename4 } from "node:path";
34392
- var GUTTING_RATIO_DEFAULT = 0.5;
34393
- var GUTTING_MIN_PRIOR_BYTES = 200;
34394
- function envFloat(name, def) {
34395
- const v = process.env[name];
34396
- if (v === undefined || v === "")
34397
- return def;
34398
- const n = Number.parseFloat(v);
34399
- return Number.isFinite(n) && n > 0 ? n : def;
34400
- }
34401
- function envInt(name, def) {
34402
- const v = process.env[name];
34403
- if (v === undefined || v === "")
34404
- return def;
34405
- const n = Number.parseInt(v, 10);
34406
- return Number.isFinite(n) && n > 0 ? n : def;
34407
- }
34408
- function guttingRatio() {
34409
- return envFloat("PLINTH_GATE_GUTTING_RATIO", GUTTING_RATIO_DEFAULT);
34410
- }
34411
- function isGutting(priorLen, newLen) {
34412
- if (priorLen === undefined)
34413
- return false;
34414
- if (priorLen < GUTTING_MIN_PRIOR_BYTES)
34415
- return false;
34416
- return newLen <= guttingRatio() * priorLen;
34417
- }
34418
- function classifyDestructive(event, entityType, state, deps = {}) {
34419
- if (event.kind === "renamed")
34420
- return null;
34421
- if (event.kind === "unlink") {
34422
- const entry3 = event.stateEntry;
34423
- if (!entry3)
34424
- return null;
34425
- return {
34426
- kind: "delete",
34427
- entityType,
34428
- slug: entry3.slug,
34429
- id: entry3.id,
34430
- updatedAt: entry3.updated_at,
34431
- absolutePath: event.absolutePath
34432
- };
34433
- }
34434
- const readFile = deps.readFile ?? ((p) => readFileSync14(p, "utf8"));
34435
- const statInode = deps.statInode ?? ((p) => {
34436
- try {
34437
- return statSync5(p).ino;
34438
- } catch {
34439
- return;
34440
- }
34441
- });
34442
- let raw;
34443
- try {
34444
- raw = readFile(event.absolutePath);
34445
- } catch {
34446
- return null;
34447
- }
34448
- let newLen;
34449
- try {
34450
- const body = import_gray_matter7.default(raw).content;
34451
- newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
34452
- } catch {
34453
- return null;
34454
- }
34455
- const liveInode = statInode(event.absolutePath);
34456
- const slug = basename4(event.absolutePath, ".md");
34457
- const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
34458
- if (!entry2)
34459
- return null;
34460
- if (!isGutting(entry2.body_len, newLen))
34461
- return null;
34462
- return {
34463
- kind: "overwrite",
34464
- entityType,
34465
- slug: entry2.slug,
34466
- id: entry2.id,
34467
- updatedAt: entry2.updated_at,
34468
- absolutePath: event.absolutePath,
34469
- priorLen: entry2.body_len ?? 0,
34470
- newLen
34471
- };
34472
- }
34473
- function defaultThresholds() {
34474
- return {
34475
- absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
34476
- propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
34477
- propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5)
34478
- };
34479
- }
34480
- function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
34481
- const count = ops.length;
34482
- if (count === 0)
34483
- return { tripped: false };
34484
- if (count >= thresholds.absCount) {
34485
- return { tripped: true, reason: { kind: "count", count } };
34486
- }
34487
- const byType = new Map;
34488
- for (const op of ops) {
34489
- byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
34490
- }
34491
- for (const [entityType, typeCount] of byType) {
34492
- const typePopulation = typePopulations[entityType] ?? typeCount;
34493
- if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
34494
- return {
34495
- tripped: true,
34496
- reason: {
34497
- kind: "proportion",
34498
- count,
34499
- entityType,
34500
- typeCount,
34501
- typePopulation,
34502
- pct: typeCount / typePopulation
34503
- }
34504
- };
34505
- }
34506
- }
34507
- return { tripped: false };
34508
- }
34509
- function dedupeOps(ops) {
34510
- const byKey = new Map;
34511
- for (const op of ops)
34512
- byKey.set(`${op.entityType}:${op.id}`, op);
34513
- return Array.from(byKey.values());
34514
- }
34515
- var DEFAULT_SETTLE_MS = 600;
34516
- var DEFAULT_MAX_BATCH = 500;
34517
-
34518
- class DestructiveGate {
34519
- pending = [];
34520
- timer = null;
34521
- settleMs;
34522
- maxBatch;
34523
- onSettle;
34524
- onError;
34525
- constructor(opts) {
34526
- this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
34527
- this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
34528
- this.onSettle = opts.onSettle;
34529
- this.onError = opts.onError;
34530
- }
34531
- consider(op) {
34532
- this.pending.push(op);
34533
- if (this.pending.length >= this.maxBatch) {
34534
- this.flush();
34535
- return;
34536
- }
34537
- if (this.timer)
34538
- clearTimeout(this.timer);
34539
- this.timer = setTimeout(() => {
34540
- this.flush();
34541
- }, this.settleMs);
34542
- }
34543
- async drain() {
34544
- await this.flush();
34545
- }
34546
- size() {
34547
- return this.pending.length;
34548
- }
34549
- async flush() {
34550
- if (this.timer) {
34551
- clearTimeout(this.timer);
34552
- this.timer = null;
34553
- }
34554
- if (this.pending.length === 0)
34555
- return;
34556
- const batch = this.pending;
34557
- this.pending = [];
34558
- try {
34559
- await this.onSettle(batch);
34560
- } catch (err) {
34561
- this.onError?.(err);
34562
- }
34563
- }
34564
- }
34565
34551
 
34566
- // src/lib/resolve-pending.ts
34567
- import { readFileSync as readFileSync20 } from "node:fs";
34552
+ // src/lib/pull-write.ts
34553
+ import { readFileSync as readFileSync15 } from "node:fs";
34568
34554
 
34569
34555
  // src/lib/conflicts.ts
34570
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, 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";
34571
34570
  import { dirname as dirname9, join as join12 } from "node:path";
34572
34571
  function conflictsRoot(workspaceSlug) {
34573
34572
  return join12(plinthHome(), "_conflicts", workspaceSlug);
@@ -34597,7 +34596,7 @@ function stashEntityConflictOnce(args) {
34597
34596
  if (newest !== undefined) {
34598
34597
  const path5 = join12(dir, newest);
34599
34598
  try {
34600
- if (readFileSync15(path5, "utf8") === args.contents)
34599
+ if (readFileSync14(path5, "utf8") === args.contents)
34601
34600
  return { path: path5, reused: true };
34602
34601
  } catch {}
34603
34602
  }
@@ -34609,13 +34608,95 @@ function fileConflictPath(args) {
34609
34608
  return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34610
34609
  }
34611
34610
  function fileConflictStem(relPath) {
34612
- 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;
34613
34684
  }
34614
34685
  function stashFileConflict(args) {
34615
- const target = fileConflictPath(args);
34616
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34617
- writeFileSync7(target, args.bytes, { mode: 384 });
34618
- 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()}`);
34619
34700
  }
34620
34701
  function stashFileConflictOnce(args) {
34621
34702
  const dir = conflictsRoot(args.workspaceSlug);
@@ -34630,10 +34711,13 @@ function stashFileConflictOnce(args) {
34630
34711
  const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34631
34712
  if (newest !== undefined) {
34632
34713
  const path5 = join12(dir, newest);
34633
- try {
34634
- if (Buffer.compare(args.bytes, readFileSync15(path5)) === 0)
34635
- return { path: path5, reused: true };
34636
- } 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
+ }
34637
34721
  }
34638
34722
  return { path: stashFileConflict(args), reused: false };
34639
34723
  }
@@ -34643,18 +34727,28 @@ function parseStamp(stamp) {
34643
34727
  const d = new Date(iso);
34644
34728
  return Number.isNaN(d.getTime()) ? null : d;
34645
34729
  }
34646
- function labelFor(file2) {
34730
+ function labelFor(file2, workspaceSlug) {
34647
34731
  const m = STAMP_RE2.exec(file2);
34648
34732
  if (!m)
34649
- return file2;
34733
+ return { label: file2 };
34650
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
+ }
34651
34745
  if (head.startsWith("files.")) {
34652
- return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
34746
+ return { label: `files/${head.slice("files.".length).replace(/__/g, "/")}` };
34653
34747
  }
34654
34748
  const dot = head.indexOf(".");
34655
34749
  if (dot === -1)
34656
- return head;
34657
- return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
34750
+ return { label: head };
34751
+ return { label: `${head.slice(0, dot)}/${head.slice(dot + 1)}` };
34658
34752
  }
34659
34753
  function listConflictStashes(workspaceSlug) {
34660
34754
  const dir = conflictsRoot(workspaceSlug);
@@ -34669,7 +34763,7 @@ function listConflictStashes(workspaceSlug) {
34669
34763
  const absPath = join12(dir, file2);
34670
34764
  let st;
34671
34765
  try {
34672
- st = statSync6(absPath);
34766
+ st = statSync5(absPath);
34673
34767
  } catch {
34674
34768
  continue;
34675
34769
  }
@@ -34679,10 +34773,13 @@ function listConflictStashes(workspaceSlug) {
34679
34773
  continue;
34680
34774
  const m = STAMP_RE2.exec(file2);
34681
34775
  const side = m?.[2];
34776
+ const labelled = labelFor(file2, workspaceSlug);
34682
34777
  out.push({
34683
34778
  file: file2,
34684
34779
  absPath,
34685
- label: labelFor(file2),
34780
+ label: labelled.label,
34781
+ ...labelled.pathUnrecorded ? { pathUnrecorded: true } : {},
34782
+ ...labelled.pathRecorded ? { pathRecorded: true } : {},
34686
34783
  stashedAt: m ? parseStamp(m[1]) : null,
34687
34784
  side: side === "local" || side === "cloud" ? side : null,
34688
34785
  mtimeMs: st.mtimeMs
@@ -34691,24 +34788,386 @@ function listConflictStashes(workspaceSlug) {
34691
34788
  return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
34692
34789
  }
34693
34790
 
34791
+ // src/lib/pull-write.ts
34792
+ var UNCACHED = { cache: false };
34793
+ function parseUncached(raw) {
34794
+ return matter(raw, UNCACHED);
34795
+ }
34796
+ var NOT_A_MAPPING = "it is not a list of `key: value` lines";
34797
+ var SELF_REFERENCE = "an alias in it refers to itself";
34798
+ function readFrontmatter(raw) {
34799
+ let parsed;
34800
+ try {
34801
+ parsed = parseUncached(raw);
34802
+ } catch (err) {
34803
+ return { ok: false, why: yamlReason(err) };
34804
+ }
34805
+ if (Object.prototype.toString.call(parsed.data) !== "[object Object]") {
34806
+ return { ok: false, why: NOT_A_MAPPING };
34807
+ }
34808
+ try {
34809
+ JSON.stringify(parsed.data);
34810
+ } catch {
34811
+ return { ok: false, why: SELF_REFERENCE };
34812
+ }
34813
+ return { ok: true, parsed };
34814
+ }
34815
+ function yamlReason(err) {
34816
+ const message = err instanceof Error ? err.message : String(err);
34817
+ return (message.split(`
34818
+ `)[0] ?? "").replace(/:\s*$/, "").trim();
34819
+ }
34820
+ function frontmatterParseError(raw) {
34821
+ const read = readFrontmatter(raw);
34822
+ return read.ok ? null : read.why;
34823
+ }
34824
+ function computeMirrorHashes(strategy, content) {
34825
+ const parsed = matter(content);
34826
+ const fm = parsed.data ?? {};
34827
+ const lifted = splitUserFrontmatter(strategy, fm);
34828
+ const userFm = userFrontmatter(strategy, lifted.frontmatter);
34829
+ const hashBody = hashableBody(strategy, parsed.content);
34830
+ return {
34831
+ bodyHash: stableHash(hashBody),
34832
+ frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
34833
+ bodyLen: hashBody.length,
34834
+ frontmatterId: fm["id"]
34835
+ };
34836
+ }
34837
+ function hashableBody(strategy, body) {
34838
+ return strategy.stripLocalBlocks?.(body) ?? body;
34839
+ }
34840
+ function relocateForSlugChange(args) {
34841
+ const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
34842
+ if (fromSlug === toSlug)
34843
+ return { kind: "not_needed", writeSlug: toSlug };
34844
+ if (!echo) {
34845
+ return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
34846
+ }
34847
+ if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
34848
+ return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
34849
+ }
34850
+ let fromPath;
34851
+ try {
34852
+ fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
34853
+ entityFilePath(workspaceSlug, strategy.type, toSlug);
34854
+ } catch (err) {
34855
+ if (err instanceof MirrorPathError) {
34856
+ return {
34857
+ kind: "skipped",
34858
+ writeSlug: toSlug,
34859
+ fromSlug,
34860
+ reason: "unresolvable"
34861
+ };
34862
+ }
34863
+ throw err;
34864
+ }
34865
+ let raw = null;
34866
+ try {
34867
+ raw = readFileSync15(fromPath, "utf8");
34868
+ } catch (err) {
34869
+ const code = err?.code;
34870
+ if (code === "ENOENT") {
34871
+ return { kind: "absent", writeSlug: toSlug, fromSlug };
34872
+ }
34873
+ return {
34874
+ kind: "refused",
34875
+ writeSlug: fromSlug,
34876
+ fromSlug,
34877
+ reason: "source_unreadable"
34878
+ };
34879
+ }
34880
+ let diverged = true;
34881
+ try {
34882
+ const hashes = computeMirrorHashes(strategy, raw);
34883
+ diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
34884
+ } catch {
34885
+ diverged = true;
34886
+ }
34887
+ const stashPath = diverged ? stashConflict({
34888
+ workspaceSlug,
34889
+ entityType: strategy.type,
34890
+ entitySlug: fromSlug,
34891
+ timestamp: now(),
34892
+ contents: raw,
34893
+ side: "local"
34894
+ }) : undefined;
34895
+ try {
34896
+ let registered = false;
34897
+ const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
34898
+ if (echo.watcher === "live") {
34899
+ echo.suppressor.registerDelete(fromPath);
34900
+ registered = true;
34901
+ }
34902
+ });
34903
+ if (result === "moved") {
34904
+ return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
34905
+ }
34906
+ if (registered && echo.watcher === "live") {
34907
+ echo.suppressor.cancelDelete(fromPath);
34908
+ }
34909
+ return { kind: "absent", writeSlug: toSlug, fromSlug };
34910
+ } catch (err) {
34911
+ if (err instanceof MirrorDestinationOccupiedError) {
34912
+ return {
34913
+ kind: "refused",
34914
+ writeSlug: fromSlug,
34915
+ fromSlug,
34916
+ reason: "destination_occupied",
34917
+ stashPath
34918
+ };
34919
+ }
34920
+ if (err instanceof MirrorPathError) {
34921
+ return {
34922
+ kind: "skipped",
34923
+ writeSlug: toSlug,
34924
+ fromSlug,
34925
+ reason: "unresolvable"
34926
+ };
34927
+ }
34928
+ throw err;
34929
+ }
34930
+ }
34931
+ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
34932
+ const targetSlug = pathSlug ?? doc2.slug;
34933
+ const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
34934
+ if (suppressor) {
34935
+ const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34936
+ suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
34937
+ }
34938
+ writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
34939
+ return content;
34940
+ }
34941
+ function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
34942
+ let content = strategy.serialise(doc2);
34943
+ if (strategy.preserveLocalBodyOnPull) {
34944
+ const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34945
+ let existing = null;
34946
+ try {
34947
+ existing = readFileSync15(target, "utf8");
34948
+ } catch (err) {
34949
+ if (err?.code !== "ENOENT")
34950
+ throw err;
34951
+ }
34952
+ if (existing !== null) {
34953
+ const existingParsed = matter(existing);
34954
+ const cloudParsed = matter(content);
34955
+ const stitched = stringifyFrontmatter(existingParsed.content, cloudParsed.data);
34956
+ content = stitched.endsWith(`
34957
+ `) ? stitched : stitched + `
34958
+ `;
34959
+ }
34960
+ }
34961
+ return content;
34962
+ }
34963
+
34964
+ // src/lib/destructive-gate.ts
34965
+ var GUTTING_RATIO_DEFAULT = 0.5;
34966
+ var GUTTING_MIN_PRIOR_BYTES = 200;
34967
+ function envFloat(name, def) {
34968
+ const v = process.env[name];
34969
+ if (v === undefined || v === "")
34970
+ return def;
34971
+ const n = Number.parseFloat(v);
34972
+ return Number.isFinite(n) && n > 0 ? n : def;
34973
+ }
34974
+ function envInt(name, def) {
34975
+ const v = process.env[name];
34976
+ if (v === undefined || v === "")
34977
+ return def;
34978
+ const n = Number.parseInt(v, 10);
34979
+ return Number.isFinite(n) && n > 0 ? n : def;
34980
+ }
34981
+ function guttingRatio() {
34982
+ return envFloat("PLINTH_GATE_GUTTING_RATIO", GUTTING_RATIO_DEFAULT);
34983
+ }
34984
+ function isGutting(priorLen, newLen) {
34985
+ if (priorLen === undefined)
34986
+ return false;
34987
+ if (priorLen < GUTTING_MIN_PRIOR_BYTES)
34988
+ return false;
34989
+ return newLen <= guttingRatio() * priorLen;
34990
+ }
34991
+ function classifyDestructive(event, entityType, state, deps = {}) {
34992
+ if (event.kind === "renamed")
34993
+ return null;
34994
+ if (event.kind === "unlink") {
34995
+ const entry3 = event.stateEntry;
34996
+ if (!entry3)
34997
+ return null;
34998
+ return {
34999
+ kind: "delete",
35000
+ entityType,
35001
+ slug: entry3.slug,
35002
+ id: entry3.id,
35003
+ updatedAt: entry3.updated_at,
35004
+ absolutePath: event.absolutePath
35005
+ };
35006
+ }
35007
+ const readFile = deps.readFile ?? ((p) => readFileSync16(p, "utf8"));
35008
+ const statInode = deps.statInode ?? ((p) => {
35009
+ try {
35010
+ return statSync6(p).ino;
35011
+ } catch {
35012
+ return;
35013
+ }
35014
+ });
35015
+ let raw;
35016
+ try {
35017
+ raw = readFile(event.absolutePath);
35018
+ } catch {
35019
+ return null;
35020
+ }
35021
+ const read = readFrontmatter(raw);
35022
+ if (!read.ok)
35023
+ return null;
35024
+ const body = read.parsed.content;
35025
+ const newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
35026
+ const liveInode = statInode(event.absolutePath);
35027
+ const slug = basename4(event.absolutePath, ".md");
35028
+ const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
35029
+ if (!entry2)
35030
+ return null;
35031
+ if (!isGutting(entry2.body_len, newLen))
35032
+ return null;
35033
+ return {
35034
+ kind: "overwrite",
35035
+ entityType,
35036
+ slug: entry2.slug,
35037
+ id: entry2.id,
35038
+ updatedAt: entry2.updated_at,
35039
+ absolutePath: event.absolutePath,
35040
+ priorLen: entry2.body_len ?? 0,
35041
+ newLen
35042
+ };
35043
+ }
35044
+ var LONE_MIN_REMOVED_DEFAULT = 5000;
35045
+ var LONE_MIN_REMOVED_BYTES_DEFAULT = 100 * 1024;
35046
+ function defaultThresholds() {
35047
+ return {
35048
+ absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
35049
+ propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
35050
+ propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5),
35051
+ loneMinRemoved: envInt("PLINTH_GATE_LONE_MIN_REMOVED", LONE_MIN_REMOVED_DEFAULT),
35052
+ loneMinRemovedBytes: envInt("PLINTH_GATE_LONE_MIN_REMOVED_BYTES", LONE_MIN_REMOVED_BYTES_DEFAULT)
35053
+ };
35054
+ }
35055
+ function tripReasonToken(reason) {
35056
+ return reason.loneRemoved !== undefined ? "size" : reason.kind;
35057
+ }
35058
+ function evaluateLoneOverwrite(count, removed, floor) {
35059
+ const loneRemoved = Math.max(0, ...removed);
35060
+ if (loneRemoved < floor)
35061
+ return { tripped: false };
35062
+ return { tripped: true, reason: { kind: "count", count, loneRemoved } };
35063
+ }
35064
+ function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
35065
+ const count = ops.length;
35066
+ if (count === 0)
35067
+ return { tripped: false };
35068
+ if (count >= thresholds.absCount) {
35069
+ return { tripped: true, reason: { kind: "count", count } };
35070
+ }
35071
+ const byType = new Map;
35072
+ for (const op of ops) {
35073
+ byType.set(op.entityType, (byType.get(op.entityType) ?? 0) + 1);
35074
+ }
35075
+ for (const [entityType, typeCount] of byType) {
35076
+ const typePopulation = typePopulations[entityType] ?? typeCount;
35077
+ if (typeCount >= thresholds.propMinOps && typePopulation > 0 && typeCount >= thresholds.propPct * typePopulation) {
35078
+ return {
35079
+ tripped: true,
35080
+ reason: {
35081
+ kind: "proportion",
35082
+ count,
35083
+ entityType,
35084
+ typeCount,
35085
+ typePopulation,
35086
+ pct: typeCount / typePopulation
35087
+ }
35088
+ };
35089
+ }
35090
+ }
35091
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorLen - op.newLen : 0), thresholds.loneMinRemoved);
35092
+ }
35093
+ function dedupeOps(ops) {
35094
+ const byKey = new Map;
35095
+ for (const op of ops)
35096
+ byKey.set(`${op.entityType}:${op.id}`, op);
35097
+ return Array.from(byKey.values());
35098
+ }
35099
+ var DEFAULT_SETTLE_MS = 600;
35100
+ var DEFAULT_MAX_BATCH = 500;
35101
+
35102
+ class DestructiveGate {
35103
+ pending = [];
35104
+ timer = null;
35105
+ settleMs;
35106
+ maxBatch;
35107
+ onSettle;
35108
+ onError;
35109
+ constructor(opts) {
35110
+ this.settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
35111
+ this.maxBatch = opts.maxBatch ?? DEFAULT_MAX_BATCH;
35112
+ this.onSettle = opts.onSettle;
35113
+ this.onError = opts.onError;
35114
+ }
35115
+ consider(op) {
35116
+ this.pending.push(op);
35117
+ if (this.pending.length >= this.maxBatch) {
35118
+ this.flush();
35119
+ return;
35120
+ }
35121
+ if (this.timer)
35122
+ clearTimeout(this.timer);
35123
+ this.timer = setTimeout(() => {
35124
+ this.flush();
35125
+ }, this.settleMs);
35126
+ }
35127
+ async drain() {
35128
+ await this.flush();
35129
+ }
35130
+ size() {
35131
+ return this.pending.length;
35132
+ }
35133
+ async flush() {
35134
+ if (this.timer) {
35135
+ clearTimeout(this.timer);
35136
+ this.timer = null;
35137
+ }
35138
+ if (this.pending.length === 0)
35139
+ return;
35140
+ const batch = this.pending;
35141
+ this.pending = [];
35142
+ try {
35143
+ await this.onSettle(batch);
35144
+ } catch (err) {
35145
+ this.onError?.(err);
35146
+ }
35147
+ }
35148
+ }
35149
+
35150
+ // src/lib/resolve-pending.ts
35151
+ import { readFileSync as readFileSync22 } from "node:fs";
35152
+
34694
35153
  // src/lib/files/push.ts
34695
- import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
35154
+ import { readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
34696
35155
 
34697
35156
  // src/lib/permanent-deletions.ts
34698
- import { createHash as createHash3, randomUUID } from "node:crypto";
35157
+ import { createHash as createHash4, randomUUID } from "node:crypto";
34699
35158
  import {
34700
35159
  chmodSync,
34701
- closeSync as closeSync4,
35160
+ closeSync as closeSync5,
34702
35161
  constants as constants2,
34703
35162
  existsSync as existsSync4,
34704
35163
  fsyncSync as fsyncSync2,
34705
- fstatSync as fstatSync2,
35164
+ fstatSync as fstatSync3,
34706
35165
  lstatSync as lstatSync4,
34707
35166
  mkdirSync as mkdirSync11,
34708
- openSync as openSync4,
34709
- readFileSync as readFileSync16,
34710
- renameSync as renameSync8,
34711
- unlinkSync as unlinkSync8,
35167
+ openSync as openSync5,
35168
+ readFileSync as readFileSync17,
35169
+ renameSync as renameSync9,
35170
+ unlinkSync as unlinkSync9,
34712
35171
  writeSync as writeSync2
34713
35172
  } from "node:fs";
34714
35173
  import { dirname as dirname10, join as join13 } from "node:path";
@@ -34806,13 +35265,13 @@ function canonical(value) {
34806
35265
  return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
34807
35266
  }
34808
35267
  function canonicalPayloadDigest(value) {
34809
- return createHash3("sha256").update(canonical(value), "utf8").digest("hex");
35268
+ return createHash4("sha256").update(canonical(value), "utf8").digest("hex");
34810
35269
  }
34811
35270
  function fileCreatePayloadDigest(contentHash, mimeType) {
34812
35271
  return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
34813
35272
  }
34814
35273
  function checksum(body) {
34815
- return createHash3("sha256").update(canonical(body), "utf8").digest("hex");
35274
+ return createHash4("sha256").update(canonical(body), "utf8").digest("hex");
34816
35275
  }
34817
35276
  function emptyJournal(workspaceSlug) {
34818
35277
  return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
@@ -34884,11 +35343,11 @@ function parseJournal(workspaceSlug, raw) {
34884
35343
  return body;
34885
35344
  }
34886
35345
  function fsyncDirectory(path5) {
34887
- const fd = openSync4(path5, constants2.O_RDONLY);
35346
+ const fd = openSync5(path5, constants2.O_RDONLY);
34888
35347
  try {
34889
35348
  fsyncSync2(fd);
34890
35349
  } finally {
34891
- closeSync4(fd);
35350
+ closeSync5(fd);
34892
35351
  }
34893
35352
  }
34894
35353
  function loadPermanentDeletionJournal(workspaceSlug) {
@@ -34897,7 +35356,7 @@ function loadPermanentDeletionJournal(workspaceSlug) {
34897
35356
  return emptyJournal(workspaceSlug);
34898
35357
  assertPrivateDirectory(dirname10(target));
34899
35358
  assertOwnerOnlyFile(target);
34900
- const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
35359
+ const journal = parseJournal(workspaceSlug, readFileSync17(target, "utf8"));
34901
35360
  try {
34902
35361
  fsyncDirectory(dirname10(target));
34903
35362
  } catch (error52) {
@@ -34932,8 +35391,8 @@ function writePermanentDeletionJournal(prior, next) {
34932
35391
  `;
34933
35392
  let fd;
34934
35393
  try {
34935
- fd = openSync4(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
34936
- 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);
34937
35396
  if (!stat.isFile() || (stat.mode & 511) !== 384) {
34938
35397
  throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
34939
35398
  }
@@ -34942,16 +35401,16 @@ function writePermanentDeletionJournal(prior, next) {
34942
35401
  while (offset < bytes.length)
34943
35402
  offset += writeSync2(fd, bytes, offset, bytes.length - offset);
34944
35403
  fsyncSync2(fd);
34945
- closeSync4(fd);
35404
+ closeSync5(fd);
34946
35405
  fd = undefined;
34947
35406
  } catch (error52) {
34948
35407
  if (fd !== undefined) {
34949
35408
  try {
34950
- closeSync4(fd);
35409
+ closeSync5(fd);
34951
35410
  } catch {}
34952
35411
  }
34953
35412
  try {
34954
- unlinkSync8(tmp);
35413
+ unlinkSync9(tmp);
34955
35414
  } catch {}
34956
35415
  if (error52 instanceof PermanentDeletionJournalError)
34957
35416
  throw error52;
@@ -34959,18 +35418,18 @@ function writePermanentDeletionJournal(prior, next) {
34959
35418
  throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
34960
35419
  }
34961
35420
  try {
34962
- renameSync8(tmp, target);
35421
+ renameSync9(tmp, target);
34963
35422
  } catch (error52) {
34964
35423
  let installed2 = false;
34965
35424
  try {
34966
35425
  assertOwnerOnlyFile(target);
34967
- const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35426
+ const candidate = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34968
35427
  installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
34969
35428
  } catch {
34970
35429
  installed2 = false;
34971
35430
  }
34972
35431
  try {
34973
- unlinkSync8(tmp);
35432
+ unlinkSync9(tmp);
34974
35433
  } catch {}
34975
35434
  if (!installed2) {
34976
35435
  const detail = error52 instanceof Error ? error52.message : String(error52);
@@ -34978,7 +35437,7 @@ function writePermanentDeletionJournal(prior, next) {
34978
35437
  }
34979
35438
  }
34980
35439
  assertOwnerOnlyFile(target);
34981
- const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35440
+ const installed = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34982
35441
  if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
34983
35442
  throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
34984
35443
  }
@@ -34986,7 +35445,7 @@ function writePermanentDeletionJournal(prior, next) {
34986
35445
  fsyncDirectory(parent);
34987
35446
  } catch (firstError) {
34988
35447
  assertOwnerOnlyFile(target);
34989
- const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35448
+ const reopened = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34990
35449
  if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
34991
35450
  throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
34992
35451
  }
@@ -35329,9 +35788,109 @@ async function prepareOrResolveGenerationCreateLocked(args) {
35329
35788
  }
35330
35789
 
35331
35790
  // src/lib/files/content-hash.ts
35332
- import { createHash as createHash4 } from "node:crypto";
35791
+ import { createHash as createHash5 } from "node:crypto";
35333
35792
  function sha256Bytes(bytes) {
35334
- 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;
35335
35894
  }
35336
35895
 
35337
35896
  // src/lib/files/mime.ts
@@ -35403,23 +35962,63 @@ async function restoreFileFromCloud(args) {
35403
35962
  const { workspaceSlug, baseUrl, pat, id, relPath, state, fetchImpl, suppressor, output } = args;
35404
35963
  const got = args.prefetched ?? await getFile({ baseUrl, pat, id, fetchImpl });
35405
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
+ };
35406
35999
  if (!got.ok) {
36000
+ const reason2 = blocked(null) ?? unsent(undefined);
36001
+ if (reason2)
36002
+ return skipped(reason2);
35407
36003
  suppressor?.registerDelete(abs);
35408
36004
  deleteFilesFile(workspaceSlug, relPath);
35409
- return dropFiles(state, relPath);
36005
+ return { state: dropFiles(state, relPath), outcome: "removed" };
35410
36006
  }
35411
36007
  if (got.row.download_url === null) {
35412
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.`);
35413
- return state;
36009
+ return { state, outcome: "no_bytes" };
35414
36010
  }
35415
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);
35416
36015
  suppressor?.registerWrite(abs, {
35417
36016
  bodyHash: got.row.content_hash,
35418
36017
  frontmatterHash: ""
35419
36018
  });
35420
36019
  writeFilesBytes(workspaceSlug, relPath, bytes);
35421
36020
  deleteExtractedText(workspaceSlug, relPath);
35422
- return persistFiles(state, {
36021
+ const restored = persistFiles(state, {
35423
36022
  id: got.row.id,
35424
36023
  path: relPath,
35425
36024
  content_hash: got.row.content_hash,
@@ -35428,6 +36027,7 @@ async function restoreFileFromCloud(args) {
35428
36027
  updated_at: got.row.updated_at,
35429
36028
  inode: safeInode(abs)
35430
36029
  });
36030
+ return { state: restored, outcome: "restored" };
35431
36031
  }
35432
36032
  async function pushFilesUpsert(args) {
35433
36033
  return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
@@ -35485,7 +36085,7 @@ async function pushFilesUpsertLocked(args) {
35485
36085
  state: next
35486
36086
  };
35487
36087
  }
35488
- bytes = readFileSync17(absolutePath);
36088
+ bytes = readFileSync19(absolutePath);
35489
36089
  hash2 = sha256Bytes(bytes);
35490
36090
  inode = st.ino;
35491
36091
  }
@@ -35692,8 +36292,9 @@ async function pushFilesUpsertLocked(args) {
35692
36292
  side: "local"
35693
36293
  });
35694
36294
  output?.log(fileConflictLine({ path: relPath, stash: stashPath, reused }));
36295
+ let restore;
35695
36296
  if (existing) {
35696
- nextState = await restoreFileFromCloud({
36297
+ const restored = await restoreFileFromCloud({
35697
36298
  workspaceSlug,
35698
36299
  baseUrl,
35699
36300
  pat,
@@ -35703,10 +36304,16 @@ async function pushFilesUpsertLocked(args) {
35703
36304
  fetchImpl,
35704
36305
  suppressor,
35705
36306
  output,
35706
- prefetched: cloudRow
36307
+ prefetched: cloudRow,
36308
+ kept: bytes
35707
36309
  });
36310
+ nextState = restored.state;
36311
+ restore = restored.outcome;
35708
36312
  }
35709
- return { outcome: { kind: "conflict", relPath, stashPath }, state: nextState };
36313
+ return {
36314
+ outcome: { kind: "conflict", relPath, stashPath, ...restore ? { restore } : {} },
36315
+ state: nextState
36316
+ };
35710
36317
  }
35711
36318
  async function pushFilesDelete(args) {
35712
36319
  const { workspaceSlug, baseUrl, pat, relPath, state, deps } = args;
@@ -35732,7 +36339,7 @@ async function pushFilesDelete(args) {
35732
36339
  output?.log(fileDeleteLine({ path: relPath, id: existing.id, note: "already_absent" }));
35733
36340
  return { outcome: { kind: "deleted", relPath, id: existing.id }, state: dropFiles(state, relPath) };
35734
36341
  }
35735
- const nextState = await restoreFileFromCloud({
36342
+ const restore = await restoreFileFromCloud({
35736
36343
  workspaceSlug,
35737
36344
  baseUrl,
35738
36345
  pat,
@@ -35743,16 +36350,71 @@ async function pushFilesDelete(args) {
35743
36350
  suppressor,
35744
36351
  output
35745
36352
  });
35746
- return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
36353
+ return {
36354
+ outcome: { kind: "conflict", relPath, stashPath: "", restore: restore.outcome },
36355
+ state: restore.state
36356
+ };
35747
36357
  }
35748
36358
 
35749
36359
  // src/lib/push.ts
35750
- var import_gray_matter10 = __toESM(require_gray_matter(), 1);
35751
- import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
36360
+ import { existsSync as existsSync5, readFileSync as readFileSync21, statSync as statSync8 } from "node:fs";
35752
36361
  import { basename as basename5, relative as relative5 } from "node:path";
35753
36362
 
36363
+ // src/lib/frontmatter-held.ts
36364
+ import { lstatSync as lstatSync6, readFileSync as readFileSync20 } from "node:fs";
36365
+ import { resolve as resolve4, sep as sep5 } from "node:path";
36366
+ function holdUnreadableFrontmatter(args) {
36367
+ const next = recordEntityFrontmatterUnreadable(args);
36368
+ args.output?.log(entityPushHeldLine({
36369
+ entityType: args.entityType,
36370
+ slug: args.slug,
36371
+ reason: FRONTMATTER_UNREADABLE_REASON
36372
+ }));
36373
+ if (next !== args.state) {
36374
+ args.output?.warn(`${args.entityType}/${args.slug}: ${frontmatterUnreadableSentence(args.why)}`);
36375
+ }
36376
+ return next;
36377
+ }
36378
+ function settleFrontmatterHolds(state, workspaceSlug) {
36379
+ const held = listRejected(state).filter(isFrontmatterHold);
36380
+ if (held.length === 0)
36381
+ return state;
36382
+ let root;
36383
+ try {
36384
+ root = workspaceMirrorPath(workspaceSlug);
36385
+ } catch {
36386
+ return state;
36387
+ }
36388
+ const settled = new Set(held.map((record2) => record2.path).filter((path5) => !stillHeld(workspaceSlug, root, path5)));
36389
+ if (settled.size === 0)
36390
+ return state;
36391
+ return commitState(state, {
36392
+ ...state,
36393
+ rejected: listRejected(state).filter((o) => !(isFrontmatterHold(o) && settled.has(o.path)))
36394
+ });
36395
+ }
36396
+ function isFrontmatterHold(record2) {
36397
+ return record2.target === "entity" && record2.reason === FRONTMATTER_UNREADABLE_REASON;
36398
+ }
36399
+ function stillHeld(workspaceSlug, root, recordPath) {
36400
+ const absolutePath = resolve4(root, recordPath);
36401
+ if (!absolutePath.startsWith(root + sep5))
36402
+ return true;
36403
+ if (hasSymlinkedComponent(workspaceSlug, absolutePath))
36404
+ return true;
36405
+ let raw;
36406
+ try {
36407
+ if (!lstatSync6(absolutePath).isFile())
36408
+ return true;
36409
+ raw = readFileSync20(absolutePath, "utf8");
36410
+ } catch (err) {
36411
+ const code = err.code;
36412
+ return code !== "ENOENT" && code !== "ENOTDIR";
36413
+ }
36414
+ return frontmatterParseError(raw) !== null;
36415
+ }
36416
+
35754
36417
  // src/lib/frontmatter-id.ts
35755
- var import_gray_matter8 = __toESM(require_gray_matter(), 1);
35756
36418
  function eolOf(content) {
35757
36419
  return /\r\n/.test(content) ? `\r
35758
36420
  ` : `
@@ -35789,158 +36451,16 @@ function withFrontmatterId(raw, parsed, id) {
35789
36451
  }
35790
36452
  function frontmatterIdWriteIsSound(next, id, expectedBody) {
35791
36453
  try {
35792
- const check2 = import_gray_matter8.default(next);
36454
+ const check2 = matter(next);
35793
36455
  return check2.data?.["id"] === id && check2.content === expectedBody;
35794
36456
  } catch {
35795
36457
  return false;
35796
36458
  }
35797
36459
  }
35798
36460
 
35799
- // src/lib/pull-write.ts
35800
- var import_gray_matter9 = __toESM(require_gray_matter(), 1);
35801
- import { readFileSync as readFileSync18 } from "node:fs";
35802
- function computeMirrorHashes(strategy, content) {
35803
- const parsed = import_gray_matter9.default(content);
35804
- const fm = parsed.data ?? {};
35805
- const lifted = splitUserFrontmatter(strategy, fm);
35806
- const userFm = userFrontmatter(strategy, lifted.frontmatter);
35807
- const hashBody = hashableBody(strategy, parsed.content);
35808
- return {
35809
- bodyHash: stableHash(hashBody),
35810
- frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
35811
- bodyLen: hashBody.length,
35812
- frontmatterId: fm["id"]
35813
- };
35814
- }
35815
- function hashableBody(strategy, body) {
35816
- return strategy.stripLocalBlocks?.(body) ?? body;
35817
- }
35818
- function relocateForSlugChange(args) {
35819
- const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
35820
- if (fromSlug === toSlug)
35821
- return { kind: "not_needed", writeSlug: toSlug };
35822
- if (!echo) {
35823
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
35824
- }
35825
- if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
35826
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
35827
- }
35828
- let fromPath;
35829
- try {
35830
- fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
35831
- entityFilePath(workspaceSlug, strategy.type, toSlug);
35832
- } catch (err) {
35833
- if (err instanceof MirrorPathError) {
35834
- return {
35835
- kind: "skipped",
35836
- writeSlug: toSlug,
35837
- fromSlug,
35838
- reason: "unresolvable"
35839
- };
35840
- }
35841
- throw err;
35842
- }
35843
- let raw = null;
35844
- try {
35845
- raw = readFileSync18(fromPath, "utf8");
35846
- } catch (err) {
35847
- const code = err?.code;
35848
- if (code === "ENOENT") {
35849
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35850
- }
35851
- return {
35852
- kind: "refused",
35853
- writeSlug: fromSlug,
35854
- fromSlug,
35855
- reason: "source_unreadable"
35856
- };
35857
- }
35858
- let diverged = true;
35859
- try {
35860
- const hashes = computeMirrorHashes(strategy, raw);
35861
- diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
35862
- } catch {
35863
- diverged = true;
35864
- }
35865
- const stashPath = diverged ? stashConflict({
35866
- workspaceSlug,
35867
- entityType: strategy.type,
35868
- entitySlug: fromSlug,
35869
- timestamp: now(),
35870
- contents: raw,
35871
- side: "local"
35872
- }) : undefined;
35873
- try {
35874
- let registered = false;
35875
- const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
35876
- if (echo.watcher === "live") {
35877
- echo.suppressor.registerDelete(fromPath);
35878
- registered = true;
35879
- }
35880
- });
35881
- if (result === "moved") {
35882
- return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
35883
- }
35884
- if (registered && echo.watcher === "live") {
35885
- echo.suppressor.cancelDelete(fromPath);
35886
- }
35887
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35888
- } catch (err) {
35889
- if (err instanceof MirrorDestinationOccupiedError) {
35890
- return {
35891
- kind: "refused",
35892
- writeSlug: fromSlug,
35893
- fromSlug,
35894
- reason: "destination_occupied",
35895
- stashPath
35896
- };
35897
- }
35898
- if (err instanceof MirrorPathError) {
35899
- return {
35900
- kind: "skipped",
35901
- writeSlug: toSlug,
35902
- fromSlug,
35903
- reason: "unresolvable"
35904
- };
35905
- }
35906
- throw err;
35907
- }
35908
- }
35909
- function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
35910
- const targetSlug = pathSlug ?? doc2.slug;
35911
- const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
35912
- if (suppressor) {
35913
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35914
- suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
35915
- }
35916
- writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
35917
- return content;
35918
- }
35919
- function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
35920
- let content = strategy.serialise(doc2);
35921
- if (strategy.preserveLocalBodyOnPull) {
35922
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35923
- let existing = null;
35924
- try {
35925
- existing = readFileSync18(target, "utf8");
35926
- } catch (err) {
35927
- if (err?.code !== "ENOENT")
35928
- throw err;
35929
- }
35930
- if (existing !== null) {
35931
- const existingParsed = import_gray_matter9.default(existing);
35932
- const cloudParsed = import_gray_matter9.default(content);
35933
- const stitched = import_gray_matter9.default.stringify(existingParsed.content, cloudParsed.data);
35934
- content = stitched.endsWith(`
35935
- `) ? stitched : stitched + `
35936
- `;
35937
- }
35938
- }
35939
- return content;
35940
- }
35941
-
35942
36461
  // src/lib/push.ts
35943
36462
  var DELETE_DECLINED_PATH_EXISTS = "the file is on disk again — a newer write owns this row";
36463
+ var FRONTMATTER_PARSE_ERROR = "frontmatter parse error";
35944
36464
  function deleteDeclinedSentence(relPath) {
35945
36465
  return `Not deleted: ${relPath} is on disk again, so the row was left alone. If you meant to delete it, remove the file and it will be retried.`;
35946
36466
  }
@@ -36049,8 +36569,8 @@ function stampStateFromMirror(args) {
36049
36569
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
36050
36570
  const targetSlug = args.pathSlug ?? doc2.slug;
36051
36571
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
36052
- const mirrorRaw = readFileSync19(mirrorPath, "utf8");
36053
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
36572
+ const mirrorRaw = readFileSync21(mirrorPath, "utf8");
36573
+ const mirrorParsed = matter(mirrorRaw);
36054
36574
  const mirrorFm = mirrorParsed.data ?? {};
36055
36575
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
36056
36576
  const mirrorUserFm = userFrontmatter(strategy, mirrorLifted.frontmatter);
@@ -36202,7 +36722,7 @@ async function pushFile(args) {
36202
36722
  }
36203
36723
  let raw;
36204
36724
  try {
36205
- raw = readFileSync19(absolutePath, "utf8");
36725
+ raw = readFileSync21(absolutePath, "utf8");
36206
36726
  } catch (err) {
36207
36727
  if (err?.code === "ENOENT") {
36208
36728
  return runPushDelete({
@@ -36222,23 +36742,32 @@ async function pushFile(args) {
36222
36742
  }
36223
36743
  throw err;
36224
36744
  }
36225
- let parsed;
36226
- try {
36227
- parsed = import_gray_matter10.default(raw);
36228
- } catch {
36229
- return {
36230
- outcome: { kind: "ignored", absolutePath, reason: "frontmatter parse error" },
36231
- state
36232
- };
36233
- }
36234
- const fm = parsed.data ?? {};
36235
- const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
36236
36745
  let parsedSlug = null;
36237
36746
  try {
36238
36747
  const rel = relative5(workspaceMirrorPath(workspaceSlug), absolutePath);
36239
36748
  parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
36240
36749
  } catch {}
36241
36750
  const slugFromPath = parsedSlug ?? basename5(absolutePath, ".md");
36751
+ const read = readFrontmatter(raw);
36752
+ if (!read.ok) {
36753
+ return {
36754
+ outcome: { kind: "ignored", absolutePath, reason: FRONTMATTER_PARSE_ERROR },
36755
+ state: holdUnreadableFrontmatter({
36756
+ state,
36757
+ workspaceSlug,
36758
+ absolutePath,
36759
+ entityType,
36760
+ slug: slugFromPath,
36761
+ why: read.why,
36762
+ now,
36763
+ output
36764
+ }),
36765
+ base: state
36766
+ };
36767
+ }
36768
+ let parsed = read.parsed;
36769
+ const fm = parsed.data ?? {};
36770
+ const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
36242
36771
  const slug = identity === "path" ? slugFromPath : slugFromFm ?? slugFromPath;
36243
36772
  if (!slug) {
36244
36773
  return {
@@ -36252,14 +36781,14 @@ async function pushFile(args) {
36252
36781
  if (strategy.classifyFieldErrors) {
36253
36782
  const desiredBody = spliceSyncErrorBlock(parsed.content, fieldErrors);
36254
36783
  if (desiredBody.replace(/\s+$/, "") !== parsed.content.replace(/\s+$/, "")) {
36255
- let rewritten = import_gray_matter10.default.stringify(desiredBody, parsed.data ?? {});
36784
+ let rewritten = stringifyFrontmatter(desiredBody, parsed.data ?? {});
36256
36785
  if (!rewritten.endsWith(`
36257
36786
  `))
36258
36787
  rewritten += `
36259
36788
  `;
36260
36789
  writeMirrorFile(workspaceSlug, entityType, slug, rewritten);
36261
36790
  raw = rewritten;
36262
- parsed = import_gray_matter10.default(rewritten);
36791
+ parsed = parseUncached(rewritten);
36263
36792
  }
36264
36793
  }
36265
36794
  const liveInode = safeInode2(absolutePath);
@@ -36537,7 +37066,7 @@ async function runPushUpsert(args) {
36537
37066
  }
36538
37067
  const wireProjection = (md) => {
36539
37068
  try {
36540
- const p = import_gray_matter10.default(md);
37069
+ const p = matter(md);
36541
37070
  const f = p.data ?? {};
36542
37071
  const l = splitUserFrontmatter(strategy, f);
36543
37072
  const u = userFrontmatter(strategy, l.frontmatter);
@@ -36552,7 +37081,7 @@ async function runPushUpsert(args) {
36552
37081
  if (liveEntry !== snapshotEntry) {
36553
37082
  let onDisk = null;
36554
37083
  try {
36555
- onDisk = readFileSync19(args.absolutePath, "utf8");
37084
+ onDisk = readFileSync21(args.absolutePath, "utf8");
36556
37085
  } catch {
36557
37086
  onDisk = null;
36558
37087
  }
@@ -37014,7 +37543,7 @@ function writeCreatedIdBack(args) {
37014
37543
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
37015
37544
  return;
37016
37545
  }
37017
- const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
37546
+ const readBack = args.readBack ?? ((p) => readFileSync21(p, "utf8"));
37018
37547
  if (readBack(args.absolutePath) !== args.raw)
37019
37548
  return;
37020
37549
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -37486,8 +38015,8 @@ async function pushRename(args) {
37486
38015
  };
37487
38016
  if (!updated.body_hash || !updated.frontmatter_hash) {
37488
38017
  try {
37489
- const mirrorRaw = readFileSync19(toPath, "utf8");
37490
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
38018
+ const mirrorRaw = readFileSync21(toPath, "utf8");
38019
+ const mirrorParsed = matter(mirrorRaw);
37491
38020
  const renameFm = mirrorParsed.data ?? {};
37492
38021
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
37493
38022
  const renameUserFm = userFrontmatter(strategy, renameLifted.frontmatter);
@@ -37523,7 +38052,7 @@ async function pushRename(args) {
37523
38052
  if (patchResult.reason === "slug_conflict") {
37524
38053
  let contents2 = "";
37525
38054
  try {
37526
- contents2 = readFileSync19(toPath, "utf8");
38055
+ contents2 = readFileSync21(toPath, "utf8");
37527
38056
  } catch {}
37528
38057
  const stashPath2 = stashConflict({
37529
38058
  workspaceSlug,
@@ -37571,7 +38100,7 @@ async function pushRename(args) {
37571
38100
  }
37572
38101
  let contents = "";
37573
38102
  try {
37574
- contents = readFileSync19(toPath, "utf8");
38103
+ contents = readFileSync21(toPath, "utf8");
37575
38104
  } catch {}
37576
38105
  const stashPath = stashConflict({
37577
38106
  workspaceSlug,
@@ -37619,7 +38148,7 @@ async function drainResolvedBatches(deps) {
37619
38148
  deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
37620
38149
  applied++;
37621
38150
  } else {
37622
- await discardBatchOps(batch, deps);
38151
+ await discardBatchOps(batch, deps, mutex2);
37623
38152
  deps.output?.log(`[DESTRUCTIVE_DISCARDED batch=${batch.batch_id} ops=${batch.ops.length}]`);
37624
38153
  discarded++;
37625
38154
  }
@@ -37692,11 +38221,11 @@ async function applyFileOp(op, deps) {
37692
38221
  });
37693
38222
  deps.applyState(base2, result.state);
37694
38223
  }
37695
- async function discardBatchOps(batch, deps) {
38224
+ async function discardBatchOps(batch, deps, mutex2) {
37696
38225
  for (const op of batch.ops) {
37697
38226
  try {
37698
38227
  if (op.target === "file") {
37699
- await discardFileOp(op, deps);
38228
+ await discardFileOp(op, deps, mutex2);
37700
38229
  continue;
37701
38230
  }
37702
38231
  const strategy = requireStrategy(op.entityType);
@@ -37704,7 +38233,7 @@ async function discardBatchOps(batch, deps) {
37704
38233
  const path5 = opPath(deps.workspaceSlug, op);
37705
38234
  let local = null;
37706
38235
  try {
37707
- local = readFileSync20(path5, "utf8");
38236
+ local = readFileSync22(path5, "utf8");
37708
38237
  } catch {
37709
38238
  local = null;
37710
38239
  }
@@ -37739,35 +38268,50 @@ async function discardBatchOps(batch, deps) {
37739
38268
  }
37740
38269
  }
37741
38270
  }
37742
- async function discardFileOp(op, deps) {
37743
- const abs = filesSubtreePath(deps.workspaceSlug, op.path);
37744
- let local = null;
37745
- try {
37746
- local = readFileSync20(abs);
37747
- } catch {
37748
- local = null;
37749
- }
37750
- if (local !== null) {
37751
- 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({
37752
38300
  workspaceSlug: deps.workspaceSlug,
38301
+ baseUrl: deps.baseUrl,
38302
+ pat: deps.pat,
38303
+ id: op.id,
37753
38304
  relPath: op.path,
37754
- timestamp: (deps.now ?? (() => new Date))(),
37755
- bytes: local,
37756
- side: "local"
38305
+ state: base2,
38306
+ fetchImpl: deps.fetchImpl,
38307
+ output: deps.output,
38308
+ kept: local
37757
38309
  });
37758
- }
37759
- const base2 = deps.getState();
37760
- const next = await restoreFileFromCloud({
37761
- workspaceSlug: deps.workspaceSlug,
37762
- baseUrl: deps.baseUrl,
37763
- pat: deps.pat,
37764
- id: op.id,
37765
- relPath: op.path,
37766
- state: base2,
37767
- fetchImpl: deps.fetchImpl,
37768
- 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);
37769
38314
  });
37770
- deps.applyState(base2, next);
37771
38315
  }
37772
38316
  function opPath(workspaceSlug, op) {
37773
38317
  try {
@@ -37780,7 +38324,7 @@ function opPath(workspaceSlug, op) {
37780
38324
  }
37781
38325
 
37782
38326
  // src/lib/files/classify.ts
37783
- import { relative as relative6, sep as sep5 } from "node:path";
38327
+ import { relative as relative6, sep as sep6 } from "node:path";
37784
38328
  var FILES_SUBTREE = "files";
37785
38329
  function classifyFilesPath(args) {
37786
38330
  let root;
@@ -37790,20 +38334,20 @@ function classifyFilesPath(args) {
37790
38334
  return null;
37791
38335
  }
37792
38336
  const rel = relative6(root, args.absolutePath);
37793
- if (rel.startsWith("..") || rel.startsWith(sep5))
38337
+ if (rel.startsWith("..") || rel.startsWith(sep6))
37794
38338
  return null;
37795
- const prefix = FILES_SUBTREE + sep5;
38339
+ const prefix = FILES_SUBTREE + sep6;
37796
38340
  if (!rel.startsWith(prefix))
37797
38341
  return null;
37798
38342
  const sub = rel.slice(prefix.length);
37799
38343
  if (sub.length === 0)
37800
38344
  return null;
37801
- const posix = sep5 === "/" ? sub : sub.split(sep5).join("/");
38345
+ const posix = sep6 === "/" ? sub : sub.split(sep6).join("/");
37802
38346
  return validateFilesRelPath(posix) ? { relPath: posix } : null;
37803
38347
  }
37804
38348
 
37805
38349
  // src/lib/memory-facts/sync.ts
37806
- import { isAbsolute as isAbsolute2, relative as relative7, sep as sep6 } from "node:path";
38350
+ import { isAbsolute as isAbsolute2, relative as relative7, sep as sep7 } from "node:path";
37807
38351
 
37808
38352
  // src/lib/memory-facts/api.ts
37809
38353
  var FactCommonSchema = exports_external.object({
@@ -38547,7 +39091,7 @@ function classifyMemoryFactPath(args) {
38547
39091
  const rel = relative7(root, args.absolutePath);
38548
39092
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
38549
39093
  return null;
38550
- const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep6).join("/")}`;
39094
+ const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep7).join("/")}`;
38551
39095
  return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
38552
39096
  }
38553
39097
  function classifyMemoryFactEvent(args) {
@@ -38644,7 +39188,7 @@ function evaluateFileBatch(ops, population, thresholds = defaultThresholds()) {
38644
39188
  }
38645
39189
  };
38646
39190
  }
38647
- return { tripped: false };
39191
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorSize - op.newSize : 0), thresholds.loneMinRemovedBytes);
38648
39192
  }
38649
39193
  function dedupeFileOps(ops) {
38650
39194
  const byKey = new Map;
@@ -38715,10 +39259,10 @@ class FileDestructiveGate {
38715
39259
  }
38716
39260
 
38717
39261
  // src/lib/files/reconcile.ts
38718
- import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
39262
+ import { readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
38719
39263
 
38720
39264
  // src/lib/unsynced.ts
38721
- import { join as join14, relative as relative8 } from "node:path";
39265
+ import { join as join15, relative as relative8 } from "node:path";
38722
39266
  var RECORDABLE_REASONS = new Set([
38723
39267
  "rate_limit",
38724
39268
  "server",
@@ -38780,7 +39324,7 @@ async function drainUnsyncedOps(args) {
38780
39324
  attempted += 1;
38781
39325
  try {
38782
39326
  if (op.target === "entity") {
38783
- const absolutePath = join14(workspaceMirrorPath(workspaceSlug), op.path);
39327
+ const absolutePath = join15(workspaceMirrorPath(workspaceSlug), op.path);
38784
39328
  const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
38785
39329
  if (!classified) {
38786
39330
  clear("entity", op.path);
@@ -39593,9 +40137,9 @@ class NodeFsHandler {
39593
40137
  if (this.fsw.closed) {
39594
40138
  return;
39595
40139
  }
39596
- const dirname12 = sp.dirname(file2);
40140
+ const dirname13 = sp.dirname(file2);
39597
40141
  const basename7 = sp.basename(file2);
39598
- const parent = this.fsw._getWatchedDir(dirname12);
40142
+ const parent = this.fsw._getWatchedDir(dirname13);
39599
40143
  let prevStats = stats;
39600
40144
  if (parent.has(basename7))
39601
40145
  return;
@@ -39622,7 +40166,7 @@ class NodeFsHandler {
39622
40166
  prevStats = newStats2;
39623
40167
  }
39624
40168
  } catch (error52) {
39625
- this.fsw._remove(dirname12, basename7);
40169
+ this.fsw._remove(dirname13, basename7);
39626
40170
  }
39627
40171
  } else if (parent.has(basename7)) {
39628
40172
  const at = newStats.atimeMs;
@@ -39711,7 +40255,7 @@ class NodeFsHandler {
39711
40255
  this._addToNodeFs(path5, initialAdd, wh, depth + 1);
39712
40256
  }
39713
40257
  }).on(EV.ERROR, this._boundHandleError);
39714
- return new Promise((resolve5, reject) => {
40258
+ return new Promise((resolve6, reject) => {
39715
40259
  if (!stream)
39716
40260
  return reject();
39717
40261
  stream.once(STR_END, () => {
@@ -39720,7 +40264,7 @@ class NodeFsHandler {
39720
40264
  return;
39721
40265
  }
39722
40266
  const wasThrottled = throttler ? throttler.clear() : false;
39723
- resolve5(undefined);
40267
+ resolve6(undefined);
39724
40268
  previous.getChildren().filter((item) => {
39725
40269
  return item !== directory && !current.has(item);
39726
40270
  }).forEach((item) => {
@@ -40479,7 +41023,7 @@ var chokidar_default = { watch, FSWatcher };
40479
41023
 
40480
41024
  // src/lib/watcher.ts
40481
41025
  import { statSync as statSync9 } from "node:fs";
40482
- import { sep as sep7 } from "node:path";
41026
+ import { sep as sep8 } from "node:path";
40483
41027
 
40484
41028
  // src/lib/rename-classifier.ts
40485
41029
  class RenameClassifier {
@@ -40610,7 +41154,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
40610
41154
  return (absolutePath) => patterns.some((re) => re.test(absolutePath));
40611
41155
  }
40612
41156
  function escSep() {
40613
- return sep7 === "/" ? "/" : "\\\\";
41157
+ return sep8 === "/" ? "/" : "\\\\";
40614
41158
  }
40615
41159
 
40616
41160
  class DebounceMap {
@@ -40660,6 +41204,12 @@ class DebounceMap {
40660
41204
  this.pending.delete(oldestKey);
40661
41205
  this.handler(entry2.event);
40662
41206
  }
41207
+ nudge(absolutePath) {
41208
+ if (this.pending.has(absolutePath))
41209
+ return false;
41210
+ this.schedule({ kind: "upsert", absolutePath });
41211
+ return true;
41212
+ }
40663
41213
  async drain() {
40664
41214
  const entries = Array.from(this.pending.entries());
40665
41215
  this.pending.clear();
@@ -40724,6 +41274,9 @@ function startWatcher(opts, handler) {
40724
41274
  drainPending() {
40725
41275
  return debounce.drain();
40726
41276
  },
41277
+ nudge(absolutePath) {
41278
+ return debounce.nudge(absolutePath);
41279
+ },
40727
41280
  pendingCount() {
40728
41281
  return debounce.size();
40729
41282
  }
@@ -40754,7 +41307,7 @@ function readBlobForPush(args) {
40754
41307
  }
40755
41308
  let bytes;
40756
41309
  try {
40757
- bytes = readFileSync21(absolutePath);
41310
+ bytes = readFileSync23(absolutePath);
40758
41311
  } catch (err) {
40759
41312
  if (err?.code === "ENOENT")
40760
41313
  return { kind: "gone" };
@@ -40897,7 +41450,7 @@ async function reconcileFilesSubtree(args) {
40897
41450
  ops: pendingOps
40898
41451
  });
40899
41452
  quarantined = ops.length;
40900
- output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason.kind} source=files-reconcile]`);
41453
+ output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=files-reconcile]`);
40901
41454
  } else {
40902
41455
  for (const c of guttingCandidates) {
40903
41456
  await pushDivergedBlob(c.op.relPath, c.op.absolutePath, c.prepared);
@@ -41064,7 +41617,7 @@ function applyArtistMemoryOnPull(args) {
41064
41617
  const cloudMemory = live.memory;
41065
41618
  const generated = live.memory_generated === true;
41066
41619
  const local = readArtistMemoryFile(workspaceSlug, slug);
41067
- const refuse = (localContent, canonical2) => {
41620
+ const refuse2 = (localContent, canonical2) => {
41068
41621
  const refused = refuseGeneratedMemoryEdit({
41069
41622
  state,
41070
41623
  workspaceSlug,
@@ -41093,7 +41646,7 @@ function applyArtistMemoryOnPull(args) {
41093
41646
  const cloudHash = stableHash(cloudMemory);
41094
41647
  if (cloudHash === ackedMemoryHash && local !== null) {
41095
41648
  if (generated && stableHash(local) !== ackedMemoryHash) {
41096
- refuse(local, cloudMemory);
41649
+ refuse2(local, cloudMemory);
41097
41650
  return { memoryHash: cloudHash, state };
41098
41651
  }
41099
41652
  if (generated) {
@@ -41105,7 +41658,7 @@ function applyArtistMemoryOnPull(args) {
41105
41658
  const localHash = stableHash(local);
41106
41659
  if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
41107
41660
  if (generated) {
41108
- refuse(local, cloudMemory);
41661
+ refuse2(local, cloudMemory);
41109
41662
  return { memoryHash: cloudHash, state };
41110
41663
  }
41111
41664
  const stashPath = stashConflict({
@@ -41815,10 +42368,10 @@ function anyUnknown(state) {
41815
42368
  }
41816
42369
 
41817
42370
  // src/lib/memory/write.ts
41818
- import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
42371
+ import { readFileSync as readFileSync24, statSync as statSync11 } from "node:fs";
41819
42372
 
41820
42373
  // src/lib/files/extracted-text.ts
41821
- import { createHash as createHash5 } from "node:crypto";
42374
+ import { createHash as createHash6 } from "node:crypto";
41822
42375
  var DERIVED_TEXT_VERSION = 2;
41823
42376
  function yamlString3(value) {
41824
42377
  return JSON.stringify(value);
@@ -41892,11 +42445,10 @@ ${doc2.text}
41892
42445
  `;
41893
42446
  }
41894
42447
  function extractedTextHash(rendered) {
41895
- return createHash5("sha256").update(rendered, "utf8").digest("hex");
42448
+ return createHash6("sha256").update(rendered, "utf8").digest("hex");
41896
42449
  }
41897
42450
 
41898
42451
  // src/lib/memory/splice.ts
41899
- var import_gray_matter11 = __toESM(require_gray_matter(), 1);
41900
42452
  var OPEN_RE = /^---\r?\n/;
41901
42453
  function findFrontmatter(content) {
41902
42454
  const open2 = OPEN_RE.exec(content);
@@ -41982,7 +42534,7 @@ function spliceAnnotation(content, lines) {
41982
42534
  }
41983
42535
  function parsesAsFrontmatter(content) {
41984
42536
  try {
41985
- import_gray_matter11.default(content, { cache: false });
42537
+ matter(content, { cache: false });
41986
42538
  return true;
41987
42539
  } catch {
41988
42540
  return false;
@@ -42010,7 +42562,7 @@ function writeDocumentAnnotation(args) {
42010
42562
  }
42011
42563
  let current;
42012
42564
  try {
42013
- current = readFileSync22(target, "utf8");
42565
+ current = readFileSync24(target, "utf8");
42014
42566
  } catch {
42015
42567
  return { kind: "absent" };
42016
42568
  }
@@ -42048,18 +42600,32 @@ function writeFileTextAnnotation(args) {
42048
42600
  const current = readExtractedText(args.workspaceSlug, args.relPath);
42049
42601
  if (current === null)
42050
42602
  return { kind: "absent" };
42051
- if (args.textHash !== undefined && extractedTextHash(current) !== args.textHash) {
42603
+ const onDisk = extractedTextHash(current);
42604
+ const acked = args.textHash;
42605
+ const offAck = acked !== undefined && onDisk !== acked;
42606
+ if (acked !== undefined && offAck && !isOwnRender(current, acked, args.priorLines)) {
42052
42607
  return { kind: "diverged", relPath: extractedTextRelPath(args.relPath) };
42053
42608
  }
42054
42609
  const spliced = spliceAnnotation(current, args.lines);
42055
- if (!spliced.changed)
42056
- return { kind: "unchanged" };
42610
+ if (!spliced.changed) {
42611
+ return offAck ? { kind: "unchanged", textHash: onDisk } : { kind: "unchanged" };
42612
+ }
42057
42613
  writeExtractedText(args.workspaceSlug, args.relPath, spliced.content);
42058
42614
  return { kind: "written", textHash: extractedTextHash(spliced.content) };
42059
42615
  }
42616
+ function isOwnRender(current, ackedHash, priorLines) {
42617
+ if (extractedTextHash(spliceAnnotation(current, []).content) === ackedHash)
42618
+ return true;
42619
+ if (priorLines === undefined)
42620
+ return false;
42621
+ return extractedTextHash(spliceAnnotation(current, priorLines).content) === ackedHash;
42622
+ }
42060
42623
  function divergedNotice(relPath, state) {
42061
42624
  return `${relPath}: its memory context is now "${state}", but the file has local ` + `edits so the marker was not written into it. Your edits are untouched. ` + `Resolve the edit and the next sync writes the marker.`;
42062
42625
  }
42626
+ function generatedDivergedNotice(relPath) {
42627
+ return `${relPath}: this generated file does not match Plinth's record of it, ` + `so its memory marker was not written. Plinth rewrites the file when its ` + `source next changes; changes made to it here are not synced.`;
42628
+ }
42063
42629
  function unreadableNotice(relPath, why) {
42064
42630
  const reason = why.replace(/\s+/g, " ").trim().slice(0, 120);
42065
42631
  return `${relPath}: its memory context could not be written because the file's ` + `frontmatter could not be read (${reason}). The file is untouched. Fix the ` + `frontmatter and the next sync writes the marker.`;
@@ -42089,6 +42655,10 @@ function heldFileTexts(state) {
42089
42655
  textHash: f.text_hash
42090
42656
  }));
42091
42657
  }
42658
+ function priorFileLines(state, id) {
42659
+ return getMemorySourceEntries(state, "workspace_files")[id]?.rendered.split(`
42660
+ `);
42661
+ }
42092
42662
  async function writeChanged(args) {
42093
42663
  const docs = new Map(heldDocuments(args.state).map((d) => [d.id, d.slug]));
42094
42664
  const texts = new Map(heldFileTexts(args.state).map((f) => [f.id, f]));
@@ -42188,7 +42758,8 @@ async function writeChanged(args) {
42188
42758
  workspaceSlug: args.workspaceSlug,
42189
42759
  relPath: file2.relPath,
42190
42760
  textHash: args.freshTextHashes?.get(answer2.id) ?? file2.textHash,
42191
- lines: answer2.lines
42761
+ lines: answer2.lines,
42762
+ priorLines: priorFileLines(args.state, answer2.id)
42192
42763
  });
42193
42764
  if (res.kind !== "diverged" && res.kind !== "absent") {
42194
42765
  args.onWritten?.("workspace_files", answer2.id, res.textHash);
@@ -42215,7 +42786,7 @@ async function writeChanged(args) {
42215
42786
  }
42216
42787
  const textPath = extractedTextRelPath(file2.relPath);
42217
42788
  if (outcome.kind === "diverged") {
42218
- report.push({ path: textPath, record: divergedNotice(textPath, answer2.entry.state) });
42789
+ report.push({ path: textPath, record: generatedDivergedNotice(textPath) });
42219
42790
  } else {
42220
42791
  clear.push(textPath);
42221
42792
  }
@@ -42224,9 +42795,11 @@ async function writeChanged(args) {
42224
42795
  if (outcome.textHash !== undefined) {
42225
42796
  textHashes.set(file2.relPath, { to: outcome.textHash });
42226
42797
  }
42798
+ } else if (outcome.kind === "unchanged" && outcome.textHash !== undefined) {
42799
+ textHashes.set(file2.relPath, { to: outcome.textHash });
42227
42800
  } else if (outcome.kind === "diverged") {
42228
42801
  diverged++;
42229
- notices.push(divergedNotice(outcome.relPath, answer2.entry.state));
42802
+ notices.push(generatedDivergedNotice(outcome.relPath));
42230
42803
  }
42231
42804
  }
42232
42805
  if (args.abortIfStale?.() === true) {
@@ -43948,8 +44521,7 @@ class PullSuppressor {
43948
44521
  }
43949
44522
 
43950
44523
  // src/lib/discover-entities.ts
43951
- var import_gray_matter12 = __toESM(require_gray_matter(), 1);
43952
- import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
44524
+ import { readFileSync as readFileSync25, statSync as statSync12 } from "node:fs";
43953
44525
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
43954
44526
  function discoveryWriteCap() {
43955
44527
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -43976,6 +44548,11 @@ function classifyUntrackedEntity(args) {
43976
44548
  }
43977
44549
  return { kind: "create" };
43978
44550
  }
44551
+ function holdsUnreadableUntracked(args) {
44552
+ if (args.readable)
44553
+ return false;
44554
+ return classifyUntrackedEntity({ ...args, frontmatter: {} }).kind === "create";
44555
+ }
43979
44556
  function classifyRenameDestination(args) {
43980
44557
  const occupant = getEntryBySlug(args.state, args.entityType, args.entitySlug);
43981
44558
  if (occupant && occupant.id !== args.movedEntryId) {
@@ -44046,13 +44623,20 @@ function planDiscoveryWrites(args) {
44046
44623
  if (getEntryBySlug(state, entityType, entitySlug))
44047
44624
  continue;
44048
44625
  counts.discovered++;
44049
- let frontmatter;
44626
+ let raw;
44050
44627
  try {
44051
- frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
44628
+ raw = readFileSync25(absolutePath, "utf8");
44052
44629
  } catch {
44053
44630
  counts.discovered--;
44054
44631
  continue;
44055
44632
  }
44633
+ const read = readFrontmatter(raw);
44634
+ if (!read.ok && holdsUnreadableUntracked({ entityType, entitySlug, readable: false })) {
44635
+ counts.discovered--;
44636
+ planned.push({ kind: "unreadable", entityType, entitySlug, absolutePath, why: read.why });
44637
+ continue;
44638
+ }
44639
+ const frontmatter = read.ok ? read.parsed.data : {};
44056
44640
  const verdict = classifyUntrackedEntity({ entityType, entitySlug, frontmatter });
44057
44641
  if (verdict.kind === "hold") {
44058
44642
  if (verdict.held === "artist")
@@ -44093,7 +44677,7 @@ async function discoverUntrackedEntities(args) {
44093
44677
  }
44094
44678
  const cap = discoveryWriteCap();
44095
44679
  const creates = planned.filter((w) => w.kind === "create");
44096
- const toRun = creates.length > cap ? planned.filter((w) => w.kind === "rename") : planned;
44680
+ const toRun = creates.length > cap ? planned.filter((w) => w.kind !== "create") : planned;
44097
44681
  if (creates.length > cap) {
44098
44682
  counts.heldCap = creates.length;
44099
44683
  output?.log(`[DISCOVERY_HELD reason=cap creates=${creates.length} cap=${cap} source=entity-discovery]`);
@@ -44116,6 +44700,19 @@ async function runPlannedWrites(args) {
44116
44700
  let state = args.state;
44117
44701
  for (const write of planned) {
44118
44702
  try {
44703
+ if (write.kind === "unreadable") {
44704
+ state = holdUnreadableFrontmatter({
44705
+ state,
44706
+ workspaceSlug,
44707
+ absolutePath: write.absolutePath,
44708
+ entityType: write.entityType,
44709
+ slug: write.entitySlug,
44710
+ why: write.why,
44711
+ now,
44712
+ output
44713
+ });
44714
+ continue;
44715
+ }
44119
44716
  if (write.kind === "rename") {
44120
44717
  state = await runDiscoveredRename({
44121
44718
  write,
@@ -44276,8 +44873,7 @@ function recordDiscoveryFailure(args) {
44276
44873
  }
44277
44874
 
44278
44875
  // src/lib/frontmatter-id-backfill.ts
44279
- var import_gray_matter13 = __toESM(require_gray_matter(), 1);
44280
- import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
44876
+ import { readFileSync as readFileSync26, statSync as statSync13 } from "node:fs";
44281
44877
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
44282
44878
  function backfillWorthReporting(r) {
44283
44879
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -44320,7 +44916,7 @@ function safeInode3(path5) {
44320
44916
  function backfillFrontmatterIds(args) {
44321
44917
  const { workspaceSlug, state } = args;
44322
44918
  const compose = args.compose ?? withFrontmatterId;
44323
- const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
44919
+ const readRaw = args.readFile ?? ((p) => readFileSync26(p, "utf8"));
44324
44920
  const result = emptyResult();
44325
44921
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
44326
44922
  return result;
@@ -44369,7 +44965,7 @@ function backfillFrontmatterIds(args) {
44369
44965
  let rawBody;
44370
44966
  let parsedData;
44371
44967
  try {
44372
- const parsed = import_gray_matter13.default(raw);
44968
+ const parsed = parseUncached(raw);
44373
44969
  parsedData = parsed.data ?? {};
44374
44970
  existing = parsedData["id"];
44375
44971
  rawBody = parsed.content;
@@ -44448,11 +45044,11 @@ function backfillFrontmatterIds(args) {
44448
45044
  }
44449
45045
 
44450
45046
  // src/lib/reconcile.ts
44451
- import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
45047
+ import { readFileSync as readFileSync27, statSync as statSync14 } from "node:fs";
44452
45048
  async function reconcileDivergedRows(args) {
44453
45049
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
44454
45050
  const now = args.now ?? (() => new Date);
44455
- let state = args.state;
45051
+ let state = settleFrontmatterHolds(args.state, workspaceSlug);
44456
45052
  let scanned = 0;
44457
45053
  let diverged = 0;
44458
45054
  let pushed = 0;
@@ -44560,7 +45156,7 @@ async function reconcileDivergedRows(args) {
44560
45156
  }
44561
45157
  let content;
44562
45158
  try {
44563
- content = readFileSync25(absolutePath, "utf8");
45159
+ content = readFileSync27(absolutePath, "utf8");
44564
45160
  } catch (err) {
44565
45161
  if (err?.code === "ENOENT") {
44566
45162
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -44568,6 +45164,20 @@ async function reconcileDivergedRows(args) {
44568
45164
  }
44569
45165
  throw err;
44570
45166
  }
45167
+ const unreadable = frontmatterParseError(content);
45168
+ if (unreadable !== null) {
45169
+ state = holdUnreadableFrontmatter({
45170
+ state,
45171
+ workspaceSlug,
45172
+ absolutePath,
45173
+ entityType,
45174
+ slug: entry2.slug,
45175
+ why: unreadable,
45176
+ now,
45177
+ output
45178
+ });
45179
+ continue;
45180
+ }
44571
45181
  const hashes = computeMirrorHashes(strategy, content);
44572
45182
  if (hashes.frontmatterId === undefined)
44573
45183
  sawTrackedFileWithoutId = true;
@@ -44618,7 +45228,7 @@ async function reconcileDivergedRows(args) {
44618
45228
  ops: pendingOps
44619
45229
  });
44620
45230
  quarantined = ops.length;
44621
- output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason.kind} source=reconcile]`);
45231
+ output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=reconcile]`);
44622
45232
  } else {
44623
45233
  for (const c of guttingCandidates) {
44624
45234
  const op = c.op;
@@ -45849,7 +46459,7 @@ class Semaphore {
45849
46459
  this.active += 1;
45850
46460
  return;
45851
46461
  }
45852
- await new Promise((resolve6) => this.waiters.push(resolve6));
46462
+ await new Promise((resolve7) => this.waiters.push(resolve7));
45853
46463
  }
45854
46464
  release() {
45855
46465
  const next = this.waiters.shift();
@@ -45911,17 +46521,17 @@ function createThrottledFetch(baseFetch, opts = {}) {
45911
46521
  // src/commands/sync.ts
45912
46522
  import {
45913
46523
  existsSync as existsSync9,
45914
- lstatSync as lstatSync6,
46524
+ lstatSync as lstatSync8,
45915
46525
  readdirSync as readdirSync7,
45916
- readFileSync as readFileSync29,
46526
+ readFileSync as readFileSync32,
45917
46527
  statSync as statSync17,
45918
- unlinkSync as unlinkSync9
46528
+ unlinkSync as unlinkSync11
45919
46529
  } from "node:fs";
45920
- import { join as join18 } from "node:path";
46530
+ import { join as join19 } from "node:path";
45921
46531
 
45922
46532
  // src/lib/conflicts-view.ts
45923
46533
  import { existsSync as existsSync7 } from "node:fs";
45924
- import { join as join17 } from "node:path";
46534
+ import { join as join18 } from "node:path";
45925
46535
  var NO_GENERATED_MEMORY = {
45926
46536
  artistMemoryGenerated: () => false,
45927
46537
  workspaceMemoryGenerated: () => false,
@@ -45963,11 +46573,13 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
45963
46573
  if (root === null || !stash.label.startsWith("files/")) {
45964
46574
  return { kind: "entity", stash };
45965
46575
  }
46576
+ if (stash.pathUnrecorded)
46577
+ return { kind: "unsafe-path", stash };
45966
46578
  const rel = stash.label.slice("files/".length);
45967
46579
  if (validateFilesRelPath(rel) === null) {
45968
46580
  return { kind: "unsafe-path", stash };
45969
46581
  }
45970
- const abs = join17(root, "files", rel);
46582
+ const abs = join18(root, "files", rel);
45971
46583
  if (hasSymlinkedComponent(workspaceSlug, abs)) {
45972
46584
  return { kind: "unsafe-path", stash };
45973
46585
  }
@@ -46034,7 +46646,9 @@ function renderRow(row) {
46034
46646
  `);
46035
46647
  }
46036
46648
  out.push("To put your version back, run this and then delete the stash:", "", "```sh", `cp ${shellQuote2(stash.absPath)} ${shellQuote2(row.mirrorPath)}`, "```");
46037
- 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) {
46038
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.");
46039
46653
  }
46040
46654
  return out.join(`
@@ -46097,17 +46711,16 @@ function refreshConflictsView(workspaceSlug) {
46097
46711
  }
46098
46712
 
46099
46713
  // src/lib/files/pull.ts
46100
- import { existsSync as existsSync8, lstatSync as lstatSync5, readFileSync as readFileSync27, statSync as statSync15 } from "node:fs";
46101
- import { dirname as dirname13 } from "node:path";
46714
+ import { existsSync as existsSync8, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
46102
46715
 
46103
46716
  // src/lib/permanent-disposition.ts
46104
- import { createHash as createHash6 } from "node:crypto";
46105
- import { readFileSync as readFileSync26 } from "node:fs";
46717
+ import { createHash as createHash7 } from "node:crypto";
46718
+ import { readFileSync as readFileSync28 } from "node:fs";
46106
46719
  function sha256(value) {
46107
- return createHash6("sha256").update(value).digest("hex");
46720
+ return createHash7("sha256").update(value).digest("hex");
46108
46721
  }
46109
46722
  function verifyStash(path5, expectedHash) {
46110
- const actual = sha256(readFileSync26(path5));
46723
+ const actual = sha256(readFileSync28(path5));
46111
46724
  if (actual !== expectedHash) {
46112
46725
  throw new Error(`permanent deletion stash verification failed for ${path5}`);
46113
46726
  }
@@ -46151,7 +46764,7 @@ function disposePermanentDocumentLocked(args) {
46151
46764
  const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
46152
46765
  let raw;
46153
46766
  try {
46154
- raw = readFileSync26(target, "utf8");
46767
+ raw = readFileSync28(target, "utf8");
46155
46768
  } catch (error52) {
46156
46769
  if (error52?.code !== "ENOENT")
46157
46770
  throw error52;
@@ -46249,7 +46862,7 @@ function disposePermanentFileLocked(args) {
46249
46862
  const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
46250
46863
  let bytes;
46251
46864
  try {
46252
- bytes = readFileSync26(target);
46865
+ bytes = readFileSync28(target);
46253
46866
  } catch (error52) {
46254
46867
  if (error52?.code !== "ENOENT")
46255
46868
  throw error52;
@@ -46296,6 +46909,34 @@ function disposePermanentFileLocked(args) {
46296
46909
  return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
46297
46910
  }
46298
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
+
46299
46940
  // src/lib/files/pull.ts
46300
46941
  function dueFileRefreshMarkers(workspaceSlug, now) {
46301
46942
  try {
@@ -46422,7 +47063,9 @@ async function replayPermanentFiles(args, markers) {
46422
47063
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46423
47064
  forceTextRewalk: args.forceTextRewalk,
46424
47065
  loggedStashNotices: args.loggedStashNotices,
46425
- recentUnlinks: args.recentUnlinks
47066
+ recentUnlinks: args.recentUnlinks,
47067
+ committedBase: lastCommittedState,
47068
+ commit: args.commit
46426
47069
  });
46427
47070
  state = outcome.state;
46428
47071
  if (outcome.action === "written")
@@ -46495,6 +47138,7 @@ async function pullFiles(args) {
46495
47138
  let finalHighWater = null;
46496
47139
  let markRefused = false;
46497
47140
  let cursorBlocked = permanentJournalBlocked;
47141
+ const deliveredPaths = new Set;
46498
47142
  while (true) {
46499
47143
  let page;
46500
47144
  try {
@@ -46512,6 +47156,7 @@ async function pullFiles(args) {
46512
47156
  let pageTextFailed = false;
46513
47157
  for (const row of page.rows) {
46514
47158
  fetched++;
47159
+ deliveredPaths.add(row.path);
46515
47160
  if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
46516
47161
  if (permanentJournalBlocked) {
46517
47162
  pageTextFailed = true;
@@ -46542,7 +47187,9 @@ async function pullFiles(args) {
46542
47187
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46543
47188
  forceTextRewalk: args.forceTextRewalk,
46544
47189
  loggedStashNotices: args.loggedStashNotices,
46545
- recentUnlinks: args.recentUnlinks
47190
+ recentUnlinks: args.recentUnlinks,
47191
+ committedBase: pageBase,
47192
+ commit: args.commit
46546
47193
  });
46547
47194
  state = outcome.state;
46548
47195
  if (!outcome.symlinkRefused) {
@@ -46596,6 +47243,14 @@ async function pullFiles(args) {
46596
47243
  return replayPermanentFiles({ ...args, state }, markers);
46597
47244
  }
46598
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;
46599
47254
  if (textUnknown > 0) {
46600
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.`);
46601
47256
  }
@@ -46668,6 +47323,9 @@ async function materialiseExtractedText(args) {
46668
47323
  });
46669
47324
  } else {
46670
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
+ }
46671
47329
  if (!got.ok)
46672
47330
  return { textHash: undefined, written: 0, deleted: 0 };
46673
47331
  if (got.row.text === null) {
@@ -46729,7 +47387,10 @@ async function applyFileRow(args) {
46729
47387
  symlinkRefused: true
46730
47388
  };
46731
47389
  }
46732
- 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();
46733
47394
  if (row.deleted_at !== null) {
46734
47395
  if (existing && existing.id !== row.id) {
46735
47396
  const key = `${row.id}:${existing.id}:${relPath}`;
@@ -46737,40 +47398,63 @@ async function applyFileRow(args) {
46737
47398
  args.loggedTombstoneSkips?.add(key);
46738
47399
  output?.log(`[FILE_TOMBSTONE_SKIP path=${relPath} row_id=${row.id} tracked_id=${existing.id} reason=id_mismatch]`);
46739
47400
  }
46740
- return { state, action: "skipped" };
47401
+ return { state: clearHoldsOfRow(state, relPath, row.id), action: "skipped" };
46741
47402
  }
46742
47403
  let localBytes;
46743
47404
  try {
46744
- localBytes = readFileSync27(abs);
47405
+ localBytes = readFileSync29(abs);
46745
47406
  } catch {
46746
47407
  localBytes = undefined;
46747
47408
  }
46748
47409
  const isOurs = localBytes === undefined || existing !== undefined && sha256Bytes(localBytes) === existing.content_hash;
46749
47410
  if (!isOurs) {
46750
47411
  output?.log(`[FILE_TOMBSTONE_KEEP path=${relPath} row_id=${row.id} reason=${existing === undefined ? "untracked" : "diverged"}]`);
46751
- const next2 = existing ? removeFileEntry(state, relPath).state : state;
46752
- 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" };
46753
47425
  }
46754
47426
  suppressor?.registerDelete(abs);
46755
47427
  const didUnlink = deleteFilesFile(workspaceSlug, relPath);
46756
47428
  const removedText = safeDeleteExtractedText(workspaceSlug, relPath, output);
46757
47429
  const textDeleted = removedText.removed ? 1 : 0;
46758
47430
  if (existing) {
46759
- const { state: next2 } = removeFileEntry(state, relPath);
46760
47431
  return {
46761
- state: next2,
47432
+ state: clearRowHolds(dropAck(state, relPath, existing, snapshotEntry, args.commit), relPath),
46762
47433
  action: "deleted",
46763
47434
  textDeleted,
46764
47435
  textFailed: removedText.failed
46765
47436
  };
46766
47437
  }
46767
47438
  return {
46768
- state,
47439
+ state: clearRowHolds(state, relPath),
46769
47440
  action: didUnlink ? "deleted" : "skipped",
46770
47441
  textDeleted,
46771
47442
  textFailed: removedText.failed
46772
47443
  };
46773
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);
46774
47458
  const textSignal = extractedTextSignal(row);
46775
47459
  const textUnknown = textSignal === "unknown";
46776
47460
  if (storedBytesSignal(row) === "absent") {
@@ -46784,7 +47468,8 @@ async function applyFileRow(args) {
46784
47468
  existing,
46785
47469
  textSignal,
46786
47470
  fetchImpl,
46787
- output
47471
+ output,
47472
+ stillCurrent: stillAt(existing)
46788
47473
  });
46789
47474
  }
46790
47475
  if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
@@ -46798,9 +47483,9 @@ async function applyFileRow(args) {
46798
47483
  } else {
46799
47484
  presentAgain(args.recentUnlinks, abs);
46800
47485
  }
46801
- 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)) {
46802
47487
  if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
46803
- return { state, action: "noop", textUnknown };
47488
+ return { state: clearRowHolds(state, relPath), action: "noop", textUnknown };
46804
47489
  }
46805
47490
  const text2 = await applyExtractedText({
46806
47491
  workspaceSlug,
@@ -46810,9 +47495,13 @@ async function applyFileRow(args) {
46810
47495
  relPath,
46811
47496
  bytesInMirror: true,
46812
47497
  fetchImpl,
46813
- output
47498
+ output,
47499
+ stillCurrent: stillAt(existing)
46814
47500
  });
46815
- const { state: next2 } = upsertFileEntry(state, {
47501
+ if (text2.stale) {
47502
+ return { state, action: "noop", textUnknown };
47503
+ }
47504
+ const { state: next2 } = upsertFileEntry(clearRowHolds(state, relPath), {
46816
47505
  ...existing,
46817
47506
  updated_at: row.updated_at,
46818
47507
  text_hash: textUnknown ? existing.text_hash : text2.textHash
@@ -46826,6 +47515,23 @@ async function applyFileRow(args) {
46826
47515
  textUnknown
46827
47516
  };
46828
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
+ }
46829
47535
  const got = await getFile({ baseUrl, pat, id: row.id, fetchImpl });
46830
47536
  if (!got.ok) {
46831
47537
  return { state, action: "skipped" };
@@ -46844,22 +47550,51 @@ async function applyFileRow(args) {
46844
47550
  existing,
46845
47551
  textSignal,
46846
47552
  fetchImpl,
46847
- output
47553
+ output,
47554
+ stillCurrent: stillAt(existing)
46848
47555
  });
46849
47556
  }
46850
47557
  const bytes = await downloadBytes(got.row.download_url, fetchImpl);
46851
47558
  if (sha256Bytes(bytes) !== row.content_hash) {
46852
47559
  output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
46853
- 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 };
46854
47588
  }
46855
- const stashFailure = stashUnpushedLocalEdit({
47589
+ const stashFailure = keepUnsentLocalCopy({
46856
47590
  workspaceSlug,
46857
47591
  relPath,
46858
47592
  abs,
46859
- existing,
46860
- rowHash: row.content_hash,
46861
- ambient: suppressor !== undefined,
47593
+ safeHashes: [row.content_hash, ackAtWrite?.content_hash],
46862
47594
  now,
47595
+ via: "pull",
47596
+ versionKey: row.content_hash,
47597
+ onBoot: suppressor === undefined,
46863
47598
  output,
46864
47599
  loggedStashNotices: args.loggedStashNotices
46865
47600
  });
@@ -46869,7 +47604,18 @@ async function applyFileRow(args) {
46869
47604
  args.loggedStashNotices?.add(key);
46870
47605
  output?.log(fileStashFailedLine({ path: relPath, ...stashFailure }));
46871
47606
  }
46872
- 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
+ };
46873
47619
  }
46874
47620
  suppressor?.registerWrite(abs, { bodyHash: row.content_hash, frontmatterHash: "" });
46875
47621
  writeFilesBytes(workspaceSlug, relPath, bytes);
@@ -46895,7 +47641,8 @@ async function applyFileRow(args) {
46895
47641
  relPath,
46896
47642
  bytesInMirror: true,
46897
47643
  fetchImpl,
46898
- output
47644
+ output,
47645
+ stillCurrent: stillAt(ackAtWrite)
46899
47646
  });
46900
47647
  }
46901
47648
  let inode;
@@ -46904,7 +47651,7 @@ async function applyFileRow(args) {
46904
47651
  } catch {
46905
47652
  inode = undefined;
46906
47653
  }
46907
- const { state: next } = upsertFileEntry(state, {
47654
+ const { state: next } = upsertFileEntry(clearRowHolds(state, relPath), {
46908
47655
  id: row.id,
46909
47656
  path: relPath,
46910
47657
  content_hash: row.content_hash,
@@ -46940,7 +47687,8 @@ async function applyPointerRow(args) {
46940
47687
  relPath,
46941
47688
  bytesInMirror,
46942
47689
  fetchImpl: args.fetchImpl,
46943
- output: args.output
47690
+ output: args.output,
47691
+ stillCurrent: args.stillCurrent
46944
47692
  });
46945
47693
  return {
46946
47694
  state,
@@ -46952,53 +47700,87 @@ async function applyPointerRow(args) {
46952
47700
  pointer: true
46953
47701
  };
46954
47702
  }
46955
- function stashUnpushedLocalEdit(args) {
46956
- const { workspaceSlug, relPath, abs, existing, output } = args;
46957
- if (hasSymlinkedComponent(workspaceSlug, dirname13(abs)))
46958
- return { reason: "symlink" };
46959
- let st;
46960
- try {
46961
- st = lstatSync5(abs);
46962
- } catch (err) {
46963
- if (err?.code === "ENOENT")
46964
- return null;
46965
- return { reason: "unreadable", code: errCode(err) };
46966
- }
46967
- if (!st.isFile())
46968
- return { reason: "not_a_file" };
46969
- let localBytes;
46970
- try {
46971
- localBytes = readFileSync27(abs);
46972
- } catch (err) {
46973
- 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);
46974
47714
  }
46975
- const localHash = sha256Bytes(localBytes);
46976
- 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) {
46977
47744
  return null;
46978
- let stashed;
46979
- try {
46980
- stashed = stashFileConflictOnce({
46981
- workspaceSlug,
46982
- relPath,
46983
- timestamp: args.now(),
46984
- bytes: localBytes,
46985
- side: "local"
46986
- });
46987
- } catch (err) {
46988
- return { reason: "stash_io", code: errCode(err) };
46989
47745
  }
46990
- const key = `reused:${stashed.path}:${args.rowHash}`;
46991
- 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")
46992
47752
  return null;
46993
- if (stashed.reused)
46994
- args.loggedStashNotices?.add(key);
46995
- output?.log(fileConflictLine({ path: relPath, stash: stashed.path, via: "pull", reused: stashed.reused }));
46996
- 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"}).`);
46997
- 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;
46998
47764
  }
46999
- function errCode(err) {
47000
- const code = err?.code;
47001
- 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;
47002
47784
  }
47003
47785
  function textIsCurrent(existing, row, workspaceSlug, relPath) {
47004
47786
  const signal = extractedTextSignal(row);
@@ -47018,7 +47800,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
47018
47800
  }
47019
47801
 
47020
47802
  // src/lib/files/rejection-sync.ts
47021
- import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
47803
+ import { readFileSync as readFileSync30, statSync as statSync16 } from "node:fs";
47022
47804
 
47023
47805
  // src/lib/files/rejections-api.ts
47024
47806
  var RejectionsFeedSchema = exports_external.object({
@@ -47151,7 +47933,7 @@ function settledDisposition(args) {
47151
47933
  return null;
47152
47934
  let bytes;
47153
47935
  try {
47154
- bytes = readFileSync28(absolute2);
47936
+ bytes = readFileSync30(absolute2);
47155
47937
  } catch {
47156
47938
  return null;
47157
47939
  }
@@ -47216,6 +47998,49 @@ function rearmMissingReports(state) {
47216
47998
 
47217
47999
  // src/commands/sync.ts
47218
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
47219
48044
  var memberRefreshFailedAt = new Map;
47220
48045
  var MEMBER_REFRESH_RETRY_AFTER_MS = 60 * 60 * 1000;
47221
48046
  function classifyApiError(err) {
@@ -47237,7 +48062,7 @@ function hasExistingContent(workspacePath) {
47237
48062
  if (!e.isDirectory())
47238
48063
  return true;
47239
48064
  try {
47240
- return readdirSync7(join18(workspacePath, e.name)).length > 0;
48065
+ return readdirSync7(join19(workspacePath, e.name)).length > 0;
47241
48066
  } catch {
47242
48067
  return true;
47243
48068
  }
@@ -47267,7 +48092,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
47267
48092
  }
47268
48093
  let raw;
47269
48094
  try {
47270
- raw = readFileSync29(target, "utf8");
48095
+ raw = readFileSync32(target, "utf8");
47271
48096
  } catch (err) {
47272
48097
  if (err?.code === "ENOENT") {
47273
48098
  return { didUnlink: false };
@@ -47285,7 +48110,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
47285
48110
  }
47286
48111
  try {
47287
48112
  suppressor?.registerDelete(target);
47288
- unlinkSync9(target);
48113
+ unlinkSync11(target);
47289
48114
  return { didUnlink: true };
47290
48115
  } catch (err) {
47291
48116
  if (err?.code === "ENOENT") {
@@ -47392,7 +48217,7 @@ async function replayPermanentDocuments(args) {
47392
48217
  for (const row of rows) {
47393
48218
  if (row.deleted_at !== null && row.permanent_delete === true)
47394
48219
  continue;
47395
- 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);
47396
48221
  state = outcome.state;
47397
48222
  if (outcome.action === "written")
47398
48223
  counters.written++;
@@ -47529,6 +48354,10 @@ async function runSync(workspaceSlug, deps = {}) {
47529
48354
  committed = state;
47530
48355
  };
47531
48356
  const counters = {};
48357
+ const deferral = {
48358
+ onPushDeferred: deps.onPushDeferred,
48359
+ logged: deps.loggedPullDeferrals
48360
+ };
47532
48361
  let restoresDeferred = 0;
47533
48362
  try {
47534
48363
  for (const entityType of allEntityTypes()) {
@@ -47552,6 +48381,7 @@ async function runSync(workspaceSlug, deps = {}) {
47552
48381
  isFrozen: deps.isFrozen,
47553
48382
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47554
48383
  recentUnlinks: deps.recentUnlinks,
48384
+ deferral,
47555
48385
  commit: (before, after) => {
47556
48386
  const next = commitState(before, after);
47557
48387
  committed = next;
@@ -47637,7 +48467,7 @@ async function runSync(workspaceSlug, deps = {}) {
47637
48467
  }
47638
48468
  let outcome;
47639
48469
  try {
47640
- 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);
47641
48471
  } catch (err) {
47642
48472
  if (!(err instanceof MirrorSymlinkError))
47643
48473
  throw err;
@@ -47697,6 +48527,7 @@ async function runSync(workspaceSlug, deps = {}) {
47697
48527
  isFrozen: deps.isFrozen,
47698
48528
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47699
48529
  recentUnlinks: deps.recentUnlinks,
48530
+ deferral,
47700
48531
  commit: (before, after) => {
47701
48532
  const next = commitState(before, after);
47702
48533
  committed = next;
@@ -47990,7 +48821,7 @@ async function runSync(workspaceSlug, deps = {}) {
47990
48821
  out.success(`Done in ${elapsed}s.`);
47991
48822
  return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
47992
48823
  }
47993
- 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) {
47994
48825
  if (isFrozen?.(strategy.type, row.id)) {
47995
48826
  return { state, action: "skipped" };
47996
48827
  }
@@ -48141,16 +48972,47 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48141
48972
  return { state, action: "noop_overwrite" };
48142
48973
  }
48143
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
+ }
48144
49003
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
48145
49004
  let localRaw = null;
48146
49005
  try {
48147
- localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
49006
+ localRaw = readFileSync32(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
48148
49007
  } catch {
48149
49008
  localRaw = null;
48150
49009
  }
48151
49010
  if (localRaw !== null) {
48152
- const localHashes = computeMirrorHashes(strategy, localRaw);
48153
- const diverged = localHashes.bodyHash !== existingEntry.body_hash || localHashes.frontmatterHash !== existingEntry.frontmatter_hash;
49011
+ let diverged = !strategy.preserveLocalBodyOnPull && frontmatterParseError(localRaw) !== null;
49012
+ if (!diverged) {
49013
+ const localHashes = computeMirrorHashes(strategy, localRaw);
49014
+ diverged = localHashes.bodyHash !== existingEntry.body_hash || localHashes.frontmatterHash !== existingEntry.frontmatter_hash;
49015
+ }
48154
49016
  if (diverged) {
48155
49017
  const stashPath = stashConflict({
48156
49018
  workspaceSlug,
@@ -48168,7 +49030,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48168
49030
  const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
48169
49031
  let localRaw = null;
48170
49032
  try {
48171
- localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
49033
+ localRaw = lstatSync8(target).isFile() ? readFileSync32(target, "utf8") : null;
48172
49034
  } catch {
48173
49035
  localRaw = null;
48174
49036
  }
@@ -48196,7 +49058,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48196
49058
  }
48197
49059
  }
48198
49060
  }
48199
- const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
49061
+ const content = adoptRaw ?? writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
48200
49062
  const hashes = computeMirrorHashes(strategy, content);
48201
49063
  let inode;
48202
49064
  try {
@@ -48233,7 +49095,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48233
49095
  memory_hash: memoryHash,
48234
49096
  memory_generated: memoryGenerated
48235
49097
  });
48236
- const action = relocation.kind === "moved" ? "moved" : "written";
49098
+ const action = adoptRaw !== undefined ? "noop_overwrite" : relocation.kind === "moved" ? "moved" : "written";
48237
49099
  return { state: nextState, action };
48238
49100
  }
48239
49101
  var syncCommand = defineCommand({
@@ -48334,7 +49196,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
48334
49196
  return { state, changed: false };
48335
49197
  return { state: { ...state, entries: nextEntries }, changed: true };
48336
49198
  }
48337
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
49199
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync33(p, "utf8")) {
48338
49200
  let changed = false;
48339
49201
  const nextEntries = {};
48340
49202
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -48346,7 +49208,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
48346
49208
  }
48347
49209
  try {
48348
49210
  const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
48349
- const bodyLen = import_gray_matter14.default(readFn(path5)).content.length;
49211
+ const bodyLen = parseUncached(readFn(path5)).content.length;
48350
49212
  nextBucket[id] = { ...entry2, body_len: bodyLen };
48351
49213
  changed = true;
48352
49214
  } catch {
@@ -48359,6 +49221,18 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
48359
49221
  return { state, changed: false };
48360
49222
  return { state: { ...state, entries: nextEntries }, changed: true };
48361
49223
  }
49224
+ function isOwnWriteEcho(suppressor, absolutePath, strategy, raw) {
49225
+ if (frontmatterParseError(raw) !== null)
49226
+ return false;
49227
+ return suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw));
49228
+ }
49229
+ function destinationIsUnreadable(absolutePath) {
49230
+ try {
49231
+ return frontmatterParseError(readFileSync33(absolutePath, "utf8")) !== null;
49232
+ } catch {
49233
+ return false;
49234
+ }
49235
+ }
48362
49236
  function makeWatchHandler(deps) {
48363
49237
  const {
48364
49238
  workspaceSlug,
@@ -48475,7 +49349,7 @@ function makeWatchHandler(deps) {
48475
49349
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
48476
49350
  return;
48477
49351
  }
48478
- const bytes = readFileSync30(absolutePath);
49352
+ const bytes = readFileSync33(absolutePath);
48479
49353
  const hash2 = sha256Bytes(bytes);
48480
49354
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
48481
49355
  bodyHash: hash2,
@@ -48539,7 +49413,7 @@ function makeWatchHandler(deps) {
48539
49413
  if (suppressor) {
48540
49414
  let raw = null;
48541
49415
  try {
48542
- raw = readFileSync30(absolutePath, "utf8");
49416
+ raw = readFileSync33(absolutePath, "utf8");
48543
49417
  } catch {
48544
49418
  raw = null;
48545
49419
  }
@@ -48578,7 +49452,7 @@ function makeWatchHandler(deps) {
48578
49452
  if (suppressor) {
48579
49453
  let raw = null;
48580
49454
  try {
48581
- raw = readFileSync30(absolutePath, "utf8");
49455
+ raw = readFileSync33(absolutePath, "utf8");
48582
49456
  } catch {
48583
49457
  raw = null;
48584
49458
  }
@@ -48617,7 +49491,7 @@ function makeWatchHandler(deps) {
48617
49491
  if (suppressor) {
48618
49492
  let raw = null;
48619
49493
  try {
48620
- raw = readFileSync30(absolutePath, "utf8");
49494
+ raw = readFileSync33(absolutePath, "utf8");
48621
49495
  } catch {
48622
49496
  raw = null;
48623
49497
  }
@@ -48656,7 +49530,7 @@ function makeWatchHandler(deps) {
48656
49530
  if (suppressor) {
48657
49531
  let raw = null;
48658
49532
  try {
48659
- raw = readFileSync30(absolutePath, "utf8");
49533
+ raw = readFileSync33(absolutePath, "utf8");
48660
49534
  } catch {
48661
49535
  raw = null;
48662
49536
  }
@@ -48695,7 +49569,7 @@ function makeWatchHandler(deps) {
48695
49569
  if (suppressor) {
48696
49570
  let raw = null;
48697
49571
  try {
48698
- raw = readFileSync30(absolutePath, "utf8");
49572
+ raw = readFileSync33(absolutePath, "utf8");
48699
49573
  } catch {
48700
49574
  raw = null;
48701
49575
  }
@@ -48741,11 +49615,11 @@ function makeWatchHandler(deps) {
48741
49615
  const strategy = getStrategy(classifiedFrom.entityType);
48742
49616
  let toRaw = null;
48743
49617
  try {
48744
- toRaw = readFileSync30(event.toPath, "utf8");
49618
+ toRaw = readFileSync33(event.toPath, "utf8");
48745
49619
  } catch {
48746
49620
  toRaw = null;
48747
49621
  }
48748
- const isEcho = strategy !== undefined && toRaw !== null && suppressor.shouldSuppressUpsert(event.toPath, computeMirrorHashes(strategy, toRaw));
49622
+ const isEcho = strategy !== undefined && toRaw !== null && isOwnWriteEcho(suppressor, event.toPath, strategy, toRaw);
48749
49623
  if (isEcho) {
48750
49624
  output?.log(`[RENAME_SUPPRESSED entityType=${classifiedFrom.entityType} ` + `from=${event.fromSlug} id=${event.id} reason=pull_echo]`);
48751
49625
  return;
@@ -48753,6 +49627,9 @@ function makeWatchHandler(deps) {
48753
49627
  await handle({ kind: "upsert", absolutePath: event.toPath });
48754
49628
  return;
48755
49629
  }
49630
+ if (destinationIsUnreadable(event.toPath)) {
49631
+ await handle({ kind: "upsert", absolutePath: event.toPath });
49632
+ }
48756
49633
  const base3 = getState();
48757
49634
  const result2 = await pushRename({
48758
49635
  workspaceSlug,
@@ -48810,7 +49687,7 @@ function makeWatchHandler(deps) {
48810
49687
  if (event.kind === "upsert") {
48811
49688
  try {
48812
49689
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
48813
- bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
49690
+ bodyHash: stableHash(readFileSync33(absolutePath, "utf8")),
48814
49691
  frontmatterHash: ""
48815
49692
  }) === true;
48816
49693
  } catch {
@@ -48865,11 +49742,11 @@ function makeWatchHandler(deps) {
48865
49742
  if (strategy) {
48866
49743
  let raw = null;
48867
49744
  try {
48868
- raw = readFileSync30(absolutePath, "utf8");
49745
+ raw = readFileSync33(absolutePath, "utf8");
48869
49746
  } catch {
48870
49747
  raw = null;
48871
49748
  }
48872
- if (raw !== null && suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw))) {
49749
+ if (raw !== null && isOwnWriteEcho(suppressor, absolutePath, strategy, raw)) {
48873
49750
  return;
48874
49751
  }
48875
49752
  }
@@ -48920,7 +49797,8 @@ function makeWatchHandler(deps) {
48920
49797
  echo: suppressor ? { watcher: "live", suppressor } : undefined
48921
49798
  }
48922
49799
  });
48923
- applyState(result.base ?? base2, clearEntityRejected(result.state, workspaceSlug, absolutePath));
49800
+ const heldUnreadable = result.outcome.kind === "ignored" && result.outcome.reason === FRONTMATTER_PARSE_ERROR;
49801
+ applyState(result.base ?? base2, heldUnreadable ? result.state : clearEntityRejected(result.state, workspaceSlug, absolutePath));
48924
49802
  reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
48925
49803
  } catch (err) {
48926
49804
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -49311,6 +50189,7 @@ async function runStart(workspaceSlug, deps = {}) {
49311
50189
  }
49312
50190
  const loggedTombstoneSkips = new Set;
49313
50191
  const loggedStashNotices = new Set;
50192
+ const loggedPullDeferrals = new Set;
49314
50193
  const initial = await runSync(workspaceSlug, {
49315
50194
  fetchImpl,
49316
50195
  keychain,
@@ -49468,7 +50347,7 @@ async function runStart(workspaceSlug, deps = {}) {
49468
50347
  return;
49469
50348
  }
49470
50349
  refreshFrozen();
49471
- out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason.kind}]`);
50350
+ out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)}]`);
49472
50351
  spinner.warn(`Held ${batch.length} destructive op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49473
50352
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49474
50353
  }
@@ -49524,7 +50403,7 @@ async function runStart(workspaceSlug, deps = {}) {
49524
50403
  return;
49525
50404
  }
49526
50405
  refreshFrozen();
49527
- out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason.kind} source=files-watch]`);
50406
+ out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)} source=files-watch]`);
49528
50407
  spinner.warn(`Held ${batch.length} destructive file op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49529
50408
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49530
50409
  }
@@ -49576,7 +50455,11 @@ async function runStart(workspaceSlug, deps = {}) {
49576
50455
  isFrozen,
49577
50456
  isFileFrozen,
49578
50457
  loggedTombstoneSkips,
49579
- loggedStashNotices
50458
+ loggedStashNotices,
50459
+ onPushDeferred: (absolutePath) => {
50460
+ watcher.nudge(absolutePath);
50461
+ },
50462
+ loggedPullDeferrals
49580
50463
  }));
49581
50464
  if (result.exitCode === 0) {
49582
50465
  const applied = result.applied ?? 0;
@@ -49616,6 +50499,9 @@ async function runStart(workspaceSlug, deps = {}) {
49616
50499
  afterPoll: () => {
49617
50500
  (async () => {
49618
50501
  state = loadState(slug);
50502
+ const unsettled = state;
50503
+ const settled = settleFrontmatterHolds(unsettled, slug);
50504
+ state = rebaseState(state, unsettled, settled);
49619
50505
  if (draining)
49620
50506
  return;
49621
50507
  draining = true;
@@ -49837,12 +50723,12 @@ async function runStart(workspaceSlug, deps = {}) {
49837
50723
  try {
49838
50724
  await Promise.race([
49839
50725
  watcher.drainPending().then(() => Promise.all([gate.drain(), fileGate.drain()])),
49840
- new Promise((resolve6) => setTimeout(() => resolve6(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
50726
+ new Promise((resolve7) => setTimeout(() => resolve7(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
49841
50727
  ]);
49842
50728
  } finally {
49843
50729
  await Promise.race([
49844
50730
  watcher.stop(),
49845
- new Promise((resolve6) => setTimeout(resolve6, SHUTDOWN_DRAIN_TIMEOUT_MS))
50731
+ new Promise((resolve7) => setTimeout(resolve7, SHUTDOWN_DRAIN_TIMEOUT_MS))
49846
50732
  ]);
49847
50733
  clearDaemonPid(slug);
49848
50734
  releaseDaemonLock(lockHandle);
@@ -49938,6 +50824,21 @@ function reportOutcome(spinner, slug, state, outcome) {
49938
50824
  return;
49939
50825
  }
49940
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
+ }
49941
50842
  function reportFileOutcome(spinner, slug, state, outcome) {
49942
50843
  const summary = watchingSummary(state);
49943
50844
  switch (outcome.kind) {
@@ -49954,7 +50855,7 @@ function reportFileOutcome(spinner, slug, state, outcome) {
49954
50855
  spinner.update(`Watching ${slug} · ${summary}`);
49955
50856
  return;
49956
50857
  case "conflict":
49957
- 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));
49958
50859
  spinner.update(`Watching ${slug} · ${summary}`);
49959
50860
  return;
49960
50861
  case "over_cap":
@@ -50094,7 +50995,7 @@ var startCommand = defineCommand({
50094
50995
  });
50095
50996
 
50096
50997
  // src/commands/status.ts
50097
- import { statSync as statSync20 } from "node:fs";
50998
+ import { existsSync as existsSync12, statSync as statSync20 } from "node:fs";
50098
50999
 
50099
51000
  // src/lib/context/context-walls.ts
50100
51001
  function prepare(raw) {
@@ -50395,14 +51296,69 @@ function assessContextWalls(workspaceSlug, deps = {}) {
50395
51296
  return { walls, played, asOf, unreadable };
50396
51297
  }
50397
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
+
50398
51354
  // src/lib/build-stamp.ts
50399
51355
  import { execFileSync as execFileSync4 } from "node:child_process";
50400
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
50401
- 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";
50402
51358
  import { fileURLToPath } from "node:url";
50403
51359
  var BUILD_STAMP_FILENAME = "build-stamp.json";
50404
51360
  function stampPath(distDir) {
50405
- return join19(distDir, BUILD_STAMP_FILENAME);
51361
+ return join20(distDir, BUILD_STAMP_FILENAME);
50406
51362
  }
50407
51363
  function readHead(repoDir) {
50408
51364
  try {
@@ -50421,7 +51377,7 @@ function readHead(repoDir) {
50421
51377
  }
50422
51378
  function readBuildStamp(distDir) {
50423
51379
  try {
50424
- const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
51380
+ const parsed = JSON.parse(readFileSync34(stampPath(distDir), "utf8"));
50425
51381
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
50426
51382
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
50427
51383
  }
@@ -50433,7 +51389,7 @@ function readBuildStamp(distDir) {
50433
51389
  function findRepoRoot(fromDir) {
50434
51390
  let dir = fromDir;
50435
51391
  for (;; ) {
50436
- if (existsSync11(join19(dir, ".git")))
51392
+ if (existsSync11(join20(dir, ".git")))
50437
51393
  return dir;
50438
51394
  const parent = dirname14(dir);
50439
51395
  if (parent === dir)
@@ -50482,12 +51438,12 @@ function checkBuildStaleness(deps = {}) {
50482
51438
  }
50483
51439
 
50484
51440
  // src/lib/memory-links.ts
50485
- import { readFileSync as readFileSync32 } from "node:fs";
50486
- import { relative as relative15, sep as sep8 } from "node:path";
51441
+ import { readFileSync as readFileSync35 } from "node:fs";
51442
+ import { relative as relative15, sep as sep9 } from "node:path";
50487
51443
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
50488
51444
  function scanMemoryLinks(deps) {
50489
51445
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
50490
- const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
51446
+ const readFile = deps.readFile ?? ((p) => readFileSync35(p, "utf8"));
50491
51447
  let root;
50492
51448
  let absPaths;
50493
51449
  try {
@@ -50496,7 +51452,7 @@ function scanMemoryLinks(deps) {
50496
51452
  } catch {
50497
51453
  return { scanned: 0, unreadable: [], dead: [] };
50498
51454
  }
50499
- const relPaths = absPaths.map((p) => relative15(root, p).split(sep8).join("/"));
51455
+ const relPaths = absPaths.map((p) => relative15(root, p).split(sep9).join("/"));
50500
51456
  const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
50501
51457
  const dead = [];
50502
51458
  const unreadable = [];
@@ -50531,9 +51487,8 @@ function scanMemoryLinks(deps) {
50531
51487
  init_output();
50532
51488
 
50533
51489
  // src/lib/pending-writes.ts
50534
- import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
51490
+ import { readFileSync as readFileSync36, statSync as statSync19 } from "node:fs";
50535
51491
  import { relative as relative16 } from "node:path";
50536
- var import_gray_matter15 = __toESM(require_gray_matter(), 1);
50537
51492
  var EMPTY3 = { writes: [], count: 0 };
50538
51493
  function reportedElsewhere(state) {
50539
51494
  const entities = new Set;
@@ -50579,7 +51534,7 @@ function scanPendingWrites(args) {
50579
51534
  continue;
50580
51535
  let hash2;
50581
51536
  try {
50582
- hash2 = sha256Bytes(readFileSync33(absolutePath));
51537
+ hash2 = sha256Bytes(readFileSync36(absolutePath));
50583
51538
  } catch {
50584
51539
  continue;
50585
51540
  }
@@ -50610,17 +51565,15 @@ function scanPendingWrites(args) {
50610
51565
  continue;
50611
51566
  let content;
50612
51567
  try {
50613
- content = readFileSync33(absolutePath, "utf8");
51568
+ content = readFileSync36(absolutePath, "utf8");
50614
51569
  } catch {
50615
51570
  continue;
50616
51571
  }
50617
51572
  if (!entry2) {
50618
- let frontmatter;
50619
- try {
50620
- frontmatter = import_gray_matter15.default(content).data ?? {};
50621
- } catch {
51573
+ const read = readFrontmatter(content);
51574
+ if (holdsUnreadableUntracked({ entityType, entitySlug, readable: read.ok }))
50622
51575
  continue;
50623
- }
51576
+ const frontmatter = read.ok ? read.parsed.data : {};
50624
51577
  const verdict = classifyUntrackedEntity({
50625
51578
  entityType,
50626
51579
  entitySlug,
@@ -51055,6 +52008,23 @@ async function runStatus(args, deps = {}) {
51055
52008
  }
51056
52009
  out.info("");
51057
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("");
51058
52028
  const rejected = listRejected(localState);
51059
52029
  const freshness = Object.values(localState.memory_sources?.unwritten ?? {}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
51060
52030
  const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
@@ -51076,7 +52046,7 @@ async function runStatus(args, deps = {}) {
51076
52046
  }
51077
52047
  }
51078
52048
  if (freshness.length > 0) {
51079
- out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state. Your edits are untouched.`));
52049
+ out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state.`));
51080
52050
  for (const r of freshness.slice(0, MAX_SHOWN)) {
51081
52051
  out.info(cont(r.message));
51082
52052
  }
@@ -51521,7 +52491,7 @@ var versionCommand = defineCommand({
51521
52491
  });
51522
52492
 
51523
52493
  // src/commands/voice-gate.ts
51524
- import { readFileSync as readFileSync36 } from "node:fs";
52494
+ import { readFileSync as readFileSync39 } from "node:fs";
51525
52495
 
51526
52496
  // src/lib/voice-gate/api.ts
51527
52497
  var VoiceGateSurfaces = [
@@ -51623,15 +52593,15 @@ async function callVoiceGate(req) {
51623
52593
  }
51624
52594
 
51625
52595
  // src/lib/voice-gate/attempts.ts
51626
- import { createHash as createHash7 } from "node:crypto";
52596
+ import { createHash as createHash8 } from "node:crypto";
51627
52597
  import {
51628
52598
  mkdirSync as mkdirSync13,
51629
- readFileSync as readFileSync34,
51630
- renameSync as renameSync9,
51631
- unlinkSync as unlinkSync10,
51632
- writeFileSync as writeFileSync9
52599
+ readFileSync as readFileSync37,
52600
+ renameSync as renameSync10,
52601
+ unlinkSync as unlinkSync12,
52602
+ writeFileSync as writeFileSync10
51633
52603
  } from "node:fs";
51634
- import { join as join20 } from "node:path";
52604
+ import { join as join21 } from "node:path";
51635
52605
  var MAX_REGENS = 2;
51636
52606
  var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
51637
52607
  var AttemptEntrySchema = exports_external.object({
@@ -51644,20 +52614,20 @@ var AttemptsFileSchema = exports_external.object({
51644
52614
  entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
51645
52615
  });
51646
52616
  function attemptsDir(workspaceSlug) {
51647
- return join20(plinthHome(), "_voice_gate", workspaceSlug);
52617
+ return join21(plinthHome(), "_voice_gate", workspaceSlug);
51648
52618
  }
51649
52619
  function attemptsPath(workspaceSlug) {
51650
- return join20(attemptsDir(workspaceSlug), "attempts.json");
52620
+ return join21(attemptsDir(workspaceSlug), "attempts.json");
51651
52621
  }
51652
52622
  function attemptKey(identity) {
51653
- return createHash7("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
52623
+ return createHash8("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
51654
52624
  }
51655
52625
  function emptyFile() {
51656
52626
  return { version: 1, entries: {} };
51657
52627
  }
51658
52628
  function load2(workspaceSlug) {
51659
52629
  try {
51660
- const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
52630
+ const raw = readFileSync37(attemptsPath(workspaceSlug), "utf8");
51661
52631
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
51662
52632
  return parsed.success ? parsed.data : emptyFile();
51663
52633
  } catch {
@@ -51678,13 +52648,13 @@ function save(workspaceSlug, file2) {
51678
52648
  const target = attemptsPath(workspaceSlug);
51679
52649
  const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
51680
52650
  mkdirSync13(dir, { recursive: true, mode: 448 });
51681
- writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
52651
+ writeFileSync10(tmp, `${JSON.stringify(file2, null, 2)}
51682
52652
  `, { mode: 384 });
51683
52653
  try {
51684
- renameSync9(tmp, target);
52654
+ renameSync10(tmp, target);
51685
52655
  } catch (err) {
51686
52656
  try {
51687
- unlinkSync10(tmp);
52657
+ unlinkSync12(tmp);
51688
52658
  } catch {}
51689
52659
  throw err;
51690
52660
  }
@@ -51820,9 +52790,9 @@ function denyUngated(detail) {
51820
52790
  }
51821
52791
 
51822
52792
  // src/lib/voice-gate/payload.ts
51823
- import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
52793
+ import { readFileSync as readFileSync38, statSync as statSync21 } from "node:fs";
51824
52794
  import { homedir as homedir2 } from "node:os";
51825
- import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
52795
+ import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
51826
52796
  var MAX_BODY_BYTES = 256 * 1024;
51827
52797
  var HELPER_BASENAME = "push_gmail_draft.py";
51828
52798
  var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
@@ -51981,7 +52951,7 @@ function defaultReadBodyFile(path5) {
51981
52951
  if (stat4.size > MAX_BODY_BYTES) {
51982
52952
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
51983
52953
  }
51984
- return readFileSync35(path5, "utf8");
52954
+ return readFileSync38(path5, "utf8");
51985
52955
  }
51986
52956
  function parseRecipients(...raw) {
51987
52957
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -52016,13 +52986,13 @@ function applyCd(tokens, state) {
52016
52986
  return { dir: expanded, known: true };
52017
52987
  if (!state.known)
52018
52988
  return state;
52019
- return { dir: resolve6(state.dir, expanded), known: true };
52989
+ return { dir: resolve7(state.dir, expanded), known: true };
52020
52990
  }
52021
52991
  function expandHome(path5) {
52022
52992
  if (path5 === "~")
52023
52993
  return homedir2();
52024
52994
  if (path5.startsWith("~/"))
52025
- return resolve6(homedir2(), path5.slice(2));
52995
+ return resolve7(homedir2(), path5.slice(2));
52026
52996
  return path5;
52027
52997
  }
52028
52998
  function classifyHelperSegment(tokens, dirState, readBodyFile) {
@@ -52048,7 +53018,7 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
52048
53018
  unreadable: `the draft push uses a relative \`--body-file\` (${bodyFile}) after a \`cd\` ` + `the voice gate cannot resolve, so it cannot tell which file ships. Pass an ` + `absolute \`--body-file\` path.`
52049
53019
  };
52050
53020
  }
52051
- const resolved = isAbsolute4(expanded) ? expanded : resolve6(dirState.dir, expanded);
53021
+ const resolved = isAbsolute4(expanded) ? expanded : resolve7(dirState.dir, expanded);
52052
53022
  let body;
52053
53023
  try {
52054
53024
  body = readBodyFile(resolved);
@@ -52341,7 +53311,7 @@ async function runVoiceGateDirect(args, deps = {}) {
52341
53311
  let text;
52342
53312
  if (args.file) {
52343
53313
  try {
52344
- text = readFileSync36(args.file, "utf8");
53314
+ text = readFileSync39(args.file, "utf8");
52345
53315
  } catch (err) {
52346
53316
  const msg = err instanceof Error ? err.message : "unknown error";
52347
53317
  out.error(`Could not read ${args.file}: ${msg}`);