Skip to content

Commit db3be62

Browse files
committed
docs(v2): explain session inbox ordering
1 parent 7526ce8 commit db3be62

3 files changed

Lines changed: 245 additions & 0 deletions

File tree

‎packages/core/src/session/sql.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ export const SessionMessageTable = sqliteTable(
122122
.notNull()
123123
.references(() => SessionTable.id, { onDelete: "cascade" }),
124124
type: text().$type<SessionMessage.Type>().notNull(),
125+
// Immutable sequence of the durable event that first projected this row.
126+
// Later events may update row data but never move it through the transcript.
125127
seq: integer().notNull(),
126128
...Timestamps,
127129
data: text({ mode: "json" }).notNull().$type<SessionMessageData>(),

‎specs/v2/session-inbox-ordering.md‎

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Session Inbox And Transcript Ordering
2+
3+
This is the proposed V2 model for prompt admission, visible transcript history,
4+
and optimistic TUI rendering.
5+
6+
## 1. Before The Inbox
7+
8+
Before the embedded V2 runner slice, a new prompt was written directly into
9+
visible transcript history. The loop reloaded history and inferred whether work
10+
remained by comparing the latest user and assistant messages.
11+
12+
```text
13+
User Transcript Loop Model
14+
│ │ │ │
15+
├─ append visible prompt ──▶ │ │
16+
│ │ │ │
17+
│ ◀─ reload history ──┤ │
18+
│ │ │ │
19+
│ │ ├────────────────────────────────────╮ │
20+
│ │ │ compare latest user and assistant │ │
21+
│ │ ◀────────────────────────────────────╯ │
22+
│ │ │ │
23+
│ │ ├─ continue if newer user work exists ────▶
24+
│ │ │ │
25+
```
26+
27+
This made accepted input and model-visible history the same thing. It also made
28+
message ID ordering carry more responsibility than it should.
29+
30+
## 2. Proposed V2 Model
31+
32+
V2 separates accepted input from visible transcript history:
33+
34+
```text
35+
session_input
36+
-> durable receipt for accepted user intent
37+
-> ordered by admission seq
38+
-> pending until promoted
39+
40+
session_message
41+
-> canonical model-visible transcript
42+
-> ordered by seq
43+
-> contains only promoted messages
44+
45+
event
46+
-> canonical durable Session event log
47+
-> event.seq is copied into session_message.seq when a row first appears
48+
```
49+
50+
```text
51+
User Inbox Runner Transcript Model
52+
│ │ │ │ │
53+
├─ admit prompt(delivery) ──▶ │ │ │
54+
│ │ │ │ │
55+
◀─ durable receipt ─────────┤ │ │ │
56+
│ │ │ │ │
57+
│ │ │ │ │
58+
│ not model-visible yet │ │ │
59+
│ │ │ │ │
60+
│ ◀─ promote at safe boundary ──┤ │ │
61+
│ │ │ │ │
62+
│ ├─ append Prompted(seq) ────────────────────────▶ │
63+
│ │ │ │ │
64+
│ │ ├─ reload visible transcript ───────▶
65+
│ │ │ │ │
66+
```
67+
68+
The two sequence fields answer different questions:
69+
70+
| Field | Meaning |
71+
| --------------------- | -------------------------------------------------------------------------- |
72+
| `session_input.seq` | In what order did OpenCode accept pending inputs? |
73+
| `session_message.seq` | At what durable Session event position did this row enter visible history? |
74+
75+
`session_message.seq` is immutable. Later text, tool, or settlement events may
76+
update a projected assistant row without moving it through the transcript.
77+
78+
```ts
79+
/**
80+
* Immutable aggregate sequence of the durable event that first projected this
81+
* visible transcript row. Later events may update the row data but never move it.
82+
*/
83+
seq: integer().notNull()
84+
```
85+
86+
## 3. Optimistic Steer Rendering
87+
88+
A default `steer` input joins the active activity at the next safe provider-turn
89+
boundary. The TUI can show it inline immediately as pending, then reconcile that
90+
same row in place when promotion assigns the canonical transcript `seq`.
91+
92+
```text
93+
User TUI Inbox Runner Transcript
94+
│ │ │ │ │
95+
├─ submit steer ──▶ │ │ │
96+
│ │ │ │ │
97+
│ ├─ admit ─────────────────────────▶ │ │
98+
│ │ │ │ │
99+
│ ├────────────────────────────╮ │ │ │
100+
│ │ render inline pending row │ │ │ │
101+
│ ◀────────────────────────────╯ │ │ │
102+
│ │ │ │ │
103+
│ │ ◀─ promote at checkpoint ──┤ │
104+
│ │ │ │ │
105+
│ │ ├─ append same id with seq ──────────────────▶
106+
│ │ │ │ │
107+
│ ◀─ reconcile same row in place ────────────────────────────────────────────────┤
108+
│ │ │ │ │
109+
```
110+
111+
Breadboard sketch while the assistant is still streaming:
112+
113+
```text
114+
┌─────────────────────────────────────────────────────────────┐
115+
│ assistant live ▌ │
116+
│ I found the migration issue. I am checking Windows too... │
117+
├─────────────────────────────────────────────────────────────┤
118+
│ you pending steer ↗ │
119+
│ Keep the fix minimal and add a regression test. │
120+
└─────────────────────────────────────────────────────────────┘
121+
```
122+
123+
After the checkpoint, the row stays where it appeared:
124+
125+
```text
126+
┌─────────────────────────────────────────────────────────────┐
127+
│ assistant │
128+
│ I found the migration issue. I am checking Windows too... │
129+
├─────────────────────────────────────────────────────────────┤
130+
│ you │
131+
│ Keep the fix minimal and add a regression test. │
132+
├─────────────────────────────────────────────────────────────┤
133+
│ assistant live ▌ │
134+
│ I will keep the patch narrow and add the regression now... │
135+
└─────────────────────────────────────────────────────────────┘
136+
```
137+
138+
The stable input ID lets the TUI replace pending state with the promoted
139+
`session_message` row instead of inserting a second bubble.
140+
141+
## 4. Queued Inputs Stay Separate
142+
143+
An explicit `queue` input belongs to a future activity. The TUI should show it in
144+
a queue tray until the current activity settles. Only then does the runner promote
145+
one queued input into visible transcript history.
146+
147+
```text
148+
User TUI Inbox Runner Transcript
149+
│ │ │ │ │
150+
├─ submit queue ──▶ │ │ │
151+
│ │ │ │ │
152+
│ ├─ admit ────────────────────▶ │ │
153+
│ │ │ │ │
154+
│ ├───────────────────────╮ │ │ │
155+
│ │ render in queue tray │ │ │ │
156+
│ ◀───────────────────────╯ │ │ │
157+
│ │ │ │ │
158+
│ │ │ ├───────────────────────────╮ │
159+
│ │ │ │ current activity settles │ │
160+
│ │ │ ◀───────────────────────────╯ │
161+
│ │ │ │ │
162+
│ │ ◀─ promote oldest queued input ──┤ │
163+
│ │ │ │ │
164+
│ │ ├─ append Prompted(seq) ────────────────────────────────────────────▶
165+
│ │ │ │ │
166+
│ ◀─ move row into transcript ─────────────────────────────────────────────────────────────────────┤
167+
│ │ │ │ │
168+
```
169+
170+
```text
171+
┌──────────────────────────────────────────────┬──────────────────────────────┐
172+
│ transcript │ queued · 2 │
173+
│ │ │
174+
│ assistant live ▌ │ 1. Run the full test suite. │
175+
│ I am fixing the migration now... │ 2. Then open a PR. │
176+
└──────────────────────────────────────────────┴──────────────────────────────┘
177+
```
178+
179+
After the current activity settles, promote one queued input:
180+
181+
```text
182+
┌──────────────────────────────────────────────┬──────────────────────────────┐
183+
│ transcript │ queued · 1 │
184+
│ │ │
185+
│ assistant │ 1. Then open a PR. │
186+
│ The migration fix is complete. │ │
187+
│ │ │
188+
│ you │ │
189+
│ Run the full test suite. │ │
190+
│ │ │
191+
│ assistant live ▌ │ │
192+
│ Running the suite now... │ │
193+
└──────────────────────────────────────────────┴──────────────────────────────┘
194+
```
195+
196+
## 5. Why Store Transcript `seq`
197+
198+
IDs identify rows. Timestamps describe authored time. Neither is canonical
199+
visible-history order once admission and promotion are separate.
200+
201+
Storing the creator event sequence directly on the `session_message` projection:
202+
203+
- keeps context assembly and pagination indexed on the smaller transcript table
204+
- keeps compaction boundaries aligned with durable Session order
205+
- avoids scanning the denser event log before every provider turn
206+
- lets future event retention differ from transcript retention
207+
208+
The pending inbox is not a complication caused by transcript `seq`. It is the
209+
domain model: accepted input and model-visible history are separate lifecycle
210+
stages.
211+
212+
## 6. Migration Boundary
213+
214+
Historical experimental builds could write `session_message` rows without durable
215+
creator events. Those rows cannot be assigned truthful transcript order.
216+
217+
Before V2 launch, reset only that experimental projection before adding `seq`:
218+
219+
```sql
220+
DELETE FROM session_message;
221+
ALTER TABLE session_message ADD COLUMN seq INTEGER NOT NULL;
222+
```
223+
224+
Canonical V1 history remains in `message` and `part`.
225+
226+
## Proposed Client Contract
227+
228+
The storage split should stay internal. A TUI-facing snapshot can combine both
229+
lanes explicitly:
230+
231+
```ts
232+
type SessionTimeline = {
233+
messages: SessionMessage[]
234+
pendingInputs: SessionInputReceipt[]
235+
}
236+
```
237+
238+
Render promoted messages in transcript `seq` order. Render pending steers inline
239+
and queued inputs in the queue tray. Reconcile pending rows by stable input ID when
240+
promotion creates the canonical transcript row.

‎specs/v2/session.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Session API
22

3+
See [`session-inbox-ordering.md`](./session-inbox-ordering.md) for the proposed
4+
prompt inbox, transcript ordering, and optimistic TUI rendering model.
5+
36
## Current V2 Core Slice
47

58
The Effect-native core facade treats prompt recording and execution as separate responsibilities:

0 commit comments

Comments
 (0)