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
@@ -2,7 +2,7 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultMetadata, resultText, } from "./tool-map.js";
4
4
  import { TodoBridge } from "./todo-bridge.js";
5
- import { recordTurn } from "./session-tree.js";
5
+ import { readSessionTree, recordTurn } from "./session-tree.js";
6
6
  import { touchedFiles } from "./session-diff.js";
7
7
  import { getAgentName } from "./agent-name.js";
8
8
  import { debugLog } from "./misc-routes.js";
@@ -42,7 +42,7 @@ export function textOf(content) {
42
42
  return "";
43
43
  return content.map((b) => (b?.type === "text" && typeof b.text === "string" ? b.text : "")).join("");
44
44
  }
45
- export function newPart(kind, sessionID, messageID) {
45
+ function newPart(kind, sessionID, messageID) {
46
46
  const base = {
47
47
  id: id("prt"),
48
48
  sessionID,
@@ -54,7 +54,7 @@ export function newPart(kind, sessionID, messageID) {
54
54
  ? { ...base, type: "text" }
55
55
  : { ...base, type: "reasoning" };
56
56
  }
57
- export function applyUsage(msg, usage) {
57
+ function applyUsage(msg, usage) {
58
58
  if (!usage)
59
59
  return;
60
60
  msg.tokens = {
@@ -75,9 +75,17 @@ export function syncSessionCost(s) {
75
75
  if (s.messages === undefined)
76
76
  return false;
77
77
  let cost = 0;
78
+ const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
78
79
  for (const message of s.messages)
79
- if (message.info.role === "assistant")
80
+ if (message.info.role === "assistant") {
80
81
  cost += message.info.cost;
82
+ tokens.input += message.info.tokens.input;
83
+ tokens.output += message.info.tokens.output;
84
+ tokens.reasoning += message.info.tokens.reasoning;
85
+ tokens.cache.read += message.info.tokens.cache.read;
86
+ tokens.cache.write += message.info.tokens.cache.write;
87
+ }
88
+ s.info.tokens = tokens;
81
89
  if ((s.info.cost ?? 0) === cost)
82
90
  return false; // absent means zero; avoid no-op updates
83
91
  s.info.cost = cost;
@@ -86,9 +94,8 @@ export function syncSessionCost(s) {
86
94
  /**
87
95
  * An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
88
96
  * part (OpenChamber renders it as `/compact`) answered by a `summary` message.
89
- * IDs derive from the compaction's first kept entry, which the live
90
- * `compaction_end` result and the persisted entry share, so a history reload
91
- * reconciles with the live messages instead of duplicating them.
97
+ * IDs derive from the native compaction entry, not its retained boundary:
98
+ * multiple compactions can retain the same entry.
92
99
  */
93
100
  export function compactionMessages(s, key, summary, created, model) {
94
101
  const sessionID = s.info.id;
@@ -135,6 +142,33 @@ export class PiEventTranslator {
135
142
  constructor(services) {
136
143
  this.services = services;
137
144
  }
145
+ /** Close dead-runtime UI state before an error or idle event can reach the client. */
146
+ finishInterruptedRun(s, error) {
147
+ const now = Date.now();
148
+ s.approvals?.finishTurn();
149
+ for (const part of s.streaming?.parts.values() ?? []) {
150
+ if (part.type === "tool")
151
+ s.toolByCallId.set(part.callID, part);
152
+ else if ((part.type === "text" || part.type === "reasoning") &&
153
+ part.time !== undefined &&
154
+ part.time.end === undefined) {
155
+ part.time.end = now;
156
+ this.emitPart(s, part);
157
+ }
158
+ }
159
+ this.finalizeDanglingTools(s);
160
+ this.services.subagents.finishCalls(s.info.id);
161
+ if (s.streaming !== undefined) {
162
+ const message = s.streaming.message;
163
+ message.finish = "error";
164
+ message.error = error;
165
+ message.time.completed = now;
166
+ this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message } });
167
+ this.finishStreaming(s);
168
+ }
169
+ delete s.compactingFromIdle;
170
+ delete s.info.time.compacting;
171
+ }
138
172
  setStatus(s, status) {
139
173
  s.status = status;
140
174
  this.services.emit(s.info.directory, {
@@ -142,6 +176,14 @@ export class PiEventTranslator {
142
176
  properties: { sessionID: s.info.id, status },
143
177
  });
144
178
  if (status.type === "idle") {
179
+ s.info.time.idle = Date.now();
180
+ const last = [...(s.messages ?? [])].reverse().find((message) => message.info.role === "assistant");
181
+ s.info.outcome =
182
+ s.interruptRequested || (last?.info.role === "assistant" && last.info.error?.name === "MessageAbortedError")
183
+ ? "interrupted"
184
+ : last?.info.role === "assistant" && last.info.finish === "error"
185
+ ? "failed"
186
+ : "succeeded";
145
187
  this.services.emit(s.info.directory, { type: "session.idle", properties: { sessionID: s.info.id } });
146
188
  }
147
189
  }
@@ -206,28 +248,59 @@ export class PiEventTranslator {
206
248
  }
207
249
  onPiEvent(s, ev) {
208
250
  this.services.subagents.wake(s, ev);
209
- const dir = s.info.directory;
210
251
  if (envSetting("DEBUG")) {
211
252
  const ame = ev.assistantMessageEvent?.type;
212
253
  debugLog(`[pi-event] ${String(ev.type)}${ame ? `/${ame}` : ""} ${JSON.stringify(ev).slice(0, 300)}`);
213
254
  }
214
255
  switch (ev.type) {
215
- case "entry_appended": {
216
- s.todoBridge ??= new TodoBridge(s.info.id);
217
- const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
218
- if (event !== undefined)
219
- this.services.emit(dir, event);
256
+ case "entry_appended":
257
+ this.onTodoEntry(s, ev);
220
258
  break;
221
- }
222
259
  case "session_info_changed":
223
260
  this.services.applyNativeTitle(s, ev.name);
224
261
  break;
225
262
  case "agent_start":
226
- this.setStatus(s, { type: "busy" });
263
+ case "agent_idle":
264
+ case "agent_end":
265
+ case "auto_retry_start":
266
+ case "turn_end":
267
+ case "extension_error":
268
+ this.onLifecycleEvent(s, ev);
269
+ break;
270
+ case "tool_execution_start":
271
+ case "tool_execution_update":
272
+ case "tool_execution_end":
273
+ this.onToolEvent(s, ev);
227
274
  break;
228
- case "agent_end": {
275
+ case "message_start":
276
+ case "message_update":
277
+ case "message_end":
278
+ this.onMessageEvent(s, ev);
279
+ break;
280
+ case "compaction_start":
281
+ case "compaction_end":
282
+ this.onCompactionEvent(s, ev);
283
+ break;
284
+ default:
285
+ break; // turn_start, queue_update — future milestones
286
+ }
287
+ }
288
+ onTodoEntry(s, ev) {
289
+ s.todoBridge ??= new TodoBridge(s.info.id);
290
+ const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
291
+ if (event !== undefined)
292
+ this.services.emit(s.info.directory, event);
293
+ }
294
+ onLifecycleEvent(s, ev) {
295
+ switch (ev.type) {
296
+ case "agent_start":
297
+ this.setStatus(s, { type: "busy" });
298
+ return;
299
+ case "agent_end":
229
300
  if (ev.willRetry === true)
230
- break; // auto-retry keeps the session busy
301
+ return; // auto-retry keeps the session busy
302
+ if (s.interruptRequested)
303
+ this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
231
304
  s.approvals?.finishTurn();
232
305
  this.finalizeDanglingTools(s);
233
306
  this.services.subagents.finishCalls(s.info.id);
@@ -235,18 +308,28 @@ export class PiEventTranslator {
235
308
  this.services.emitDiff(s);
236
309
  s.info.time.updated = Date.now();
237
310
  this.services.persistMeta(s);
238
- this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
311
+ this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
239
312
  if (s.piPath !== undefined)
240
313
  this.services.store.update(s.info.id, { updated: Date.now() });
241
314
  recordTurn(s);
242
315
  this.services.persistMeta(s);
243
- break;
244
- }
316
+ return;
245
317
  case "agent_idle":
246
318
  // agent_end closes one run, not the session: OmO may still retry or
247
319
  // start a deferred continuation. Only agent_idle settles that work.
320
+ if (s.interruptRequested) {
321
+ this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
322
+ this.services.emit(s.info.directory, {
323
+ type: "session.error",
324
+ properties: {
325
+ sessionID: s.info.id,
326
+ error: { name: "MessageAbortedError", data: { message: "Cancelled" } },
327
+ },
328
+ });
329
+ delete s.interruptRequested;
330
+ }
248
331
  this.setStatus(s, { type: "idle" });
249
- break;
332
+ return;
250
333
  case "turn_end": {
251
334
  s.approvals?.finishTurn();
252
335
  // pi delivers tool results here, matched back to parts by toolCallId.
@@ -254,18 +337,41 @@ export class PiEventTranslator {
254
337
  if (Array.isArray(results))
255
338
  this.applyToolResults(s, results);
256
339
  this.services.emitDiff(s);
257
- break;
340
+ return;
258
341
  }
342
+ case "auto_retry_start":
343
+ this.setStatus(s, {
344
+ type: "retry",
345
+ attempt: Number(ev.attempt ?? 1),
346
+ message: String(ev.errorMessage ?? "retrying"),
347
+ next: Date.now() + Number(ev.delayMs ?? 0),
348
+ });
349
+ return;
350
+ case "extension_error":
351
+ this.services.emit(s.info.directory, {
352
+ type: "session.error",
353
+ properties: {
354
+ sessionID: s.info.id,
355
+ error: { name: "UnknownError", data: { message: String(ev.error ?? "pi extension error") } },
356
+ },
357
+ });
358
+ return;
359
+ default:
360
+ return;
361
+ }
362
+ }
363
+ onToolEvent(s, ev) {
364
+ switch (ev.type) {
259
365
  case "tool_execution_start": {
260
366
  if (typeof ev.toolCallId === "string" && typeof ev.toolName === "string") {
261
367
  s.approvals?.noteToolStart(ev.toolCallId, ev.toolName, ev.args);
262
368
  }
263
369
  if (ev.toolName === "task")
264
- break; // Expanded when the argument stream closes.
370
+ return; // Expanded when the argument stream closes.
265
371
  const callID = typeof ev.toolCallId === "string" ? ev.toolCallId : undefined;
266
372
  const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
267
373
  if (part === undefined)
268
- break;
374
+ return;
269
375
  const name = typeof ev.toolName === "string" ? ev.toolName : part.tool;
270
376
  const mapped = mapTool(name, name === "todo" ? part.state.input : ev.args);
271
377
  part.tool = mapped.tool;
@@ -277,31 +383,37 @@ export class PiEventTranslator {
277
383
  time: { start: part.state.status !== "pending" ? part.state.time.start : Date.now() },
278
384
  };
279
385
  this.emitPart(s, part);
280
- break;
386
+ return;
281
387
  }
282
388
  case "tool_execution_update": {
283
389
  if (typeof ev.toolCallId === "string" &&
284
390
  this.services.subagents.result(s, ev.toolCallId, ev.partialResult !== null && typeof ev.partialResult === "object" ? ev.partialResult : {}, false))
285
- break;
391
+ return;
286
392
  const part = typeof ev.toolCallId === "string" ? s.toolByCallId.get(ev.toolCallId) : undefined;
287
393
  if (part === undefined || part.state.status !== "running")
288
- break;
394
+ return;
289
395
  this.emitPart(s, part);
290
- break;
396
+ return;
291
397
  }
292
398
  case "tool_execution_end": {
293
399
  if (typeof ev.toolCallId === "string")
294
400
  s.approvals?.noteToolEnd(ev.toolCallId);
295
401
  const result = ev.result !== null && typeof ev.result === "object" ? ev.result : {};
296
402
  this.applyToolResults(s, [{ ...result, toolCallId: ev.toolCallId, isError: ev.isError }]);
297
- break;
403
+ return;
298
404
  }
405
+ default:
406
+ return;
407
+ }
408
+ }
409
+ onMessageEvent(s, ev) {
410
+ switch (ev.type) {
299
411
  case "message_start": {
300
412
  if (this.reconcileUser(s, ev))
301
- break;
413
+ return;
302
414
  const msg = ev.message;
303
415
  if (msg?.role !== "assistant")
304
- break; // user message already recorded at prompt()
416
+ return; // user message already recorded at prompt()
305
417
  // pi stamps the assistant message with the model really serving it.
306
418
  // Trust that over our snapshot, which goes stale whenever pi switches on
307
419
  // its own side (subagent routing, an in-session /model, a resumed file).
@@ -324,91 +436,121 @@ export class PiEventTranslator {
324
436
  };
325
437
  s.streaming = { message: assistant, parts: new Map() };
326
438
  s.messages?.push({ info: assistant, parts: [] });
327
- this.services.emit(dir, { type: "message.updated", properties: { info: assistant } });
328
- break;
439
+ this.services.emit(s.info.directory, { type: "message.updated", properties: { info: assistant } });
440
+ return;
329
441
  }
330
442
  case "message_update":
331
443
  this.onMessageUpdate(s, ev);
332
- break;
444
+ return;
333
445
  case "message_end": {
334
446
  if (this.reconcileUser(s, ev))
335
- break;
447
+ return;
336
448
  const ended = ev.message;
337
- if (ended?.role === "assistant" && s.streaming) {
338
- this.onProviderTools(s, ev.message);
339
- if (ended.stopReason === "aborted" || ended.stopReason === "error") {
340
- for (const part of finishProviderToolParts(s.toolByCallId.values(), Date.now(), ended.stopReason === "aborted" ? "Cancelled" : "No result"))
341
- this.emitPart(s, part);
342
- }
343
- // Last chance to correct attribution: some providers only reveal the
344
- // concrete model once the stream has closed.
345
- if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
346
- s.model !== undefined) {
347
- s.streaming.message.providerID = s.model.providerID;
348
- s.streaming.message.modelID = s.model.modelID;
349
- }
350
- applyUsage(s.streaming.message, ended.usage);
351
- Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
352
- s.streaming.message.time.completed = Date.now();
353
- this.services.emit(dir, { type: "message.updated", properties: { info: s.streaming.message } });
354
- this.finishStreaming(s);
355
- if (syncSessionCost(s))
356
- this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
449
+ if (ended?.role !== "assistant" || !s.streaming)
450
+ return;
451
+ this.onProviderTools(s, ev.message);
452
+ if (ended.stopReason === "aborted" || ended.stopReason === "error") {
453
+ for (const part of finishProviderToolParts(s.toolByCallId.values(), Date.now(), ended.stopReason === "aborted" ? "Cancelled" : "No result"))
454
+ this.emitPart(s, part);
357
455
  }
358
- break;
456
+ // Last chance to correct attribution: some providers only reveal the
457
+ // concrete model once the stream has closed.
458
+ if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
459
+ s.model !== undefined) {
460
+ s.streaming.message.providerID = s.model.providerID;
461
+ s.streaming.message.modelID = s.model.modelID;
462
+ }
463
+ applyUsage(s.streaming.message, ended.usage);
464
+ Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
465
+ s.streaming.message.time.completed = Date.now();
466
+ this.services.emit(s.info.directory, { type: "message.updated", properties: { info: s.streaming.message } });
467
+ this.finishStreaming(s);
468
+ if (syncSessionCost(s))
469
+ this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
470
+ return;
359
471
  }
360
- case "auto_retry_start":
361
- this.setStatus(s, {
362
- type: "retry",
363
- attempt: Number(ev.attempt ?? 1),
364
- message: String(ev.errorMessage ?? "retrying"),
365
- next: Date.now() + Number(ev.delayMs ?? 0),
366
- });
367
- break;
368
- case "extension_error":
369
- this.services.emit(dir, {
370
- type: "session.error",
472
+ default:
473
+ return;
474
+ }
475
+ }
476
+ onCompactionEvent(s, ev) {
477
+ switch (ev.type) {
478
+ case "compaction_start":
479
+ s.compactionID = id("compaction");
480
+ this.services.emit(s.info.directory, {
481
+ type: "session.compaction.started",
371
482
  properties: {
372
483
  sessionID: s.info.id,
373
- error: { name: "UnknownError", data: { message: String(ev.error ?? "pi extension error") } },
484
+ inputID: s.compactionID,
485
+ reason: typeof ev.reason === "string" ? ev.reason : "manual",
374
486
  },
375
487
  });
376
- break;
377
- case "compaction_start": {
378
488
  s.info.time.compacting = Date.now();
379
- this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
489
+ this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
380
490
  // A manual /compact runs no agent turn, so nothing else marks the session busy.
381
491
  if (s.status.type === "idle") {
382
492
  s.compactingFromIdle = true;
383
493
  this.setStatus(s, { type: "busy" });
384
494
  }
385
- break;
386
- }
495
+ return;
387
496
  case "compaction_end": {
388
497
  delete s.info.time.compacting;
389
498
  const result = ev.result;
499
+ const inputID = s.compactionID ?? id("compaction");
500
+ delete s.compactionID;
501
+ this.services.emit(s.info.directory, ev.aborted === true || typeof result?.summary !== "string"
502
+ ? {
503
+ type: "session.compaction.failed",
504
+ properties: {
505
+ sessionID: s.info.id,
506
+ inputID,
507
+ reason: typeof ev.reason === "string" ? ev.reason : "manual",
508
+ error: {
509
+ name: "CompactionError",
510
+ data: { message: ev.aborted === true ? "Compaction cancelled" : "Compaction failed" },
511
+ },
512
+ },
513
+ }
514
+ : {
515
+ type: "session.compaction.ended",
516
+ properties: {
517
+ sessionID: s.info.id,
518
+ inputID,
519
+ reason: typeof ev.reason === "string" ? ev.reason : "manual",
520
+ text: result.summary,
521
+ recent: "",
522
+ },
523
+ });
390
524
  if (ev.aborted !== true && typeof result?.summary === "string" && typeof result.firstKeptEntryId === "string") {
391
525
  const messages = this.services.ensureMessages(s);
392
- for (const message of compactionMessages(s, result.firstKeptEntryId, result.summary, Date.now(), s.model)) {
526
+ let key;
527
+ // compaction_end follows persistence, but its result omits the entry ID.
528
+ if (s.piPath !== undefined) {
529
+ for (const entry of readSessionTree(s.piPath).entries) {
530
+ if (entry.type === "compaction")
531
+ key = entry.id;
532
+ }
533
+ }
534
+ for (const message of compactionMessages(s, key ?? id("compaction"), result.summary, Date.now(), s.model)) {
393
535
  if (messages.some((item) => item.info.id === message.info.id))
394
536
  continue;
395
537
  // Keep a hidden redo branch after the new turn, as appendMessage does.
396
538
  const boundary = messages.findIndex((item) => item.info.id === s.tree.revert?.messageID);
397
539
  messages.splice(boundary < 0 ? messages.length : boundary, 0, message);
398
- this.services.emit(dir, { type: "message.updated", properties: { info: message.info } });
540
+ this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message.info } });
399
541
  for (const part of message.parts)
400
542
  this.emitPart(s, part);
401
543
  }
402
544
  }
403
- this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
545
+ this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
404
546
  if (s.compactingFromIdle) {
405
547
  delete s.compactingFromIdle;
406
548
  this.setStatus(s, { type: "idle" });
407
549
  }
408
- break;
550
+ return;
409
551
  }
410
552
  default:
411
- break; // turn_start, queue_update — future milestones
553
+ return;
412
554
  }
413
555
  }
414
556
  finishStreaming(s) {
@@ -449,7 +591,19 @@ export class PiEventTranslator {
449
591
  const textPart = part;
450
592
  if (ame.type === `${wire}_delta` && typeof ame.delta === "string") {
451
593
  textPart.text += ame.delta;
452
- this.emitPart(s, textPart, ame.delta);
594
+ this.services.emit(dir, {
595
+ type: "message.part.delta",
596
+ properties: {
597
+ sessionID: s.info.id,
598
+ messageID: textPart.messageID,
599
+ partID: textPart.id,
600
+ field: "text",
601
+ delta: ame.delta,
602
+ },
603
+ });
604
+ // Append before publishing the authoritative snapshot. Sending another
605
+ // append delta on the snapshot would duplicate text in streaming clients.
606
+ this.emitPart(s, textPart);
453
607
  }
454
608
  else if (ame.type === `${wire}_end`) {
455
609
  if (typeof ame.content === "string")
package/dist/pi-rpc.d.ts CHANGED
@@ -21,7 +21,20 @@ export declare const RPC_COMMAND_TIMEOUT_MS = 60000;
21
21
  * long before a large session finished, leaving the run orphaned.
22
22
  */
23
23
  export declare const COMPACT_RPC_TIMEOUT_MS: number;
24
- export declare const MAX_JSONL_FRAME_BYTES: number;
24
+ export declare class UpstreamProtocolError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ /** A process's group and kernel start time: with the PID, the identity that survives PID reuse checks. */
28
+ interface ProcessIdentity {
29
+ readonly pid: number;
30
+ readonly group: number;
31
+ readonly start: string;
32
+ }
33
+ /** Reads process identities so disposal can tell owned survivors from a reused PID. */
34
+ export interface ProcessTable {
35
+ identity(pid: number): ProcessIdentity | undefined;
36
+ groupMembers(group: number): ProcessIdentity[];
37
+ }
25
38
  export type PiRpcResponse<T = unknown> = {
26
39
  readonly id: string;
27
40
  readonly type: "response";
@@ -35,13 +48,14 @@ export type PiRpcResponse<T = unknown> = {
35
48
  readonly error?: string;
36
49
  readonly data?: never;
37
50
  });
38
- export interface PiRpcClientOptions {
51
+ interface PiRpcClientOptions {
39
52
  cwd: string;
40
53
  /** Path/name of the pi binary. Defaults to ANTECHAMBER_PI_BINARY env or "omo" from PATH. */
41
54
  piBinary?: string;
42
55
  /** Extra CLI flags, e.g. ["--no-session"] or ["--provider", "x"]. */
43
56
  args?: string[];
44
57
  clock?: LifecycleClock;
58
+ processTable?: ProcessTable;
45
59
  }
46
60
  export declare class PiRpcClient extends EventEmitter {
47
61
  private options;
@@ -61,6 +75,9 @@ export declare class PiRpcClient extends EventEmitter {
61
75
  constructor(options: PiRpcClientOptions);
62
76
  private drain;
63
77
  private frameOverflow;
78
+ /** Node has reaped the leader, so its PID (and group ID) may already belong to another process. */
79
+ private reaped;
80
+ private closeForTransportFailure;
64
81
  private handleLine;
65
82
  private handleExtensionUI;
66
83
  respondExtensionUI(response: Record<string, unknown>): void;
@@ -72,3 +89,4 @@ export declare class PiRpcClient extends EventEmitter {
72
89
  notify(cmd: Omit<PiRpcCommand, "id">): void;
73
90
  dispose(): Promise<void>;
74
91
  }
92
+ export {};