@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (150) hide show
  1. package/README.md +41 -1671
  2. package/bin/connect-linux.sh +501 -53
  3. package/bin/connect-windows.ps1 +430 -0
  4. package/bin/kit.js +200 -159
  5. package/bootstrap.json +28 -0
  6. package/docs/configuration.md +104 -0
  7. package/docs/google.md +33 -0
  8. package/docs/linux.md +77 -0
  9. package/docs/macos.md +26 -0
  10. package/docs/telegram.md +29 -0
  11. package/docs/windows.md +36 -0
  12. package/package.json +7 -2
  13. package/src/adapter-mcp.js +92 -12
  14. package/src/agents.js +63 -8
  15. package/src/binding.js +52 -0
  16. package/src/blocks.js +2 -26
  17. package/src/bridge.js +28 -14
  18. package/src/bytes.js +19 -8
  19. package/src/configuration.js +14 -211
  20. package/src/connector-login.js +3 -84
  21. package/src/contract.js +44 -12
  22. package/src/conversation-originals.js +1 -3
  23. package/src/delivery.js +4 -7
  24. package/src/directory.js +6 -26
  25. package/src/draft.js +4 -82
  26. package/src/git-main.js +138 -0
  27. package/src/git.js +62 -0
  28. package/src/google/calendar.js +39 -45
  29. package/src/google/drive.js +61 -186
  30. package/src/google/entries.js +349 -0
  31. package/src/google/gateway.js +3 -6
  32. package/src/google/gmail.js +33 -23
  33. package/src/google/oauth.js +106 -66
  34. package/src/google/source.js +21 -23
  35. package/src/host.js +6 -2
  36. package/src/house-main.js +65 -8
  37. package/src/house.js +446 -127
  38. package/src/how-we-work.md +25 -0
  39. package/src/inbound.js +2 -18
  40. package/src/kit-work-stream.js +120 -0
  41. package/src/launcher.js +46 -10
  42. package/src/local-files.js +3 -11
  43. package/src/login.js +27 -14
  44. package/src/mcp.js +18 -0
  45. package/src/mount.js +9 -18
  46. package/src/original.js +4 -4
  47. package/src/pacing.js +41 -0
  48. package/src/plan.js +37 -45
  49. package/src/repository.js +3 -61
  50. package/src/session-bin/git +2 -0
  51. package/src/setup.js +14 -15
  52. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  53. package/src/skill-set/ask-matt/SKILL.md +91 -0
  54. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  55. package/src/skill-set/code-review/LICENSE +21 -0
  56. package/src/skill-set/code-review/SKILL.md +89 -0
  57. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  58. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  59. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  60. package/src/skill-set/codebase-design/LICENSE +21 -0
  61. package/src/skill-set/codebase-design/SKILL.md +116 -0
  62. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  63. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  64. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  65. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  66. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  67. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  68. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  69. package/src/skill-set/domain-modeling/LICENSE +21 -0
  70. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  71. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  72. package/src/skill-set/grill-me/LICENSE +21 -0
  73. package/src/skill-set/grill-me/SKILL.md +8 -0
  74. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  75. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  76. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  77. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  78. package/src/skill-set/grilling/LICENSE +21 -0
  79. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  80. package/src/skill-set/handoff/LICENSE +21 -0
  81. package/src/skill-set/handoff/SKILL.md +17 -0
  82. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  83. package/src/skill-set/implement/LICENSE +21 -0
  84. package/src/skill-set/implement/SKILL.md +16 -0
  85. package/src/skill-set/implement/agents/openai.yaml +4 -0
  86. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  87. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  88. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  89. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  90. package/src/skill-set/prototype/LICENSE +21 -0
  91. package/src/skill-set/prototype/LOGIC.md +67 -0
  92. package/src/skill-set/prototype/SKILL.md +28 -0
  93. package/src/skill-set/prototype/UI.md +112 -0
  94. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  95. package/src/skill-set/research/LICENSE +21 -0
  96. package/src/skill-set/research/SKILL.md +14 -0
  97. package/src/skill-set/research/agents/openai.yaml +3 -0
  98. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  99. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  100. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  101. package/src/skill-set/tdd/LICENSE +21 -0
  102. package/src/skill-set/tdd/SKILL.md +40 -0
  103. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  104. package/src/skill-set/tdd/mocking.md +59 -0
  105. package/src/skill-set/tdd/tests.md +77 -0
  106. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  107. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  108. package/src/skill-set/teach/LICENSE +21 -0
  109. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  110. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  111. package/src/skill-set/teach/SKILL.md +141 -0
  112. package/src/skill-set/teach/agents/openai.yaml +4 -0
  113. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  114. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  115. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  116. package/src/skill-set/to-spec/LICENSE +21 -0
  117. package/src/skill-set/to-spec/SKILL.md +76 -0
  118. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  119. package/src/skill-set/to-tickets/LICENSE +21 -0
  120. package/src/skill-set/to-tickets/SKILL.md +106 -0
  121. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  122. package/src/skill-set/wait-what/LICENSE +21 -0
  123. package/src/skill-set/wait-what/SKILL.md +8 -0
  124. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  125. package/src/skill-set/wayfinder/LICENSE +21 -0
  126. package/src/skill-set/wayfinder/SKILL.md +129 -0
  127. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  128. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  129. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  130. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  131. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  132. package/src/skill-set.js +85 -0
  133. package/src/source.js +93 -54
  134. package/src/state.js +19 -1
  135. package/src/sync-library.js +7 -11
  136. package/src/sync-state.js +12 -3
  137. package/src/sync.js +142 -43
  138. package/src/tasks.js +92 -0
  139. package/src/telegram-view.js +217 -0
  140. package/src/telegram.js +204 -69
  141. package/src/turn-frames.js +69 -0
  142. package/src/worker.js +738 -189
  143. package/src/working-copy.js +1274 -0
  144. package/src/workspace.js +299 -88
  145. package/src/google/declared.js +0 -146
  146. package/src/google/id.js +0 -1
  147. package/src/instance/google.js +0 -1
  148. package/src/starter-set.js +0 -17
  149. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  150. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/telegram.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  PERSONAL_BOT_OFFSET_RELATIVE_PATH,
11
11
  PERSONAL_BOT_TOKEN_RELATIVE_PATH,
12
12
  TELEGRAM_BOT_API_TIMEOUT_MS,
13
+ TELEGRAM_CAPTION_CHARACTERS_MAX,
13
14
  TELEGRAM_CONTROL_KEYBOARD_COMMANDS,
14
15
  TELEGRAM_DELIVERY_BATCH_MAX,
15
16
  TELEGRAM_DELIVERY_KINDS,
@@ -18,14 +19,18 @@ import {
18
19
  TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES,
19
20
  TELEGRAM_INGRESS_BATCH_MAX,
20
21
  TELEGRAM_MEDIA_KINDS,
22
+ TELEGRAM_MESSAGE_CHARACTERS_MAX,
21
23
  TELEGRAM_UNSUPPORTED_MEDIA_FIELDS,
22
24
  TELEGRAM_UPDATE_OUTCOMES,
23
25
  telegramApiUrl,
24
26
  telegramFileUrl,
25
27
  } from "./contract.js";
26
28
  import { heldControl, houseRequest, maintainControl, openSession } from "./house.js";
29
+ import { watchKitWork } from "./kit-work-stream.js";
27
30
  import { KitRefusal } from "./refusal.js";
28
31
  import { writeText } from "./state.js";
32
+ import { richPieces, slices, turnViews } from "./telegram-view.js";
33
+ import { kitStreamWriter } from "./turn-frames.js";
29
34
 
30
35
  const TOKEN_PROMPT = "Paste the BotFather token for this Environment: ";
31
36
  const AMBIGUOUS_DELIVERY = Object.freeze({ telegram_message_id: null, telegram_topic_id: null });
@@ -282,6 +287,28 @@ export function deliveryCall(delivery) {
282
287
  return { method: "sendMessage", body: sendBody(delivery) };
283
288
  }
284
289
 
290
+ function topicMessage(chat, topic, text) {
291
+ const body = { chat_id: Number(chat), text };
292
+ if (topic !== null) body.message_thread_id = Number(topic);
293
+ return { method: "sendMessage", body };
294
+ }
295
+
296
+ function deliveryCalls(delivery) {
297
+ const call = deliveryCall(delivery);
298
+ if (call.method === "sendMessage" && call.body.text.length > TELEGRAM_MESSAGE_CHARACTERS_MAX) {
299
+ const [first, ...following] = slices(call.body.text, TELEGRAM_MESSAGE_CHARACTERS_MAX);
300
+ return [
301
+ { method: call.method, body: { ...call.body, text: first } },
302
+ ...following.map((text) => topicMessage(delivery.chat_id, delivery.topic_id, text)),
303
+ ];
304
+ }
305
+ if (call.method !== "sendRichMessage") return [call];
306
+ return richPieces(call.body.rich_message).map((rich_message) => ({
307
+ method: call.method,
308
+ body: { ...call.body, rich_message },
309
+ }));
310
+ }
311
+
285
312
  function acceptedDelivery(delivery, result) {
286
313
  if (delivery.kind === "conversation_topic") {
287
314
  const threadId = plain(result) ? result.message_thread_id : undefined;
@@ -353,12 +380,15 @@ function queuedDelivery(value) {
353
380
  return value;
354
381
  }
355
382
 
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;
383
+ function threadsOf(answer) {
384
+ if (!Array.isArray(answer?.threads)) refuseAnswer();
385
+ return answer.threads.map((thread) => {
386
+ if (!plain(thread)) refuseAnswer();
387
+ for (const named of ["conversation_id", "chat_id", "topic_id"]) {
388
+ if (typeof thread[named] !== "string" || thread[named].length === 0) refuseAnswer();
389
+ }
390
+ return { conversation_id: thread.conversation_id, chat_id: thread.chat_id, topic_id: thread.topic_id };
391
+ });
362
392
  }
363
393
 
364
394
  function queuedUpload(value) {
@@ -374,7 +404,6 @@ function queueOf(answer) {
374
404
  return answer.queue.map((item) => {
375
405
  if (!plain(item)) refuseAnswer();
376
406
  if (item.type === "message") return { type: "message", delivery: queuedDelivery(item.delivery) };
377
- if (item.type === "draft") return { type: "draft", draft: queuedDraft(item.draft) };
378
407
  if (item.type !== "file") refuseAnswer();
379
408
  return { type: "file", upload: queuedUpload(item.upload) };
380
409
  });
@@ -722,7 +751,7 @@ async function relayInboundFiles(gateway) {
722
751
  gateway.summary.relayed.push({ attachment: file.id, bytes: bytes.length });
723
752
  } catch (error) {
724
753
  if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
725
- gateway.summary.failures.push({ activity: "inbound", item: file.id, code: error.code });
754
+ refusedPass(gateway, { activity: "inbound", item: file.id, code: error.code });
726
755
  }
727
756
  }
728
757
  }
@@ -751,23 +780,51 @@ async function reportUpload(gateway, upload, settled) {
751
780
 
752
781
  async function sendQueuedMessage(gateway, token, delivery) {
753
782
  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;
783
+ let first = null;
784
+ for (const call of deliveryCalls(delivery)) {
785
+ let answered;
786
+ try {
787
+ answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
788
+ } catch (error) {
789
+ if (cancelled(error)) throw error;
790
+ await reportDelivery(gateway, delivery, {
791
+ outcome: "delivery_unknown",
792
+ report: AMBIGUOUS_DELIVERY,
793
+ });
794
+ return false;
795
+ }
796
+ const settled = deliveryOutcome(delivery, answered);
797
+ const retryAfter = telegramRetryAfter(answered);
798
+ if (retryAfter !== null) gateway.gate.hold(retryAfter);
799
+ if (settled.outcome !== "delivered") {
800
+ await reportDelivery(gateway, delivery, settled);
801
+ return retryAfter !== null;
802
+ }
803
+ first ??= settled;
765
804
  }
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;
805
+ await reportDelivery(gateway, delivery, first);
806
+ return false;
807
+ }
808
+
809
+ function captionPieces(caption) {
810
+ if (caption === null || caption.length <= TELEGRAM_CAPTION_CHARACTERS_MAX) return [caption];
811
+ const [head] = slices(caption, TELEGRAM_CAPTION_CHARACTERS_MAX);
812
+ return [head, ...slices(caption.slice(head.length), TELEGRAM_MESSAGE_CHARACTERS_MAX)];
813
+ }
814
+
815
+ async function sentOverflow(gateway, token, upload, overflow) {
816
+ for (const text of overflow) {
817
+ const call = topicMessage(upload.chat_id, upload.topic_id, text);
818
+ let answered;
819
+ try {
820
+ answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
821
+ } catch (error) {
822
+ if (cancelled(error)) throw error;
823
+ return { settled: uploadOutcome(null), answered: null };
824
+ }
825
+ if (!botApiAccepted(answered)) return { settled: uploadOutcome(answered), answered };
826
+ }
827
+ return null;
771
828
  }
772
829
 
773
830
  async function sendQueuedUpload(gateway, token, upload) {
@@ -789,12 +846,13 @@ async function sendQueuedUpload(gateway, token, upload) {
789
846
  });
790
847
  return false;
791
848
  }
849
+ const [caption, ...overflow] = captionPieces(upload.caption);
792
850
  let answered;
793
851
  try {
794
852
  answered = await telegramUpload(
795
853
  gateway,
796
854
  telegramApiUrl(token, "sendPhoto"),
797
- uploadFields(upload),
855
+ uploadFields({ ...upload, caption }),
798
856
  { field: "photo", name: upload.file_name, bytes },
799
857
  );
800
858
  } catch (error) {
@@ -805,78 +863,121 @@ async function sendQueuedUpload(gateway, token, upload) {
805
863
  });
806
864
  return false;
807
865
  }
808
- await reportUpload(gateway, upload, uploadOutcome(answered));
809
- const retryAfter = telegramRetryAfter(answered);
866
+ const photographed = uploadOutcome(answered);
867
+ const unsent =
868
+ photographed.outcome === "delivered" ? await sentOverflow(gateway, token, upload, overflow) : null;
869
+ await reportUpload(gateway, upload, unsent?.settled ?? photographed);
870
+ const retryAfter = telegramRetryAfter(unsent === null ? answered : unsent.answered);
810
871
  if (retryAfter === null) return false;
811
872
  gateway.gate.hold(retryAfter);
812
873
  return true;
813
874
  }
814
875
 
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;
876
+ function liveDrafts(gateway) {
877
+ const stream = kitStreamWriter();
878
+ const views = turnViews({ gap: (frame) => stream.write(frame) });
879
+ const dirty = new Set();
880
+ let pumping = null;
881
+
882
+ async function pump() {
883
+ while (dirty.size > 0) {
884
+ const [conversation] = dirty;
885
+ dirty.delete(conversation);
886
+ if (gateway.gate.open()) {
887
+ dirty.clear();
888
+ return;
889
+ }
890
+ const draft = views.draft(conversation);
891
+ if (draft === null) continue;
892
+ const call = draftCall(draft);
893
+ let answered;
894
+ try {
895
+ const token = await gatewayToken(gateway);
896
+ answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
897
+ } catch (error) {
898
+ if (!(error instanceof KitRefusal)) throw error;
899
+ if (cancelled(error)) dirty.clear();
900
+ continue;
901
+ }
902
+ const retryAfter = telegramRetryAfter(answered);
903
+ if (retryAfter !== null) {
904
+ gateway.gate.hold(retryAfter);
905
+ dirty.clear();
906
+ }
830
907
  }
831
908
  }
909
+
910
+ function start() {
911
+ if (pumping !== null || dirty.size === 0) return;
912
+ pumping = pump().finally(() => {
913
+ pumping = null;
914
+ start();
915
+ });
916
+ pumping.catch(() => undefined);
917
+ }
918
+
919
+ return {
920
+ heard: {
921
+ available: () => undefined,
922
+ changed: () => undefined,
923
+ opened: stream.opened,
924
+ closed: stream.closed,
925
+ viewed: (frame) => {
926
+ const conversation = views.receive(frame);
927
+ if (conversation === null) return;
928
+ dirty.add(conversation);
929
+ start();
930
+ },
931
+ },
932
+ address: (threads) => views.address(threads),
933
+ settled: () => pumping ?? Promise.resolve(),
934
+ };
832
935
  }
833
936
 
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
- );
937
+ function watchGatewayStream(session, signal, heard) {
938
+ if (session.relay !== undefined) return session.relay(signal, heard);
939
+ return (session.dependencies.watchKitWork ?? watchKitWork)(session, signal, heard, session.dependencies);
839
940
  }
840
941
 
841
942
  async function drainOutboundQueue(gateway) {
842
943
  if (gateway.gate.open()) return;
843
944
  const token = await gatewayToken(gateway);
844
- const queue = queueOf(
845
- await houseCall(gateway, "queue/acquire", { max: TELEGRAM_DELIVERY_BATCH_MAX }),
846
- );
847
- const drafts = [];
945
+ const answer = await houseCall(gateway, "queue/acquire", { max: TELEGRAM_DELIVERY_BATCH_MAX });
946
+ const queue = queueOf(answer);
947
+ gateway.live?.address(threadsOf(answer));
848
948
  for (const item of queue) {
849
- if (item.type === "draft") {
850
- drafts.push(item.draft);
851
- continue;
852
- }
853
949
  const gated =
854
950
  item.type === "file"
855
951
  ? await sendQueuedUpload(gateway, token, item.upload)
856
952
  : await sendQueuedMessage(gateway, token, item.delivery);
857
953
  if (gated) return;
858
954
  }
859
- await streamQueuedDrafts(gateway, token, drafts);
955
+ }
956
+
957
+ function refusedPass(gateway, failure) {
958
+ gateway.summary.failures.push(failure);
959
+ gateway.refused(failure);
860
960
  }
861
961
 
862
962
  async function onePass(gateway, activity, work) {
863
963
  try {
864
964
  await work(gateway);
865
965
  } catch (error) {
866
- if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
867
- gateway.summary.failures.push({ activity, item: null, code: error.code });
966
+ if (cancelled(error) || !(error instanceof KitRefusal) || error.code === "kit_update_draining") throw error;
967
+ refusedPass(gateway, { activity, item: null, code: error.code });
868
968
  }
869
969
  }
870
970
 
871
971
  async function paceActivity(gateway, stopping, activity, work) {
872
972
  try {
873
- for (let index = 0; index < gateway.passes; index += 1) {
973
+ for (let index = 0; index < gateway.passes && !gateway.draining; index += 1) {
874
974
  if (gateway.signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
875
975
  await onePass(gateway, activity, work);
876
976
  if (index + 1 < gateway.passes) await gateway.wait(TELEGRAM_GATEWAY_POLL_MS, gateway.signal);
877
977
  }
878
978
  } catch (error) {
879
- stopping.abort();
979
+ if (error.code === "kit_update_draining") gateway.draining = true;
980
+ else stopping.abort();
880
981
  throw error;
881
982
  }
882
983
  }
@@ -955,22 +1056,32 @@ async function proveOwner(gateway, token, telegramBotUserId) {
955
1056
  throw new KitRefusal("telegram_owner_not_proven");
956
1057
  }
957
1058
 
958
- function gatewayContext(session, options, dependencies, signal, summary = emptySummary()) {
1059
+ function gatewayContext(session, options, signal, summary = emptySummary()) {
959
1060
  return {
960
1061
  session,
961
1062
  directory: options.directory,
962
- dependencies,
1063
+ dependencies: session.dependencies,
963
1064
  signal,
964
1065
  gate: rateGate(() => session.now().getTime()),
965
- wait: dependencies.wait ?? pause,
1066
+ wait: session.dependencies.wait ?? pause,
966
1067
  passes: options.passes ?? Number.POSITIVE_INFINITY,
1068
+ draining: false,
967
1069
  summary,
1070
+ refused: options.refused,
968
1071
  };
969
1072
  }
970
1073
 
971
1074
  export async function connectPersonalBot(options, dependencies = {}) {
972
1075
  const session = await openSession(options, dependencies);
973
- const gateway = gatewayContext(session, options, dependencies, options.signal);
1076
+ return maintainControl(
1077
+ session,
1078
+ (signal) => connectBot(session, options, dependencies, signal),
1079
+ options.signal,
1080
+ );
1081
+ }
1082
+
1083
+ async function connectBot(session, options, dependencies, signal) {
1084
+ const gateway = gatewayContext(session, options, signal);
974
1085
  const ask = dependencies.prompt ?? (() => askForToken(dependencies));
975
1086
  const token = (await ask()).trim();
976
1087
  if (token.length === 0) throw new KitRefusal("telegram_bot_token_required");
@@ -989,7 +1100,7 @@ export async function connectPersonalBot(options, dependencies = {}) {
989
1100
  owner_telegram_user_id: await proveOwner(gateway, token, identity.telegramBotUserId),
990
1101
  has_topics_enabled: true,
991
1102
  };
992
- await writeOwned(tokenPath(options.directory), `${token}\n`, options.signal);
1103
+ await writeOwned(tokenPath(options.directory), `${token}\n`, signal);
993
1104
  try {
994
1105
  await houseCall(gateway, "personal-bot", setup);
995
1106
  } catch (error) {
@@ -1001,7 +1112,15 @@ export async function connectPersonalBot(options, dependencies = {}) {
1001
1112
 
1002
1113
  export async function disconnectPersonalBot(options, dependencies = {}) {
1003
1114
  const session = await openSession(options, dependencies);
1004
- const gateway = gatewayContext(session, options, dependencies, options.signal);
1115
+ return maintainControl(
1116
+ session,
1117
+ (signal) => disconnectBot(session, options, signal),
1118
+ options.signal,
1119
+ );
1120
+ }
1121
+
1122
+ async function disconnectBot(session, options, signal) {
1123
+ const gateway = gatewayContext(session, options, signal);
1005
1124
  const directory = options.directory;
1006
1125
  const marked = await disconnecting(directory);
1007
1126
  let token = null;
@@ -1010,7 +1129,7 @@ export async function disconnectPersonalBot(options, dependencies = {}) {
1010
1129
  } catch (error) {
1011
1130
  if (!marked) throw error;
1012
1131
  }
1013
- if (!marked) await writeOwned(disconnectPath(directory), "{}\n", options.signal);
1132
+ if (!marked) await writeOwned(disconnectPath(directory), "{}\n", signal);
1014
1133
  if (token !== null) {
1015
1134
  await telegramCall(gateway, token, "deleteWebhook", { drop_pending_updates: false });
1016
1135
  await Promise.all([
@@ -1032,9 +1151,25 @@ export async function serveTelegramGateway(options, dependencies = {}) {
1032
1151
  async (signal) => {
1033
1152
  const stopping = new AbortController();
1034
1153
  const shared = AbortSignal.any([signal, stopping.signal]);
1035
- const gateway = gatewayContext(session, options, dependencies, shared, summary);
1154
+ const gateway = gatewayContext(session, options, shared, summary);
1036
1155
  summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
1037
- await serveActivities(gateway, stopping);
1156
+ gateway.live = liveDrafts(gateway);
1157
+ const watcher = new AbortController();
1158
+ const watching = watchGatewayStream(
1159
+ session,
1160
+ AbortSignal.any([shared, watcher.signal]),
1161
+ gateway.live.heard,
1162
+ ).catch((error) => {
1163
+ if (cancelled(error)) return;
1164
+ refusedPass(gateway, { activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
1165
+ });
1166
+ try {
1167
+ await serveActivities(gateway, stopping);
1168
+ } finally {
1169
+ watcher.abort();
1170
+ await watching;
1171
+ await gateway.live.settled();
1172
+ }
1038
1173
  },
1039
1174
  options.signal,
1040
1175
  );
@@ -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
+ }