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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,17 @@ a numeric message row id as a string). The fork branches the source's
pre-populated with the source's messages up to the fork point, so the new
chat displays exactly the history the agent remembers.

The fork stores the source's native id and the fork turn when it is created
(`fork_source_native_id`, `fork_source_turn_id`). A fork of the whole chat
branches at the source's last completed turn, so a turn that the source
completes later is neither copied nor known to the fork's agent (history
from before per-turn recording is forked whole). Each start of the fork's
runtime branches from the stored source until a turn completes in the fork's
own conversation. A stop, a crash, a restart, an archive, or a later change to
the source does not change what the fork branches from. If the source
conversation no longer exists, the fork's next turn fails with a message
that says so, and the message after it starts a new conversation.

Errors: `404` unknown source; `409` not forkable — the source has no native
conversation yet (no completed turn), or the anchor message has no native
turn mapping (sessions predating per-turn recording can only be forked
Expand All @@ -1177,7 +1188,10 @@ Response: { "id": "fork-a1b2c3d4", "title": "My Fork", "source": "web", "status"
```

#### `POST /api/sessions/{id}/resume`
Resume a stopped or idle session (must have a stored `sdk_session_id`).
Resume a stopped or idle session (must have a stored `sdk_session_id`). A
`stopped`, `idle` or `error` session goes to `created`; any other status (for
example `active`, while a turn runs) stays as it is. The next message resumes
the stored native conversation.

```json
Response: { "id": "a1b2c3d4", "status": "created", "sdk_session_id": "..." }
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Every database open runs the runner: the gateway's start and every CLI command t
### Schema

SQLite with WAL mode (schema version 16):
- `sessions` — Session metadata with lifecycle columns (`status`, `sdk_session_id`, `connected_at`, `parent_session_id`, `forked_from_message`, `last_activity_at`, `archived_at`, `message_count`, `total_cost_usd`) and `created_by_actor_id` → `actor_refs`: who caused the session to exist
- `sessions` — Session metadata with lifecycle columns (`status`, `sdk_session_id`, `connected_at`, `parent_session_id`, `forked_from_message`, `fork_source_native_id`, `fork_source_turn_id`, `last_activity_at`, `archived_at`, `message_count`, `total_cost_usd`) and `created_by_actor_id` → `actor_refs`: who caused the session to exist
- `messages` — Conversation messages with tool call data and ordered `blocks` JSON column (preserves interleaving of text/thinking/tool_call blocks across page reloads), plus `actor_id` → `actor_refs`: whose input the row records. NULL means attribution was not recorded (legacy history, unidentified external people, and assistant/tool output). Nothing is backfilled — see [Accounts and identity](accounts.md)
- `session_events`: append-only lifecycle audit log (created, started, idle, stopped, archived, error, and the changes listed in [Session changes](accounts.md#session-changes)), with `actor_id` and `caused_by_actor_id` → `actor_refs`
- `session_deletions`: one row for each deleted session: last title, time, `actor_id` and `caused_by_actor_id`. Written in the transaction of the delete, which removes the session's events
Expand Down
2 changes: 2 additions & 0 deletions nerve/agent/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
AgentClient,
BackendCapabilities,
BackendError,
ForkSourceMissingError,
SessionSpec,
TransportDiedError,
TurnInput,
Expand Down Expand Up @@ -95,6 +96,7 @@ def build_backends(deps: BackendDeps) -> dict[str, AgentBackend]:
"BackendCapabilities",
"BackendDeps",
"BackendError",
"ForkSourceMissingError",
"ModelObserved",
"NormalizedUsage",
"SessionSpec",
Expand Down
4 changes: 4 additions & 0 deletions nerve/agent/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class TransportDiedError(BackendError):
"""The backend subprocess/stream died mid-operation."""


class ForkSourceMissingError(BackendError):
"""The native conversation that a fork branches from no longer exists."""


@dataclass
class TurnInput:
"""One user turn, engine-normalized.
Expand Down
6 changes: 5 additions & 1 deletion nerve/agent/backends/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,11 @@ def __init__(
# at connect, and a pre-configured copy would set both can_use_tool and
# permission_prompt_tool_name (a ValueError there).
self._sdk = ClaudeSDKClient(options=options, transport=self._transport)
self._native_session_id: str | None = spec.resume_native_id
# A fork gets its own id from the CLI's first message. Until then it
# has none: the resume target is the parent's conversation.
self._native_session_id: str | None = (
None if spec.fork else spec.resume_native_id
)
# The resolved model this client was built with (engine reads it
# to detect mid-session model switches).
self.model: str = options.model or ""
Expand Down
3 changes: 2 additions & 1 deletion nerve/agent/backends/codex/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
AgentClient,
BackendCapabilities,
BackendError,
ForkSourceMissingError,
SessionSpec,
TransportDiedError,
TurnInput,
Expand Down Expand Up @@ -902,7 +903,7 @@ async def connect(self) -> None:
if not _is_missing_thread_error(e):
raise
if spec.fork:
raise BackendError(
raise ForkSourceMissingError(
f"Cannot fork missing Codex thread {spec.resume_native_id!r}"
) from e
logger.warning(
Expand Down
Loading
Loading