@farmslot/protocol 0.74.10 → 0.75.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 (140) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/client-cache/artifact-response.d.ts +15 -0
  3. package/dist/client-cache/artifact-response.d.ts.map +1 -0
  4. package/dist/client-cache/artifact-response.js +103 -0
  5. package/dist/client-cache/artifact-response.js.map +1 -0
  6. package/dist/client-cache/cache.d.ts +6 -1
  7. package/dist/client-cache/cache.d.ts.map +1 -1
  8. package/dist/client-cache/cache.js +207 -33
  9. package/dist/client-cache/cache.js.map +1 -1
  10. package/dist/client-cache/command-method.d.ts +17 -4
  11. package/dist/client-cache/command-method.d.ts.map +1 -1
  12. package/dist/client-cache/command-method.js +221 -1
  13. package/dist/client-cache/command-method.js.map +1 -1
  14. package/dist/client-cache/conformance.d.ts +2 -5
  15. package/dist/client-cache/conformance.d.ts.map +1 -1
  16. package/dist/client-cache/conformance.js +4 -0
  17. package/dist/client-cache/conformance.js.map +1 -1
  18. package/dist/client-cache/content.d.ts +11 -0
  19. package/dist/client-cache/content.d.ts.map +1 -1
  20. package/dist/client-cache/content.js +43 -0
  21. package/dist/client-cache/content.js.map +1 -1
  22. package/dist/client-cache/index.d.ts +5 -2
  23. package/dist/client-cache/index.d.ts.map +1 -1
  24. package/dist/client-cache/index.js +5 -2
  25. package/dist/client-cache/index.js.map +1 -1
  26. package/dist/client-cache/network-cost.d.ts +4 -0
  27. package/dist/client-cache/network-cost.d.ts.map +1 -0
  28. package/dist/client-cache/network-cost.js +24 -0
  29. package/dist/client-cache/network-cost.js.map +1 -0
  30. package/dist/client-cache/prefetch.d.ts +17 -0
  31. package/dist/client-cache/prefetch.d.ts.map +1 -1
  32. package/dist/client-cache/prefetch.js +110 -47
  33. package/dist/client-cache/prefetch.js.map +1 -1
  34. package/dist/client-cache/read-stored.d.ts +7 -0
  35. package/dist/client-cache/read-stored.d.ts.map +1 -0
  36. package/dist/client-cache/read-stored.js +18 -0
  37. package/dist/client-cache/read-stored.js.map +1 -0
  38. package/dist/client-cache/registry.d.ts +309 -16
  39. package/dist/client-cache/registry.d.ts.map +1 -1
  40. package/dist/client-cache/registry.js +422 -27
  41. package/dist/client-cache/registry.js.map +1 -1
  42. package/dist/client-cache/run-details.d.ts +29 -0
  43. package/dist/client-cache/run-details.d.ts.map +1 -0
  44. package/dist/client-cache/run-details.js +145 -0
  45. package/dist/client-cache/run-details.js.map +1 -0
  46. package/dist/client-cache/run-list-versions.d.ts +2 -0
  47. package/dist/client-cache/run-list-versions.d.ts.map +1 -1
  48. package/dist/client-cache/run-list-versions.js +9 -0
  49. package/dist/client-cache/run-list-versions.js.map +1 -1
  50. package/dist/client-cache/store.d.ts +6 -0
  51. package/dist/client-cache/store.d.ts.map +1 -1
  52. package/dist/client-cache/store.js +25 -0
  53. package/dist/client-cache/store.js.map +1 -1
  54. package/dist/client-cache/transport.d.ts +8 -1
  55. package/dist/client-cache/transport.d.ts.map +1 -1
  56. package/dist/client-cache/transport.js +8 -0
  57. package/dist/client-cache/transport.js.map +1 -1
  58. package/dist/client-cache/types.d.ts +11 -1
  59. package/dist/client-cache/types.d.ts.map +1 -1
  60. package/dist/contracts/agents.d.ts +2 -0
  61. package/dist/contracts/agents.d.ts.map +1 -1
  62. package/dist/contracts/agents.js.map +1 -1
  63. package/dist/contracts/config.d.ts +10 -0
  64. package/dist/contracts/config.d.ts.map +1 -1
  65. package/dist/contracts/config.js +7 -0
  66. package/dist/contracts/config.js.map +1 -1
  67. package/dist/contracts/dispatch.d.ts +18 -0
  68. package/dist/contracts/dispatch.d.ts.map +1 -1
  69. package/dist/contracts/run-lifecycle-view.d.ts +9 -0
  70. package/dist/contracts/run-lifecycle-view.d.ts.map +1 -1
  71. package/dist/contracts/run-lifecycle-view.js +8 -0
  72. package/dist/contracts/run-lifecycle-view.js.map +1 -1
  73. package/dist/contracts/runs.d.ts +76 -7
  74. package/dist/contracts/runs.d.ts.map +1 -1
  75. package/dist/contracts/runs.js +33 -2
  76. package/dist/contracts/runs.js.map +1 -1
  77. package/dist/contracts/slot-recovery.d.ts +11 -0
  78. package/dist/contracts/slot-recovery.d.ts.map +1 -1
  79. package/dist/contracts/slot-recovery.js +11 -1
  80. package/dist/contracts/slot-recovery.js.map +1 -1
  81. package/dist/contracts/slots.d.ts +17 -0
  82. package/dist/contracts/slots.d.ts.map +1 -1
  83. package/dist/index.d.ts +3 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/recipe/recording.d.ts +1 -1
  88. package/dist/recipe/recording.d.ts.map +1 -1
  89. package/dist/recipe/recording.js +2 -2
  90. package/dist/recipe/recording.js.map +1 -1
  91. package/dist/rpc/dispatch.d.ts +8 -0
  92. package/dist/rpc/dispatch.d.ts.map +1 -1
  93. package/dist/rpc/dispatch.js +8 -0
  94. package/dist/rpc/dispatch.js.map +1 -1
  95. package/dist/rpc/fleet.d.ts +3 -1
  96. package/dist/rpc/fleet.d.ts.map +1 -1
  97. package/dist/rpc/fleet.js.map +1 -1
  98. package/dist/rpc/native-session.d.ts +11 -0
  99. package/dist/rpc/native-session.d.ts.map +1 -1
  100. package/dist/rpc/native-session.js +9 -0
  101. package/dist/rpc/native-session.js.map +1 -1
  102. package/dist/rpc/run.d.ts +4 -0
  103. package/dist/rpc/run.d.ts.map +1 -1
  104. package/dist/rpc/run.js.map +1 -1
  105. package/dist/rpc/runner-models.d.ts +14 -3
  106. package/dist/rpc/runner-models.d.ts.map +1 -1
  107. package/dist/rpc/runner-models.js +37 -13
  108. package/dist/rpc/runner-models.js.map +1 -1
  109. package/dist/rpc/slot.d.ts +3 -2
  110. package/dist/rpc/slot.d.ts.map +1 -1
  111. package/dist/rpc/slot.js.map +1 -1
  112. package/dist/runs/list-trim.d.ts +13 -0
  113. package/dist/runs/list-trim.d.ts.map +1 -0
  114. package/dist/runs/list-trim.js +71 -0
  115. package/dist/runs/list-trim.js.map +1 -0
  116. package/dist/runs/list-version.d.ts +4 -0
  117. package/dist/runs/list-version.d.ts.map +1 -0
  118. package/dist/runs/list-version.js +9 -0
  119. package/dist/runs/list-version.js.map +1 -0
  120. package/dist/runs/review-chain.d.ts.map +1 -1
  121. package/dist/runs/review-chain.js +2 -1
  122. package/dist/runs/review-chain.js.map +1 -1
  123. package/dist/runs/run-family-groups.d.ts.map +1 -1
  124. package/dist/runs/run-family-groups.js +5 -4
  125. package/dist/runs/run-family-groups.js.map +1 -1
  126. package/dist/runs/run-list-view.d.ts.map +1 -1
  127. package/dist/runs/run-list-view.js +2 -1
  128. package/dist/runs/run-list-view.js.map +1 -1
  129. package/dist/slots/tracking-branch.d.ts +7 -0
  130. package/dist/slots/tracking-branch.d.ts.map +1 -1
  131. package/dist/slots/tracking-branch.js.map +1 -1
  132. package/dist/text-order.d.ts +7 -0
  133. package/dist/text-order.d.ts.map +1 -0
  134. package/dist/text-order.js +7 -0
  135. package/dist/text-order.js.map +1 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.d.ts.map +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/version.js.map +1 -1
  140. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ 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.75.0 - 2026-11-03
10
+
11
+ - `SlotReleaseParams.skipArtifacts` is removed: a slot release no longer copies artifacts, so there is nothing to skip (F313).
12
+ - `SlotStatus.retained` (`{ items, bytes }`): the nested checkouts that slot releases moved aside on the slot host instead of deleting them, and their total size there (F310).
13
+ - `ReadyGatePayload.mergeConflicts`/`behindMain` are probed against the publication base (a stacked run's upstream branch), and a conflict on a publication gate now blocks its advancing actions (doc only; no shape change).
14
+ - A client-cache observer that throws no longer stops the other observers or the cache's reads; each failing observer is logged once with `console.error`. Before, an observer that threw as a read started, or on its first delivery from `observe`, left that query loading with no request sent until the next reconnect.
15
+ - `DispatchPreview.inheritedAcceptance`: the parent run and its original criteria a pr-complete run is held to, or `unavailable` with the reason.
16
+ - `Run.parkedCommits` (`ParkedRunCommits[]`): an artifact-only run's own branch commits, parked under `refs/farmslot/parked/<run>/<head>` when its slot returned to idle (append-only).
17
+ - `Run.releasedCheckout`: the branch and head an artifact-only run left on its slot when COMPLETE released the slot's ownership; a later ownerless return to idle parks only that head.
18
+ - `RunEngineState.publishGate.refusedEvidencePackageId`: the prepared package a human-gate replay found the worker's evidence refused against; while it is still `packageId`, the gate rebuilds rather than reuses it.
19
+ - `RunPipelineReviewPolicy` (`'default' | 'none'`) on `Run`, `RunCreateParams` and `DispatchPreviewParams` (with `completionPolicy`), `DispatchPreview.pipelineReview`, and the shared `pipelineReviewOffReason` / `pipelineReviewPolicyError` rule. Only an artifact-only dev or fix-bug run may turn its pipeline review off, and not alongside `reviewSequence`, `reviewPolicy` or an eval experiment; an unknown value is refused.
20
+ - `compareText` orders strings as `localeCompare` does, through one shared `Intl.Collator`. Run lists and family groups sort with it.
21
+ - Content prefetch hashes files and artifacts in 16 KB slices, one per macrotask, so a large file no longer blocks a frame; content a person opens still verifies at once.
22
+ - Change notifications match queries by dependency ids computed when the query is first observed.
23
+ - `ContentPrefetchOptions.media` (`ContentPrefetchMedia`, `ContentPrefetchMediaEntry`): a pending gate's videos and images over the content row cap go to a client media holder by digest, with the gate's age, instead of loading only when opened. `base64Bytes` and `createSha256` (chunked SHA-256) are exported from the client cache.
24
+ - A gate or run whose first `run.get` fails (a timeout on a busy gateway, not a refusal), or a gate whose package does not fully load, is prefetched again after 5 s, 30 s, then every 2 minutes until a read succeeds or the run leaves the targets. Before, nothing read it again until the gateway connection changed, so a pending gate's evidence, videos included, never reached the device. A gate whose review diff cannot be read still hands its videos and large images to the media holder.
25
+ - `awaitsHumanDecision(run)` is exported: a run at its human gate (also while a review it requested is still running and its ready decision is already resolved) or blocked at an open one, the runs the review queues show as waiting on a decision. Prefetch treats such a run as a gate and keeps prefetching its current package after its ready decision is resolved.
26
+ - `CommandMethod` is now the closed `GATEWAY_COMMAND_METHODS` list, and `GATEWAY_READ_EXCEPTIONS` names the only reads a client sends outside the shared cache (ADR-071 Amendment 2: offline-first reads).
27
+ - New `AppBindingMismatchPayload` (`kind: 'app_binding_mismatch'`) in `RunDecisionPayload`: the slot, the run's app, the slot's app and its source, and the project's slots bound to the run's app when they could be listed, and `operatorPick` when the slot came from a slot picker (F306).
28
+ - New `projectDefaultApp(project)`: `recipe.app`, else the project's only declared app.
29
+ - `PoolConfig` slots may carry `app`.
30
+ - Record the question-tool setting of native worker processes and terminal worker contexts so retained task handoffs can resume or relaunch when it changes.
31
+ - `trimRunForList`, `trimDecisionForList` and `RUN_LIST_PAYLOAD_VALUE_LIMIT` move here from the gateway (`runs/list-trim`), beside the new `runHasTrimmedDetail(run)`, moved from Command Center: whether a run.list row left out its output manifest or any decision payload value, which a view shows as not loaded until `run.get` lands. One module now holds what run.list leaves out and how a client tells.
32
+ - `readRefused(error)` (client-cache): whether a read error is a gateway refusal (`RUN_NOT_FOUND`, `NOT_FOUND`, `RESOURCE_NOT_FOUND`, `AUTH_REQUIRED`, `AUTH_FORBIDDEN`), the same codes on which the cache evicts saved data. A network failure or timeout is not one.
33
+ - `currentReviewChainEntry` reports `unresolvedCount: null` when the review result's `lineComments` is missing, as on a run.list row that trimmed it, instead of throwing.
34
+ - `slotRecoveryEligibility` treats a clean ownerless checkout with unpushed commits as safe for bulk release with no confirmation, because the release parks those commits. New `parksSlotCommits`, `SlotParkedCommits` and `slotParkedDetail`. `SlotRefreshResult` and `FleetRefreshSlotUpdateEvent` gain an optional `parked` report.
35
+ - `FleetStatus.poolErrors` lists pool files the last fleet refresh could not parse, and `SlotStatus.configError` gives why a slot's config could not be loaded.
36
+ - Recording timelines accept equal adjacent presentation timestamps without dropping decoded frames; backwards, invalid and out-of-duration timestamps remain invalid.
37
+ - Template previews and options on Dispatch accept an execution template id of the form `<flow>/<name>` (for example `pr-complete/default`). Before, every preview failed with "Invalid configuration selector: executionTemplateId". Command Center checks the selector before sending, and the gateway checks it again on marked reads, so both must run this version. The id still refuses `\`, empty segments, and `.` or `..` segments, and other configuration selectors stay single segments (F319).
38
+ - `isPickerCatalogModel` and `runnerModelProviderLabel` hold the one rule for which node catalog models a picker shows: LiteLLM ("Consensys LiteLLM"), xAI, Anthropic and local omlx/lmstudio models, without Claude 4.x or older or Grok before 4.7. `RunnerCatalogModel.nodes` names the execution nodes that list a model when some node does not. Pi and OpenCode have no `RUNNER_PICKER_MODELS` seed any more, `PI_ANTHROPIC_MODELS` is removed, and `DEFAULT_PI_MODEL` and `DEFAULT_GROK_MODEL` are `grok-4.7`. `NativeRunnerOption.nodeModels` holds each execution node's models for Pi and OpenCode, and `nativeRunnerModels(option, executionNodeId)` picks the list for a session's node. `DispatchCandidatesParams` takes the chosen `runner` and `model`.
39
+ - Keep bounded Dispatch candidate and ticket-match snapshots in the shared client store, with candidate invalidation when fleet, run, configuration or model catalogs change.
40
+
9
41
  ## 0.74.10 - 2026-11-01
10
42
 
11
43
  - `runWorkerEffort` names a run's worker effort (the worker context, then its native session, the dispatch effort, then the execution-profile choice for its runner and model), and `runEngineLabel` formats `runner/model · effort` with the effort only when known.
@@ -0,0 +1,15 @@
1
+ import type { GatewayResourceCache } from './types.js';
2
+ export interface StoredArtifactRead {
3
+ current(): GatewayResourceCache | undefined;
4
+ url: string;
5
+ online: boolean;
6
+ read(): Promise<Response>;
7
+ signal?: AbortSignal;
8
+ /** Explicit reloads wait for fresh bytes; ordinary display opens saved bytes immediately. */
9
+ refresh?: boolean;
10
+ onStored?(response: Response): unknown;
11
+ onRefreshError?(error: unknown | undefined): void;
12
+ }
13
+ /** Retained text uses the same verified, bounded content store as images and files. */
14
+ export declare function readStoredArtifactResponse({ current, url, online, read, signal, refresh, onRefreshError, onStored, }: StoredArtifactRead): Promise<Response>;
15
+ //# sourceMappingURL=artifact-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/client-cache/artifact-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,OAAO,IAAI,oBAAoB,GAAG,SAAS,CAAC;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IACvC,cAAc,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;CACnD;AAeD,uFAAuF;AACvF,wBAAsB,0BAA0B,CAAC,EAC/C,OAAO,EACP,GAAG,EACH,MAAM,EACN,IAAI,EACJ,MAAM,EACN,OAAe,EACf,cAAc,EACd,QAAQ,GACT,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsFxC"}
@@ -0,0 +1,103 @@
1
+ import { createSha256, RESOURCE_CONTENT_LIMITS } from './content.js';
2
+ function textResponse(data, init) {
3
+ const response = new Response(data.slice().buffer, init);
4
+ // React Native's Response polyfill decodes ArrayBuffers one byte per character.
5
+ Object.defineProperty(response, 'text', { value: async () => new TextDecoder().decode(data) });
6
+ return response;
7
+ }
8
+ function digest(data) {
9
+ const hash = createSha256();
10
+ hash.update(data);
11
+ return hash.hex();
12
+ }
13
+ /** Retained text uses the same verified, bounded content store as images and files. */
14
+ export async function readStoredArtifactResponse({ current, url, online, read, signal, refresh = false, onRefreshError, onStored, }) {
15
+ const cache = current();
16
+ if (!cache)
17
+ throw new Error('The offline store is not open yet');
18
+ const source = new URL(url, 'https://artifact.invalid');
19
+ if (source.pathname !== '/api/run-artifact')
20
+ throw new Error('Expected a retained artifact URL');
21
+ refresh ||= source.searchParams.has('m');
22
+ // Mirror refresh is an explicit reread, not part of the retained file's identity.
23
+ for (const name of ['token', 'password', 'm'])
24
+ source.searchParams.delete(name);
25
+ source.hash = '';
26
+ source.searchParams.sort();
27
+ // No URL, query credential or document body is written into an alias.
28
+ const alias = `artifact-response:${digest(new TextEncoder().encode(source.href))}`;
29
+ const token = cache.contentWriteToken();
30
+ const assertCurrent = () => {
31
+ if (signal?.aborted)
32
+ throw Object.assign(new Error('Artifact read aborted'), { name: 'AbortError' });
33
+ if (cache !== current() || cache.contentWriteToken().ownerVersion !== token.ownerVersion)
34
+ throw new Error('The artifact source changed');
35
+ };
36
+ assertCurrent();
37
+ const held = await cache.openContentByAlias(alias);
38
+ assertCurrent();
39
+ if (held && !online)
40
+ return textResponse(held);
41
+ if (!online)
42
+ throw new Error('This artifact has not been stored on this device');
43
+ const fetchFresh = async () => {
44
+ const response = await read();
45
+ assertCurrent();
46
+ if (!response.ok) {
47
+ if (held && (response.status === 401 || response.status === 403))
48
+ await cache.forgetContent({ kind: 'artifact', id: digest(held) });
49
+ return response;
50
+ }
51
+ const data = new Uint8Array(await response.arrayBuffer());
52
+ assertCurrent();
53
+ // Over-cap files remain the named large-media exception and are fetched only on open.
54
+ if (data.byteLength <= RESOURCE_CONTENT_LIMITS.fileBytes)
55
+ await cache.storeContent({
56
+ key: { kind: 'artifact', id: digest(data) },
57
+ data,
58
+ alias,
59
+ origin: 'opened',
60
+ cohort: 'run',
61
+ token,
62
+ });
63
+ assertCurrent();
64
+ return textResponse(data, {
65
+ status: response.status,
66
+ statusText: response.statusText,
67
+ headers: response.headers,
68
+ });
69
+ };
70
+ if (held && !refresh && onStored) {
71
+ await onStored(textResponse(held));
72
+ assertCurrent();
73
+ try {
74
+ const response = await fetchFresh();
75
+ if (response.status >= 500 && onRefreshError) {
76
+ onRefreshError(new Error(`HTTP ${response.status}`));
77
+ return textResponse(held);
78
+ }
79
+ onRefreshError?.(response.ok ? undefined : new Error(`HTTP ${response.status}`));
80
+ return response;
81
+ }
82
+ catch (error) {
83
+ assertCurrent();
84
+ if (!onRefreshError)
85
+ throw error;
86
+ onRefreshError(error);
87
+ return textResponse(held);
88
+ }
89
+ }
90
+ if (held && !refresh && onRefreshError) {
91
+ // The saved response is the display fallback. Report refresh failures separately so
92
+ // temporary network loss cannot hide it or leave a rejected background promise.
93
+ void fetchFresh().then((response) => onRefreshError(response.ok ? undefined : new Error(`HTTP ${response.status}`)), (error) => {
94
+ if (cache === current() &&
95
+ !signal?.aborted &&
96
+ cache.contentWriteToken().ownerVersion === token.ownerVersion)
97
+ onRefreshError(error);
98
+ });
99
+ return textResponse(held);
100
+ }
101
+ return fetchFresh();
102
+ }
103
+ //# sourceMappingURL=artifact-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-response.js","sourceRoot":"","sources":["../../src/client-cache/artifact-response.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,8 @@
1
- import type { GatewayResourceCache, GatewayResourceCacheOptions } from './types.js';
1
+ import type { GatewayResourceCache, GatewayResourceCacheOptions, ResourceQueryError } from './types.js';
2
+ /**
3
+ * The gateway refused the read: the resource is gone or not this principal's. The
4
+ * cache evicts saved data on these; a network failure or timeout is not one.
5
+ */
6
+ export declare function readRefused(error: ResourceQueryError | undefined): boolean;
2
7
  export declare function createGatewayResourceCache(options: GatewayResourceCacheOptions): GatewayResourceCache;
3
8
  //# sourceMappingURL=cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/client-cache/cache.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EACV,oBAAoB,EACpB,2BAA2B,EAS5B,MAAM,YAAY,CAAC;AAgGpB,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CA+qCtB"}
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,CAu2CtB"}
@@ -1,11 +1,13 @@
1
1
  import { Methods } from '../rpc/registry.js';
2
+ import { runListVersion } from '../runs/list-version.js';
2
3
  import { ResourceCollectorAdmission } from './collector-admission.js';
3
- import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, } from './content.js';
4
+ import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, verifyResourceContentInSlices, } from './content.js';
4
5
  import { isKnownResourceFamily, RESOURCE_PUBLISH_INTERVAL_MS, RESOURCE_REREAD_INTERVAL_MS, RESOURCE_RETRY_MAX_WAIT_MS, resourceKeyId, } from './keys.js';
5
6
  import { nativeHistoryStorageId, readNativeHistory, storedNativeHistoryMatches, } from './native-history.js';
6
7
  import { resourceQuery } from './registry.js';
8
+ import { createRunDetailSync, runDetailTarget } from './run-details.js';
7
9
  import { isRunListCollectionRead, RUN_LIST_COLLECTION_ID, RunListCollection, runListSummaryFlags, } from './run-list-versions.js';
8
- import { createMemoryResourceStore, normalizeResourceStoreScope, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
10
+ import { createMemoryResourceStore, normalizeResourceStoreScope, readStoredQueryBatch, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
9
11
  import { isUpstreamStoredQuery, upstreamRetentionEvictions } from './upstream-retention.js';
10
12
  import { workspaceRetentionEvictions, workspaceRetentionMembership, } from './workspace-retention.js';
11
13
  const deletionReasons = new Set([
@@ -21,6 +23,13 @@ const evictionCodes = new Set([
21
23
  'RUN_NOT_FOUND',
22
24
  'RESOURCE_NOT_FOUND',
23
25
  ]);
26
+ /**
27
+ * The gateway refused the read: the resource is gone or not this principal's. The
28
+ * cache evicts saved data on these; a network failure or timeout is not one.
29
+ */
30
+ export function readRefused(error) {
31
+ return Boolean(error && evictionCodes.has(error.code));
32
+ }
24
33
  function queryError(error) {
25
34
  const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : undefined;
26
35
  return {
@@ -44,6 +53,7 @@ function normalizeConnection(connection) {
44
53
  };
45
54
  }
46
55
  const defaultContentBudgetBytes = 500 * 1024 * 1024;
56
+ const startupQueryBytes = 8 * 1024 * 1024;
47
57
  // Only the identity crosses the store seam, never a caller's other fields.
48
58
  const contentKey = ({ kind, id }) => ({ kind, id });
49
59
  function profileId(connection) {
@@ -56,6 +66,9 @@ export function createGatewayResourceCache(options) {
56
66
  const transport = options.transport;
57
67
  let store = options.store ?? createMemoryResourceStore();
58
68
  let metadata;
69
+ let startupStore;
70
+ let startupOwnerVersion = -1;
71
+ const startupQueries = new Map();
59
72
  let connection = normalizeConnection(transport.getConnection());
60
73
  let usable = false;
61
74
  let disposed = false;
@@ -91,9 +104,33 @@ export function createGatewayResourceCache(options) {
91
104
  maxBodyBytes: resourceQuery(Methods.RUN_LIST).maxBytes,
92
105
  });
93
106
  let runListLoad;
107
+ const fallbackDetailTargets = new Map();
108
+ const detailSync = createRunDetailSync({
109
+ connection: () => connection,
110
+ list: async () => (await readStoredContent((target) => target.listQueries())) ?? [],
111
+ stored: (runId) => readStoredContent((target) => target.getQuery(resourceQuery(Methods.RUN_GET, { runId }).id)),
112
+ touch: async (runId) => {
113
+ const target = store;
114
+ const ownerVersion = hydrationOwnerVersion;
115
+ await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.touchQuery?.(resourceQuery(Methods.RUN_GET, { runId }).id, Date.now()) ??
116
+ Promise.resolve());
117
+ },
118
+ fetch: (runId, maxStoredBytes, signal) => retryQuery(resourceQuery(Methods.RUN_GET, { runId }), { origin: 'prefetch', cohort: 'run', maxStoredBytes }, signal),
119
+ onError: (error) => options.onStorageNotice?.(`Background run details could not sync: ${queryError(error).message}`),
120
+ });
121
+ function refreshDetailTargets() {
122
+ const versioned = runList.detailTargets();
123
+ detailSync.setTargets(store.persistent && usable
124
+ ? runList.held
125
+ ? versioned
126
+ : [...fallbackDetailTargets.values()]
127
+ : []);
128
+ }
94
129
  function forgetRunList() {
95
130
  runList.clear();
96
131
  runListLoad = undefined;
132
+ fallbackDetailTargets.clear();
133
+ detailSync.reset();
97
134
  }
98
135
  function updateCollectors() {
99
136
  collectors.update({ ...connection, foreground: usable && connection.foreground !== false }, usable
@@ -116,12 +153,29 @@ export function createGatewayResourceCache(options) {
116
153
  unsubscribeStore();
117
154
  store = next;
118
155
  storeVersion++;
156
+ startupStore = undefined;
157
+ startupQueries.clear();
158
+ detailSync.reset();
119
159
  unsubscribeStore = store.onDeleted(storeDeleted);
120
160
  }
161
+ /** Observers whose failure was already logged, so a repeating one is reported once. */
162
+ const failedObservers = new WeakSet();
163
+ /** One observer's failure is its own: the other observers and the cache's reads go on. */
164
+ function deliver(entry, listener) {
165
+ try {
166
+ listener(entry.state);
167
+ }
168
+ catch (error) {
169
+ if (failedObservers.has(listener))
170
+ return;
171
+ failedObservers.add(listener);
172
+ console.error(`Resource cache observer of ${entry.query.id} failed:`, error);
173
+ }
174
+ }
121
175
  function notify(entry) {
122
176
  entry.state = { ...entry.state, storageNotice };
123
177
  for (const listener of entry.listeners)
124
- listener(entry.state);
178
+ deliver(entry, listener);
125
179
  }
126
180
  function cancelScheduledRead(entry) {
127
181
  if (entry.readTimer !== undefined)
@@ -232,6 +286,46 @@ export function createGatewayResourceCache(options) {
232
286
  if (entry.state.status !== 'ready')
233
287
  hydrateEntry(entry);
234
288
  }
289
+ async function warmStoredQueries() {
290
+ if (!store.persistent ||
291
+ (startupStore === store && startupOwnerVersion === hydrationOwnerVersion))
292
+ return;
293
+ const readingStore = store;
294
+ const incarnation = storeVersion;
295
+ const ownerVersion = hydrationOwnerVersion;
296
+ const current = () => !disposed &&
297
+ store === readingStore &&
298
+ !readingStore.deleted &&
299
+ incarnation === storeVersion &&
300
+ ownerVersion === hydrationOwnerVersion;
301
+ try {
302
+ const batch = await readStoredQueryBatch(readingStore, startupQueryBytes);
303
+ if (!current())
304
+ return;
305
+ const collection = batch.find((saved) => saved.id === RUN_LIST_COLLECTION_ID);
306
+ const rows = collection && readingStore.getRows
307
+ ? await readingStore.getRows(RUN_LIST_COLLECTION_ID)
308
+ : undefined;
309
+ if (!current())
310
+ return;
311
+ startupQueries.clear();
312
+ let remaining = startupQueryBytes;
313
+ for (const saved of batch) {
314
+ if (saved.bytes > remaining)
315
+ continue;
316
+ remaining -= saved.bytes;
317
+ if (saved.id !== RUN_LIST_COLLECTION_ID)
318
+ startupQueries.set(saved.id, saved);
319
+ }
320
+ if (rows)
321
+ runList.hydrate(rows);
322
+ startupStore = readingStore;
323
+ startupOwnerVersion = ownerVersion;
324
+ }
325
+ catch (error) {
326
+ await recoverStore(error, readingStore);
327
+ }
328
+ }
235
329
  function schedule(entry) {
236
330
  if (entry.queued || disposed)
237
331
  return;
@@ -261,8 +355,13 @@ export function createGatewayResourceCache(options) {
261
355
  connection.id === next.id &&
262
356
  !scopeChanged &&
263
357
  !authorityChanged &&
264
- !revoke)
358
+ !revoke) {
359
+ if (connection.metered !== next.metered) {
360
+ connection = { ...connection, metered: next.metered };
361
+ refreshDetailTargets();
362
+ }
265
363
  return;
364
+ }
266
365
  const ticket = ++version;
267
366
  runList.abort();
268
367
  // Stored facts belong to an owner, not a socket attempt. Fence a revoked
@@ -273,6 +372,7 @@ export function createGatewayResourceCache(options) {
273
372
  }
274
373
  connection = next;
275
374
  usable = false;
375
+ refreshDetailTargets();
276
376
  updateCollectors();
277
377
  revisions.clear();
278
378
  for (const entry of entries.values()) {
@@ -361,9 +461,14 @@ export function createGatewayResourceCache(options) {
361
461
  await recoverStore(error, target);
362
462
  }
363
463
  }
464
+ if (disposed || ticket !== version)
465
+ return;
466
+ if (!revoke && canPersist(next))
467
+ await warmStoredQueries();
364
468
  if (disposed || ticket !== version)
365
469
  return;
366
470
  usable = next.connected && next.foreground !== false && !revoke;
471
+ refreshDetailTargets();
367
472
  updateCollectors();
368
473
  if (usable)
369
474
  for (const entry of entries.values())
@@ -409,7 +514,7 @@ export function createGatewayResourceCache(options) {
409
514
  if (change.revision <= (revisions.get(id) ?? -1))
410
515
  continue;
411
516
  revisions.set(id, change.revision);
412
- const affected = [...entries.values()].filter((entry) => entry.query.dependencies.some((key) => resourceKeyId(key) === id));
517
+ const affected = [...entries.values()].filter((entry) => entry.dependencyIds.has(id));
413
518
  // Coverage counts declared dependencies only, so a known key that no entry declares (another
414
519
  // slot, an unopened workspace) changes nothing held. Only a family this client cannot name might.
415
520
  if (!affected.length && isKnownResourceFamily(change.key?.family))
@@ -441,6 +546,7 @@ export function createGatewayResourceCache(options) {
441
546
  }
442
547
  const entry = {
443
548
  query,
549
+ dependencyIds: new Set(query.dependencies.map(resourceKeyId)),
444
550
  state: { status: 'loading' },
445
551
  listeners: new Set(),
446
552
  hydration: Promise.resolve(),
@@ -454,9 +560,40 @@ export function createGatewayResourceCache(options) {
454
560
  failures: 0,
455
561
  };
456
562
  entries.set(query.id, entry);
457
- hydrateEntry(entry);
563
+ const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
564
+ if (applyStoredQuery(entry, saved)) {
565
+ entry.hydrated = true;
566
+ queueMicrotask(prune);
567
+ }
568
+ else
569
+ hydrateEntry(entry);
458
570
  return entry;
459
571
  }
572
+ function takeStartupQuery(query) {
573
+ if (startupStore !== store || startupOwnerVersion !== hydrationOwnerVersion)
574
+ return undefined;
575
+ const id = nativeHistoryStorageId(query);
576
+ const saved = startupQueries.get(id);
577
+ startupQueries.delete(id);
578
+ return saved;
579
+ }
580
+ function applyStoredQuery(entry, saved) {
581
+ const query = entry.query;
582
+ if (!saved ||
583
+ !query.persisted ||
584
+ saved.bytes > query.maxBytes ||
585
+ (query.method === Methods.NATIVE_SESSION_READ &&
586
+ !storedNativeHistoryMatches(query.params, saved.data)))
587
+ return false;
588
+ const sourceObservation = query.sourceObservation?.(saved.data);
589
+ entry.state = {
590
+ status: 'stale',
591
+ data: saved.data,
592
+ observedAt: saved.observedAt,
593
+ ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
594
+ };
595
+ return true;
596
+ }
460
597
  function hydrateEntry(entry) {
461
598
  const query = entry.query;
462
599
  const hydrationSerial = ++entry.hydrationSerial;
@@ -470,7 +607,8 @@ export function createGatewayResourceCache(options) {
470
607
  const readingStore = store;
471
608
  try {
472
609
  const saved = query.persisted
473
- ? ((isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
610
+ ? (takeStartupQuery(query) ??
611
+ (isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
474
612
  (await readingStore.getQuery(nativeHistoryStorageId(query))))
475
613
  : undefined;
476
614
  if (!disposed &&
@@ -480,17 +618,7 @@ export function createGatewayResourceCache(options) {
480
618
  ownerVersion === hydrationOwnerVersion &&
481
619
  entry.hydrationSerial === hydrationSerial &&
482
620
  entry.state.status !== 'ready' &&
483
- saved &&
484
- saved.bytes <= query.maxBytes &&
485
- (query.method !== Methods.NATIVE_SESSION_READ ||
486
- storedNativeHistoryMatches(query.params, saved.data))) {
487
- const sourceObservation = query.sourceObservation?.(saved.data);
488
- entry.state = {
489
- status: 'stale',
490
- data: saved.data,
491
- observedAt: saved.observedAt,
492
- ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
493
- };
621
+ applyStoredQuery(entry, saved)) {
494
622
  notify(entry);
495
623
  }
496
624
  }
@@ -521,6 +649,9 @@ export function createGatewayResourceCache(options) {
521
649
  };
522
650
  await runListLoad.promise;
523
651
  }
652
+ return heldRunListView(query);
653
+ }
654
+ function heldRunListView(query) {
524
655
  const data = runList.view(query.params);
525
656
  return data && runList.observedAt !== undefined
526
657
  ? { id: query.id, data, observedAt: runList.observedAt, bytes: 0 }
@@ -623,7 +754,11 @@ export function createGatewayResourceCache(options) {
623
754
  const shared = entry.pending.prefetch;
624
755
  entry.pending.prefetch =
625
756
  shared && prefetch
626
- ? { origin: 'prefetch', cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort }
757
+ ? {
758
+ origin: 'prefetch',
759
+ cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort,
760
+ maxStoredBytes: Math.min(shared.maxStoredBytes ?? Infinity, prefetch.maxStoredBytes ?? Infinity),
761
+ }
627
762
  : undefined;
628
763
  return entry.pending.promise;
629
764
  }
@@ -744,6 +879,7 @@ export function createGatewayResourceCache(options) {
744
879
  const retention = entry.query.retention;
745
880
  const saved = serialized !== undefined &&
746
881
  bytes <= entry.query.maxBytes &&
882
+ (entry.listeners.size > 0 || bytes <= (pending.prefetch?.maxStoredBytes ?? Infinity)) &&
747
883
  (!retention || bytes <= retention.maxBytes)
748
884
  ? {
749
885
  id: nativeHistoryStorageId(entry.query),
@@ -816,7 +952,9 @@ export function createGatewayResourceCache(options) {
816
952
  (retention.cohort === 'upstream'
817
953
  ? isUpstreamStoredQuery(value.id)
818
954
  : value.id.startsWith(`${entry.query.method}:`)))
819
- .sort((a, b) => a.observedAt - b.observedAt ||
955
+ .sort((a, b) => (entry.query.method === Methods.RUN_GET
956
+ ? (a.accessedAt ?? a.observedAt) - (b.accessedAt ?? b.observedAt)
957
+ : a.observedAt - b.observedAt) ||
820
958
  (entries.get(a.id)?.lastUsed ?? 0) - (entries.get(b.id)?.lastUsed ?? 0));
821
959
  if (!current())
822
960
  return;
@@ -836,6 +974,14 @@ export function createGatewayResourceCache(options) {
836
974
  await writingStore.putQuery(row);
837
975
  });
838
976
  }
977
+ if (store.persistent && entry.query.method === Methods.RUN_LIST) {
978
+ const listed = reply.data;
979
+ if (Array.isArray(listed.runs)) {
980
+ for (const run of listed.runs)
981
+ fallbackDetailTargets.set(run.id, runDetailTarget(run, runListVersion(run)));
982
+ }
983
+ refreshDetailTargets();
984
+ }
839
985
  }
840
986
  catch (error) {
841
987
  if (disposed ||
@@ -962,14 +1108,25 @@ export function createGatewayResourceCache(options) {
962
1108
  });
963
1109
  return written;
964
1110
  }
1111
+ function contentWriteToken() {
1112
+ return { connectionId: connection.id, ownerVersion: hydrationOwnerVersion, storeVersion };
1113
+ }
965
1114
  /** Stores verified bytes within the caps and the shared budget. */
966
- function storeContent(input) {
1115
+ async function storeContent(input) {
967
1116
  const key = contentKey(input.key);
968
1117
  const { data, origin, cohort } = input;
969
- if (!resourceContentWithinLimits(key, data) ||
970
- !verifyResourceContent(key, data, input.untrackedFiles))
971
- return Promise.resolve(false);
972
- return writeContent(input.token, async (target, current) => {
1118
+ // Prefetch verifies between frames, so the write still answers to the owner it was asked under.
1119
+ const token = input.token ?? contentWriteToken();
1120
+ if (!resourceContentWithinLimits(key, data))
1121
+ return false;
1122
+ // Prefetch is background work; content a person opened is on their critical path and
1123
+ // verifies at once, as before.
1124
+ const verified = origin === 'prefetch'
1125
+ ? await verifyResourceContentInSlices(key, data, input.untrackedFiles)
1126
+ : verifyResourceContent(key, data, input.untrackedFiles);
1127
+ if (!verified)
1128
+ return false;
1129
+ return writeContent(token, async (target, current) => {
973
1130
  const accessedAt = Date.now();
974
1131
  const content = await target.listContent();
975
1132
  if (content.some((row) => row.kind === key.kind && row.id === key.id)) {
@@ -1019,6 +1176,13 @@ export function createGatewayResourceCache(options) {
1019
1176
  return true;
1020
1177
  });
1021
1178
  }
1179
+ async function retryQuery(query, placement, signal) {
1180
+ if (disposed)
1181
+ throw new Error('Resource cache is disposed');
1182
+ const entry = entryFor(query);
1183
+ await fetchEntry(entry, false, placement, signal);
1184
+ return entry.state;
1185
+ }
1022
1186
  return {
1023
1187
  observedKey(match) {
1024
1188
  for (const entry of entries.values())
@@ -1071,7 +1235,7 @@ export function createGatewayResourceCache(options) {
1071
1235
  notify(entry);
1072
1236
  });
1073
1237
  }
1074
- receive(entry.state);
1238
+ deliver(entry, receive);
1075
1239
  if (entry.state.status !== 'ready' || entry.dirty)
1076
1240
  schedule(entry);
1077
1241
  return () => {
@@ -1091,13 +1255,21 @@ export function createGatewayResourceCache(options) {
1091
1255
  throw new Error('Resource cache is disposed');
1092
1256
  return entryFor(query).state;
1093
1257
  },
1094
- async retry(query, placement, signal) {
1258
+ async readStored(query, refresh = true) {
1095
1259
  if (disposed)
1096
1260
  throw new Error('Resource cache is disposed');
1261
+ const ownerVersion = hydrationOwnerVersion;
1262
+ await barrier;
1097
1263
  const entry = entryFor(query);
1098
- await fetchEntry(entry, false, placement, signal);
1264
+ await entry.hydration;
1265
+ if (disposed || ownerVersion !== hydrationOwnerVersion)
1266
+ throw new Error('The offline store owner changed');
1267
+ // fetchEntry records network refusals in the query state and notifies its readers.
1268
+ if (refresh && entry.state.data !== undefined)
1269
+ void fetchEntry(entry);
1099
1270
  return entry.state;
1100
1271
  },
1272
+ retry: retryQuery,
1101
1273
  async retryAfterPending(query) {
1102
1274
  if (disposed)
1103
1275
  throw new Error('Resource cache is disposed');
@@ -1112,20 +1284,22 @@ export function createGatewayResourceCache(options) {
1112
1284
  readContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), false),
1113
1285
  runListProjections: () => runList.projections(),
1114
1286
  openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
1287
+ async forgetContent(key) {
1288
+ const target = store;
1289
+ const ownerVersion = hydrationOwnerVersion;
1290
+ await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.deleteContent(contentKey(key)));
1291
+ },
1115
1292
  readContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), false),
1116
1293
  openContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), true),
1117
1294
  storeContent,
1118
1295
  retainContent,
1119
- contentWriteToken: () => ({
1120
- connectionId: connection.id,
1121
- ownerVersion: hydrationOwnerVersion,
1122
- storeVersion,
1123
- }),
1296
+ contentWriteToken,
1124
1297
  async dispose(reason) {
1125
1298
  if (disposed)
1126
1299
  return;
1127
1300
  const withdrawing = collectors.close();
1128
1301
  disposed = true;
1302
+ detailSync.reset();
1129
1303
  version++;
1130
1304
  unsubscribeChanges();
1131
1305
  unsubscribeConnection();