@plinth-music/cli 0.21.0 → 0.21.1

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
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
7584
7584
  // package.json
7585
7585
  var package_default = {
7586
7586
  name: "@plinth-music/cli",
7587
- version: "0.21.0",
7587
+ version: "0.21.1",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -26125,6 +26125,27 @@ function isLaterVersion(a, b) {
26125
26125
  return false;
26126
26126
  return ta > tb;
26127
26127
  }
26128
+ function committedEntryIfNewer(workspaceSlug, entityType, entry) {
26129
+ let raw;
26130
+ try {
26131
+ raw = readFileSync7(stateFilePath(workspaceSlug), "utf8");
26132
+ } catch {
26133
+ return;
26134
+ }
26135
+ let committed;
26136
+ try {
26137
+ const parsed = JSON.parse(raw);
26138
+ committed = parsed.entries?.[entityType]?.[entry.id];
26139
+ } catch {
26140
+ return;
26141
+ }
26142
+ if (committed === undefined)
26143
+ return;
26144
+ const valid = StateEntrySchema.safeParse(committed);
26145
+ if (!valid.success)
26146
+ return;
26147
+ return isLaterVersion(valid.data, entry) ? valid.data : undefined;
26148
+ }
26128
26149
  function mergeKey(fresh, before, after) {
26129
26150
  if (before === after)
26130
26151
  return { value: fresh, overruled: false };
@@ -26200,8 +26221,7 @@ function mergeAnchor(fresh, before, after) {
26200
26221
  return fresh;
26201
26222
  return after;
26202
26223
  }
26203
- function commitState(before, after) {
26204
- const fresh = loadState(after.workspace_slug);
26224
+ function mergeWorkspaceState(fresh, before, after, wantCorrected = true) {
26205
26225
  const entityTypes = new Set([
26206
26226
  ...Object.keys(fresh.entries),
26207
26227
  ...Object.keys(before.entries),
@@ -26242,10 +26262,11 @@ function commitState(before, after) {
26242
26262
  last_trim_at: mergeScalar(fresh.last_trim_at, before.last_trim_at, after.last_trim_at),
26243
26263
  memory_facts: mergeScalar(fresh.memory_facts, before.memory_facts, after.memory_facts)
26244
26264
  };
26245
- writeState(merged);
26265
+ if (!wantCorrected)
26266
+ return { merged, corrected: after };
26246
26267
  const singletonOverruled = rules.overruled || memory.overruled || userMemory.overruled || userVoice.overruled;
26247
26268
  if (overruledEntries.length === 0 && files.overruled.length === 0 && !singletonOverruled) {
26248
- return after;
26269
+ return { merged, corrected: after };
26249
26270
  }
26250
26271
  let corrected = after;
26251
26272
  if (rules.overruled)
@@ -26264,8 +26285,20 @@ function commitState(before, after) {
26264
26285
  const committed = merged.files?.[path];
26265
26286
  corrected = committed ? upsertFileEntry(corrected, committed).state : removeFileEntry(corrected, path).state;
26266
26287
  }
26288
+ return { merged, corrected };
26289
+ }
26290
+ function commitState(before, after) {
26291
+ const { merged, corrected } = mergeWorkspaceState(loadState(after.workspace_slug), before, after);
26292
+ writeState(merged);
26267
26293
  return corrected;
26268
26294
  }
26295
+ function rebaseState(current, base2, next) {
26296
+ if (next === base2)
26297
+ return current;
26298
+ if (current === base2)
26299
+ return next;
26300
+ return mergeWorkspaceState(current, base2, next, false).merged;
26301
+ }
26269
26302
  function upsertEntry(state, entityType, entry) {
26270
26303
  const bucket = state.entries[entityType] ?? {};
26271
26304
  const existing = bucket[entry.id];
@@ -34324,7 +34357,7 @@ class PerIdMutex {
34324
34357
 
34325
34358
  // src/lib/push.ts
34326
34359
  var import_gray_matter10 = __toESM(require_gray_matter(), 1);
34327
- import { readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
34360
+ import { existsSync as existsSync4, readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
34328
34361
  import { basename as basename5, relative as relative5 } from "node:path";
34329
34362
 
34330
34363
  // src/lib/frontmatter-id.ts
@@ -34512,6 +34545,10 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor,
34512
34545
  }
34513
34546
 
34514
34547
  // src/lib/push.ts
34548
+ var DELETE_DECLINED_PATH_EXISTS = "the file is on disk again — a newer write owns this row";
34549
+ function deleteDeclinedSentence(relPath) {
34550
+ 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.`;
34551
+ }
34515
34552
  function normaliseWireBody(body) {
34516
34553
  if (body === undefined)
34517
34554
  return;
@@ -34889,6 +34926,7 @@ async function reconcileContextDocConflict(args) {
34889
34926
  now,
34890
34927
  output
34891
34928
  } = args;
34929
+ const base2 = args.state;
34892
34930
  let state = args.state;
34893
34931
  const existingSlug = existing.slug;
34894
34932
  const existingId = existing.id;
@@ -34938,7 +34976,8 @@ async function reconcileContextDocConflict(args) {
34938
34976
  entitySlug: fileSlug,
34939
34977
  stashPath
34940
34978
  },
34941
- state: nextState
34979
+ state: nextState,
34980
+ base: base2
34942
34981
  };
34943
34982
  }
34944
34983
  const stashPreLandCloudCopy = () => stashConflict({
@@ -35000,7 +35039,8 @@ async function reconcileContextDocConflict(args) {
35000
35039
  entitySlug: updated.slug,
35001
35040
  stashPath: preLandStash
35002
35041
  },
35003
- state
35042
+ state,
35043
+ base: base2
35004
35044
  };
35005
35045
  }
35006
35046
  if (patchResult.code === 404) {
@@ -35058,7 +35098,8 @@ async function reconcileContextDocConflict(args) {
35058
35098
  entitySlug: fileSlug,
35059
35099
  stashPath
35060
35100
  },
35061
- state
35101
+ state,
35102
+ base: base2
35062
35103
  };
35063
35104
  }
35064
35105
  if (patchResult.reason === "context_doc_conflict") {
@@ -35086,10 +35127,11 @@ async function runPushUpsert(args) {
35086
35127
  const hashBody = hashableBody(strategy, bodyMarkdown);
35087
35128
  const newBodyHash = stableHash(hashBody);
35088
35129
  const newFrontmatterHash = stableHash({ ...userFm, ...lifted.topLevel });
35089
- const liveEntry = getEntry(args.state, strategy.type, args.stateEntry.id) ?? args.stateEntry;
35130
+ const snapshotEntry = getEntry(args.state, strategy.type, args.stateEntry.id) ?? args.stateEntry;
35131
+ const liveEntry = committedEntryIfNewer(args.workspaceSlug, strategy.type, snapshotEntry) ?? snapshotEntry;
35090
35132
  const bodyChanged = newBodyHash !== liveEntry.body_hash;
35091
35133
  const frontmatterChanged = newFrontmatterHash !== liveEntry.frontmatter_hash;
35092
- let workingState = args.state;
35134
+ let workingState = liveEntry === snapshotEntry ? args.state : upsertEntry(args.state, strategy.type, liveEntry).state;
35093
35135
  if (fm["id"] === undefined && workingState.frontmatter_id_backfill_version !== undefined) {
35094
35136
  const cleared = clearFrontmatterIdBackfillMarker(workingState);
35095
35137
  if (cleared.changed) {
@@ -35098,6 +35140,69 @@ async function runPushUpsert(args) {
35098
35140
  } catch {}
35099
35141
  }
35100
35142
  }
35143
+ const wireProjection = (md) => {
35144
+ try {
35145
+ const p = import_gray_matter10.default(md);
35146
+ const f = p.data ?? {};
35147
+ const l = splitUserFrontmatter(strategy, f);
35148
+ const u = userFrontmatter(strategy, l.frontmatter);
35149
+ return stableHash({
35150
+ body: strategy.bodyToWire(p.content),
35151
+ frontmatter: { ...u, ...l.topLevel }
35152
+ });
35153
+ } catch {
35154
+ return null;
35155
+ }
35156
+ };
35157
+ if (liveEntry !== snapshotEntry) {
35158
+ let onDisk = null;
35159
+ try {
35160
+ onDisk = readFileSync17(args.absolutePath, "utf8");
35161
+ } catch {
35162
+ onDisk = null;
35163
+ }
35164
+ if (onDisk !== null && onDisk !== args.raw) {
35165
+ let disk = null;
35166
+ try {
35167
+ disk = computeMirrorHashes(strategy, onDisk);
35168
+ } catch {
35169
+ disk = null;
35170
+ }
35171
+ if (disk !== null && disk.bodyHash === liveEntry.body_hash && disk.frontmatterHash === liveEntry.frontmatter_hash) {
35172
+ const ours = wireProjection(args.raw);
35173
+ const theirs = wireProjection(onDisk);
35174
+ if (ours !== null && theirs !== null && ours === theirs) {
35175
+ return {
35176
+ outcome: {
35177
+ kind: "noop",
35178
+ entityType: strategy.type,
35179
+ entitySlug: liveEntry.slug
35180
+ },
35181
+ state: workingState,
35182
+ base: workingState
35183
+ };
35184
+ }
35185
+ const stashPath2 = stashConflict({
35186
+ workspaceSlug: args.workspaceSlug,
35187
+ entityType: strategy.type,
35188
+ entitySlug: liveEntry.slug,
35189
+ timestamp: args.now(),
35190
+ contents: args.raw,
35191
+ side: "local"
35192
+ });
35193
+ return {
35194
+ outcome: {
35195
+ kind: "conflict",
35196
+ entityType: strategy.type,
35197
+ entitySlug: liveEntry.slug,
35198
+ stashPath: stashPath2
35199
+ },
35200
+ state: workingState,
35201
+ base: workingState
35202
+ };
35203
+ }
35204
+ }
35205
+ }
35101
35206
  if (!bodyChanged && !frontmatterChanged) {
35102
35207
  return {
35103
35208
  outcome: {
@@ -35105,7 +35210,8 @@ async function runPushUpsert(args) {
35105
35210
  entityType: strategy.type,
35106
35211
  entitySlug: liveEntry.slug
35107
35212
  },
35108
- state: workingState
35213
+ state: workingState,
35214
+ base: workingState
35109
35215
  };
35110
35216
  }
35111
35217
  const wireBodyFields = bodyChanged ? strategy.bodyToWire(bodyMarkdown) : undefined;
@@ -35124,7 +35230,8 @@ async function runPushUpsert(args) {
35124
35230
  entityType: strategy.type,
35125
35231
  entitySlug: liveEntry.slug
35126
35232
  },
35127
- state: persistEntry(workingState, strategy.type, updated)
35233
+ state: persistEntry(workingState, strategy.type, updated),
35234
+ base: workingState
35128
35235
  };
35129
35236
  }
35130
35237
  const mergedTopLevel = split || wireBodyFields?.topLevel ? {
@@ -35160,7 +35267,8 @@ async function runPushUpsert(args) {
35160
35267
  fields,
35161
35268
  pushed: false
35162
35269
  },
35163
- state: nextState2
35270
+ state: nextState2,
35271
+ base: workingState
35164
35272
  };
35165
35273
  }
35166
35274
  let patchResult = await patchEntity({
@@ -35233,7 +35341,8 @@ async function runPushUpsert(args) {
35233
35341
  fields,
35234
35342
  pushed: true
35235
35343
  },
35236
- state: nextState2
35344
+ state: nextState2,
35345
+ base: workingState
35237
35346
  };
35238
35347
  }
35239
35348
  return {
@@ -35242,7 +35351,8 @@ async function runPushUpsert(args) {
35242
35351
  entityType: strategy.type,
35243
35352
  entitySlug: updated.slug
35244
35353
  },
35245
- state: nextState2
35354
+ state: nextState2,
35355
+ base: workingState
35246
35356
  };
35247
35357
  }
35248
35358
  if (patchResult.code === 404) {
@@ -35252,7 +35362,8 @@ async function runPushUpsert(args) {
35252
35362
  entityType: strategy.type,
35253
35363
  entitySlug: liveEntry.slug
35254
35364
  },
35255
- state: workingState
35365
+ state: workingState,
35366
+ base: workingState
35256
35367
  };
35257
35368
  }
35258
35369
  if (patchResult.reason === "slug_conflict") {
@@ -35322,7 +35433,8 @@ async function runPushUpsert(args) {
35322
35433
  entitySlug: liveEntry.slug,
35323
35434
  stashPath
35324
35435
  },
35325
- state: nextState
35436
+ state: nextState,
35437
+ base: workingState
35326
35438
  };
35327
35439
  });
35328
35440
  }
@@ -35341,6 +35453,7 @@ async function reconcileTaskConflict(args) {
35341
35453
  now,
35342
35454
  output
35343
35455
  } = args;
35456
+ const base2 = args.state;
35344
35457
  let state = args.state;
35345
35458
  const stashLocalSide = () => stashConflict({
35346
35459
  workspaceSlug,
@@ -35373,7 +35486,8 @@ async function reconcileTaskConflict(args) {
35373
35486
  entitySlug: liveEntry.slug,
35374
35487
  stashPath: stashPath2
35375
35488
  },
35376
- state
35489
+ state,
35490
+ base: base2
35377
35491
  };
35378
35492
  }
35379
35493
  let cloudRow = null;
@@ -35391,7 +35505,8 @@ async function reconcileTaskConflict(args) {
35391
35505
  entityType: strategy.type,
35392
35506
  entitySlug: liveEntry.slug
35393
35507
  },
35394
- state
35508
+ state,
35509
+ base: base2
35395
35510
  };
35396
35511
  }
35397
35512
  const stashCloudSide = () => stashConflict({
@@ -35431,7 +35546,8 @@ async function reconcileTaskConflict(args) {
35431
35546
  entitySlug: patchResult.doc.slug,
35432
35547
  stashPath: stashPath2
35433
35548
  },
35434
- state
35549
+ state,
35550
+ base: base2
35435
35551
  };
35436
35552
  }
35437
35553
  if (patchResult.code === 404) {
@@ -35442,7 +35558,8 @@ async function reconcileTaskConflict(args) {
35442
35558
  entityType: strategy.type,
35443
35559
  entitySlug: liveEntry.slug
35444
35560
  },
35445
- state
35561
+ state,
35562
+ base: base2
35446
35563
  };
35447
35564
  }
35448
35565
  const stashPath = stashLocalSide();
@@ -35465,7 +35582,8 @@ async function reconcileTaskConflict(args) {
35465
35582
  entitySlug: liveEntry.slug,
35466
35583
  stashPath
35467
35584
  },
35468
- state
35585
+ state,
35586
+ base: base2
35469
35587
  };
35470
35588
  }
35471
35589
  function writeCreatedIdBack(args) {
@@ -35576,7 +35694,8 @@ async function runPushCreate(args) {
35576
35694
  fields,
35577
35695
  pushed: true
35578
35696
  },
35579
- state: nextState2
35697
+ state: nextState2,
35698
+ base: args.state
35580
35699
  };
35581
35700
  }
35582
35701
  return {
@@ -35585,7 +35704,8 @@ async function runPushCreate(args) {
35585
35704
  entityType: strategy.type,
35586
35705
  entitySlug: doc2.slug
35587
35706
  },
35588
- state: nextState2
35707
+ state: nextState2,
35708
+ base: args.state
35589
35709
  };
35590
35710
  }
35591
35711
  if (createResult.reason === "context_doc_conflict") {
@@ -35634,7 +35754,8 @@ async function runPushCreate(args) {
35634
35754
  entitySlug: args.slug,
35635
35755
  stashPath
35636
35756
  },
35637
- state: nextState
35757
+ state: nextState,
35758
+ base: args.state
35638
35759
  };
35639
35760
  });
35640
35761
  }
@@ -35668,12 +35789,31 @@ async function runPushDelete(args) {
35668
35789
  absolutePath: args.absolutePath,
35669
35790
  reason: "untracked file — no cloud row to delete"
35670
35791
  },
35671
- state: args.state
35792
+ state: args.state,
35793
+ base: args.state
35672
35794
  };
35673
35795
  }
35674
35796
  return args.mutex.run(id, async () => {
35675
- const live = getEntry(args.state, strategy.type, id) ?? null;
35797
+ const snapshot = getEntry(args.state, strategy.type, id) ?? null;
35798
+ const live = snapshot ? committedEntryIfNewer(args.workspaceSlug, strategy.type, snapshot) ?? snapshot : null;
35799
+ if (existsSync4(args.absolutePath)) {
35800
+ args.output?.log(entityDeleteSuppressedLine({
35801
+ entityType: strategy.type,
35802
+ slug,
35803
+ reason: "path_exists"
35804
+ }));
35805
+ return {
35806
+ outcome: {
35807
+ kind: "ignored",
35808
+ absolutePath: args.absolutePath,
35809
+ reason: DELETE_DECLINED_PATH_EXISTS
35810
+ },
35811
+ state: args.state,
35812
+ base: args.state
35813
+ };
35814
+ }
35676
35815
  const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
35816
+ const workingState = live && live !== snapshot ? upsertEntry(args.state, strategy.type, live).state : args.state;
35677
35817
  const result = await deleteEntity({
35678
35818
  strategy,
35679
35819
  baseUrl: args.baseUrl,
@@ -35683,7 +35823,7 @@ async function runPushDelete(args) {
35683
35823
  fetchImpl: args.fetchImpl
35684
35824
  });
35685
35825
  if (result.ok) {
35686
- const nextState2 = dropEntry(args.state, strategy.type, id);
35826
+ const nextState2 = dropEntry(workingState, strategy.type, id);
35687
35827
  args.output?.log(`[DELETE entityType=${strategy.type} slug=${slug} id=${id}]`);
35688
35828
  return {
35689
35829
  outcome: {
@@ -35691,11 +35831,12 @@ async function runPushDelete(args) {
35691
35831
  entityType: strategy.type,
35692
35832
  entitySlug: slug
35693
35833
  },
35694
- state: nextState2
35834
+ state: nextState2,
35835
+ base: workingState
35695
35836
  };
35696
35837
  }
35697
35838
  if (result.code === 404) {
35698
- const nextState2 = dropEntry(args.state, strategy.type, id);
35839
+ const nextState2 = dropEntry(workingState, strategy.type, id);
35699
35840
  args.output?.log(`[DELETE entityType=${strategy.type} slug=${slug} id=${id} note=already_absent]`);
35700
35841
  return {
35701
35842
  outcome: {
@@ -35703,7 +35844,8 @@ async function runPushDelete(args) {
35703
35844
  entityType: strategy.type,
35704
35845
  entitySlug: slug
35705
35846
  },
35706
- state: nextState2
35847
+ state: nextState2,
35848
+ base: workingState
35707
35849
  };
35708
35850
  }
35709
35851
  const nextState = await pullAndWriteMirror({
@@ -35725,7 +35867,8 @@ async function runPushDelete(args) {
35725
35867
  entitySlug: slug,
35726
35868
  stashPath: ""
35727
35869
  },
35728
- state: nextState
35870
+ state: nextState,
35871
+ base: workingState
35729
35872
  };
35730
35873
  });
35731
35874
  }
@@ -35757,8 +35900,10 @@ async function pushRename(args) {
35757
35900
  toSlug = basename5(toPath, ".md");
35758
35901
  }
35759
35902
  return mutex.run(id, async () => {
35760
- const live = getEntry(state, entityType, id);
35903
+ const snapshot = getEntry(state, entityType, id);
35904
+ const live = snapshot ? committedEntryIfNewer(workspaceSlug, entityType, snapshot) ?? snapshot : undefined;
35761
35905
  const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
35906
+ const workingState = live && live !== snapshot ? upsertEntry(state, entityType, live).state : state;
35762
35907
  let patchResult = await patchEntity({
35763
35908
  strategy,
35764
35909
  baseUrl,
@@ -35818,7 +35963,7 @@ async function pushRename(args) {
35818
35963
  updated.body_len = renameHashBody.length;
35819
35964
  } catch {}
35820
35965
  }
35821
- const nextState2 = persistEntry(state, entityType, updated);
35966
+ const nextState2 = persistEntry(workingState, entityType, updated);
35822
35967
  output?.log(`[RENAME entityType=${entityType} fromSlug=${fromSlug} toSlug=${doc2.slug} id=${doc2.id}]`);
35823
35968
  return {
35824
35969
  outcome: {
@@ -35827,13 +35972,15 @@ async function pushRename(args) {
35827
35972
  fromSlug,
35828
35973
  toSlug: doc2.slug
35829
35974
  },
35830
- state: nextState2
35975
+ state: nextState2,
35976
+ base: workingState
35831
35977
  };
35832
35978
  }
35833
35979
  if (patchResult.code === 404) {
35834
35980
  return {
35835
35981
  outcome: { kind: "not_found", entityType, entitySlug: fromSlug },
35836
- state
35982
+ state: workingState,
35983
+ base: workingState
35837
35984
  };
35838
35985
  }
35839
35986
  if (patchResult.reason === "slug_conflict") {
@@ -35855,7 +36002,7 @@ async function pushRename(args) {
35855
36002
  baseUrl,
35856
36003
  pat,
35857
36004
  id: patchResult.existing.id,
35858
- state,
36005
+ state: workingState,
35859
36006
  fetchImpl,
35860
36007
  echo,
35861
36008
  output,
@@ -35881,7 +36028,8 @@ async function pushRename(args) {
35881
36028
  toSlug,
35882
36029
  stashPath: stashPath2
35883
36030
  },
35884
- state: finalState
36031
+ state: finalState,
36032
+ base: workingState
35885
36033
  };
35886
36034
  }
35887
36035
  let contents = "";
@@ -35902,7 +36050,7 @@ async function pushRename(args) {
35902
36050
  baseUrl,
35903
36051
  pat,
35904
36052
  id,
35905
- state,
36053
+ state: workingState,
35906
36054
  fetchImpl,
35907
36055
  echo,
35908
36056
  output,
@@ -35915,7 +36063,8 @@ async function pushRename(args) {
35915
36063
  entitySlug: fromSlug,
35916
36064
  stashPath
35917
36065
  },
35918
- state: nextState
36066
+ state: nextState,
36067
+ base: workingState
35919
36068
  };
35920
36069
  });
35921
36070
  }
@@ -35951,13 +36100,14 @@ async function applyBatch(batch, deps, mutex) {
35951
36100
  await applyFileOp(op, deps);
35952
36101
  continue;
35953
36102
  }
36103
+ const base2 = deps.getState();
35954
36104
  const result = await pushFile({
35955
36105
  workspaceSlug: deps.workspaceSlug,
35956
36106
  baseUrl: deps.baseUrl,
35957
36107
  pat: deps.pat,
35958
36108
  absolutePath: opPath(deps.workspaceSlug, op),
35959
36109
  entityType: op.entityType,
35960
- state: deps.getState(),
36110
+ state: base2,
35961
36111
  exists: op.kind === "overwrite",
35962
36112
  unlinkContext: op.kind === "delete" ? { id: op.id, slug: op.slug, updated_at: op.updated_at } : null,
35963
36113
  deps: {
@@ -35968,7 +36118,10 @@ async function applyBatch(batch, deps, mutex) {
35968
36118
  echo: deps.echo
35969
36119
  }
35970
36120
  });
35971
- deps.setState(result.state);
36121
+ deps.applyState(result.base ?? base2, result.state);
36122
+ if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
36123
+ deps.output?.warn(deleteDeclinedSentence(opPath(deps.workspaceSlug, op)));
36124
+ }
35972
36125
  } catch (err) {
35973
36126
  const msg = err instanceof Error ? err.message : "unknown error";
35974
36127
  deps.output?.warn(`Apply failed for ${opLabel(op)}: ${msg} (left for the next pull to reconcile).`);
@@ -35978,27 +36131,29 @@ async function applyBatch(batch, deps, mutex) {
35978
36131
  async function applyFileOp(op, deps) {
35979
36132
  const fileDeps = { fetchImpl: deps.fetchImpl, now: deps.now, output: deps.output };
35980
36133
  if (op.kind === "delete") {
36134
+ const base3 = deps.getState();
35981
36135
  const result2 = await pushFilesDelete({
35982
36136
  workspaceSlug: deps.workspaceSlug,
35983
36137
  baseUrl: deps.baseUrl,
35984
36138
  pat: deps.pat,
35985
36139
  relPath: op.path,
35986
- state: deps.getState(),
36140
+ state: base3,
35987
36141
  deps: fileDeps
35988
36142
  });
35989
- deps.setState(result2.state);
36143
+ deps.applyState(base3, result2.state);
35990
36144
  return;
35991
36145
  }
36146
+ const base2 = deps.getState();
35992
36147
  const result = await pushFilesUpsert({
35993
36148
  workspaceSlug: deps.workspaceSlug,
35994
36149
  baseUrl: deps.baseUrl,
35995
36150
  pat: deps.pat,
35996
36151
  absolutePath: filesSubtreePath(deps.workspaceSlug, op.path),
35997
36152
  relPath: op.path,
35998
- state: deps.getState(),
36153
+ state: base2,
35999
36154
  deps: fileDeps
36000
36155
  });
36001
- deps.setState(result.state);
36156
+ deps.applyState(base2, result.state);
36002
36157
  }
36003
36158
  async function discardBatchOps(batch, deps) {
36004
36159
  for (const op of batch.ops) {
@@ -36027,19 +36182,20 @@ async function discardBatchOps(batch, deps) {
36027
36182
  });
36028
36183
  }
36029
36184
  }
36185
+ const base2 = deps.getState();
36030
36186
  const next = await pullAndWriteMirror({
36031
36187
  strategy,
36032
36188
  workspaceSlug: deps.workspaceSlug,
36033
36189
  baseUrl: deps.baseUrl,
36034
36190
  pat: deps.pat,
36035
36191
  id: op.id,
36036
- state: deps.getState(),
36192
+ state: base2,
36037
36193
  fetchImpl: deps.fetchImpl,
36038
36194
  echo: deps.echo,
36039
36195
  output: deps.output,
36040
36196
  now: deps.now
36041
36197
  });
36042
- deps.setState(next);
36198
+ deps.applyState(base2, next);
36043
36199
  } catch (err) {
36044
36200
  const msg = err instanceof Error ? err.message : "unknown error";
36045
36201
  deps.output?.warn(`Discard restore failed for ${opLabel(op)}: ${msg} (the next pull will reconcile).`);
@@ -36063,17 +36219,18 @@ async function discardFileOp(op, deps) {
36063
36219
  side: "local"
36064
36220
  });
36065
36221
  }
36222
+ const base2 = deps.getState();
36066
36223
  const next = await restoreFileFromCloud({
36067
36224
  workspaceSlug: deps.workspaceSlug,
36068
36225
  baseUrl: deps.baseUrl,
36069
36226
  pat: deps.pat,
36070
36227
  id: op.id,
36071
36228
  relPath: op.path,
36072
- state: deps.getState(),
36229
+ state: base2,
36073
36230
  fetchImpl: deps.fetchImpl,
36074
36231
  output: deps.output
36075
36232
  });
36076
- deps.setState(next);
36233
+ deps.applyState(base2, next);
36077
36234
  }
36078
36235
  function opPath(workspaceSlug, op) {
36079
36236
  try {
@@ -36634,7 +36791,7 @@ function recordFileUnsynced(args) {
36634
36791
  });
36635
36792
  }
36636
36793
  async function drainUnsyncedOps(args) {
36637
- const { workspaceSlug, baseUrl, pat, getState, setState, fetchImpl, mutex, output, echo } = args;
36794
+ const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex, output, echo } = args;
36638
36795
  const now = args.now ?? (() => new Date);
36639
36796
  const ops = listUnsynced(getState());
36640
36797
  let attempted = 0;
@@ -36645,7 +36802,7 @@ async function drainUnsyncedOps(args) {
36645
36802
  const before = getState();
36646
36803
  const { state: next, changed } = removeUnsynced(before, target, path5);
36647
36804
  if (changed)
36648
- setState(commitState(before, next));
36805
+ applyState(before, commitState(before, next));
36649
36806
  };
36650
36807
  for (const op of ops) {
36651
36808
  attempted += 1;
@@ -36658,38 +36815,46 @@ async function drainUnsyncedOps(args) {
36658
36815
  resolved += 1;
36659
36816
  continue;
36660
36817
  }
36818
+ const base2 = getState();
36661
36819
  const result = await pushFile({
36662
36820
  workspaceSlug,
36663
36821
  baseUrl,
36664
36822
  pat,
36665
36823
  absolutePath,
36666
36824
  entityType: classified.entityType,
36667
- state: getState(),
36825
+ state: base2,
36668
36826
  exists: op.op === "upsert",
36669
36827
  unlinkContext: null,
36670
36828
  deps: { fetchImpl, now, mutex, output, echo }
36671
36829
  });
36672
- setState(result.state);
36830
+ applyState(result.base ?? base2, result.state);
36831
+ if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
36832
+ output?.warn(deleteDeclinedSentence(op.path));
36833
+ }
36673
36834
  } else {
36674
36835
  const absolutePath = filesSubtreePath(workspaceSlug, op.path);
36675
- const doFile = () => op.op === "delete" ? pushFilesDelete({
36676
- workspaceSlug,
36677
- baseUrl,
36678
- pat,
36679
- relPath: op.path,
36680
- state: getState(),
36681
- deps: { fetchImpl, now, output }
36682
- }) : pushFilesUpsert({
36683
- workspaceSlug,
36684
- baseUrl,
36685
- pat,
36686
- absolutePath,
36687
- relPath: op.path,
36688
- state: getState(),
36689
- deps: { fetchImpl, now, output }
36690
- });
36836
+ let base2;
36837
+ const doFile = () => {
36838
+ base2 = getState();
36839
+ return op.op === "delete" ? pushFilesDelete({
36840
+ workspaceSlug,
36841
+ baseUrl,
36842
+ pat,
36843
+ relPath: op.path,
36844
+ state: base2,
36845
+ deps: { fetchImpl, now, output }
36846
+ }) : pushFilesUpsert({
36847
+ workspaceSlug,
36848
+ baseUrl,
36849
+ pat,
36850
+ absolutePath,
36851
+ relPath: op.path,
36852
+ state: base2,
36853
+ deps: { fetchImpl, now, output }
36854
+ });
36855
+ };
36691
36856
  const result = mutex ? await mutex.run(`files:${op.path}`, doFile) : await doFile();
36692
- setState(result.state);
36857
+ applyState(base2, result.state);
36693
36858
  }
36694
36859
  clear(op.target, op.path);
36695
36860
  resolved += 1;
@@ -36709,7 +36874,7 @@ async function drainUnsyncedOps(args) {
36709
36874
  message: cat.serverMessage,
36710
36875
  rejected_at: now().toISOString()
36711
36876
  });
36712
- setState(commitState(beforeReject, next2));
36877
+ applyState(beforeReject, commitState(beforeReject, next2));
36713
36878
  continue;
36714
36879
  }
36715
36880
  stillFailing += 1;
@@ -36722,7 +36887,7 @@ async function drainUnsyncedOps(args) {
36722
36887
  last_reason: cat.reason
36723
36888
  });
36724
36889
  if (changed)
36725
- setState(commitState(beforeRetry, next));
36890
+ applyState(beforeRetry, commitState(beforeRetry, next));
36726
36891
  }
36727
36892
  }
36728
36893
  return { attempted, resolved, stillFailing, rejected };
@@ -39001,7 +39166,9 @@ async function pushArtistMemory(args) {
39001
39166
  }
39002
39167
  const localHash = stableHash(local);
39003
39168
  const runPatch = async () => {
39004
- let base2 = args.mutex ? loadState(workspaceSlug) : state;
39169
+ const snapshotEntry = getEntryBySlug(state, ENTITY_TYPE, slug);
39170
+ const fresher = args.mutex && snapshotEntry ? committedEntryIfNewer(workspaceSlug, ENTITY_TYPE, snapshotEntry) : undefined;
39171
+ let base2 = fresher ? upsertEntry(state, ENTITY_TYPE, fresher).state : state;
39005
39172
  const entry2 = getEntryBySlug(base2, ENTITY_TYPE, slug);
39006
39173
  if (!entry2)
39007
39174
  return { state: base2, outcome: { kind: "ignored", slug } };
@@ -43877,7 +44044,7 @@ function makeWatchHandler(deps) {
43877
44044
  spinner,
43878
44045
  mutex,
43879
44046
  getState,
43880
- setState,
44047
+ applyState,
43881
44048
  fetchImpl,
43882
44049
  now,
43883
44050
  output,
@@ -43887,6 +44054,10 @@ function makeWatchHandler(deps) {
43887
44054
  fileGate,
43888
44055
  isFileFrozen
43889
44056
  } = deps;
44057
+ const applyFromCurrent = (update) => {
44058
+ const base2 = getState();
44059
+ applyState(base2, update(base2));
44060
+ };
43890
44061
  const handleFilesEvent = async (relPath, absolutePath, exists) => {
43891
44062
  try {
43892
44063
  if (isFileFrozen?.(relPath))
@@ -43896,8 +44067,8 @@ function makeWatchHandler(deps) {
43896
44067
  } catch (err) {
43897
44068
  if (!(err instanceof MirrorSymlinkError))
43898
44069
  throw err;
43899
- setState(recordFileSymlinkRefused({
43900
- state: getState(),
44070
+ applyFromCurrent((base2) => recordFileSymlinkRefused({
44071
+ state: base2,
43901
44072
  relPath,
43902
44073
  op: exists ? "upsert" : "delete",
43903
44074
  now: now ?? (() => new Date)
@@ -43923,15 +44094,16 @@ function makeWatchHandler(deps) {
43923
44094
  }
43924
44095
  }
43925
44096
  await mutex.run(`files:${relPath}`, async () => {
44097
+ const base2 = getState();
43926
44098
  const result = await pushFilesDelete({
43927
44099
  workspaceSlug,
43928
44100
  baseUrl,
43929
44101
  pat,
43930
44102
  relPath,
43931
- state: getState(),
44103
+ state: base2,
43932
44104
  deps: { fetchImpl, now, output, suppressor }
43933
44105
  });
43934
- setState(clearReportedSafeFileRejected(result.state, relPath));
44106
+ applyState(base2, clearReportedSafeFileRejected(result.state, relPath));
43935
44107
  reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
43936
44108
  });
43937
44109
  return;
@@ -43956,8 +44128,8 @@ function makeWatchHandler(deps) {
43956
44128
  max: MAX_WORKSPACE_FILE_BYTES,
43957
44129
  source: "client"
43958
44130
  }));
43959
- setState(recordFileOverCap({
43960
- state: getState(),
44131
+ applyFromCurrent((base2) => recordFileOverCap({
44132
+ state: base2,
43961
44133
  relPath,
43962
44134
  size: st.size,
43963
44135
  max: MAX_WORKSPACE_FILE_BYTES,
@@ -43985,17 +44157,18 @@ function makeWatchHandler(deps) {
43985
44157
  }
43986
44158
  }
43987
44159
  await mutex.run(`files:${relPath}`, async () => {
44160
+ const base2 = getState();
43988
44161
  const result = await pushFilesUpsert({
43989
44162
  workspaceSlug,
43990
44163
  baseUrl,
43991
44164
  pat,
43992
44165
  absolutePath,
43993
44166
  relPath,
43994
- state: getState(),
44167
+ state: base2,
43995
44168
  prepared: { bytes, hash: hash2, size: st.size, inode: st.ino },
43996
44169
  deps: { fetchImpl, now, output, suppressor }
43997
44170
  });
43998
- setState(result.outcome.kind === "over_cap" ? result.state : clearFileRejected(result.state, relPath));
44171
+ applyState(base2, result.outcome.kind === "over_cap" ? result.state : clearFileRejected(result.state, relPath));
43999
44172
  reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
44000
44173
  });
44001
44174
  } catch (err) {
@@ -44003,16 +44176,16 @@ function makeWatchHandler(deps) {
44003
44176
  output?.log(fileFailedLine({ path: relPath, ...categorizeError(err) }));
44004
44177
  spinner.warn(`File push failed for files/${relPath}: ${msg}`);
44005
44178
  if (isRecordableFailure(err)) {
44006
- setState(recordFileUnsynced({
44007
- state: getState(),
44179
+ applyFromCurrent((base2) => recordFileUnsynced({
44180
+ state: base2,
44008
44181
  relPath,
44009
44182
  op: exists ? "upsert" : "delete",
44010
44183
  err,
44011
44184
  now: now ?? (() => new Date)
44012
44185
  }));
44013
44186
  } else if (isPermanentReject(err)) {
44014
- setState(recordFileRejected({
44015
- state: getState(),
44187
+ applyFromCurrent((base2) => recordFileRejected({
44188
+ state: base2,
44016
44189
  relPath,
44017
44190
  op: exists ? "upsert" : "delete",
44018
44191
  err,
@@ -44040,17 +44213,18 @@ function makeWatchHandler(deps) {
44040
44213
  }
44041
44214
  try {
44042
44215
  await mutex.run(WORKSPACE_RULES_MUTEX_KEY, async () => {
44216
+ const base2 = getState();
44043
44217
  const result = await pushWorkspaceRules({
44044
44218
  workspaceSlug,
44045
44219
  baseUrl,
44046
44220
  pat,
44047
- state: getState(),
44221
+ state: base2,
44048
44222
  fetchImpl,
44049
44223
  now,
44050
44224
  output,
44051
44225
  suppressor
44052
44226
  });
44053
- setState(result.state);
44227
+ applyState(base2, result.state);
44054
44228
  reportWorkspaceRulesOutcome(spinner, workspaceSlug, result.state, result.outcome);
44055
44229
  });
44056
44230
  } catch (err) {
@@ -44078,17 +44252,18 @@ function makeWatchHandler(deps) {
44078
44252
  }
44079
44253
  try {
44080
44254
  await mutex.run(WORKSPACE_MEMORY_MUTEX_KEY, async () => {
44255
+ const base2 = getState();
44081
44256
  const result = await pushWorkspaceMemory({
44082
44257
  workspaceSlug,
44083
44258
  baseUrl,
44084
44259
  pat,
44085
- state: getState(),
44260
+ state: base2,
44086
44261
  fetchImpl,
44087
44262
  now,
44088
44263
  output,
44089
44264
  suppressor
44090
44265
  });
44091
- setState(result.state);
44266
+ applyState(base2, result.state);
44092
44267
  reportWorkspaceMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
44093
44268
  });
44094
44269
  } catch (err) {
@@ -44116,17 +44291,18 @@ function makeWatchHandler(deps) {
44116
44291
  }
44117
44292
  try {
44118
44293
  await mutex.run(USER_MEMORY_MUTEX_KEY, async () => {
44294
+ const base2 = getState();
44119
44295
  const result = await pushUserMemory({
44120
44296
  workspaceSlug,
44121
44297
  baseUrl,
44122
44298
  pat,
44123
- state: getState(),
44299
+ state: base2,
44124
44300
  fetchImpl,
44125
44301
  now,
44126
44302
  output,
44127
44303
  suppressor
44128
44304
  });
44129
- setState(result.state);
44305
+ applyState(base2, result.state);
44130
44306
  reportUserMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
44131
44307
  });
44132
44308
  } catch (err) {
@@ -44154,17 +44330,18 @@ function makeWatchHandler(deps) {
44154
44330
  }
44155
44331
  try {
44156
44332
  await mutex.run(VOICE_MUTEX_KEY, async () => {
44333
+ const base2 = getState();
44157
44334
  const result = await pushVoice({
44158
44335
  workspaceSlug,
44159
44336
  baseUrl,
44160
44337
  pat,
44161
- state: getState(),
44338
+ state: base2,
44162
44339
  fetchImpl,
44163
44340
  now,
44164
44341
  output,
44165
44342
  suppressor
44166
44343
  });
44167
- setState(result.state);
44344
+ applyState(base2, result.state);
44168
44345
  reportVoiceOutcome(spinner, workspaceSlug, result.state, result.outcome);
44169
44346
  });
44170
44347
  } catch (err) {
@@ -44191,19 +44368,20 @@ function makeWatchHandler(deps) {
44191
44368
  }
44192
44369
  }
44193
44370
  try {
44371
+ const base2 = getState();
44194
44372
  const result = await pushArtistMemory({
44195
44373
  workspaceSlug,
44196
44374
  baseUrl,
44197
44375
  pat,
44198
44376
  slug,
44199
- state: getState(),
44377
+ state: base2,
44200
44378
  fetchImpl,
44201
44379
  now,
44202
44380
  output,
44203
44381
  suppressor,
44204
44382
  mutex
44205
44383
  });
44206
- setState(result.state);
44384
+ applyState(base2, result.state);
44207
44385
  reportArtistMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
44208
44386
  } catch (err) {
44209
44387
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -44236,6 +44414,7 @@ function makeWatchHandler(deps) {
44236
44414
  await handle({ kind: "upsert", absolutePath: event.toPath });
44237
44415
  return;
44238
44416
  }
44417
+ const base3 = getState();
44239
44418
  const result2 = await pushRename({
44240
44419
  workspaceSlug,
44241
44420
  baseUrl,
@@ -44246,7 +44425,7 @@ function makeWatchHandler(deps) {
44246
44425
  id: event.id,
44247
44426
  expectedUpdatedAt: event.expectedUpdatedAt,
44248
44427
  fromSlug: event.fromSlug,
44249
- state: getState(),
44428
+ state: base3,
44250
44429
  deps: {
44251
44430
  fetchImpl,
44252
44431
  now,
@@ -44255,7 +44434,7 @@ function makeWatchHandler(deps) {
44255
44434
  echo: suppressor ? { watcher: "live", suppressor } : undefined
44256
44435
  }
44257
44436
  });
44258
- setState(result2.state);
44437
+ applyState(base3, result2.state);
44259
44438
  reportOutcome(spinner, workspaceSlug, result2.state, result2.outcome);
44260
44439
  return;
44261
44440
  }
@@ -44314,8 +44493,8 @@ function makeWatchHandler(deps) {
44314
44493
  if (!classified) {
44315
44494
  const key = entitySymlinkRefusalKey({ workspaceSlug, absolutePath });
44316
44495
  if (key) {
44317
- setState(recordEntitySymlinkRefused({
44318
- state: getState(),
44496
+ applyFromCurrent((base3) => recordEntitySymlinkRefused({
44497
+ state: base3,
44319
44498
  mirrorRelativePath: key,
44320
44499
  op: event.kind === "unlink" ? "delete" : "upsert",
44321
44500
  now: now ?? (() => new Date)
@@ -44372,13 +44551,14 @@ function makeWatchHandler(deps) {
44372
44551
  return;
44373
44552
  }
44374
44553
  }
44554
+ const base2 = getState();
44375
44555
  const result = await pushFile({
44376
44556
  workspaceSlug,
44377
44557
  baseUrl,
44378
44558
  pat,
44379
44559
  absolutePath,
44380
44560
  entityType: classified.entityType,
44381
- state: getState(),
44561
+ state: base2,
44382
44562
  exists: event.kind === "upsert",
44383
44563
  unlinkContext: event.kind === "unlink" && event.stateEntry ? {
44384
44564
  id: event.stateEntry.id,
@@ -44394,7 +44574,7 @@ function makeWatchHandler(deps) {
44394
44574
  echo: suppressor ? { watcher: "live", suppressor } : undefined
44395
44575
  }
44396
44576
  });
44397
- setState(clearEntityRejected(result.state, workspaceSlug, absolutePath));
44577
+ applyState(result.base ?? base2, clearEntityRejected(result.state, workspaceSlug, absolutePath));
44398
44578
  reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
44399
44579
  } catch (err) {
44400
44580
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -44410,8 +44590,8 @@ function makeWatchHandler(deps) {
44410
44590
  }));
44411
44591
  }
44412
44592
  if (classified && event.kind !== "renamed" && isRecordableFailure(err)) {
44413
- setState(recordEntityUnsynced({
44414
- state: getState(),
44593
+ applyFromCurrent((base2) => recordEntityUnsynced({
44594
+ state: base2,
44415
44595
  workspaceSlug,
44416
44596
  absolutePath: path5,
44417
44597
  op: event.kind === "unlink" ? "delete" : "upsert",
@@ -44419,8 +44599,8 @@ function makeWatchHandler(deps) {
44419
44599
  now: now ?? (() => new Date)
44420
44600
  }));
44421
44601
  } else if (classified && event.kind !== "renamed" && isPermanentReject(err)) {
44422
- setState(recordEntityRejected({
44423
- state: getState(),
44602
+ applyFromCurrent((base2) => recordEntityRejected({
44603
+ state: base2,
44424
44604
  workspaceSlug,
44425
44605
  absolutePath: path5,
44426
44606
  op: event.kind === "unlink" ? "delete" : "upsert",
@@ -44439,6 +44619,7 @@ function discardSink() {
44439
44619
  }
44440
44620
  async function flushHeldEntityOp(deps) {
44441
44621
  const now = deps.now ?? (() => new Date);
44622
+ const base2 = deps.getState();
44442
44623
  try {
44443
44624
  const result = await pushFile({
44444
44625
  workspaceSlug: deps.workspaceSlug,
@@ -44446,7 +44627,7 @@ async function flushHeldEntityOp(deps) {
44446
44627
  pat: deps.pat,
44447
44628
  absolutePath: deps.op.absolutePath,
44448
44629
  entityType: deps.op.entityType,
44449
- state: deps.getState(),
44630
+ state: base2,
44450
44631
  exists: deps.op.kind === "overwrite",
44451
44632
  unlinkContext: deps.op.kind === "delete" ? { id: deps.op.id, slug: deps.op.slug, updated_at: deps.op.updatedAt } : null,
44452
44633
  deps: {
@@ -44457,7 +44638,10 @@ async function flushHeldEntityOp(deps) {
44457
44638
  echo: deps.echo
44458
44639
  }
44459
44640
  });
44460
- deps.setState(result.state);
44641
+ deps.applyState(result.base ?? base2, result.state);
44642
+ if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
44643
+ deps.spinner.warn(deleteDeclinedSentence(deps.op.absolutePath));
44644
+ }
44461
44645
  reportOutcome(deps.spinner, deps.workspaceSlug, deps.getState(), result.outcome);
44462
44646
  } catch (err) {
44463
44647
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -44469,8 +44653,8 @@ async function flushHeldEntityOp(deps) {
44469
44653
  }));
44470
44654
  deps.spinner.warn(`Push failed for ${deps.op.entityType}/${deps.op.slug}: ${msg}`);
44471
44655
  if (isRecordableFailure(err)) {
44472
- deps.setState(recordEntityUnsynced({
44473
- state: deps.getState(),
44656
+ deps.applyState(base2, recordEntityUnsynced({
44657
+ state: base2,
44474
44658
  workspaceSlug: deps.workspaceSlug,
44475
44659
  absolutePath: deps.op.absolutePath,
44476
44660
  op: deps.op.kind === "delete" ? "delete" : "upsert",
@@ -44488,14 +44672,16 @@ async function flushHeldFileOpImpl(deps) {
44488
44672
  output: deps.output,
44489
44673
  suppressor: deps.suppressor
44490
44674
  };
44675
+ let base2;
44491
44676
  const run = async () => {
44677
+ base2 = deps.getState();
44492
44678
  try {
44493
44679
  const result = deps.op.kind === "delete" ? await pushFilesDelete({
44494
44680
  workspaceSlug: deps.workspaceSlug,
44495
44681
  baseUrl: deps.baseUrl,
44496
44682
  pat: deps.pat,
44497
44683
  relPath: deps.op.relPath,
44498
- state: deps.getState(),
44684
+ state: base2,
44499
44685
  deps: fileDeps
44500
44686
  }) : await pushFilesUpsert({
44501
44687
  workspaceSlug: deps.workspaceSlug,
@@ -44503,18 +44689,18 @@ async function flushHeldFileOpImpl(deps) {
44503
44689
  pat: deps.pat,
44504
44690
  absolutePath: deps.op.absolutePath,
44505
44691
  relPath: deps.op.relPath,
44506
- state: deps.getState(),
44692
+ state: base2,
44507
44693
  deps: fileDeps
44508
44694
  });
44509
- deps.setState(result.state);
44695
+ deps.applyState(base2, result.state);
44510
44696
  reportFileOutcome(deps.spinner, deps.workspaceSlug, deps.getState(), result.outcome);
44511
44697
  } catch (err) {
44512
44698
  const msg = err instanceof Error ? err.message : "unknown push error";
44513
44699
  deps.output?.log(fileFailedLine({ path: deps.op.relPath, ...categorizeError(err) }));
44514
44700
  deps.spinner.warn(`File push failed for files/${deps.op.relPath}: ${msg}`);
44515
44701
  if (isRecordableFailure(err)) {
44516
- deps.setState(recordFileUnsynced({
44517
- state: deps.getState(),
44702
+ deps.applyState(base2, recordFileUnsynced({
44703
+ state: base2,
44518
44704
  relPath: deps.op.relPath,
44519
44705
  op: deps.op.kind === "delete" ? "delete" : "upsert",
44520
44706
  err,
@@ -44809,8 +44995,8 @@ async function runStart(workspaceSlug, deps = {}) {
44809
44995
  baseUrl: entry2.api_base_url,
44810
44996
  pat,
44811
44997
  getState: () => state,
44812
- setState: (s) => {
44813
- state = s;
44998
+ applyState: (base2, next) => {
44999
+ state = rebaseState(state, base2, next);
44814
45000
  },
44815
45001
  fetchImpl,
44816
45002
  now: deps.now,
@@ -44827,8 +45013,8 @@ async function runStart(workspaceSlug, deps = {}) {
44827
45013
  baseUrl: entry2.api_base_url,
44828
45014
  pat,
44829
45015
  getState: () => state,
44830
- setState: (s) => {
44831
- state = s;
45016
+ applyState: (base2, next) => {
45017
+ state = rebaseState(state, base2, next);
44832
45018
  },
44833
45019
  fetchImpl,
44834
45020
  now: deps.now,
@@ -44866,8 +45052,8 @@ async function runStart(workspaceSlug, deps = {}) {
44866
45052
  pat,
44867
45053
  op,
44868
45054
  getState: () => state,
44869
- setState: (s) => {
44870
- state = s;
45055
+ applyState: (base2, next) => {
45056
+ state = rebaseState(state, base2, next);
44871
45057
  },
44872
45058
  mutex,
44873
45059
  fetchImpl,
@@ -44930,8 +45116,8 @@ async function runStart(workspaceSlug, deps = {}) {
44930
45116
  pat,
44931
45117
  op,
44932
45118
  getState: () => state,
44933
- setState: (s) => {
44934
- state = s;
45119
+ applyState: (base2, next) => {
45120
+ state = rebaseState(state, base2, next);
44935
45121
  },
44936
45122
  mutex,
44937
45123
  fetchImpl,
@@ -44987,8 +45173,8 @@ async function runStart(workspaceSlug, deps = {}) {
44987
45173
  spinner,
44988
45174
  mutex,
44989
45175
  getState: () => state,
44990
- setState: (s) => {
44991
- state = s;
45176
+ applyState: (base2, next) => {
45177
+ state = rebaseState(state, base2, next);
44992
45178
  },
44993
45179
  fetchImpl,
44994
45180
  now: deps.now,
@@ -45053,8 +45239,8 @@ async function runStart(workspaceSlug, deps = {}) {
45053
45239
  baseUrl: entry2.api_base_url,
45054
45240
  pat,
45055
45241
  getState: () => state,
45056
- setState: (s) => {
45057
- state = s;
45242
+ applyState: (base2, next) => {
45243
+ state = rebaseState(state, base2, next);
45058
45244
  },
45059
45245
  fetchImpl,
45060
45246
  now: deps.now,
@@ -45071,8 +45257,8 @@ async function runStart(workspaceSlug, deps = {}) {
45071
45257
  baseUrl: entry2.api_base_url,
45072
45258
  pat,
45073
45259
  getState: () => state,
45074
- setState: (s) => {
45075
- state = s;
45260
+ applyState: (base2, next) => {
45261
+ state = rebaseState(state, base2, next);
45076
45262
  },
45077
45263
  fetchImpl,
45078
45264
  now: deps.now,
@@ -45087,58 +45273,70 @@ async function runStart(workspaceSlug, deps = {}) {
45087
45273
  spinner.warn(`${redriven.rejected} write${redriven.rejected === 1 ? "" : "s"} permanently rejected — see \`plinth status\``);
45088
45274
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
45089
45275
  }
45090
- const rulesRecon = await mutex.run(WORKSPACE_RULES_MUTEX_KEY, () => reconcileWorkspaceRules({
45091
- workspaceSlug: slug,
45092
- baseUrl: entry2.api_base_url,
45093
- pat,
45094
- state,
45095
- fetchImpl,
45096
- now: deps.now,
45097
- output: out,
45098
- suppressor
45099
- }));
45100
- if (rulesRecon.state !== state)
45101
- state = rulesRecon.state;
45276
+ let rulesBase = state;
45277
+ const rulesRecon = await mutex.run(WORKSPACE_RULES_MUTEX_KEY, () => {
45278
+ rulesBase = state;
45279
+ return reconcileWorkspaceRules({
45280
+ workspaceSlug: slug,
45281
+ baseUrl: entry2.api_base_url,
45282
+ pat,
45283
+ state: rulesBase,
45284
+ fetchImpl,
45285
+ now: deps.now,
45286
+ output: out,
45287
+ suppressor
45288
+ });
45289
+ });
45290
+ state = rebaseState(state, rulesBase, rulesRecon.state);
45102
45291
  reportSingletonReconcile(spinner, slug, state, WORKSPACE_RULES_FILENAME, rulesRecon.outcome);
45103
- const memoryRecon = await mutex.run(WORKSPACE_MEMORY_MUTEX_KEY, () => reconcileWorkspaceMemory({
45104
- workspaceSlug: slug,
45105
- baseUrl: entry2.api_base_url,
45106
- pat,
45107
- state,
45108
- fetchImpl,
45109
- now: deps.now,
45110
- output: out,
45111
- suppressor
45112
- }));
45113
- if (memoryRecon.state !== state)
45114
- state = memoryRecon.state;
45292
+ let memoryBase = state;
45293
+ const memoryRecon = await mutex.run(WORKSPACE_MEMORY_MUTEX_KEY, () => {
45294
+ memoryBase = state;
45295
+ return reconcileWorkspaceMemory({
45296
+ workspaceSlug: slug,
45297
+ baseUrl: entry2.api_base_url,
45298
+ pat,
45299
+ state: memoryBase,
45300
+ fetchImpl,
45301
+ now: deps.now,
45302
+ output: out,
45303
+ suppressor
45304
+ });
45305
+ });
45306
+ state = rebaseState(state, memoryBase, memoryRecon.state);
45115
45307
  reportSingletonReconcile(spinner, slug, state, WORKSPACE_MEMORY_FILENAME, memoryRecon.outcome);
45116
- const userMemoryRecon = await mutex.run(USER_MEMORY_MUTEX_KEY, () => reconcileUserMemory({
45117
- workspaceSlug: slug,
45118
- baseUrl: entry2.api_base_url,
45119
- pat,
45120
- state,
45121
- fetchImpl,
45122
- now: deps.now,
45123
- output: out,
45124
- suppressor
45125
- }));
45126
- if (userMemoryRecon.state !== state)
45127
- state = userMemoryRecon.state;
45128
- reportSingletonReconcile(spinner, slug, state, USER_MEMORY_FILENAME, userMemoryRecon.outcome);
45129
- try {
45130
- const voiceRecon = await mutex.run(VOICE_MUTEX_KEY, () => reconcileVoice({
45308
+ let userMemoryBase = state;
45309
+ const userMemoryRecon = await mutex.run(USER_MEMORY_MUTEX_KEY, () => {
45310
+ userMemoryBase = state;
45311
+ return reconcileUserMemory({
45131
45312
  workspaceSlug: slug,
45132
45313
  baseUrl: entry2.api_base_url,
45133
45314
  pat,
45134
- state,
45315
+ state: userMemoryBase,
45135
45316
  fetchImpl,
45136
45317
  now: deps.now,
45137
45318
  output: out,
45138
45319
  suppressor
45139
- }));
45140
- if (voiceRecon.state !== state)
45141
- state = voiceRecon.state;
45320
+ });
45321
+ });
45322
+ state = rebaseState(state, userMemoryBase, userMemoryRecon.state);
45323
+ reportSingletonReconcile(spinner, slug, state, USER_MEMORY_FILENAME, userMemoryRecon.outcome);
45324
+ try {
45325
+ let voiceBase = state;
45326
+ const voiceRecon = await mutex.run(VOICE_MUTEX_KEY, () => {
45327
+ voiceBase = state;
45328
+ return reconcileVoice({
45329
+ workspaceSlug: slug,
45330
+ baseUrl: entry2.api_base_url,
45331
+ pat,
45332
+ state: voiceBase,
45333
+ fetchImpl,
45334
+ now: deps.now,
45335
+ output: out,
45336
+ suppressor
45337
+ });
45338
+ });
45339
+ state = rebaseState(state, voiceBase, voiceRecon.state);
45142
45340
  reportSingletonReconcile(spinner, slug, state, VOICE_FILENAME, voiceRecon.outcome);
45143
45341
  const reconWarning = voiceMalformedWarning(state, slug);
45144
45342
  if (reconWarning)
@@ -45146,19 +45344,19 @@ async function runStart(workspaceSlug, deps = {}) {
45146
45344
  } catch (err) {
45147
45345
  out.log(`[VOICE_RECONCILE_FAILED ${categorizeError(err).reason}]`);
45148
45346
  }
45347
+ const memBase = state;
45149
45348
  const memRecon = await reconcileArtistMemory({
45150
45349
  workspaceSlug: slug,
45151
45350
  baseUrl: entry2.api_base_url,
45152
45351
  pat,
45153
- state,
45352
+ state: memBase,
45154
45353
  fetchImpl,
45155
45354
  now: deps.now,
45156
45355
  output: out,
45157
45356
  suppressor,
45158
45357
  mutex
45159
45358
  });
45160
- if (memRecon.state !== state)
45161
- state = memRecon.state;
45359
+ state = rebaseState(state, memBase, memRecon.state);
45162
45360
  if (memRecon.pushed > 0) {
45163
45361
  spinner.success(`↑ synced ${memRecon.pushed} memory file${memRecon.pushed === 1 ? "" : "s"}`);
45164
45362
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);