@volter/world-core 3.0.149 → 3.0.151

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/inject.cjs CHANGED
@@ -1731,6 +1731,19 @@ function installWorldTime() {
1731
1731
  function installMiniflareOutbound() {
1732
1732
  if (Module.__volterMiniflareOutboundInstalled) return;
1733
1733
  Module.__volterMiniflareOutboundInstalled = true;
1734
+ // Miniflare's Request is not necessarily an instance of the receiving fetch's Request. Pass a URL and explicitly
1735
+ // read its getters (spreading the Request loses them); neither Node nor another undici version must stringify it.
1736
+ const nodeFetch = (fetcher, request) => {
1737
+ const init = { method: request.method, headers: Array.from(request.headers) };
1738
+ for (const key of ['signal', 'redirect', 'cache', 'credentials', 'integrity', 'keepalive', 'mode', 'referrer', 'referrerPolicy']) {
1739
+ if (request[key] !== undefined) init[key] = request[key];
1740
+ }
1741
+ if (request.body != null) {
1742
+ init.body = request.body;
1743
+ init.duplex = 'half';
1744
+ }
1745
+ return fetcher(request.url, init);
1746
+ };
1734
1747
  // A host this process maps to a twin goes through the patched fetch. Any other goes where the World routes it: through
1735
1748
  // its proxy (HTTPS_PROXY, trusting the World's CA), which Node's own fetch does not read, so through miniflare's undici
1736
1749
  // with the environment's proxy agent; loopback stays direct.
@@ -1740,17 +1753,16 @@ function installMiniflareOutbound() {
1740
1753
  if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
1741
1754
  const undici = Module.createRequire(miniflarePath)('undici');
1742
1755
  const dispatcher = new undici.EnvHttpProxyAgent({ noProxy: localGuestProxy() ? '' : process.env.NO_PROXY || process.env.no_proxy || 'localhost,127.0.0.1,::1' });
1743
- proxied = (request) => undici.fetch(request, { dispatcher });
1756
+ proxied = (url, init) => undici.fetch(url, { ...init, dispatcher });
1744
1757
  }
1745
1758
  } catch { proxied = null; }
1746
1759
  const mapped = (request) => { try { const url = new URL(request.url); return Boolean(resolveTwin(url.hostname, MAP, url.pathname)); } catch { return false; } };
1747
- // The Request itself is handed on: its method, headers and body are getters, which a copy of it as `init` loses.
1748
1760
  return (request) => {
1749
1761
  // Node fetch cannot return an upgraded connection. Miniflare's fetch couples the socket to its response;
1750
1762
  // its ws client uses Node's request/socket paths, so the same injector routing and refusals still apply.
1751
1763
  if (request.method === 'GET' && request.headers.get('upgrade')?.toLowerCase() === 'websocket'
1752
1764
  && typeof upgradeFetch === 'function') return upgradeFetch(request, {});
1753
- return !proxied || (installed && mapped(request)) ? globalThis.fetch(request) : proxied(request);
1765
+ return nodeFetch(!proxied || (installed && mapped(request)) ? globalThis.fetch : proxied, request);
1754
1766
  };
1755
1767
  };
1756
1768
  let outbound = null;
@@ -1777,7 +1789,7 @@ function installMiniflareOutbound() {
1777
1789
  if (!outbound) {
1778
1790
  let at = null;
1779
1791
  try { at = Module._resolveFilename(request, parent); } catch { at = null; }
1780
- outbound = at ? outboundFor(at, exports.fetch) : (request) => globalThis.fetch(request);
1792
+ outbound = outboundFor(at, exports.fetch);
1781
1793
  }
1782
1794
  const Original = exports.Miniflare;
1783
1795
  class Miniflare extends Original {
@@ -57,7 +57,7 @@ import { worldPaths } from "./storage.js";
57
57
  // the control root that holds it) and never learns what a world is. `volter-world` supplies
58
58
  // discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
59
59
  import { createHash } from 'node:crypto';
60
- import { aliasesFrom, readTreeMap, wholeLog } from "./log.js";
60
+ import { aliasesFrom, readTreeMapsAt, wholeLog } from "./log.js";
61
61
  import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from "./actions.js";
62
62
  import { canonicalJson, diffSubjectFields, hashFieldValue } from "./hash.js";
63
63
  import { applyTwinWrite } from "./serve.js";
@@ -173,11 +173,13 @@ export function describeActionFields(ledgers, actions) {
173
173
  if (!positions.has(e.id))
174
174
  positions.set(e.id, e.batch ? batchStart : i);
175
175
  });
176
- for (const { action, service, stateService } of actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService)) {
177
- const at = positions.get(action.id);
178
- if (at === undefined || !action.fields)
176
+ const mine = actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService && a.action.fields && positions.has(a.action.id));
177
+ // every write's pre-state from one capture of this twin's history (a capture per write read the whole log each time)
178
+ const trees = mine.length ? readTreeMapsAt(ledger.stateService, ledger.controlRoot, mine.map(a => positions.get(a.action.id))) : new Map();
179
+ for (const { action, service, stateService } of mine) {
180
+ if (!action.fields)
179
181
  continue;
180
- const subject = readTreeMap(stateService, ledger.controlRoot, { at }).get(`${action.subject.type}:${action.subject.id}`);
182
+ const subject = trees.get(positions.get(action.id))?.get(`${action.subject.type}:${action.subject.id}`);
181
183
  const fields = subject?.fields ?? {};
182
184
  fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
183
185
  subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
@@ -40,6 +40,9 @@ export declare function validateHistoryOrigin(origin: HistoryOrigin, layout?: Hi
40
40
  export declare const historyEntryTime: (entry: Entry) => number;
41
41
  export declare const historyDigest: (value: unknown) => string;
42
42
  export declare function historyLength(layout: HistoryLayout, depth?: number): number;
43
+ /** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
44
+ * fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
45
+ export declare function historyLayoutAt(layout: HistoryLayout, at: number): HistoryLayout;
43
46
  export declare function readHistoryView(directory: string, id: string): HistoryView;
44
47
  export declare function historyEntries(view: HistoryView): Entry[];
45
48
  export declare function historyPrefix(view: HistoryView, at: number): HistoryView;
@@ -40,6 +40,13 @@ const viewPath = (directory, id) => {
40
40
  function rows(directory, file) {
41
41
  return getActiveWorldStore().readLines(join(directory, file)).filter(line => line.trim()).map(line => toEntry(parseExactJson(line)));
42
42
  }
43
+ /** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
44
+ * fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
45
+ export function historyLayoutAt(layout, at) {
46
+ if (!Number.isInteger(at) || at < 0 || at > historyLength(layout))
47
+ throw new Error(`Invalid history position ${at}`);
48
+ return sliceLayout(layout, at);
49
+ }
43
50
  function sliceLayout(layout, at) {
44
51
  const base = layout.base ? historyLength(layout.base) : 0;
45
52
  return { ...(layout.base ? { base: sliceLayout(layout.base, Math.min(at, base)) } : {}), parent: Math.min(Math.max(0, at - base), layout.parent), branch: Math.min(Math.max(0, at - base - layout.parent), layout.branch) };
package/dist/src/log.d.ts CHANGED
@@ -183,6 +183,10 @@ export declare function treeChangesSince(service: string, root: string | undefin
183
183
  removed: string[];
184
184
  appended: string[];
185
185
  } | undefined;
186
+ /** The tree at each of several positions of a twin's history, folded from one capture of it: what
187
+ * `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
188
+ * changeset's field changes read one position per write, and a capture reads the whole log). */
189
+ export declare function readTreeMapsAt(service: string, root: string | undefined, positions: Iterable<number>): Map<number, Tree>;
186
190
  export declare function cutCheckpoint(service: string, root?: string): void;
187
191
  /** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
188
192
  export declare function unpushedEntries(service: string, root?: string): Entry[];
package/dist/src/log.js CHANGED
@@ -23,7 +23,7 @@ import { parseExactJson } from "./exact-json.js";
23
23
  // holds. The parent log accepts the v1 observed-event rows too (`toEntry` reads a delta event as
24
24
  // an entry; a non-delta observation folds nothing and is kept for `listEvents`), so a World whose
25
25
  // log was written in that form still reads.
26
- import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin } from "./history.js";
26
+ import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLayoutAt, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin } from "./history.js";
27
27
  import { dirname, join } from 'node:path';
28
28
  import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncestryLock, withHistoryLock } from "./ancestry.js";
29
29
  import { resolveReferences } from "./references.js";
@@ -569,24 +569,39 @@ export function treeChangesSince(service, root, since) {
569
569
  }
570
570
  return { stamp, changed: treeResources(present), removed, appended: [...order.keys()].filter((k) => tree.has(k)) };
571
571
  }
572
+ /** The tree at each of several positions of a twin's history, folded from one capture of it: what
573
+ * `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
574
+ * changeset's field changes read one position per write, and a capture reads the whole log). */
575
+ export function readTreeMapsAt(service, root, positions) {
576
+ return withHistoryLock(worldPaths(service, root).dir, () => {
577
+ const view = captureHistory(service, root).descriptor;
578
+ const entries = historyEntries(view);
579
+ const trees = new Map();
580
+ for (const at of positions)
581
+ if (!trees.has(at))
582
+ trees.set(at, treeAt(view, entries, at, service));
583
+ return trees;
584
+ });
585
+ }
586
+ /** The tree after a view's first `at` entries; a position inside an observation has none. */
587
+ function treeAt(view, entries, at, service) {
588
+ if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
589
+ let from = at;
590
+ let to = at;
591
+ const batch = entries[at].batch;
592
+ while (from > 0 && entries[from - 1]?.batch === batch)
593
+ from--;
594
+ while (to < entries.length && entries[to]?.batch === batch)
595
+ to++;
596
+ throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
597
+ }
598
+ return foldHistory(entries.slice(0, at), historyLayoutAt(view.layout, at), service);
599
+ }
572
600
  function readTreeMapFull(service, root, opts = {}) {
573
601
  return withHistoryLock(worldPaths(service, root).dir, () => {
574
602
  if (opts.at !== undefined || opts.view !== undefined) {
575
603
  const view = opts.view ? readHistoryView(worldPaths(service, root).dir, opts.view) : captureHistory(service, root).descriptor;
576
- const at = opts.at ?? historyLength(view.layout);
577
- const entries = historyEntries(view);
578
- if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
579
- let from = at;
580
- let to = at;
581
- const batch = entries[at].batch;
582
- while (from > 0 && entries[from - 1]?.batch === batch)
583
- from--;
584
- while (to < entries.length && entries[to]?.batch === batch)
585
- to++;
586
- throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
587
- }
588
- const prefix = historyPrefix(view, at);
589
- return foldHistory(entries.slice(0, at), prefix.layout, service);
604
+ return treeAt(view, historyEntries(view), opts.at ?? historyLength(view.layout), service);
590
605
  }
591
606
  // a cold read (no memo) trusts a checkpoint cut at exactly these log facts without re-hashing the logs
592
607
  const facts = opts.until === undefined ? `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}` : null;
package/inject.cjs CHANGED
@@ -1731,6 +1731,19 @@ function installWorldTime() {
1731
1731
  function installMiniflareOutbound() {
1732
1732
  if (Module.__volterMiniflareOutboundInstalled) return;
1733
1733
  Module.__volterMiniflareOutboundInstalled = true;
1734
+ // Miniflare's Request is not necessarily an instance of the receiving fetch's Request. Pass a URL and explicitly
1735
+ // read its getters (spreading the Request loses them); neither Node nor another undici version must stringify it.
1736
+ const nodeFetch = (fetcher, request) => {
1737
+ const init = { method: request.method, headers: Array.from(request.headers) };
1738
+ for (const key of ['signal', 'redirect', 'cache', 'credentials', 'integrity', 'keepalive', 'mode', 'referrer', 'referrerPolicy']) {
1739
+ if (request[key] !== undefined) init[key] = request[key];
1740
+ }
1741
+ if (request.body != null) {
1742
+ init.body = request.body;
1743
+ init.duplex = 'half';
1744
+ }
1745
+ return fetcher(request.url, init);
1746
+ };
1734
1747
  // A host this process maps to a twin goes through the patched fetch. Any other goes where the World routes it: through
1735
1748
  // its proxy (HTTPS_PROXY, trusting the World's CA), which Node's own fetch does not read, so through miniflare's undici
1736
1749
  // with the environment's proxy agent; loopback stays direct.
@@ -1740,17 +1753,16 @@ function installMiniflareOutbound() {
1740
1753
  if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
1741
1754
  const undici = Module.createRequire(miniflarePath)('undici');
1742
1755
  const dispatcher = new undici.EnvHttpProxyAgent({ noProxy: localGuestProxy() ? '' : process.env.NO_PROXY || process.env.no_proxy || 'localhost,127.0.0.1,::1' });
1743
- proxied = (request) => undici.fetch(request, { dispatcher });
1756
+ proxied = (url, init) => undici.fetch(url, { ...init, dispatcher });
1744
1757
  }
1745
1758
  } catch { proxied = null; }
1746
1759
  const mapped = (request) => { try { const url = new URL(request.url); return Boolean(resolveTwin(url.hostname, MAP, url.pathname)); } catch { return false; } };
1747
- // The Request itself is handed on: its method, headers and body are getters, which a copy of it as `init` loses.
1748
1760
  return (request) => {
1749
1761
  // Node fetch cannot return an upgraded connection. Miniflare's fetch couples the socket to its response;
1750
1762
  // its ws client uses Node's request/socket paths, so the same injector routing and refusals still apply.
1751
1763
  if (request.method === 'GET' && request.headers.get('upgrade')?.toLowerCase() === 'websocket'
1752
1764
  && typeof upgradeFetch === 'function') return upgradeFetch(request, {});
1753
- return !proxied || (installed && mapped(request)) ? globalThis.fetch(request) : proxied(request);
1765
+ return nodeFetch(!proxied || (installed && mapped(request)) ? globalThis.fetch : proxied, request);
1754
1766
  };
1755
1767
  };
1756
1768
  let outbound = null;
@@ -1777,7 +1789,7 @@ function installMiniflareOutbound() {
1777
1789
  if (!outbound) {
1778
1790
  let at = null;
1779
1791
  try { at = Module._resolveFilename(request, parent); } catch { at = null; }
1780
- outbound = at ? outboundFor(at, exports.fetch) : (request) => globalThis.fetch(request);
1792
+ outbound = outboundFor(at, exports.fetch);
1781
1793
  }
1782
1794
  const Original = exports.Miniflare;
1783
1795
  class Miniflare extends Original {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.149",
3
+ "version": "3.0.151",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
package/src/changeset.ts CHANGED
@@ -57,7 +57,7 @@ import { worldPaths } from './storage.ts';
57
57
  // the control root that holds it) and never learns what a world is. `volter-world` supplies
58
58
  // discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
59
59
  import { createHash } from 'node:crypto';
60
- import { aliasesFrom, parentEntries, readTreeMap, wholeLog, type Entry } from './log.ts';
60
+ import { aliasesFrom, parentEntries, readTreeMapsAt, wholeLog, type Entry } from './log.ts';
61
61
  import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from './actions.ts';
62
62
  import type { TwinActionRevertSpec } from './actions.ts';
63
63
  import { touchedSubjects } from './actions.ts';
@@ -351,10 +351,12 @@ export function describeActionFields(ledgers: LedgerRef[], actions: ChangesetAct
351
351
  if (!e.batch || e.batch !== entries[i - 1]?.batch) batchStart = i;
352
352
  if (!positions.has(e.id)) positions.set(e.id, e.batch ? batchStart : i);
353
353
  });
354
- for (const { action, service, stateService } of actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService)) {
355
- const at = positions.get(action.id);
356
- if (at === undefined || !action.fields) continue;
357
- const subject = readTreeMap(stateService, ledger.controlRoot, { at }).get(`${action.subject.type}:${action.subject.id}`);
354
+ const mine = actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService && a.action.fields && positions.has(a.action.id));
355
+ // every write's pre-state from one capture of this twin's history (a capture per write read the whole log each time)
356
+ const trees = mine.length ? readTreeMapsAt(ledger.stateService, ledger.controlRoot, mine.map(a => positions.get(a.action.id)!)) : new Map();
357
+ for (const { action, service, stateService } of mine) {
358
+ if (!action.fields) continue;
359
+ const subject = trees.get(positions.get(action.id)!)?.get(`${action.subject.type}:${action.subject.id}`);
358
360
  const fields = subject?.fields ?? {};
359
361
  fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
360
362
  subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
package/src/history.ts CHANGED
@@ -37,6 +37,12 @@ const viewPath = (directory: string, id: string) => {
37
37
  function rows(directory: string, file: Segment['file']): Entry[] {
38
38
  return getActiveWorldStore().readLines(join(directory, file)).filter(line => line.trim()).map(line => toEntry(parseExactJson(line)));
39
39
  }
40
+ /** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
41
+ * fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
42
+ export function historyLayoutAt(layout: HistoryLayout, at: number): HistoryLayout {
43
+ if (!Number.isInteger(at) || at < 0 || at > historyLength(layout)) throw new Error(`Invalid history position ${at}`);
44
+ return sliceLayout(layout, at);
45
+ }
40
46
  function sliceLayout(layout: HistoryLayout, at: number): HistoryLayout {
41
47
  const base = layout.base ? historyLength(layout.base) : 0;
42
48
  return { ...(layout.base ? { base: sliceLayout(layout.base, Math.min(at, base)) } : {}), parent: Math.min(Math.max(0, at - base), layout.parent), branch: Math.min(Math.max(0, at - base - layout.parent), layout.branch) };
package/src/log.ts CHANGED
@@ -23,7 +23,7 @@ import { parseExactJson } from './exact-json.ts';
23
23
  // holds. The parent log accepts the v1 observed-event rows too (`toEntry` reads a delta event as
24
24
  // an entry; a non-delta observation folds nothing and is kept for `listEvents`), so a World whose
25
25
  // log was written in that form still reads.
26
- import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin, type HistoryLayout } from './history.ts';
26
+ import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLayoutAt, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin, type HistoryLayout } from './history.ts';
27
27
  import { dirname, join } from 'node:path';
28
28
  import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncestryLock, withHistoryLock } from './ancestry.ts';
29
29
  import { resolveReferences } from './references.ts';
@@ -591,20 +591,35 @@ export function treeChangesSince(service: string, root: string | undefined, sinc
591
591
  return { stamp, changed: treeResources(present), removed, appended: [...order.keys()].filter((k) => tree.has(k)) };
592
592
  }
593
593
 
594
+ /** The tree at each of several positions of a twin's history, folded from one capture of it: what
595
+ * `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
596
+ * changeset's field changes read one position per write, and a capture reads the whole log). */
597
+ export function readTreeMapsAt(service: string, root: string | undefined, positions: Iterable<number>): Map<number, Tree> {
598
+ return withHistoryLock(worldPaths(service, root).dir, () => {
599
+ const view = captureHistory(service, root).descriptor;
600
+ const entries = historyEntries(view);
601
+ const trees = new Map<number, Tree>();
602
+ for (const at of positions) if (!trees.has(at)) trees.set(at, treeAt(view, entries, at, service));
603
+ return trees;
604
+ });
605
+ }
606
+
607
+ /** The tree after a view's first `at` entries; a position inside an observation has none. */
608
+ function treeAt(view: ReturnType<typeof captureHistory>['descriptor'], entries: Entry[], at: number, service: string): Tree {
609
+ if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
610
+ let from = at; let to = at; const batch = entries[at]!.batch;
611
+ while (from > 0 && entries[from - 1]?.batch === batch) from--;
612
+ while (to < entries.length && entries[to]?.batch === batch) to++;
613
+ throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
614
+ }
615
+ return foldHistory(entries.slice(0, at), historyLayoutAt(view.layout, at), service);
616
+ }
617
+
594
618
  function readTreeMapFull(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): Tree {
595
619
  return withHistoryLock(worldPaths(service, root).dir, () => {
596
620
  if (opts.at !== undefined || opts.view !== undefined) {
597
621
  const view = opts.view ? readHistoryView(worldPaths(service, root).dir, opts.view) : captureHistory(service, root).descriptor;
598
- const at = opts.at ?? historyLength(view.layout);
599
- const entries = historyEntries(view);
600
- if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
601
- let from = at; let to = at; const batch = entries[at]!.batch;
602
- while (from > 0 && entries[from - 1]?.batch === batch) from--;
603
- while (to < entries.length && entries[to]?.batch === batch) to++;
604
- throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
605
- }
606
- const prefix = historyPrefix(view, at);
607
- return foldHistory(entries.slice(0, at), prefix.layout, service);
622
+ return treeAt(view, historyEntries(view), opts.at ?? historyLength(view.layout), service);
608
623
  }
609
624
  // a cold read (no memo) trusts a checkpoint cut at exactly these log facts without re-hashing the logs
610
625
  const facts = opts.until === undefined ? `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}` : null;