@farmslot/protocol 0.76.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/client-cache/cache.d.ts.map +1 -1
  3. package/dist/client-cache/cache.js +174 -14
  4. package/dist/client-cache/cache.js.map +1 -1
  5. package/dist/client-cache/command-method.d.ts +1 -1
  6. package/dist/client-cache/command-method.d.ts.map +1 -1
  7. package/dist/client-cache/command-method.js +2 -0
  8. package/dist/client-cache/command-method.js.map +1 -1
  9. package/dist/client-cache/index.d.ts +1 -1
  10. package/dist/client-cache/index.d.ts.map +1 -1
  11. package/dist/client-cache/index.js +1 -1
  12. package/dist/client-cache/index.js.map +1 -1
  13. package/dist/client-cache/prefetch.d.ts +13 -1
  14. package/dist/client-cache/prefetch.d.ts.map +1 -1
  15. package/dist/client-cache/prefetch.js +88 -26
  16. package/dist/client-cache/prefetch.js.map +1 -1
  17. package/dist/client-cache/read-stored.d.ts +5 -1
  18. package/dist/client-cache/read-stored.d.ts.map +1 -1
  19. package/dist/client-cache/read-stored.js +64 -1
  20. package/dist/client-cache/read-stored.js.map +1 -1
  21. package/dist/client-cache/registry.d.ts +29 -1
  22. package/dist/client-cache/registry.d.ts.map +1 -1
  23. package/dist/client-cache/registry.js +55 -4
  24. package/dist/client-cache/registry.js.map +1 -1
  25. package/dist/client-cache/store.d.ts +2 -0
  26. package/dist/client-cache/store.d.ts.map +1 -1
  27. package/dist/client-cache/store.js +9 -1
  28. package/dist/client-cache/store.js.map +1 -1
  29. package/dist/client-cache/transport.d.ts +3 -1
  30. package/dist/client-cache/transport.d.ts.map +1 -1
  31. package/dist/client-cache/transport.js +2 -1
  32. package/dist/client-cache/transport.js.map +1 -1
  33. package/dist/client-cache/types.d.ts +8 -1
  34. package/dist/client-cache/types.d.ts.map +1 -1
  35. package/dist/contracts/agents.d.ts +5 -2
  36. package/dist/contracts/agents.d.ts.map +1 -1
  37. package/dist/contracts/agents.js.map +1 -1
  38. package/dist/contracts/config.d.ts +3 -0
  39. package/dist/contracts/config.d.ts.map +1 -1
  40. package/dist/contracts/config.js +5 -1
  41. package/dist/contracts/config.js.map +1 -1
  42. package/dist/contracts/family.d.ts +55 -3
  43. package/dist/contracts/family.d.ts.map +1 -1
  44. package/dist/contracts/family.js.map +1 -1
  45. package/dist/contracts/review-workspace.d.ts +23 -1
  46. package/dist/contracts/review-workspace.d.ts.map +1 -1
  47. package/dist/contracts/review-workspace.js +3 -0
  48. package/dist/contracts/review-workspace.js.map +1 -1
  49. package/dist/contracts/reviews.d.ts +1 -0
  50. package/dist/contracts/reviews.d.ts.map +1 -1
  51. package/dist/contracts/reviews.js.map +1 -1
  52. package/dist/contracts/runs.d.ts +45 -5
  53. package/dist/contracts/runs.d.ts.map +1 -1
  54. package/dist/contracts/runs.js.map +1 -1
  55. package/dist/contracts/runtime-capabilities.d.ts +9 -0
  56. package/dist/contracts/runtime-capabilities.d.ts.map +1 -1
  57. package/dist/contracts/runtime-capabilities.js.map +1 -1
  58. package/dist/contracts/slot-recovery.d.ts +2 -2
  59. package/dist/contracts/slot-recovery.d.ts.map +1 -1
  60. package/dist/contracts/slot-recovery.js +5 -3
  61. package/dist/contracts/slot-recovery.js.map +1 -1
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +3 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/performance/client.d.ts +18 -0
  67. package/dist/performance/client.d.ts.map +1 -0
  68. package/dist/performance/client.js +66 -0
  69. package/dist/performance/client.js.map +1 -0
  70. package/dist/performance/histogram.d.ts +5 -0
  71. package/dist/performance/histogram.d.ts.map +1 -0
  72. package/dist/performance/histogram.js +38 -0
  73. package/dist/performance/histogram.js.map +1 -0
  74. package/dist/performance/index.d.ts +10 -0
  75. package/dist/performance/index.d.ts.map +1 -0
  76. package/dist/performance/index.js +10 -0
  77. package/dist/performance/index.js.map +1 -0
  78. package/dist/performance/paint.d.ts +3 -0
  79. package/dist/performance/paint.d.ts.map +1 -0
  80. package/dist/performance/paint.js +14 -0
  81. package/dist/performance/paint.js.map +1 -0
  82. package/dist/performance/prometheus.d.ts +3 -0
  83. package/dist/performance/prometheus.d.ts.map +1 -0
  84. package/dist/performance/prometheus.js +53 -0
  85. package/dist/performance/prometheus.js.map +1 -0
  86. package/dist/performance/ranking.d.ts +26 -0
  87. package/dist/performance/ranking.d.ts.map +1 -0
  88. package/dist/performance/ranking.js +92 -0
  89. package/dist/performance/ranking.js.map +1 -0
  90. package/dist/performance/recorder.d.ts +43 -0
  91. package/dist/performance/recorder.d.ts.map +1 -0
  92. package/dist/performance/recorder.js +352 -0
  93. package/dist/performance/recorder.js.map +1 -0
  94. package/dist/performance/types.d.ts +93 -0
  95. package/dist/performance/types.d.ts.map +1 -0
  96. package/dist/performance/types.js +40 -0
  97. package/dist/performance/types.js.map +1 -0
  98. package/dist/performance/validation.d.ts +3 -0
  99. package/dist/performance/validation.d.ts.map +1 -0
  100. package/dist/performance/validation.js +55 -0
  101. package/dist/performance/validation.js.map +1 -0
  102. package/dist/recipes/step-io.d.ts +2 -0
  103. package/dist/recipes/step-io.d.ts.map +1 -1
  104. package/dist/recipes/step-io.js.map +1 -1
  105. package/dist/rpc/auth.d.ts +1 -0
  106. package/dist/rpc/auth.d.ts.map +1 -1
  107. package/dist/rpc/auth.js.map +1 -1
  108. package/dist/rpc/fs.d.ts +2 -0
  109. package/dist/rpc/fs.d.ts.map +1 -1
  110. package/dist/rpc/fs.js +2 -0
  111. package/dist/rpc/fs.js.map +1 -1
  112. package/dist/rpc/index.d.ts +1 -0
  113. package/dist/rpc/index.d.ts.map +1 -1
  114. package/dist/rpc/index.js +1 -0
  115. package/dist/rpc/index.js.map +1 -1
  116. package/dist/rpc/performance.d.ts +6 -0
  117. package/dist/rpc/performance.d.ts.map +1 -0
  118. package/dist/rpc/performance.js +2 -0
  119. package/dist/rpc/performance.js.map +1 -0
  120. package/dist/rpc/registry.d.ts +2 -0
  121. package/dist/rpc/registry.d.ts.map +1 -1
  122. package/dist/rpc/registry.js +2 -0
  123. package/dist/rpc/registry.js.map +1 -1
  124. package/dist/rpc/run.d.ts +4 -0
  125. package/dist/rpc/run.d.ts.map +1 -1
  126. package/dist/rpc/runtime-capabilities.d.ts +6 -0
  127. package/dist/rpc/runtime-capabilities.d.ts.map +1 -1
  128. package/dist/rpc/runtime-capabilities.js.map +1 -1
  129. package/dist/rpc/slot.d.ts +2 -0
  130. package/dist/rpc/slot.d.ts.map +1 -1
  131. package/dist/rpc/tmux.d.ts +1 -1
  132. package/dist/rpc/tmux.d.ts.map +1 -1
  133. package/dist/runs/list-trim.d.ts.map +1 -1
  134. package/dist/runs/list-trim.js +1 -19
  135. package/dist/runs/list-trim.js.map +1 -1
  136. package/dist/surfaces/run-view-state.d.ts +22 -0
  137. package/dist/surfaces/run-view-state.d.ts.map +1 -0
  138. package/dist/surfaces/run-view-state.js +26 -0
  139. package/dist/surfaces/run-view-state.js.map +1 -0
  140. package/dist/surfaces/view-links.d.ts +1 -1
  141. package/dist/surfaces/view-links.d.ts.map +1 -1
  142. package/dist/surfaces/view-links.js +144 -16
  143. package/dist/surfaces/view-links.js.map +1 -1
  144. package/dist/surfaces/view-state.d.ts +50 -0
  145. package/dist/surfaces/view-state.d.ts.map +1 -0
  146. package/dist/surfaces/view-state.js +181 -0
  147. package/dist/surfaces/view-state.js.map +1 -0
  148. package/dist/transport/gateway-recovery.d.ts +39 -0
  149. package/dist/transport/gateway-recovery.d.ts.map +1 -0
  150. package/dist/transport/gateway-recovery.js +134 -0
  151. package/dist/transport/gateway-recovery.js.map +1 -0
  152. package/dist/utf8-byte-length.d.ts +2 -0
  153. package/dist/utf8-byte-length.d.ts.map +1 -0
  154. package/dist/utf8-byte-length.js +22 -0
  155. package/dist/utf8-byte-length.js.map +1 -0
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/package.json +6 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ All notable changes to `@farmslot/protocol` are tracked here.
6
6
 
7
7
  - Active-development baseline; add user-facing changes here before release or package publication.
8
8
 
9
+ ## 0.77.0 - 2026-11-06
10
+
11
+ - Add a family usage projection for shared session receipts, accounting contributions and review-round timing.
12
+ - Measure shared RPC and offline cache performance, with authenticated diagnostics and action budgets.
13
+ - Share context-configured display state across clients and expose bounded resident content previews for cached first paint.
14
+ - Preserve structured missing-file errors through display reads.
15
+ - Restore the Review History choice from shared links and retain it across native tab navigation.
16
+ - Give current run reads a bounded two-minute window for publication-source and retained-evidence verification, so a slow read can refresh a ready gate.
17
+ - Add shared pipeline session usage receipts with runner, model, effort, role, flow, identity, token splits, pricing status and duration, retained across relaunches and replays.
18
+ - `run.extendReviewBudget` takes an optional `slotId`: the slot a failed run whose slot was released re-binds at the approved head.
19
+ - Clarify that farm-wide worker discovery skips disconnected machines and pools with no enabled slots.
20
+ - Retain watched check failure conclusions and CI fix scope refusals for operator decisions.
21
+ - Allow the four scripted recovery behaviours in scenario configuration.
22
+ - Declare bounded shared runtime claims and project recording capability metadata.
23
+ - Record when a pending ticket refresh is superseded so its history survives a fresh admission.
24
+ - Add opt-in ticket scope refresh and its person action history to publication package refresh.
25
+ - Retain slotless review workspaces and exact reviewer conversations for a configurable project window, default seven days, so re-reviews resume the prior session on only the new diff. Expired storage is reclaimed safely and expiry is reported before a cold review.
26
+ - Runs record `requestedSlotId`, the slot a caller or operator explicitly asked for, separately from the slot the engine placed them on.
27
+ - Share bounded reconnect backoff, liveness probes and lifecycle/network recovery across Command Center and Companion.
28
+ - Resume interrupted recovery without an identity from the client. Keep live owners and local work protected, and park unpublished commits retained by a terminal owner.
29
+ - Open held review source without rereading it and batch missing source reads during prefetch.
30
+
9
31
  ## 0.76.0 - 2026-11-04
10
32
 
11
33
  - Define admin-person standalone session requester attribution, execution-target availability and real home metadata.
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/client-cache/cache.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EACV,oBAAoB,EACpB,2BAA2B,EAM3B,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAuDpB;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAE1E;AA8CD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAu3CtB"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/client-cache/cache.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EACV,oBAAoB,EACpB,2BAA2B,EAM3B,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAwDpB;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAE1E;AA8CD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAoiDtB"}
@@ -1,3 +1,4 @@
1
+ import { afterForegroundPaint } from '../performance/paint.js';
1
2
  import { Methods } from '../rpc/registry.js';
2
3
  import { runListVersion } from '../runs/list-version.js';
3
4
  import { ResourceCollectorAdmission } from './collector-admission.js';
@@ -64,6 +65,36 @@ function profileId(connection) {
64
65
  }
65
66
  export function createGatewayResourceCache(options) {
66
67
  const transport = options.transport;
68
+ const metrics = options.performance ?? transport.performance;
69
+ const readContexts = new WeakMap();
70
+ const collectionIdentities = new Map();
71
+ function request(method, params, signal) {
72
+ const context = readContexts.get(signal);
73
+ let identity = context?.identity ?? collectionIdentities.get(method);
74
+ if (!identity) {
75
+ identity = {};
76
+ collectionIdentities.set(method, identity);
77
+ }
78
+ metrics?.reread(method, identity);
79
+ return transport.request(method, params, signal, context?.blocking ? 'interactive-read' : 'background-refresh');
80
+ }
81
+ function recordRead(entry, source, blocking) {
82
+ if (!metrics)
83
+ return;
84
+ const labels = { method: entry.query.method, cache_source: source };
85
+ metrics.increment('farmslot_client_cache_reads_total', labels);
86
+ if (entry.state.status === 'stale')
87
+ metrics.increment('farmslot_client_stale_reads_total', labels);
88
+ if (blocking)
89
+ metrics.increment('farmslot_client_blocking_reads_total', labels);
90
+ const age = entry.state.observedAt === undefined
91
+ ? undefined
92
+ : (Date.now() - entry.state.observedAt) / 1000;
93
+ if (age !== undefined && Number.isFinite(age) && age >= 0)
94
+ metrics.observe('farmslot_client_cache_age_seconds', labels, age);
95
+ else
96
+ metrics.increment('farmslot_client_unknown_age_total', labels);
97
+ }
67
98
  let store = options.store ?? createMemoryResourceStore();
68
99
  let metadata;
69
100
  let startupStore;
@@ -75,6 +106,35 @@ export function createGatewayResourceCache(options) {
75
106
  let version = 0;
76
107
  let storeVersion = 0;
77
108
  let hydrationOwnerVersion = 0;
109
+ // A bounded working set of the store's aliased bytes, for synchronous cached paint.
110
+ // Eviction and authority changes invalidate it with the durable rows.
111
+ const residentContent = new Map();
112
+ const residentLimit = Math.min(options.contentBudgetBytes ?? defaultContentBudgetBytes, 8 * 1024 * 1024);
113
+ let residentVersion = 0;
114
+ function forgetResident(key) {
115
+ residentVersion++;
116
+ for (const [alias, value] of residentContent)
117
+ if (value.key.kind === key.kind && value.key.id === key.id)
118
+ residentContent.delete(alias);
119
+ }
120
+ async function removeStoredContent(target, key) {
121
+ forgetResident(key);
122
+ await target.deleteContent(key);
123
+ forgetResident(key);
124
+ }
125
+ function rememberResident(alias, key, data) {
126
+ residentContent.delete(alias);
127
+ if (data.byteLength > residentLimit)
128
+ return;
129
+ residentContent.set(alias, { key, data: data.slice() });
130
+ let bytes = [...residentContent.values()].reduce((sum, value) => sum + value.data.byteLength, 0);
131
+ for (const [old, value] of residentContent) {
132
+ if (bytes <= residentLimit)
133
+ break;
134
+ residentContent.delete(old);
135
+ bytes -= value.data.byteLength;
136
+ }
137
+ }
78
138
  let lastUsed = 0;
79
139
  let storageNotice;
80
140
  let needsStore = options.store === undefined && options.openStore !== undefined;
@@ -89,7 +149,7 @@ export function createGatewayResourceCache(options) {
89
149
  let unsubscribeStore = store.onDeleted(storeDeleted);
90
150
  // Version lists: one shared run collection answers every run.list view.
91
151
  const runList = new RunListCollection({
92
- request: (params, signal) => transport.request(Methods.RUN_LIST, params, signal),
152
+ request: (params, signal) => request(Methods.RUN_LIST, params, signal),
93
153
  persist: (merge) => {
94
154
  const writingStore = store;
95
155
  const incarnation = storeVersion;
@@ -142,6 +202,7 @@ export function createGatewayResourceCache(options) {
142
202
  return;
143
203
  usable = false;
144
204
  updateCollectors();
205
+ residentContent.clear();
145
206
  hydrationOwnerVersion++;
146
207
  version++;
147
208
  forgetRunList();
@@ -151,6 +212,7 @@ export function createGatewayResourceCache(options) {
151
212
  }
152
213
  function replaceStore(next) {
153
214
  unsubscribeStore();
215
+ residentContent.clear();
154
216
  store = next;
155
217
  storeVersion++;
156
218
  startupStore = undefined;
@@ -328,6 +390,27 @@ export function createGatewayResourceCache(options) {
328
390
  if (saved.id !== RUN_LIST_COLLECTION_ID)
329
391
  startupQueries.set(saved.id, saved);
330
392
  }
393
+ // Restore small aliased report bodies before exposing their cached run. Network
394
+ // prefetch can be queued behind other gates, even when these bytes are already held.
395
+ let contentRemaining = residentLimit;
396
+ const content = (await readingStore.listContent())
397
+ .filter((entry) => entry.kind === 'artifact' && entry.aliases?.length)
398
+ .sort((a, b) => b.accessedAt - a.accessedAt);
399
+ if (!current())
400
+ return;
401
+ for (const info of content) {
402
+ const aliasCount = info.aliases?.length ?? 0;
403
+ if (info.bytes * aliasCount > contentRemaining)
404
+ continue;
405
+ const saved = await readingStore.getContent(info);
406
+ if (!current())
407
+ return;
408
+ if (!saved || saved.data.byteLength * aliasCount > contentRemaining)
409
+ continue;
410
+ contentRemaining -= saved.data.byteLength * aliasCount;
411
+ for (const name of info.aliases ?? [])
412
+ rememberResident(name, saved, saved.data);
413
+ }
331
414
  if (rows)
332
415
  runList.hydrate(rows);
333
416
  startupStore = readingStore;
@@ -378,6 +461,8 @@ export function createGatewayResourceCache(options) {
378
461
  // Stored facts belong to an owner, not a socket attempt. Fence a revoked
379
462
  // owner's pending I/O synchronously, before queued deletion can run.
380
463
  if (revoke || scopeChanged || authorityChanged) {
464
+ metrics?.resetScope();
465
+ residentContent.clear();
381
466
  hydrationOwnerVersion++;
382
467
  forgetRunList();
383
468
  }
@@ -421,6 +506,7 @@ export function createGatewayResourceCache(options) {
421
506
  switchingProfile ||
422
507
  ((next.connected || next.authority !== undefined) &&
423
508
  (storedScopeChanged || storedAuthorityChanged))) {
509
+ residentContent.clear();
424
510
  storeVersion++;
425
511
  forgetRunList();
426
512
  for (const entry of entries.values())
@@ -574,7 +660,7 @@ export function createGatewayResourceCache(options) {
574
660
  };
575
661
  entries.set(query.id, entry);
576
662
  const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
577
- if (applyStoredQuery(entry, saved)) {
663
+ if (applyStoredQuery(entry, saved, 'memory')) {
578
664
  entry.hydrated = true;
579
665
  queueMicrotask(prune);
580
666
  }
@@ -590,7 +676,7 @@ export function createGatewayResourceCache(options) {
590
676
  startupQueries.delete(id);
591
677
  return saved;
592
678
  }
593
- function applyStoredQuery(entry, saved) {
679
+ function applyStoredQuery(entry, saved, origin = 'stored') {
594
680
  const query = entry.query;
595
681
  if (!saved ||
596
682
  !query.persisted ||
@@ -598,6 +684,7 @@ export function createGatewayResourceCache(options) {
598
684
  (query.method === Methods.NATIVE_SESSION_READ &&
599
685
  !storedNativeHistoryMatches(query.params, saved.data)))
600
686
  return false;
687
+ entry.metricSource = origin;
601
688
  const sourceObservation = query.sourceObservation?.(saved.data);
602
689
  entry.state = {
603
690
  status: 'stale',
@@ -708,7 +795,7 @@ export function createGatewayResourceCache(options) {
708
795
  observedAt: runList.observedAt,
709
796
  };
710
797
  // A view whose summaries the version reply cannot answer keeps its own full read.
711
- return transport.request(Methods.RUN_LIST, params, signal);
798
+ return request(Methods.RUN_LIST, params, signal);
712
799
  }
713
800
  function stampCovers(entry, stamp) {
714
801
  if (!stamp ||
@@ -803,6 +890,10 @@ export function createGatewayResourceCache(options) {
803
890
  prefetch,
804
891
  };
805
892
  entry.pending = pending;
893
+ const refreshStarted = metrics?.clock();
894
+ const refreshEpoch = metrics?.epoch;
895
+ const blocking = entry.state.data === undefined;
896
+ readContexts.set(pending.controller.signal, { identity: entry, blocking });
806
897
  entry.dirty = false;
807
898
  entry.rereadFloor = undefined;
808
899
  const unknownGeneration = entry.unknownGeneration;
@@ -829,8 +920,8 @@ export function createGatewayResourceCache(options) {
829
920
  const reply = fromRunList
830
921
  ? await readRunListView(entry, pending.controller.signal)
831
922
  : entry.query.method === Methods.NATIVE_SESSION_READ
832
- ? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) => transport.request(Methods.NATIVE_SESSION_READ, params, signal), pending.controller.signal)
833
- : await transport.request(entry.query.method, entry.query.params, pending.controller.signal);
923
+ ? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) => request(Methods.NATIVE_SESSION_READ, params, signal), pending.controller.signal)
924
+ : await request(entry.query.method, entry.query.params, pending.controller.signal);
834
925
  if (disposed ||
835
926
  store.deleted ||
836
927
  pending.version !== version ||
@@ -859,6 +950,7 @@ export function createGatewayResourceCache(options) {
859
950
  throw Object.assign(new Error(sourceObservation.error?.message ?? 'The gateway has not observed an upstream value.'), {
860
951
  code: sourceObservation.error?.code ?? 'UPSTREAM_UNOBSERVED',
861
952
  });
953
+ entry.metricSource = 'network';
862
954
  entry.state = {
863
955
  status: covered
864
956
  ? sourceObservation && sourceObservation.status !== 'ready'
@@ -954,7 +1046,7 @@ export function createGatewayResourceCache(options) {
954
1046
  return;
955
1047
  }
956
1048
  for (const key of victims.content) {
957
- await writingStore.deleteContent(key);
1049
+ await removeStoredContent(writingStore, key);
958
1050
  if (!current())
959
1051
  return;
960
1052
  }
@@ -1055,6 +1147,13 @@ export function createGatewayResourceCache(options) {
1055
1147
  notify(entry);
1056
1148
  }
1057
1149
  finally {
1150
+ if (metrics && refreshStarted !== undefined && metrics.epoch === refreshEpoch)
1151
+ metrics.observe(blocking
1152
+ ? 'farmslot_client_blocking_refresh_seconds'
1153
+ : 'farmslot_client_background_refresh_seconds', {
1154
+ method: entry.query.method,
1155
+ action_class: blocking ? 'interactive-read' : 'background-refresh',
1156
+ }, metrics.clock() - refreshStarted);
1058
1157
  if (entry.pending === pending) {
1059
1158
  entry.pending = undefined;
1060
1159
  if (entry.dirty && entry.listeners.size && usable)
@@ -1103,6 +1202,25 @@ export function createGatewayResourceCache(options) {
1103
1202
  : undefined);
1104
1203
  return saved?.data;
1105
1204
  }
1205
+ async function readAliasedContent(alias, open) {
1206
+ const version = residentVersion;
1207
+ const owner = hydrationOwnerVersion;
1208
+ await barrier;
1209
+ if (owner !== hydrationOwnerVersion || disposed)
1210
+ return undefined;
1211
+ const incarnation = storeVersion;
1212
+ const saved = await readStoredContent((target) => target.getContentByAlias(alias), open
1213
+ ? (target, { kind, id }) => target.touchContent({ kind, id }, Date.now(), 'opened')
1214
+ : undefined);
1215
+ if (version !== residentVersion ||
1216
+ owner !== hydrationOwnerVersion ||
1217
+ incarnation !== storeVersion ||
1218
+ disposed)
1219
+ return undefined;
1220
+ if (saved)
1221
+ rememberResident(alias, saved, saved.data);
1222
+ return saved?.data;
1223
+ }
1106
1224
  /** Content writes accept only the current connection's owner, from the token's fetch onward. */
1107
1225
  async function writeContent(token, write) {
1108
1226
  if (disposed || !connection.connected)
@@ -1162,7 +1280,7 @@ export function createGatewayResourceCache(options) {
1162
1280
  return false;
1163
1281
  }
1164
1282
  for (const victim of victims.content) {
1165
- await target.deleteContent(victim);
1283
+ await removeStoredContent(target, victim);
1166
1284
  if (!current())
1167
1285
  return false;
1168
1286
  }
@@ -1175,8 +1293,11 @@ export function createGatewayResourceCache(options) {
1175
1293
  accessedAt,
1176
1294
  });
1177
1295
  }
1178
- if (input.alias !== undefined && current())
1296
+ if (input.alias !== undefined && current()) {
1179
1297
  await target.putContentAlias(input.alias, key);
1298
+ if (current())
1299
+ rememberResident(input.alias, key, data);
1300
+ }
1180
1301
  return true;
1181
1302
  });
1182
1303
  }
@@ -1210,8 +1331,38 @@ export function createGatewayResourceCache(options) {
1210
1331
  observe(query, listener) {
1211
1332
  if (disposed)
1212
1333
  throw new Error('Resource cache is disposed');
1334
+ const started = metrics?.clock();
1335
+ const epoch = metrics?.epoch;
1336
+ const alreadyHeld = entries.get(query.id)?.state.data !== undefined;
1213
1337
  const entry = entryFor(query);
1214
- const receive = (state) => listener(state);
1338
+ const blocking = entry.state.data === undefined;
1339
+ let first = true, active = true;
1340
+ let cancelPaint;
1341
+ const receive = (state) => {
1342
+ listener(state);
1343
+ if (state.data === undefined)
1344
+ return;
1345
+ const source = alreadyHeld ? 'memory' : (entry.metricSource ?? 'unknown');
1346
+ recordRead(entry, source, blocking && first);
1347
+ if (!first || !metrics || started === undefined)
1348
+ return;
1349
+ first = false;
1350
+ if (connection.foreground === false)
1351
+ return;
1352
+ cancelPaint = (options.afterPaint ?? afterForegroundPaint)(() => {
1353
+ if (!active || disposed || metrics.epoch !== epoch || connection.foreground === false)
1354
+ return;
1355
+ metrics.observe(source === 'network'
1356
+ ? 'farmslot_client_visible_read_seconds'
1357
+ : 'farmslot_client_cached_paint_seconds', {
1358
+ method: query.method,
1359
+ action_class: source === 'network' ? 'interactive-read' : 'cached-paint',
1360
+ cache_source: source,
1361
+ }, metrics.clock() - started);
1362
+ });
1363
+ if (!cancelPaint)
1364
+ metrics.increment('farmslot_client_unmeasured_paints_total', { method: query.method });
1365
+ };
1215
1366
  entry.listeners.add(receive);
1216
1367
  updateCollectors();
1217
1368
  if (query.retention?.cohort === 'workspace') {
@@ -1256,6 +1407,8 @@ export function createGatewayResourceCache(options) {
1256
1407
  if (entry.state.status !== 'ready' || entry.dirty)
1257
1408
  schedule(entry);
1258
1409
  return () => {
1410
+ active = false;
1411
+ cancelPaint?.();
1259
1412
  entry.listeners.delete(receive);
1260
1413
  updateCollectors();
1261
1414
  if (!entry.listeners.size) {
@@ -1270,7 +1423,9 @@ export function createGatewayResourceCache(options) {
1270
1423
  read(query) {
1271
1424
  if (disposed)
1272
1425
  throw new Error('Resource cache is disposed');
1273
- return entryFor(query).state;
1426
+ const entry = entryFor(query);
1427
+ recordRead(entry, 'memory', entry.state.data === undefined);
1428
+ return entry.state;
1274
1429
  },
1275
1430
  async readStored(query, refresh = true) {
1276
1431
  if (disposed)
@@ -1302,20 +1457,25 @@ export function createGatewayResourceCache(options) {
1302
1457
  runListProjections: () => runList.projections(),
1303
1458
  openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
1304
1459
  async forgetContent(key) {
1460
+ forgetResident(key);
1305
1461
  const target = store;
1306
1462
  const ownerVersion = hydrationOwnerVersion;
1307
- await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.deleteContent(contentKey(key)));
1463
+ await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => removeStoredContent(target, contentKey(key)));
1308
1464
  },
1309
- readContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), false),
1310
- openContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), true),
1465
+ peekContentByAlias: (alias) => residentContent.get(alias)?.data.slice(),
1466
+ readContentByAlias: (alias) => readAliasedContent(alias, false),
1467
+ openContentByAlias: (alias) => readAliasedContent(alias, true),
1311
1468
  storeContent,
1312
1469
  retainContent,
1313
1470
  contentWriteToken,
1314
1471
  async dispose(reason) {
1472
+ if (reason && reason !== 'network' && reason !== 'background' && reason !== 'token-refresh')
1473
+ metrics?.resetScope();
1315
1474
  if (disposed)
1316
1475
  return;
1317
1476
  const withdrawing = collectors.close();
1318
1477
  disposed = true;
1478
+ residentContent.clear();
1319
1479
  detailSync.reset();
1320
1480
  version++;
1321
1481
  unsubscribeChanges();