feat(durable): refine record and query contracts

This commit is contained in:
Mario Zechner 2026-09-18 21:01:46 +02:00
parent b5ef419d5b
commit a16ccd9be8
8 changed files with 562 additions and 199 deletions

View file

@ -72,7 +72,7 @@ type Stroke = { color: string; points: { x: number; y: number }[] };
type CanvasState = { strokes: Stroke[] };
const CanvasDoc = defineDoc<CanvasState>({
id: "app.canvas", version: 1, scope: "session",
kind: "app.canvas", version: 1, scope: "session",
initial: () => ({ strokes: [] }),
// This append-only example adds one stroke per commit.
checkpointWhen: value => value.strokes.length % 100 === 0,
@ -179,29 +179,29 @@ A method's successful commit does not promise every remote callback has run yet.
## 2. Conversation-scoped diff reviews
A **document family** uses one definition for many instances. The logical key is
`(definition, conversationId, instanceId)`; the persisted numeric document ID
identifies an incarnation and is never reused. Initializer input is not identity.
`(kind, conversationId, key)`; the persisted numeric document ID identifies an
incarnation and is never reused. Initializer input is not part of the key.
```ts
type ReviewInput = { path: string; patch: string };
type ReviewComment = { id: string; line: number; text: string };
type ReviewState = ReviewInput & { comments: ReviewComment[] };
const ReviewDoc = defineDocFamily<ReviewState, ReviewInput>({
id: "app.diff-review", version: 1, family: true, scope: "conversation",
kind: "app.diff-review", version: 1, family: true, scope: "conversation",
history: "latest", fork: "current",
initial: input => ({ path: input.path, patch: input.patch, comments: [] }),
checkpointWhen: value => value.comments.length % 50 === 0,
});
interface DiffReviewService {
readonly state: ReplicatedState<ReviewState | null>;
identity(context: Context): Promise<{ conversationId: Id; instanceId: string }>;
identity(context: Context): Promise<{ conversationId: Id; key: string }>;
addComment(comment: ReviewComment, context: Context): Promise<void>;
}
const DiffReviews = defineService<DiffReviewService>("app.diff-reviews");
function reviewFacet(
session: Session, conversationId: Id,
reviews: readonly { instanceId: string; initial: ReviewInput }[], context: Context,
reviews: readonly { key: string; initial: ReviewInput }[], context: Context,
): Facet {
return defineFacet({
id: "app.diff-reviews/session",
@ -213,10 +213,10 @@ function reviewFacet(
const source = await session.documentSource(ReviewDoc, target, context);
const replica = await documentReplicatedState(source, context);
env.own(() => replica.dispose());
// Chord instance keys route services; they are not persisted document IDs.
instances.spawn(JSON.stringify([conversationId, review.instanceId]), {
// Chord instance keys route services; they are not numeric document incarnation IDs.
instances.spawn(JSON.stringify([conversationId, review.key]), {
state: replica.state,
async identity() { return { conversationId, instanceId: review.instanceId }; },
async identity() { return { conversationId, key: review.key }; },
async addComment(comment, context) {
await session.commit(async tx => {
const draft = await tx.doc(ReviewDoc, target);
@ -231,8 +231,8 @@ function reviewFacet(
}
```
Example input: `[{ instanceId: "review-7", initial: { path: "a.ts", patch: "-old\n+new" } }]`.
Pass distinct instance IDs and a real conversation ID; install the facet as above.
Example input: `[{ key: "review-7", initial: { path: "a.ts", patch: "-old\n+new" } }]`.
Pass distinct family keys and a real conversation ID; install the facet as above.
Keyed consumers use `env.observe(DiffReviews, handler)`, not `env.use`. The handler
receives `(review, context)`; call `review.identity(context)` to identify it and
`review.state.subscribe` to observe comments.
@ -240,7 +240,7 @@ receives `(review, context)`; call `review.identity(context)` to identify it and
These comments are conversation-scoped: ending the generating task or unloading
the facet does not retire them. `current` copies every logically
present review at fork-commit time into independent child instances with new
numeric IDs, retaining family instance IDs. Parent and child then diverge.
numeric IDs, retaining family keys. Parent and child then diverge.
```text
parent review-7: comment A -> transcript entry E -> comment B
@ -251,7 +251,7 @@ child adds C: parent still contains only A and B
With `fork: "initial"`, no instance copies; first child access uses supplied input.
`latest` cannot read history or fork `asOf`; use `history: "rewindable"` with `asOf`
to reflect E's commit. Reaccess ignores `initial`; updating the patch requires
an explicit mutation or a distinct review instance ID.
an explicit mutation or a distinct review key.
## 3. Task-scoped output and a tool/task watch
@ -262,7 +262,7 @@ it task lifetime; no history, fork, owner, or conversation setting is needed.
type JobInput = { command: string };
type JobOutput = { stdout: string; chunks: number };
const JobOutputDoc = defineDoc<JobOutput>({
id: "app.job-output", version: 1, scope: "task",
kind: "app.job-output", version: 1, scope: "task",
initial: () => ({ stdout: "", chunks: 0 }),
checkpointWhen: value => value.chunks % 100 === 0,
});
@ -331,7 +331,7 @@ task-scoped family only when one task needs several independently keyed document
`ConversationView` contains `conversation`, raw transcript `entries`, and
`docs: Readonly<Record<string, JsonObject>>`. Selected built-in singletons mount
under `docs` by stable definition ID, not numeric incarnation ID. The spec's
under `docs` by stable document kind, not numeric incarnation ID. The spec's
illustrative path mapping is:
```text
@ -339,7 +339,7 @@ document ["s", ["message"], value]
-> view ["s", ["docs", "pi.live", "message"], value]
```
Built-in IDs/fields await approval; `pi.live` is illustrative, not an available API.
Built-in kinds and fields await approval; `pi.live` is illustrative, not an available API.
The mount publishes one batch per complete Session commit: entry/head changes and
changed mounted documents together, without a tracker or semantic projection.
Third-party documents are **not automatically mounted**; use their own Chord
@ -374,8 +374,8 @@ nothing and poisons the open Session; close and reopen it instead of continuing.
as an audit log.
- A listener may call `stop()`, but must not await its own `closed` promise.
- Definitions own checkpoints, not storage heuristics. Revise the counting predicates
above if mutations change. Keep schema IDs, versions, fork policies, and public
paths stable; schema changes require migration, not a source-only rename.
above if mutations change. Keep document kinds, versions, fork policies, and
public paths stable; schema changes require migration, not a source-only rename.
Chord sources: [types](../../chord/src/types.ts), [facet examples](../../chord/test/facets.test.ts),
[state](../../chord/src/services/state.ts), [provider](../../chord/src/services/provider.ts),

View file

@ -17,7 +17,7 @@ facades, membranes, document routing, view projection, events, or clone chains.
Implement IDs, sequences, reserved root conversation ID `1`,
`ConversationRecord`, `EntryRecord`, tagged inputs, live/terminal `TaskRecord`
values, document metadata, storage writes, backend-opaque JSON cursors, and
values, document lifecycle records, storage writes, backend-opaque JSON cursors, and
detached `MemoryStorage` tables.
Reserve `Conversation` for the public conversation object, `Entry` for the typed
entry definition, and `Task` for the typed executable definition returned by
@ -77,8 +77,8 @@ unload/reload.
## 7. Document definitions and access
Implement `defineDoc`, `defineDocFamily`, identity validation, the three direct
scopes, and get-or-create `tx.doc`, `snapshot`, and `documentSource` acquisition.
Implement `defineDoc`, `defineDocFamily`, document kind/key validation, the three
direct scopes, and get-or-create `tx.doc`, `snapshot`, and `documentSource` acquisition.
Test concurrent initialization once, initial bases, detached snapshots, family
initializer use only on first creation, scope/target mismatch, terminal-task

View file

@ -103,9 +103,10 @@ order yields the effective prompt and tool set.
```ts
type ContextEdit = {
readonly target: Id;
readonly action: "omit" | "replace";
readonly messages?: readonly Message[];
};
} & (
| { readonly action: "omit"; readonly messages?: never }
| { readonly action: "replace"; readonly messages: readonly Message[] }
);
type EntryRecord = {
readonly id: Id;
@ -265,7 +266,7 @@ interface InputHandle {
}
type SettledTask<R> = TaskRecord<JsonValue, JsonValue, R> & {
readonly state: TerminalTask<R>;
readonly state: Extract<TaskState<JsonValue, R>, { status: "terminal" }>;
};
type ConversationWatch = WatchHandle<ConversationView>;
@ -531,7 +532,7 @@ type DocumentSemantics =
| { readonly scope: "task" };
type CommonDocDefinition<T extends JsonObject> = {
readonly id: string;
readonly kind: string;
readonly version: number;
initial(): T;
migrate?(value: JsonObject, fromVersion: number): T;
@ -588,7 +589,7 @@ type AnyDocToken = DocToken<JsonObject> | DocFamilyToken<JsonObject, JsonValue>;
Validation rules:
- Definition IDs are unique among loaded definitions.
- Document kinds are unique among loaded definitions.
- Versions are positive integers.
- Session documents are current-only and belong to the Session. Closing and
reopening the Session does not retire them.
@ -605,16 +606,17 @@ Concrete built-in document grouping and semantics are declared when the built-in
definitions are implemented. The generic document mechanism does not special
case model, tool, inbox, or presentation state.
### 3.2 Identity
### 3.2 Records and lifetimes
A persisted document instance has:
A persisted document instance has one lifecycle record:
```ts
type DocumentIdentity = {
readonly id: Id; // unique incarnation
readonly definitionId: string;
readonly definitionVersion: number;
readonly instanceId?: string; // families only
type DocumentRecord = {
readonly id: Id; // unique incarnation
readonly kind: string; // registered definition kind
readonly key?: string; // families only
readonly createdAt: Seq; // stamped by the committing storage
readonly retiredAt?: Seq;
} & (
| { readonly scope: { readonly kind: "session" } }
| ({ readonly scope: { readonly kind: "conversation"; readonly conversationId: Id } } & (
@ -627,19 +629,23 @@ type DocumentIdentity = {
| { readonly scope: { readonly kind: "task"; readonly taskId: Id } }
);
type DocumentMetadata = DocumentIdentity & {
readonly createdAt: Seq; // stamped by the committing storage
readonly retiredAt?: Seq;
};
type DocumentCreate = DocumentRecord extends infer Record
? Record extends DocumentRecord
? Omit<Record, "createdAt" | "retiredAt">
: never
: never;
```
`id` is never reused. Retiring and recreating the same logical definition,
scope, and instance ID creates a new incarnation.
`id` is never reused. Retiring and recreating the same logical kind, scope, and
family key creates a new incarnation.
A singleton is identified logically by definition and scope. A family is
identified logically by definition, scope, and `instanceId`. Persisted metadata
records the scope and conversation history/fork semantics so absent plugin code
does not make existing data disappear.
A singleton is identified logically by kind and scope. A family is identified
logically by kind, scope, and `key`. The record preserves scope and conversation
history/fork semantics so absent plugin code does not make existing data
disappear. Definition versions belong to stored bases and deltas because one
incarnation may contain records written by multiple definition versions.
`DocumentCreate` is not another persisted record; it is the same scoped union
without storage-assigned lifetime fields.
### 3.3 Access and creation
@ -721,14 +727,14 @@ type DocTarget =
| { readonly scope: "conversation"; readonly conversationId: Id }
| { readonly scope: "task"; readonly taskId: Id };
type FamilyKey = DocTarget & { readonly instanceId: string };
type FamilyKey = DocTarget & { readonly key: string };
type FamilyTarget<I> = FamilyKey & { readonly initial: I };
type HistoricalFamilyTarget = {
readonly scope: "conversation";
readonly conversationId: Id;
readonly instanceId: string;
readonly key: string;
};
```
@ -941,11 +947,6 @@ Storage ownership:
### 5.1 Definition
```ts
type LiveTask<S> = {
readonly status: "pending" | "running";
readonly checkpoint: S;
};
type TaskOutcome<R> =
| { readonly status: "completed"; readonly result: R }
| { readonly status: "failed"; readonly error: StoredError; readonly result?: R }
@ -953,10 +954,10 @@ type TaskOutcome<R> =
| { readonly status: "orphaned"; readonly reason: string }
| { readonly status: "faulted"; readonly error: StoredError };
type TerminalTask<R> = {
readonly status: "terminal";
readonly outcome: TaskOutcome<R>;
};
type TaskState<S, R> =
| { readonly status: "pending"; readonly checkpoint: S }
| { readonly status: "running"; readonly checkpoint: S }
| { readonly status: "terminal"; readonly outcome: TaskOutcome<R> };
type TaskRecord<I, S, R> = {
readonly id: Id;
@ -964,15 +965,22 @@ type TaskRecord<I, S, R> = {
readonly kind: string;
readonly version: number;
readonly input: I;
readonly state: LiveTask<S> | TerminalTask<R>;
readonly after: readonly Id[];
readonly background: boolean;
readonly abortRequested: boolean;
readonly memos?: Readonly<Record<string, JsonValue>>; // live tasks only
};
} & (
| {
readonly state: Extract<TaskState<S, R>, { status: "pending" | "running" }>;
readonly memos?: Readonly<Record<string, JsonValue>>;
}
| {
readonly state: Extract<TaskState<S, R>, { status: "terminal" }>;
readonly memos?: never;
}
);
type RunningTask<I, S, R> = TaskRecord<I, S, R> & {
readonly state: LiveTask<S> & { readonly status: "running" };
readonly state: Extract<TaskState<S, R>, { status: "running" }>;
};
interface HookRunner<H extends object> {
@ -1782,9 +1790,8 @@ type Cursor = Readonly<Record<string, JsonValue>>;
type EntryQuery = {
readonly conversationId: Id;
readonly before?: Id; // strict semantic cutoff, independent of page cursor
readonly kind?: string;
readonly withHead?: boolean;
readonly minEntryId?: Id; // inclusive
readonly maxEntryId?: Id; // inclusive
};
type TaskQuery = {
@ -1796,9 +1803,9 @@ type TaskQuery = {
};
type DocumentQuery = {
readonly definitionId?: string;
readonly scope?: DocumentIdentity["scope"];
readonly instanceId?: string;
readonly kind?: string;
readonly scope?: DocumentRecord["scope"];
readonly key?: string;
readonly at?: Seq | "current";
readonly includeRetired?: boolean;
};
@ -1808,7 +1815,7 @@ type StoredDocumentRecord =
| { readonly seq: Seq; readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
type StoredDocument = {
readonly metadata: DocumentMetadata;
readonly record: DocumentRecord;
readonly records: readonly [
Extract<StoredDocumentRecord, { kind: "base" }>,
...Extract<StoredDocumentRecord, { kind: "delta" }>[],
@ -1822,12 +1829,13 @@ type StorageWrite =
| { readonly type: "input"; readonly value: Input }
| {
readonly type: "document.create";
readonly identity: DocumentIdentity;
readonly record: DocumentCreate;
readonly version: number;
readonly value: JsonObject; // always a base; storage stamps createdAt
}
| {
readonly type: "document.change";
readonly metadata: DocumentMetadata;
readonly record: DocumentRecord;
readonly definition: AnyDocToken;
readonly ops: readonly Op[];
readonly value: JsonObject;
@ -1843,9 +1851,9 @@ interface Storage {
conversation(id: Id, context: Context): Promise<ConversationRecord | undefined>;
scanConversations(cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<ConversationRecord, Cursor>>;
entries(ids: readonly Id[], context: Context): Promise<ReadonlyMap<Id, EntryRecord>>;
entry(id: Id, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
findLatestHeadMarker(conversationId: Id, atOrBeforeEntryId: Id | undefined, context: Context): Promise<(EntryRecord & { readonly head: Id }) | undefined>;
scanEntries(query: EntryQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<EntryRecord, Cursor>>;
entryCommit(id: Id, context: Context): Promise<Seq | undefined>;
task(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
scanTasks(query: TaskQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
@ -1854,7 +1862,7 @@ interface Storage {
inputByRequest(conversationId: Id, requestId: string, context: Context): Promise<Input | undefined>;
document(id: Id, at: Seq | "current", context: Context): Promise<StoredDocument | undefined>;
scanDocuments(query: DocumentQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<DocumentMetadata, Cursor>>;
scanDocuments(query: DocumentQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<DocumentRecord, Cursor>>;
close(context: Context): Promise<void>;
}
@ -1865,9 +1873,17 @@ scan on the same storage; cross-storage or cross-query use is unsupported. The
Session owns the mutation line, so storage implementations do not add a second
caller-facing commit mutex. Each backend still makes one admitted batch atomic.
`EntryQuery` supports conversation ancestry, a strict `before` entry cursor,
kind filtering, and `withHead`. Document queries support definition, scope,
instance, current/as-of membership, and retired membership. Task queries support
`findLatestHeadMarker()` returns the newest visible entry carrying `head` at or
below its optional inclusive cutoff. The returned entry is the marker; its
`head` value is the actual lower bound for context. `scanEntries()` pages the
inclusive ID range in newest-first order while applying every conversation
ancestry cap. With no bounds it pages complete visible history. To read context
through entry `E`, find the marker at or before `E`, then scan from
`marker?.head` through `E`. For current context the upper bound is omitted.
`entry()` combines exact global lookup with the commit sequence required by
historical document reads. `limit` is always the maximum page size.
Document queries support kind, scope, family key, current/as-of membership, and
retired membership. Task queries support
conversation, kind, live/terminal
status, abort mark, and background status.
@ -1889,7 +1905,7 @@ only after a committed base or retirement.
One SQL transaction is one Session commit. SQLite stores:
- conversation, entry, task, and input records;
- document metadata;
- document lifecycle records;
- indexed document bases/deltas by document and commit sequence.
Live task transitions replace one row. Terminal tasks remain as small records.
@ -1959,8 +1975,8 @@ These are contracts, not invitations to add defensive machinery:
live.
- **Wrong fork setting:** `current`, `initial`, and `asOf` are product semantics,
not optimizations. Changing one changes child conversation behavior.
- **Schema stability:** definition IDs, mounted document IDs, and visible paths
are persisted/public protocol. Value migration cannot rename an ID; an ID
- **Schema stability:** document kinds and visible mount paths are
persisted/public protocol. Value migration cannot rename a kind; a kind
change requires explicit copy and retirement.
- **Watch activation:** `watch.value` is the immutable acquisition snapshot.
Initialize the consumer from it before `start()`; queued operations are based

View file

@ -3,14 +3,13 @@ export type {
ContextEdit,
ConversationRecord,
Cursor,
DocumentIdentity,
DocumentMetadata,
DocumentCreate,
DocumentRecord,
EntryDraft,
EntryQuery,
EntryRecord,
Id,
Input,
LiveTask,
Page,
Seq,
Storage,
@ -19,6 +18,6 @@ export type {
TaskOutcome,
TaskQuery,
TaskRecord,
TerminalTask,
TaskState,
} from "./types.ts";
export { ROOT_CONVERSATION_ID } from "./types.ts";

View file

@ -20,7 +20,8 @@ type TableName = "conversation" | "entry" | "task" | "input";
type State = {
conversations: Map<Id, ConversationRecord>;
entries: Map<Id, EntryRecord>;
entryCommits: Map<Id, Seq>;
entryIds: Map<Id, Id[]>;
entryCommitSeqs: Map<Id, Seq>;
tasks: Map<Id, StoredTask>;
inputs: Map<Id, Input>;
};
@ -69,7 +70,8 @@ export class MemoryStorage implements Storage {
private readonly state: State = {
conversations: new Map(),
entries: new Map(),
entryCommits: new Map(),
entryIds: new Map(),
entryCommitSeqs: new Map(),
tasks: new Map(),
inputs: new Map(),
};
@ -88,10 +90,24 @@ export class MemoryStorage implements Storage {
case "conversation":
this.state.conversations.set(write.value.id, write.value);
break;
case "entry":
case "entry": {
this.state.entries.set(write.value.id, write.value);
this.state.entryCommits.set(write.value.id, seq);
this.state.entryCommitSeqs.set(write.value.id, seq);
let ids = this.state.entryIds.get(write.value.conversationId);
if (ids === undefined) {
ids = [];
this.state.entryIds.set(write.value.conversationId, ids);
}
let low = 0;
let high = ids.length;
while (low < high) {
const middle = (low + high) >>> 1;
if (ids[middle] < write.value.id) low = middle + 1;
else high = middle;
}
ids.splice(low, 0, write.value.id);
break;
}
case "task":
this.state.tasks.set(write.value.id, write.value);
break;
@ -130,14 +146,26 @@ export class MemoryStorage implements Storage {
return page(values, limit);
}
async entries(ids: readonly Id[], _context: Context): Promise<ReadonlyMap<Id, EntryRecord>> {
async entry(
id: Id,
_context: Context,
): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined> {
this.assertOpen();
const result = new Map<Id, EntryRecord>();
for (const id of ids) {
const value = this.state.entries.get(id);
if (value !== undefined) result.set(id, clone(value));
const entry = this.state.entries.get(id);
if (entry === undefined) return undefined;
return { entry: clone(entry), commitSeq: this.state.entryCommitSeqs.get(id)! };
}
async findLatestHeadMarker(
conversationId: Id,
atOrBeforeEntryId: Id | undefined,
_context: Context,
): Promise<(EntryRecord & { readonly head: Id }) | undefined> {
this.assertOpen();
for (const entry of this.visibleEntries(conversationId, undefined, atOrBeforeEntryId)) {
if (entry.head !== undefined) return clone({ ...entry, head: entry.head });
}
return result;
return undefined;
}
async scanEntries(
@ -147,39 +175,17 @@ export class MemoryStorage implements Storage {
_context: Context,
): Promise<Page<EntryRecord, Cursor>> {
this.assertOpen();
if (!this.state.conversations.has(query.conversationId)) {
throw new Error(`Unknown conversation: ${query.conversationId}`);
}
const beforeCursor = cursorId(cursor);
const after = cursorId(cursor);
const maxEntryId =
after === undefined ? query.maxEntryId : Math.min(query.maxEntryId ?? Number.POSITIVE_INFINITY, after - 1);
const visible: EntryRecord[] = [];
let currentId = query.conversationId;
let upperEntryId = Number.POSITIVE_INFINITY;
const visited = new Set<Id>();
while (true) {
if (visited.has(currentId)) throw new Error("Conversation parent cycle");
visited.add(currentId);
for (const entry of this.state.entries.values()) {
if (entry.conversationId !== currentId || entry.id > upperEntryId) continue;
if (query.before !== undefined && entry.id >= query.before) continue;
if (beforeCursor !== undefined && entry.id >= beforeCursor) continue;
if (query.kind !== undefined && entry.kind !== query.kind) continue;
if (query.withHead === true && entry.head === undefined) continue;
visible.push(entry);
}
const conversation = this.state.conversations.get(currentId)!;
if (conversation.parent === undefined) break;
upperEntryId = Math.min(upperEntryId, conversation.parent.at);
currentId = conversation.parent.conversationId;
for (const entry of this.visibleEntries(query.conversationId, query.minEntryId, maxEntryId)) {
visible.push(entry);
if (visible.length > limit) break;
}
visible.sort((left, right) => right.id - left.id);
return page(visible, limit);
}
async entryCommit(id: Id, _context: Context): Promise<Seq | undefined> {
this.assertOpen();
return this.state.entryCommits.get(id);
}
async task(id: Id, _context: Context): Promise<StoredTask | undefined> {
this.assertOpen();
const value = this.state.tasks.get(id);
@ -223,6 +229,41 @@ export class MemoryStorage implements Storage {
this.closed = true;
}
private *visibleEntries(
conversationId: Id,
minEntryId = Number.NEGATIVE_INFINITY,
maxEntryId = Number.POSITIVE_INFINITY,
): Generator<EntryRecord> {
if (!this.state.conversations.has(conversationId)) {
throw new Error(`Unknown conversation: ${conversationId}`);
}
let currentId = conversationId;
let upperEntryId = maxEntryId;
const visited = new Set<Id>();
while (true) {
if (visited.has(currentId)) throw new Error("Conversation parent cycle");
visited.add(currentId);
const ids = this.state.entryIds.get(currentId) ?? [];
let low = 0;
let high = ids.length;
while (low < high) {
const middle = (low + high) >>> 1;
if (ids[middle] <= upperEntryId) low = middle + 1;
else high = middle;
}
for (let index = low - 1; index >= 0; index--) {
const id = ids[index];
if (id < minEntryId) break;
yield this.state.entries.get(id)!;
}
const conversation = this.state.conversations.get(currentId)!;
if (conversation.parent === undefined) break;
upperEntryId = Math.min(upperEntryId, conversation.parent.at);
if (upperEntryId < minEntryId) break;
currentId = conversation.parent.conversationId;
}
}
private checkImmutableIds(writes: readonly StorageWrite[]): void {
const claimed = new Map<Id, TableName>();
for (const write of writes) {

View file

@ -1,66 +1,90 @@
import type { Context, JsonValue } from "@earendil-works/chord";
import type { Message } from "@earendil-works/pi-ai";
/** Session-global durable record identifier. */
/** Session-global identifier shared by every durable record table. */
export type Id = number;
/** Storage-assigned atomic commit sequence. */
/** Monotonic sequence assigned to one atomic storage commit. */
export type Seq = number;
/** The root conversation always uses this reserved ID. */
export const ROOT_CONVERSATION_ID: Id = 1;
/** JSON-safe error snapshot persisted instead of a runtime `Error` object. */
export type StoredError = {
readonly message: string;
/** Optional structured diagnostic data for inspection or recovery. */
readonly detail?: JsonValue;
};
/** Immutable identity, history ancestry, and task ownership of a transcript scope. */
export type ConversationRecord = {
readonly id: Id;
/** Transcript ancestry and the inclusive entry through which history is inherited. */
/** Fork source and inclusive parent entry through which history is inherited. */
readonly parent?: {
readonly conversationId: Id;
readonly at: Id;
};
/** Durable task-ownership edge used for subtree abort and idle traversal. */
/** Creator edge used for authorization, subtree abort, and subtree idle waits. */
readonly owner?: {
readonly conversationId: Id;
readonly taskId: Id;
};
};
/** An immutable override of one visible entry's contribution to model context. */
export type ContextEdit = {
/** Entry whose model messages are omitted or replaced. */
readonly target: Id;
readonly action: "omit" | "replace";
readonly messages?: readonly Message[];
};
} & (
| {
readonly action: "omit";
readonly messages?: never;
}
| {
readonly action: "replace";
/** Messages contributed instead of the target entry's model messages. */
readonly messages: readonly Message[];
}
);
/** Immutable transcript event with separate model-facing and application-facing payloads. */
export type EntryRecord = {
readonly id: Id;
readonly conversationId: Id;
/** Application-defined entry discriminator. */
readonly kind: string;
/** Messages contributed to model context; absent for display or bookkeeping entries. */
readonly model?: readonly Message[];
/** JSON payload consumed by views, plugins, or bookkeeping logic. */
readonly data?: JsonValue;
/** Entry at which the active context begins. */
/** First entry in the active context selected by this entry. */
readonly head?: Id;
/** Context-only overrides of earlier visible entries. */
readonly edits?: readonly ContextEdit[];
/** Task that appended this entry, when it was produced by durable work. */
readonly byTaskId?: Id;
};
/** Entry content supplied before the Session assigns identity and task attribution. */
export type EntryDraft = Omit<EntryRecord, "id" | "conversationId" | "byTaskId" | "head"> & {
/** `"self"` starts active context at the newly assigned entry ID. */
readonly head?: Id | "self";
};
/** Identity fields shared by every input lifecycle state. */
type InputBase = {
readonly id: Id;
readonly conversationId: Id;
/** Host-provided deduplication key, scoped to the conversation. */
readonly requestId?: string;
};
/** Durable admission and settlement state for one host input. */
/** Durable lifecycle of one admitted host input and the handle waiting for it. */
export type Input = InputBase &
(
| {
/** Admitted but not yet represented in the transcript. */
readonly status: "queued";
readonly entry?: never;
readonly answer?: never;
@ -68,6 +92,7 @@ export type Input = InputBase &
readonly detail?: never;
}
| {
/** Added to the transcript and owned by an active turn. */
readonly status: "placed";
/** User transcript entry created when this input was placed. */
readonly entry: Id;
@ -76,6 +101,7 @@ export type Input = InputBase &
readonly detail?: never;
}
| {
/** Successfully settled with a durable transcript result. */
readonly status: "done";
/** User or passive-write transcript entry created when this input was placed. */
readonly entry: Id;
@ -85,87 +111,175 @@ export type Input = InputBase &
readonly detail?: never;
}
| {
/** Terminal input that can no longer receive an answer. */
readonly status: "unanswered";
/** Present when the input was placed before becoming unanswered. */
readonly entry?: Id;
readonly answer?: never;
/** Stable machine-readable explanation such as `"aborted"` or `"stale"`. */
readonly reason: string;
/** Optional structured diagnostic data. */
readonly detail?: JsonValue;
}
);
export type LiveTask<S> = {
readonly status: "pending" | "running";
readonly checkpoint: S;
};
/** Durable reason and optional result recorded when a task becomes terminal. */
export type TaskOutcome<R> =
| { readonly status: "completed"; readonly result: R }
/** Expected task/domain failure explicitly committed by its implementation. */
| { readonly status: "failed"; readonly error: StoredError; readonly result?: R }
| { readonly status: "aborted"; readonly reason?: string; readonly result?: R }
| { readonly status: "orphaned"; readonly reason: string }
/** Harness-detected contract failure, such as an uncaught throw or no durable progress. */
| { readonly status: "faulted"; readonly error: StoredError };
| {
readonly status: "completed";
readonly result: R;
readonly error?: never;
readonly reason?: never;
}
/** Expected task or domain failure explicitly committed by its implementation. */
| {
readonly status: "failed";
readonly error: StoredError;
readonly result?: R;
readonly reason?: never;
}
/** Explicit cancellation handled by the task's abort protocol. */
| {
readonly status: "aborted";
readonly reason?: string;
readonly result?: R;
readonly error?: never;
}
/** Task that cannot resume because its definition or migration is unavailable. */
| {
readonly status: "orphaned";
readonly reason: string;
readonly result?: never;
readonly error?: never;
}
/** Runtime-detected contract failure, such as an uncaught throw or no durable progress. */
| {
readonly status: "faulted";
readonly error: StoredError;
readonly result?: never;
readonly reason?: never;
};
export type TerminalTask<R> = {
readonly status: "terminal";
readonly outcome: TaskOutcome<R>;
};
/** Complete durable execution state of a task. */
export type TaskState<S, R> =
| {
/** Eligible for scheduling when its dependencies are terminal. */
readonly status: "pending";
/** Complete durable state from which execution resumes. */
readonly checkpoint: S;
readonly outcome?: never;
}
| {
/** Reserved by one in-memory task invocation. */
readonly status: "running";
/** Complete durable state from which execution resumes. */
readonly checkpoint: S;
readonly outcome?: never;
}
| {
/** Permanently settled durable result receipt. */
readonly status: "terminal";
readonly checkpoint?: never;
readonly outcome: TaskOutcome<R>;
};
export type TaskRecord<I, S, R> = {
/** Identity, definition, and scheduling fields shared by every task state. */
type TaskRecordBase<I> = {
readonly id: Id;
readonly conversationId: Id;
/** Registered task definition name. */
readonly kind: string;
/** Definition version used to migrate live input and checkpoints. */
readonly version: number;
/** Original task input retained while the task is live or terminal. */
readonly input: I;
readonly state: LiveTask<S> | TerminalTask<R>;
/** Tasks that must be terminal before ordinary execution may begin. */
readonly after: readonly Id[];
/** Whether this task is excluded from ordinary idle waits and conversation aborts. */
readonly background: boolean;
/** Durable abort mark checked before run-mode progress is committed. */
readonly abortRequested: boolean;
readonly memos?: Readonly<Record<string, JsonValue>>;
};
/** Persisted identity of one durable document incarnation. */
export type DocumentIdentity = {
/** Complete replacement record for one durable task state machine. */
export type TaskRecord<I, S, R> = TaskRecordBase<I> &
(
| {
readonly state: Extract<TaskState<S, R>, { readonly status: "pending" | "running" }>;
/** Small first-writer-wins values retained while the task is live. */
readonly memos?: Readonly<Record<string, JsonValue>>;
}
| {
readonly state: Extract<TaskState<S, R>, { readonly status: "terminal" }>;
readonly memos?: never;
}
);
/** Persisted lifecycle record for one create-to-retire document incarnation. */
export type DocumentRecord = {
/** Unique incarnation ID; never reused when the same logical document is recreated. */
readonly id: Id;
readonly definitionId: string;
readonly definitionVersion: number;
readonly instanceId?: string;
/** Registered document kind. */
readonly kind: string;
/** Family member key; absent for singleton documents. */
readonly key?: string;
/** Commit that created the incarnation, stamped by storage. */
readonly createdAt: Seq;
/** Commit that retired the incarnation; absent while it is current. */
readonly retiredAt?: Seq;
} & (
| { readonly scope: { readonly kind: "session" } }
| {
readonly scope: { readonly kind: "session" };
readonly history?: never;
readonly fork?: never;
}
| ({ readonly scope: { readonly kind: "conversation"; readonly conversationId: Id } } & (
| { readonly history: "latest"; readonly fork: "current" | "initial" }
| {
/** Retain only current state. */
readonly history: "latest";
/** Initialize a fork from current source state or the definition's initial value. */
readonly fork: "current" | "initial";
}
| {
/** Retain history needed for as-of reads. */
readonly history: "rewindable";
/** Initialize a fork at its cutoff, from current state, or from the initial value. */
readonly fork: "asOf" | "current" | "initial";
}
))
| { readonly scope: { readonly kind: "task"; readonly taskId: Id } }
| {
readonly scope: { readonly kind: "task"; readonly taskId: Id };
readonly history?: never;
readonly fork?: never;
}
);
/** Commit-sequence lifetime of one persisted document incarnation. */
export type DocumentMetadata = DocumentIdentity & {
readonly createdAt: Seq;
readonly retiredAt?: Seq;
};
/** Fields supplied when storage creates and stamps a new `DocumentRecord`. */
export type DocumentCreate = DocumentRecord extends infer Record
? Record extends DocumentRecord
? Omit<Record, "createdAt" | "retiredAt">
: never
: never;
/** One ordered scan result and its optional continuation state. */
export type Page<T, C> = {
readonly items: readonly T[];
readonly next?: C;
};
/** Backend-owned, JSON-serializable continuation state. Callers must treat it as opaque. */
/** Backend-owned JSON continuation state that callers only round-trip to the same scan. */
export type Cursor = Readonly<Record<string, JsonValue>>;
/** Inclusive ID bounds for a newest-first scan of one conversation's fork-aware history. */
export type EntryQuery = {
readonly conversationId: Id;
/** Strict semantic cutoff: only entries with IDs less than this value match. */
readonly before?: Id;
readonly kind?: string;
readonly withHead?: boolean;
/** Oldest entry ID that may be returned. */
readonly minEntryId?: Id;
/** Newest entry ID that may be returned. */
readonly maxEntryId?: Id;
};
/** Optional filters for an ordered scan of durable task records. */
export type TaskQuery = {
readonly conversationId?: Id;
readonly kind?: string;
@ -174,39 +288,61 @@ export type TaskQuery = {
readonly background?: boolean;
};
/** Package 1 table writes. Document lifecycle writes are added in Package 2. */
/** One table mutation in an atomic storage commit; task and input writes replace whole records. */
export type StorageWrite =
| { readonly type: "conversation"; readonly value: ConversationRecord }
| { readonly type: "entry"; readonly value: EntryRecord }
| { readonly type: "task"; readonly value: TaskRecord<JsonValue, JsonValue, JsonValue> }
| { readonly type: "input"; readonly value: Input };
/**
* Atomic persistence boundary for Session records.
*
* Implementations return detached values, own cursor encoding, and protect global ID
* uniqueness. The owning Session serializes commits on its mutation line.
*/
export interface Storage {
/**
* Persist one batch atomically and return its commit sequence.
* The owning Session serializes calls on its mutation line; Storage implementations
* do not provide a second caller-facing mutation mutex.
*/
/** Atomically persist one batch and return the sequence assigned to that commit. */
commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
/** Return a fresh candidate from the Session-global record ID namespace. */
mintId(): Id;
/** Look up one conversation by exact ID. */
conversation(id: Id, context: Context): Promise<ConversationRecord | undefined>;
/** Scan conversations in ascending ID order. */
scanConversations(
cursor: Cursor | undefined,
limit: number,
context: Context,
): Promise<Page<ConversationRecord, Cursor>>;
entries(ids: readonly Id[], context: Context): Promise<ReadonlyMap<Id, EntryRecord>>;
/** Look up one global entry and the sequence of the commit that persisted it. */
entry(id: Id, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
/**
* Return the newest visible entry with a `head` at or below the optional inclusive cutoff.
* The returned entry is the marker; its `head` value is the range's actual lower bound.
*/
findLatestHeadMarker(
conversationId: Id,
atOrBeforeEntryId: Id | undefined,
context: Context,
): Promise<(EntryRecord & { readonly head: Id }) | undefined>;
/** Scan the inclusive visible range newest-first, returning at most `limit` entries. */
scanEntries(
query: EntryQuery,
cursor: Cursor | undefined,
limit: number,
context: Context,
): Promise<Page<EntryRecord, Cursor>>;
entryCommit(id: Id, context: Context): Promise<Seq | undefined>;
/** Look up the latest complete record for one task. */
task(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
/** Scan task records matching every supplied filter. */
scanTasks(
query: TaskQuery,
cursor: Cursor | undefined,
@ -214,8 +350,12 @@ export interface Storage {
context: Context,
): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
/** Look up the latest complete record for one admitted input. */
input(id: Id, context: Context): Promise<Input | undefined>;
/** Find an input by its conversation-scoped host deduplication key. */
inputByRequest(conversationId: Id, requestId: string, context: Context): Promise<Input | undefined>;
/** Release backend resources; all later operations must reject. */
close(context: Context): Promise<void>;
}

View file

@ -19,7 +19,7 @@ async function createRoot(storage: MemoryStorage): Promise<Id> {
return ROOT_CONVERSATION_ID;
}
function pendingTask(id: Id, conversationId: Id, phase = "ready"): StoredTask {
function pendingTask(id: Id, conversationId: Id, phase = "ready") {
return {
id,
conversationId,
@ -30,7 +30,7 @@ function pendingTask(id: Id, conversationId: Id, phase = "ready"): StoredTask {
after: [],
background: false,
abortRequested: false,
};
} satisfies StoredTask;
}
function entry(id: Id, conversationId: Id, kind = "message", extra: Partial<EntryRecord> = {}): EntryRecord {
@ -71,9 +71,10 @@ describe("Pico MemoryStorage", () => {
context,
);
expect(await storage.entries([entryId], context)).toEqual(
new Map([[entryId, entry(entryId, rootId, "user", { data: { text: "hello" } })]]),
);
expect(await storage.entry(entryId, context)).toEqual({
entry: entry(entryId, rootId, "user", { data: { text: "hello" } }),
commitSeq: initialSeq,
});
expect(await storage.task(taskId, context)).toEqual(task);
expect(await storage.input(inputId, context)).toEqual(input);
@ -97,8 +98,7 @@ describe("Pico MemoryStorage", () => {
expect(await storage.task(taskId, context)).toEqual(task);
expect(await storage.input(inputId, context)).toEqual(input);
expect(await storage.entries([transientEntryId], context)).toEqual(new Map());
expect(await storage.entryCommit(transientEntryId, context)).toBeUndefined();
expect(await storage.entry(transientEntryId, context)).toBeUndefined();
expect(
await storage.commit([{ type: "entry", value: entry(storage.mintId(), rootId, "after-rollback") }], context),
).toBe(initialSeq + 1);
@ -137,14 +137,14 @@ describe("Pico MemoryStorage", () => {
entryData.nested.push(3);
checkpoint.nested.count = 2;
detail.codes.push("mutated");
expect((await storage.entries([entryId], context)).get(entryId)?.data).toEqual({ nested: [1, 2] });
expect((await storage.entry(entryId, context))?.entry.data).toEqual({ nested: [1, 2] });
expect((await storage.task(taskId, context))?.state).toEqual({
status: "pending",
checkpoint: { phase: "ready", nested: { count: 1 } },
});
expect((await storage.input(inputId, context))?.detail).toEqual({ codes: ["initial"] });
const readEntry = (await storage.entries([entryId], context)).get(entryId)!;
const readEntry = (await storage.entry(entryId, context))!.entry;
(readEntry.data as { nested: number[] }).nested.push(9);
const readTask = (await storage.task(taskId, context))!;
if (readTask.state.status !== "terminal") {
@ -153,7 +153,7 @@ describe("Pico MemoryStorage", () => {
const readInput = (await storage.input(inputId, context))!;
(readInput.detail as { codes: string[] }).codes.push("read mutation");
expect((await storage.entries([entryId], context)).get(entryId)?.data).toEqual({ nested: [1, 2] });
expect((await storage.entry(entryId, context))?.entry.data).toEqual({ nested: [1, 2] });
expect((await storage.task(taskId, context))?.state).toEqual({
status: "pending",
checkpoint: { phase: "ready", nested: { count: 1 } },
@ -253,27 +253,54 @@ describe("Pico MemoryStorage", () => {
expect(third.items.map(({ id }) => id)).toEqual([rootFirst]);
expect(third.next).toBeUndefined();
const currentMarker = await storage.findLatestHeadMarker(grandchildId, undefined, context);
expect(currentMarker?.id).toBe(grandchildHead);
expect(currentMarker?.head).toBe(grandchildHead);
const historicalMarker = await storage.findLatestHeadMarker(grandchildId, childForkPoint, context);
expect(historicalMarker?.id).toBe(rootForkPoint);
expect(historicalMarker?.head).toBe(rootFirst);
expect(await storage.findLatestHeadMarker(grandchildId, rootFirst, context)).toBeUndefined();
const activeFirst = await storage.scanEntries(
{ conversationId: grandchildId, minEntryId: currentMarker?.head },
undefined,
1,
context,
);
expect(activeFirst.items.map(({ id }) => id)).toEqual([grandchildTail]);
expect(activeFirst.next).toBeDefined();
const activeSecond = await storage.scanEntries(
{ conversationId: grandchildId, minEntryId: currentMarker?.head },
activeFirst.next,
1,
context,
);
expect(activeSecond.items.map(({ id }) => id)).toEqual([grandchildHead]);
expect(activeSecond.next).toBeUndefined();
expect(
(
await storage.scanEntries({ conversationId: grandchildId, before: grandchildHead }, undefined, 10, context)
await storage.scanEntries(
{
conversationId: grandchildId,
minEntryId: historicalMarker?.head,
maxEntryId: childForkPoint,
},
undefined,
10,
context,
)
).items.map(({ id }) => id),
).toEqual([childForkPoint, rootForkPoint, rootFirst]);
expect(
(
await storage.scanEntries({ conversationId: grandchildId, kind: "marker" }, undefined, 10, context)
).items.map(({ id }) => id),
).toEqual([grandchildHead, rootForkPoint]);
expect(
(
await storage.scanEntries({ conversationId: grandchildId, withHead: true }, undefined, 10, context)
).items.map(({ id }) => id),
).toEqual([grandchildHead, rootForkPoint]);
expect(await storage.entryCommit(rootFirst, context)).toBe(rootEntriesSeq);
expect(await storage.entryCommit(rootForkPoint, context)).toBe(rootEntriesSeq);
expect(await storage.entryCommit(grandchildHead, context)).toBe(grandchildEntriesSeq);
expect(await storage.entryCommit(grandchildTail, context)).toBe(grandchildEntriesSeq);
expect(await storage.entryCommit(999_999, context)).toBeUndefined();
expect(await storage.entry(rootFirst, context)).toEqual({
entry: entry(rootFirst, rootId),
commitSeq: rootEntriesSeq,
});
expect((await storage.entry(rootForkPoint, context))?.commitSeq).toBe(rootEntriesSeq);
expect((await storage.entry(grandchildHead, context))?.commitSeq).toBe(grandchildEntriesSeq);
expect((await storage.entry(grandchildTail, context))?.commitSeq).toBe(grandchildEntriesSeq);
expect(await storage.entry(999_999, context)).toBeUndefined();
await expect(storage.scanEntries({ conversationId: 999_999 }, undefined, 10, context)).rejects.toThrow(
"Unknown conversation",
);

View file

@ -0,0 +1,140 @@
import { expectTypeOf, it } from "vitest";
import type {
ContextEdit,
DocumentCreate,
DocumentRecord,
Input,
TaskOutcome,
TaskRecord,
TaskState,
} from "../src/index.ts";
it("encodes discriminator-dependent fields", () => {
const omit = { target: 1, action: "omit" } satisfies ContextEdit;
const replace = { target: 1, action: "replace", messages: [] } satisfies ContextEdit;
const pending = { status: "pending", checkpoint: { phase: "ready" } } satisfies TaskState<
{ phase: string },
{ value: number }
>;
const terminal = {
status: "terminal",
outcome: { status: "completed", result: { value: 1 } },
} satisfies TaskState<{ phase: string }, { value: number }>;
const conversationDocument = {
id: 1,
kind: "test",
scope: { kind: "conversation", conversationId: 1 },
history: "rewindable",
fork: "asOf",
} satisfies DocumentCreate;
expectTypeOf(omit.action).toEqualTypeOf<"omit">();
expectTypeOf(replace.action).toEqualTypeOf<"replace">();
expectTypeOf(pending.status).toEqualTypeOf<"pending">();
expectTypeOf(terminal.status).toEqualTypeOf<"terminal">();
expectTypeOf(conversationDocument.fork).toEqualTypeOf<"asOf">();
const compileTimeFailures = () => {
// @ts-expect-error replacement edits require replacement messages
const missingReplacement: ContextEdit = { target: 1, action: "replace" };
// @ts-expect-error omission edits cannot carry replacement messages
const omissionWithMessages: ContextEdit = { target: 1, action: "omit", messages: [] };
const pendingWithOutcome: TaskState<{ phase: string }, number> = {
status: "pending",
checkpoint: { phase: "ready" },
// @ts-expect-error live task state cannot carry a terminal outcome
outcome: { status: "completed", result: 1 },
};
// @ts-expect-error terminal task state cannot retain a live checkpoint
const terminalWithCheckpoint: TaskState<{ phase: string }, number> = {
status: "terminal",
checkpoint: { phase: "ready" },
outcome: { status: "completed", result: 1 },
};
// @ts-expect-error terminal task records cannot retain live memos
const terminalWithMemos: TaskRecord<null, { phase: string }, number> = {
id: 1,
conversationId: 1,
kind: "test",
version: 1,
input: null,
state: { status: "terminal", outcome: { status: "completed", result: 1 } },
after: [],
background: false,
abortRequested: false,
memos: { retained: true },
};
// @ts-expect-error session documents do not declare conversation history behavior
const sessionWithHistory: DocumentRecord = {
id: 1,
kind: "test",
createdAt: 1,
scope: { kind: "session" },
history: "latest",
fork: "current",
};
// @ts-expect-error conversation document creation requires history and fork policies
const conversationWithoutPolicy: DocumentCreate = {
id: 1,
kind: "test",
scope: { kind: "conversation", conversationId: 1 },
};
// @ts-expect-error session document creation cannot declare conversation policies
const sessionCreateWithPolicy: DocumentCreate = {
id: 1,
kind: "test",
scope: { kind: "session" },
history: "latest",
fork: "current",
};
const taskWithPolicy = {
id: 1,
kind: "test",
scope: { kind: "task", taskId: 1 },
history: "latest",
fork: "initial",
} as const;
// @ts-expect-error task document creation cannot declare conversation policies
const taskCreateWithPolicy: DocumentCreate = taskWithPolicy;
// @ts-expect-error latest document creation cannot use as-of fork behavior
const latestCreateWithAsOf: DocumentCreate = {
id: 1,
kind: "test",
scope: { kind: "conversation", conversationId: 1 },
history: "latest",
fork: "asOf",
};
const createWithSequence: DocumentCreate = {
id: 1,
kind: "test",
scope: { kind: "session" },
// @ts-expect-error storage, not the create command, supplies createdAt
createdAt: 1,
};
// @ts-expect-error completed outcomes cannot carry errors
const completedWithError: TaskOutcome<number> = {
status: "completed",
result: 1,
error: { message: "impossible" },
};
// @ts-expect-error queued inputs cannot reference transcript entries
const queuedWithEntry: Input = { id: 1, conversationId: 1, status: "queued", entry: 2 };
void [
missingReplacement,
omissionWithMessages,
pendingWithOutcome,
terminalWithCheckpoint,
terminalWithMemos,
sessionWithHistory,
conversationWithoutPolicy,
sessionCreateWithPolicy,
taskCreateWithPolicy,
latestCreateWithAsOf,
createWithSequence,
completedWithError,
queuedWithEntry,
];
};
expectTypeOf(compileTimeFailures).toBeFunction();
});