@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3d0b9ac4a82a

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 (132) hide show
  1. package/README.md +42 -40
  2. package/dist/acp.js +92 -115
  3. package/dist/agents.js +112 -24
  4. package/dist/api.js +10 -6
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +15 -44
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +131 -27
  13. package/dist/codex-main.js +58 -0
  14. package/dist/connection.js +116 -0
  15. package/dist/conversations.js +456 -200
  16. package/dist/copies.js +185 -123
  17. package/dist/end.js +24 -5
  18. package/dist/enrol-main.js +7 -1
  19. package/dist/events.js +16 -78
  20. package/dist/files.js +139 -31
  21. package/dist/git.js +13 -11
  22. package/dist/home.js +96 -7
  23. package/dist/house-cli.js +299 -0
  24. package/dist/house-main.js +7 -162
  25. package/dist/instructions.js +8 -10
  26. package/dist/kit-cli.js +68 -0
  27. package/dist/kit-main.js +8 -22
  28. package/dist/login.js +118 -107
  29. package/dist/logout.js +14 -16
  30. package/dist/mcp.js +6 -2
  31. package/dist/parts.js +34 -16
  32. package/dist/placement.js +39 -0
  33. package/dist/relay.js +33 -2
  34. package/dist/resident.js +116 -51
  35. package/dist/scope.js +206 -0
  36. package/dist/service-main.js +9 -0
  37. package/dist/service.js +55 -0
  38. package/dist/sign-in.js +23 -40
  39. package/dist/silence.js +28 -0
  40. package/dist/skills.js +49 -35
  41. package/dist/transcription.js +85 -45
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +115 -53
  44. package/dist/web-wizard.js +101 -0
  45. package/dist/wire.js +22 -0
  46. package/dist/work.js +346 -0
  47. package/package.json +20 -20
  48. package/dist/bootstrap.json +0 -24
  49. package/dist/stream.js +0 -72
  50. package/how-we-work.md +0 -23
  51. package/skills/ask-matt/LICENSE +0 -21
  52. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  53. package/skills/ask-matt/SKILL.md +0 -91
  54. package/skills/ask-matt/agents/openai.yaml +0 -4
  55. package/skills/code-review/LICENSE +0 -21
  56. package/skills/code-review/SKILL.md +0 -89
  57. package/skills/code-review/agents/openai.yaml +0 -3
  58. package/skills/codebase-design/DEEPENING.md +0 -37
  59. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  60. package/skills/codebase-design/LICENSE +0 -21
  61. package/skills/codebase-design/SKILL.md +0 -116
  62. package/skills/codebase-design/agents/openai.yaml +0 -3
  63. package/skills/diagnosing-bugs/LICENSE +0 -21
  64. package/skills/diagnosing-bugs/SKILL.md +0 -140
  65. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  66. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  67. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  68. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  69. package/skills/domain-modeling/LICENSE +0 -21
  70. package/skills/domain-modeling/SKILL.md +0 -76
  71. package/skills/domain-modeling/agents/openai.yaml +0 -3
  72. package/skills/grill-me/LICENSE +0 -21
  73. package/skills/grill-me/SKILL.md +0 -8
  74. package/skills/grill-me/agents/openai.yaml +0 -4
  75. package/skills/grill-with-docs/LICENSE +0 -21
  76. package/skills/grill-with-docs/SKILL.md +0 -8
  77. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  78. package/skills/grilling/LICENSE +0 -21
  79. package/skills/grilling/SKILL.md +0 -30
  80. package/skills/grilling/agents/openai.yaml +0 -3
  81. package/skills/handoff/LICENSE +0 -21
  82. package/skills/handoff/SKILL.md +0 -17
  83. package/skills/handoff/agents/openai.yaml +0 -4
  84. package/skills/implement/LICENSE +0 -21
  85. package/skills/implement/SKILL.md +0 -16
  86. package/skills/implement/agents/openai.yaml +0 -4
  87. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  88. package/skills/improve-codebase-architecture/LICENSE +0 -21
  89. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  90. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  91. package/skills/prototype/LICENSE +0 -21
  92. package/skills/prototype/LOGIC.md +0 -67
  93. package/skills/prototype/SKILL.md +0 -28
  94. package/skills/prototype/UI.md +0 -112
  95. package/skills/prototype/agents/openai.yaml +0 -3
  96. package/skills/research/LICENSE +0 -21
  97. package/skills/research/SKILL.md +0 -14
  98. package/skills/research/agents/openai.yaml +0 -3
  99. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  100. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  101. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  102. package/skills/tdd/LICENSE +0 -21
  103. package/skills/tdd/SKILL.md +0 -40
  104. package/skills/tdd/agents/openai.yaml +0 -3
  105. package/skills/tdd/mocking.md +0 -59
  106. package/skills/tdd/tests.md +0 -77
  107. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  108. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  109. package/skills/teach/LICENSE +0 -21
  110. package/skills/teach/MISSION-FORMAT.md +0 -31
  111. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  112. package/skills/teach/SKILL.md +0 -141
  113. package/skills/teach/agents/openai.yaml +0 -4
  114. package/skills/to-questionnaire/LICENSE +0 -21
  115. package/skills/to-questionnaire/SKILL.md +0 -55
  116. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  117. package/skills/to-spec/LICENSE +0 -21
  118. package/skills/to-spec/SKILL.md +0 -76
  119. package/skills/to-spec/agents/openai.yaml +0 -4
  120. package/skills/to-tickets/LICENSE +0 -21
  121. package/skills/to-tickets/SKILL.md +0 -106
  122. package/skills/to-tickets/agents/openai.yaml +0 -4
  123. package/skills/wait-what/LICENSE +0 -21
  124. package/skills/wait-what/SKILL.md +0 -8
  125. package/skills/wait-what/agents/openai.yaml +0 -4
  126. package/skills/wayfinder/LICENSE +0 -21
  127. package/skills/wayfinder/SKILL.md +0 -129
  128. package/skills/wayfinder/agents/openai.yaml +0 -4
  129. package/skills/writing-for-agents/LICENSE +0 -21
  130. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  131. package/skills/writing-for-agents/SKILL.md +0 -83
  132. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,44 +1,117 @@
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
- import { killMarked, processes, startAdapter } from './acp.js';
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';
17
+ import { killMarked, Scope } from './scope.js';
14
18
  import { holdSecretInput } from './secret-input.js';
15
- import { createHowWeWork } from './skills.js';
16
19
  import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
17
22
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
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
+ class NotStarted extends Error {
38
+ }
39
+ class ResumeRefused extends NotStarted {
40
+ }
41
+ async function step(reason, work) {
42
+ try {
43
+ return await work;
44
+ }
45
+ catch (error) {
46
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
+ }
48
+ }
49
+ function signedOut(error) {
50
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
+ }
52
+ function unopened(reason) {
53
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
54
+ }
55
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
56
+ const STARTING_SHOWN_AFTER_MS = 1_000;
57
+ function unbridged(error) {
58
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
59
+ }
60
+ const EXIT_PLAN_MODE = {
61
+ parse(value) {
62
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
63
+ throw new Error('_x.ai/exit_plan_mode needs an object');
64
+ }
65
+ return value;
66
+ },
67
+ };
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
69
  function causeOf(error) {
21
70
  return error instanceof Error ? error.message : String(error);
22
71
  }
72
+ function answeredWith(error) {
73
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
74
+ }
75
+ function exitedWith(running, cause) {
76
+ if (running.killed)
77
+ return { failure: 'killed', cause: KILLED };
78
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
79
+ }
23
80
  function answerMessage(request, response) {
24
81
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
82
  }
83
+ async function sentPart(url, bytes, headers) {
84
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
85
+ await answer.arrayBuffer();
86
+ if (!answer.ok)
87
+ throw storeRefusal('upload', answer.status);
88
+ }
26
89
  function logged(error) {
27
90
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
91
  }
92
+ function refusal(conversation, error) {
93
+ if (conversation.kills > 0)
94
+ return KILLED;
95
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
96
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
97
+ return causeOf(error);
98
+ }
99
+ function audioOf(result) {
100
+ if (result.isError === true)
101
+ return null;
102
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
103
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
104
+ }
29
105
  function marker(bridge) {
30
106
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
107
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
34
- }
35
108
  class Turn {
36
109
  id = randomUUID();
37
110
  parts;
38
- admitted = false;
39
111
  questions = 0;
40
112
  secrets = 0;
41
113
  prompted = false;
114
+ taken = null;
42
115
  ended = false;
43
116
  images = 0;
44
117
  uploads = [];
@@ -52,13 +125,18 @@ class Conversation {
52
125
  session = null;
53
126
  turn = null;
54
127
  commands = null;
128
+ options = null;
129
+ exitUntold = false;
55
130
  context = null;
56
- busy = new Set();
131
+ leftovers = new Set();
57
132
  waiting = [];
58
133
  sending = null;
59
134
  subagents = new Map();
60
135
  held = [];
61
136
  opening = null;
137
+ bridge = null;
138
+ launch = null;
139
+ prompted = null;
62
140
  kills = 0;
63
141
  killed = new AbortController();
64
142
  interrupts = 0;
@@ -66,6 +144,11 @@ class Conversation {
66
144
  open = 0;
67
145
  queue = Promise.resolve();
68
146
  reports = Promise.resolve();
147
+ starting = null;
148
+ preparing = null;
149
+ told = null;
150
+ untold = false;
151
+ shown = null;
69
152
  constructor(id) {
70
153
  this.id = id;
71
154
  }
@@ -81,70 +164,71 @@ function asksSecret(request) {
81
164
  export class Conversations {
82
165
  kit;
83
166
  conversations = new Map();
84
- acks = new Map();
85
167
  questions = new Map();
86
- carrying = 0;
87
168
  reporting = 0;
88
169
  watched = false;
89
- recheck = null;
90
170
  constructor(kit) {
91
171
  this.kit = kit;
92
- }
93
- input(input) {
94
- this.carrying++;
95
- const carried = this.acks.get(input.input_id) ??
96
- (() => {
97
- const conversation = this.conversation(input.conversation_id);
98
- if (input.kind === 'kill') {
99
- conversation.kills++;
100
- conversation.killed.abort(new Error(KILLED));
101
- for (const waiting of conversation.waiting.splice(0))
102
- waiting.settle({ refused: KILLED });
103
- this.halt(conversation);
104
- }
105
- if (input.kind === 'interrupt') {
106
- conversation.interrupts++;
107
- conversation.interrupted.abort();
108
- }
109
- const handled = conversation.queue.then(() => this.carry(conversation, input));
110
- conversation.queue = handled.catch(() => undefined);
111
- const work = handled.then(({ ack }) => ack);
112
- this.acks.set(input.input_id, work);
113
- return work;
114
- })();
115
- carried
116
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
117
- .catch(logged)
118
- .finally(() => {
119
- this.carrying--;
120
- this.kit.changed();
172
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
173
+ setInterval(() => this.look(), 2_000);
174
+ }
175
+ call(call) {
176
+ void this.kit.calls.carry(call.input_id, () => {
177
+ const conversation = this.conversation(call.conversation_id);
178
+ if (call.kind === 'kill') {
179
+ conversation.kills++;
180
+ conversation.killed.abort(new Error(KILLED));
181
+ for (const waiting of conversation.waiting.splice(0))
182
+ waiting.settle({ refused: KILLED });
183
+ this.halt(conversation);
184
+ }
185
+ if (call.kind === 'interrupt') {
186
+ conversation.interrupts++;
187
+ conversation.interrupted.abort();
188
+ }
189
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
190
+ conversation.queue = handled.catch(() => undefined);
191
+ return handled.then(async ({ result }) => ({ result: await result }));
121
192
  });
122
193
  }
123
194
  idle() {
124
- if (this.carrying > 0 || this.reporting > 0)
195
+ if (this.reporting > 0)
125
196
  return false;
126
197
  for (const conversation of this.conversations.values()) {
127
198
  const turn = conversation.turn;
128
199
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
200
+ if (conversation.open > (asking ? 1 : 0))
130
201
  return false;
131
202
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
203
+ const conversations = [...this.conversations.values()];
204
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
205
  return false;
206
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
207
+ }
208
+ quiet() {
209
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
210
+ }
211
+ look() {
212
+ for (const conversation of this.conversations.values()) {
213
+ for (const scope of conversation.leftovers) {
214
+ if (!scope.empty())
215
+ continue;
216
+ scope.close();
217
+ conversation.leftovers.delete(scope);
218
+ }
143
219
  }
144
- return true;
220
+ this.kit.changed();
145
221
  }
146
222
  opened() {
147
223
  this.watched = false;
224
+ for (const conversation of this.conversations.values()) {
225
+ if (conversation.running !== null)
226
+ this.facts(conversation);
227
+ else if (conversation.exitUntold)
228
+ this.process(conversation, false);
229
+ if (conversation.untold || conversation.told !== null)
230
+ this.retell(conversation);
231
+ }
148
232
  }
149
233
  watching(watched) {
150
234
  const resend = watched && !this.watched;
@@ -164,63 +248,99 @@ export class Conversations {
164
248
  }
165
249
  return conversation;
166
250
  }
167
- async carry(conversation, input) {
168
- if (input.kind === 'open')
169
- return { ack: Promise.resolve(await this.open(conversation, input)) };
170
- if (input.kind === 'message')
171
- return this.message(conversation, input);
172
- if (input.kind === 'interrupt') {
251
+ async carry(conversation, call) {
252
+ if (call.kind === 'open')
253
+ return { result: this.open(conversation, call) };
254
+ if (call.kind === 'message')
255
+ return this.message(conversation, call);
256
+ await this.launched(conversation);
257
+ if (call.kind === 'interrupt') {
173
258
  if (--conversation.interrupts === 0)
174
259
  conversation.interrupted = new AbortController();
175
- await this.interrupt(conversation, String(input.turn_id));
260
+ await this.interrupt(conversation, String(call.turn_id));
176
261
  }
177
- if (input.kind === 'kill') {
262
+ if (call.kind === 'kill') {
178
263
  await this.kill(conversation);
179
264
  await conversation.sending;
180
265
  if (--conversation.kills === 0)
181
266
  conversation.killed = new AbortController();
182
267
  }
183
- if (input.kind === 'option')
184
- await this.option(conversation, String(input.option), input.value);
185
- if (input.kind === 'answer') {
186
- const question = this.questions.get(String(input.interaction_id));
268
+ if (call.kind === 'option')
269
+ await this.option(conversation, String(call.option), call.value);
270
+ if (call.kind === 'answer') {
271
+ const question = this.questions.get(String(call.interaction_id));
187
272
  if (question === undefined) {
188
- const text = answerMessage(input.request, input.response);
189
- return this.message(conversation, { ...input, text, files: [], first: false });
273
+ const text = answerMessage(call.request, call.response);
274
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
190
275
  }
191
- question.answer(input.response);
276
+ question.answer(call.response);
192
277
  }
193
- return { ack: Promise.resolve({}) };
278
+ return { result: Promise.resolve({}) };
194
279
  }
195
- async open(conversation, input) {
280
+ open(conversation, call) {
196
281
  if (conversation.running !== null)
197
- return {};
198
- try {
199
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
200
- }
201
- catch (error) {
202
- return { refused: causeOf(error) };
203
- }
204
- }
205
- async message(conversation, input) {
206
- const content = [{ type: 'text', text: String(input.text) }];
282
+ return Promise.resolve({});
283
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
284
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
285
+ }
286
+ launch(conversation, route, sessionId, empty) {
287
+ let opened;
288
+ let failed;
289
+ const bridge = new Promise((resolve, reject) => {
290
+ opened = resolve;
291
+ failed = reject;
292
+ });
293
+ bridge.catch(() => undefined);
294
+ const session = this.start(conversation, route, sessionId, empty, opened);
295
+ session.catch(failed);
296
+ const launch = { bridge, session };
297
+ conversation.launch = launch;
298
+ void session
299
+ .catch(() => undefined)
300
+ .then(() => {
301
+ if (conversation.launch !== launch)
302
+ return;
303
+ conversation.launch = null;
304
+ this.closeIdleBridge(conversation);
305
+ this.next(conversation);
306
+ });
307
+ this.next(conversation);
308
+ return launch;
309
+ }
310
+ async launched(conversation) {
311
+ while (conversation.launch !== null)
312
+ await conversation.launch.session.catch(() => undefined);
313
+ }
314
+ instructed(conversation, route) {
315
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
316
+ if (bridge === undefined)
317
+ return null;
318
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
319
+ read.catch(() => undefined);
320
+ return read;
321
+ }
322
+ async message(conversation, call) {
323
+ const content = [{ type: 'text', text: String(call.text) }];
207
324
  let route;
325
+ let instructing = null;
208
326
  try {
209
- route = await this.route(conversation, input.agent_id);
210
- const files = input.files;
211
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
212
- const lines = await this.transcribed(conversation, files, paths);
213
- if (lines.length > 0)
214
- content.push({ type: 'text', text: lines.join('\n') });
327
+ route = await this.route(conversation, call.agent_id);
328
+ if (call.first === true)
329
+ instructing = this.instructed(conversation, route);
330
+ if (call.message_id !== null)
331
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
215
332
  if (conversation.kills > 0)
216
333
  throw new Error(KILLED);
217
334
  }
218
335
  catch (error) {
219
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
336
+ this.closeIdleBridge(conversation);
337
+ this.prepares(conversation, null);
338
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
220
339
  }
340
+ conversation.preparing = null;
221
341
  return {
222
- ack: new Promise((settle) => {
223
- conversation.waiting.push({ input, route, content, settle });
342
+ result: new Promise((settle) => {
343
+ conversation.waiting.push({ call, route, content, instructing, settle });
224
344
  this.next(conversation);
225
345
  }),
226
346
  };
@@ -229,62 +349,148 @@ export class Conversations {
229
349
  if (conversation.sending !== null || conversation.open > 0)
230
350
  return;
231
351
  const waiting = conversation.waiting.shift();
232
- if (waiting === undefined)
352
+ if (waiting === undefined) {
353
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
354
+ if (conversation.starting !== step)
355
+ this.starts(conversation, step);
233
356
  return;
357
+ }
234
358
  conversation.sending = this.send(conversation, waiting).finally(() => {
235
359
  conversation.sending = null;
236
360
  this.next(conversation);
237
361
  });
238
362
  }
239
363
  async send(conversation, waiting) {
240
- const { input } = waiting;
241
- let ack = {};
364
+ const { call } = waiting;
365
+ let result = {};
366
+ let started = false;
367
+ let instructing = waiting.instructing;
242
368
  const prompt = [];
243
369
  try {
244
370
  if (conversation.running === null) {
245
- const resumed = input.provider_session_id ?? conversation.session;
246
- const opened = await this.start(conversation, waiting.route, resumed);
247
- if (resumed === null)
248
- ack = { provider_session_id: opened };
371
+ this.starts(conversation, 'session');
372
+ await this.launched(conversation);
373
+ }
374
+ if (conversation.running === null) {
375
+ const stored = conversation.session ?? call.provider_session_id;
376
+ const empty = call.first === true && conversation.prompted !== stored;
377
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
378
+ if (call.first === true)
379
+ instructing = this.instructed(conversation, waiting.route);
380
+ started = (await launch.session) !== stored;
249
381
  }
250
- if (input.first === true) {
251
- prompt.push({
252
- type: 'text',
253
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
254
- });
382
+ const session = conversation.running.sessionId;
383
+ if (started || (call.first === true && session !== call.provider_session_id)) {
384
+ result = { provider_session_id: session };
385
+ }
386
+ if (call.first === true) {
387
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
255
388
  }
256
389
  if (conversation.kills > 0)
257
390
  throw new Error(KILLED);
258
391
  }
259
392
  catch (error) {
260
- if ('provider_session_id' in ack) {
393
+ this.starts(conversation, null);
394
+ if (started) {
261
395
  conversation.session = null;
262
396
  await this.kill(conversation);
263
397
  }
264
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
398
+ const refused = refusal(conversation, error);
399
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
400
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
401
+ return;
402
+ }
403
+ this.starts(conversation, null);
404
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
405
+ waiting.settle(result);
406
+ }
407
+ starts(conversation, step) {
408
+ conversation.starting = step;
409
+ if (conversation.shown !== null)
410
+ clearTimeout(conversation.shown);
411
+ conversation.shown = null;
412
+ if (step === null || conversation.told !== null) {
413
+ this.tell(conversation, step);
265
414
  return;
266
415
  }
267
- this.prompt(conversation, [...prompt, ...waiting.content]);
268
- waiting.settle(ack);
416
+ conversation.shown = setTimeout(() => {
417
+ conversation.shown = null;
418
+ if (conversation.starting === step)
419
+ this.tell(conversation, step);
420
+ }, STARTING_SHOWN_AFTER_MS);
269
421
  }
270
- async transcribed(conversation, files, paths) {
271
- const lines = [];
272
- for (const [index, path] of paths.entries()) {
273
- lines.push(path);
274
- if (files[index].media_type?.startsWith('audio/') !== true)
275
- continue;
276
- try {
277
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
278
- if (text !== null && text !== '')
279
- lines.push(`Transcript: ${text}`);
280
- }
281
- catch (error) {
282
- if (conversation.kills > 0)
283
- throw error;
284
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
422
+ prepares(conversation, step) {
423
+ conversation.preparing = step;
424
+ this.next(conversation);
425
+ }
426
+ tell(conversation, step) {
427
+ if (conversation.told === step && !conversation.untold)
428
+ return;
429
+ conversation.told = step;
430
+ this.retell(conversation);
431
+ }
432
+ retell(conversation) {
433
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
434
+ }
435
+ async transcribed(conversation, messageId, text) {
436
+ const paths = linkedPaths(text);
437
+ if (paths.length === 0)
438
+ return [];
439
+ const killed = conversation.killed.signal;
440
+ const interrupted = conversation.interrupted.signal;
441
+ const transcribing = AbortSignal.any([killed, interrupted]);
442
+ let signal = killed;
443
+ const blocks = [];
444
+ try {
445
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
446
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
447
+ for (const path of paths) {
448
+ let answered = await read(path);
449
+ if (answered === null)
450
+ continue;
451
+ signal = transcribing;
452
+ this.prepares(conversation, 'transcription');
453
+ const said = await transcribe(async () => {
454
+ const address = answered ?? (await read(path));
455
+ answered = null;
456
+ if (address === null)
457
+ throw new Error(`${path} is no longer an audio file`);
458
+ return address;
459
+ }, signal);
460
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
461
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
462
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
285
463
  }
286
464
  }
287
- return lines;
465
+ catch (error) {
466
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
467
+ throw new NotStarted(INTERRUPTED, { cause: error });
468
+ throw error;
469
+ }
470
+ return blocks;
471
+ }
472
+ bridged(conversation) {
473
+ if (conversation.bridge === null) {
474
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
475
+ conversation.bridge = bridging;
476
+ bridging.catch(() => {
477
+ if (conversation.bridge === bridging)
478
+ conversation.bridge = null;
479
+ });
480
+ }
481
+ return conversation.bridge;
482
+ }
483
+ closeBridge(conversation, bridging, bridge) {
484
+ bridge.close();
485
+ if (conversation.bridge === bridging)
486
+ conversation.bridge = null;
487
+ }
488
+ closeIdleBridge(conversation) {
489
+ const bridging = conversation.bridge;
490
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
491
+ return;
492
+ conversation.bridge = null;
493
+ void bridging.then((bridge) => bridge.close(), () => undefined);
288
494
  }
289
495
  async interrupt(conversation, turnId) {
290
496
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -295,11 +501,13 @@ export class Conversations {
295
501
  }
296
502
  halt(conversation) {
297
503
  if (conversation.opening !== null)
298
- stop(conversation.opening);
504
+ killMarked(marker(conversation.opening));
505
+ for (const scope of conversation.leftovers)
506
+ scope.kill();
299
507
  if (conversation.running === null)
300
508
  return;
301
509
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
510
+ conversation.running.scope.kill();
303
511
  }
304
512
  async kill(conversation) {
305
513
  const running = conversation.running;
@@ -337,53 +545,78 @@ export class Conversations {
337
545
  }
338
546
  return route;
339
547
  }
340
- async start(conversation, route, sessionId) {
548
+ async start(conversation, routed, sessionId, empty, bridged) {
549
+ const route = await routed;
341
550
  const reader = readerFor(route.kind);
342
551
  reader.session = sessionId;
343
552
  const app = client({ name: '@agentshouse/kit' })
344
553
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
345
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
554
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
555
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
346
556
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
347
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
557
+ const bridging = this.bridged(conversation);
558
+ const bridge = await step(unbridged, bridging);
348
559
  if (conversation.kills > 0) {
349
- bridge.close();
560
+ this.closeBridge(conversation, bridging, bridge);
350
561
  throw new Error(KILLED);
351
562
  }
352
563
  conversation.opening = bridge;
564
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
565
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
566
+ let adapter = null;
353
567
  try {
354
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
358
- void adapter.exited.then(() => bridge.close());
568
+ bridged(bridge);
569
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
570
+ const streamed = await step(NO_CONNECTION, work.open(cli));
571
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
572
+ work.read(message);
573
+ this.read(conversation, reader, message);
574
+ }, { ...bridge.env, ...streamed }, undefined, scope));
575
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
359
576
  const agent = adapter.connection.agent;
360
577
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
362
- const opened = sessionId === null
363
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
578
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
579
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
580
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
365
581
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
367
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
582
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
583
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
584
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
585
+ }
586
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
587
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
588
+ void work.settled().then(() => scope.record());
369
589
  conversation.running = running;
590
+ adapter.turn(conversation.open > 0);
370
591
  conversation.session = running.sessionId;
371
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
372
- if (conversation.commands !== null)
373
- this.commands(conversation, conversation.commands);
374
- this.options(conversation, options);
375
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
592
+ conversation.options = options;
593
+ this.facts(conversation);
594
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
376
595
  return running.sessionId;
377
596
  }
378
597
  catch (error) {
379
- stop(bridge);
380
- bridge.close();
598
+ adapter?.retire();
599
+ work.close();
600
+ conversation.leftovers.add(scope);
601
+ this.closeBridge(conversation, bridging, bridge);
381
602
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
603
  }
383
604
  finally {
384
605
  conversation.opening = null;
385
606
  }
386
607
  }
608
+ async resume(conversation, kind, adapter, request, empty) {
609
+ try {
610
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
611
+ }
612
+ catch (error) {
613
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
614
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
615
+ }
616
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
617
+ return null;
618
+ }
619
+ }
387
620
  async launchSettings(running, route, offered) {
388
621
  let options = offered;
389
622
  const settings = [
@@ -405,26 +638,37 @@ export class Conversations {
405
638
  }
406
639
  return options;
407
640
  }
408
- exited(conversation, adapter, cause) {
409
- const running = conversation.running;
410
- if (running?.adapter !== adapter)
411
- return;
412
- this.detach(conversation);
641
+ exited(conversation, running, cause) {
642
+ this.detach(conversation, running);
413
643
  if (conversation.turn !== null) {
414
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
644
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
415
645
  }
416
646
  this.next(conversation);
417
647
  this.kit.changed();
418
648
  }
419
- detach(conversation) {
649
+ detach(conversation, running) {
650
+ if (conversation.bridge === running.bridging)
651
+ conversation.bridge = null;
652
+ running.work.close();
653
+ conversation.leftovers.add(running.scope);
420
654
  conversation.running = null;
421
655
  conversation.commands = null;
422
- conversation.busy.clear();
656
+ conversation.options = null;
423
657
  for (const [id, question] of this.questions) {
424
658
  if (question.conversation === conversation)
425
659
  this.questions.delete(id);
426
660
  }
427
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
661
+ this.process(conversation, false);
662
+ }
663
+ process(conversation, running) {
664
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
665
+ }
666
+ facts(conversation) {
667
+ this.process(conversation, true);
668
+ if (conversation.commands !== null)
669
+ this.commands(conversation, conversation.commands);
670
+ if (conversation.options !== null)
671
+ this.options(conversation, conversation.options);
428
672
  }
429
673
  commands(conversation, commands) {
430
674
  conversation.commands = commands;
@@ -435,6 +679,7 @@ export class Conversations {
435
679
  options(conversation, options) {
436
680
  if (conversation.running === null)
437
681
  return;
682
+ conversation.options = options;
438
683
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
439
684
  }
440
685
  deliver(conversation, turn, event, body) {
@@ -449,7 +694,7 @@ export class Conversations {
449
694
  return delivered;
450
695
  }
451
696
  write(conversation, turn, operation) {
452
- if (!turn.admitted || turn.ended || !this.watched)
697
+ if (turn.ended || !this.watched)
453
698
  return;
454
699
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
455
700
  }
@@ -460,61 +705,74 @@ export class Conversations {
460
705
  begin(conversation) {
461
706
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
462
707
  conversation.open++;
463
- void this.deliver(conversation, turn, 'started', {})
464
- .then(() => {
465
- turn.admitted = true;
466
- this.replay(conversation, turn);
467
- })
468
- .catch(logged);
708
+ conversation.running?.adapter.turn(true);
709
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
469
710
  conversation.turn = turn;
470
711
  if (conversation.context !== null)
471
712
  turn.parts.measure(conversation.context);
472
713
  for (const part of conversation.held.splice(0))
473
714
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
715
  this.kit.changed();
477
716
  return turn;
478
717
  }
479
- end(conversation, turn, failed) {
718
+ end(conversation, turn, failed, notice = null) {
480
719
  if (turn.ended)
481
720
  return;
721
+ if (failed !== null)
722
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
482
723
  if (conversation.turn === turn) {
483
724
  for (const event of conversation.running?.reader.closeTurn() ?? [])
484
725
  this.apply(conversation, turn, event);
485
726
  }
727
+ if (notice !== null)
728
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
486
729
  turn.ended = true;
487
730
  conversation.open--;
731
+ conversation.running?.adapter.turn(conversation.open > 0);
488
732
  if (conversation.turn === turn)
489
733
  conversation.turn = null;
490
734
  const body = Promise.allSettled(turn.uploads).then(() => ({
491
- parts: turn.parts.parts,
735
+ parts: turn.parts.saved(),
492
736
  context: turn.parts.context,
493
- ...failed,
737
+ ...(failed === null ? {} : { failed: failed.failure }),
494
738
  }));
495
739
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
496
740
  this.next(conversation);
497
741
  this.kit.changed();
498
742
  }
499
- prompt(conversation, prompt) {
743
+ prompt(conversation, prompt, taken) {
500
744
  const running = conversation.running;
501
745
  const turn = this.begin(conversation);
502
746
  turn.prompted = true;
503
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
747
+ turn.taken = taken;
748
+ conversation.prompted = running.sessionId;
749
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
750
+ this.took(turn);
751
+ const notice = answeredFailure(answer);
752
+ if (notice !== null) {
753
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
754
+ }
755
+ else {
756
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
757
+ }
758
+ }, async (error) => {
504
759
  if (running.adapter.connection.signal.aborted) {
505
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
760
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
506
761
  }
507
762
  else {
763
+ this.took(turn);
508
764
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
765
+ running.adapter.retire();
766
+ this.detach(conversation, running);
511
767
  }
512
- this.end(conversation, turn, { failed: causeOf(error) });
768
+ this.end(conversation, turn, answeredWith(error));
513
769
  }
514
770
  });
515
771
  }
516
772
  started(conversation) {
517
- return conversation.turn ?? this.begin(conversation);
773
+ const turn = conversation.turn ?? this.begin(conversation);
774
+ this.took(turn);
775
+ return turn;
518
776
  }
519
777
  ended(conversation) {
520
778
  const turn = conversation.turn;
@@ -592,20 +850,6 @@ export class Conversations {
592
850
  case 'options':
593
851
  this.options(conversation, event.options);
594
852
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
853
  case 'context':
610
854
  conversation.context = { used: event.used, window: event.window };
611
855
  conversation.turn?.parts.measure(conversation.context);
@@ -623,6 +867,11 @@ export class Conversations {
623
867
  }
624
868
  this.apply(conversation, this.started(conversation), event);
625
869
  }
870
+ took(turn) {
871
+ const taken = turn.taken;
872
+ turn.taken = null;
873
+ taken?.();
874
+ }
626
875
  apply(conversation, turn, event) {
627
876
  switch (event.kind) {
628
877
  case 'text':
@@ -647,7 +896,7 @@ export class Conversations {
647
896
  turn.parts.plan(event.entries);
648
897
  return;
649
898
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
899
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
900
  this.kit.changed();
652
901
  return;
653
902
  }
@@ -667,23 +916,30 @@ export class Conversations {
667
916
  else
668
917
  conversation.turn.parts.start(part);
669
918
  }
670
- async image(conversation, turn, data, mediaType) {
919
+ async image(conversation, turn, reserved, data, mediaType) {
671
920
  const bytes = Buffer.from(data, 'base64');
672
921
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
922
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
673
923
  try {
674
924
  const declared = (await this.deliver(conversation, turn, 'images', {
675
925
  name,
676
926
  media_type: mediaType,
677
927
  bytes: bytes.length,
678
- sha256: createHash('sha256').update(bytes).digest('hex'),
928
+ sha256,
679
929
  }));
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 });
930
+ const saved = await uploaded(declared.upload, {
931
+ name,
932
+ bytes: bytes.length,
933
+ sha256,
934
+ read: (start, end) => [bytes.subarray(start, end)],
935
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
936
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
937
+ });
938
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
684
939
  }
685
940
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
941
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
942
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
687
943
  }
688
944
  }
689
945
  }