@manyoars/oars 0.5.4 → 0.5.5

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 (47) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +18 -7
  3. package/dist/club.d.ts +2 -0
  4. package/dist/club.js +10 -4
  5. package/dist/sessions/archive.js +1 -1
  6. package/dist/sessions/card-command.d.ts +2 -2
  7. package/dist/sessions/card-command.js +7 -5
  8. package/dist/sessions/card-line.d.ts +6 -2
  9. package/dist/sessions/card-line.js +6 -3
  10. package/dist/sessions/cell-increments.d.ts +7 -0
  11. package/dist/sessions/cell-increments.js +25 -10
  12. package/dist/sessions/cells.d.ts +7 -0
  13. package/dist/sessions/cells.js +118 -107
  14. package/dist/sessions/crew-mark.d.ts +6 -0
  15. package/dist/sessions/crew-mark.js +22 -0
  16. package/dist/sessions/hook-index.d.ts +2 -1
  17. package/dist/sessions/hook-index.js +2 -2
  18. package/dist/sessions/pool-command.d.ts +22 -4
  19. package/dist/sessions/pool-command.js +516 -82
  20. package/dist/sessions/pool-delta.d.ts +5 -0
  21. package/dist/sessions/pool-delta.js +32 -12
  22. package/dist/sessions/pool-fold.d.ts +22 -0
  23. package/dist/sessions/pool-fold.js +65 -0
  24. package/dist/sessions/pool-outbox.d.ts +9 -0
  25. package/dist/sessions/pool-outbox.js +33 -12
  26. package/dist/sessions/pool.d.ts +10 -5
  27. package/dist/sessions/pool.js +16 -15
  28. package/dist/sessions/read-call-shape.js +3 -2
  29. package/dist/sessions/read-folds.d.ts +21 -0
  30. package/dist/sessions/read-folds.js +37 -0
  31. package/dist/sessions/read-recovery.d.ts +1 -1
  32. package/dist/sessions/read-recovery.js +6 -5
  33. package/dist/sessions/read-support.d.ts +4 -1
  34. package/dist/sessions/recall-consequence.d.ts +2 -1
  35. package/dist/sessions/recall-consequence.js +1 -2
  36. package/dist/sessions/recall-reads.d.ts +23 -1
  37. package/dist/sessions/recall-reads.js +91 -63
  38. package/dist/sessions/reversion.d.ts +41 -2
  39. package/dist/sessions/reversion.js +17 -10
  40. package/dist/sessions/store.d.ts +55 -2
  41. package/dist/sessions/store.js +312 -61
  42. package/dist/telemetry/measures.d.ts +5 -0
  43. package/dist/telemetry/measures.js +16 -2
  44. package/dist/telemetry/schema.js +3 -1
  45. package/dist/telemetry/words.d.ts +6 -0
  46. package/dist/telemetry/words.js +4 -0
  47. package/package.json +1 -1
@@ -52,6 +52,11 @@ export interface Carried {
52
52
  * again under the same key, builds none it already carried. Nothing is marked here: the caller marks them once the batch is written.
53
53
  */
54
54
  export declare const carriedDeltas: (account: string, sessions: readonly Gated[], sent: ReadonlySet<string>) => Map<string, Carried>;
55
+ /**
56
+ * The same deltas as `carriedDeltas`, one window at a time: `sessions` are placed from their session records alone, and `load` reads the full records of the ids one window needs (all of the window before, the first `DELTA_MIN_SESSIONS` after).
57
+ * What is held at once is one window's records, never the history.
58
+ */
59
+ export declare const carriedDeltasByWindow: (account: string, sessions: readonly Gated[], sent: ReadonlySet<string>, load: (ids: ReadonlySet<string>) => Promise<ReadonlyMap<string, SessionRecords>>, fillers?: ReadonlySet<string>) => Promise<Map<string, Carried>>;
55
60
  /** The delta entries the batch under `key` in this outbox already carries; none when it has no batch, or one this reader does not take. */
56
61
  export declare const heldDeltas: (outbox: string, key: string) => Promise<DeltaEntry[]>;
57
62
  /**
@@ -173,19 +173,39 @@ export const placedOf = (records) => {
173
173
  export const carriedDeltas = (account, sessions, sent) => {
174
174
  const carried = new Map();
175
175
  for (const { index, before, after, filler } of windowsOf(sessions)) {
176
- const entries = [];
177
- const marks = [];
178
- for (const entry of deltasOf(before.map(({ records }) => records),
179
176
  // The outcome is fixed at the fill: sessions after the filler change nothing, so a delta is the same whenever it is built.
180
- after.slice(0, DELTA_MIN_SESSIONS).map(({ records }) => records))) {
181
- const mark = deltaMark(account, entry, index);
182
- if (sent.has(mark))
183
- continue;
184
- entries.push(entry);
185
- marks.push(mark);
186
- }
187
- if (entries.length > 0)
188
- carried.set(filler.id, { entries, marks });
177
+ const found = carriedIn(account, index, before.map(({ records }) => records), after.slice(0, DELTA_MIN_SESSIONS).map(({ records }) => records), sent);
178
+ if (found.entries.length > 0)
179
+ carried.set(filler.id, found);
180
+ }
181
+ return carried;
182
+ };
183
+ const carriedIn = (account, index, before, after, sent) => {
184
+ const found = { entries: [], marks: [] };
185
+ for (const entry of deltasOf(before, after)) {
186
+ const mark = deltaMark(account, entry, index);
187
+ if (sent.has(mark))
188
+ continue;
189
+ found.entries.push(entry);
190
+ found.marks.push(mark);
191
+ }
192
+ return found;
193
+ };
194
+ /**
195
+ * The same deltas as `carriedDeltas`, one window at a time: `sessions` are placed from their session records alone, and `load` reads the full records of the ids one window needs (all of the window before, the first `DELTA_MIN_SESSIONS` after).
196
+ * What is held at once is one window's records, never the history.
197
+ */
198
+ export const carriedDeltasByWindow = async (account, sessions, sent, load, fillers) => {
199
+ const carried = new Map();
200
+ for (const { index, before, after, filler } of windowsOf(sessions)) {
201
+ // Only the batch of the session that fills a window carries its delta, so a window whose filler is not being built is not read.
202
+ if (fillers !== undefined && !fillers.has(filler.id))
203
+ continue;
204
+ const needed = [...before, ...after.slice(0, DELTA_MIN_SESSIONS)];
205
+ const records = await load(new Set(needed.map(({ id }) => id)));
206
+ const found = carriedIn(account, index, before.flatMap(({ id }) => records.get(id) ?? []), after.slice(0, DELTA_MIN_SESSIONS).flatMap(({ id }) => records.get(id) ?? []), sent);
207
+ if (found.entries.length > 0)
208
+ carried.set(filler.id, found);
189
209
  }
190
210
  return carried;
191
211
  };
@@ -0,0 +1,22 @@
1
+ import type { CellSet } from './cells.js';
2
+ import { type ReadFolds } from './read-folds.js';
3
+ import type { Recall } from './recall-object.js';
4
+ import { type ReversionIndex } from './reversion.js';
5
+ import type { Store } from './store.js';
6
+ /** What the pool step reads in place of the whole store's tools and prompts. */
7
+ export interface PoolFolds {
8
+ /** The recalls in `recalls.jsonl` order, each of a held session with its consequence. */
9
+ recalls: Recall[];
10
+ cells: CellSet;
11
+ reads: ReadFolds;
12
+ }
13
+ export interface PoolFold {
14
+ /** One run of sessions: their main and subagent records, prompts, tools, and own errors. A session of another run is not in it. */
15
+ add: (run: Store) => void;
16
+ finish: () => PoolFolds;
17
+ /** The matching's input as it stands, for a report of its size. */
18
+ index: ReversionIndex;
19
+ }
20
+ export declare const poolFold: (recalls: readonly Recall[]) => PoolFold;
21
+ /** The folds of a store held whole: one run. */
22
+ export declare const poolFoldsOf: (recalls: readonly Recall[], store: Store) => PoolFolds;
@@ -0,0 +1,65 @@
1
+ // What `refreshPool` needs of the rows of every turn, folded one run of sessions at a time (t1243, spec 4 Build 2): each recall's consequence, the cells, and the two read families' inputs.
2
+ // A run is a set of sessions with their prompts, tools, and own errors; the reversion matching waits for the last run, so `reverted` is filled in `finish`.
3
+ import { cellsFold, projectedStore } from './cell-increments.js';
4
+ import { shellReader } from './projection.js';
5
+ import { readFold } from './read-folds.js';
6
+ import { deriveConsequences, NO_RECALLS, recallGraphOf } from './recall-consequence.js';
7
+ import { finishReversions, foldReversions, newReversionIndex } from './reversion.js';
8
+ import { vocabulary } from './vocab-load.js';
9
+ export const poolFold = (recalls) => {
10
+ const bySid = new Map();
11
+ for (const [at, recall] of recalls.entries()) {
12
+ if (recall.sid === null)
13
+ continue;
14
+ bySid.set(recall.sid, [...(bySid.get(recall.sid) ?? []), { at, recall }]);
15
+ }
16
+ const derived = [...recalls];
17
+ const index = newReversionIndex();
18
+ // The cells are tallied with the graph of the run being added.
19
+ let graph = NO_RECALLS;
20
+ const cells = cellsFold({ place: (tool, steps) => graph.place(tool, steps), exposedPrompt: (session, at) => graph.exposedPrompt(session, at) });
21
+ const reads = readFold();
22
+ const read = shellReader(vocabulary());
23
+ // The recalls whose outcome holds a reversion lookup, which the last run settles.
24
+ const pending = [];
25
+ const add = (run) => {
26
+ foldReversions(index, run);
27
+ // In `recalls.jsonl` order, as the whole store's call keeps them.
28
+ const mine = run.sessions.flatMap((session) => bySid.get(session.session) ?? []).sort((a, b) => a.at - b.at);
29
+ const asked = [];
30
+ const placeholder = (session, prompt) => {
31
+ asked.push({ session, prompt });
32
+ return null;
33
+ };
34
+ const filled = deriveConsequences(run, mine.map(({ recall }) => recall), placeholder);
35
+ const promptKeys = new Set(run.prompts.map((prompt) => `${prompt.session}#${String(prompt.index)}`));
36
+ filled.forEach((recall, position) => {
37
+ const entry = mine[position];
38
+ const lookup = asked[position];
39
+ if (entry === undefined || lookup === undefined)
40
+ return;
41
+ derived[entry.at] = recall;
42
+ if (promptKeys.has(`${lookup.session}#${String(lookup.prompt)}`))
43
+ pending.push({ at: entry.at, filled: recall, ...lookup });
44
+ });
45
+ graph = recallGraphOf(run, filled);
46
+ cells.add(projectedStore(run, read));
47
+ reads.add(run);
48
+ };
49
+ const finish = () => {
50
+ const reverted = finishReversions(index);
51
+ for (const { at, filled, session, prompt } of pending) {
52
+ if (filled.consequence === undefined)
53
+ continue;
54
+ derived[at] = { ...filled, consequence: { ...filled.consequence, outcome: { ...filled.consequence.outcome, reverted: reverted(session, prompt) } } };
55
+ }
56
+ return { recalls: derived, cells: cells.finish(), reads: reads.finish() };
57
+ };
58
+ return { add, finish, index };
59
+ };
60
+ /** The folds of a store held whole: one run. */
61
+ export const poolFoldsOf = (recalls, store) => {
62
+ const fold = poolFold(recalls);
63
+ fold.add(store);
64
+ return fold.finish();
65
+ };
@@ -55,6 +55,12 @@ export declare const heldBatches: (outbox: string, stamp?: string) => Promise<{
55
55
  batches: Held[];
56
56
  edited: number;
57
57
  }>;
58
+ /** A batch file of the outbox, found by its name alone: nothing of it is read yet, so a run over thousands of them holds one at a time. */
59
+ export type HeldName = Omit<Held, 'bytes'>;
60
+ /** The batch files filed under this stamp, by name, in name order (the same ones `heldBatches` reads, whether or not they still hash to their names). */
61
+ export declare function namesHeld(outbox: string, stamp?: string): AsyncGenerator<HeldName>;
62
+ /** `namesHeld` over every stamp a send reads, the current one first. */
63
+ export declare function namesHeldAcross(outbox: string): AsyncGenerator<HeldName>;
58
64
  /** The stamps a send reads: the current one, then every other stamp `pulled.json` lists. A listed name that is not a stamp folder is not a path this reads. */
59
65
  export declare const sendStamps: (outbox: string) => Promise<string[]>;
60
66
  /** `heldBatches` over every stamp a send reads, the current one first. */
@@ -70,6 +76,9 @@ export declare const machineKeys: (outbox: string) => Promise<Set<string>>;
70
76
  export declare const noteMachineKey: (outbox: string, key: string) => Promise<void>;
71
77
  /** Records that this hash was printed, one line for each print: the lock below sends only a hash that appears here. */
72
78
  export declare const markShown: (outbox: string, hash: string, at: number) => Promise<void>;
79
+ /** Every hash `shown.log` holds, read once: a run over thousands of batches must not read the file for each. */
80
+ export declare const shownHashes: (outbox: string) => Promise<Set<string>>;
81
+ export declare const wasShown: (outbox: string, hash: string) => Promise<boolean>;
73
82
  export type LockRefusal = 'batch_unknown' | 'batch_not_shown' | 'batch_changed';
74
83
  /**
75
84
  * The lock on a send: a batch goes only if it is in the outbox, its hash was printed, and its file still hashes to that hash. The first check that fails names why. The detached sender (hook-bin.js nightly) sends with `requireShown: false`,
@@ -246,11 +246,22 @@ export const unchangedBytes = async (file, hash) => {
246
246
  * under any other stamp it is only a batch `pulled.json` lists there, since anything else in that folder is stale and is never printed or sent.
247
247
  */
248
248
  export const heldBatches = async (outbox, stamp = currentStamp()) => {
249
+ const batches = [];
250
+ let edited = 0;
251
+ for await (const named of namesHeld(outbox, stamp)) {
252
+ const bytes = await unchangedBytes(named.file, named.hash);
253
+ if (bytes === undefined)
254
+ edited += 1;
255
+ else
256
+ batches.push({ ...named, bytes });
257
+ }
258
+ return { batches, edited };
259
+ };
260
+ /** The batch files filed under this stamp, by name, in name order (the same ones `heldBatches` reads, whether or not they still hash to their names). */
261
+ export async function* namesHeld(outbox, stamp = currentStamp()) {
249
262
  const folder = join(outbox, stamp);
250
263
  const listed = await pulledAt(outbox, stamp);
251
264
  const here = stamp === currentStamp();
252
- const batches = [];
253
- let edited = 0;
254
265
  for (const name of await namesIn(folder)) {
255
266
  const found = BATCH_FILE.exec(name);
256
267
  if (found === null)
@@ -258,15 +269,14 @@ export const heldBatches = async (outbox, stamp = currentStamp()) => {
258
269
  const [, key = '', hash = ''] = found;
259
270
  if (!here && !listed.has(key))
260
271
  continue;
261
- const file = join(folder, name);
262
- const bytes = await unchangedBytes(file, hash);
263
- if (bytes === undefined)
264
- edited += 1;
265
- else
266
- batches.push({ key, stamp, pulled: listed.has(key), hash, file, bytes });
267
- }
268
- return { batches, edited };
269
- };
272
+ yield { key, stamp, pulled: listed.has(key), hash, file: join(folder, name) };
273
+ }
274
+ }
275
+ /** `namesHeld` over every stamp a send reads, the current one first. */
276
+ export async function* namesHeldAcross(outbox) {
277
+ for (const stamp of await sendStamps(outbox))
278
+ yield* namesHeld(outbox, stamp);
279
+ }
270
280
  /** The stamps a send reads: the current one, then every other stamp `pulled.json` lists. A listed name that is not a stamp folder is not a path this reads. */
271
281
  export const sendStamps = async (outbox) => {
272
282
  const others = new Set(Object.values(await readPulled(outbox, POOL_STEP)).filter((stamp) => stamp !== currentStamp() && STAMP_FOLDER.test(stamp)));
@@ -323,7 +333,18 @@ export const markShown = async (outbox, hash, at) => {
323
333
  await mkdirOwnerOnly(outbox);
324
334
  await appendOwnerOnly(shownFile(outbox), `${hash} ${new Date(at).toISOString()}\n`);
325
335
  };
326
- const wasShown = async (outbox, hash) => {
336
+ /** Every hash `shown.log` holds, read once: a run over thousands of batches must not read the file for each. */
337
+ export const shownHashes = async (outbox) => {
338
+ try {
339
+ return new Set((await readFile(shownFile(outbox), 'utf8')).split('\n').map((line) => line.split(' ')[0] ?? '').filter((hash) => hash !== ''));
340
+ }
341
+ catch (error) {
342
+ if (isMissing(error))
343
+ return new Set();
344
+ throw error;
345
+ }
346
+ };
347
+ export const wasShown = async (outbox, hash) => {
327
348
  try {
328
349
  return (await readFile(shownFile(outbox), 'utf8')).split('\n').some((line) => line.split(' ')[0] === hash);
329
350
  }
@@ -1,5 +1,7 @@
1
1
  import { type Pool } from './cells.js';
2
2
  import { type Read } from './read.js';
3
+ import { type PoolFolds } from './pool-fold.js';
4
+ import type { ReadFolds } from './read-folds.js';
3
5
  import { type Store } from './store.js';
4
6
  export declare const poolFiles: (storeDir: string) => {
5
7
  cells: string;
@@ -8,17 +10,20 @@ export declare const poolFiles: (storeDir: string) => {
8
10
  reads: (version: string) => string;
9
11
  };
10
12
  /** Every family's order 1 reads of one pool, in family order and then by slice, so a rerun writes the same bytes. */
11
- export declare const deriveFirstOrder: (pool: Pool, store: Store) => Read[];
13
+ export declare const deriveFirstOrder: (pool: Pool, store: Store, folds?: ReadFolds) => Read[];
12
14
  /** Order 1, then each stage of `relations` over everything before it. */
13
- export declare const deriveReads: (pool: Pool, store: Store) => Read[];
15
+ export declare const deriveReads: (pool: Pool, store: Store, folds?: ReadFolds) => Read[];
14
16
  export declare const serializeReads: (reads: readonly Read[]) => string;
15
- /** The current pool's reads: already written when this pool has been read before, else derived and written once. */
16
- export declare const refreshPool: (storeDir: string, store: Store) => Promise<{
17
+ /**
18
+ * The current pool's reads: already written when this pool has been read before, else derived and written once. `store` holds the sessions, subagents, and errors; the rows of every turn are read through
19
+ * `folds`, which a refresh folds run by run. Without them `store` is taken whole, with its prompts and tools, and folded as one run.
20
+ */
21
+ export declare const refreshPool: (storeDir: string, store: Store, folds?: PoolFolds) => Promise<{
17
22
  pool: Pool;
18
23
  reads: Read[];
19
24
  }>;
20
25
  /** The pool a deposit's index step reads. It answers without one (spec 2.7): a pool that cannot be read or written costs only the stored read IDs, whose answers keep their stand-ins, and one line on stderr says so. */
21
- export declare const currentPool: (storeDir: string, store: Store) => Promise<{
26
+ export declare const currentPool: (storeDir: string, store: Store, folds?: PoolFolds | Error) => Promise<{
22
27
  pool: Pool;
23
28
  reads: Read[];
24
29
  } | undefined>;
@@ -4,12 +4,9 @@
4
4
  import { readdir, readFile, stat } from 'node:fs/promises';
5
5
  import { basename, dirname, join } from 'node:path';
6
6
  import { CliError, ExitCode } from '../errors.js';
7
- import { cellsOf, projectedStore } from './cell-increments.js';
8
7
  import { poolOf, serializeCells, UNKNOWN } from './cells.js';
9
8
  import { LOAD_STATUS_FILE, loadStatusLines } from './load-status.js';
10
- import { shellReader } from './projection.js';
11
9
  import { contentHash, FIRST_ORDER_FAMILIES, frozenRead, makeRead, orderResolver } from './read.js';
12
- import { deriveConsequences, recallGraphOf } from './recall-consequence.js';
13
10
  import { readRecalls } from './recall-object.js';
14
11
  import { deriveAdoption } from './read-adoption.js';
15
12
  import { deriveCallShape } from './read-call-shape.js';
@@ -21,7 +18,7 @@ import { deriveMethod } from './read-method.js';
21
18
  import { deriveNextStep } from './read-next-step.js';
22
19
  import { deriveRecovery } from './read-recovery.js';
23
20
  import { deriveToolDay } from './read-tool-day.js';
24
- import { vocabulary } from './vocab-load.js';
21
+ import { poolFoldsOf } from './pool-fold.js';
25
22
  import { deriveDayMove } from './read-day-move.js';
26
23
  import { deriveInteraction } from './read-interaction.js';
27
24
  import { deriveLeadLag } from './read-lead-lag.js';
@@ -54,19 +51,19 @@ const made = (bodies, asOf, orderOf) => bodies
54
51
  .sort((a, b) => (a.key < b.key ? -1 : 1))
55
52
  .map(({ read }) => read);
56
53
  /** Every family's order 1 reads of one pool, in family order and then by slice, so a rerun writes the same bytes. */
57
- export const deriveFirstOrder = (pool, store) => {
54
+ export const deriveFirstOrder = (pool, store, folds) => {
58
55
  const asOf = { pool: pool.version, day: pool.lastDay };
59
56
  const orderOf = orderResolver([]);
60
57
  const byFamily = derivers();
61
- return FIRST_ORDER_FAMILIES.flatMap((family) => made(byFamily[family]({ pool, store }), asOf, orderOf));
58
+ return FIRST_ORDER_FAMILIES.flatMap((family) => made(byFamily[family]({ pool, store, ...(folds === undefined ? {} : { folds }) }), asOf, orderOf));
62
59
  };
63
60
  /** Order 1, then each stage of `relations` over everything before it. */
64
- export const deriveReads = (pool, store) => {
61
+ export const deriveReads = (pool, store, folds) => {
65
62
  const asOf = { pool: pool.version, day: pool.lastDay };
66
63
  return relations().reduce((known, stage) => {
67
64
  const orderOf = orderResolver(known);
68
65
  return [...known, ...stage.flatMap((derive) => made(derive(known), asOf, orderOf))];
69
- }, deriveFirstOrder(pool, store));
66
+ }, deriveFirstOrder(pool, store, folds));
70
67
  };
71
68
  export const serializeReads = (reads) => reads.map((read) => `${JSON.stringify(read)}\n`).join('');
72
69
  const isRead = (value) => typeof value === 'object' && value !== null && 'readId' in value && 'family' in value;
@@ -94,10 +91,12 @@ const storedRows = async (file, isRow) => {
94
91
  }
95
92
  };
96
93
  const readStored = async (file) => (await storedRows(file, isRead))?.map(frozenRead);
97
- /** The current pool's reads: already written when this pool has been read before, else derived and written once. */
98
- export const refreshPool = async (storeDir, store) => {
99
- const recalls = deriveConsequences(store, await readRecalls(storeDir));
100
- const set = cellsOf(projectedStore(store, shellReader(vocabulary())), recallGraphOf(store, recalls));
94
+ /**
95
+ * The current pool's reads: already written when this pool has been read before, else derived and written once. `store` holds the sessions, subagents, and errors; the rows of every turn are read through
96
+ * `folds`, which a refresh folds run by run. Without them `store` is taken whole, with its prompts and tools, and folded as one run.
97
+ */
98
+ export const refreshPool = async (storeDir, store, folds) => {
99
+ const { recalls, cells: set, reads: readFolds } = folds ?? poolFoldsOf(await readRecalls(storeDir), store);
101
100
  const pool = poolOf(set, store.errors);
102
101
  const files = poolFiles(storeDir);
103
102
  await writeAtomic(files.consequences, recalls.flatMap(({ id, n, consequence }) => (consequence === undefined ? [] : [`${JSON.stringify({ id, n, consequence })}\n`])).join(''));
@@ -108,7 +107,7 @@ export const refreshPool = async (storeDir, store) => {
108
107
  const stored = await readStored(file);
109
108
  if (stored !== undefined)
110
109
  return { pool, reads: stored };
111
- const reads = deriveReads(pool, store);
110
+ const reads = deriveReads(pool, store, readFolds);
112
111
  await writeAtomic(file, serializeReads(reads));
113
112
  return { pool, reads };
114
113
  };
@@ -117,9 +116,11 @@ const unbuilt = (error) => {
117
116
  process.stderr.write(`${POOL_FAILED}${error instanceof Error ? error.message : String(error)}\n`);
118
117
  };
119
118
  /** The pool a deposit's index step reads. It answers without one (spec 2.7): a pool that cannot be read or written costs only the stored read IDs, whose answers keep their stand-ins, and one line on stderr says so. */
120
- export const currentPool = async (storeDir, store) => {
119
+ export const currentPool = async (storeDir, store, folds) => {
121
120
  try {
122
- return await refreshPool(storeDir, store);
121
+ if (folds instanceof Error)
122
+ throw folds;
123
+ return await refreshPool(storeDir, store, folds);
123
124
  }
124
125
  catch (error) {
125
126
  unbuilt(error);
@@ -5,9 +5,10 @@
5
5
  import { lastDayOfCell, parseShapeClass, UNKNOWN } from './cells.js';
6
6
  import { OPEN_GATE, wireKeys } from './pool-wire.js';
7
7
  import { CALL_SHAPE_METHOD } from './read.js';
8
- import { actionOfShape, fitReads, toolShapeId } from './recall-reads.js';
8
+ import { actionOfShape, toolShapeId } from './recall-reads.js';
9
+ import { readFoldsOf } from './read-folds.js';
9
10
  import { body, atGrain, rate, wilson } from './read-support.js';
10
- export const deriveCallShape = ({ pool, store }) => fitReads(store).flatMap((read) => {
11
+ export const deriveCallShape = ({ pool, store, folds }) => (folds ?? readFoldsOf(store)).fit.flatMap((read) => {
11
12
  const action = actionOfShape(read.tool, read.harnesses);
12
13
  // The cell names the keys as the wire sends them (`projectedStore`); a record captured before steps was never projected, so its raw names are the other spelling.
13
14
  const names = new Set([wireKeys(read.keys, read.tool, OPEN_GATE).keys.join(','), read.keys.join(',')]);
@@ -0,0 +1,21 @@
1
+ import { type RecallRead } from './recall-reads.js';
2
+ import type { PromptObservation } from './schema.js';
3
+ import type { Store } from './store.js';
4
+ export interface ReadFolds {
5
+ /** `fitReads` over the whole store. */
6
+ fit: RecallRead[];
7
+ /** The hook index's fit reads: `fitReads` at the push floors, kept above the base rate of their action. */
8
+ push: RecallRead[];
9
+ /** The frame day of each prompt an error of a main session names, by `session#index`. */
10
+ promptDays: ReadonlyMap<string, string>;
11
+ }
12
+ /** The day a prompt's frame was written under: its frame's, else the day of its stamp. */
13
+ export declare const promptDayOf: (prompt: PromptObservation) => string;
14
+ export interface ReadFold {
15
+ /** One run of sessions; its errors name prompts of its own sessions only. */
16
+ add: (run: Store) => void;
17
+ finish: () => ReadFolds;
18
+ }
19
+ export declare const readFold: () => ReadFold;
20
+ /** The folds of a store held whole: one run. */
21
+ export declare const readFoldsOf: (store: Store) => ReadFolds;
@@ -0,0 +1,37 @@
1
+ // What the two families that read the rows of every turn keep of them (t1243): the fit shapes `deriveCallShape` cites, and the days of the prompts a recovery read's errors name. A refresh folds
2
+ // them one run of sessions at a time, so neither family holds the tools or prompts of the whole history; a store held whole folds the same way as one run.
3
+ import { UNKNOWN } from './cells.js';
4
+ import { utcDay } from './frame.js';
5
+ import { actionFold, aboveActionRate, fitFold, PUSH_FLOORS } from './recall-reads.js';
6
+ /** The day a prompt's frame was written under: its frame's, else the day of its stamp. */
7
+ export const promptDayOf = (prompt) => prompt.frame?.day ?? utcDay(prompt.ts) ?? UNKNOWN;
8
+ export const readFold = () => {
9
+ const fit = fitFold();
10
+ const pushFit = fitFold(PUSH_FLOORS);
11
+ const actions = actionFold();
12
+ const promptDays = new Map();
13
+ return {
14
+ add: (run) => {
15
+ fit.add(run);
16
+ pushFit.add(run);
17
+ actions.add(run);
18
+ const prompts = new Map(run.prompts.map((prompt) => [`${prompt.session}#${String(prompt.index)}`, prompt]));
19
+ for (const error of run.errors) {
20
+ // A subagent's error is filed under its own session's frame, never a prompt's.
21
+ if (error.agent !== undefined)
22
+ continue;
23
+ const key = `${error.session}#${String(error.prompt)}`;
24
+ const prompt = prompts.get(key);
25
+ if (prompt !== undefined)
26
+ promptDays.set(key, promptDayOf(prompt));
27
+ }
28
+ },
29
+ finish: () => ({ fit: fit.finish(), push: aboveActionRate(actions.finish(), pushFit.finish()), promptDays }),
30
+ };
31
+ };
32
+ /** The folds of a store held whole: one run. */
33
+ export const readFoldsOf = (store) => {
34
+ const fold = readFold();
35
+ fold.add(store);
36
+ return fold.finish();
37
+ };
@@ -16,5 +16,5 @@ export interface RecoveryOutcome {
16
16
  [reason in RecoveryDrop]: number;
17
17
  };
18
18
  }
19
- export declare const recoveryOf: ({ pool, store }: FamilyInput) => RecoveryOutcome;
19
+ export declare const recoveryOf: ({ pool, store, folds }: FamilyInput) => RecoveryOutcome;
20
20
  export declare const deriveRecovery: DeriveFamily;
@@ -6,6 +6,7 @@ import { UNKNOWN, weekStartOf } from './cells.js';
6
6
  import { utcDay } from './frame.js';
7
7
  import { shapeMotif } from './motif.js';
8
8
  import { contentHash } from './read.js';
9
+ import { readFoldsOf } from './read-folds.js';
9
10
  import { atGrain, body, groupBy } from './read-support.js';
10
11
  import { harnessOf } from './schema.js';
11
12
  import { signatureRecord } from './store.js';
@@ -35,9 +36,9 @@ const oneRowPerCall = (errors) => {
35
36
  }
36
37
  return [...rows, ...calls.values()];
37
38
  };
38
- export const recoveryOf = ({ pool, store }) => {
39
+ export const recoveryOf = ({ pool, store, folds }) => {
39
40
  const sessions = new Map([...store.sessions, ...store.subagents].map((session) => [session.session, session]));
40
- const prompts = new Map(store.prompts.map((prompt) => [`${prompt.session}#${String(prompt.index)}`, prompt]));
41
+ const promptDays = (folds ?? readFoldsOf(store)).promptDays;
41
42
  const actionIn = (session, tool) => actionOf(ACTION_TABLES[harnessOf(sessions.get(session) ?? {})], tool).action;
42
43
  // The failed call is a one-step shape, which has a live day cell. The step after it is a two-step window, which has a week cell only (rule 6), so it is found on the week its day falls in.
43
44
  const cellsAt = groupBy(atGrain(pool, 'day'), (cell) => `${cell.motifId}/${cell.day}`);
@@ -45,9 +46,9 @@ export const recoveryOf = ({ pool, store }) => {
45
46
  const cellsOf = groupBy(atGrain(pool, 'day'), (cell) => cell.motifId);
46
47
  /** The day a cell files this error's call under: its frame's, which is its prompt's, or its session's for a subagent or a call before the first prompt. An error can run past that midnight. */
47
48
  const frameDayOf = (error) => {
48
- const prompt = error.agent === undefined ? prompts.get(`${error.session}#${String(error.prompt)}`) : undefined;
49
- if (prompt !== undefined)
50
- return prompt.frame?.day ?? utcDay(prompt.ts) ?? UNKNOWN;
49
+ const day = error.agent === undefined ? promptDays.get(`${error.session}#${String(error.prompt)}`) : undefined;
50
+ if (day !== undefined)
51
+ return day;
51
52
  const session = sessions.get(error.agent ?? error.session);
52
53
  return session?.frame?.day ?? utcDay(session?.tsFirst) ?? UNKNOWN;
53
54
  };
@@ -1,11 +1,14 @@
1
1
  import { type Cell, type Grain, type Pool } from './cells.js';
2
2
  import type { Motif } from './motif.js';
3
3
  import { type Family, type ReadDraft, type ReadInterval, type ReadSlice, type ReadValue } from './read.js';
4
+ import type { ReadFolds } from './read-folds.js';
4
5
  import type { Store } from './store.js';
5
6
  export interface FamilyInput {
6
7
  pool: Pool;
7
- /** Only recovery reads this: an error's signature is not in a cell. */
8
+ /** Its sessions, subagents, and errors. Only recovery reads this: an error's signature is not in a cell. A refresh holds no tools or prompts here: the two families that read them take `folds`. */
8
9
  store: Store;
10
+ /** What a refresh folded from the rows of every turn, run by run; absent, the two families that need it fold `store` whole. */
11
+ folds?: ReadFolds;
9
12
  }
10
13
  /** A read as a family states it; the pool's snapshot is added to all of them in one place. */
11
14
  export type ReadBody = Omit<ReadDraft, 'asOf'>;
@@ -1,11 +1,12 @@
1
1
  import { type CountedStep } from './motif.js';
2
2
  import type { Recall } from './recall-object.js';
3
+ import { reversionsOf } from './reversion.js';
3
4
  import { type ToolObservation } from './schema.js';
4
5
  import type { Store } from './store.js';
5
6
  /** A consequence window ends at the prompt's end or after this many calls (spec Section 2.7). */
6
7
  export declare const WINDOW_CALLS = 20;
7
8
  /** Fills `n` and `consequence`. A recall with no session, or from a session this store does not hold, has nothing to read and comes back as it was. */
8
- export declare const deriveConsequences: (store: Store, recalls: readonly Recall[]) => Recall[];
9
+ export declare const deriveConsequences: (store: Store, recalls: readonly Recall[], reverted?: ReturnType<typeof reversionsOf>) => Recall[];
9
10
  /** A step as a cell counts it, and whether it sits inside a shown recall's window. */
10
11
  export interface PlacedStep {
11
12
  step: CountedStep;
@@ -74,11 +74,10 @@ const consequenceOf = (recall, session, line, position, harness, prompts, revert
74
74
  };
75
75
  };
76
76
  /** Fills `n` and `consequence`. A recall with no session, or from a session this store does not hold, has nothing to read and comes back as it was. */
77
- export const deriveConsequences = (store, recalls) => {
77
+ export const deriveConsequences = (store, recalls, reverted = reversionsOf(store)) => {
78
78
  const lines = mainLines(store);
79
79
  const prompts = new Map(store.prompts.map((prompt) => [promptKey(prompt.session, prompt.index), prompt]));
80
80
  const sessions = new Map(store.sessions.map((session) => [session.session, session]));
81
- const reverted = reversionsOf(store);
82
81
  return recalls.map((recall) => {
83
82
  const session = recall.sid === null ? undefined : sessions.get(recall.sid);
84
83
  if (session === undefined)
@@ -53,7 +53,16 @@ export declare const FIT_FLOORS: FitFloors;
53
53
  * `FIT_FLOORS`: a base rate is still a fact to read on request.
54
54
  */
55
55
  export declare const PUSH_FLOORS: FitFloors;
56
- /** `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends. */
56
+ export interface FitFold {
57
+ add: (store: Store) => void;
58
+ finish: () => RecallRead[];
59
+ }
60
+ /**
61
+ * `fitReads` as a fold: each `add` tallies one run of sessions into the same shapes, and `finish` applies the floors. Runs come in the whole store's row order, so the tie order of `followed` matches.
62
+ * `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends.
63
+ */
64
+ export declare const fitFold: (floors?: FitFloors, keep?: (call: ToolObservation) => boolean) => FitFold;
65
+ /** `fitFold` over the whole store at once. `keep` narrows the calls tallied, not what follows them. */
57
66
  export declare const fitReads: (store: Store, floors?: FitFloors, keep?: (call: ToolObservation) => boolean) => RecallRead[];
58
67
  /** The action of a tool under the first of its harnesses whose table lists it, else under the first harness (the default one when a read names none). */
59
68
  export declare const actionOfShape: (tool: string, harnesses: readonly string[]) => CanonicalAction;
@@ -63,6 +72,19 @@ export declare const actionOfShape: (tool: string, harnesses: readonly string[])
63
72
  * 79 of 79 pushes, at 4.5x and 8.6x their action's share: a real signal, not a defect. Open parameter, like the floors.
64
73
  */
65
74
  export declare const PUSH_MIN_LIFT = 2;
75
+ /** The per-action call tally `aboveBaseRate` compares against, as a fold: each `add` tallies one run of sessions, so a refresh never holds every call. */
76
+ export declare const actionFold: () => {
77
+ add: (store: Store) => void;
78
+ finish: () => Map<CanonicalAction, {
79
+ calls: number;
80
+ failed: number;
81
+ }>;
82
+ };
83
+ /** `aboveBaseRate` over a finished action tally. */
84
+ export declare const aboveActionRate: (byAction: ReadonlyMap<CanonicalAction, {
85
+ calls: number;
86
+ failed: number;
87
+ }>, reads: readonly RecallRead[], lift?: number) => RecallRead[];
66
88
  /**
67
89
  * The reads whose shape fails at `lift` times the share of its whole action or more, where the action's share counts every call of every shape of it, those under the floors too.
68
90
  * A tool no table lists is the action `other` (or `mcp.call`), so it is held to that action's share like any other and can enter the index and fire. Compared in integers, so a tie is exact.