@agentshouse/kit 0.1.0-alpha.1 → 0.1.0-alpha.10

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.
package/src/house.js CHANGED
@@ -1,6 +1,8 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { chmod, mkdir, readFile, rm } from "node:fs/promises";
2
+ import { connect, createServer } from "node:net";
3
+ import { dirname, join } from "node:path";
3
4
  import { assertAcquiredCommandAdapter } from "./adapter.js";
5
+ import { readFrames, writeFrame } from "./channel.js";
4
6
  import {
5
7
  assistantDraftsPath,
6
8
  assistantTextPath,
@@ -35,6 +37,8 @@ import { declarationReader } from "./declaration.js";
35
37
  import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
36
38
 
37
39
  const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
40
+ const RESIDENT = "Resident";
41
+ const LEASE_ACTIVE = "environment_control_lease_active";
38
42
  const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
39
43
  const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
40
44
 
@@ -105,6 +109,172 @@ function heldLease(answer) {
105
109
  return { lease: answer.lease, expiresAt: Date.parse(answer.expires_at) };
106
110
  }
107
111
 
112
+ function residentSocket(directory) {
113
+ return join(directory, "run", "resident.sock");
114
+ }
115
+
116
+ function residentAnswer(status, code) {
117
+ const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
118
+ return { status, type: "application/json", body };
119
+ }
120
+
121
+ async function residentRelay(session, request, signal) {
122
+ const path = typeof request.path === "string" ? request.path : "";
123
+ if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
124
+ return residentAnswer(403, "kit_resident_route_refused");
125
+ }
126
+ let authorization;
127
+ try {
128
+ authorization = await heldControl(session, signal);
129
+ } catch (error) {
130
+ return residentAnswer(error.status ?? 401, error.code ?? "kit_lease_rejected");
131
+ }
132
+ try {
133
+ const response = await (session.dependencies.fetch ?? fetch)(`${session.house}${path}`, {
134
+ method: request.method,
135
+ headers: { ...request.headers, authorization },
136
+ body: request.text ?? (request.bytes === undefined ? undefined : Buffer.from(request.bytes, "base64")),
137
+ signal,
138
+ });
139
+ const body = Buffer.from(await response.arrayBuffer());
140
+ return {
141
+ status: response.status,
142
+ type: response.headers.get("content-type"),
143
+ body: body.length === 0 ? null : body.toString("base64"),
144
+ };
145
+ } catch {
146
+ return residentAnswer(503, "kit_house_unavailable");
147
+ }
148
+ }
149
+
150
+ export async function serveResident(session, directory, signal) {
151
+ const socket = residentSocket(directory);
152
+ await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
153
+ await rm(socket, { force: true });
154
+ let holder = null;
155
+ const server = createServer((connection) => {
156
+ connection.on("error", () => connection.destroy());
157
+ if (holder !== null) {
158
+ writeFrame(connection, { refused: LEASE_ACTIVE });
159
+ connection.end();
160
+ return;
161
+ }
162
+ holder = connection;
163
+ const relayed = new Map();
164
+ connection.on("close", () => {
165
+ if (holder === connection) holder = null;
166
+ for (const cancelled of relayed.values()) cancelled.abort();
167
+ });
168
+ renewControl(session, signal).then(
169
+ () => {
170
+ writeFrame(connection, { held: true });
171
+ readFrames(connection, {
172
+ onFrame: (request) => {
173
+ if (request.cancel === true) {
174
+ relayed.get(request.id)?.abort();
175
+ return;
176
+ }
177
+ const cancelled = new AbortController();
178
+ relayed.set(request.id, cancelled);
179
+ void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal])).then(
180
+ (answer) => {
181
+ relayed.delete(request.id);
182
+ if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
183
+ },
184
+ );
185
+ },
186
+ onRefusal: () => connection.destroy(),
187
+ });
188
+ },
189
+ (error) => {
190
+ writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
191
+ connection.end();
192
+ },
193
+ );
194
+ });
195
+ await new Promise((resolve, reject) => {
196
+ server.once("error", reject);
197
+ server.listen(socket, resolve);
198
+ });
199
+ await chmod(socket, 0o600);
200
+ return async () => {
201
+ holder?.destroy();
202
+ await new Promise((resolve) => server.close(resolve));
203
+ await rm(socket, { force: true });
204
+ };
205
+ }
206
+
207
+ async function residentDelegation(directory, fetcher) {
208
+ const connection = connect(residentSocket(directory));
209
+ const pending = new Map();
210
+ let greet;
211
+ const greeting = new Promise((resolve) => {
212
+ greet = resolve;
213
+ });
214
+ const unavailable = () => residentAnswer(503, "kit_resident_unavailable");
215
+ function settle(id, answer) {
216
+ pending.get(id)?.(answer);
217
+ pending.delete(id);
218
+ if (pending.size === 0) connection.unref();
219
+ }
220
+ connection.on("error", () => connection.destroy());
221
+ connection.on("close", () => {
222
+ greet(null);
223
+ for (const id of [...pending.keys()]) settle(id, unavailable());
224
+ });
225
+ readFrames(connection, {
226
+ onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
227
+ onRefusal: () => connection.destroy(),
228
+ });
229
+ const held = await greeting;
230
+ if (held === null) return null;
231
+ if (held.held !== true) {
232
+ connection.destroy();
233
+ throw new KitRefusal(held.refused);
234
+ }
235
+ connection.unref();
236
+ let next = 0;
237
+ async function delegated(url, init = {}) {
238
+ const { authorization, ...headers } = init.headers ?? {};
239
+ if (authorization !== RESIDENT) return fetcher(url, init);
240
+ init.signal?.throwIfAborted();
241
+ const address = new URL(url);
242
+ const answer = connection.destroyed
243
+ ? unavailable()
244
+ : await new Promise((resolve, reject) => {
245
+ const id = (next += 1);
246
+ const cancel = () => {
247
+ pending.delete(id);
248
+ settle(id);
249
+ if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
250
+ reject(init.signal.reason);
251
+ };
252
+ init.signal?.addEventListener("abort", cancel, { once: true });
253
+ pending.set(id, (answer) => {
254
+ init.signal?.removeEventListener("abort", cancel);
255
+ resolve(answer);
256
+ });
257
+ connection.ref();
258
+ writeFrame(connection, {
259
+ id,
260
+ method: init.method ?? "GET",
261
+ path: `${address.pathname}${address.search}`,
262
+ headers,
263
+ ...(typeof init.body === "string"
264
+ ? { text: init.body }
265
+ : init.body === undefined
266
+ ? {}
267
+ : { bytes: Buffer.from(init.body).toString("base64") }),
268
+ });
269
+ });
270
+ return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
271
+ status: answer.status,
272
+ headers: answer.type === null ? {} : { "content-type": answer.type },
273
+ });
274
+ }
275
+ return { connection, fetch: delegated };
276
+ }
277
+
108
278
  export async function openSession(options, dependencies = {}) {
109
279
  const session = {
110
280
  house: options.house,
@@ -114,6 +284,15 @@ export async function openSession(options, dependencies = {}) {
114
284
  expiresAt: 0,
115
285
  signal: options.signal,
116
286
  };
287
+ const resident =
288
+ options.acquire === true
289
+ ? null
290
+ : await residentDelegation(options.directory, dependencies.fetch ?? fetch);
291
+ if (resident !== null) {
292
+ session.resident = resident.connection;
293
+ session.dependencies = { ...dependencies, fetch: resident.fetch };
294
+ return session;
295
+ }
117
296
  const credential = await readCredential(options.directory);
118
297
  Object.assign(
119
298
  session,
@@ -131,10 +310,7 @@ export async function openSession(options, dependencies = {}) {
131
310
  return session;
132
311
  }
133
312
 
134
- export async function heldControl(session, signal = session.signal) {
135
- if (session.expiresAt - session.now().getTime() > LEASE_RENEWAL_MARGIN_MS) {
136
- return `Bearer ${session.lease}`;
137
- }
313
+ async function renewControl(session, signal) {
138
314
  Object.assign(
139
315
  session,
140
316
  heldLease(
@@ -148,6 +324,13 @@ export async function heldControl(session, signal = session.signal) {
148
324
  ),
149
325
  ),
150
326
  );
327
+ }
328
+
329
+ export async function heldControl(session, signal = session.signal) {
330
+ if (session.resident !== undefined) return RESIDENT;
331
+ if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
332
+ await renewControl(session, signal);
333
+ }
151
334
  return `Bearer ${session.lease}`;
152
335
  }
153
336
 
@@ -195,6 +378,7 @@ export function mutatingCall(classification) {
195
378
  }
196
379
 
197
380
  export async function releaseControl(session, options = {}) {
381
+ session.resident?.end();
198
382
  if (session.lease === null) return;
199
383
  const held = session.lease;
200
384
  try {
@@ -229,6 +413,7 @@ export async function maintainControl(session, operation, signal, { release = tr
229
413
  signal?.addEventListener("abort", cancelOperation, { once: true });
230
414
  });
231
415
  const renewal = (async () => {
416
+ if (session.resident !== undefined) return;
232
417
  const sleep = session.dependencies.controlSleep ?? controlSleep;
233
418
  while (!stopped) {
234
419
  const delay = Math.max(
@@ -480,6 +665,11 @@ export async function reportLogin(session, login, outcome, signal = session.sign
480
665
 
481
666
  const envelope = declarationReader("kit_acquire_rejected");
482
667
 
668
+ function routineInput(value, declaration) {
669
+ if (typeof value !== "string" || Array.from(value).length > 20_000) envelope.refuse(declaration);
670
+ return value;
671
+ }
672
+
483
673
  function acquiredRoute(value, declaration) {
484
674
  const route = envelope.plain(value, declaration);
485
675
  if (!Array.isArray(route.arguments)) envelope.refuse(`${declaration}.arguments`);
@@ -551,7 +741,7 @@ function acquiredRequest(acquired, declaration) {
551
741
  name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
552
742
  version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
553
743
  },
554
- payload: envelope.plain(request.payload, `${declaration}.request.payload`),
744
+ payload: routineInput(request.payload, `${declaration}.request.payload`),
555
745
  requested_by: envelope.text(
556
746
  request.requested_by,
557
747
  `${declaration}.request.requested_by`,
@@ -0,0 +1,84 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import WebSocket from "ws";
3
+ import { KIT_CONTRACT_VERSION } from "./contract.js";
4
+ import { heldControl } from "./house.js";
5
+ import { KitRefusal } from "./refusal.js";
6
+
7
+ const PROTOCOL = "house.kit.stream.1";
8
+
9
+ function streamAddress(house) {
10
+ const url = new URL("/kit/stream", house);
11
+ if (url.protocol === "https:") url.protocol = "wss:";
12
+ else if (url.protocol === "http:") url.protocol = "ws:";
13
+ else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
14
+ return url;
15
+ }
16
+
17
+ async function connected(session, signal, available, dependencies) {
18
+ const authorization = await heldControl(session, signal);
19
+ return new Promise((resolve, reject) => {
20
+ const Socket = dependencies.workSocket ?? WebSocket;
21
+ const socket = new Socket(streamAddress(session.house), PROTOCOL, {
22
+ headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
23
+ handshakeTimeout: 10_000,
24
+ maxPayload: 4096,
25
+ perMessageDeflate: false,
26
+ });
27
+ let finished = false;
28
+ const finish = (error = null) => {
29
+ if (finished) return;
30
+ finished = true;
31
+ signal.removeEventListener("abort", stop);
32
+ if (error === null) resolve();
33
+ else reject(error);
34
+ };
35
+ const stop = () => {
36
+ socket.terminate();
37
+ finish();
38
+ };
39
+ signal.addEventListener("abort", stop, { once: true });
40
+ socket.on("open", available);
41
+ socket.on("message", (data) => {
42
+ let frame;
43
+ try {
44
+ frame = JSON.parse(data.toString());
45
+ } catch {
46
+ return;
47
+ }
48
+ if (frame?.type === "work_available" && frame.subject === "agents") available();
49
+ });
50
+ socket.on("unexpected-response", (_request, response) => {
51
+ response.resume();
52
+ const refusal =
53
+ response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
54
+ socket.terminate();
55
+ finish(refusal);
56
+ });
57
+ socket.on("error", () => {
58
+ socket.terminate();
59
+ finish();
60
+ });
61
+ socket.on("close", () => finish());
62
+ if (signal.aborted) stop();
63
+ });
64
+ }
65
+
66
+ export async function watchKitAgentWork(session, signal, available, dependencies = {}) {
67
+ let retryMs = 500;
68
+ while (!signal.aborted) {
69
+ const began = Date.now();
70
+ try {
71
+ await connected(session, signal, available, dependencies);
72
+ } catch (error) {
73
+ if (error instanceof KitRefusal && error.code === "kit_upgrade_required") throw error;
74
+ if (signal.aborted) return;
75
+ }
76
+ if (signal.aborted) return;
77
+ retryMs = Date.now() - began >= 30_000 ? 1000 : Math.min(retryMs * 2, 30_000);
78
+ try {
79
+ await delay(dependencies.workReconnectMs ?? retryMs, undefined, { signal });
80
+ } catch (error) {
81
+ if (!signal.aborted) throw error;
82
+ }
83
+ }
84
+ }
package/src/mount.js CHANGED
@@ -84,7 +84,6 @@ export function mountTarget(subtree) {
84
84
  refuse("kit_mount_subtree_invalid", subtree);
85
85
  }
86
86
  return {
87
- kind: own ? "private" : "room",
88
87
  handle: own ? null : segments[1],
89
88
  root: own ? "/private" : `/rooms/${segments[1]}`,
90
89
  subtree,
@@ -107,10 +106,11 @@ function discoveryOf(value) {
107
106
 
108
107
  function authorityFor(discovery, target) {
109
108
  const found = discovery.authorities.find((entry) =>
110
- target.kind === "private" ? entry.kind === "private" : entry.room_handle === target.handle,
109
+ entry.kind === "room" && entry.room_handle === target.handle,
111
110
  );
112
111
  if (found === undefined) refuse("kit_mount_authority_unknown", target.root);
113
112
  if (
113
+ typeof found.room_ref !== "string" ||
114
114
  !Array.isArray(found.protected_paths) ||
115
115
  found.operations === null ||
116
116
  typeof found.operations !== "object" ||
@@ -119,8 +119,8 @@ function authorityFor(discovery, target) {
119
119
  refuse("kit_mount_rejected", "discover");
120
120
  }
121
121
  return {
122
- kind: found.kind,
123
- authority: found.kind === "private" ? "private" : found.room_ref,
122
+ kind: "room",
123
+ authority: found.room_ref,
124
124
  path: target.root,
125
125
  protected_paths: found.protected_paths,
126
126
  operations: found.operations,
@@ -326,7 +326,7 @@ function pendingTransfer(held, save) {
326
326
  };
327
327
  }
328
328
 
329
- async function submitAttachments(session, held, target, authority, plan, signal) {
329
+ async function submitAttachments(session, held, authority, plan, signal) {
330
330
  const transfers = [];
331
331
  for (const save of plan.saves) {
332
332
  const local = held.attachments.get(save.path);
@@ -341,7 +341,7 @@ async function submitAttachments(session, held, target, authority, plan, signal)
341
341
  version: save.version,
342
342
  name: declared.name,
343
343
  mediaType: declared.mediaType ?? undefined,
344
- room: target.kind === "room" ? authority.authority : null,
344
+ room: authority.authority,
345
345
  body,
346
346
  signal,
347
347
  });
@@ -471,7 +471,7 @@ export async function mountPackage(held, options, dependencies = {}) {
471
471
  };
472
472
  const localReport = join(held.root, "MIGRATION.json");
473
473
  await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
474
- result.transfers = await submitAttachments(session, held, target, authority, plan, signal);
474
+ result.transfers = await submitAttachments(session, held, authority, plan, signal);
475
475
 
476
476
  const report = `${target.subtree}/${MOUNT_REPORT_NAME}`;
477
477
  await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
package/src/setup.js CHANGED
@@ -114,14 +114,14 @@ async function signedIn(module) {
114
114
  }
115
115
  }
116
116
 
117
- async function setUpModule(context, module) {
117
+ async function setUpModule(context, module, install = true) {
118
118
  const summary = (status, reason = null) => ({
119
119
  category: module.category,
120
120
  name: module.name,
121
121
  status,
122
122
  reason,
123
123
  });
124
- if (module.install !== null) {
124
+ if (install && module.install !== null) {
125
125
  try {
126
126
  await module.install();
127
127
  } catch (error) {
@@ -144,10 +144,15 @@ async function setUpModule(context, module) {
144
144
  return summary("ready");
145
145
  }
146
146
 
147
+ export function signInWantedAgent(context, kind) {
148
+ return setUpModule(context, agentModule(context, kind), false);
149
+ }
150
+
147
151
  export async function runSetup(context, request) {
148
152
  await prepareLaunchBase(launchBase(context.dependencies));
149
153
  const modules = [];
150
154
  for (const kind of context.configuration.agents) {
155
+ await context.waitAgentLogin?.(kind);
151
156
  modules.push(await setUpModule(context, agentModule(context, kind)));
152
157
  }
153
158
  for (const instance of context.configuration.instances) {
package/src/source.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  newCheckpoint,
13
13
  stallOf,
14
14
  } from "./delivery.js";
15
- import { captureBatch, openSession } from "./house.js";
15
+ import { captureBatch, maintainControl, openSession } from "./house.js";
16
16
  import { googlePage } from "./google/source.js";
17
17
  import { houseAnswered, houseAnsweredFailure, houseFailed, KitRefusal } from "./refusal.js";
18
18
  import { recordAcceptedOriginals, sameReference, saveSourceOriginals } from "./source-originals.js";
@@ -278,9 +278,13 @@ export async function resetSource(options, dependencies = {}) {
278
278
  const configuration = await readConfiguration(options.configuration);
279
279
  const instance = sourceInstance(configuration, options.instance);
280
280
  declaredServices(instance, options.service);
281
- await openSession(options, dependencies);
282
- return {
283
- instance: instance.id,
284
- services: await resetCheckpoints(options.directory, instance.id, options.service),
285
- };
281
+ const session = await openSession(options, dependencies);
282
+ return maintainControl(
283
+ session,
284
+ async () => ({
285
+ instance: instance.id,
286
+ services: await resetCheckpoints(options.directory, instance.id, options.service),
287
+ }),
288
+ options.signal,
289
+ );
286
290
  }
package/src/sync-state.js CHANGED
@@ -26,6 +26,9 @@ function syncFile(directory, key, kind = "") {
26
26
  function recordedAuthority(authority) {
27
27
  const operations = authority?.operations;
28
28
  return (
29
+ authority?.kind === "room" &&
30
+ typeof authority.authority === "string" &&
31
+ typeof authority.path === "string" &&
29
32
  typeof authority?.log_epoch === "string" &&
30
33
  operations !== null &&
31
34
  typeof operations === "object" &&
@@ -43,6 +46,7 @@ export async function readReplicaState(directory, replica) {
43
46
  }
44
47
  const state = await readNote(syncFile(directory, replicaKey(canonical)), null);
45
48
  if (state === null) return null;
49
+ if (state.version !== EXPLICIT_SYNC_VERSION) return null;
46
50
  if (!Array.isArray(state.authorities) || !state.authorities.every(recordedAuthority)) return null;
47
51
  return state;
48
52
  }
package/src/sync.js CHANGED
@@ -105,9 +105,6 @@ async function bootstrapBundle(session, prefixes, signal) {
105
105
  }
106
106
 
107
107
  function matchedDiscovery(discovery, scope) {
108
- if (scope.manifest?.scope === "private") {
109
- return discovery.authorities.find((entry) => entry.kind === "private");
110
- }
111
108
  return discovery.authorities.find(
112
109
  (entry) => entry.kind === "room" && entry.room_ref === scope.manifest?.anchor,
113
110
  );
@@ -173,11 +170,17 @@ function operationsOf(value) {
173
170
  function authorityRecord(scope, discovery, files) {
174
171
  const found = matchedDiscovery(discovery, scope);
175
172
  const operations = found === undefined ? null : operationsOf(found.operations);
176
- if (found === undefined || !stringList(found.protected_paths) || operations === null) {
173
+ if (
174
+ scope.manifest?.scope !== "room" ||
175
+ typeof scope.manifest.anchor !== "string" ||
176
+ found === undefined ||
177
+ !stringList(found.protected_paths) ||
178
+ operations === null
179
+ ) {
177
180
  throw new KitRefusal("kit_bootstrap_rejected");
178
181
  }
179
182
  return {
180
- kind: scope.manifest.scope === "private" ? "private" : "room",
183
+ kind: "room",
181
184
  authority: scope.manifest.anchor,
182
185
  room_handle: scope.manifest.room_handle,
183
186
  path: scope.manifest.path,
@@ -192,9 +195,10 @@ function authorityRecord(scope, discovery, files) {
192
195
  async function materialize(options, discovery, bundle, signal, previous = null) {
193
196
  const authorities = [];
194
197
  for (const scope of bundle.scopes) {
198
+ const record = authorityRecord(scope, discovery, []);
195
199
  const held = previous?.authorities.find((entry) => entry.authority === scope.manifest?.anchor);
196
200
  const files = await replaceScope(options.replica, held, scope, signal);
197
- authorities.push(authorityRecord(scope, discovery, files));
201
+ authorities.push({ ...record, files });
198
202
  }
199
203
  const state = {
200
204
  version: EXPLICIT_SYNC_VERSION,
@@ -409,6 +413,7 @@ export async function bootstrap(options, dependencies = {}) {
409
413
  const canonical = await canonicalReplica(options.replica);
410
414
  const existing = await readReplicaState(options.directory, canonical);
411
415
  if (existing !== null) throw new KitRefusal("kit_sync_already_bootstrapped", canonical);
416
+ await clearPending(options.directory, canonical);
412
417
  return withSession(options, dependencies, async (session, signal) => {
413
418
  const discovery = discoveryOf(
414
419
  await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: options.prefixes }, signal),
package/src/telegram.js CHANGED
@@ -955,14 +955,14 @@ async function proveOwner(gateway, token, telegramBotUserId) {
955
955
  throw new KitRefusal("telegram_owner_not_proven");
956
956
  }
957
957
 
958
- function gatewayContext(session, options, dependencies, signal, summary = emptySummary()) {
958
+ function gatewayContext(session, options, signal, summary = emptySummary()) {
959
959
  return {
960
960
  session,
961
961
  directory: options.directory,
962
- dependencies,
962
+ dependencies: session.dependencies,
963
963
  signal,
964
964
  gate: rateGate(() => session.now().getTime()),
965
- wait: dependencies.wait ?? pause,
965
+ wait: session.dependencies.wait ?? pause,
966
966
  passes: options.passes ?? Number.POSITIVE_INFINITY,
967
967
  summary,
968
968
  };
@@ -970,7 +970,15 @@ function gatewayContext(session, options, dependencies, signal, summary = emptyS
970
970
 
971
971
  export async function connectPersonalBot(options, dependencies = {}) {
972
972
  const session = await openSession(options, dependencies);
973
- const gateway = gatewayContext(session, options, dependencies, options.signal);
973
+ return maintainControl(
974
+ session,
975
+ (signal) => connectBot(session, options, dependencies, signal),
976
+ options.signal,
977
+ );
978
+ }
979
+
980
+ async function connectBot(session, options, dependencies, signal) {
981
+ const gateway = gatewayContext(session, options, signal);
974
982
  const ask = dependencies.prompt ?? (() => askForToken(dependencies));
975
983
  const token = (await ask()).trim();
976
984
  if (token.length === 0) throw new KitRefusal("telegram_bot_token_required");
@@ -989,7 +997,7 @@ export async function connectPersonalBot(options, dependencies = {}) {
989
997
  owner_telegram_user_id: await proveOwner(gateway, token, identity.telegramBotUserId),
990
998
  has_topics_enabled: true,
991
999
  };
992
- await writeOwned(tokenPath(options.directory), `${token}\n`, options.signal);
1000
+ await writeOwned(tokenPath(options.directory), `${token}\n`, signal);
993
1001
  try {
994
1002
  await houseCall(gateway, "personal-bot", setup);
995
1003
  } catch (error) {
@@ -1001,7 +1009,15 @@ export async function connectPersonalBot(options, dependencies = {}) {
1001
1009
 
1002
1010
  export async function disconnectPersonalBot(options, dependencies = {}) {
1003
1011
  const session = await openSession(options, dependencies);
1004
- const gateway = gatewayContext(session, options, dependencies, options.signal);
1012
+ return maintainControl(
1013
+ session,
1014
+ (signal) => disconnectBot(session, options, signal),
1015
+ options.signal,
1016
+ );
1017
+ }
1018
+
1019
+ async function disconnectBot(session, options, signal) {
1020
+ const gateway = gatewayContext(session, options, signal);
1005
1021
  const directory = options.directory;
1006
1022
  const marked = await disconnecting(directory);
1007
1023
  let token = null;
@@ -1010,7 +1026,7 @@ export async function disconnectPersonalBot(options, dependencies = {}) {
1010
1026
  } catch (error) {
1011
1027
  if (!marked) throw error;
1012
1028
  }
1013
- if (!marked) await writeOwned(disconnectPath(directory), "{}\n", options.signal);
1029
+ if (!marked) await writeOwned(disconnectPath(directory), "{}\n", signal);
1014
1030
  if (token !== null) {
1015
1031
  await telegramCall(gateway, token, "deleteWebhook", { drop_pending_updates: false });
1016
1032
  await Promise.all([
@@ -1032,7 +1048,7 @@ export async function serveTelegramGateway(options, dependencies = {}) {
1032
1048
  async (signal) => {
1033
1049
  const stopping = new AbortController();
1034
1050
  const shared = AbortSignal.any([signal, stopping.signal]);
1035
- const gateway = gatewayContext(session, options, dependencies, shared, summary);
1051
+ const gateway = gatewayContext(session, options, shared, summary);
1036
1052
  summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
1037
1053
  await serveActivities(gateway, stopping);
1038
1054
  },