Skip to content

Commit b3dccfd

Browse files
committed
Merge remote-tracking branch 'upstream/main'
# Conflicts: # frontend/components/live/ActivityFeed.tsx
2 parents d15f001 + f65fd22 commit b3dccfd

13 files changed

Lines changed: 1269 additions & 63 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@ The fastest way to try CXDB is with the pre-built Docker image:
1818
# Run the server (binary protocol :9009, HTTP :9010)
1919
docker run -p 9009:9009 -p 9010:9010 -v $(pwd)/data:/data cxdb/cxdb:latest
2020

21-
# Create a context and append a turn
22-
curl -X POST http://localhost:9010/v1/contexts
21+
# Create a context and append a turn (HTTP write path)
22+
curl -X POST http://localhost:9010/v1/contexts/create \
23+
-H "Content-Type: application/json" \
24+
-d '{"base_turn_id": "0"}'
2325
# => {"context_id": "1", "head_turn_id": "0", "head_depth": 0}
2426

25-
curl -X POST http://localhost:9010/v1/contexts/1/turns \
27+
curl -X POST http://localhost:9010/v1/contexts/1/append \
2628
-H "Content-Type: application/json" \
2729
-d '{
2830
"type_id": "com.example.Message",
2931
"type_version": 1,
30-
"payload": {"role": "user", "text": "Hello!"}
32+
"data": {"role": "user", "text": "Hello!"}
3133
}'
3234

3335
# View in the UI

docs/http-api.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ GET /v1/contexts
2626
| Parameter | Type | Default | Description |
2727
|-----------|------|---------|-------------|
2828
| `limit` | int | 100 | Max contexts to return |
29-
| `offset` | int | 0 | Pagination offset |
29+
| `tag` | string | - | Filter by exact client tag |
30+
| `include_provenance` | bool | false | Include provenance in each context |
31+
| `include_lineage` | bool | false | Include parent/root/children lineage summary |
3032

3133
**Response:**
3234

@@ -50,6 +52,13 @@ GET /v1/contexts
5052
GET /v1/contexts/:context_id
5153
```
5254

55+
**Query Parameters:**
56+
57+
| Parameter | Type | Default | Description |
58+
|-----------|------|---------|-------------|
59+
| `include_provenance` | bool | true | Include provenance block |
60+
| `include_lineage` | bool | true | Include lineage block with parent/root/children |
61+
5362
**Response:**
5463

5564
```json
@@ -65,12 +74,33 @@ GET /v1/contexts/:context_id
6574

6675
- `404 Not Found` - Context doesn't exist
6776

77+
### List Child Contexts
78+
79+
```http
80+
GET /v1/contexts/:context_id/children
81+
```
82+
83+
**Query Parameters:**
84+
85+
| Parameter | Type | Default | Description |
86+
|-----------|------|---------|-------------|
87+
| `recursive` | bool | false | Include all descendants, not just direct children |
88+
| `limit` | int | 256 | Max child contexts to return |
89+
| `include_provenance` | bool | true | Include provenance in each child |
90+
| `include_lineage` | bool | true | Include lineage in each child |
91+
6892
### Create Context
6993

7094
```http
7195
POST /v1/contexts/create
7296
```
7397

98+
Alias:
99+
100+
```http
101+
POST /v1/contexts
102+
```
103+
74104
**Request Body:**
75105

76106
```json
@@ -235,6 +265,12 @@ Use `next_before_turn_id` from the previous response to continue paging.
235265
POST /v1/contexts/:context_id/append
236266
```
237267

268+
Alias:
269+
270+
```http
271+
POST /v1/contexts/:context_id/turns
272+
```
273+
238274
**Request Body:**
239275

240276
```json
@@ -254,10 +290,13 @@ POST /v1/contexts/:context_id/append
254290
|-------|------|----------|-------------|
255291
| `type_id` | string | Yes | Type identifier |
256292
| `type_version` | int | Yes | Type version |
257-
| `data` | object | Yes | Turn payload (will be encoded as msgpack) |
293+
| `data` | object | Yes* | Turn payload (will be encoded as msgpack) |
294+
| `payload` | object | Yes* | Alias for `data` (for compatibility) |
258295
| `parent_turn_id` | string | No | Parent turn (default: current head) |
259296
| `idempotency_key` | string | No | For safe retries |
260297

298+
\*At least one of `data` or `payload` is required.
299+
261300
**Response:**
262301

263302
```json
@@ -275,7 +314,7 @@ POST /v1/contexts/:context_id/append
275314
- `409 Conflict` - Invalid parent_turn_id
276315
- `422 Unprocessable Entity` - Invalid data or missing type
277316

278-
**Note:** The HTTP API accepts JSON `data` and converts it to msgpack internally. Numeric field tags are derived from the type registry. For maximum control over msgpack encoding, use the binary protocol.
317+
**Note:** The HTTP API accepts JSON payloads and converts them to msgpack internally. If a type descriptor exists, numeric tags are derived from the registry. If no descriptor exists, the JSON structure is still persisted as msgpack (string/numeric keys preserved). For maximum control over encoding, use the binary protocol.
279318

280319
## Registry
281320

frontend/app/page.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,53 @@ export default function Home() {
114114
);
115115
}
116116

117+
// Link child context to parent context lineage
118+
if (event.type === 'context_linked') {
119+
setContexts(prev =>
120+
prev.map(c => {
121+
if (c.context_id === event.data.child_context_id) {
122+
return {
123+
...c,
124+
lineage: {
125+
parent_context_id: event.data.parent_context_id,
126+
root_context_id: event.data.root_context_id,
127+
spawn_reason: event.data.spawn_reason,
128+
child_context_count: c.lineage?.child_context_count ?? 0,
129+
child_context_ids: c.lineage?.child_context_ids ?? [],
130+
},
131+
provenance: {
132+
...(c.provenance ?? {}),
133+
parent_context_id: Number(event.data.parent_context_id),
134+
root_context_id: event.data.root_context_id
135+
? Number(event.data.root_context_id)
136+
: c.provenance?.root_context_id,
137+
spawn_reason: event.data.spawn_reason ?? c.provenance?.spawn_reason,
138+
},
139+
};
140+
}
141+
142+
if (c.context_id === event.data.parent_context_id) {
143+
const existingChildren = c.lineage?.child_context_ids ?? [];
144+
const childContextIds = existingChildren.includes(event.data.child_context_id)
145+
? existingChildren
146+
: [...existingChildren, event.data.child_context_id];
147+
return {
148+
...c,
149+
lineage: {
150+
parent_context_id: c.lineage?.parent_context_id,
151+
root_context_id: c.lineage?.root_context_id,
152+
spawn_reason: c.lineage?.spawn_reason,
153+
child_context_count: childContextIds.length,
154+
child_context_ids: childContextIds,
155+
},
156+
};
157+
}
158+
159+
return c;
160+
})
161+
);
162+
}
163+
117164
// Update context activity timestamp on turn append
118165
if (event.type === 'turn_appended') {
119166
setContexts(prev =>

frontend/components/live/ActivityFeed.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ function getEventDisplay(event: StoreEvent): {
156156
icon: <AlertCircle size={12} />,
157157
label: `${event.data.kind} ${event.data.status_code}: ${event.data.message}`,
158158
};
159+
160+
case 'context_linked':
161+
return {
162+
icon: <Folder size={12} />,
163+
label: `Linked to parent ${event.data.parent_context_id}`,
164+
contextId: event.data.child_context_id,
165+
};
159166
}
160167
}
161168

frontend/hooks/useEventStream.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ConnectionState,
88
ContextCreatedEvent,
99
ContextMetadataUpdatedEvent,
10+
ContextLinkedEvent,
1011
TurnAppendedEvent,
1112
ClientConnectedEvent,
1213
ClientDisconnectedEvent,
@@ -74,6 +75,14 @@ export function useEventStream(options: UseEventStreamOptions = {}): UseEventStr
7475
if (event.type === 'context_metadata_updated' && event.data.context_id !== contextId) {
7576
return;
7677
}
78+
if (event.type === 'context_linked') {
79+
if (
80+
event.data.child_context_id !== contextId &&
81+
event.data.parent_context_id !== contextId
82+
) {
83+
return;
84+
}
85+
}
7786
}
7887

7988
setLastEvent(event);
@@ -154,6 +163,15 @@ export function useEventStream(options: UseEventStreamOptions = {}): UseEventStr
154163
}
155164
});
156165

166+
eventSource.addEventListener('context_linked', (e: MessageEvent) => {
167+
try {
168+
const data: ContextLinkedEvent = JSON.parse(e.data);
169+
handleEvent({ type: 'context_linked', data });
170+
} catch (err) {
171+
console.error('Failed to parse context_linked event:', err);
172+
}
173+
});
174+
157175
eventSource.addEventListener('turn_appended', (e: MessageEvent) => {
158176
try {
159177
const data: TurnAppendedEvent = JSON.parse(e.data);

frontend/lib/api.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export interface FetchContextsOptions {
111111
tag?: string;
112112
/** Include full provenance data for each context. */
113113
include_provenance?: boolean;
114+
/** Include parent/root/children lineage summary for each context. */
115+
include_lineage?: boolean;
114116
}
115117

116118
/**
@@ -131,6 +133,9 @@ export async function fetchContexts(limitOrOptions: number | FetchContextsOption
131133
if (options.include_provenance) {
132134
params.set('include_provenance', '1');
133135
}
136+
if (options.include_lineage) {
137+
params.set('include_lineage', '1');
138+
}
134139

135140
const queryString = params.toString();
136141
const url = `${API_BASE}/contexts${queryString ? `?${queryString}` : ''}`;
@@ -153,6 +158,103 @@ export async function fetchContexts(limitOrOptions: number | FetchContextsOption
153158
return response.json();
154159
}
155160

161+
export interface FetchContextOptions {
162+
include_provenance?: boolean;
163+
include_lineage?: boolean;
164+
}
165+
166+
/**
167+
* Fetch details for a specific context.
168+
*/
169+
export async function fetchContext(
170+
contextId: string,
171+
options: FetchContextOptions = {}
172+
): Promise<ContextEntry> {
173+
const params = new URLSearchParams();
174+
if (options.include_provenance !== false) {
175+
params.set('include_provenance', '1');
176+
}
177+
if (options.include_lineage !== false) {
178+
params.set('include_lineage', '1');
179+
}
180+
181+
const queryString = params.toString();
182+
const url = `${API_BASE}/contexts/${encodeURIComponent(contextId)}${queryString ? `?${queryString}` : ''}`;
183+
const response = await fetch(url);
184+
185+
if (!response.ok) {
186+
let errorData: ErrorResponse | undefined;
187+
try {
188+
errorData = await response.json();
189+
} catch {
190+
// Ignore JSON parse errors
191+
}
192+
throw new ApiError(
193+
errorData?.error?.message || `HTTP ${response.status}`,
194+
errorData?.error?.code || response.status,
195+
errorData
196+
);
197+
}
198+
199+
return response.json();
200+
}
201+
202+
export interface FetchContextChildrenOptions {
203+
recursive?: boolean;
204+
limit?: number;
205+
include_provenance?: boolean;
206+
include_lineage?: boolean;
207+
}
208+
209+
export interface ContextChildrenResponse {
210+
context_id: string;
211+
recursive: boolean;
212+
count: number;
213+
children: ContextEntry[];
214+
}
215+
216+
/**
217+
* Fetch child contexts for a parent context.
218+
*/
219+
export async function fetchContextChildren(
220+
contextId: string,
221+
options: FetchContextChildrenOptions = {}
222+
): Promise<ContextChildrenResponse> {
223+
const params = new URLSearchParams();
224+
if (options.recursive) {
225+
params.set('recursive', '1');
226+
}
227+
if (options.limit !== undefined) {
228+
params.set('limit', String(options.limit));
229+
}
230+
if (options.include_provenance !== false) {
231+
params.set('include_provenance', '1');
232+
}
233+
if (options.include_lineage !== false) {
234+
params.set('include_lineage', '1');
235+
}
236+
237+
const queryString = params.toString();
238+
const url = `${API_BASE}/contexts/${encodeURIComponent(contextId)}/children${queryString ? `?${queryString}` : ''}`;
239+
const response = await fetch(url);
240+
241+
if (!response.ok) {
242+
let errorData: ErrorResponse | undefined;
243+
try {
244+
errorData = await response.json();
245+
} catch {
246+
// Ignore JSON parse errors
247+
}
248+
throw new ApiError(
249+
errorData?.error?.message || `HTTP ${response.status}`,
250+
errorData?.error?.code || response.status,
251+
errorData
252+
);
253+
}
254+
255+
return response.json();
256+
}
257+
156258
/**
157259
* Search response from CQL query.
158260
*/

frontend/types/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export interface ContextEntry {
101101
labels?: string[];
102102
// Provenance (origin story)
103103
provenance?: import('./provenance').Provenance;
104+
// Server-provided lineage summary (optional, include_lineage=1)
105+
lineage?: {
106+
parent_context_id?: string;
107+
root_context_id?: string;
108+
spawn_reason?: string;
109+
child_context_count: number;
110+
child_context_ids: string[];
111+
};
104112
}
105113

106114
// ============================================
@@ -141,6 +149,13 @@ export interface ContextMetadataUpdatedEvent {
141149
has_provenance: boolean;
142150
}
143151

152+
export interface ContextLinkedEvent {
153+
child_context_id: string;
154+
parent_context_id: string;
155+
root_context_id?: string;
156+
spawn_reason?: string;
157+
}
158+
144159
export interface ClientConnectedEvent {
145160
session_id: string;
146161
client_tag: string;
@@ -164,6 +179,7 @@ export interface ErrorOccurredEvent {
164179
export type StoreEvent =
165180
| { type: 'context_created'; data: ContextCreatedEvent }
166181
| { type: 'context_metadata_updated'; data: ContextMetadataUpdatedEvent }
182+
| { type: 'context_linked'; data: ContextLinkedEvent }
167183
| { type: 'turn_appended'; data: TurnAppendedEvent }
168184
| { type: 'client_connected'; data: ClientConnectedEvent }
169185
| { type: 'client_disconnected'; data: ClientDisconnectedEvent }

0 commit comments

Comments
 (0)