@botcord/daemon 0.1.1

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 (149) hide show
  1. package/dist/activity-tracker.d.ts +43 -0
  2. package/dist/activity-tracker.js +110 -0
  3. package/dist/adapters/runtimes.d.ts +14 -0
  4. package/dist/adapters/runtimes.js +18 -0
  5. package/dist/agent-discovery.d.ts +81 -0
  6. package/dist/agent-discovery.js +181 -0
  7. package/dist/agent-workspace.d.ts +31 -0
  8. package/dist/agent-workspace.js +221 -0
  9. package/dist/config.d.ts +116 -0
  10. package/dist/config.js +180 -0
  11. package/dist/control-channel.d.ts +99 -0
  12. package/dist/control-channel.js +388 -0
  13. package/dist/cross-room.d.ts +23 -0
  14. package/dist/cross-room.js +55 -0
  15. package/dist/daemon-config-map.d.ts +61 -0
  16. package/dist/daemon-config-map.js +153 -0
  17. package/dist/daemon.d.ts +123 -0
  18. package/dist/daemon.js +349 -0
  19. package/dist/doctor.d.ts +89 -0
  20. package/dist/doctor.js +191 -0
  21. package/dist/gateway/channel-manager.d.ts +54 -0
  22. package/dist/gateway/channel-manager.js +292 -0
  23. package/dist/gateway/channels/botcord.d.ts +93 -0
  24. package/dist/gateway/channels/botcord.js +510 -0
  25. package/dist/gateway/channels/index.d.ts +2 -0
  26. package/dist/gateway/channels/index.js +1 -0
  27. package/dist/gateway/channels/sanitize.d.ts +20 -0
  28. package/dist/gateway/channels/sanitize.js +56 -0
  29. package/dist/gateway/dispatcher.d.ts +73 -0
  30. package/dist/gateway/dispatcher.js +431 -0
  31. package/dist/gateway/gateway.d.ts +87 -0
  32. package/dist/gateway/gateway.js +158 -0
  33. package/dist/gateway/index.d.ts +15 -0
  34. package/dist/gateway/index.js +15 -0
  35. package/dist/gateway/log.d.ts +9 -0
  36. package/dist/gateway/log.js +20 -0
  37. package/dist/gateway/router.d.ts +10 -0
  38. package/dist/gateway/router.js +48 -0
  39. package/dist/gateway/runtimes/claude-code.d.ts +30 -0
  40. package/dist/gateway/runtimes/claude-code.js +162 -0
  41. package/dist/gateway/runtimes/codex.d.ts +83 -0
  42. package/dist/gateway/runtimes/codex.js +272 -0
  43. package/dist/gateway/runtimes/gemini.d.ts +15 -0
  44. package/dist/gateway/runtimes/gemini.js +29 -0
  45. package/dist/gateway/runtimes/ndjson-stream.d.ts +43 -0
  46. package/dist/gateway/runtimes/ndjson-stream.js +169 -0
  47. package/dist/gateway/runtimes/probe.d.ts +17 -0
  48. package/dist/gateway/runtimes/probe.js +54 -0
  49. package/dist/gateway/runtimes/registry.d.ts +59 -0
  50. package/dist/gateway/runtimes/registry.js +94 -0
  51. package/dist/gateway/session-store.d.ts +39 -0
  52. package/dist/gateway/session-store.js +133 -0
  53. package/dist/gateway/types.d.ts +265 -0
  54. package/dist/gateway/types.js +1 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +854 -0
  57. package/dist/log.d.ts +7 -0
  58. package/dist/log.js +44 -0
  59. package/dist/provision.d.ts +88 -0
  60. package/dist/provision.js +749 -0
  61. package/dist/room-context-fetcher.d.ts +18 -0
  62. package/dist/room-context-fetcher.js +101 -0
  63. package/dist/room-context.d.ts +53 -0
  64. package/dist/room-context.js +112 -0
  65. package/dist/sender-classify.d.ts +30 -0
  66. package/dist/sender-classify.js +32 -0
  67. package/dist/snapshot-writer.d.ts +37 -0
  68. package/dist/snapshot-writer.js +84 -0
  69. package/dist/status-render.d.ts +28 -0
  70. package/dist/status-render.js +97 -0
  71. package/dist/system-context.d.ts +57 -0
  72. package/dist/system-context.js +91 -0
  73. package/dist/turn-text.d.ts +36 -0
  74. package/dist/turn-text.js +57 -0
  75. package/dist/user-auth.d.ts +75 -0
  76. package/dist/user-auth.js +245 -0
  77. package/dist/working-memory.d.ts +46 -0
  78. package/dist/working-memory.js +274 -0
  79. package/package.json +39 -0
  80. package/src/__tests__/activity-tracker.test.ts +130 -0
  81. package/src/__tests__/agent-discovery.test.ts +191 -0
  82. package/src/__tests__/agent-workspace.test.ts +147 -0
  83. package/src/__tests__/control-channel.test.ts +327 -0
  84. package/src/__tests__/cross-room.test.ts +116 -0
  85. package/src/__tests__/daemon-config-map.test.ts +416 -0
  86. package/src/__tests__/daemon.test.ts +300 -0
  87. package/src/__tests__/device-code.test.ts +152 -0
  88. package/src/__tests__/doctor.test.ts +218 -0
  89. package/src/__tests__/protocol-core-reexport.test.ts +24 -0
  90. package/src/__tests__/provision.test.ts +922 -0
  91. package/src/__tests__/room-context.test.ts +233 -0
  92. package/src/__tests__/runtime-discovery.test.ts +173 -0
  93. package/src/__tests__/snapshot-writer.test.ts +141 -0
  94. package/src/__tests__/status-render.test.ts +137 -0
  95. package/src/__tests__/system-context.test.ts +315 -0
  96. package/src/__tests__/turn-text.test.ts +116 -0
  97. package/src/__tests__/user-auth.test.ts +125 -0
  98. package/src/__tests__/working-memory.test.ts +240 -0
  99. package/src/activity-tracker.ts +140 -0
  100. package/src/adapters/runtimes.ts +30 -0
  101. package/src/agent-discovery.ts +262 -0
  102. package/src/agent-workspace.ts +247 -0
  103. package/src/config.ts +290 -0
  104. package/src/control-channel.ts +455 -0
  105. package/src/cross-room.ts +89 -0
  106. package/src/daemon-config-map.ts +200 -0
  107. package/src/daemon.ts +478 -0
  108. package/src/doctor.ts +282 -0
  109. package/src/gateway/__tests__/.gitkeep +0 -0
  110. package/src/gateway/__tests__/botcord-channel.test.ts +480 -0
  111. package/src/gateway/__tests__/channel-manager.test.ts +475 -0
  112. package/src/gateway/__tests__/claude-code-adapter.test.ts +318 -0
  113. package/src/gateway/__tests__/codex-adapter.test.ts +350 -0
  114. package/src/gateway/__tests__/dispatcher.test.ts +1159 -0
  115. package/src/gateway/__tests__/gateway-add-channel.test.ts +180 -0
  116. package/src/gateway/__tests__/gateway-managed-routes.test.ts +181 -0
  117. package/src/gateway/__tests__/gateway.test.ts +222 -0
  118. package/src/gateway/__tests__/router.test.ts +247 -0
  119. package/src/gateway/__tests__/sanitize.test.ts +193 -0
  120. package/src/gateway/__tests__/session-store.test.ts +235 -0
  121. package/src/gateway/channel-manager.ts +349 -0
  122. package/src/gateway/channels/botcord.ts +605 -0
  123. package/src/gateway/channels/index.ts +6 -0
  124. package/src/gateway/channels/sanitize.ts +68 -0
  125. package/src/gateway/dispatcher.ts +554 -0
  126. package/src/gateway/gateway.ts +211 -0
  127. package/src/gateway/index.ts +29 -0
  128. package/src/gateway/log.ts +30 -0
  129. package/src/gateway/router.ts +60 -0
  130. package/src/gateway/runtimes/claude-code.ts +180 -0
  131. package/src/gateway/runtimes/codex.ts +312 -0
  132. package/src/gateway/runtimes/gemini.ts +43 -0
  133. package/src/gateway/runtimes/ndjson-stream.ts +225 -0
  134. package/src/gateway/runtimes/probe.ts +73 -0
  135. package/src/gateway/runtimes/registry.ts +143 -0
  136. package/src/gateway/session-store.ts +157 -0
  137. package/src/gateway/types.ts +325 -0
  138. package/src/index.ts +961 -0
  139. package/src/log.ts +47 -0
  140. package/src/provision.ts +879 -0
  141. package/src/room-context-fetcher.ts +124 -0
  142. package/src/room-context.ts +167 -0
  143. package/src/sender-classify.ts +46 -0
  144. package/src/snapshot-writer.ts +103 -0
  145. package/src/status-render.ts +132 -0
  146. package/src/system-context.ts +162 -0
  147. package/src/turn-text.ts +93 -0
  148. package/src/user-auth.ts +295 -0
  149. package/src/working-memory.ts +352 -0
@@ -0,0 +1,349 @@
1
+ import type {
2
+ ChannelAdapter,
3
+ ChannelStartContext,
4
+ ChannelStatusSnapshot,
5
+ GatewayConfig,
6
+ GatewayInboundEnvelope,
7
+ } from "./types.js";
8
+ import type { GatewayLogger } from "./log.js";
9
+
10
+ /** Exponential backoff tuning for crashed-channel restarts. */
11
+ export interface ChannelBackoffOptions {
12
+ initial?: number;
13
+ max?: number;
14
+ factor?: number;
15
+ }
16
+
17
+ /** Constructor options for `ChannelManager`. */
18
+ export interface ChannelManagerOptions {
19
+ config: GatewayConfig;
20
+ channels: ChannelAdapter[];
21
+ log: GatewayLogger;
22
+ emit: (env: GatewayInboundEnvelope) => Promise<void>;
23
+ backoffMs?: ChannelBackoffOptions;
24
+ }
25
+
26
+ type LifecycleState = "idle" | "starting" | "running" | "stopping" | "crashed";
27
+
28
+ interface ChannelEntry {
29
+ adapter: ChannelAdapter;
30
+ accountId: string;
31
+ state: LifecycleState;
32
+ snapshot: ChannelStatusSnapshot;
33
+ controller: AbortController | null;
34
+ runPromise: Promise<void> | null;
35
+ restartTimer: ReturnType<typeof setTimeout> | null;
36
+ nextBackoff: number;
37
+ currentStartAt: number;
38
+ reconnectAttempts: number;
39
+ stopRequested: boolean;
40
+ }
41
+
42
+ const DEFAULT_INITIAL_BACKOFF = 1000;
43
+ const DEFAULT_MAX_BACKOFF = 60_000;
44
+ const DEFAULT_FACTOR = 2;
45
+ const LONG_RUN_THRESHOLD_MS = 30_000;
46
+
47
+ /** Supervises channel adapters: lifecycle, status tracking, and crash restart with backoff. */
48
+ export class ChannelManager {
49
+ private readonly config: GatewayConfig;
50
+ private readonly log: GatewayLogger;
51
+ private readonly emit: (env: GatewayInboundEnvelope) => Promise<void>;
52
+ private readonly initialBackoff: number;
53
+ private readonly maxBackoff: number;
54
+ private readonly factor: number;
55
+ private readonly entries: Map<string, ChannelEntry> = new Map();
56
+
57
+ constructor(opts: ChannelManagerOptions) {
58
+ this.config = opts.config;
59
+ this.log = opts.log;
60
+ this.emit = opts.emit;
61
+ this.initialBackoff = opts.backoffMs?.initial ?? DEFAULT_INITIAL_BACKOFF;
62
+ this.maxBackoff = opts.backoffMs?.max ?? DEFAULT_MAX_BACKOFF;
63
+ this.factor = opts.backoffMs?.factor ?? DEFAULT_FACTOR;
64
+
65
+ for (const adapter of opts.channels) {
66
+ this.registerAdapter(adapter);
67
+ }
68
+ }
69
+
70
+ private registerAdapter(adapter: ChannelAdapter): ChannelEntry {
71
+ const accountId = this.findAccountId(adapter.id);
72
+ const entry: ChannelEntry = {
73
+ adapter,
74
+ accountId,
75
+ state: "idle",
76
+ snapshot: {
77
+ channel: adapter.id,
78
+ accountId,
79
+ running: false,
80
+ reconnectAttempts: 0,
81
+ restartPending: false,
82
+ lastError: null,
83
+ },
84
+ controller: null,
85
+ runPromise: null,
86
+ restartTimer: null,
87
+ nextBackoff: this.initialBackoff,
88
+ currentStartAt: 0,
89
+ reconnectAttempts: 0,
90
+ stopRequested: false,
91
+ };
92
+ this.entries.set(adapter.id, entry);
93
+ return entry;
94
+ }
95
+
96
+ /** Start every configured channel; already-running channels are skipped. */
97
+ async startAll(): Promise<void> {
98
+ for (const entry of this.entries.values()) {
99
+ if (entry.state === "idle" || entry.state === "crashed") {
100
+ this.launch(entry);
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Launch a single adapter that wasn't present at construction — used by
107
+ * `Gateway.addChannel()` to hot-plug a new agent without a full restart.
108
+ * Idempotent: if an entry with the same id is already running, this is a
109
+ * no-op (after logging a warning).
110
+ */
111
+ addOne(adapter: ChannelAdapter): void {
112
+ const existing = this.entries.get(adapter.id);
113
+ if (existing) {
114
+ this.log.warn("channel.addOne: id already present", { channel: adapter.id });
115
+ return;
116
+ }
117
+ const entry = this.registerAdapter(adapter);
118
+ this.launch(entry);
119
+ }
120
+
121
+ /**
122
+ * Stop and forget a single channel. Cancels any pending restart timer,
123
+ * aborts the running turn, awaits the adapter's `stop()`, and removes
124
+ * the entry from the status map. Safe to call on an unknown id.
125
+ */
126
+ async removeOne(id: string, reason?: string): Promise<void> {
127
+ const entry = this.entries.get(id);
128
+ if (!entry) return;
129
+ entry.stopRequested = true;
130
+ if (entry.restartTimer) {
131
+ clearTimeout(entry.restartTimer);
132
+ entry.restartTimer = null;
133
+ entry.snapshot = { ...entry.snapshot, restartPending: false };
134
+ }
135
+ const pending: Promise<void>[] = [];
136
+ if (entry.state === "running" || entry.state === "starting") {
137
+ entry.state = "stopping";
138
+ entry.controller?.abort();
139
+ const adapter = entry.adapter;
140
+ if (adapter.stop) {
141
+ try {
142
+ const p = adapter.stop({ reason });
143
+ pending.push(Promise.resolve(p).catch((err) => {
144
+ this.log.warn("channel.stop failed", {
145
+ channel: adapter.id,
146
+ error: err instanceof Error ? err.message : String(err),
147
+ });
148
+ }));
149
+ } catch (err) {
150
+ this.log.warn("channel.stop threw", {
151
+ channel: adapter.id,
152
+ error: err instanceof Error ? err.message : String(err),
153
+ });
154
+ }
155
+ }
156
+ }
157
+ if (entry.runPromise) {
158
+ pending.push(entry.runPromise.catch(() => undefined));
159
+ }
160
+ await Promise.all(pending);
161
+ this.entries.delete(id);
162
+ }
163
+
164
+ /** Abort every channel, cancel pending restarts, and await all run promises. */
165
+ async stopAll(reason?: string): Promise<void> {
166
+ const pending: Promise<void>[] = [];
167
+ for (const entry of this.entries.values()) {
168
+ entry.stopRequested = true;
169
+ if (entry.restartTimer) {
170
+ clearTimeout(entry.restartTimer);
171
+ entry.restartTimer = null;
172
+ entry.snapshot = { ...entry.snapshot, restartPending: false };
173
+ }
174
+ if (entry.state === "running" || entry.state === "starting") {
175
+ entry.state = "stopping";
176
+ entry.controller?.abort();
177
+ const adapter = entry.adapter;
178
+ if (adapter.stop) {
179
+ try {
180
+ const p = adapter.stop({ reason });
181
+ pending.push(Promise.resolve(p).catch((err) => {
182
+ this.log.warn("channel.stop failed", {
183
+ channel: adapter.id,
184
+ error: err instanceof Error ? err.message : String(err),
185
+ });
186
+ }));
187
+ } catch (err) {
188
+ this.log.warn("channel.stop threw", {
189
+ channel: adapter.id,
190
+ error: err instanceof Error ? err.message : String(err),
191
+ });
192
+ }
193
+ }
194
+ }
195
+ if (entry.runPromise) {
196
+ pending.push(entry.runPromise.catch(() => undefined));
197
+ }
198
+ }
199
+ await Promise.all(pending);
200
+ // Reset stop flag so startAll can re-enter.
201
+ for (const entry of this.entries.values()) {
202
+ entry.stopRequested = false;
203
+ }
204
+ }
205
+
206
+ /** Return a shallow copy of per-channel status snapshots keyed by channel id. */
207
+ status(): Record<string, ChannelStatusSnapshot> {
208
+ const out: Record<string, ChannelStatusSnapshot> = {};
209
+ for (const [id, entry] of this.entries) {
210
+ out[id] = { ...entry.snapshot };
211
+ }
212
+ return out;
213
+ }
214
+
215
+ /** Look up a registered channel adapter by id. */
216
+ getChannel(id: string): ChannelAdapter | undefined {
217
+ return this.entries.get(id)?.adapter;
218
+ }
219
+
220
+ private findAccountId(channelId: string): string {
221
+ const cfg = this.config.channels.find((c) => c.id === channelId);
222
+ return cfg?.accountId ?? "";
223
+ }
224
+
225
+ private launch(entry: ChannelEntry): void {
226
+ if (entry.state === "starting" || entry.state === "running") return;
227
+
228
+ entry.stopRequested = false;
229
+ entry.state = "starting";
230
+ entry.controller = new AbortController();
231
+ entry.currentStartAt = Date.now();
232
+ entry.snapshot = {
233
+ ...entry.snapshot,
234
+ running: true,
235
+ restartPending: false,
236
+ lastStartAt: entry.currentStartAt,
237
+ lastError: null,
238
+ reconnectAttempts: entry.reconnectAttempts,
239
+ };
240
+
241
+ const ctx: ChannelStartContext = {
242
+ config: this.config,
243
+ accountId: entry.accountId,
244
+ abortSignal: entry.controller.signal,
245
+ log: this.log,
246
+ emit: (env) => this.safeEmit(entry.adapter.id, env),
247
+ setStatus: (patch) => {
248
+ entry.snapshot = { ...entry.snapshot, ...patch };
249
+ },
250
+ };
251
+
252
+ this.log.info("channel starting", { channel: entry.adapter.id });
253
+ const run = (async () => {
254
+ try {
255
+ await entry.adapter.start(ctx);
256
+ this.onExit(entry, null);
257
+ } catch (err) {
258
+ this.onExit(entry, err);
259
+ }
260
+ })();
261
+ entry.runPromise = run;
262
+ entry.state = "running";
263
+ }
264
+
265
+ private onExit(entry: ChannelEntry, err: unknown): void {
266
+ const ranForMs = Date.now() - entry.currentStartAt;
267
+ const channelId = entry.adapter.id;
268
+ const crashed = err !== null && err !== undefined;
269
+
270
+ entry.snapshot = {
271
+ ...entry.snapshot,
272
+ running: false,
273
+ lastStopAt: Date.now(),
274
+ lastError: crashed
275
+ ? err instanceof Error
276
+ ? err.message
277
+ : String(err)
278
+ : entry.snapshot.lastError ?? null,
279
+ };
280
+
281
+ if (crashed) {
282
+ this.log.warn("channel crashed", {
283
+ channel: channelId,
284
+ error: err instanceof Error ? err.message : String(err),
285
+ });
286
+ entry.state = "crashed";
287
+ } else {
288
+ this.log.info("channel exited", { channel: channelId });
289
+ entry.state = "idle";
290
+ }
291
+
292
+ if (entry.stopRequested) {
293
+ entry.runPromise = null;
294
+ return;
295
+ }
296
+
297
+ // Long-run resets backoff to initial.
298
+ if (ranForMs >= LONG_RUN_THRESHOLD_MS) {
299
+ entry.nextBackoff = this.initialBackoff;
300
+ }
301
+
302
+ this.scheduleRestart(entry);
303
+ }
304
+
305
+ private scheduleRestart(entry: ChannelEntry): void {
306
+ const delay = entry.nextBackoff;
307
+ entry.snapshot = { ...entry.snapshot, restartPending: true };
308
+ this.log.info("channel restart scheduled", {
309
+ channel: entry.adapter.id,
310
+ delayMs: delay,
311
+ attempts: entry.reconnectAttempts,
312
+ });
313
+
314
+ const timer = setTimeout(() => {
315
+ entry.restartTimer = null;
316
+ entry.runPromise = null;
317
+ if (entry.stopRequested) return;
318
+ entry.reconnectAttempts += 1;
319
+ entry.snapshot = {
320
+ ...entry.snapshot,
321
+ restartPending: false,
322
+ reconnectAttempts: entry.reconnectAttempts,
323
+ };
324
+ entry.nextBackoff = Math.min(entry.nextBackoff * this.factor, this.maxBackoff);
325
+ this.launch(entry);
326
+ }, delay);
327
+ entry.restartTimer = timer;
328
+ }
329
+
330
+ private async safeEmit(channelId: string, env: GatewayInboundEnvelope): Promise<void> {
331
+ const msg = env?.message;
332
+ if (!msg || typeof msg.id !== "string" || !msg.id || typeof msg.channel !== "string" || !msg.channel) {
333
+ this.log.warn("dropping malformed inbound envelope", {
334
+ channel: channelId,
335
+ hasMessage: Boolean(msg),
336
+ messageId: msg && typeof msg === "object" ? (msg as { id?: unknown }).id : undefined,
337
+ });
338
+ return;
339
+ }
340
+ try {
341
+ await this.emit(env);
342
+ } catch (err) {
343
+ this.log.error("emit failed", {
344
+ channel: channelId,
345
+ error: err instanceof Error ? err.message : String(err),
346
+ });
347
+ }
348
+ }
349
+ }