@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (142) hide show
  1. package/README.md +697 -260
  2. package/bin/connect-linux.sh +200 -25
  3. package/bin/kit.js +179 -157
  4. package/bootstrap.json +17 -0
  5. package/package.json +7 -2
  6. package/src/adapter-mcp.js +76 -12
  7. package/src/agents.js +63 -8
  8. package/src/binding.js +52 -0
  9. package/src/blocks.js +2 -26
  10. package/src/bridge.js +28 -14
  11. package/src/bytes.js +19 -8
  12. package/src/configuration.js +13 -209
  13. package/src/connector-login.js +3 -84
  14. package/src/contract.js +36 -11
  15. package/src/delivery.js +4 -7
  16. package/src/directory.js +6 -26
  17. package/src/draft.js +4 -82
  18. package/src/git-main.js +138 -0
  19. package/src/git.js +62 -0
  20. package/src/google/calendar.js +39 -45
  21. package/src/google/drive.js +61 -186
  22. package/src/google/entries.js +346 -0
  23. package/src/google/gateway.js +3 -6
  24. package/src/google/gmail.js +33 -23
  25. package/src/google/oauth.js +106 -66
  26. package/src/google/source.js +21 -23
  27. package/src/host.js +6 -2
  28. package/src/house-main.js +65 -8
  29. package/src/house.js +446 -127
  30. package/src/how-we-work.md +25 -0
  31. package/src/inbound.js +2 -18
  32. package/src/kit-work-stream.js +118 -0
  33. package/src/launcher.js +46 -10
  34. package/src/local-files.js +3 -11
  35. package/src/login.js +23 -11
  36. package/src/mcp.js +18 -0
  37. package/src/mount.js +13 -21
  38. package/src/original.js +4 -4
  39. package/src/pacing.js +41 -0
  40. package/src/plan.js +37 -45
  41. package/src/repository.js +3 -61
  42. package/src/session-bin/git +2 -0
  43. package/src/setup.js +14 -15
  44. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  45. package/src/skill-set/ask-matt/SKILL.md +91 -0
  46. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  47. package/src/skill-set/code-review/LICENSE +21 -0
  48. package/src/skill-set/code-review/SKILL.md +89 -0
  49. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  50. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  51. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/src/skill-set/codebase-design/LICENSE +21 -0
  53. package/src/skill-set/codebase-design/SKILL.md +116 -0
  54. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  55. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  56. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  57. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  58. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  59. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  60. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  61. package/src/skill-set/domain-modeling/LICENSE +21 -0
  62. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  63. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  64. package/src/skill-set/grill-me/LICENSE +21 -0
  65. package/src/skill-set/grill-me/SKILL.md +8 -0
  66. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  67. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  68. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  69. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  70. package/src/skill-set/grilling/LICENSE +21 -0
  71. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  72. package/src/skill-set/handoff/LICENSE +21 -0
  73. package/src/skill-set/handoff/SKILL.md +17 -0
  74. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  75. package/src/skill-set/implement/LICENSE +21 -0
  76. package/src/skill-set/implement/SKILL.md +16 -0
  77. package/src/skill-set/implement/agents/openai.yaml +4 -0
  78. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  79. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  80. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  81. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  82. package/src/skill-set/prototype/LICENSE +21 -0
  83. package/src/skill-set/prototype/LOGIC.md +67 -0
  84. package/src/skill-set/prototype/SKILL.md +28 -0
  85. package/src/skill-set/prototype/UI.md +112 -0
  86. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  87. package/src/skill-set/research/LICENSE +21 -0
  88. package/src/skill-set/research/SKILL.md +14 -0
  89. package/src/skill-set/research/agents/openai.yaml +3 -0
  90. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  91. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  92. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  93. package/src/skill-set/tdd/LICENSE +21 -0
  94. package/src/skill-set/tdd/SKILL.md +40 -0
  95. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  96. package/src/skill-set/tdd/mocking.md +59 -0
  97. package/src/skill-set/tdd/tests.md +77 -0
  98. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  99. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  100. package/src/skill-set/teach/LICENSE +21 -0
  101. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  102. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  103. package/src/skill-set/teach/SKILL.md +141 -0
  104. package/src/skill-set/teach/agents/openai.yaml +4 -0
  105. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  106. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  107. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  108. package/src/skill-set/to-spec/LICENSE +21 -0
  109. package/src/skill-set/to-spec/SKILL.md +76 -0
  110. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  111. package/src/skill-set/to-tickets/LICENSE +21 -0
  112. package/src/skill-set/to-tickets/SKILL.md +106 -0
  113. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  114. package/src/skill-set/wait-what/LICENSE +21 -0
  115. package/src/skill-set/wait-what/SKILL.md +8 -0
  116. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  117. package/src/skill-set/wayfinder/LICENSE +21 -0
  118. package/src/skill-set/wayfinder/SKILL.md +129 -0
  119. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  120. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  121. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  122. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  123. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  124. package/src/skill-set.js +85 -0
  125. package/src/source.js +93 -54
  126. package/src/state.js +19 -1
  127. package/src/sync-library.js +7 -11
  128. package/src/sync-state.js +16 -3
  129. package/src/sync.js +36 -21
  130. package/src/tasks.js +75 -0
  131. package/src/telegram-view.js +214 -0
  132. package/src/telegram.js +161 -66
  133. package/src/turn-frames.js +69 -0
  134. package/src/worker.js +674 -184
  135. package/src/working-copy.js +1221 -0
  136. package/src/workspace.js +40 -32
  137. package/src/google/declared.js +0 -146
  138. package/src/google/id.js +0 -1
  139. package/src/instance/google.js +0 -1
  140. package/src/starter-set.js +0 -17
  141. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  142. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
@@ -0,0 +1,214 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ TELEGRAM_RICH_MESSAGE_BLOCKS_MAX,
4
+ TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX,
5
+ } from "./contract.js";
6
+
7
+ const PARTS = Object.freeze({ draft: "blocks", plan: "steps" });
8
+ const UNCOUNTED = new Set(["type", "language"]);
9
+
10
+ function richCharacters(value, key = null) {
11
+ if (typeof value === "string") return UNCOUNTED.has(key) ? 0 : value.length;
12
+ if (Array.isArray(value)) return value.reduce((sum, item) => sum + richCharacters(item), 0);
13
+ if (typeof value === "object" && value !== null) {
14
+ return Object.entries(value).reduce((sum, [name, item]) => sum + richCharacters(item, name), 0);
15
+ }
16
+ return 0;
17
+ }
18
+
19
+ function richText(value, key = null) {
20
+ if (typeof value === "string") return UNCOUNTED.has(key) ? [] : [value];
21
+ if (Array.isArray(value)) return value.flatMap((item) => richText(item));
22
+ if (typeof value === "object" && value !== null) {
23
+ return Object.entries(value).flatMap(([name, item]) => richText(item, name));
24
+ }
25
+ return [];
26
+ }
27
+
28
+ function slices(text) {
29
+ const pieces = [];
30
+ let start = 0;
31
+ while (start < text.length) {
32
+ let end = Math.min(start + TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX, text.length);
33
+ const last = text.charCodeAt(end - 1);
34
+ if (end < text.length && last >= 0xd800 && last <= 0xdbff) end -= 1;
35
+ pieces.push(text.slice(start, end));
36
+ start = end;
37
+ }
38
+ return pieces;
39
+ }
40
+
41
+ function grouped(block, listed) {
42
+ const groups = [];
43
+ let group = [];
44
+ let characters = 0;
45
+ for (const entry of block[listed]) {
46
+ const length = richCharacters(entry);
47
+ if (group.length > 0 && characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
48
+ groups.push(group);
49
+ group = [];
50
+ characters = 0;
51
+ }
52
+ group.push(entry);
53
+ characters += length;
54
+ }
55
+ if (group.length > 0) groups.push(group);
56
+ return groups.flatMap((entries) => fitted({ ...block, [listed]: entries }));
57
+ }
58
+
59
+ function fitted(block) {
60
+ if (richCharacters(block) <= TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) return [block];
61
+ if (typeof block.text === "string") return slices(block.text).map((text) => ({ ...block, text }));
62
+ if (typeof block.expression === "string") {
63
+ return slices(block.expression).map((expression) => ({ ...block, expression }));
64
+ }
65
+ for (const listed of ["items", "cells"]) {
66
+ if (Array.isArray(block[listed]) && block[listed].length > 1) return grouped(block, listed);
67
+ }
68
+ return slices(richText(block).join("\n")).map((text) => ({ type: "paragraph", text }));
69
+ }
70
+
71
+ export function richPieces(message) {
72
+ const pieces = [];
73
+ let blocks = [];
74
+ let characters = 0;
75
+ for (const part of message.blocks.flatMap(fitted)) {
76
+ const length = richCharacters(part);
77
+ if (
78
+ blocks.length > 0 &&
79
+ (blocks.length >= TELEGRAM_RICH_MESSAGE_BLOCKS_MAX ||
80
+ characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX)
81
+ ) {
82
+ pieces.push({ ...message, blocks });
83
+ blocks = [];
84
+ characters = 0;
85
+ }
86
+ blocks.push(part);
87
+ characters += length;
88
+ }
89
+ pieces.push({ ...message, blocks });
90
+ return pieces;
91
+ }
92
+
93
+ function listItems(items, ordered) {
94
+ return items.map((item, index) => ({
95
+ blocks: [{ type: "paragraph", text: item }],
96
+ ...(ordered ? { value: index + 1, type: "1" } : {}),
97
+ }));
98
+ }
99
+
100
+ function telegramBlock(block) {
101
+ if (typeof block !== "object" || block === null) return null;
102
+ if (block.type === "paragraph") return { type: "paragraph", text: block.text };
103
+ if (block.type === "heading") return { type: "heading", size: block.level, text: block.text };
104
+ if (block.type === "code") {
105
+ return {
106
+ type: "pre",
107
+ ...(typeof block.language === "string" ? { language: block.language } : {}),
108
+ text: block.text,
109
+ };
110
+ }
111
+ if (block.type === "list" && Array.isArray(block.items)) {
112
+ return { type: "list", items: listItems(block.items, block.ordered === true) };
113
+ }
114
+ if (block.type === "table" && Array.isArray(block.headers) && Array.isArray(block.rows)) {
115
+ return {
116
+ type: "table",
117
+ cells: [
118
+ block.headers.map((text) => ({ text, is_header: true })),
119
+ ...block.rows.map((row) => (Array.isArray(row) ? row : []).map((text) => ({ text }))),
120
+ ],
121
+ is_bordered: true,
122
+ };
123
+ }
124
+ if (block.type === "formula") return { type: "mathematical_expression", expression: block.expression };
125
+ return null;
126
+ }
127
+
128
+ function planBlocks(steps) {
129
+ return [
130
+ { type: "heading", text: "Plan", size: 3 },
131
+ {
132
+ type: "list",
133
+ items: steps.map((step, index) => ({
134
+ blocks: [{ type: "paragraph", text: [`${step.label} — `, { type: "bold", text: step.status }] }],
135
+ value: index + 1,
136
+ type: "1",
137
+ })),
138
+ },
139
+ ];
140
+ }
141
+
142
+ function draftId(turn) {
143
+ return (createHash("sha256").update(turn).digest().readUInt32BE(0) % 2_147_483_647) + 1;
144
+ }
145
+
146
+ function viewFrame(frame) {
147
+ if (typeof frame !== "object" || frame === null || !Object.hasOwn(PARTS, frame.type)) return null;
148
+ for (const named of ["conversation_id", "turn_id"]) {
149
+ if (typeof frame[named] !== "string" || frame[named] === "") return null;
150
+ }
151
+ if (!Number.isSafeInteger(frame.sequence) || frame.sequence < 1) return null;
152
+ if (!Number.isSafeInteger(frame.from) || frame.from < 0) return null;
153
+ return Array.isArray(frame[PARTS[frame.type]]) ? frame : null;
154
+ }
155
+
156
+ export function turnViews(options) {
157
+ let threads = new Map();
158
+ const views = new Map();
159
+
160
+ function applied(view, frame) {
161
+ const items = frame[PARTS[frame.type]];
162
+ const held = view[frame.type];
163
+ if (held !== null && frame.sequence <= held.sequence) return false;
164
+ if (frame.from === 0) {
165
+ view[frame.type] = { sequence: frame.sequence, items };
166
+ view.asked = false;
167
+ return true;
168
+ }
169
+ if (held === null || frame.sequence !== held.sequence + 1 || frame.from > held.items.length) {
170
+ if (!view.asked) {
171
+ view.asked = true;
172
+ options.gap({ type: "gap", conversation_id: frame.conversation_id, turn_id: frame.turn_id });
173
+ }
174
+ return false;
175
+ }
176
+ view[frame.type] = { sequence: frame.sequence, items: [...held.items.slice(0, frame.from), ...items] };
177
+ return true;
178
+ }
179
+
180
+ return {
181
+ address(listed) {
182
+ threads = new Map(listed.map((thread) => [thread.conversation_id, thread]));
183
+ for (const conversation of views.keys()) {
184
+ if (!threads.has(conversation)) views.delete(conversation);
185
+ }
186
+ },
187
+ receive(value) {
188
+ const frame = viewFrame(value);
189
+ if (frame === null || !threads.has(frame.conversation_id)) return null;
190
+ let view = views.get(frame.conversation_id);
191
+ if (view === undefined || view.turn !== frame.turn_id) {
192
+ view = { turn: frame.turn_id, draft: null, plan: null, asked: false };
193
+ views.set(frame.conversation_id, view);
194
+ }
195
+ return applied(view, frame) ? frame.conversation_id : null;
196
+ },
197
+ draft(conversation) {
198
+ const view = views.get(conversation);
199
+ const thread = threads.get(conversation);
200
+ if (view === undefined || thread === undefined) return null;
201
+ const blocks = [
202
+ ...(view.plan === null ? [] : planBlocks(view.plan.items)),
203
+ ...(view.draft === null ? [] : view.draft.items.map(telegramBlock).filter((block) => block !== null)),
204
+ ];
205
+ if (blocks.length === 0) return null;
206
+ return {
207
+ chat_id: thread.chat_id,
208
+ topic_id: thread.topic_id,
209
+ draft_id: draftId(view.turn),
210
+ rich_message: richPieces({ blocks })[0],
211
+ };
212
+ },
213
+ };
214
+ }
package/src/telegram.js CHANGED
@@ -24,8 +24,11 @@ import {
24
24
  telegramFileUrl,
25
25
  } from "./contract.js";
26
26
  import { heldControl, houseRequest, maintainControl, openSession } from "./house.js";
27
+ import { watchKitWork } from "./kit-work-stream.js";
27
28
  import { KitRefusal } from "./refusal.js";
28
29
  import { writeText } from "./state.js";
30
+ import { richPieces, turnViews } from "./telegram-view.js";
31
+ import { kitStreamWriter } from "./turn-frames.js";
29
32
 
30
33
  const TOKEN_PROMPT = "Paste the BotFather token for this Environment: ";
31
34
  const AMBIGUOUS_DELIVERY = Object.freeze({ telegram_message_id: null, telegram_topic_id: null });
@@ -282,6 +285,15 @@ export function deliveryCall(delivery) {
282
285
  return { method: "sendMessage", body: sendBody(delivery) };
283
286
  }
284
287
 
288
+ function deliveryCalls(delivery) {
289
+ const call = deliveryCall(delivery);
290
+ if (call.method !== "sendRichMessage") return [call];
291
+ return richPieces(call.body.rich_message).map((rich_message) => ({
292
+ method: call.method,
293
+ body: { ...call.body, rich_message },
294
+ }));
295
+ }
296
+
285
297
  function acceptedDelivery(delivery, result) {
286
298
  if (delivery.kind === "conversation_topic") {
287
299
  const threadId = plain(result) ? result.message_thread_id : undefined;
@@ -353,12 +365,15 @@ function queuedDelivery(value) {
353
365
  return value;
354
366
  }
355
367
 
356
- function queuedDraft(value) {
357
- if (!plain(value) || !plain(value.rich_message)) refuseAnswer();
358
- for (const named of ["conversation_id", "turn_id", "chat_id", "topic_id", "draft_id"]) {
359
- if (typeof value[named] !== "string" || value[named].length === 0) refuseAnswer();
360
- }
361
- return value;
368
+ function threadsOf(answer) {
369
+ if (!Array.isArray(answer?.threads)) refuseAnswer();
370
+ return answer.threads.map((thread) => {
371
+ if (!plain(thread)) refuseAnswer();
372
+ for (const named of ["conversation_id", "chat_id", "topic_id"]) {
373
+ if (typeof thread[named] !== "string" || thread[named].length === 0) refuseAnswer();
374
+ }
375
+ return { conversation_id: thread.conversation_id, chat_id: thread.chat_id, topic_id: thread.topic_id };
376
+ });
362
377
  }
363
378
 
364
379
  function queuedUpload(value) {
@@ -374,7 +389,6 @@ function queueOf(answer) {
374
389
  return answer.queue.map((item) => {
375
390
  if (!plain(item)) refuseAnswer();
376
391
  if (item.type === "message") return { type: "message", delivery: queuedDelivery(item.delivery) };
377
- if (item.type === "draft") return { type: "draft", draft: queuedDraft(item.draft) };
378
392
  if (item.type !== "file") refuseAnswer();
379
393
  return { type: "file", upload: queuedUpload(item.upload) };
380
394
  });
@@ -722,7 +736,7 @@ async function relayInboundFiles(gateway) {
722
736
  gateway.summary.relayed.push({ attachment: file.id, bytes: bytes.length });
723
737
  } catch (error) {
724
738
  if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
725
- gateway.summary.failures.push({ activity: "inbound", item: file.id, code: error.code });
739
+ refusedPass(gateway, { activity: "inbound", item: file.id, code: error.code });
726
740
  }
727
741
  }
728
742
  }
@@ -751,23 +765,30 @@ async function reportUpload(gateway, upload, settled) {
751
765
 
752
766
  async function sendQueuedMessage(gateway, token, delivery) {
753
767
  await houseCall(gateway, `deliveries/${delivery.id}/attempt`);
754
- const call = deliveryCall(delivery);
755
- let answered;
756
- try {
757
- answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
758
- } catch (error) {
759
- if (cancelled(error)) throw error;
760
- await reportDelivery(gateway, delivery, {
761
- outcome: "delivery_unknown",
762
- report: AMBIGUOUS_DELIVERY,
763
- });
764
- return false;
768
+ let first = null;
769
+ for (const call of deliveryCalls(delivery)) {
770
+ let answered;
771
+ try {
772
+ answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
773
+ } catch (error) {
774
+ if (cancelled(error)) throw error;
775
+ await reportDelivery(gateway, delivery, {
776
+ outcome: "delivery_unknown",
777
+ report: AMBIGUOUS_DELIVERY,
778
+ });
779
+ return false;
780
+ }
781
+ const settled = deliveryOutcome(delivery, answered);
782
+ const retryAfter = telegramRetryAfter(answered);
783
+ if (retryAfter !== null) gateway.gate.hold(retryAfter);
784
+ if (settled.outcome !== "delivered") {
785
+ await reportDelivery(gateway, delivery, settled);
786
+ return retryAfter !== null;
787
+ }
788
+ first ??= settled;
765
789
  }
766
- await reportDelivery(gateway, delivery, deliveryOutcome(delivery, answered));
767
- const retryAfter = telegramRetryAfter(answered);
768
- if (retryAfter === null) return false;
769
- gateway.gate.hold(retryAfter);
770
- return true;
790
+ await reportDelivery(gateway, delivery, first);
791
+ return false;
771
792
  }
772
793
 
773
794
  async function sendQueuedUpload(gateway, token, upload) {
@@ -812,71 +833,111 @@ async function sendQueuedUpload(gateway, token, upload) {
812
833
  return true;
813
834
  }
814
835
 
815
- export async function streamDrafts(drafts, gate, post) {
816
- for (const draft of drafts) {
817
- if (gate.open()) return;
818
- const call = draftCall(draft);
819
- let answered;
820
- try {
821
- answered = await post(call);
822
- } catch (error) {
823
- if (cancelled(error)) throw error;
824
- return;
825
- }
826
- const retryAfter = telegramRetryAfter(answered);
827
- if (retryAfter !== null) {
828
- gate.hold(retryAfter);
829
- return;
836
+ function liveDrafts(gateway) {
837
+ const stream = kitStreamWriter();
838
+ const views = turnViews({ gap: (frame) => stream.write(frame) });
839
+ const dirty = new Set();
840
+ let pumping = null;
841
+
842
+ async function pump() {
843
+ while (dirty.size > 0) {
844
+ const [conversation] = dirty;
845
+ dirty.delete(conversation);
846
+ if (gateway.gate.open()) {
847
+ dirty.clear();
848
+ return;
849
+ }
850
+ const draft = views.draft(conversation);
851
+ if (draft === null) continue;
852
+ const call = draftCall(draft);
853
+ let answered;
854
+ try {
855
+ const token = await gatewayToken(gateway);
856
+ answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
857
+ } catch (error) {
858
+ if (!(error instanceof KitRefusal)) throw error;
859
+ if (cancelled(error)) dirty.clear();
860
+ continue;
861
+ }
862
+ const retryAfter = telegramRetryAfter(answered);
863
+ if (retryAfter !== null) {
864
+ gateway.gate.hold(retryAfter);
865
+ dirty.clear();
866
+ }
830
867
  }
831
868
  }
869
+
870
+ function start() {
871
+ if (pumping !== null || dirty.size === 0) return;
872
+ pumping = pump().finally(() => {
873
+ pumping = null;
874
+ start();
875
+ });
876
+ pumping.catch(() => undefined);
877
+ }
878
+
879
+ return {
880
+ heard: {
881
+ available: () => undefined,
882
+ changed: () => undefined,
883
+ opened: stream.opened,
884
+ closed: stream.closed,
885
+ viewed: (frame) => {
886
+ const conversation = views.receive(frame);
887
+ if (conversation === null) return;
888
+ dirty.add(conversation);
889
+ start();
890
+ },
891
+ },
892
+ address: (threads) => views.address(threads),
893
+ settled: () => pumping ?? Promise.resolve(),
894
+ };
832
895
  }
833
896
 
834
- async function streamQueuedDrafts(gateway, token, drafts) {
835
- if (gateway.gate.open()) return;
836
- await streamDrafts(drafts, gateway.gate, (call) =>
837
- telegramPost(gateway, telegramApiUrl(token, call.method), call.body),
838
- );
897
+ function watchGatewayStream(session, signal, heard) {
898
+ if (session.relay !== undefined) return session.relay(signal, heard);
899
+ return (session.dependencies.watchKitWork ?? watchKitWork)(session, signal, heard, session.dependencies);
839
900
  }
840
901
 
841
902
  async function drainOutboundQueue(gateway) {
842
903
  if (gateway.gate.open()) return;
843
904
  const token = await gatewayToken(gateway);
844
- const queue = queueOf(
845
- await houseCall(gateway, "queue/acquire", { max: TELEGRAM_DELIVERY_BATCH_MAX }),
846
- );
847
- const drafts = [];
905
+ const answer = await houseCall(gateway, "queue/acquire", { max: TELEGRAM_DELIVERY_BATCH_MAX });
906
+ const queue = queueOf(answer);
907
+ gateway.live?.address(threadsOf(answer));
848
908
  for (const item of queue) {
849
- if (item.type === "draft") {
850
- drafts.push(item.draft);
851
- continue;
852
- }
853
909
  const gated =
854
910
  item.type === "file"
855
911
  ? await sendQueuedUpload(gateway, token, item.upload)
856
912
  : await sendQueuedMessage(gateway, token, item.delivery);
857
913
  if (gated) return;
858
914
  }
859
- await streamQueuedDrafts(gateway, token, drafts);
915
+ }
916
+
917
+ function refusedPass(gateway, failure) {
918
+ gateway.summary.failures.push(failure);
919
+ gateway.refused(failure);
860
920
  }
861
921
 
862
922
  async function onePass(gateway, activity, work) {
863
923
  try {
864
924
  await work(gateway);
865
925
  } catch (error) {
866
- if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
867
- gateway.summary.failures.push({ activity, item: null, code: error.code });
926
+ if (cancelled(error) || !(error instanceof KitRefusal) || error.code === "kit_update_draining") throw error;
927
+ refusedPass(gateway, { activity, item: null, code: error.code });
868
928
  }
869
929
  }
870
930
 
871
931
  async function paceActivity(gateway, stopping, activity, work) {
872
932
  try {
873
- for (let index = 0; index < gateway.passes; index += 1) {
933
+ for (let index = 0; index < gateway.passes && !gateway.draining; index += 1) {
874
934
  if (gateway.signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
875
935
  await onePass(gateway, activity, work);
876
936
  if (index + 1 < gateway.passes) await gateway.wait(TELEGRAM_GATEWAY_POLL_MS, gateway.signal);
877
937
  }
878
938
  } catch (error) {
879
- stopping.abort();
939
+ if (error.code === "kit_update_draining") gateway.draining = true;
940
+ else stopping.abort();
880
941
  throw error;
881
942
  }
882
943
  }
@@ -955,22 +1016,32 @@ async function proveOwner(gateway, token, telegramBotUserId) {
955
1016
  throw new KitRefusal("telegram_owner_not_proven");
956
1017
  }
957
1018
 
958
- function gatewayContext(session, options, dependencies, signal, summary = emptySummary()) {
1019
+ function gatewayContext(session, options, signal, summary = emptySummary()) {
959
1020
  return {
960
1021
  session,
961
1022
  directory: options.directory,
962
- dependencies,
1023
+ dependencies: session.dependencies,
963
1024
  signal,
964
1025
  gate: rateGate(() => session.now().getTime()),
965
- wait: dependencies.wait ?? pause,
1026
+ wait: session.dependencies.wait ?? pause,
966
1027
  passes: options.passes ?? Number.POSITIVE_INFINITY,
1028
+ draining: false,
967
1029
  summary,
1030
+ refused: options.refused,
968
1031
  };
969
1032
  }
970
1033
 
971
1034
  export async function connectPersonalBot(options, dependencies = {}) {
972
1035
  const session = await openSession(options, dependencies);
973
- const gateway = gatewayContext(session, options, dependencies, options.signal);
1036
+ return maintainControl(
1037
+ session,
1038
+ (signal) => connectBot(session, options, dependencies, signal),
1039
+ options.signal,
1040
+ );
1041
+ }
1042
+
1043
+ async function connectBot(session, options, dependencies, signal) {
1044
+ const gateway = gatewayContext(session, options, signal);
974
1045
  const ask = dependencies.prompt ?? (() => askForToken(dependencies));
975
1046
  const token = (await ask()).trim();
976
1047
  if (token.length === 0) throw new KitRefusal("telegram_bot_token_required");
@@ -989,7 +1060,7 @@ export async function connectPersonalBot(options, dependencies = {}) {
989
1060
  owner_telegram_user_id: await proveOwner(gateway, token, identity.telegramBotUserId),
990
1061
  has_topics_enabled: true,
991
1062
  };
992
- await writeOwned(tokenPath(options.directory), `${token}\n`, options.signal);
1063
+ await writeOwned(tokenPath(options.directory), `${token}\n`, signal);
993
1064
  try {
994
1065
  await houseCall(gateway, "personal-bot", setup);
995
1066
  } catch (error) {
@@ -1001,7 +1072,15 @@ export async function connectPersonalBot(options, dependencies = {}) {
1001
1072
 
1002
1073
  export async function disconnectPersonalBot(options, dependencies = {}) {
1003
1074
  const session = await openSession(options, dependencies);
1004
- const gateway = gatewayContext(session, options, dependencies, options.signal);
1075
+ return maintainControl(
1076
+ session,
1077
+ (signal) => disconnectBot(session, options, signal),
1078
+ options.signal,
1079
+ );
1080
+ }
1081
+
1082
+ async function disconnectBot(session, options, signal) {
1083
+ const gateway = gatewayContext(session, options, signal);
1005
1084
  const directory = options.directory;
1006
1085
  const marked = await disconnecting(directory);
1007
1086
  let token = null;
@@ -1010,7 +1089,7 @@ export async function disconnectPersonalBot(options, dependencies = {}) {
1010
1089
  } catch (error) {
1011
1090
  if (!marked) throw error;
1012
1091
  }
1013
- if (!marked) await writeOwned(disconnectPath(directory), "{}\n", options.signal);
1092
+ if (!marked) await writeOwned(disconnectPath(directory), "{}\n", signal);
1014
1093
  if (token !== null) {
1015
1094
  await telegramCall(gateway, token, "deleteWebhook", { drop_pending_updates: false });
1016
1095
  await Promise.all([
@@ -1032,9 +1111,25 @@ export async function serveTelegramGateway(options, dependencies = {}) {
1032
1111
  async (signal) => {
1033
1112
  const stopping = new AbortController();
1034
1113
  const shared = AbortSignal.any([signal, stopping.signal]);
1035
- const gateway = gatewayContext(session, options, dependencies, shared, summary);
1114
+ const gateway = gatewayContext(session, options, shared, summary);
1036
1115
  summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
1037
- await serveActivities(gateway, stopping);
1116
+ gateway.live = liveDrafts(gateway);
1117
+ const watcher = new AbortController();
1118
+ const watching = watchGatewayStream(
1119
+ session,
1120
+ AbortSignal.any([shared, watcher.signal]),
1121
+ gateway.live.heard,
1122
+ ).catch((error) => {
1123
+ if (cancelled(error)) return;
1124
+ refusedPass(gateway, { activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
1125
+ });
1126
+ try {
1127
+ await serveActivities(gateway, stopping);
1128
+ } finally {
1129
+ watcher.abort();
1130
+ await watching;
1131
+ await gateway.live.settled();
1132
+ }
1038
1133
  },
1039
1134
  options.signal,
1040
1135
  );
@@ -0,0 +1,69 @@
1
+ import { assistantBlocks } from "./blocks.js";
2
+
3
+ export function kitStreamWriter() {
4
+ let send = null;
5
+ let generation = 0;
6
+ return {
7
+ get generation() {
8
+ return generation;
9
+ },
10
+ opened(write) {
11
+ generation += 1;
12
+ send = write;
13
+ },
14
+ closed() {
15
+ send = null;
16
+ },
17
+ write(frame) {
18
+ if (send === null) return false;
19
+ send(frame);
20
+ return true;
21
+ },
22
+ };
23
+ }
24
+
25
+ function firstChange(written, items) {
26
+ const shared = Math.min(written.length, items.length);
27
+ for (let index = 0; index < shared; index += 1) {
28
+ if (JSON.stringify(written[index]) !== JSON.stringify(items[index])) return index;
29
+ }
30
+ return shared;
31
+ }
32
+
33
+ export function turnFrames(stream) {
34
+ let identity = null;
35
+ const parts = {
36
+ draft: { key: "blocks", latest: null, written: null, generation: null },
37
+ plan: { key: "steps", latest: null, written: null, generation: null },
38
+ };
39
+
40
+ function write(type) {
41
+ const part = parts[type];
42
+ if (identity === null || part.latest === null) return;
43
+ const { sequence, items } = part.latest;
44
+ const whole = part.written === null || part.generation !== stream.generation;
45
+ const from = whole ? 0 : firstChange(part.written, items);
46
+ const generation = stream.generation;
47
+ const sent = stream.write({ type, ...identity, sequence, from, [part.key]: items.slice(from) });
48
+ part.written = sent ? items : null;
49
+ part.generation = generation;
50
+ }
51
+
52
+ return {
53
+ identify(named) {
54
+ identity = named;
55
+ write("draft");
56
+ write("plan");
57
+ },
58
+ draft(view) {
59
+ const blocks = assistantBlocks(view.text);
60
+ if (blocks === null) return;
61
+ parts.draft.latest = { sequence: view.sequence, items: blocks };
62
+ write("draft");
63
+ },
64
+ plan(snapshot) {
65
+ parts.plan.latest = { sequence: snapshot.sequence, items: snapshot.steps };
66
+ write("plan");
67
+ },
68
+ };
69
+ }