antechamber 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +17 -14
  3. package/bin/opencode-antechamber +10 -1
  4. package/bin/opencode-pi +11 -1
  5. package/dist/approvals.d.ts +4 -1
  6. package/dist/approvals.js +51 -33
  7. package/dist/catalog.d.ts +1 -1
  8. package/dist/catalog.js +1 -1
  9. package/dist/commands/context.d.ts +0 -1
  10. package/dist/commands/context.js +1 -1
  11. package/dist/commands/doctor.d.ts +3 -2
  12. package/dist/commands/serve.d.ts +2 -1
  13. package/dist/commands/setup.d.ts +2 -3
  14. package/dist/commands/setup.js +16 -4
  15. package/dist/http.d.ts +8 -1
  16. package/dist/http.js +42 -19
  17. package/dist/pi-event-translation.d.ts +9 -5
  18. package/dist/pi-event-translation.js +233 -79
  19. package/dist/pi-rpc.d.ts +20 -2
  20. package/dist/pi-rpc.js +96 -29
  21. package/dist/prompt-parts.d.ts +40 -0
  22. package/dist/prompt-parts.js +157 -0
  23. package/dist/route-services.d.ts +45 -2
  24. package/dist/route-services.js +70 -3
  25. package/dist/routes-v1.js +110 -65
  26. package/dist/routes-v2.js +376 -73
  27. package/dist/server.d.ts +9 -1
  28. package/dist/server.js +109 -9
  29. package/dist/session-commands.d.ts +12 -15
  30. package/dist/session-commands.js +52 -11
  31. package/dist/session-diff.d.ts +1 -1
  32. package/dist/session-diff.js +10 -2
  33. package/dist/session-history.js +108 -42
  34. package/dist/session-hosts.js +2 -2
  35. package/dist/session-store.d.ts +8 -4
  36. package/dist/session-store.js +183 -33
  37. package/dist/session-tree.d.ts +9 -1
  38. package/dist/session-tree.js +51 -26
  39. package/dist/sessions.d.ts +32 -5
  40. package/dist/sessions.js +330 -119
  41. package/dist/sse.d.ts +16 -6
  42. package/dist/sse.js +77 -21
  43. package/dist/subagents.d.ts +5 -0
  44. package/dist/subagents.js +84 -71
  45. package/dist/tool-map.d.ts +3 -2
  46. package/dist/types.d.ts +152 -1
  47. package/dist/v2.d.ts +18 -4
  48. package/dist/v2.js +128 -44
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/extensions/omo-dag/README.md +2 -0
  52. package/extensions/omo-dag/docs/DESIGN.md +10 -1
  53. package/extensions/omo-dag/panel/index.html +1 -1
  54. package/extensions/omo-dag/panel/main.js +60 -60
  55. package/extensions/omo-dag/service/main.js +72 -8
  56. package/package.json +1 -1
package/dist/sse.d.ts CHANGED
@@ -2,18 +2,28 @@
2
2
  import type { ServerResponse } from "node:http";
3
3
  import type { OCEvent } from "./types.js";
4
4
  import type { TodoUpdated } from "./todo-bridge.js";
5
+ export type SSESubscriber = {
6
+ flavor: "v1" | "v2";
7
+ directory?: string;
8
+ global: boolean;
9
+ };
5
10
  export declare class SSE {
6
11
  private options;
7
- /** SSE subscribers tagged by protocol flavor: v1 (`/event`) or v2 (`/api/event`). */
8
- readonly clients: Map<ServerResponse, "v1" | "v2">;
12
+ /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
13
+ readonly clients: Map<ServerResponse, SSESubscriber>;
9
14
  private v2events;
15
+ private nextEventID;
16
+ private replay;
10
17
  constructor(options: {
11
18
  defaultDirectory: string;
12
19
  },
13
- /** SSE subscribers tagged by protocol flavor: v1 (`/event`) or v2 (`/api/event`). */
14
- clients: Map<ServerResponse, "v1" | "v2">);
20
+ /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
21
+ clients: Map<ServerResponse, SSESubscriber>);
15
22
  close(): void;
16
- broadcast(_directory: string, event: OCEvent | TodoUpdated): void;
23
+ broadcast(directory: string, event: OCEvent | TodoUpdated): void;
17
24
  writeSSE(res: ServerResponse, frame: string): boolean;
18
- handleSSE(res: ServerResponse, flavor: "v1" | "v2", write: (res: ServerResponse, frame: string) => boolean): void;
25
+ handleSSE(res: ServerResponse, subscriber: SSESubscriber, lastEventID: string | undefined, write: (res: ServerResponse, frame: string) => boolean): void;
26
+ private record;
27
+ private retain;
28
+ private writeRecord;
19
29
  }
package/dist/sse.js CHANGED
@@ -4,12 +4,15 @@ import { errorBody, json } from "./http.js";
4
4
  import { V2EventTranslator } from "./v2.js";
5
5
  const MAX_SSE_SUBSCRIBERS = 64;
6
6
  const MAX_SSE_PENDING_BYTES = 8 * 1024 * 1024;
7
+ const MAX_SSE_REPLAY_RECORDS = 1024;
7
8
  export class SSE {
8
9
  options;
9
10
  clients;
10
11
  v2events = new V2EventTranslator();
12
+ nextEventID = 0;
13
+ replay = [];
11
14
  constructor(options,
12
- /** SSE subscribers tagged by protocol flavor: v1 (`/event`) or v2 (`/api/event`). */
15
+ /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
13
16
  clients) {
14
17
  this.options = options;
15
18
  this.clients = clients;
@@ -20,25 +23,25 @@ export class SSE {
20
23
  res.end();
21
24
  }
22
25
  }
23
- broadcast(_directory, event) {
24
- const v1Frame = `data: ${JSON.stringify(event)}\n\n`;
25
- const frames = event.type === "todo.updated"
26
- ? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory: _directory }, data: event.properties }]
27
- : this.v2events.translate(_directory, event);
28
- const v2Frames = frames.map((frame) => `data: ${JSON.stringify(frame)}\n\n`);
29
- for (const [res, flavor] of this.clients) {
26
+ broadcast(directory, event) {
27
+ const v2Frames = event.type === "todo.updated"
28
+ ? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
29
+ : this.v2events.translate(directory, event);
30
+ const records = v2Frames.length === 0
31
+ ? [this.record(directory, { v1: event })]
32
+ : v2Frames.map((v2, index) => this.record(directory, index === 0 ? { v1: event, v2 } : { v2 }));
33
+ for (const record of records)
34
+ this.retain(record);
35
+ for (const [res, subscriber] of this.clients) {
30
36
  if (res.writableEnded || res.destroyed) {
31
37
  this.clients.delete(res);
32
38
  continue;
33
39
  }
34
- if (flavor === "v2") {
35
- for (const frame of v2Frames) {
36
- if (!this.writeSSE(res, frame))
37
- break;
38
- }
39
- }
40
- else {
41
- this.writeSSE(res, v1Frame);
40
+ for (const record of records) {
41
+ if (!subscriber.global && subscriber.directory !== directory)
42
+ continue;
43
+ if (!this.writeRecord(res, subscriber, record))
44
+ break;
42
45
  }
43
46
  }
44
47
  }
@@ -56,7 +59,7 @@ export class SSE {
56
59
  res.destroy();
57
60
  return false;
58
61
  }
59
- handleSSE(res, flavor, write) {
62
+ handleSSE(res, subscriber, lastEventID, write) {
60
63
  if (this.clients.size >= MAX_SSE_SUBSCRIBERS) {
61
64
  json(res, 503, errorBody("ServiceUnavailableError", "Too many SSE subscribers"));
62
65
  return;
@@ -66,7 +69,7 @@ export class SSE {
66
69
  "Cache-Control": "no-cache",
67
70
  Connection: "keep-alive",
68
71
  });
69
- this.clients.set(res, flavor);
72
+ this.clients.set(res, subscriber);
70
73
  // OpenChamber's SSE proxy aborts an upstream that is silent for 20s, so
71
74
  // mirror real opencode's `: heartbeat` comment keepalive.
72
75
  const heartbeat = setInterval(() => {
@@ -78,15 +81,68 @@ export class SSE {
78
81
  this.clients.delete(res);
79
82
  });
80
83
  // OpenCode emits server.connected on subscribe; OpenChamber waits for it.
81
- const connected = flavor === "v2"
84
+ const connected = subscriber.flavor === "v2"
82
85
  ? {
83
86
  id: `evt_${Date.now()}`,
84
87
  created: Date.now(),
85
88
  type: "server.connected",
86
- location: { directory: this.options.defaultDirectory },
89
+ location: { directory: subscriber.directory ?? this.options.defaultDirectory },
87
90
  data: {},
88
91
  }
89
92
  : { type: "server.connected", properties: {} };
90
- write(res, `data: ${JSON.stringify(connected)}\n\n`);
93
+ const initial = subscriber.flavor === "v1" && subscriber.global
94
+ ? {
95
+ directory: subscriber.directory ?? this.options.defaultDirectory,
96
+ payload: { id: `evt_${randomUUID()}`, ...connected },
97
+ }
98
+ : connected;
99
+ write(res, `data: ${JSON.stringify(initial)}\n\n`);
100
+ const cursor = Number(lastEventID);
101
+ if (!Number.isSafeInteger(cursor) || cursor < 0)
102
+ return;
103
+ for (const record of this.replay) {
104
+ if (record.id > cursor && !this.writeRecord(res, subscriber, record))
105
+ break;
106
+ }
107
+ }
108
+ record(directory, frames) {
109
+ return { id: ++this.nextEventID, directory, ...frames };
110
+ }
111
+ retain(record) {
112
+ this.replay.push(record);
113
+ if (this.replay.length > MAX_SSE_REPLAY_RECORDS)
114
+ this.replay.shift();
115
+ }
116
+ writeRecord(res, subscriber, record) {
117
+ if (!subscriber.global && subscriber.directory !== record.directory)
118
+ return true;
119
+ const data = subscriber.flavor === "v2"
120
+ ? record.v2
121
+ : record.v1 === undefined
122
+ ? undefined
123
+ : subscriber.global
124
+ ? {
125
+ directory: record.directory,
126
+ project: record.directory,
127
+ payload: {
128
+ id: `evt_${record.id}`,
129
+ ...record.v1,
130
+ properties: {
131
+ ...record.v1.properties,
132
+ ...(record.v1.type === "session.created" ||
133
+ record.v1.type === "session.updated" ||
134
+ record.v1.type === "session.deleted"
135
+ ? { sessionID: record.v1.properties.info.id }
136
+ : {}),
137
+ },
138
+ },
139
+ }
140
+ : record.v1;
141
+ if (data === undefined)
142
+ return true;
143
+ const frame = subscriber.flavor === "v1" && !subscriber.global
144
+ ? `data: ${JSON.stringify(data)}\n\n`
145
+ : `id: ${record.id}\ndata: ${JSON.stringify(data)}\n\n`;
146
+ return this.writeSSE(res, frame);
91
147
  }
92
148
  }
@@ -46,11 +46,16 @@ export declare class SubagentBridge {
46
46
  private discoverParts;
47
47
  private create;
48
48
  restore(id: string): ManagedSession | undefined;
49
+ private managedSession;
49
50
  restoreAll(): void;
50
51
  owns(id: string): boolean;
51
52
  messages(id: string): OCMessageWithParts[];
52
53
  private watch;
53
54
  private refresh;
55
+ private reconcileTaskRecord;
56
+ private hydrateTranscript;
57
+ private reconcileRuntime;
58
+ private publishRefresh;
54
59
  private saveArchive;
55
60
  private fallback;
56
61
  private persist;
package/dist/subagents.js CHANGED
@@ -295,26 +295,15 @@ export class SubagentBridge {
295
295
  }
296
296
  }
297
297
  create(parent, task) {
298
- const session = {
299
- info: {
300
- id: ocIdFor(task.id),
301
- parentID: parent.info.id,
302
- projectID: parent.info.projectID,
303
- directory: parent.info.directory,
304
- title: typeof task.input.description === "string" ? task.input.description : task.id,
305
- version: parent.info.version,
306
- time: { ...parent.info.time },
307
- },
308
- pi: undefined,
309
- messages: undefined,
310
- model: undefined,
311
- streaming: undefined,
312
- status: { type: "idle" },
313
- toolByCallId: new Map(),
314
- tree: { messageEntries: {} },
315
- hasPendingInteraction: () => false,
316
- hasRunningChildren: () => false,
317
- };
298
+ const session = this.managedSession({
299
+ id: ocIdFor(task.id),
300
+ parentID: parent.info.id,
301
+ projectID: parent.info.projectID,
302
+ directory: parent.info.directory,
303
+ title: typeof task.input.description === "string" ? task.input.description : task.id,
304
+ version: parent.info.version,
305
+ time: { ...parent.info.time },
306
+ });
318
307
  const child = {
319
308
  session,
320
309
  task,
@@ -339,15 +328,21 @@ export class SubagentBridge {
339
328
  const entry = this.store.get(id);
340
329
  if (!entry?.task || !entry.parentID)
341
330
  return undefined;
342
- const parent = {
343
- info: {
344
- id: entry.parentID,
345
- projectID: entry.directory,
346
- directory: entry.directory,
347
- title: "",
348
- version: ADAPTER_VERSION,
349
- time: { created: entry.created, updated: entry.updated },
350
- },
331
+ const parent = this.managedSession({
332
+ id: entry.parentID,
333
+ projectID: entry.directory,
334
+ directory: entry.directory,
335
+ title: "",
336
+ version: ADAPTER_VERSION,
337
+ time: { created: entry.created, updated: entry.updated },
338
+ });
339
+ const child = this.create(parent, entry.task);
340
+ child.session.info.title = entry.title ?? child.session.info.title;
341
+ return child.session;
342
+ }
343
+ managedSession(info) {
344
+ return {
345
+ info,
351
346
  pi: undefined,
352
347
  messages: undefined,
353
348
  model: undefined,
@@ -358,9 +353,6 @@ export class SubagentBridge {
358
353
  hasPendingInteraction: () => false,
359
354
  hasRunningChildren: () => false,
360
355
  };
361
- const child = this.create(parent, entry.task);
362
- child.session.info.title = entry.title ?? child.session.info.title;
363
- return child.session;
364
356
  }
365
357
  restoreAll() {
366
358
  for (const id of this.store.childIds())
@@ -396,44 +388,63 @@ export class SubagentBridge {
396
388
  }
397
389
  refresh(child, publish, useRecord = true) {
398
390
  const archive = this.archive(child.task.id);
391
+ const found = this.reconcileTaskRecord(child, archive, useRecord);
392
+ const running = active(child.task.details.status) && (found !== undefined || child.live);
393
+ const state = {
394
+ previousMessages: child.session.messages ?? [],
395
+ previousStatus: child.session.status.type,
396
+ running,
397
+ costChanged: this.hydrateTranscript(child, archive, running, child.session.status.type),
398
+ updatedParts: this.reconcileRuntime(child, running),
399
+ };
400
+ if (state.costChanged)
401
+ this.emit(child.session.info.directory, { type: "session.updated", properties: { info: child.session.info } });
402
+ if (publish)
403
+ for (const part of state.updatedParts)
404
+ this.emitPart(child.session, part);
405
+ this.persist(child);
406
+ this.publishRefresh(child, state, publish);
407
+ }
408
+ reconcileTaskRecord(child, archive, useRecord) {
399
409
  const found = findTask(child.task.id, child.task.parentPiId);
400
- if (found) {
401
- child.recordPath = found.path;
402
- const updated = typeof found.data.updated_at === "string" ? Date.parse(found.data.updated_at) : NaN;
403
- const retainedTerminal = Math.max(child.task.terminalAt ?? 0, existsSync(archive) ? statSync(archive).mtimeMs : 0);
404
- const staleRunning = active(found.data.status) && terminal(child.task.details.status) && updated <= retainedTerminal;
405
- if (useRecord && !staleRunning)
406
- child.task.details = { ...child.task.details, ...found.data };
407
- const host = found.data.host_session;
408
- if (record(host) && typeof host.session_path === "string")
409
- child.livePath = host.session_path;
410
- else if (typeof found.data.child_session_id === "string" && found.data.child_session_id !== "") {
411
- // In-process agents persist under the task store but have no RPC host_session.
412
- // Match the recorded session, not another retained run in the same directory.
413
- const sessions = join(dirname(dirname(found.path)), "children", child.task.id, "sessions", child.task.id);
414
- try {
415
- const file = readdirSync(sessions, { withFileTypes: true }).find((entry) => entry.isFile() && entry.name.endsWith(`_${found.data.child_session_id}.jsonl`));
416
- child.livePath = file ? join(sessions, file.name) : undefined;
417
- }
418
- catch (error) {
419
- if (!(error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code))))
420
- throw error;
421
- child.livePath = undefined;
422
- }
423
- }
410
+ if (!found)
411
+ return undefined;
412
+ child.recordPath = found.path;
413
+ const updated = typeof found.data.updated_at === "string" ? Date.parse(found.data.updated_at) : NaN;
414
+ const retainedTerminal = Math.max(child.task.terminalAt ?? 0, existsSync(archive) ? statSync(archive).mtimeMs : 0);
415
+ const staleRunning = active(found.data.status) && terminal(child.task.details.status) && updated <= retainedTerminal;
416
+ if (useRecord && !staleRunning)
417
+ child.task.details = { ...child.task.details, ...found.data };
418
+ const host = found.data.host_session;
419
+ if (record(host) && typeof host.session_path === "string") {
420
+ child.livePath = host.session_path;
421
+ return found;
424
422
  }
425
- const running = active(child.task.details.status) && (found !== undefined || child.live);
426
- const previousMessages = child.session.messages ?? [];
427
- const previousStatus = child.session.status.type;
423
+ if (typeof found.data.child_session_id !== "string" || found.data.child_session_id === "")
424
+ return found;
425
+ // In-process agents persist under the task store but have no RPC host_session.
426
+ // Match the recorded session, not another retained run in the same directory.
427
+ const sessions = join(dirname(dirname(found.path)), "children", child.task.id, "sessions", child.task.id);
428
+ try {
429
+ const file = readdirSync(sessions, { withFileTypes: true }).find((entry) => entry.isFile() && entry.name.endsWith(`_${found.data.child_session_id}.jsonl`));
430
+ child.livePath = file ? join(sessions, file.name) : undefined;
431
+ }
432
+ catch (error) {
433
+ if (!(error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code))))
434
+ throw error;
435
+ child.livePath = undefined;
436
+ }
437
+ return found;
438
+ }
439
+ hydrateTranscript(child, archive, running, previousStatus) {
428
440
  // A resumed live task can be newer than its archive; completed history prefers the archive.
429
441
  const liveRun = running || previousStatus === "busy";
430
442
  let raw = liveRun ? undefined : optionalFile(archive);
431
443
  if (running && child.recordPath)
432
444
  this.watch(child, child.recordPath);
433
445
  if (raw === undefined && child.livePath !== undefined) {
434
- if (running) {
446
+ if (running)
435
447
  this.watch(child, child.livePath);
436
- }
437
448
  child.tail.read(child.livePath);
438
449
  if (child.tail.text !== "")
439
450
  raw = child.tail.text;
@@ -459,36 +470,38 @@ export class SubagentBridge {
459
470
  }
460
471
  child.loaded = true;
461
472
  // Each child reports only its own spend; OpenChamber sums the subtree by parentID.
462
- if (syncSessionCost(child.session))
463
- this.emit(child.session.info.directory, { type: "session.updated", properties: { info: child.session.info } });
473
+ return syncSessionCost(child.session);
474
+ }
475
+ reconcileRuntime(child, running) {
464
476
  child.session.status = { type: running ? "busy" : "idle" };
465
477
  if (!running)
466
478
  this.closeWatchers(child);
467
- for (const part of child.parts.values()) {
479
+ const parts = [...child.parts.values()];
480
+ for (const part of parts) {
468
481
  updateTaskParts([part], {
469
482
  details: child.task.details,
470
483
  content: [{ type: "text", text: child.task.details.final_response ?? "" }],
471
484
  }, !running, Date.now());
472
- if (publish)
473
- this.emitPart(child.session, part);
474
485
  }
475
- this.persist(child);
486
+ return parts;
487
+ }
488
+ publishRefresh(child, state, publish) {
476
489
  if (!publish)
477
490
  return;
478
- const before = new Map(previousMessages.map((message) => [message.info.id, JSON.stringify(message)]));
479
- for (const message of child.session.messages) {
491
+ const before = new Map(state.previousMessages.map((message) => [message.info.id, JSON.stringify(message)]));
492
+ for (const message of child.session.messages ?? []) {
480
493
  if (before.get(message.info.id) === JSON.stringify(message))
481
494
  continue;
482
495
  this.emit(child.session.info.directory, { type: "message.updated", properties: { info: message.info } });
483
496
  for (const part of message.parts)
484
497
  this.emitPart(child.session, part);
485
498
  }
486
- if (previousStatus !== child.session.status.type) {
499
+ if (state.previousStatus !== child.session.status.type) {
487
500
  this.emit(child.session.info.directory, {
488
501
  type: "session.status",
489
502
  properties: { sessionID: child.session.info.id, status: child.session.status },
490
503
  });
491
- if (!running)
504
+ if (!state.running)
492
505
  this.emit(child.session.info.directory, {
493
506
  type: "session.idle",
494
507
  properties: { sessionID: child.session.info.id },
@@ -1,6 +1,6 @@
1
1
  import type { OCToolPart } from "./types.js";
2
2
  /** OmO arguments translated to OpenChamber's renderer contracts. */
3
- export interface MappedTool {
3
+ interface MappedTool {
4
4
  tool: string;
5
5
  input: Record<string, unknown>;
6
6
  title: string;
@@ -12,7 +12,7 @@ export interface ToolResult {
12
12
  details?: unknown;
13
13
  isError?: boolean;
14
14
  }
15
- export type ToolMapper = (args: Record<string, unknown>, result?: ToolResult) => MappedTool;
15
+ type ToolMapper = (args: Record<string, unknown>, result?: ToolResult) => MappedTool;
16
16
  /** Mutable registry: todos 6/7 can assign handlers for `todo` and `task`. */
17
17
  export declare const toolMappers: Record<string, ToolMapper>;
18
18
  /** Text blocks only; images and structured details are not tool output text. */
@@ -30,3 +30,4 @@ export declare function providerToolParts(content: unknown, sessionID: string, m
30
30
  export declare function applyProviderToolResults(content: unknown, parts: Iterable<OCToolPart>, timestamp: number): OCToolPart[];
31
31
  /** Only an actual result (or a self-contained provider status) means success. */
32
32
  export declare function finishProviderToolParts(parts: Iterable<OCToolPart>, timestamp: number, error?: string): OCToolPart[];
33
+ export {};
package/dist/types.d.ts CHANGED
@@ -12,6 +12,31 @@ export interface OCSession {
12
12
  projectID: string;
13
13
  directory: string;
14
14
  parentID?: string;
15
+ parentMessageID?: string;
16
+ agent?: string;
17
+ model?: {
18
+ id: string;
19
+ providerID: string;
20
+ variant?: string;
21
+ };
22
+ metadata?: Record<string, unknown>;
23
+ outcome?: "succeeded" | "failed" | "interrupted";
24
+ slug?: string;
25
+ path?: string;
26
+ summary?: {
27
+ additions: number;
28
+ deletions: number;
29
+ files: number;
30
+ };
31
+ tokens?: {
32
+ input: number;
33
+ output: number;
34
+ reasoning: number;
35
+ cache: {
36
+ read: number;
37
+ write: number;
38
+ };
39
+ };
15
40
  title: string;
16
41
  version: string;
17
42
  revert?: {
@@ -22,6 +47,7 @@ export interface OCSession {
22
47
  time: {
23
48
  created: number;
24
49
  updated: number;
50
+ idle?: number;
25
51
  compacting?: number;
26
52
  /** Epoch ms — presence marks the session archived (SDK v2 session.update). */
27
53
  archived?: number;
@@ -36,6 +62,10 @@ export interface OCUserMessage {
36
62
  };
37
63
  agent: string;
38
64
  model: OCModelRef;
65
+ /** No-reply context admitted without a turn (v2 lists it as `type: "synthetic"`). */
66
+ synthetic?: boolean;
67
+ /** Client metadata sent with the prompt (OpenChamber 2.x). */
68
+ metadata?: Record<string, unknown>;
39
69
  }
40
70
  export interface OCAssistantMessage {
41
71
  id: string;
@@ -156,7 +186,34 @@ export interface OCCompactionPart {
156
186
  type: "compaction";
157
187
  auto: boolean;
158
188
  }
159
- export type OCPart = OCTextPart | OCReasoningPart | OCToolPart | OCCompactionPart;
189
+ export type OCPromptPart = ({
190
+ type: "text";
191
+ text: string;
192
+ synthetic?: boolean;
193
+ } | {
194
+ type: "file";
195
+ mime: string;
196
+ url: string;
197
+ filename?: string;
198
+ } | {
199
+ type: "agent";
200
+ name: string;
201
+ source?: {
202
+ value: string;
203
+ start: number;
204
+ end: number;
205
+ };
206
+ }) & {
207
+ id?: string;
208
+ };
209
+ export type OCInputPart = OCPromptPart & {
210
+ id: string;
211
+ sessionID: string;
212
+ messageID: string;
213
+ };
214
+ export type OCPart = OCTextPart | OCReasoningPart | OCToolPart | OCCompactionPart | Exclude<OCInputPart, {
215
+ type: "text";
216
+ }>;
160
217
  export interface OCMessageWithParts {
161
218
  info: OCMessage;
162
219
  parts: OCPart[];
@@ -181,6 +238,15 @@ export type OCEvent = {
181
238
  delivery: "steer" | "queue";
182
239
  payload: {
183
240
  text: string;
241
+ files?: Array<Record<string, unknown>>;
242
+ agents?: Array<Record<string, unknown>>;
243
+ metadata?: Record<string, unknown>;
244
+ };
245
+ } | {
246
+ type: "synthetic";
247
+ payload: {
248
+ text: string;
249
+ metadata?: Record<string, unknown>;
184
250
  };
185
251
  };
186
252
  };
@@ -209,6 +275,79 @@ export type OCEvent = {
209
275
  properties: {
210
276
  info: OCSession;
211
277
  };
278
+ } | {
279
+ type: "session.revert.staged";
280
+ properties: {
281
+ sessionID: string;
282
+ revert: {
283
+ messageID: string;
284
+ };
285
+ };
286
+ } | {
287
+ type: "session.revert.cleared";
288
+ properties: {
289
+ sessionID: string;
290
+ };
291
+ } | {
292
+ type: "session.revert.committed";
293
+ properties: {
294
+ sessionID: string;
295
+ to: string;
296
+ };
297
+ } | {
298
+ type: "session.compaction.started";
299
+ properties: {
300
+ sessionID: string;
301
+ reason: string;
302
+ inputID: string;
303
+ };
304
+ } | {
305
+ type: "session.compaction.ended";
306
+ properties: {
307
+ sessionID: string;
308
+ reason: string;
309
+ inputID: string;
310
+ text: string;
311
+ recent: string;
312
+ };
313
+ } | {
314
+ type: "session.compaction.failed";
315
+ properties: {
316
+ sessionID: string;
317
+ reason: string;
318
+ inputID: string;
319
+ error: {
320
+ name: string;
321
+ data: {
322
+ message: string;
323
+ };
324
+ };
325
+ };
326
+ } | {
327
+ type: "session.shell.started" | "session.shell.ended";
328
+ properties: {
329
+ sessionID: string;
330
+ shell: {
331
+ id: string;
332
+ command: string;
333
+ cwd: string;
334
+ shell: string;
335
+ file: string;
336
+ status: "running" | "exited" | "timeout" | "killed";
337
+ exit?: number;
338
+ metadata: Record<string, unknown>;
339
+ time: {
340
+ started: number;
341
+ completed?: number;
342
+ };
343
+ };
344
+ output?: {
345
+ output: string;
346
+ cursor: number;
347
+ size: number;
348
+ truncated: boolean;
349
+ };
350
+ };
212
351
  } | {
213
352
  type: "session.deleted";
214
353
  properties: {
@@ -253,6 +392,15 @@ export type OCEvent = {
253
392
  part: OCPart;
254
393
  delta?: string;
255
394
  };
395
+ } | {
396
+ type: "message.part.delta";
397
+ properties: {
398
+ sessionID: string;
399
+ messageID: string;
400
+ partID: string;
401
+ field: "text";
402
+ delta: string;
403
+ };
256
404
  } | {
257
405
  type: "permission.asked";
258
406
  properties: {
@@ -332,12 +480,15 @@ export interface OCProject {
332
480
  vcs?: "git";
333
481
  time: {
334
482
  created: number;
483
+ updated?: number;
335
484
  initialized?: number;
336
485
  };
486
+ sandboxes?: string[];
337
487
  }
338
488
  /** Incoming prompt body for POST /session/{id}/message and /prompt_async */
339
489
  export interface OCPromptBody {
340
490
  messageID?: string;
491
+ delivery?: "steer" | "queue";
341
492
  model?: OCModelRef;
342
493
  agent?: string;
343
494
  noReply?: boolean;