docs(agent): finalize Pico5 architecture and presentation

This commit is contained in:
Mario Zechner 2026-09-15 17:36:13 +02:00
parent ea7f84678c
commit c8e4a5a552
4 changed files with 2490 additions and 1343 deletions

View file

@ -0,0 +1,364 @@
# Pico5 documents through Chord
This guide uses the contracts in the [Pico5 specification](pico-v5.md).
- **Session:** a durable container for conversations, entries, tasks, and documents;
it serializes mutations on one commit line.
- **Facet:** a feature's setup unit in a Chord host. Setup declares services and
dependencies synchronously; `onActivate` runs after dependencies are ready.
- **Context:** explicitly passed cancellation and invocation values. Forward the caller's context.
- **Service:** a typed token and implementation acquired with `env.use`. Remote
services contain replicated state and async methods with JSON arguments plus a trailing `Context`.
- **Durable document:** JSON persisted as a complete base, Chord operations, and
checkpoints. Its definition token declares schema, version, scope, and initialization;
conversation scope also declares history and fork behavior.
- **Transaction draft:** the mutable tracked object from `await tx.doc`, valid
only inside that commit callback, including all nested objects.
- **DocumentSource:** an opaque handle to one document incarnation's committed changes,
not a mutable value or a public subscription API.
- **ReplicatedState:** Chord's immutable complete values through `value` and
`subscribe(listener)`. `value` is `undefined` before hydration or while disconnected.
The listener receives `(value, context, delivery)`; delivery contains `kind`
(`hydrate` or `update`) and a stream `sequence`, not a Session commit number.
Documents are scoped directly to a Session, conversation, or task. A task is
durable work attached to a conversation; its documents retire when it becomes
terminal and never participate in conversation forks.
## Imports and the adapter boundary
Examples build on one another. Pico5 names (`defineDoc`, `defineDocFamily`,
`Session`, `DocumentSource`, `Id`, `TaskRuntime`, `DocumentObserver`)
refer to normative contracts, without a specified import path or runnable Pico5
package. These Chord imports exist today:
```ts
import {
createFacetHost, createRemoteServiceBinding, defineFacet, defineService,
type Context, type Facet, type JsonValue, type RemoteServiceTransport,
type ReplicatedState,
} from "@earendil-works/chord";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
// The spec calls this JsonObject; current Chord exports JsonValue, not JsonObject.
type JsonObject = { [key: string]: JsonValue };
// Required by Pico5, but not a current Chord export yet.
declare function documentReplicatedState<T extends JsonObject>(
source: DocumentSource<T>, context: Context,
): Promise<{ readonly state: ReplicatedState<T | null>; dispose(): void }>;
```
The adapter must register Chord-recognized state, atomically hydrate matching
committed value/revision and subscribe, and forward committed ops without another
tracker or re-diff. Disposal releases observation, not the document. Even Chord's
hydration requests must never flush an uncommitted draft.
Current `env.replicatedState(initial)` creates a **mutable, in-memory** tracker
with `.state` and `.publish(context)`, not a durable source adapter. There is no
`replicatedState.own(source)`. Do not mirror documents into another tracker.
## 1. A Session-wide canvas
All conversations in this Session share one canvas. Session documents are
current-only and have no history or fork settings.
```ts
type Stroke = { color: string; points: { x: number; y: number }[] };
type CanvasState = { strokes: Stroke[] };
const CanvasDoc = defineDoc<CanvasState>({
id: "app.canvas", version: 1, scope: "session",
initial: () => ({ strokes: [] }),
// This append-only example adds one stroke per commit.
checkpointWhen: value => value.strokes.length % 100 === 0,
});
interface CanvasService {
readonly state: ReplicatedState<CanvasState | null>;
addStroke(stroke: Stroke, context: Context): Promise<void>;
}
const Canvas = defineService<CanvasService>("app.canvas");
async function createCanvasFacet(session: Session, context: Context): Promise<Facet> {
const target = { scope: "session" } as const;
// Get-or-create: an absent canvas is initialized in a serialized commit.
const source = await session.documentSource(CanvasDoc, target, context);
const replica = await documentReplicatedState(source, context);
return defineFacet({
id: "app.canvas/session",
setup(env) {
env.own(() => replica.dispose());
env.provide(Canvas, {
state: replica.state,
async addStroke(stroke, context) {
await session.commit(async tx => {
const draft = await tx.doc(CanvasDoc, target);
draft.strokes.push(stroke); // The draft now owns stroke; caller must not mutate it.
}, context);
},
});
},
});
}
const CanvasConsumer = defineFacet({
id: "app.canvas/consumer",
setup(env) {
const canvas = env.use(Canvas); // Declare now; access only after activation.
env.onActivate(() => {
env.own(canvas.state.subscribe((value, _context, delivery) => {
console.log(delivery.kind, delivery.sequence, value?.strokes.length ?? "retired");
})); // subscribe delivers the current hydrated value, then updates.
});
},
});
async function runCanvasExample(session: Session): Promise<void> {
const context = BACKGROUND_CONTEXT;
const provider = await createCanvasFacet(session, context);
const host = await createFacetHost({ facets: [provider, CanvasConsumer] });
try {
await host.services.use(Canvas).addStroke({
color: "black", points: [{ x: 10, y: 20 }, { x: 30, y: 40 }],
}, context);
} finally {
await host.dispose(); // Unsubscribes; does not delete the canvas or close Session.
}
}
```
The application owns the open Session. Acquire state **before** synchronous
`setup`; `env.provide` cannot run in `onActivate`. Install one provider per
Session host. Chord may reload presentation facets independently, but v1 does
not use facet reload to replace Session-side task or hook implementations. A
host plugin code change closes and reopens the Harness with the new definition
set.
Remote clients supply a `RemoteServiceTransport` connected to the host's `services`
provider; Chord prescribes no socket protocol. `CanvasConsumer` also works unchanged
in a UI host configured with a remote service source.
```ts
async function connectCanvas(transport: RemoteServiceTransport, context: Context) {
const services = createRemoteServiceBinding({ services: [Canvas], transport });
const canvas = services.use(Canvas);
const stop = canvas.state.subscribe((value, _context, delivery) => {
console.log(delivery.kind, delivery.sequence, value?.strokes ?? "retired");
});
try {
await services.ready(context); // Initial snapshot installed; not all future updates.
} catch (error) {
stop();
await services.dispose(BACKGROUND_CONTEXT);
throw error;
}
return async () => {
stop();
await services.dispose(BACKGROUND_CONTEXT);
};
}
```
```text
first acquisition -> commit base { strokes: [] }
addStroke(A) -> commit A -> local and remote subscribers see A
worker restarts -> open same durable storage; install canvas facet again
acquire source -> load base + committed deltas, without rerunning initial()
late client -> hydrate complete canvas including A, then ordered updates
conversation fork -> still uses this same Session canvas
```
Use persistent storage for restart survival. The memory backend is not persistent.
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.
```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",
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 }>;
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,
): Facet {
return defineFacet({
id: "app.diff-reviews/session",
setup(env) {
const instances = env.provideMany(DiffReviews);
env.onActivate(async () => {
for (const review of reviews) {
const target = { scope: "conversation" as const, conversationId, ...review };
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]), {
state: replica.state,
async identity() { return { conversationId, instanceId: review.instanceId }; },
async addComment(comment, context) {
await session.commit(async tx => {
const draft = await tx.doc(ReviewDoc, target);
draft.comments.push(comment); // Do not mutate comment after this assignment.
}, context);
},
}); // The facet owns spawned service lifetimes automatically.
}
});
},
});
}
```
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.
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.
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.
```text
parent review-7: comment A -> transcript entry E -> comment B
fork at E with current: child review-7 contains A and B
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.
## 3. Task-scoped output and a tool/task watch
This is a bounded progress window, not the permanent result. Scope itself gives
it task lifetime; no history, fork, owner, or conversation setting is needed.
```ts
type JobInput = { command: string };
type JobOutput = { stdout: string; chunks: number };
const JobOutputDoc = defineDoc<JobOutput>({
id: "app.job-output", version: 1, scope: "task",
initial: () => ({ stdout: "", chunks: 0 }),
checkpointWhen: value => value.chunks % 100 === 0,
});
function jobOutputTarget(taskId: Id) {
return { scope: "task" as const, taskId };
}
async function appendJobOutput(
runtime: TaskRuntime<JobInput, { phase: "running" }, null, {}>,
chunk: string, context: Context,
): Promise<void> {
// Read process output outside this callback. The runtime gates the live task.
await runtime.commit(async tx => {
const draft = await tx.doc(JobOutputDoc, jobOutputTarget(runtime.taskId));
draft.stdout = (draft.stdout + chunk).slice(-50_000);
draft.chunks += 1;
}, context);
}
async function observeJob(
api: DocumentObserver, producerTaskId: Id,
finished: Promise<void>, context: Context,
): Promise<void> {
const target = jobOutputTarget(producerTaskId);
const watch = await api.watchDoc(JobOutputDoc, target, context);
try {
console.log(watch.revision, watch.value.stdout); // Consume captured initial value.
watch.start((value, ops, revision, _context) => {
console.log(revision, value === null ? "retired" : value.stdout, ops);
}); // Drain buffered changes in order, then continue live delivery.
await finished; // Caller-supplied observation lifetime; outside any commit.
} finally {
watch.stop(); // Idempotent; discards any remaining buffer.
}
}
```
The running phase calls `appendJobOutput` and must also commit its next checkpoint
or terminal outcome. `api` is the invocation's `DocumentObserver`; `TaskRuntime`
includes that interface. Acquisition validates the target task is live and
derives its conversation from the task record.
```text
watchDoc: capture value V at revision 8 + register listener atomically on Session line
producer commits revision 9 before start: buffer 9
consumer reads watch.value (V), calls start: deliver 9, then subsequent revisions
producer becomes terminal: same commit retires output; watch receives null + ["r", null]
```
Watches automatically stop when their invocation ends. A Session-acquired watch
is caller-owned and stops on Session close. Retirement ends the incarnation's
stream; recreation requires a new watch/source and a new numeric
document ID. A terminal task cannot create more output. Task-scoped documents
never copy into forks. Preserve required output in result entries or Session- or
conversation-scoped documents in the terminal commit before retirement. Use a
task-scoped family only when one task needs several independently keyed documents.
## 4. Built-in conversation views and commit delivery
`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
illustrative path mapping is:
```text
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.
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
services or trusted invocation watches. A retired source publishes `null`; a
dynamic service should then withdraw its instance rather than expose stale data.
```text
addStroke -> hold Session mutation line -> await tx.doc -> mutate tracked draft
callback succeeds -> tracker flush: incremental ops + candidate value
atomic storage commit: all document and record writes; checkpoint predicate selects ops or base
storage succeeds -> adopt committed baseline + enqueue source publication, still on line
release line -> deliver committed source ops -> adapter -> local/remote Chord consumers
late subscriber -> atomically capture committed value/revision + subscription
```
No visible-undurable path exists. Callback failure restores the unchanged
baseline. Failure after flush, including checkpoint/storage failure, publishes
nothing and poisons the open Session; close and reopen it instead of continuing.
## Footguns
- Never retain drafts, nested proxies, or bound array methods across commits.
Never mutate inserted aliases or insert one mutable object at multiple paths.
- Async commit holds the line through storage settlement and baseline adoption.
Await document access there, not models, processes, network calls, or humans.
- Normal `snapshot`, `documentSource`, and `watchDoc` reads are get-or-create.
Family `initial` is first-creation input, not an update.
- An unstarted watch buffers without a fixed bound. Start or stop promptly.
- 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.
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),
[Context](../../chord/src/context/index.ts), [Delta](../../chord/src/delta/README.md).

View file

@ -1,555 +1,266 @@
# Pico5 handoff and decision register
> Working coordination document. `pico-v5.md` is the compact design. This file
> records accepted choices, unresolved decisions, examples, and layer sign-offs so
> work survives conversation compaction. No unresolved item may be decided silently
> during implementation.
## Current status
- `packages/agent/src/harness/pico/` and its tests were removed.
- The untracked `pico4` source/tests were removed.
- `pico3` remains as a reference, not an implementation base.
- Storage benchmark artifacts are in `/tmp/pi-pico5-storage-bench/`.
- Astra approved starting the memory + SQLite storage/conformance slice after the
storage decisions below are signed off.
## Accepted direction
1. Entries are immutable and paged; the Session is not one materialized DOM.
2. Authors mutate ordinary JSON documents; Chord produces nested/list/string ops.
3. History policy is per document: `latest` or `rewindable`.
4. Built-ins use three conversation documents:
- `rewindable`: historical configuration;
- `sticky`: present state such as queues and retry policy;
- `live`: high-churn turn, generation, tool, and compaction presentation state.
5. Typed document definitions and dynamic families are required. Family instances
may be task-owned and retire automatically.
6. Task recovery state is a complete replacement, separate from presentation and
application documents.
7. There is no visible-but-undurable publication. Every Chord-visible update follows
a successful durable commit.
8. Interrupted assistant/tool partials are converted into durable aborted/interrupted
entries by their recovering task kind, then removed from `live`.
9. A built-in needs cross-task document observation, so `watchDoc` is initial scope.
10. UI status is state, not an event protocol. There is no public event channel
initially; clients observe document paths. A bounded `live` notice field may be
designed with the built-in view if concrete transient facts require it.
11. Third-party documents initially use their own Chord services. Automatic mounting
into the agent view is deferred.
12. SQLite is the long-running/cloud backend. JSONL remains local and readable.
13. Implementation proceeds layer by layer and stops for user sign-off after each.
## Decisions required before storage implementation
### S1. Document identity and families — accepted
A definition carries type, logical ID, scope, history, initializer, and optional
family ownership rules:
```ts
const live = defineDoc<LiveState>({
id: "pi.live",
scope: "conversation",
history: "latest",
initial: () => ({ tools: [] }),
});
const jobOutput = defineDocFamily<JobState, JobInput>({
id: "pi.job",
scope: "conversation",
history: "latest",
owner: "task",
initial: input => ({ command: input.command, stdout: "" }),
});
```
A persisted instance has a unique, never-reused storage identity plus definition ID,
scope owner, family instance ID, creation sequence, history policy, and retirement
sequence. Reusing one logical family key creates a new incarnation.
### S2. Historical document membership — accepted
A rewindable document retired today still exists in an as-of query from yesterday.
A latest document can be deleted after retirement.
Example:
```text
10 create rewindable review A
20 append entry E
30 retire A
40 fork at E
```
The fork enumerates A at commit 20 and copies its as-of value into a new child-owned
instance. Current enumeration at commit 40 omits A.
### S3. Storage ownership and copying — proposed recommendation
Simplest safe contract:
- Commit inputs are borrowed until `Storage.commit()` settles.
- A backend retaining anything after settlement detaches it first.
- Memory uses a small recursive `copyJson`; JSONL/SQLite detach through serialization.
- Every storage read returns a fresh owned JSON copy. Mutating it cannot corrupt the
backend or another caller. Public types remain readonly, but correctness does not
rely on TypeScript at runtime.
- Large strings are immutable and may share their string value during an in-memory
copy; object/array containers never share mutably.
Example failure this prevents:
# Pico5 implementation handoff
```ts
const task = { phase: "running" };
await storage.commit([{ type: "task", task }]);
task.phase = "done"; // must not mutate MemoryStorage behind its back
```
Decision: approve this contract, or replace it with explicit ownership transfer and
state which caller may still use document ops for Chord publication.
### S4. Table reads and writes inside `Tx` — proposed recommendation
Adopt the other design's simple rule:
```text
table reads first -> first table write -> later table reads throw ReadAfterWrite
document reads/mutations always read their transaction draft
```
`packages/agent/docs/pico-v5.md` is normative. Implement this list in order.
After every package: run its tests, run `npm run check`, and stop for user review.
Do not redesign later packages while implementing the current one.
Example:
Pico3 is reference material only. Preserve useful behavior, not its capability
facades, membranes, document routing, view projection, events, or clone chains.
```ts
const inputs = await tx.inputs(ids); // read
const head = await tx.newestEntry(c); // read
const entry = tx.appendEntry(c, assistant); // first table write
tx.doc(live, c).message = undefined; // document draft is allowed
// await tx.entry(other) now throws ReadAfterWrite
```
## Status
Benefit: no shadow indexes for newly created entries/tasks/inputs. Cost: helpers must
load everything before their first table write. Validate all built-in settlement
traces before accepting.
- Obsolete `pico` and `pico4` prototypes were removed.
- `pico3` remains.
- No Pico5 implementation exists.
Decision: adopt this rule, or support read-your-writes for tables and pay for overlays.
## 1. Records, cursors, and memory tables
### S5. Escaped document draft — open
Implement IDs, sequences, conversations, entries, inputs, live/terminal tasks,
document metadata, storage writes, cursors, and detached `MemoryStorage` tables.
A callback can retain a document proxy:
Test mixed atomic commits, rollback, detached reads/writes, cursor boundaries,
fork-aware entry scans through deep ancestor caps, head lookup,
entry-to-commit lookup, and full task replacement.
```ts
let escaped: LiveState;
await session.commit(tx => { escaped = tx.doc(live, c); });
escaped.message = partial;
```
## 2. Memory document records
Options:
Add document creation bases, deltas, required/checkpoint bases, retirement,
reincarnation, current/as-of membership, and base-plus-tail reconstruction.
- **Detached stale draft:** mutation succeeds locally but has no effect. Simplest,
but silently hides a bug.
- **Revoked draft:** every later read/write throws. Clearest, but requires tracker
proxies to share an active guard or another membrane.
- **Debug assertion:** stale mutation is detected at the next transaction. Cheaper,
but delayed.
Test Session-, conversation-, and task-scoped documents, retired historical
membership, family queries, and no scans of unrelated document records.
Recommendation: first determine whether Chord tracker proxies can cheaply share one
revocation guard. Do not let an escaped proxy mutate committed state or contaminate a
later commit.
## 3. SQLite backend
### S6. Base policy — partly accepted
Implement the complete storage contract with ordinary rows and indexed document
records. Do not translate Chord operations into SQL JSON patches.
Every document may receive bases. Storage gets both incremental ops and a borrowed
candidate value, serializing only one:
Run the memory conformance suite after reopen. Test SQL transaction rollback,
recent/ancient as-of reads, query plans, latest reclamation, WAL checkpointing,
deleted-page reuse, and representative storage sizes.
```ts
persist(shouldBase(id, ops) ? [["r", value]] : ops);
```
## 4. JSONL publication
- `latest`: a committed base permits reclaiming older records.
- `rewindable`: bases bound as-of replay; historical segments remain.
- Trigger on both encoded bytes and record count.
Implement table writes in `main.jsonl`, one document sidecar per incarnation,
one sidecar per live task, and one main marker per commit. Do not add a
standalone-sidecar protocol.
Open values:
Fault-test torn/short sidecar writes, failures between sidecars, every marker
boundary, unconfirmed tails, missing confirmed data, and poisoned writes.
- byte threshold;
- record-count threshold;
- whether document definitions may request `rebaseWhenInitial`.
## 5. JSONL reclamation
`pi.live` benefits from `rebaseWhenInitial`: when a turn ends at `{ tools: [] }`, a
tiny base allows immediate reclamation of streamed output. The definition/document
manager can recognize its initial state; storage still owns physical reclamation.
Implement task-document retirement and current-only base reclamation using
committed markers, temporary replacement, rename, and descriptor invalidation.
### S7. Task rows — accepted
Crash-test every rewrite/rename boundary. Verify that rewindable history is
never reclaimed and default no-fsync behavior matches the specification.
Task records are full replacements. SQLite uses one live row and `DELETE` on terminal.
JSONL uses one task sidecar and unlinks it only after terminal publication.
## 6. Tracker transaction core
The runtime envelope owns kind/input/dependencies/memos/effect IDs. Kind-defined
recovery state cannot overwrite memos. A terminal transaction transfers any needed
outcome and retires every task-owned document family instance atomically.
Keep one tracker per loaded document. Add transaction begin, callback rollback
from the unchanged baseline, flush, fatal post-flush failure, committed baseline
adoption, and eviction.
This atomic retirement removes the need for an application-level orphan sweep:
Do not add membranes, transaction proxy graphs, capability facades, or defensive
full-document clones. Test callback failure, no-op transactions, storage failure
poisoning, and unload/reload.
- crash before terminal: task and owned docs remain live and recover;
- crash after terminal marker: logical retirement is known, and storage may clean
leftover files during open.
## 7. Document definitions and access
### S8. SQLite representation — accepted direction
Implement `defineDoc`, `defineDocFamily`, identity validation, the three direct
scopes, and get-or-create `tx.doc`, `snapshot`, and `documentSource` acquisition.
Use one SQL transaction per Session commit:
Test concurrent initialization once, initial bases, detached snapshots, family
initializer use only on first creation, scope/target mismatch, terminal-task
rejection, task-derived conversation identity, retirement, and
reincarnation-bound sources. Include create-task-then-document,
document-after-terminal rejection, and create-document-then-terminal settlement
in one transaction; internal candidate validation must not trigger
`ReadAfterWrite`.
```text
conversations / entries / inputs
document metadata
document base+delta records indexed by document and commit
a current live-task row
```
## 8. Checkpoints and migration
Do not translate Chord ops into SQLite JSON functions initially. The benchmark found
full JSONB replacement slower than TEXT and generic SQL patching slower with more WAL
writes. Current/as-of reads select a base and indexed tail; they do not scan unrelated
documents.
Inside `Storage.commit()`, evaluate `checkpointWhen(value, ops)` exactly once
for ordinary mutations. Implement required creation/version bases and
all-older-version migration.
Still choose schema details, WAL checkpoint cadence, and `synchronous` default during
the SQLite layer.
Test latest migration persistence, rewindable migration on current/historical
read, first post-migration mutation base, newer-version rejection, absent plugin
preservation, and checkpoint starvation without backend heuristics.
### S9. JSONL physical layout — current lean: uniform sidecars
## 9. Conversation document forks
Simplest layout from the other design:
Using fixture conversations and entry-to-commit mappings, implement the `asOf`,
`current`, and `initial` settings for singleton and family documents.
```text
main.jsonl tables + one publication marker per commit
doc-<storage-id>.jsonl one file per document instance
task-<storage-id>.jsonl one file per live task
```
Test unknown definitions, retired membership, new child incarnations, lazy
`initial` creation, and exclusion of task- and Session-scoped documents.
Every sidecar record is prepared. Sidecars append first; one main marker is always
last. This deliberately permits records such as:
## 10. Chord structural array operations
```json
{"seq":14,"refs":["task-9"]}
{"seq":15,"refs":["task-9"]}
{"seq":16,"refs":["pi.live-1"]}
```
Make ordinary positional mutations encode scattered removals without carrying
retained payloads. Callers must not write operations manually.
The markers are cosmetic overhead but make every commit use one protocol. They can be
compact; fields such as `writes:[]` and repeated `maxId` are unnecessary if high-water
is represented elsewhere. Without `fsync`, the second append is unlikely to be the
performance bottleneck; the benchmark's no-fsync sidecar backend still handled about
9.5k–30k commits/s depending on workload while opening/closing each sidecar.
Test front/tail/middle/scattered/all/no removal, retained 256 KiB and 1 MiB
payloads, append plus removal, later nested/index writes, exact replay, and
unchanged previous immutable snapshots. Internal flushes must remain one commit.
Alternative: standalone one-file commits avoid markers but introduce two record modes,
distributed commit discovery, and more recovery cases.
## 11. Chord document source
Decision: approve uniform marker publication or select the standalone optimization.
Add the thin opaque-source adapter to `ReplicatedState`. It must use committed
value/ops directly, with no tracker or re-diff.
### S10. JSONL process-crash contract — accepted direction
Test contiguous revisions, atomic hydrate/subscribe, attachment races,
retirement ending one incarnation, recreation requiring reacquisition, and
listener isolation.
No `fsync` by default. Awaited/full sidecar writes precede the awaited/full main
marker. For an ordinary process crash with the OS/filesystem alive:
## 12. Document watches
- torn final lines are removed;
- sidecar records without a marker are uncommitted and removed;
- a complete marker publishes the transaction.
Implement get-or-create `watchDoc`: capture value/revision and register its
listener atomically, buffer without a cap until `start()` or `stop()`, and always
deliver value plus ops.
Power loss, host/kernel failure, or weak filesystem ordering may lose acknowledged
commits. Optional durable mode may fsync sidecars before the marker.
Test ordered draining, live delivery, retirement as absence, idempotent stop,
listener errors, and explicit cleanup. Invocation-owned cleanup is integrated in
package 15.
Any uncertain write error poisons the open backend until reopen. Reclamation occurs
only after its base/retirement commits and uses temp + rename; cached descriptors are
then reopened.
## 13. Conversations and entries
### S11. Missing confirmed sidecar — open
Implement conversation history/ownership records, entry creation, cursor-based
fork-aware scans, head lookup, and entry edits.
Example:
Test conversation creation and actual forks, deep ancestor caps, same-commit
entry prefixes, newest-edit wins, self-head resolution, raw head-to-tail
transcript, and ownership traversal.
```text
sidecar record 20 was reported written
main marker 20 survives
sidecar record 20 is absent after machine failure
```
## 14. Context derivation and system messages
This should not arise from the stated ordinary process-crash contract. Options:
Implement model-context reduction, PR #9548 positional `SystemMessage` replay,
tool-result ordering, and missing post-fork tool results. Replay `content`,
ordered named `sections` with `null` removal, then tool removals/additions.
- **Fail open:** report corruption and preserve all bytes for diagnosis/repair.
Simplest and never invents state.
- **Roll back globally:** truncate every file to the latest complete global prefix.
Recovers availability but may discard acknowledged later commits and must interact
correctly with reclaimed prefixes and standalone records if those exist.
- **Best-effort skip:** invalid because it exposes a partial atomic commit.
Test model-less and excluded-stop-reason entries, replacements/omissions,
multiple heads, section replacement/removal/re-addition order, rejection of
integer-like section keys, tool addition/removal/replacement order, and raw-view
versus model context.
Recommendation: fail open initially. Add a separate repair tool only after physical
fault tests establish a safe prefix algorithm.
## 15. Task definitions and invocations
### S12. Main JSONL growth — accepted limitation
Implement `defineTask`, exhaustive phase maps, full checkpoint replacement,
kind migration, runtime commits, memos, and invocation close gates.
Document/task sidecars reclaim high-churn data; `main.jsonl` still grows by entries,
lifecycle records, and commit markers. JSONL may scan it on open. It is the local
backend. Years-long products use SQLite. Do not add global JSONL compaction initially.
Use a fake two-phase effect. Test intent/effect/outcome recovery,
unchanged-checkpoint faulting, same-phase checkpoint progress, cancellation
precedence, thrown-handler faulting, close/reopen without abort marks, outcomes,
or task-document retirement, no fresh phase/abort dispatch while closing,
first-writer-wins memos, and automatic watch cleanup.
### S13. Storage query surface — needs exact signatures
## 16. Scheduler and terminal tasks
The first storage contract must support:
Implement reservation, running-task reopen reconciliation, dependencies,
terminal outcomes, waits, holds, joins, and orphaning.
- create/current/retired conversation records;
- paged immutable entries and entry -> commit mapping;
- current task lookup/scan;
- current and as-of document enumeration;
- current and as-of document value;
- family-instance enumeration by definition/scope/owner;
- one atomic mixed commit;
- close/reopen.
Test result values and entry IDs, terminal records after reopen, dependency
eligibility, unknown kinds, and terminal removal of checkpoints/memos.
As-of results distinguish not-yet-created, retired after target, and unavailable
history. Concrete TypeScript signatures are the first layer's first sign-off.
## 17. Abort and owned conversations
## Decisions required before document/task implementation
Implement durable abort marks, signal/join/fresh-abort invocation, owned
conversation creation, subtree traversal, background behavior, and idle waits.
### D1. Three built-in documents — accepted
Test commit rejection after a run task is marked, crashes at every abort stage,
close precedence over a previously marked task, deep ownership trees, and atomic
retirement of task-scoped documents.
```ts
rewindable // model/tool/config choices inherited by historical fork
sticky // inbox, queue modes, retry policy
live // current message/tools/generation/collapse and maybe notices
```
## 18. Inputs and positional inbox
`live` is durable because recovery converts its partial output into transcript entries.
Its high churn is independently reclaimable when it returns to initial state.
Define the initial inbox and turn-control documents, then implement request-ID
deduplication, awaitable input handles, busy admission, withdrawal, queue modes,
and `postTools`/`final` boundaries. Use a fake successor task.
### D2. Document adapter to Chord — accepted direction
Table-test every input transition, interleaved steer/follow-up/write selection,
self-head cuts, stale targets, successor triggers, reopen waits, compact
large-payload removals, and orphan/fault cleanup of active turn control.
A thin adapter such as `replicatedState.own(documentSource)` exposes committed value
and ops. It owns no second tracker. Each document and mounted view has a contiguous
publication revision separate from sparse global Session commit sequence.
## 19. Remaining built-in documents and view
Hydration captures value/revision and subscribes atomically. Listener errors after
persistence are isolated. Current Chord needs a small public source adapter; it does
not need harness knowledge.
Define the concrete configuration, preference, and live presentation documents;
reuse the approved inbox/turn definitions. Record all IDs, fields, history,
fork settings, migration, and checkpoint predicates in the normative
specification.
### D3. Cross-task `watchDoc` — accepted, exact scope open
Implement `{ conversation, entries, docs }`. Test direct task writes, one
publication per Session commit, atomic
entry/preview settlement, head changes, contiguous revisions, stable public
paths, retry/collapse late-join status, bounded-output truncation metadata, and
absence of semantic projection. Specify which diagnostics become entries,
terminal details, or bounded document state.
Required semantics:
## 20. Registries, hooks, and sections
```ts
const watch = await api.watchDoc(jobOutput, job.id, context);
consume(watch.value); // snapshot first
watch.start((ops, value) => consume(value));
```
Implement task/tool/entry/section registries, Session and owned-subtree hooks,
positional PR #9548 section/tool updates, complete baselines after a head cut,
and preparation revision checks. Do not add a public event stream or
plugin-state router.
- capture + subscription are one Session-line operation;
- callbacks run off the line and cannot re-enter a commit synchronously;
- watch is volatile and invocation-owned;
- recovery re-enters the handler, captures a fresh snapshot, and subscribes again;
- retirement appears as document absence;
- `stop()` is idempotent.
Test registration lifetimes, hook replay with memos, exact persisted rendered
section strings, minimal section patches and `null` removals, complete baseline
tool declarations, and a head cut that retains earlier system messages. Verify
the new baseline entry omits those messages through `ContextEdit` before replay,
including retained `content`, section order, and tool changes. Include a retained
delta whose ID precedes the head-carrying entry: select omissions by retained
context membership, not an ID comparison with the head entry. Test tool loadout
additions/removals and preparation retry after registry movement. Do
not implement in-process replacement of Session-side plugin code; a host plugin
change uses the Harness close/reopen boundary.
Open scope: same conversation only, or any conversation owned by the observing task's
subtree. Initial built-in use must provide the concrete required edge.
## 21. Tool and post-tools tasks
### D4. Reopen conversion — accepted
Implement offered-set checks, argument validation, tool hooks, durable bounded
progress, owned APIs, interrupted/replay-safe recovery, result entries,
post-tools joining, controls, and boundaries using fake tools.
Generation recovery reads `live.message`, appends an assistant entry with
`stopReason: "aborted"`, clears the preview, and retries/fails atomically. Tool
recovery turns persisted output into its interrupted result before clearing the slot.
The entry uses the normal model/render shape while model projection excludes aborted
content.
Test recovery from every phase, both stored/current replay-policy directions,
and atomic assistant/tool/post-tools settlement. Use a fake generation
successor; package 22 replaces it and reruns integration.
Conversion runs before retiring any task-owned family document it needs.
## 22. Generation task
### D5. Document versioning/migration — open
Implement preparation, request intent, durable throttled partials, attempts,
retry policy, response classification, continuation, and deferred polling/
cancellation with the faux provider.
Document paths and persisted values are public/durable schema. A plugin upgrade may
change `{ comments: [...] }` to `{ threads: [...] }`.
Test every phase before and after reopen, aborted partial conversion, overflow
through a fake collapse kind, input settlement, and no visible-undurable update.
Replace the fake tool successor and rerun the package 21 integration tests.
Options:
## 23. Collapse task
- definition has an integer version and migration from complete current value;
- task/document kind owns migration during open;
- reject mismatched versions initially.
Implement manual, threshold, and overflow collapse; exchange-boundary range
selection; summarization; retries; staleness; and headed summary entries.
Recommendation: persist definition version now even if initial implementation only
rejects mismatches. Retrofitting identity/version after stored products ship is harder.
Test context before/after collapse, provider failure, declined/stale work, and
reopen from every phase. Replace generation's fake overflow target and rerun its
overflow integration test.
## Decisions required before view implementation
## 24. Job and plugin tasks
### V1. Public view shape — open
Implement process jobs with bounded durable output, interruption recovery,
rescheduling, and registered plugin handlers.
Fixed top-level fields:
Test process exit/error/abort/reopen, background idle behavior, notifications as
state or entries, and plugin handler recovery.
```ts
{ conversation, entries, rewindable, sticky, live }
```
## 25. Harness integration
Typed and direct, but every added built-in document changes the protocol.
Expose conversation/input/task handles, watches, dynamic registries, close,
suspend, service withdrawal/client detach, and product wiring. Implement the v1
host-plugin reload path as stop admission, close/join, dispose, rebuild with all
new definitions, reopen/migrate, and resume. Test that closing seals commit and
get-or-create admission, lets storage settlement for already-flushed admitted
commits finish despite caller cancellation, writes no abort or terminal outcome, starts no fresh abort
invocation, and does not run old and new generations concurrently.
Document map:
```ts
{
conversation,
entries,
docs: {
"pi.rewindable": RewindableState,
"pi.sticky": StickyState,
"pi.live": LiveState,
},
}
```
Makes document identity/lifetime explicit and can add selected documents later, but
paths and IDs become public protocol. It can remain typed for the three built-ins.
Recommendation: decide before writing any UI adapter. In either shape, mount only the
three built-ins initially and treat every visible path as versioned public API.
### V2. Public events — accepted: none initially
A compaction-start event cannot describe current state to a late joiner. Instead:
```ts
live.collapse = { through, stage: "summarizing" }; // running
live.collapse = undefined; // finished
```
Clients react to state ops by path and always hydrate the current truth. Hooks remain
pre-decision interception. Internal commit subscribers may exist mechanically, but
there is no separate public semantic event protocol initially.
Whether failed attempts need a bounded `live.notices` list is deferred until the UI
is implemented. It remains state with explicit retention, not fire-and-forget events.
### V3. Atomic publication — accepted
The built-in mount consumes the complete Session commit, prefixes every changed
built-in document path, combines entry changes, and publishes one Chord batch. It has
no tracker or semantic projection. Independent third-party Chord services may publish
separately.
### V4. Active transcript bound — open
A Slack conversation cannot hydrate every entry ever written. Options include:
- durable `head` entry: active transcript is the range beginning at its target;
- fixed recent entry/count/byte window;
- product-selected window with a stable cursor.
Example requirement:
```text
10 years / 500,000 entries
late UI attach -> bounded snapshot
older history -> explicit paged query
model context -> independently derived bounded projection
```
Choose before view implementation, not before storage. Entry paging and indexes are
required now regardless.
### V5. Multi-entry fork boundary — open
One commit may append entries E and F and leave one final rewindable state. Fork at E:
- **entry prefix:** child transcript includes E but not F, while document state is the
final state of the containing commit;
- **commit boundary:** selecting E means inherit all entries from its indivisible
commit, including F;
- **restriction:** only designated forkable entries/commits may be targets.
Choose before fork/view API. Storage records entry -> commit either way.
### V6. Sticky fork initialization — current lean: defaults
A historical fork inherits rewindable documents as-of its target and no live tasks.
Copying current sticky/live state can copy queued inputs or running previews from the
future. Simplest initial rule:
```text
rewindable -> historical copy
sticky -> fresh defaults
live -> fresh defaults
```
Explicit sticky-copy options remain deferred.
### V7. Third-party mounting — accepted defer
Third-party documents initially expose application-specific Chord services. Do not add
`inView`, privacy flags, dynamic mount registries, or client-selected arbitrary docs.
A document is not a security boundary; services deliberately choose what crosses a
process boundary.
## Layer plan and sign-off gates
### Layer 0 — cleanup
- Remove `pico` source/tests: done.
- Remove `pico4` source/tests: done.
- Keep `pico3`: done.
- Run repository check after deletions and document additions: pending.
### Layer 1 — logical storage + memory reference
Before code: sign off S3 and S6, retain accepted S1/S2/S7, and approve the exact S13 interfaces.
Acceptance:
- atomic mixed commits;
- document creation, bases, current/as-of membership and values;
- family instances and task ownership;
- full task replacement/retirement;
- entry paging and entry -> commit;
- ownership isolation and failure rollback.
Stop for user review.
### Layer 2 — SQLite
Before code: approve concrete schema and durability defaults.
Acceptance: Layer 1 conformance plus reopen, indexed query plans, latest reclamation,
retired rewindable history, WAL checkpoint behavior, and realistic storage benchmark.
Stop for user review.
### Layer 3 — JSONL
Before code: decide S9 and S11.
Acceptance: semantic conformance plus physical fault injection at every sidecar,
marker, rewrite, rename, retirement, and reopen boundary.
Stop for user review.
### Layer 4 — document manager + Chord source
Before code: decide S4, S5, D3 scope, and D5.
Acceptance: ordinary mutations, no duplicate tracker, failed-commit rollback,
contiguous publication, hydration fencing, listener isolation, eviction, and
`watchDoc` snapshot/update/retirement behavior.
Stop for user review.
### Layer 5 — tasks
Acceptance: phase-narrowed full replacements, effect sandwich, runtime-owned memos,
abortable ownership tree, atomic family retirement/outcome handoff, generation/tool
reopen conversion, and no visible-undurable state.
Stop for user review.
### Layer 6 — built-in documents and conversation view
Before code: decide V1, V4, V5, and V6.
Acceptance: direct built-in writes, fixed structural mount, one publication per commit,
bounded late hydration, crash-visible progress, and no public event dependency.
Stop for user review.
### Layer 7 — remaining harness
Inputs, hooks, tools, system sections, dynamic tools, deferred providers, and product
wiring. Preserve accepted APIs from earlier design work rather than redesigning them
inside the state implementation.
Run all package-specific tests and the repository check. Verify a local
coding-agent turn and a reopened interrupted turn, then stop for final review.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff