@hachej/boring-agent 0.4.19 → 0.4.21

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 (2) hide show
  1. package/dist/room.js +32 -10
  2. package/package.json +3 -3
package/dist/room.js CHANGED
@@ -287,19 +287,38 @@ export function createRoomTurn(options) {
287
287
  if (index >= 0)
288
288
  change(items, index);
289
289
  }, context);
290
- /** Relay the run's frames and each new `ask_user` while the wake is open. */
291
- async function follow(wakeId, from) {
290
+ /**
291
+ * Relay the run's frames and each new `ask_user` while the wake is open. A queued wake has no entry yet: its input
292
+ * lands when the earlier run is done, so follow from the start and learn `from` the first time its entry exists.
293
+ * Nothing is relayed before then, and never anything at or below the input's own entry (that is another wake's run).
294
+ * Text is relayed as growth: per entry the text length and step count already sent are remembered, only the rest goes.
295
+ */
296
+ async function follow(wakeId, known) {
292
297
  const watch = await conversation.watch(context);
293
- const seen = new Set();
298
+ const sent = new Map();
299
+ let from = known;
294
300
  let queue = Promise.resolve();
295
301
  // Port calls are chained off the watch so a slow room never holds native frames.
296
302
  const relay = async (view) => {
297
- const fresh = view.entries.filter(entry => entry.id > from && !seen.has(entry.id));
298
- for (const entry of fresh)
299
- seen.add(entry.id);
300
- const frames = framesOf(fresh);
301
- const questions = pendingQuestions(view.entries);
302
303
  queue = queue.then(async () => {
304
+ if (from === undefined)
305
+ from = (await conversation.commit(tx => tx.submissionByRequest(conversation.id, requestId(wakeId)), context))?.entry;
306
+ if (from === undefined)
307
+ return;
308
+ const own = view.entries.filter(entry => entry.id > from);
309
+ const frames = [];
310
+ for (const entry of own) {
311
+ const before = sent.get(entry.id) ?? { text: 0, steps: 0 };
312
+ const all = framesOf([entry]);
313
+ const text = all.flatMap(frame => frame.type === 'text' ? [frame.text] : []).join('\n\n');
314
+ const steps = all.filter(frame => frame.type === 'step');
315
+ // Steps first: a tool call that began after the text belongs after it, but a lost order costs only polish.
316
+ frames.push(...steps.slice(before.steps));
317
+ if (text.length > before.text)
318
+ frames.push({ type: 'text', text: text.slice(before.text) });
319
+ sent.set(entry.id, { text: Math.max(before.text, text.length), steps: Math.max(before.steps, steps.length) });
320
+ }
321
+ const questions = pendingQuestions(own);
303
322
  if (frames.length && port.stream)
304
323
  await port.stream(wakeId, frames);
305
324
  if (!port.question)
@@ -328,7 +347,8 @@ export function createRoomTurn(options) {
328
347
  // The same `requestId` returns the first submission: a resend or a restart never starts a second run.
329
348
  const submission = await conversation.submit({ type: 'input', requestId: requestId(wakeId), content: item.input, whenBusy: 'followUp' }, context);
330
349
  const placed = await conversation.commit(tx => tx.submissionByRequest(conversation.id, requestId(wakeId)), context);
331
- const stop = placed?.entry === undefined || placed.status === 'done' || placed.status === 'unanswered' ? async () => { } : await follow(wakeId, placed.entry);
350
+ // A queued submission has no entry yet; it is followed all the same (see `follow`).
351
+ const stop = placed?.status === 'done' || placed?.status === 'unanswered' ? async () => { } : await follow(wakeId, placed?.entry);
332
352
  let record;
333
353
  try {
334
354
  record = await submission.wait(context);
@@ -336,11 +356,13 @@ export function createRoomTurn(options) {
336
356
  finally {
337
357
  await stop();
338
358
  }
339
- // An unanswered run leaves the wake open: the room's resend cap closes it with its own line.
359
+ // An unanswered run is closed with the room's own line, never dropped: left open it would only wait for its resends.
340
360
  if (record.type === 'input' && record.status === 'done') {
341
361
  const entry = (await conversation.entries({ minEntryId: record.answer, maxEntryId: record.answer }, 1, undefined, context)).items[0];
342
362
  await port.settle(wakeId, { text: answerText(entry) });
343
363
  }
364
+ else if (record.type === 'input' && record.status === 'unanswered')
365
+ await port.settle(wakeId, { text: ROOM.unanswered });
344
366
  await update(wakeId, (items, index) => { items.splice(index, 1); });
345
367
  })().catch(report).finally(() => delivering.delete(wakeId));
346
368
  delivering.set(wakeId, run);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -160,8 +160,8 @@
160
160
  "@earendil-works/pi-durable": "~1.2.0",
161
161
  "@earendil-works/pi-ai": "~1.2.0",
162
162
  "@earendil-works/chord": "~1.2.0",
163
- "@hachej/boring-files": "^0.4.19",
164
- "@hachej/boring-ui-kit": "^0.4.19",
163
+ "@hachej/boring-files": "^0.4.21",
164
+ "@hachej/boring-ui-kit": "^0.4.21",
165
165
  "xlsx": "^0.20.3"
166
166
  },
167
167
  "peerDependenciesMeta": {