Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/types/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export interface MessageStoreView {
get(id: MessageId): StoredMessage | null;
/**
* A message's sequence, as `get(id)?.sequence` answers it, without reading
* the message (optional — MessageStore provides it; a reader without it
* falls back to `get`).
* the message where the view can (optional — a reader without it falls
* back to `get`). MessageStore answers from its listing while that is
* cached, as it is during a compile, and reads the item otherwise; a merged
* view asks each source's, and the source's `get` where it has none.
*/
sequenceOf?(id: MessageId): number | undefined;
/** Get messages from a specific index */
Expand Down
78 changes: 78 additions & 0 deletions test/compile-provenance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,84 @@ describe('attribution reads what the raw entries name, not the stored history',
rmSync(path, { recursive: true, force: true });
}
});

it('under a view filter, after a write, a compile reads no stored message to place the runs the filter breaks (#273)', async () => {
const { ContextManager, PassthroughStrategy, MessageStore } = await import('../src/index.js');
const { rmSync } = await import('node:fs');
// Hiding every other message starts a run at each shown one. Attribution
// then reads the merged view before the filter, and a resident writes
// before each compile.
const path = './test-attribution-lookups-filtered';
rmSync(path, { recursive: true, force: true });
const proto = MessageStore.prototype as unknown as { getInternal(index: number): unknown };
const getInternal = proto.getInternal;
let reads = 0;
try {
const cm = await ContextManager.open({ path, strategy: new PassthroughStrategy(), viewFilter: (m) => !m.metadata?.tuneOut });
const turn = (i: number) => {
cm.addMessage('bob', [text(`hidden ${i}`)], { tuneOut: { epochId: `e${i}` } });
return cm.addMessage('alice', [text(`shown ${i}`)]);
};
const shown = Array.from({ length: 20 }, (_, i) => turn(i));
await cm.compile();
shown.push(turn(20));
proto.getInternal = function (this: unknown, index: number) {
reads++;
return getInternal.call(this, index);
};
const result = await cm.compile();
proto.getInternal = getInternal;
const raw = result.provenance!.messages.map((m) => (m.kind === 'raw' ? m.bodies.map((b) => [b.messageId, b.complete]) : null));
assert.deepEqual(raw, shown.map((id) => [[id, true]]), 'each shown message is its own complete body');
assert.equal(reads, 0, `compile read ${reads} stored messages one at a time`);
cm.close();
} finally {
proto.getInternal = getInternal;
rmSync(path, { recursive: true, force: true });
}
});

it('a raw run from an auxiliary slot is placed without reading its message (#273)', async () => {
const { ContextManager, PassthroughStrategy, MessageStore } = await import('../src/index.js');
const { rmSync } = await import('node:fs');
// The hidden message breaks the run, so the second shown one, which only
// the auxiliary slot holds, is placed by its sequence.
const path = './test-attribution-lookups-auxiliary';
rmSync(path, { recursive: true, force: true });
const proto = MessageStore.prototype as unknown as { getInternal(index: number): unknown };
const getInternal = proto.getInternal;
let reads = 0;
try {
const main = await ContextManager.open({ path, strategy: new PassthroughStrategy() });
const side = await ContextManager.open({
store: main.getStore(),
namespace: 'subconscious/reader',
isolate: true,
strategy: new PassthroughStrategy(),
auxiliaryMessageViews: [{}],
viewFilter: (m) => !m.metadata?.tuneOut,
});
const first = main.addMessage('alice', [text('first')]);
main.addMessage('bob', [text('hidden')], { tuneOut: { epochId: 'e1' } });
const second = main.addMessage('alice', [text('second')]);
// The first compile reads the writer's appends into the reader's listing.
await side.compile();
proto.getInternal = function (this: unknown, index: number) {
reads++;
return getInternal.call(this, index);
};
const result = await side.compile();
proto.getInternal = getInternal;
const raw = result.provenance!.messages.map((m) => (m.kind === 'raw' ? m.bodies.map((b) => [b.messageId, b.complete]) : null));
assert.deepEqual(raw, [[[first, true]], [[second, true]]]);
assert.equal(reads, 0, `compile read ${reads} stored messages one at a time`);
side.close();
main.close();
} finally {
proto.getInternal = getInternal;
rmSync(path, { recursive: true, force: true });
}
});
});

describe('a tool result moved by repair is still carried', () => {
Expand Down
64 changes: 61 additions & 3 deletions test/message-store-sequence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@

import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert';
import { rmSync, existsSync } from 'node:fs';
import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
import type { MessageStore } from '../src/index.js';
import { rmSync, existsSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { JsStore } from '@animalabs/chronicle';
import { ContextManager, AutobiographicalStrategy, MessageStore } from '../src/index.js';
import type { ContentBlock } from '@animalabs/membrane';

const TEST_STORE_PATH = './test-message-store-sequence';
Expand Down Expand Up @@ -250,3 +252,59 @@ describe('MessageStore — sequence semantics', () => {
manager.close();
});
});

describe('MessageStore.sequenceOf against the slot itself (#273)', () => {
function openSlot(t: { after(fn: () => void): void }) {
const dir = mkdtempSync(join(tmpdir(), 'cm-sequence-of-'));
const js = JsStore.openOrCreate({ path: join(dir, 'store') });
t.after(() => {
js.close();
rmSync(dir, { recursive: true, force: true });
});
MessageStore.register(js);
// The oracle reads the slot as chronicle holds it, so it shares no index
// or cache with the store under test.
const slotSequence = (id: string) =>
(js.getStateJson('messages') as Array<{ id: string; sequence: number }>).find((m) => m.id === id)?.sequence;
return { js, slotSequence };
}

it("follows a removal that shifts every later message, and a sibling's removal, edit and append", (t) => {
const { js, slotSequence } = openSlot(t);
const reader = new MessageStore(js);
const writer = new MessageStore(js);
const ids = ['a', 'b', 'c', 'd', 'e'].map((x) => reader.append('user', textBlock(x)).id);
const agrees = (when: string, known: string[]) => {
for (const id of known) assert.equal(reader.sequenceOf(id), slotSequence(id), `${id} ${when}`);
};
agrees('after the appends', ids);

reader.remove(ids[0]!);
assert.equal(reader.sequenceOf(ids[0]!), undefined, 'the removed message');
agrees('after a removal that shifts the rest', ids.slice(1));

writer.remove(ids[2]!);
assert.equal(reader.sequenceOf(ids[2]!), undefined, "the sibling's removed message");
agrees("after a sibling's removal", [ids[1]!, ids[3]!, ids[4]!]);

writer.edit(ids[3]!, textBlock('d, edited by a sibling'));
const appended = writer.append('user', textBlock('f')).id;
agrees("after a sibling's edit and append", [ids[1]!, ids[3]!, ids[4]!, appended]);
});

it("reads the item while the listing isn't cached, as on a chronicle without point lookup", (t) => {
const { js, slotSequence } = openSlot(t);
// Without point lookup an append can't write the listing through, so it
// drops the cache, and the next lookup reads the item.
const legacy = new Proxy(js, {
get(target, prop, receiver) {
if (prop === 'getStateItemJson') return undefined;
const v = Reflect.get(target, prop, receiver);
return typeof v === 'function' ? v.bind(target) : v;
},
}) as JsStore;
const messages = new MessageStore(legacy);
const ids = ['a', 'b'].map((x) => messages.append('user', textBlock(x)).id);
for (const id of ids) assert.equal(messages.sequenceOf(id), slotSequence(id), id);
});
});