@agentshouse/kit 0.2.1-c70284f7a7466 → 0.2.1-c7a397cb0cce7

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.
@@ -4,6 +4,7 @@ import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
6
  import { startAdapter } from './acp.js';
7
+ import { finalRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
10
  import { answeredFailure, readerFor } from './events.js';
@@ -11,6 +12,7 @@ import { placeFiles, uploadBytes } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
13
14
  import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
14
16
  import { killMarked, Scope } from './scope.js';
15
17
  import { holdSecretInput } from './secret-input.js';
16
18
  import { createHowWeWork } from './skills.js';
@@ -19,10 +21,17 @@ import { workFor } from './work.js';
19
21
  const SIGNED_OUT = RequestError.authRequired().code;
20
22
  const KILLED = 'the conversation was killed';
21
23
  const REFUSED = 'the CLI refused the prompt';
22
- const NO_CONNECTION = "the Kit could not open this conversation's connection";
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
23
31
  const NO_START = "the Agent's CLI did not start";
24
32
  const NO_SESSION = "the Agent's CLI did not open a session";
25
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
26
35
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
27
36
  class NotStarted extends Error {
28
37
  }
@@ -31,9 +40,20 @@ async function step(reason, work) {
31
40
  return await work;
32
41
  }
33
42
  catch (error) {
34
- throw new NotStarted(reason, { cause: error });
43
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
35
44
  }
36
45
  }
46
+ function signedOut(error) {
47
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
48
+ }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
54
+ function unbridged(error) {
55
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
56
+ }
37
57
  const EXIT_PLAN_MODE = {
38
58
  parse(value) {
39
59
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -47,8 +67,7 @@ function causeOf(error) {
47
67
  return error instanceof Error ? error.message : String(error);
48
68
  }
49
69
  function answeredWith(error) {
50
- const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
- return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
52
71
  }
53
72
  function exitedWith(running, cause) {
54
73
  return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
@@ -72,10 +91,10 @@ function marker(bridge) {
72
91
  class Turn {
73
92
  id = randomUUID();
74
93
  parts;
75
- admitted = false;
76
94
  questions = 0;
77
95
  secrets = 0;
78
96
  prompted = false;
97
+ taken = null;
79
98
  ended = false;
80
99
  images = 0;
81
100
  uploads = [];
@@ -89,6 +108,8 @@ class Conversation {
89
108
  session = null;
90
109
  turn = null;
91
110
  commands = null;
111
+ options = null;
112
+ exitUntold = false;
92
113
  context = null;
93
114
  leftovers = new Set();
94
115
  waiting = [];
@@ -96,6 +117,7 @@ class Conversation {
96
117
  subagents = new Map();
97
118
  held = [];
98
119
  opening = null;
120
+ launch = null;
99
121
  prompted = null;
100
122
  kills = 0;
101
123
  killed = new AbortController();
@@ -104,6 +126,11 @@ class Conversation {
104
126
  open = 0;
105
127
  queue = Promise.resolve();
106
128
  reports = Promise.resolve();
129
+ starting = null;
130
+ preparing = null;
131
+ told = null;
132
+ untold = false;
133
+ shown = null;
107
134
  constructor(id) {
108
135
  this.id = id;
109
136
  }
@@ -119,9 +146,7 @@ function asksSecret(request) {
119
146
  export class Conversations {
120
147
  kit;
121
148
  conversations = new Map();
122
- acks = new Map();
123
149
  questions = new Map();
124
- carrying = 0;
125
150
  reporting = 0;
126
151
  watched = false;
127
152
  constructor(kit) {
@@ -130,37 +155,26 @@ export class Conversations {
130
155
  setInterval(() => this.look(), 2_000);
131
156
  }
132
157
  input(input) {
133
- this.carrying++;
134
- const carried = this.acks.get(input.input_id) ??
135
- (() => {
136
- const conversation = this.conversation(input.conversation_id);
137
- if (input.kind === 'kill') {
138
- conversation.kills++;
139
- conversation.killed.abort(new Error(KILLED));
140
- for (const waiting of conversation.waiting.splice(0))
141
- waiting.settle({ refused: KILLED });
142
- this.halt(conversation);
143
- }
144
- if (input.kind === 'interrupt') {
145
- conversation.interrupts++;
146
- conversation.interrupted.abort();
147
- }
148
- const handled = conversation.queue.then(() => this.carry(conversation, input));
149
- conversation.queue = handled.catch(() => undefined);
150
- const work = handled.then(({ ack }) => ack);
151
- this.acks.set(input.input_id, work);
152
- return work;
153
- })();
154
- carried
155
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
156
- .catch(logged)
157
- .finally(() => {
158
- this.carrying--;
159
- this.kit.changed();
158
+ void this.kit.inputs.carry(input.input_id, () => {
159
+ const conversation = this.conversation(input.conversation_id);
160
+ if (input.kind === 'kill') {
161
+ conversation.kills++;
162
+ conversation.killed.abort(new Error(KILLED));
163
+ for (const waiting of conversation.waiting.splice(0))
164
+ waiting.settle({ refused: KILLED });
165
+ this.halt(conversation);
166
+ }
167
+ if (input.kind === 'interrupt') {
168
+ conversation.interrupts++;
169
+ conversation.interrupted.abort();
170
+ }
171
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
172
+ conversation.queue = handled.catch(() => undefined);
173
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
160
174
  });
161
175
  }
162
176
  idle() {
163
- if (this.carrying > 0 || this.reporting > 0)
177
+ if (this.reporting > 0)
164
178
  return false;
165
179
  for (const conversation of this.conversations.values()) {
166
180
  const turn = conversation.turn;
@@ -189,6 +203,14 @@ export class Conversations {
189
203
  }
190
204
  opened() {
191
205
  this.watched = false;
206
+ for (const conversation of this.conversations.values()) {
207
+ if (conversation.running !== null)
208
+ this.facts(conversation);
209
+ else if (conversation.exitUntold)
210
+ this.process(conversation, false);
211
+ if (conversation.untold || conversation.told !== null)
212
+ this.retell(conversation);
213
+ }
192
214
  }
193
215
  watching(watched) {
194
216
  const resend = watched && !this.watched;
@@ -210,9 +232,10 @@ export class Conversations {
210
232
  }
211
233
  async carry(conversation, input) {
212
234
  if (input.kind === 'open')
213
- return { ack: Promise.resolve(await this.open(conversation, input)) };
235
+ return { ack: this.open(conversation, input) };
214
236
  if (input.kind === 'message')
215
237
  return this.message(conversation, input);
238
+ await this.launched(conversation);
216
239
  if (input.kind === 'interrupt') {
217
240
  if (--conversation.interrupts === 0)
218
241
  conversation.interrupted = new AbortController();
@@ -236,22 +259,58 @@ export class Conversations {
236
259
  }
237
260
  return { ack: Promise.resolve({}) };
238
261
  }
239
- async open(conversation, input) {
262
+ open(conversation, input) {
240
263
  if (conversation.running !== null)
241
- return {};
242
- try {
243
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
244
- }
245
- catch (error) {
246
- return { refused: refusal(conversation, error) };
247
- }
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
266
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
267
+ }
268
+ launch(conversation, route, sessionId, empty) {
269
+ let opened;
270
+ let failed;
271
+ const bridge = new Promise((resolve, reject) => {
272
+ opened = resolve;
273
+ failed = reject;
274
+ });
275
+ bridge.catch(() => undefined);
276
+ const session = this.start(conversation, route, sessionId, empty, opened);
277
+ session.catch(failed);
278
+ const launch = { bridge, session };
279
+ conversation.launch = launch;
280
+ void session
281
+ .catch(() => undefined)
282
+ .then(() => {
283
+ if (conversation.launch !== launch)
284
+ return;
285
+ conversation.launch = null;
286
+ this.next(conversation);
287
+ });
288
+ this.next(conversation);
289
+ return launch;
290
+ }
291
+ async launched(conversation) {
292
+ while (conversation.launch !== null)
293
+ await conversation.launch.session.catch(() => undefined);
294
+ }
295
+ instructed(conversation, route) {
296
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
297
+ if (bridge === undefined)
298
+ return null;
299
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
300
+ read.catch(() => undefined);
301
+ return read;
248
302
  }
249
303
  async message(conversation, input) {
250
304
  const content = [{ type: 'text', text: String(input.text) }];
251
305
  let route;
306
+ let instructing = null;
252
307
  try {
253
308
  route = await this.route(conversation, input.agent_id);
309
+ if (input.first === true)
310
+ instructing = this.instructed(conversation, route);
254
311
  const files = input.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
255
314
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
256
315
  const lines = await this.transcribed(conversation, files, paths);
257
316
  if (lines.length > 0)
@@ -260,11 +319,13 @@ export class Conversations {
260
319
  throw new Error(KILLED);
261
320
  }
262
321
  catch (error) {
322
+ this.prepares(conversation, null);
263
323
  return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
264
324
  }
325
+ conversation.preparing = null;
265
326
  return {
266
327
  ack: new Promise((settle) => {
267
- conversation.waiting.push({ input, route, content, settle });
328
+ conversation.waiting.push({ input, route, content, instructing, settle });
268
329
  this.next(conversation);
269
330
  }),
270
331
  };
@@ -273,8 +334,12 @@ export class Conversations {
273
334
  if (conversation.sending !== null || conversation.open > 0)
274
335
  return;
275
336
  const waiting = conversation.waiting.shift();
276
- if (waiting === undefined)
337
+ if (waiting === undefined) {
338
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
339
+ if (conversation.starting !== step)
340
+ this.starts(conversation, step);
277
341
  return;
342
+ }
278
343
  conversation.sending = this.send(conversation, waiting).finally(() => {
279
344
  conversation.sending = null;
280
345
  this.next(conversation);
@@ -284,27 +349,33 @@ export class Conversations {
284
349
  const { input } = waiting;
285
350
  let ack = {};
286
351
  let started = false;
352
+ let instructing = waiting.instructing;
287
353
  const prompt = [];
288
354
  try {
355
+ if (conversation.running === null) {
356
+ this.starts(conversation, 'session');
357
+ await this.launched(conversation);
358
+ }
289
359
  if (conversation.running === null) {
290
360
  const stored = conversation.session ?? input.provider_session_id;
291
361
  const empty = input.first === true && conversation.prompted !== stored;
292
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (input.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
293
366
  }
294
367
  const session = conversation.running.sessionId;
295
368
  if (started || (input.first === true && session !== input.provider_session_id)) {
296
369
  ack = { provider_session_id: session };
297
370
  }
298
371
  if (input.first === true) {
299
- prompt.push({
300
- type: 'text',
301
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
302
- });
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
303
373
  }
304
374
  if (conversation.kills > 0)
305
375
  throw new Error(KILLED);
306
376
  }
307
377
  catch (error) {
378
+ this.starts(conversation, null);
308
379
  if (started) {
309
380
  conversation.session = null;
310
381
  await this.kill(conversation);
@@ -312,15 +383,45 @@ export class Conversations {
312
383
  waiting.settle({ refused: refusal(conversation, error) });
313
384
  return;
314
385
  }
315
- this.prompt(conversation, [...prompt, ...waiting.content]);
386
+ this.starts(conversation, null);
387
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
316
388
  waiting.settle(ack);
317
389
  }
390
+ starts(conversation, step) {
391
+ conversation.starting = step;
392
+ if (conversation.shown !== null)
393
+ clearTimeout(conversation.shown);
394
+ conversation.shown = null;
395
+ if (step === null || conversation.told !== null) {
396
+ this.tell(conversation, step);
397
+ return;
398
+ }
399
+ conversation.shown = setTimeout(() => {
400
+ conversation.shown = null;
401
+ if (conversation.starting === step)
402
+ this.tell(conversation, step);
403
+ }, STARTING_SHOWN_AFTER_MS);
404
+ }
405
+ prepares(conversation, step) {
406
+ conversation.preparing = step;
407
+ this.next(conversation);
408
+ }
409
+ tell(conversation, step) {
410
+ if (conversation.told === step && !conversation.untold)
411
+ return;
412
+ conversation.told = step;
413
+ this.retell(conversation);
414
+ }
415
+ retell(conversation) {
416
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
417
+ }
318
418
  async transcribed(conversation, files, paths) {
319
419
  const lines = [];
320
420
  for (const [index, path] of paths.entries()) {
321
421
  lines.push(path);
322
422
  if (files[index].media_type?.startsWith('audio/') !== true)
323
423
  continue;
424
+ this.prepares(conversation, 'transcription');
324
425
  try {
325
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
326
427
  if (text !== null && text !== '')
@@ -387,7 +488,8 @@ export class Conversations {
387
488
  }
388
489
  return route;
389
490
  }
390
- async start(conversation, route, sessionId, empty) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
391
493
  const reader = readerFor(route.kind);
392
494
  reader.session = sessionId;
393
495
  const app = client({ name: '@agentshouse/kit' })
@@ -395,7 +497,7 @@ export class Conversations {
395
497
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
396
498
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
397
499
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
398
- const bridge = await step(NO_CONNECTION, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
500
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
399
501
  if (conversation.kills > 0) {
400
502
  bridge.close();
401
503
  throw new Error(KILLED);
@@ -403,11 +505,13 @@ export class Conversations {
403
505
  conversation.opening = bridge;
404
506
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
405
507
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
+ let adapter = null;
406
509
  try {
407
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
+ bridged(bridge);
408
512
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
409
513
  const streamed = await step(NO_CONNECTION, work.open(cli));
410
- const adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
514
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
411
515
  work.read(message);
412
516
  this.read(conversation, reader, message);
413
517
  }, { ...bridge.env, ...streamed }, undefined, scope));
@@ -416,26 +520,24 @@ export class Conversations {
416
520
  const cwd = route.working_directory;
417
521
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
418
522
  const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
419
- const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
523
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
420
524
  reader.session = opened.sessionId;
421
525
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
422
526
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
423
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
424
528
  }
425
529
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
426
530
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
427
531
  void work.settled().then(() => scope.record());
428
532
  conversation.running = running;
429
533
  conversation.session = running.sessionId;
430
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
431
- if (conversation.commands !== null)
432
- this.commands(conversation, conversation.commands);
433
- this.options(conversation, options);
434
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
534
+ conversation.options = options;
535
+ this.facts(conversation);
536
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
435
537
  return running.sessionId;
436
538
  }
437
539
  catch (error) {
438
- scope.kill();
540
+ adapter?.retire();
439
541
  work.close();
440
542
  conversation.leftovers.add(scope);
441
543
  bridge.close();
@@ -451,7 +553,7 @@ export class Conversations {
451
553
  }
452
554
  catch (error) {
453
555
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
454
- throw new NotStarted(NO_RESUME, { cause: error });
556
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
455
557
  }
456
558
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
457
559
  return null;
@@ -478,10 +580,7 @@ export class Conversations {
478
580
  }
479
581
  return options;
480
582
  }
481
- exited(conversation, adapter, cause) {
482
- const running = conversation.running;
483
- if (running?.adapter !== adapter)
484
- return;
583
+ exited(conversation, running, cause) {
485
584
  this.detach(conversation, running);
486
585
  if (conversation.turn !== null) {
487
586
  this.end(conversation, conversation.turn, exitedWith(running, cause));
@@ -494,11 +593,22 @@ export class Conversations {
494
593
  conversation.leftovers.add(running.scope);
495
594
  conversation.running = null;
496
595
  conversation.commands = null;
596
+ conversation.options = null;
497
597
  for (const [id, question] of this.questions) {
498
598
  if (question.conversation === conversation)
499
599
  this.questions.delete(id);
500
600
  }
501
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
601
+ this.process(conversation, false);
602
+ }
603
+ process(conversation, running) {
604
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
605
+ }
606
+ facts(conversation) {
607
+ this.process(conversation, true);
608
+ if (conversation.commands !== null)
609
+ this.commands(conversation, conversation.commands);
610
+ if (conversation.options !== null)
611
+ this.options(conversation, conversation.options);
502
612
  }
503
613
  commands(conversation, commands) {
504
614
  conversation.commands = commands;
@@ -509,6 +619,7 @@ export class Conversations {
509
619
  options(conversation, options) {
510
620
  if (conversation.running === null)
511
621
  return;
622
+ conversation.options = options;
512
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
513
624
  }
514
625
  deliver(conversation, turn, event, body) {
@@ -523,7 +634,7 @@ export class Conversations {
523
634
  return delivered;
524
635
  }
525
636
  write(conversation, turn, operation) {
526
- if (!turn.admitted || turn.ended || !this.watched)
637
+ if (turn.ended || !this.watched)
527
638
  return;
528
639
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
529
640
  }
@@ -534,12 +645,7 @@ export class Conversations {
534
645
  begin(conversation) {
535
646
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
536
647
  conversation.open++;
537
- void this.deliver(conversation, turn, 'started', {})
538
- .then(() => {
539
- turn.admitted = true;
540
- this.replay(conversation, turn);
541
- })
542
- .catch(logged);
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
543
649
  conversation.turn = turn;
544
650
  if (conversation.context !== null)
545
651
  turn.parts.measure(conversation.context);
@@ -572,12 +678,14 @@ export class Conversations {
572
678
  this.next(conversation);
573
679
  this.kit.changed();
574
680
  }
575
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
576
682
  const running = conversation.running;
577
683
  const turn = this.begin(conversation);
578
684
  turn.prompted = true;
685
+ turn.taken = taken;
579
686
  conversation.prompted = running.sessionId;
580
687
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
688
+ this.took(turn);
581
689
  const notice = answeredFailure(answer);
582
690
  if (notice !== null) {
583
691
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -590,8 +698,9 @@ export class Conversations {
590
698
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
591
699
  }
592
700
  else {
701
+ this.took(turn);
593
702
  if (conversation.open === 1 && conversation.running === running) {
594
- running.scope.kill();
703
+ running.adapter.retire();
595
704
  this.detach(conversation, running);
596
705
  }
597
706
  this.end(conversation, turn, answeredWith(error));
@@ -599,7 +708,9 @@ export class Conversations {
599
708
  });
600
709
  }
601
710
  started(conversation) {
602
- return conversation.turn ?? this.begin(conversation);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
713
+ return turn;
603
714
  }
604
715
  ended(conversation) {
605
716
  const turn = conversation.turn;
@@ -694,6 +805,11 @@ export class Conversations {
694
805
  }
695
806
  this.apply(conversation, this.started(conversation), event);
696
807
  }
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
812
+ }
697
813
  apply(conversation, turn, event) {
698
814
  switch (event.kind) {
699
815
  case 'text':
package/dist/copies.js CHANGED
@@ -58,6 +58,9 @@ function expiredAuthority(error) {
58
58
  return undefined;
59
59
  }
60
60
  }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
61
64
  function covers(path, prefix) {
62
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
63
66
  }
@@ -219,26 +222,35 @@ export class LocalCopies {
219
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
220
223
  .sort();
221
224
  }
222
- async connected() {
223
- const copied = await this.answered(() => this.fetched(false));
224
- if (copied.refused)
225
- logged(copied.text);
225
+ connected() {
226
+ return this.selected();
227
+ }
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
226
231
  }
227
232
  fetch() {
228
233
  return this.answered(() => this.fetched(true));
229
234
  }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
230
240
  answered(work) {
231
241
  const answered = this.answering.then(work);
232
242
  this.answering = answered.catch(() => undefined);
233
243
  return answered;
234
244
  }
235
- async fetched(refreshing) {
236
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
245
+ async fetched(refreshing, rooms) {
246
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
237
247
  const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
238
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
248
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
249
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
250
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
239
251
  const said = [];
240
252
  let refused = false;
241
- for (const copy of [...this.copies.values()]) {
253
+ for (const copy of held()) {
242
254
  const returned = synced.has(copy.key);
243
255
  if (returned && copy.erasing === undefined)
244
256
  continue;
@@ -269,13 +281,12 @@ export class LocalCopies {
269
281
  }
270
282
  }
271
283
  if (refreshing && current.length > 0) {
272
- try {
273
- const refreshed = await this.refreshed(current);
274
- current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
275
- }
276
- catch (error) {
284
+ const failed = await this.refreshed(current).then(() => null, houseCause);
285
+ for (const copy of current)
286
+ said.push(...moved(copy, this.copies.get(copy.key)));
287
+ if (failed !== null) {
277
288
  refused = true;
278
- said.push(houseCause(error));
289
+ said.push(failed);
279
290
  }
280
291
  }
281
292
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -450,19 +461,24 @@ export class LocalCopies {
450
461
  return next;
451
462
  }
452
463
  async refreshed(copies) {
453
- const discovery = await this.house.post('/kit/door/discover', {
454
- prefixes: copies.map((copy) => rootOf(copy.room_handle)),
455
- });
456
- const regranted = new Map();
464
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
465
+ const [first, ...rest] = copies;
466
+ const { bounds, authorities } = await discover([first]);
467
+ for (const group of grouped(rest, bounds.max_prefixes))
468
+ authorities.push(...(await discover(group)).authorities);
469
+ const unchanged = [];
457
470
  for (const copy of copies) {
458
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
471
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
459
472
  if (authority !== undefined &&
460
473
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
461
- regranted.set(copy.key, await this.save(await this.received(copy)));
474
+ await this.save(await this.received(copy));
475
+ }
476
+ else {
477
+ unchanged.push(copy);
462
478
  }
463
479
  }
464
- const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
465
- return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
480
+ for (const group of grouped(unchanged, bounds.max_prefixes))
481
+ await this.caughtUp(group, false);
466
482
  }
467
483
  async caughtUp(copies, whole) {
468
484
  const caught = new Map(copies.map((copy) => [copy.key, copy]));