- MessageStore.fork(source_message_id, name=, model=) creates a new
conversation rooted at an existing message. The shared prefix is
reused in place; only one conversations row is written.
- Model defaults to whatever most recently wrote a call touching the
source message; can be overridden. Raises ValueError if the message
doesn't exist or no model can be inferred.
- New `llm fork <message_id>` CLI prints the new conversation id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Conversation (sync + async) gains head_message_id; from_row populates
it from the DB so CLI -c continuation resumes the existing chain
instead of starting a parallel one.
- MessageStore gains save_with_dedup(): one call that locates the
longest existing prefix and appends only the unmatched tail. Writes
zero rows when the full chain already exists — the stateless-API
continuation case from plans/dag-schema.md.
- Tests cover find_longest_existing_prefix (miss / partial / full),
save_with_dedup idempotency, and end-to-end: a second prompt on a
reloaded Conversation extends the chain via head_message_id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites m023 in place to the DAG-shaped message store from
plans/dag-schema.md:
- messages: id, parent_id, content_hash, role, provider_metadata_json,
created_at. Chain roots point at a self-referencing sentinel row
("root") so the unique (parent_id, content_hash) index works at
every chain position — NULL-parent uniqueness footgun avoided.
- message_parts: structurally unchanged.
- calls: one row per LLM call, anchoring head_input/head_output
message ids and recording model + timing + usage.
- conversations.head_message_id: advances each turn; history is
reconstructed by walking parent_id from the head.
New llm/storage.py provides MessageStore.save_chain (with dedup),
load_chain, and find_longest_existing_prefix (for the stateless-API
case wired in phase 3).
Response.log_to_db now writes the DAG + a calls row alongside the
existing responses-table writes (kept for llm logs compatibility
until phase 5). Response._load_messages_from_db walks the chain
using calls pointers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>