Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7c2ed87
docs: define lindy session event-log data model
osolmaz Feb 27, 2026
7462b20
docs: rename acpx session model proposal
osolmaz Feb 27, 2026
959aea5
docs: use uuid examples for agentSessionId
osolmaz Feb 27, 2026
c65c768
docs: align acpx session model with ACP schema semantics
osolmaz Feb 27, 2026
79f8ca0
session: persist ACP projection and derived state
osolmaz Feb 27, 2026
a047957
Session model: align persistence with Zed thread schema
osolmaz Feb 27, 2026
70336b7
Session model: switch to strict Zed-analogous thread schema
osolmaz Feb 27, 2026
a35b933
Docs: define full session + NDJSON audit model
osolmaz Feb 27, 2026
6e1e7ae
Persistence: enforce snake_case session schema and add key-policy lint
osolmaz Feb 27, 2026
0b701d5
Docs: finalize single-schema event/session model spec
osolmaz Feb 27, 2026
5bff781
Session model: canonical ACP events with persisted event log
osolmaz Feb 27, 2026
68a45fb
Queue owner: return after draining pending tasks
osolmaz Feb 27, 2026
c8be686
Runtime: canonical queue stream + shared session defaults
osolmaz Feb 27, 2026
9802992
Errors: remove legacy fallback parsing paths
osolmaz Feb 27, 2026
b4f38fd
Events: rename kind to type and centralize event type constants
osolmaz Feb 27, 2026
47407fb
Runtime: warm queue owner and canonical control events
osolmaz Feb 27, 2026
d48186c
runtime: recover empty-session load failures on first prompt
osolmaz Feb 27, 2026
612f1eb
session: drop unused conversation metadata fields
osolmaz Feb 27, 2026
aca6422
session: cut over to top-level acpx.session.v1 schema
osolmaz Feb 27, 2026
4387d03
refactor: split session modules and harden quiet output
osolmaz Feb 27, 2026
419d0da
queue/event-log: recover stale owners and track real segments
osolmaz Feb 28, 2026
e270e12
Merge upstream/main into feat/acpx-session-model
osolmaz Feb 28, 2026
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
442 changes: 442 additions & 0 deletions docs/2026-02-27-acpx-session-model.md

Large diffs are not rendered by default.

295 changes: 295 additions & 0 deletions docs/2026-02-27-zed-thread-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
# Zed Thread Schema Reference

This document describes Zed's persisted thread schema as implemented in Zed commit:

- `511be9a3ffa032da6bab82ddfdd2e492c68298e3`

Source files used:

- `crates/agent/src/db.rs`
- `crates/agent/src/thread.rs`
- `crates/acp_thread/src/connection.rs`
- `crates/acp_thread/src/mention.rs`
- `crates/language_model/src/language_model.rs`
- `crates/language_model/src/request.rs`

## 1. Storage Layout

Zed stores threads in SQLite table `threads` with metadata columns and a serialized payload blob:

- `id TEXT PRIMARY KEY`
- `parent_id TEXT` (added by migration; set from subagent context)
- `folder_paths TEXT`
- `folder_paths_order TEXT`
- `summary TEXT NOT NULL`
- `updated_at TEXT NOT NULL` (RFC3339)
- `data_type TEXT NOT NULL` (`"json"` or `"zstd"`)
- `data BLOB NOT NULL`

Current writes use:

- `data_type = "zstd"`
- `data = zstd(level=3, utf8_json)`

Notes:

- Reads support both `json` and `zstd` blobs.
- `summary`/`updated_at` are duplicated in both columns and payload JSON.

## 2. Persisted JSON Envelope

The blob JSON is not raw `DbThread`; Zed wraps it with a top-level `version` field via a flattened wrapper.

Canonical shape written today:

```json
{
"title": "...",
"messages": [...],
"updated_at": "2026-02-27T12:34:56Z",
"detailed_summary": null,
"initial_project_snapshot": null,
"cumulative_token_usage": {...},
"request_token_usage": {...},
"model": null,
"profile": null,
"imported": false,
"subagent_context": null,
"speed": null,
"thinking_enabled": false,
"thinking_effort": null,
"version": "0.3.0"
}
```

Version handling:

- `DbThread::VERSION = "0.3.0"`.
- On load, if `version` is missing or not `0.3.0`, Zed attempts legacy upgrade (`upgrade_from_agent_1`).

## 3. DbThread Fields (Exact)

`DbThread` fields:

- `title: SharedString`
- `messages: Vec<Message>`
- `updated_at: DateTime<Utc>`
- `detailed_summary: Option<SharedString>` (`#[serde(default)]`)
- `initial_project_snapshot: Option<ProjectSnapshot>` (`#[serde(default)]`)
- `cumulative_token_usage: TokenUsage` (`#[serde(default)]`)
- `request_token_usage: HashMap<UserMessageId, TokenUsage>` (`#[serde(default)]`)
- `model: Option<SerializedLanguageModel>` (`#[serde(default)]`)
- `profile: Option<AgentProfileId>` (`#[serde(default)]`)
- `imported: bool` (`#[serde(default)]`)
- `subagent_context: Option<SubagentContext>` (`#[serde(default)]`)
- `speed: Option<Speed>` (`#[serde(default)]`)
- `thinking_enabled: bool` (`#[serde(default)]`)
- `thinking_effort: Option<String>` (`#[serde(default)]`)

Important defaults:

- Missing defaulted fields deserialize cleanly.
- `imported` defaults to `false`.
- `subagent_context` defaults to `null`/`None`.

## 4. Message Schema

`Message` enum variants:

- `User(UserMessage)`
- `Agent(AgentMessage)`
- `Resume`

Because serde default enum tagging is used (externally tagged):

- `User` serializes as `{ "User": { ... } }`
- `Agent` serializes as `{ "Agent": { ... } }`
- Unit variant `Resume` serializes as string: `"Resume"`

### 4.1 UserMessage

Fields:

- `id: UserMessageId` (newtype string)
- `content: Vec<UserMessageContent>`

`UserMessageContent` variants:

- `Text(String)`
- `Mention { uri: MentionUri, content: String }`
- `Image(LanguageModelImage)`

### 4.2 AgentMessage

Fields:

- `content: Vec<AgentMessageContent>`
- `tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>`
- `reasoning_details: Option<serde_json::Value>`

`AgentMessageContent` variants:

- `Text(String)`
- `Thinking { text: String, signature: Option<String> }`
- `RedactedThinking(String)`
- `ToolUse(LanguageModelToolUse)`

## 5. MentionUri Schema

`MentionUri` variants used inside `UserMessageContent::Mention.uri`:

- `File { abs_path: PathBuf }`
- `PastedImage`
- `Directory { abs_path: PathBuf }`
- `Symbol { abs_path: PathBuf, name: String, line_range: RangeInclusive<u32> }`
- `Thread { id: SessionId, name: String }`
- `TextThread { path: PathBuf, name: String }`
- `Rule { id: PromptId, name: String }`
- `Diagnostics { include_errors: bool, include_warnings: bool }`
- `Selection { abs_path: Option<PathBuf>, line_range: RangeInclusive<u32> }`
- `Fetch { url: Url }`
- `TerminalSelection { line_count: u32 }`
- `GitDiff { base_ref: String }`

Idiosyncrasies:

- `Diagnostics.include_errors` defaults `true` when absent.
- `Diagnostics.include_warnings` defaults `false` when absent.
- `Selection.abs_path` is omitted when `None` due `skip_serializing_if`.

## 6. Tool and Usage Schema

### 6.1 TokenUsage

`TokenUsage` fields:

- `input_tokens: u64`
- `output_tokens: u64`
- `cache_creation_input_tokens: u64`
- `cache_read_input_tokens: u64`

Each field has:

- `#[serde(default)]`
- `#[serde(skip_serializing_if = "is_default")]`

So zero values are often omitted from stored JSON.

### 6.2 LanguageModelToolUse

Fields:

- `id: LanguageModelToolUseId` (newtype string)
- `name: Arc<str>`
- `raw_input: String`
- `input: serde_json::Value`
- `is_input_complete: bool`
- `thought_signature: Option<String>`

### 6.3 LanguageModelToolResult

Fields:

- `tool_use_id: LanguageModelToolUseId`
- `tool_name: Arc<str>`
- `is_error: bool`
- `content: LanguageModelToolResultContent`
- `output: Option<serde_json::Value>`

`LanguageModelToolResultContent` canonical serialized variants:

- `Text(Arc<str>)`
- `Image(LanguageModelImage)`

Deserializer is intentionally permissive and accepts multiple forms:

- Plain string (`"..."`) -> `Text`
- Wrapped text object (`{"type":"text","text":"..."}`), case-insensitive key matching
- Single-key wrapped enum style (`{"text":"..."}` or case variants)
- Wrapped image (`{"image": {...}}`) with case-insensitive key matching
- Direct image object with case-insensitive `source`/`size`/`width`/`height`

Serialization remains canonical enum serialization; deserialization accepts broader shapes.

### 6.4 Speed

`Speed` enum uses `#[serde(rename_all = "snake_case")]`:

- `Standard` <-> `"standard"`
- `Fast` <-> `"fast"`

## 7. ID Types and Wire Behavior

Types used in persisted payload:

- `SessionId` (ACP thread/session id)
- `UserMessageId`
- `LanguageModelToolUseId`

All three are newtype wrappers and serialize as strings in JSON.

`UserMessageId` generation:

- UUID v4 string when created in Zed (`UserMessageId::new()`).

## 8. Metadata vs Payload Split

`DbThreadMetadata` (used by list API) is derived from SQLite columns, not blob decode.

Fields:

- `id`
- `parent_session_id`
- `title`
- `updated_at`
- `folder_paths`

Idiosyncrasy:

- `title` has `#[serde(alias = "summary")]` for compatibility with prior serialized naming.

## 9. Legacy Upgrade Idiosyncrasies

When upgrading old thread format:

- Legacy system-role messages are dropped.
- Old user message IDs are not preserved; fresh `UserMessageId` values are generated.
- Tool-use `raw_input` is reconstructed via `serde_json::to_string(input)`.
- If tool result name lookup fails, tool name is set to `"unknown"`.
- Per-request token usage is remapped to regenerated user message IDs.

These are semantic transforms, not byte-preserving migrations.

## 10. SharedThread (Export/Import) Schema

Zed also defines `SharedThread` (separate from normal `DbThread` persistence):

Fields:

- `title`
- `messages`
- `updated_at`
- `model`
- `version` (`"1.0.0"`)

Encoding:

- serialized to JSON, then zstd-compressed

Import behavior into `DbThread`:

- title is prefixed with `"🔗 "`
- `imported = true`
- several fields reset/defaulted (`detailed_summary`, token usage, profile, subagent context, speed, thinking fields)

## 11. Non-Obvious Serde Behaviors (Important)

- Enum representation is default externally tagged everywhere unless explicit serde attributes override it.
- Unit enum variants become bare strings (for example `"Resume"`).
- Most structs do not use `deny_unknown_fields`; extra unknown keys are generally ignored.
- Many optional/default fields deserialize even when absent, so sparse payloads are accepted.

## 12. Scope and Accuracy Notes

This reference is exact for the pinned commit above. It is not a protocol spec for all Zed versions.
If Zed changes serde attributes, enum variants, or wrapper/version logic, this document must be re-audited against source.
50 changes: 30 additions & 20 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,44 +392,54 @@ When a prompt is already in flight for a session, `acpx` uses a per-session queu
### Prompt/exec output behavior

- `text`: assistant text, tool status blocks, client-operation logs, plan updates, and `[done] <reason>`
- `json`: one JSON object per line with event types like `text`, `thought`, `tool_call`, `client_operation`, `plan`, `update`, `done`, `error`
- `json`: one canonical `acpx.event.v1` object per line
- `quiet`: concatenated assistant text only

JSON events include a stable envelope for correlation:
Canonical event envelope:

```json
{
"eventVersion": 1,
"sessionId": "abc123",
"requestId": "req-42",
"seq": 7,
"stream": "prompt",
"type": "tool_call"
"schema": "acpx.event.v1",
"event_id": "...",
"session_id": "...",
"acp_session_id": "...",
"agent_session_id": "...",
"request_id": "...",
"seq": 12,
"ts": "2026-02-27T00:00:00.000Z",
"type": "output_delta",
"data": {
"stream": "output",
"text": "..."
}
}
```

JSON error events include stable top-level `code`, optional `detailCode`, and optional `acp` payload.
JSON error events are also emitted as canonical `acpx.event.v1` payloads with `type: "error"`.

### Control-command JSON mapping

When `--format json` is used, control commands emit canonical `acpx.event.v1` events:

- `prompt --no-wait`: `type: "prompt_queued"`
- `sessions new`: `type: "session_ensured"` with `data.created: true`
- `sessions ensure`: `type: "session_ensured"` with `data.created: true|false`
- `sessions close`: `type: "session_closed"`
- `cancel`: `type: "cancel_result"`
- `set-mode`: `type: "mode_set"`
- `set`: `type: "config_set"`
- `status`: `type: "status_snapshot"`

### Sessions command output behavior
### Sessions/query command output behavior

- `sessions list` with `text`: tab-separated `id`, `name`, `cwd`, `lastUsedAt` (closed sessions include a `[closed]` marker next to id)
- `sessions list` with `json`: a single JSON array of session records
- `sessions list` with `quiet`: one session id per line (closed sessions include `[closed]`)
- `sessions new` with `text`: new session id (and replaced id when applicable)
- `sessions new` with `json`: `{"type":"session_created",...}`
- `sessions new` with `quiet`: new session id
- `sessions ensure` with `text`: ensured session id (created or reused)
- `sessions ensure` with `json`: `{"type":"session_ensured","created":true|false,...}`
- `sessions ensure` with `quiet`: ensured session id
- `sessions close` with `text`: closed record id
- `sessions close` with `json`: `{"type":"session_closed",...}`
- `sessions close` with `quiet`: no output
- `sessions show` with `text`: key/value metadata dump
- `sessions show` with `json`: full session record object
- `sessions history` with `text`: tab-separated `timestamp role textPreview` entries
- `sessions history` with `json`: object containing `entries` array
- `status` with `text`: key/value process status lines
- `status` with `json`: structured status object with `running|dead|no-session`

## Permission modes

Expand Down
Loading