@manyoars/oars 0.5.3 → 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.
- package/README.md +6 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +79 -6
- package/dist/club.d.ts +2 -0
- package/dist/club.js +10 -4
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/sessions/archive.js +1 -1
- package/dist/sessions/card-command.d.ts +2 -2
- package/dist/sessions/card-command.js +7 -5
- package/dist/sessions/card-line.d.ts +6 -2
- package/dist/sessions/card-line.js +6 -3
- package/dist/sessions/cell-increments.d.ts +7 -0
- package/dist/sessions/cell-increments.js +25 -10
- package/dist/sessions/cells.d.ts +7 -0
- package/dist/sessions/cells.js +118 -107
- package/dist/sessions/crew-mark.d.ts +6 -0
- package/dist/sessions/crew-mark.js +22 -0
- package/dist/sessions/hook-index.d.ts +2 -1
- package/dist/sessions/hook-index.js +2 -2
- package/dist/sessions/pool-command.d.ts +47 -1
- package/dist/sessions/pool-command.js +694 -90
- package/dist/sessions/pool-delta.d.ts +5 -0
- package/dist/sessions/pool-delta.js +32 -12
- package/dist/sessions/pool-fold.d.ts +22 -0
- package/dist/sessions/pool-fold.js +65 -0
- package/dist/sessions/pool-outbox.d.ts +22 -0
- package/dist/sessions/pool-outbox.js +78 -11
- package/dist/sessions/pool-send-lock.d.ts +2 -1
- package/dist/sessions/pool-send-lock.js +4 -2
- package/dist/sessions/pool.d.ts +10 -5
- package/dist/sessions/pool.js +16 -15
- package/dist/sessions/read-call-shape.js +3 -2
- package/dist/sessions/read-folds.d.ts +21 -0
- package/dist/sessions/read-folds.js +37 -0
- package/dist/sessions/read-recovery.d.ts +1 -1
- package/dist/sessions/read-recovery.js +6 -5
- package/dist/sessions/read-support.d.ts +4 -1
- package/dist/sessions/recall-consequence.d.ts +2 -1
- package/dist/sessions/recall-consequence.js +1 -2
- package/dist/sessions/recall-reads.d.ts +23 -1
- package/dist/sessions/recall-reads.js +91 -63
- package/dist/sessions/reversion.d.ts +41 -2
- package/dist/sessions/reversion.js +17 -10
- package/dist/sessions/store.d.ts +55 -2
- package/dist/sessions/store.js +312 -61
- package/dist/telemetry/measures.d.ts +5 -0
- package/dist/telemetry/measures.js +16 -2
- package/dist/telemetry/schema.js +3 -1
- package/dist/telemetry/words.d.ts +6 -0
- package/dist/telemetry/words.js +4 -0
- 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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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`,
|
|
@@ -94,6 +103,19 @@ export interface Pulled {
|
|
|
94
103
|
export declare const readPulled: (outbox: string, step: string) => Promise<Pulled>;
|
|
95
104
|
/** Staged and renamed, so a crash leaves the whole old file or the whole new one. */
|
|
96
105
|
export declare const writePulled: (outbox: string, pulled: Pulled) => Promise<void>;
|
|
106
|
+
/** One session a person took out with `oars pool remove`: the hash of the batch that was removed, which is how `--restore` names the way back, and the session id when it could be resolved. The outbox key derives from the account or the machine's environment, so only the session id keeps the session out after either changes; an entry with no id (the session was gone from the store, or the file is older) skips by key alone. */
|
|
107
|
+
export interface RemovedEntry {
|
|
108
|
+
hash: string;
|
|
109
|
+
session?: string;
|
|
110
|
+
}
|
|
111
|
+
/** The sessions a person took out of the outbox with `oars pool remove`, by outbox key. Local only; nothing in it goes to the pool. */
|
|
112
|
+
export interface Removed {
|
|
113
|
+
[key: string]: RemovedEntry;
|
|
114
|
+
}
|
|
115
|
+
/** `removed.json` as it stands; none when the file is missing. A file that is not a list of keys and entries fails the command that reads it, so a damaged list never becomes "send everything". A bare string is an entry with only a hash, the form before session ids were kept. */
|
|
116
|
+
export declare const readRemoved: (outbox: string, step: string) => Promise<Removed>;
|
|
117
|
+
/** Staged and renamed, like `pulled.json`: a crash leaves the whole old list or the whole new one. */
|
|
118
|
+
export declare const writeRemoved: (outbox: string, removed: Removed) => Promise<void>;
|
|
97
119
|
/** Whether any string or key in `value` holds one of `terms`, compared lowercased the way the local gate compares a transcript. */
|
|
98
120
|
export declare const holdsTerm: (value: unknown, terms: readonly string[]) => boolean;
|
|
99
121
|
/**
|
|
@@ -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
|
-
|
|
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 });
|
|
272
|
+
yield { key, stamp, pulled: listed.has(key), hash, file: join(folder, name) };
|
|
267
273
|
}
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -385,6 +406,52 @@ export const writePulled = async (outbox, pulled) => {
|
|
|
385
406
|
await writeOwnerOnly(staged, `${JSON.stringify(Object.fromEntries(Object.entries(pulled).sort(([a], [b]) => (a < b ? -1 : 1))), null, 2)}\n`);
|
|
386
407
|
await rename(staged, join(outbox, PULLED));
|
|
387
408
|
};
|
|
409
|
+
const REMOVED = 'removed.json';
|
|
410
|
+
const removedEntryOf = (value) => {
|
|
411
|
+
if (typeof value === 'string')
|
|
412
|
+
return { hash: value };
|
|
413
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || !('hash' in value) || typeof value.hash !== 'string')
|
|
414
|
+
return undefined;
|
|
415
|
+
if (!('session' in value))
|
|
416
|
+
return { hash: value.hash };
|
|
417
|
+
return typeof value.session === 'string' ? { hash: value.hash, session: value.session } : undefined;
|
|
418
|
+
};
|
|
419
|
+
/** `removed.json` as it stands; none when the file is missing. A file that is not a list of keys and entries fails the command that reads it, so a damaged list never becomes "send everything". A bare string is an entry with only a hash, the form before session ids were kept. */
|
|
420
|
+
export const readRemoved = async (outbox, step) => {
|
|
421
|
+
let text;
|
|
422
|
+
try {
|
|
423
|
+
text = await readFile(join(outbox, REMOVED), 'utf8');
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
if (isMissing(error))
|
|
427
|
+
return {};
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
let value;
|
|
431
|
+
try {
|
|
432
|
+
value = JSON.parse(text);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
value = undefined;
|
|
436
|
+
}
|
|
437
|
+
const entries = typeof value !== 'object' || value === null || Array.isArray(value) ? undefined : Object.entries(value).map(([key, entry]) => [key, removedEntryOf(entry)]);
|
|
438
|
+
const kept = [];
|
|
439
|
+
for (const [key, entry] of entries ?? [])
|
|
440
|
+
if (entry !== undefined)
|
|
441
|
+
kept.push([key, entry]);
|
|
442
|
+
if (entries === undefined || kept.length !== entries.length) {
|
|
443
|
+
throw new CliError(`${join(outbox, REMOVED)} is not a list of removed sessions. Move it aside and run the command again; nothing was changed, built, or sent.`, ExitCode.INVALID, step);
|
|
444
|
+
}
|
|
445
|
+
return Object.fromEntries(kept);
|
|
446
|
+
};
|
|
447
|
+
/** Staged and renamed, like `pulled.json`: a crash leaves the whole old list or the whole new one. */
|
|
448
|
+
export const writeRemoved = async (outbox, removed) => {
|
|
449
|
+
await mkdirOwnerOnly(outbox);
|
|
450
|
+
const staged = join(outbox, `${REMOVED}.tmp`);
|
|
451
|
+
await writeOwnerOnly(staged, `${JSON.stringify(Object.fromEntries(Object.entries(removed).sort(([a], [b]) => (a < b ? -1 : 1))), null, 2)}
|
|
452
|
+
`);
|
|
453
|
+
await rename(staged, join(outbox, REMOVED));
|
|
454
|
+
};
|
|
388
455
|
const POOL_STEP = 'pool-outbox';
|
|
389
456
|
/** Whether any string or key in `value` holds one of `terms`, compared lowercased the way the local gate compares a transcript. */
|
|
390
457
|
export const holdsTerm = (value, terms) => {
|
|
@@ -68,7 +68,8 @@ export declare const withSendLock: <T>(outbox: string, run: () => Promise<T>, ti
|
|
|
68
68
|
/** Why an interactive send ended before it finished: a signal reached it, or the process that launched it is gone. */
|
|
69
69
|
export declare class SendInterrupted extends Error {
|
|
70
70
|
readonly kind: 'signal' | 'launcher';
|
|
71
|
-
|
|
71
|
+
readonly signal?: NodeJS.Signals | undefined;
|
|
72
|
+
constructor(kind: 'signal' | 'launcher', message: string, signal?: NodeJS.Signals | undefined);
|
|
72
73
|
}
|
|
73
74
|
/** What a test replaces: the process's signal hooks, its launcher, and how that launcher's liveness is read. */
|
|
74
75
|
export interface InterruptHost {
|
|
@@ -147,9 +147,11 @@ export const withSendLock = async (outbox, run, times = {}, fs = { stat }, wait
|
|
|
147
147
|
/** Why an interactive send ended before it finished: a signal reached it, or the process that launched it is gone. */
|
|
148
148
|
export class SendInterrupted extends Error {
|
|
149
149
|
kind;
|
|
150
|
-
|
|
150
|
+
signal;
|
|
151
|
+
constructor(kind, message, signal) {
|
|
151
152
|
super(message);
|
|
152
153
|
this.kind = kind;
|
|
154
|
+
this.signal = signal;
|
|
153
155
|
this.name = 'SendInterrupted';
|
|
154
156
|
}
|
|
155
157
|
}
|
|
@@ -164,7 +166,7 @@ export const watchInterrupt = (host = { on: (event, listener) => process.on(even
|
|
|
164
166
|
const listeners = SIGNALS.map((name) => [
|
|
165
167
|
name,
|
|
166
168
|
() => {
|
|
167
|
-
controller.abort(new SendInterrupted('signal', `Interrupted by ${name}
|
|
169
|
+
controller.abort(new SendInterrupted('signal', `Interrupted by ${name}.`, name));
|
|
168
170
|
},
|
|
169
171
|
]);
|
|
170
172
|
for (const [name, listener] of listeners)
|
package/dist/sessions/pool.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
16
|
-
|
|
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>;
|
package/dist/sessions/pool.js
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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,
|
|
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 }) =>
|
|
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
|
|
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
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
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)
|