@agentshouse/kit 0.2.1-cca496635583e → 0.2.1-ccb257fa8a318

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 (129) hide show
  1. package/README.md +36 -35
  2. package/dist/acp.js +85 -18
  3. package/dist/agents.js +110 -23
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +4 -7
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +87 -22
  13. package/dist/connection.js +116 -0
  14. package/dist/conversations.js +480 -159
  15. package/dist/copies.js +89 -52
  16. package/dist/end.js +11 -2
  17. package/dist/events.js +6 -2
  18. package/dist/files.js +139 -31
  19. package/dist/git.js +13 -11
  20. package/dist/home.js +56 -5
  21. package/dist/house-cli.js +299 -0
  22. package/dist/house-main.js +7 -189
  23. package/dist/instructions.js +7 -11
  24. package/dist/kit-cli.js +68 -0
  25. package/dist/kit-main.js +8 -22
  26. package/dist/login.js +50 -16
  27. package/dist/logout.js +0 -2
  28. package/dist/mcp.js +6 -2
  29. package/dist/parts.js +34 -16
  30. package/dist/placement.js +39 -0
  31. package/dist/relay.js +22 -1
  32. package/dist/resident.js +94 -34
  33. package/dist/scope.js +3 -0
  34. package/dist/scripts.js +255 -0
  35. package/dist/service.js +23 -1
  36. package/dist/sign-in.js +22 -39
  37. package/dist/silence.js +28 -0
  38. package/dist/skills.js +49 -35
  39. package/dist/transcription.js +85 -45
  40. package/dist/translate.js +2 -8
  41. package/dist/update.js +115 -53
  42. package/dist/wire.js +22 -0
  43. package/dist/work.js +3 -5
  44. package/package.json +20 -20
  45. package/dist/bootstrap.json +0 -24
  46. package/dist/stream.js +0 -74
  47. package/how-we-work.md +0 -23
  48. package/skills/ask-matt/LICENSE +0 -21
  49. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  50. package/skills/ask-matt/SKILL.md +0 -91
  51. package/skills/ask-matt/agents/openai.yaml +0 -4
  52. package/skills/code-review/LICENSE +0 -21
  53. package/skills/code-review/SKILL.md +0 -89
  54. package/skills/code-review/agents/openai.yaml +0 -3
  55. package/skills/codebase-design/DEEPENING.md +0 -37
  56. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  57. package/skills/codebase-design/LICENSE +0 -21
  58. package/skills/codebase-design/SKILL.md +0 -116
  59. package/skills/codebase-design/agents/openai.yaml +0 -3
  60. package/skills/diagnosing-bugs/LICENSE +0 -21
  61. package/skills/diagnosing-bugs/SKILL.md +0 -140
  62. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  63. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  64. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  65. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  66. package/skills/domain-modeling/LICENSE +0 -21
  67. package/skills/domain-modeling/SKILL.md +0 -76
  68. package/skills/domain-modeling/agents/openai.yaml +0 -3
  69. package/skills/grill-me/LICENSE +0 -21
  70. package/skills/grill-me/SKILL.md +0 -8
  71. package/skills/grill-me/agents/openai.yaml +0 -4
  72. package/skills/grill-with-docs/LICENSE +0 -21
  73. package/skills/grill-with-docs/SKILL.md +0 -8
  74. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  75. package/skills/grilling/LICENSE +0 -21
  76. package/skills/grilling/SKILL.md +0 -30
  77. package/skills/grilling/agents/openai.yaml +0 -3
  78. package/skills/handoff/LICENSE +0 -21
  79. package/skills/handoff/SKILL.md +0 -17
  80. package/skills/handoff/agents/openai.yaml +0 -4
  81. package/skills/implement/LICENSE +0 -21
  82. package/skills/implement/SKILL.md +0 -16
  83. package/skills/implement/agents/openai.yaml +0 -4
  84. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  85. package/skills/improve-codebase-architecture/LICENSE +0 -21
  86. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  87. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  88. package/skills/prototype/LICENSE +0 -21
  89. package/skills/prototype/LOGIC.md +0 -67
  90. package/skills/prototype/SKILL.md +0 -28
  91. package/skills/prototype/UI.md +0 -112
  92. package/skills/prototype/agents/openai.yaml +0 -3
  93. package/skills/research/LICENSE +0 -21
  94. package/skills/research/SKILL.md +0 -14
  95. package/skills/research/agents/openai.yaml +0 -3
  96. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  97. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  98. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  99. package/skills/tdd/LICENSE +0 -21
  100. package/skills/tdd/SKILL.md +0 -40
  101. package/skills/tdd/agents/openai.yaml +0 -3
  102. package/skills/tdd/mocking.md +0 -59
  103. package/skills/tdd/tests.md +0 -77
  104. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  105. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  106. package/skills/teach/LICENSE +0 -21
  107. package/skills/teach/MISSION-FORMAT.md +0 -31
  108. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  109. package/skills/teach/SKILL.md +0 -141
  110. package/skills/teach/agents/openai.yaml +0 -4
  111. package/skills/to-questionnaire/LICENSE +0 -21
  112. package/skills/to-questionnaire/SKILL.md +0 -55
  113. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  114. package/skills/to-spec/LICENSE +0 -21
  115. package/skills/to-spec/SKILL.md +0 -76
  116. package/skills/to-spec/agents/openai.yaml +0 -4
  117. package/skills/to-tickets/LICENSE +0 -21
  118. package/skills/to-tickets/SKILL.md +0 -106
  119. package/skills/to-tickets/agents/openai.yaml +0 -4
  120. package/skills/wait-what/LICENSE +0 -21
  121. package/skills/wait-what/SKILL.md +0 -8
  122. package/skills/wait-what/agents/openai.yaml +0 -4
  123. package/skills/wayfinder/LICENSE +0 -21
  124. package/skills/wayfinder/SKILL.md +0 -129
  125. package/skills/wayfinder/agents/openai.yaml +0 -4
  126. package/skills/writing-for-agents/LICENSE +0 -21
  127. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  128. package/skills/writing-for-agents/SKILL.md +0 -83
  129. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,41 +1,121 @@
1
- import { client, } from '@agentclientprotocol/sdk';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  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, HouseRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
- import { readerFor } from './events.js';
10
- import { placeFiles, uploadBytes } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } 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';
16
+ import { relayed } from './relay.js';
14
17
  import { killMarked, Scope } from './scope.js';
15
18
  import { holdSecretInput } from './secret-input.js';
16
- import { createHowWeWork } from './skills.js';
17
19
  import { transcribe } from './transcription.js';
18
20
  import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
19
22
  const KILLED = 'the conversation was killed';
23
+ const INTERRUPTED = 'the conversation was interrupted';
24
+ const REFUSED = 'the CLI refused the prompt';
25
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
26
+ const GONE = 'House no longer holds this conversation; start a new one';
27
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
28
+ const ENDED = {
29
+ conversation_not_found: GONE,
30
+ conversation_historical: RETIRED,
31
+ };
32
+ const NO_START = "the Agent's CLI did not start";
33
+ const NO_SESSION = "the Agent's CLI did not open a session";
34
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
35
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
36
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
37
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
38
+ class NotStarted extends Error {
39
+ }
40
+ class ResumeRefused extends NotStarted {
41
+ }
42
+ async function step(reason, work) {
43
+ try {
44
+ return await work;
45
+ }
46
+ catch (error) {
47
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
48
+ }
49
+ }
50
+ function signedOut(error) {
51
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
52
+ }
53
+ function unopened(reason) {
54
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
55
+ }
56
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
57
+ const STARTING_SHOWN_AFTER_MS = 1_000;
58
+ function unbridged(error) {
59
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
60
+ }
61
+ const EXIT_PLAN_MODE = {
62
+ parse(value) {
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
64
+ throw new Error('_x.ai/exit_plan_mode needs an object');
65
+ }
66
+ return value;
67
+ },
68
+ };
69
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
20
70
  function causeOf(error) {
21
71
  return error instanceof Error ? error.message : String(error);
22
72
  }
73
+ function answeredWith(error) {
74
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
75
+ }
76
+ function exitedWith(running, cause) {
77
+ if (running.killed)
78
+ return { failure: 'killed', cause: KILLED };
79
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
80
+ }
23
81
  function answerMessage(request, response) {
24
82
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
83
  }
84
+ async function sentPart(url, bytes, headers) {
85
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
86
+ await answer.arrayBuffer();
87
+ if (!answer.ok)
88
+ throw storeRefusal('upload', answer.status);
89
+ }
26
90
  function logged(error) {
27
91
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
92
  }
93
+ function refusal(conversation, error) {
94
+ if (conversation.kills > 0)
95
+ return KILLED;
96
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
97
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
98
+ return causeOf(error);
99
+ }
100
+ function audioOf(result) {
101
+ if (result.isError === true)
102
+ return null;
103
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
104
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
105
+ }
29
106
  function marker(bridge) {
30
107
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
108
  }
109
+ function steers(adapter) {
110
+ return adapter.initialized._meta?.steering?.supported === true;
111
+ }
32
112
  class Turn {
33
113
  id = randomUUID();
34
114
  parts;
35
- admitted = false;
36
115
  questions = 0;
37
116
  secrets = 0;
38
117
  prompted = false;
118
+ taken = null;
39
119
  ended = false;
40
120
  images = 0;
41
121
  uploads = [];
@@ -49,13 +129,19 @@ class Conversation {
49
129
  session = null;
50
130
  turn = null;
51
131
  commands = null;
132
+ options = null;
133
+ exitUntold = false;
52
134
  context = null;
53
135
  leftovers = new Set();
54
136
  waiting = [];
137
+ done = false;
55
138
  sending = null;
56
139
  subagents = new Map();
57
140
  held = [];
58
141
  opening = null;
142
+ bridge = null;
143
+ launch = null;
144
+ prompted = null;
59
145
  kills = 0;
60
146
  killed = new AbortController();
61
147
  interrupts = 0;
@@ -63,6 +149,11 @@ class Conversation {
63
149
  open = 0;
64
150
  queue = Promise.resolve();
65
151
  reports = Promise.resolve();
152
+ starting = null;
153
+ preparing = null;
154
+ told = null;
155
+ untold = false;
156
+ shown = null;
66
157
  constructor(id) {
67
158
  this.id = id;
68
159
  }
@@ -78,9 +169,7 @@ function asksSecret(request) {
78
169
  export class Conversations {
79
170
  kit;
80
171
  conversations = new Map();
81
- acks = new Map();
82
172
  questions = new Map();
83
- carrying = 0;
84
173
  reporting = 0;
85
174
  watched = false;
86
175
  constructor(kit) {
@@ -88,38 +177,27 @@ export class Conversations {
88
177
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
178
  setInterval(() => this.look(), 2_000);
90
179
  }
91
- input(input) {
92
- this.carrying++;
93
- const carried = this.acks.get(input.input_id) ??
94
- (() => {
95
- const conversation = this.conversation(input.conversation_id);
96
- if (input.kind === 'kill') {
97
- conversation.kills++;
98
- conversation.killed.abort(new Error(KILLED));
99
- for (const waiting of conversation.waiting.splice(0))
100
- waiting.settle({ refused: KILLED });
101
- this.halt(conversation);
102
- }
103
- if (input.kind === 'interrupt') {
104
- conversation.interrupts++;
105
- conversation.interrupted.abort();
106
- }
107
- const handled = conversation.queue.then(() => this.carry(conversation, input));
108
- conversation.queue = handled.catch(() => undefined);
109
- const work = handled.then(({ ack }) => ack);
110
- this.acks.set(input.input_id, work);
111
- return work;
112
- })();
113
- carried
114
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
115
- .catch(logged)
116
- .finally(() => {
117
- this.carrying--;
118
- this.kit.changed();
180
+ call(call) {
181
+ void this.kit.calls.carry(call.input_id, () => {
182
+ const conversation = this.conversation(call.conversation_id);
183
+ if (call.kind === 'kill') {
184
+ conversation.kills++;
185
+ conversation.killed.abort(new Error(KILLED));
186
+ for (const waiting of conversation.waiting.splice(0))
187
+ waiting.settle({ refused: KILLED });
188
+ this.halt(conversation);
189
+ }
190
+ if (call.kind === 'interrupt') {
191
+ conversation.interrupts++;
192
+ conversation.interrupted.abort();
193
+ }
194
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
195
+ conversation.queue = handled.catch(() => undefined);
196
+ return handled.then(async ({ result }) => ({ result: await result }));
119
197
  });
120
198
  }
121
199
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
200
+ if (this.reporting > 0)
123
201
  return false;
124
202
  for (const conversation of this.conversations.values()) {
125
203
  const turn = conversation.turn;
@@ -148,6 +226,14 @@ export class Conversations {
148
226
  }
149
227
  opened() {
150
228
  this.watched = false;
229
+ for (const conversation of this.conversations.values()) {
230
+ if (conversation.running !== null)
231
+ this.facts(conversation);
232
+ else if (conversation.exitUntold)
233
+ this.process(conversation, false);
234
+ if (conversation.untold || conversation.told !== null)
235
+ this.retell(conversation);
236
+ }
151
237
  }
152
238
  watching(watched) {
153
239
  const resend = watched && !this.watched;
@@ -167,127 +253,309 @@ export class Conversations {
167
253
  }
168
254
  return conversation;
169
255
  }
170
- async carry(conversation, input) {
171
- if (input.kind === 'open')
172
- return { ack: Promise.resolve(await this.open(conversation, input)) };
173
- if (input.kind === 'message')
174
- return this.message(conversation, input);
175
- if (input.kind === 'interrupt') {
256
+ async carry(conversation, call) {
257
+ if (call.kind === 'open')
258
+ return this.open(conversation, call);
259
+ if (call.kind === 'message')
260
+ return this.message(conversation, call);
261
+ if (call.kind === 'done') {
262
+ conversation.done = true;
263
+ this.next(conversation);
264
+ return { result: Promise.resolve({}) };
265
+ }
266
+ await this.launched(conversation);
267
+ if (call.kind === 'interrupt') {
176
268
  if (--conversation.interrupts === 0)
177
269
  conversation.interrupted = new AbortController();
178
- await this.interrupt(conversation, String(input.turn_id));
270
+ await this.interrupt(conversation, String(call.turn_id));
179
271
  }
180
- if (input.kind === 'kill') {
272
+ if (call.kind === 'kill') {
181
273
  await this.kill(conversation);
182
274
  await conversation.sending;
183
275
  if (--conversation.kills === 0)
184
276
  conversation.killed = new AbortController();
185
277
  }
186
- if (input.kind === 'option')
187
- await this.option(conversation, String(input.option), input.value);
188
- if (input.kind === 'answer') {
189
- const question = this.questions.get(String(input.interaction_id));
278
+ if (call.kind === 'option')
279
+ await this.option(conversation, String(call.option), call.value);
280
+ if (call.kind === 'answer') {
281
+ const question = this.questions.get(String(call.interaction_id));
190
282
  if (question === undefined) {
191
- const text = answerMessage(input.request, input.response);
192
- return this.message(conversation, { ...input, text, files: [], first: false });
283
+ const text = answerMessage(call.request, call.response);
284
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
193
285
  }
194
- question.answer(input.response);
286
+ question.answer(call.response);
195
287
  }
196
- return { ack: Promise.resolve({}) };
288
+ return { result: Promise.resolve({}) };
289
+ }
290
+ async open(conversation, call) {
291
+ await this.stop(conversation);
292
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
293
+ return {
294
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
295
+ };
197
296
  }
198
- async open(conversation, input) {
199
- if (conversation.running !== null)
200
- return {};
297
+ async stop(conversation) {
298
+ const running = conversation.running;
299
+ running?.scope.kill();
300
+ await running?.adapter.exited;
301
+ }
302
+ launch(conversation, route, sessionId, empty) {
303
+ let opened;
304
+ let failed;
305
+ const bridge = new Promise((resolve, reject) => {
306
+ opened = resolve;
307
+ failed = reject;
308
+ });
309
+ bridge.catch(() => undefined);
310
+ const session = this.start(conversation, route, sessionId, empty, opened);
311
+ session.catch(failed);
312
+ const launch = { bridge, session };
313
+ conversation.launch = launch;
314
+ void session
315
+ .catch(() => undefined)
316
+ .then(() => {
317
+ if (conversation.launch !== launch)
318
+ return;
319
+ conversation.launch = null;
320
+ this.closeIdleBridge(conversation);
321
+ this.next(conversation);
322
+ });
323
+ this.next(conversation);
324
+ return launch;
325
+ }
326
+ async launched(conversation) {
327
+ while (conversation.launch !== null)
328
+ await conversation.launch.session.catch(() => undefined);
329
+ }
330
+ instructed(conversation, route) {
331
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
332
+ if (bridge === undefined)
333
+ return null;
334
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
335
+ read.catch(() => undefined);
336
+ return read;
337
+ }
338
+ async message(conversation, call) {
339
+ const content = [
340
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
341
+ { type: 'text', text: String(call.text) },
342
+ ];
343
+ let route;
344
+ let instructing = null;
201
345
  try {
202
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
346
+ route = await this.route(conversation, call.agent_id);
347
+ if (call.first === true)
348
+ instructing = this.instructed(conversation, route);
349
+ if (call.message_id !== null)
350
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
351
+ if (conversation.kills > 0)
352
+ throw new Error(KILLED);
203
353
  }
204
354
  catch (error) {
205
- return { refused: causeOf(error) };
355
+ this.closeIdleBridge(conversation);
356
+ this.prepares(conversation, null);
357
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
358
+ }
359
+ conversation.preparing = null;
360
+ const { promise, resolve: settle } = Promise.withResolvers();
361
+ const waiting = { call, route, content, instructing, settle };
362
+ if (call.at_once === true && conversation.open > 0)
363
+ await this.atOnce(conversation, waiting);
364
+ else
365
+ conversation.waiting.push(waiting);
366
+ this.next(conversation);
367
+ return { result: promise };
368
+ }
369
+ async atOnce(conversation, waiting) {
370
+ while (conversation.sending !== null)
371
+ await conversation.sending;
372
+ const running = conversation.running;
373
+ if (running !== null && conversation.open > 0 && steers(running.adapter)) {
374
+ conversation.sending = this.steer(conversation, running, waiting);
375
+ await conversation.sending;
376
+ conversation.sending = null;
377
+ return;
206
378
  }
379
+ this.ahead(conversation, waiting);
380
+ if (conversation.turn !== null)
381
+ await this.interrupt(conversation, conversation.turn.id);
207
382
  }
208
- async message(conversation, input) {
209
- const content = [{ type: 'text', text: String(input.text) }];
210
- let route;
383
+ ahead(conversation, waiting) {
384
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
385
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
386
+ }
387
+ async steer(conversation, running, waiting) {
388
+ let outcome;
211
389
  try {
212
- route = await this.route(conversation, input.agent_id);
213
- const files = input.files;
214
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
215
- const lines = await this.transcribed(conversation, files, paths);
216
- if (lines.length > 0)
217
- content.push({ type: 'text', text: lines.join('\n') });
218
- if (conversation.kills > 0)
219
- throw new Error(KILLED);
390
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
391
+ sessionId: running.sessionId,
392
+ prompt: waiting.content,
393
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
394
+ }));
220
395
  }
221
396
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
397
+ outcome = error;
398
+ }
399
+ if (outcome === 'promptRequired') {
400
+ this.ahead(conversation, waiting);
401
+ }
402
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
403
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
404
+ waiting.settle({});
405
+ }
406
+ else {
407
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
408
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
223
409
  }
224
- return {
225
- ack: new Promise((settle) => {
226
- conversation.waiting.push({ input, route, content, settle });
227
- this.next(conversation);
228
- }),
229
- };
230
410
  }
231
411
  next(conversation) {
232
412
  if (conversation.sending !== null || conversation.open > 0)
233
413
  return;
234
414
  const waiting = conversation.waiting.shift();
235
- if (waiting === undefined)
415
+ if (waiting === undefined) {
416
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
417
+ if (conversation.starting !== step)
418
+ this.starts(conversation, step);
419
+ if (conversation.done && conversation.launch === null)
420
+ void this.stop(conversation).catch(logged);
236
421
  return;
422
+ }
237
423
  conversation.sending = this.send(conversation, waiting).finally(() => {
238
424
  conversation.sending = null;
239
425
  this.next(conversation);
240
426
  });
241
427
  }
242
428
  async send(conversation, waiting) {
243
- const { input } = waiting;
244
- let ack = {};
429
+ const { call } = waiting;
430
+ let result = {};
431
+ let started = false;
432
+ let instructing = waiting.instructing;
245
433
  const prompt = [];
246
434
  try {
247
435
  if (conversation.running === null) {
248
- const resumed = input.provider_session_id ?? conversation.session;
249
- const opened = await this.start(conversation, waiting.route, resumed);
250
- if (resumed === null)
251
- ack = { provider_session_id: opened };
436
+ this.starts(conversation, 'session');
437
+ await this.launched(conversation);
252
438
  }
253
- if (input.first === true) {
254
- prompt.push({
255
- type: 'text',
256
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
257
- });
439
+ if (conversation.running === null) {
440
+ const stored = conversation.session ?? call.provider_session_id;
441
+ const empty = call.first === true && conversation.prompted !== stored;
442
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
443
+ if (call.first === true)
444
+ instructing = this.instructed(conversation, waiting.route);
445
+ started = (await launch.session) !== stored;
446
+ }
447
+ const session = conversation.running.sessionId;
448
+ if (started || (call.first === true && session !== call.provider_session_id)) {
449
+ result = { provider_session_id: session };
450
+ }
451
+ if (call.first === true) {
452
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
258
453
  }
259
454
  if (conversation.kills > 0)
260
455
  throw new Error(KILLED);
261
456
  }
262
457
  catch (error) {
263
- if ('provider_session_id' in ack) {
458
+ this.starts(conversation, null);
459
+ if (started) {
264
460
  conversation.session = null;
265
461
  await this.kill(conversation);
266
462
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
463
+ const refused = refusal(conversation, error);
464
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
465
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
466
+ return;
467
+ }
468
+ this.starts(conversation, null);
469
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
470
+ waiting.settle(result);
471
+ }
472
+ starts(conversation, step) {
473
+ conversation.starting = step;
474
+ if (conversation.shown !== null)
475
+ clearTimeout(conversation.shown);
476
+ conversation.shown = null;
477
+ if (step === null || conversation.told !== null) {
478
+ this.tell(conversation, step);
268
479
  return;
269
480
  }
270
- this.prompt(conversation, [...prompt, ...waiting.content]);
271
- waiting.settle(ack);
481
+ conversation.shown = setTimeout(() => {
482
+ conversation.shown = null;
483
+ if (conversation.starting === step)
484
+ this.tell(conversation, step);
485
+ }, STARTING_SHOWN_AFTER_MS);
272
486
  }
273
- async transcribed(conversation, files, paths) {
274
- const lines = [];
275
- for (const [index, path] of paths.entries()) {
276
- lines.push(path);
277
- if (files[index].media_type?.startsWith('audio/') !== true)
278
- continue;
279
- try {
280
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
281
- if (text !== null && text !== '')
282
- lines.push(`Transcript: ${text}`);
283
- }
284
- catch (error) {
285
- if (conversation.kills > 0)
286
- throw error;
287
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
487
+ prepares(conversation, step) {
488
+ conversation.preparing = step;
489
+ this.next(conversation);
490
+ }
491
+ tell(conversation, step) {
492
+ if (conversation.told === step && !conversation.untold)
493
+ return;
494
+ conversation.told = step;
495
+ this.retell(conversation);
496
+ }
497
+ retell(conversation) {
498
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
499
+ }
500
+ async transcribed(conversation, messageId, text) {
501
+ const paths = linkedPaths(text);
502
+ if (paths.length === 0)
503
+ return [];
504
+ const killed = conversation.killed.signal;
505
+ const interrupted = conversation.interrupted.signal;
506
+ const transcribing = AbortSignal.any([killed, interrupted]);
507
+ let signal = killed;
508
+ const blocks = [];
509
+ try {
510
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
511
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
512
+ for (const path of paths) {
513
+ let answered = await read(path);
514
+ if (answered === null)
515
+ continue;
516
+ signal = transcribing;
517
+ this.prepares(conversation, 'transcription');
518
+ const said = await transcribe(async () => {
519
+ const address = answered ?? (await read(path));
520
+ answered = null;
521
+ if (address === null)
522
+ throw new Error(`${path} is no longer an audio file`);
523
+ return address;
524
+ }, signal);
525
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
526
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
527
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
288
528
  }
289
529
  }
290
- return lines;
530
+ catch (error) {
531
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
532
+ throw new NotStarted(INTERRUPTED, { cause: error });
533
+ throw error;
534
+ }
535
+ return blocks;
536
+ }
537
+ bridged(conversation) {
538
+ if (conversation.bridge === null) {
539
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
540
+ conversation.bridge = bridging;
541
+ bridging.catch(() => {
542
+ if (conversation.bridge === bridging)
543
+ conversation.bridge = null;
544
+ });
545
+ }
546
+ return conversation.bridge;
547
+ }
548
+ closeBridge(conversation, bridging, bridge) {
549
+ bridge.close();
550
+ if (conversation.bridge === bridging)
551
+ conversation.bridge = null;
552
+ }
553
+ closeIdleBridge(conversation) {
554
+ const bridging = conversation.bridge;
555
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
556
+ return;
557
+ conversation.bridge = null;
558
+ void bridging.then((bridge) => bridge.close(), () => undefined);
291
559
  }
292
560
  async interrupt(conversation, turnId) {
293
561
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -342,63 +610,78 @@ export class Conversations {
342
610
  }
343
611
  return route;
344
612
  }
345
- async start(conversation, route, sessionId) {
613
+ async start(conversation, routed, sessionId, empty, bridged) {
614
+ const route = await routed;
346
615
  const reader = readerFor(route.kind);
347
616
  reader.session = sessionId;
348
617
  const app = client({ name: '@agentshouse/kit' })
349
618
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
350
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
619
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
620
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
351
621
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
352
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
622
+ const bridging = this.bridged(conversation);
623
+ const bridge = await step(unbridged, bridging);
353
624
  if (conversation.kills > 0) {
354
- bridge.close();
625
+ this.closeBridge(conversation, bridging, bridge);
355
626
  throw new Error(KILLED);
356
627
  }
357
628
  conversation.opening = bridge;
358
629
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
630
  const scope = new Scope(marker(bridge), randomUUID(), cli);
631
+ let adapter = null;
360
632
  try {
361
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
633
+ bridged(bridge);
362
634
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
635
+ const streamed = await step(NO_CONNECTION, work.open(cli));
636
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
364
637
  work.read(message);
365
638
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
367
- void adapter.exited.then(() => bridge.close());
639
+ }, { ...bridge.env, ...streamed }, undefined, scope));
640
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
368
641
  const agent = adapter.connection.agent;
369
642
  const cwd = route.working_directory;
370
643
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
371
- const opened = sessionId === null
372
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
373
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
644
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
645
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
374
646
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
647
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
648
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
649
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
650
  }
379
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
651
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
652
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
653
  void work.settled().then(() => scope.record());
382
654
  conversation.running = running;
655
+ adapter.turn(conversation.open > 0);
383
656
  conversation.session = running.sessionId;
384
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
385
- if (conversation.commands !== null)
386
- this.commands(conversation, conversation.commands);
387
- this.options(conversation, options);
388
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
657
+ conversation.options = options;
658
+ this.facts(conversation);
659
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
389
660
  return running.sessionId;
390
661
  }
391
662
  catch (error) {
392
- scope.kill();
663
+ adapter?.retire();
393
664
  work.close();
394
665
  conversation.leftovers.add(scope);
395
- bridge.close();
666
+ this.closeBridge(conversation, bridging, bridge);
396
667
  throw conversation.kills > 0 ? new Error(KILLED) : error;
397
668
  }
398
669
  finally {
399
670
  conversation.opening = null;
400
671
  }
401
672
  }
673
+ async resume(conversation, kind, adapter, request, empty) {
674
+ try {
675
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
676
+ }
677
+ catch (error) {
678
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
679
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
680
+ }
681
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
682
+ return null;
683
+ }
684
+ }
402
685
  async launchSettings(running, route, offered) {
403
686
  let options = offered;
404
687
  const settings = [
@@ -420,27 +703,37 @@ export class Conversations {
420
703
  }
421
704
  return options;
422
705
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
706
+ exited(conversation, running, cause) {
427
707
  this.detach(conversation, running);
428
708
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
709
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
710
  }
431
711
  this.next(conversation);
432
712
  this.kit.changed();
433
713
  }
434
714
  detach(conversation, running) {
715
+ if (conversation.bridge === running.bridging)
716
+ conversation.bridge = null;
435
717
  running.work.close();
436
718
  conversation.leftovers.add(running.scope);
437
719
  conversation.running = null;
438
720
  conversation.commands = null;
721
+ conversation.options = null;
439
722
  for (const [id, question] of this.questions) {
440
723
  if (question.conversation === conversation)
441
724
  this.questions.delete(id);
442
725
  }
443
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
726
+ this.process(conversation, false);
727
+ }
728
+ process(conversation, running) {
729
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
730
+ }
731
+ facts(conversation) {
732
+ this.process(conversation, true);
733
+ if (conversation.commands !== null)
734
+ this.commands(conversation, conversation.commands);
735
+ if (conversation.options !== null)
736
+ this.options(conversation, conversation.options);
444
737
  }
445
738
  commands(conversation, commands) {
446
739
  conversation.commands = commands;
@@ -451,6 +744,7 @@ export class Conversations {
451
744
  options(conversation, options) {
452
745
  if (conversation.running === null)
453
746
  return;
747
+ conversation.options = options;
454
748
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
455
749
  }
456
750
  deliver(conversation, turn, event, body) {
@@ -465,7 +759,7 @@ export class Conversations {
465
759
  return delivered;
466
760
  }
467
761
  write(conversation, turn, operation) {
468
- if (!turn.admitted || turn.ended || !this.watched)
762
+ if (turn.ended || !this.watched)
469
763
  return;
470
764
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
471
765
  }
@@ -476,12 +770,8 @@ export class Conversations {
476
770
  begin(conversation) {
477
771
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
478
772
  conversation.open++;
479
- void this.deliver(conversation, turn, 'started', {})
480
- .then(() => {
481
- turn.admitted = true;
482
- this.replay(conversation, turn);
483
- })
484
- .catch(logged);
773
+ conversation.running?.adapter.turn(true);
774
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
485
775
  conversation.turn = turn;
486
776
  if (conversation.context !== null)
487
777
  turn.parts.measure(conversation.context);
@@ -490,45 +780,64 @@ export class Conversations {
490
780
  this.kit.changed();
491
781
  return turn;
492
782
  }
493
- end(conversation, turn, failed) {
783
+ end(conversation, turn, failed, notice = null) {
494
784
  if (turn.ended)
495
785
  return;
786
+ if (failed !== null)
787
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
788
  if (conversation.turn === turn) {
497
789
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
790
  this.apply(conversation, turn, event);
499
791
  }
792
+ if (notice !== null)
793
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
794
  turn.ended = true;
501
795
  conversation.open--;
796
+ conversation.running?.adapter.turn(conversation.open > 0);
502
797
  if (conversation.turn === turn)
503
798
  conversation.turn = null;
504
799
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
- parts: turn.parts.parts,
800
+ parts: turn.parts.saved(),
506
801
  context: turn.parts.context,
507
- ...failed,
802
+ ...(failed === null ? {} : { failed: failed.failure }),
508
803
  }));
509
804
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
805
  this.next(conversation);
511
806
  this.kit.changed();
512
807
  }
513
- prompt(conversation, prompt) {
808
+ prompt(conversation, prompt, taken) {
514
809
  const running = conversation.running;
515
810
  const turn = this.begin(conversation);
516
811
  turn.prompted = true;
517
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
812
+ turn.taken = taken;
813
+ conversation.prompted = running.sessionId;
814
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
815
+ this.took(turn);
816
+ const notice = answeredFailure(answer);
817
+ if (notice !== null) {
818
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
819
+ }
820
+ else {
821
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
822
+ }
823
+ }, async (error) => {
518
824
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
825
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
826
  }
521
827
  else {
828
+ this.took(turn);
522
829
  if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
830
+ running.adapter.retire();
524
831
  this.detach(conversation, running);
525
832
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
833
+ this.end(conversation, turn, answeredWith(error));
527
834
  }
528
835
  });
529
836
  }
530
837
  started(conversation) {
531
- return conversation.turn ?? this.begin(conversation);
838
+ const turn = conversation.turn ?? this.begin(conversation);
839
+ this.took(turn);
840
+ return turn;
532
841
  }
533
842
  ended(conversation) {
534
843
  const turn = conversation.turn;
@@ -623,6 +932,11 @@ export class Conversations {
623
932
  }
624
933
  this.apply(conversation, this.started(conversation), event);
625
934
  }
935
+ took(turn) {
936
+ const taken = turn.taken;
937
+ turn.taken = null;
938
+ taken?.();
939
+ }
626
940
  apply(conversation, turn, event) {
627
941
  switch (event.kind) {
628
942
  case 'text':
@@ -647,7 +961,7 @@ export class Conversations {
647
961
  turn.parts.plan(event.entries);
648
962
  return;
649
963
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
964
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
965
  this.kit.changed();
652
966
  return;
653
967
  }
@@ -667,23 +981,30 @@ export class Conversations {
667
981
  else
668
982
  conversation.turn.parts.start(part);
669
983
  }
670
- async image(conversation, turn, data, mediaType) {
984
+ async image(conversation, turn, reserved, data, mediaType) {
671
985
  const bytes = Buffer.from(data, 'base64');
672
986
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
987
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
673
988
  try {
674
989
  const declared = (await this.deliver(conversation, turn, 'images', {
675
990
  name,
676
991
  media_type: mediaType,
677
992
  bytes: bytes.length,
678
- sha256: createHash('sha256').update(bytes).digest('hex'),
993
+ sha256,
679
994
  }));
680
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
- if (saved.save.status !== 'saved')
682
- throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
995
+ const saved = await uploaded(declared.upload, {
996
+ name,
997
+ bytes: bytes.length,
998
+ sha256,
999
+ read: (start, end) => [bytes.subarray(start, end)],
1000
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1001
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
1002
+ });
1003
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
684
1004
  }
685
1005
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
1006
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1007
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
687
1008
  }
688
1009
  }
689
1010
  }