feat(core): replace instruction checkpoints with value-delta sync#36254
Open
kitlangton wants to merge 9 commits into
Open
feat(core): replace instruction checkpoints with value-delta sync#36254kitlangton wants to merge 9 commits into
kitlangton wants to merge 9 commits into
Conversation
Instruction sync now stores only irreducible facts. The durable event
becomes session.instructions.updated.2 { delta }, mapping changed source
keys to SHA-256 content hashes with a literal "removed" sentinel for
observed absence. Canonical JSON bodies live once in a global
instruction_blob table; instruction_state is a rebuildable fold cache
(epoch_start, through_seq, initial/current values), never primary state.
Initial instructions and chronological update messages are rendered from
stored values at request assembly inside one read transaction. Completed
compaction moves the epoch at the exact ended-event sequence; move and
committed revert clear the fold. session.forked.2 carries an
authoritative parentSeq so forks fold parent ancestry through the
selected cutoff instead of copying the parent's latest checkpoint.
API entries keep tombstoned rows so removal can be admitted and
narrated; PUT rejects values over 8KB with a typed 413. Legacy prose
events migrate to read-only session.instructions.legacy.1 and their
projected System rows are deleted; durable projectors now dispatch by
exact type+version. Clients display changed keys instead of privileged
prose.
Rewrites the consolidated session spec's instructions section for value-delta sync, keeps the pre-dispatch location fence on upstream's simplified request variable, and registers the instruction sync design in the spec index. Also applies review follow-ups: deterministic code-unit ordering in canonical JSON hashing, hoisted event decoder, concurrent blob batch loads, and a single- statement epoch advance.
MoveSession now interrupts the active drain and awaits idle before publishing session.moved, the same serialization removal uses, so a relocation can never race a step that was assembled under the source Location's instructions and history. This replaces the narrower pre-dispatch location recheck in the runner, which only shrank the window without closing it.
The schema module names the concept, not the mechanism: Instruction.Key, Instruction.Hash, Instruction.Values, Instruction.Delta. Core's Instructions algebra keeps its plural name and re-exports the vocabulary.
Source is now the single erased interface over canonical JSON — the same representation that is hashed, stored, and replayed — and Instructions is a plain readonly array of it. make<A> keeps typed authoring as a constructor instead of a parallel interface, deleting PackedSource and the symbol-keyed opaque carrier. The unavailable/removed sentinels become tagged-enum singletons discriminated by identity, replacing Symbol.for registry keys borrowed from Effect internals.
Use initial consistently across renderers, remove the pre-beta legacy event surface, and limit migration cleanup to obsolete instruction events and their projections. Preserve JSON null as a real instruction value with Option-based hydrated deltas and nullable JSON storage, while tombstones remain explicit through the removed flag. Reuse event definition versions and skip empty inherited event ranges.
# ------------------------ >8 ------------------------ # Do not modify or remove the line above. # Everything below it will be ignored. # # Conflicts: # packages/core/src/session/compaction.ts # packages/core/src/session/runner/llm.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the V2 instruction checkpoint lifecycle with value-delta instruction sync. Mutable privileged context such as
AGENTS.md, environment and date, skill/reference/MCP guidance, and API entries can change during a session without persisting rendered prose or rewriting text already sent to the model.The durable log stores only the irreducible fact: which source values changed, and when. Initial instructions, chronological update messages, and the current comparison state are derived from the durable log, content-addressed values, and current renderer code.
The implemented design is documented in
specs/v2/instruction-sync-proposal.md.Source Model
Instruction producers author a typed
Source.Definition<A>:Instructions.makecaptures the codec and typed renderers in one JSON-levelSource.Instructionsis an ordered readonly collection of those sources.Instructions.combinepreserves source order and rejects duplicate keys.This keeps producer code typed while giving the runtime one heterogeneous representation for hashing, persistence, replay, and historical rendering.
UnavailableandRemovedare tagged singleton values discriminated by identity rather than global symbols.Architecture
flowchart LR D["Source.Definition<A>"] -->|"make: encode + capture renderers"| S["Source: JSON-level"] S --> I["Instructions: ordered sources"] I -->|"read concurrently once"| V["Canonical JSON values"] V -->|"SHA-256"| X{"Initial boundary or changed?"} X -->|"yes"| T["One DB transaction"] T --> B[(instruction_blob)] T --> E["session.instructions.updated.2"] E --> F[(instruction_state fold)] X -->|"no"| A["Request assembly"] B --> A F --> A S --> A A --> M["Initial instructions + chronological updates"]Durable Data
session.instructions.updated.2 { delta }is the only durable instruction fact. Changed source keys map to SHA-256 content hashes; an observed removal maps to the literal"removed". The sentinel cannot collide with a 64-character hexadecimal hash and avoids nullable record values that do not survive every client generator.instruction_blobis a machine-local content-addressed store. Each distinct canonical JSON value is stored once by hash. Object keys are canonicalized by code-unit order before hashing. Blob insertion and event publication share the event transaction.instruction_stateis a rebuildable fold cache containingsession_id,epoch_start,through_seq,initial_values, andcurrent_values. A missing or stale row is reconstructed from durable events without publishing another instruction event.No blob garbage collection ships in this change. Mark-and-sweep can be added later without changing the schema or event format.
Safe Boundary
Before pending user input is promoted, each physical runner attempt performs this sequence:
instruction_state.current_values.{}for a genuinely empty source set.If initial instruction discovery is temporarily unavailable, the attempt fails with
Instructions.InitializationBlockedwhile pending input remains retryable. Later temporary failures do not erase context the model already knows.Boundary Sequence
sequenceDiagram participant R as Runner participant S as Instruction sources participant D as Database participant P as Pending input participant M as Model Note over R,D: Source reads and fold validation start concurrently R->>S: Read every source once R->>D: Ensure instruction_state through latest event S-->>R: Value / Removed / Unavailable D-->>R: Current source hashes alt Initial source unavailable R-->>R: Fail InitializationBlocked Note over P: Input remains pending else Complete initial or later boundary R->>R: Encode JSON, canonicalize, hash, diff alt Initial boundary or changed value R->>D: Atomically insert blobs and publish delta D-->>R: Fold cache advanced else No instruction change Note over R: Publish no instruction event end R->>P: Promote pending input R->>D: Read epoch values, deltas, blobs, and history D-->>R: One consistent request snapshot R->>R: Render initial text and chronological updates R->>M: Send model request endRequest Assembly
Assembly runs inside one database read transaction. It loads the epoch's initial hashes, chronological post-epoch deltas, and referenced blobs, then uses the current source renderers to produce:
Removedand supplied a removal renderer.Rendered model-visible prose is never stored in events or projected message rows. Instruction update messages are excluded from compaction summaries. Client-facing event notices show changed keys, for example
Instructions updated: core/date, without exposing privileged values.Hydrated updates use
Option<Json>: a missing record key means unchanged,Some(value)means set or change, andNonemeans removal. This keeps valid JSONnulldistinct asSome(null)instead of treating it as deletion.Epochs, Compaction, And Rebuilds
An instruction epoch starts at the initial complete V2 delta or the latest
session.compaction.endedsequence. Completed compaction advances the epoch in one SQL update by making current hashes initial hashes. It does not read sources or publish an instruction event.The fold consumes exact event type/version pairs. Instruction deltas update current values, compaction advances the epoch, and committed revert or session movement clears the fold so the next safe boundary must establish a complete value set.
Forks
session.forked.2carries authoritativeparentSeq. Forking before message N recordsmessage.seq - 1, so the child inherits parent instruction events through the exact fork boundary rather than copying the parent's latest state.The child stores this cutoff in
session.fork_seq. Its instruction fold consists of parent ancestry through the frozen cutoff followed by child events. Replay supports the intentional inherited sequence prefix, so rebuilding a fork preserves the same instruction state. Assembly skips ancestor ranges that cannot contain post-epoch updates.Moves And Execution Serialization
Moving a session interrupts its active execution and awaits idle before capturing changes or publishing
session.moved. This prevents the active old-location drain from admitting input, assembling stale instructions, or racing Git change capture while it shuts down. The ordering matches session removal:interrupt,awaitIdle, then mutate durable state.API Entries
Each stored entry becomes one source keyed
api/<key>. DELETE writes a hidden tombstone instead of physically deleting the row, preserving the renderer needed to admit and narrate removal. List responses hide tombstones, and a later PUT revives the same source.The SQLite value column is nullable because JSON
nullis a valid value. The separateremovedflag distinguishes that value from a tombstone; removed rows storevalue = NULLrather than a fake empty string. Null-safe conflict predicates preserve no-op PUT behavior across null and non-null values.PUT measures encoded JSON as UTF-8 and rejects values larger than 8KB with typed
InstructionEntryValueTooLargeError, exposed as HTTP 413. Values are never truncated.Migration And Compatibility
The consolidated migration:
instruction_bloband rebuildableinstruction_statetables.instruction_entryto add the tombstone flag and allow JSONnull.session.instructions.updated.1events and only their event-derived System rows.session.fork_seqbefore deleting old instruction events, preserving authoritative fork cutoffs.session.forked.1to.2withparentSeq.instruction_checkpoint.Unrelated events, System messages, sessions, and canonical V1 tables are left intact. Existing sessions establish one complete V2 delta at their next safe boundary.
Both generated client surfaces were regenerated. The delta wire type remains
{ [key: string]: string | "removed" }; hash bodies do not cross the wire. The obsolete legacy instruction event no longer appears in schemas or clients.Verification
The full core suite still has the two pre-existing
LocationWatcher.git/HEADwatcher timeouts documented during development; focused suites covering this change are green.