@dobby.ai/dobby 0.1.1 → 0.1.2

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 (136) hide show
  1. package/README.md +20 -7
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. package/tsconfig.json +0 -18
@@ -1,609 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import type { ImageContent } from "@mariozechner/pi-ai";
3
- import { EventForwarder } from "../agent/event-forwarder.js";
4
- import type { Executor } from "../sandbox/executor.js";
5
- import { parseControlCommand, type ControlCommand } from "./control-command.js";
6
- import type { DedupStore } from "./dedup-store.js";
7
- import { BindingResolver, RouteResolver } from "./routing.js";
8
- import { RuntimeRegistry } from "./runtime-registry.js";
9
- import { createTypingKeepAliveController } from "./typing-controller.js";
10
- import type {
11
- BindingResolution,
12
- ConnectorPlugin,
13
- GatewayAgentRuntime,
14
- GatewayConfig,
15
- GatewayLogger,
16
- InboundAttachment,
17
- InboundEnvelope,
18
- Platform,
19
- PromptPayload,
20
- ProviderInstance,
21
- RouteResolution,
22
- } from "./types.js";
23
-
24
- interface GatewayOptions {
25
- config: GatewayConfig;
26
- connectors: ConnectorPlugin[];
27
- providers: Map<string, ProviderInstance>;
28
- executors: Map<string, Executor>;
29
- routeResolver: RouteResolver;
30
- bindingResolver: BindingResolver;
31
- dedupStore: DedupStore;
32
- runtimeRegistry: RuntimeRegistry;
33
- logger: GatewayLogger;
34
- }
35
-
36
- interface MessageHandlingOptions {
37
- origin: "connector" | "scheduled";
38
- useDedup: boolean;
39
- stateless: boolean;
40
- includeReplyTo: boolean;
41
- conversationKeyOverride?: string;
42
- routeIdOverride?: string;
43
- sessionPolicy?: "shared-session" | "ephemeral";
44
- timeoutMs?: number;
45
- }
46
-
47
- interface StopControlEvent {
48
- type: "stop";
49
- connectorId: string;
50
- platform: Platform;
51
- accountId: string;
52
- chatId: string;
53
- threadId?: string;
54
- }
55
-
56
- interface ResolvedMessageRoute {
57
- route: RouteResolution;
58
- routeId: string;
59
- binding?: BindingResolution;
60
- }
61
-
62
- export interface ScheduledExecutionRequest {
63
- jobId: string;
64
- runId: string;
65
- connectorId: string;
66
- routeId: string;
67
- channelId: string;
68
- threadId?: string;
69
- prompt: string;
70
- timeoutMs?: number;
71
- }
72
-
73
- function isImageAttachment(attachment: InboundAttachment): boolean {
74
- return Boolean(attachment.mimeType?.startsWith("image/") && attachment.localPath);
75
- }
76
-
77
- function dedupKey(message: InboundEnvelope): string {
78
- return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.messageId}`;
79
- }
80
-
81
- function conversationKey(message: InboundEnvelope): string {
82
- return `${message.connectorId}:${message.platform}:${message.accountId}:${message.chatId}:${message.threadId ?? "root"}`;
83
- }
84
-
85
- export class Gateway {
86
- private readonly connectorsById = new Map<string, ConnectorPlugin>();
87
- private started = false;
88
-
89
- constructor(private readonly options: GatewayOptions) {
90
- for (const connector of options.connectors) {
91
- this.connectorsById.set(connector.id, connector);
92
- }
93
- }
94
-
95
- async start(): Promise<void> {
96
- if (this.started) return;
97
-
98
- await this.options.dedupStore.load();
99
- this.options.dedupStore.startAutoFlush();
100
-
101
- for (const connector of this.options.connectors) {
102
- await connector.start({
103
- emitInbound: async (message) => this.handleInbound(message),
104
- emitControl: async (event) => this.handleControl(event),
105
- });
106
- }
107
-
108
- this.started = true;
109
- }
110
-
111
- async stop(): Promise<void> {
112
- if (!this.started) return;
113
-
114
- for (const connector of this.options.connectors) {
115
- await connector.stop();
116
- }
117
-
118
- this.options.dedupStore.stopAutoFlush();
119
- await this.options.dedupStore.flush();
120
- await this.options.runtimeRegistry.closeAll();
121
-
122
- this.started = false;
123
- }
124
-
125
- async handleScheduled(request: ScheduledExecutionRequest): Promise<void> {
126
- const connector = this.connectorsById.get(request.connectorId);
127
- if (!connector) {
128
- throw new Error(`No connector found for scheduled run '${request.runId}' (${request.connectorId})`);
129
- }
130
-
131
- const syntheticInbound: InboundEnvelope = {
132
- connectorId: request.connectorId,
133
- platform: connector.platform,
134
- accountId: request.connectorId,
135
- source: {
136
- type: "chat",
137
- id: request.channelId,
138
- },
139
- chatId: request.channelId,
140
- ...(request.threadId ? { threadId: request.threadId } : {}),
141
- messageId: `cron:${request.runId}`,
142
- userId: "cron",
143
- userName: "cron",
144
- text: request.prompt,
145
- attachments: [],
146
- timestampMs: Date.now(),
147
- raw: {
148
- type: "cron",
149
- jobId: request.jobId,
150
- runId: request.runId,
151
- },
152
- isDirectMessage: false,
153
- mentionedBot: true,
154
- };
155
-
156
- await this.handleMessage(syntheticInbound, {
157
- origin: "scheduled",
158
- useDedup: true,
159
- stateless: true,
160
- includeReplyTo: false,
161
- conversationKeyOverride: `cron:${request.runId}`,
162
- routeIdOverride: request.routeId,
163
- sessionPolicy: "ephemeral",
164
- ...(request.timeoutMs !== undefined ? { timeoutMs: request.timeoutMs } : {}),
165
- });
166
- }
167
-
168
- private outboundBaseFromInbound(message: InboundEnvelope): {
169
- platform: Platform;
170
- accountId: string;
171
- chatId: string;
172
- threadId?: string;
173
- } {
174
- return {
175
- platform: message.platform,
176
- accountId: message.accountId,
177
- chatId: message.chatId,
178
- ...(message.threadId ? { threadId: message.threadId } : {}),
179
- };
180
- }
181
-
182
- private outboundBaseFromControl(event: StopControlEvent): {
183
- platform: Platform;
184
- accountId: string;
185
- chatId: string;
186
- threadId?: string;
187
- } {
188
- return {
189
- platform: event.platform,
190
- accountId: event.accountId,
191
- chatId: event.chatId,
192
- ...(event.threadId ? { threadId: event.threadId } : {}),
193
- };
194
- }
195
-
196
- private async sendCommandReply(
197
- connector: ConnectorPlugin,
198
- message: InboundEnvelope,
199
- text: string,
200
- ): Promise<void> {
201
- await connector.send({
202
- ...this.outboundBaseFromInbound(message),
203
- mode: "create",
204
- text,
205
- });
206
- }
207
-
208
- private async handleInbound(message: InboundEnvelope): Promise<void> {
209
- await this.handleMessage(message, {
210
- origin: "connector",
211
- useDedup: true,
212
- stateless: false,
213
- includeReplyTo: true,
214
- sessionPolicy: "shared-session",
215
- });
216
- }
217
-
218
- private resolveMessageRoute(message: InboundEnvelope, handling: MessageHandlingOptions): ResolvedMessageRoute | null {
219
- const overrideRouteId = handling.routeIdOverride?.trim();
220
- if (overrideRouteId) {
221
- const route = this.options.routeResolver.resolve(overrideRouteId);
222
- if (!route) {
223
- return null;
224
- }
225
-
226
- return {
227
- routeId: overrideRouteId,
228
- route,
229
- };
230
- }
231
-
232
- const binding = this.options.bindingResolver.resolve(message.connectorId, message.source, {
233
- isDirectMessage: message.isDirectMessage,
234
- });
235
- if (!binding) {
236
- if (handling.origin === "connector") {
237
- this.options.logger.debug(
238
- {
239
- connectorId: message.connectorId,
240
- sourceType: message.source.type,
241
- sourceId: message.source.id,
242
- },
243
- "Ignoring inbound message from unbound source",
244
- );
245
- }
246
- return null;
247
- }
248
-
249
- const route = this.options.routeResolver.resolve(binding.config.route);
250
- if (!route) {
251
- return null;
252
- }
253
-
254
- return {
255
- routeId: binding.config.route,
256
- route,
257
- binding,
258
- };
259
- }
260
-
261
- private async handleMessage(message: InboundEnvelope, handling: MessageHandlingOptions): Promise<void> {
262
- const connector = this.connectorsById.get(message.connectorId);
263
- if (!connector) {
264
- this.options.logger.warn({ connectorId: message.connectorId }, "No connector found for inbound message");
265
- return;
266
- }
267
-
268
- if (handling.useDedup) {
269
- const key = dedupKey(message);
270
- if (this.options.dedupStore.has(key)) {
271
- this.options.logger.debug({ dedupKey: key }, "Skipping duplicate message");
272
- return;
273
- }
274
- this.options.dedupStore.add(key);
275
- }
276
-
277
- const resolvedRoute = this.resolveMessageRoute(message, handling);
278
- if (!resolvedRoute) {
279
- if (handling.routeIdOverride) {
280
- await connector.send({
281
- ...this.outboundBaseFromInbound(message),
282
- mode: "create",
283
- ...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
284
- text: `No route configured for route '${handling.routeIdOverride}'.`,
285
- });
286
- }
287
- return;
288
- }
289
-
290
- if (handling.origin === "connector") {
291
- const command = parseControlCommand(message.text);
292
- if (command) {
293
- try {
294
- await this.handleCommand(connector, message, command, resolvedRoute.route);
295
- } catch (error) {
296
- this.options.logger.error({ err: error, messageId: message.messageId }, "Failed to handle control command");
297
- await this.sendCommandReply(
298
- connector,
299
- message,
300
- `Error: ${error instanceof Error ? error.message : String(error)}`,
301
- );
302
- }
303
- return;
304
- }
305
- }
306
-
307
- const { route } = resolvedRoute;
308
- if (route.profile.mentions === "required" && !message.isDirectMessage && !message.mentionedBot) {
309
- this.options.logger.debug(
310
- {
311
- sourceType: message.source.type,
312
- sourceId: message.source.id,
313
- routeId: route.routeId,
314
- },
315
- "Ignoring non-mention message",
316
- );
317
- return;
318
- }
319
-
320
- const providerId = route.profile.provider;
321
- const sandboxId = route.profile.sandbox;
322
- const provider = this.options.providers.get(providerId);
323
- const executor = this.options.executors.get(sandboxId);
324
-
325
- if (!provider || !executor) {
326
- await connector.send({
327
- ...this.outboundBaseFromInbound(message),
328
- mode: "create",
329
- ...(handling.includeReplyTo ? { replyToMessageId: message.messageId } : {}),
330
- text: `Route runtime not available (provider='${providerId}', sandbox='${sandboxId}')`,
331
- });
332
- return;
333
- }
334
-
335
- const convKey = handling.conversationKeyOverride ?? conversationKey(message);
336
-
337
- if (handling.stateless) {
338
- const runtime = await provider.createRuntime({
339
- conversationKey: convKey,
340
- route,
341
- inbound: message,
342
- executor,
343
- ...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
344
- });
345
- try {
346
- await this.processMessage(connector, runtime, route, message, {
347
- includeReplyTo: handling.includeReplyTo,
348
- ...(handling.timeoutMs !== undefined ? { timeoutMs: handling.timeoutMs } : {}),
349
- });
350
- } finally {
351
- runtime.dispose();
352
- }
353
- return;
354
- }
355
-
356
- await this.options.runtimeRegistry.run(
357
- convKey,
358
- async () => {
359
- const runtime = await provider.createRuntime({
360
- conversationKey: convKey,
361
- route,
362
- inbound: message,
363
- executor,
364
- ...(handling.sessionPolicy ? { sessionPolicy: handling.sessionPolicy } : {}),
365
- });
366
-
367
- return {
368
- key: convKey,
369
- routeId: route.routeId,
370
- route: route.profile,
371
- providerId,
372
- sandboxId,
373
- runtime,
374
- close: async () => {
375
- runtime.dispose();
376
- },
377
- };
378
- },
379
- async (runtime) => {
380
- await this.processMessage(connector, runtime.runtime, route, message, {
381
- includeReplyTo: handling.includeReplyTo,
382
- });
383
- },
384
- );
385
- }
386
-
387
- private async processMessage(
388
- connector: ConnectorPlugin,
389
- runtime: GatewayAgentRuntime,
390
- route: RouteResolution,
391
- message: InboundEnvelope,
392
- options: {
393
- includeReplyTo: boolean;
394
- timeoutMs?: number;
395
- },
396
- ): Promise<void> {
397
- this.options.logger.info(
398
- {
399
- connectorId: message.connectorId,
400
- routeId: route.routeId,
401
- sourceType: message.source.type,
402
- sourceId: message.source.id,
403
- chatId: message.chatId,
404
- threadId: message.threadId,
405
- messageId: message.messageId,
406
- },
407
- "Processing inbound message",
408
- );
409
-
410
- const typingController = createTypingKeepAliveController(connector, message, this.options.logger);
411
- let unsubscribe: (() => void) | null = null;
412
- const forwarder = new EventForwarder(connector, message, null, this.options.logger, {
413
- onOutboundActivity: typingController.markVisibleOutput,
414
- });
415
- try {
416
- await typingController.prime();
417
- unsubscribe = runtime.subscribe(forwarder.handleEvent);
418
-
419
- const payload = await this.buildPromptPayload(message);
420
- this.options.logger.info(
421
- {
422
- routeId: route.routeId,
423
- messageId: message.messageId,
424
- rootMessageId: forwarder.primaryMessageId() ?? null,
425
- hasImages: payload.images.length > 0,
426
- textLength: payload.text.length,
427
- },
428
- "Starting provider prompt",
429
- );
430
- await this.promptWithOptionalTimeout(runtime, payload, options.timeoutMs);
431
- this.options.logger.info(
432
- {
433
- routeId: route.routeId,
434
- messageId: message.messageId,
435
- rootMessageId: forwarder.primaryMessageId() ?? null,
436
- },
437
- "Provider prompt finished",
438
- );
439
- await forwarder.finalize();
440
- this.options.logger.info({ routeId: route.routeId, messageId: message.messageId }, "Inbound message processed");
441
- } catch (error) {
442
- this.options.logger.error({ err: error, routeId: route.routeId }, "Failed to process inbound message");
443
- const rootMessageId = forwarder.primaryMessageId();
444
- const canEditExisting = connector.capabilities.updateStrategy === "edit" && rootMessageId !== null;
445
- await connector.send(
446
- canEditExisting
447
- ? {
448
- ...this.outboundBaseFromInbound(message),
449
- mode: "update",
450
- targetMessageId: rootMessageId,
451
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
452
- }
453
- : {
454
- ...this.outboundBaseFromInbound(message),
455
- mode: "create",
456
- ...(
457
- rootMessageId
458
- ? { replyToMessageId: rootMessageId }
459
- : options.includeReplyTo
460
- ? { replyToMessageId: message.messageId }
461
- : {}
462
- ),
463
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
464
- },
465
- );
466
- } finally {
467
- unsubscribe?.();
468
- typingController.stop();
469
- }
470
- }
471
-
472
- private async promptWithOptionalTimeout(
473
- runtime: GatewayAgentRuntime,
474
- payload: PromptPayload,
475
- timeoutMs?: number,
476
- ): Promise<void> {
477
- if (timeoutMs === undefined || timeoutMs <= 0) {
478
- await runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined);
479
- return;
480
- }
481
-
482
- let timer: NodeJS.Timeout | null = null;
483
- try {
484
- await Promise.race([
485
- runtime.prompt(payload.text, payload.images.length > 0 ? { images: payload.images } : undefined),
486
- new Promise<never>((_, reject) => {
487
- timer = setTimeout(() => {
488
- void runtime.abort().catch(() => {
489
- // Best-effort abort on timeout.
490
- });
491
- reject(new Error(`Cron run timed out after ${timeoutMs}ms`));
492
- }, timeoutMs);
493
- }),
494
- ]);
495
- } finally {
496
- if (timer) {
497
- clearTimeout(timer);
498
- }
499
- }
500
- }
501
-
502
- private async buildPromptPayload(message: InboundEnvelope): Promise<PromptPayload> {
503
- const textParts: string[] = [];
504
- const baseText = message.text.trim();
505
- textParts.push(baseText.length > 0 ? baseText : "(empty message)");
506
-
507
- const images: ImageContent[] = [];
508
- const otherAttachments: string[] = [];
509
-
510
- for (const attachment of message.attachments) {
511
- if (isImageAttachment(attachment) && attachment.localPath && attachment.mimeType) {
512
- try {
513
- const buffer = await readFile(attachment.localPath);
514
- images.push({
515
- type: "image",
516
- mimeType: attachment.mimeType,
517
- data: buffer.toString("base64"),
518
- });
519
- continue;
520
- } catch (error) {
521
- this.options.logger.warn({ err: error, attachment: attachment.localPath }, "Failed to read image attachment");
522
- }
523
- }
524
-
525
- if (attachment.localPath) {
526
- otherAttachments.push(attachment.localPath);
527
- } else if (attachment.remoteUrl) {
528
- otherAttachments.push(attachment.remoteUrl);
529
- }
530
- }
531
-
532
- if (otherAttachments.length > 0) {
533
- textParts.push(`<attachments>\n${otherAttachments.join("\n")}\n</attachments>`);
534
- }
535
-
536
- return {
537
- text: textParts.join("\n\n"),
538
- images,
539
- };
540
- }
541
-
542
- private async handleCommand(
543
- connector: ConnectorPlugin,
544
- message: InboundEnvelope,
545
- command: ControlCommand,
546
- route: RouteResolution,
547
- ): Promise<void> {
548
- const convKey = conversationKey(message);
549
- if (command === "cancel") {
550
- const cancelled = await this.options.runtimeRegistry.cancel(convKey);
551
- this.options.logger.info({ conversationKey: convKey, cancelled }, "Conversation cancel requested");
552
- await this.sendCommandReply(
553
- connector,
554
- message,
555
- cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._",
556
- );
557
- return;
558
- }
559
-
560
- const providerId = route.profile.provider;
561
- const provider = this.options.providers.get(providerId);
562
- if (!provider) {
563
- throw new Error(`Route provider not available (provider='${providerId}')`);
564
- }
565
- if (!provider.archiveSession) {
566
- throw new Error(`Provider '${providerId}' does not support session archival`);
567
- }
568
-
569
- const hadRuntime = await this.options.runtimeRegistry.reset(convKey);
570
- const archiveResult = await provider.archiveSession({
571
- conversationKey: convKey,
572
- inbound: message,
573
- sessionPolicy: "shared-session",
574
- archivedAtMs: message.timestampMs,
575
- });
576
-
577
- this.options.logger.info(
578
- {
579
- conversationKey: convKey,
580
- providerId,
581
- hadRuntime,
582
- archived: archiveResult.archived,
583
- archivePath: archiveResult.archivePath ?? null,
584
- },
585
- "New session requested",
586
- );
587
-
588
- await this.sendCommandReply(connector, message, "_Started a new session._");
589
- }
590
-
591
- private async handleControl(event: StopControlEvent): Promise<void> {
592
- const convKey = `${event.connectorId}:${event.platform}:${event.accountId}:${event.chatId}:${event.threadId ?? "root"}`;
593
- const connector = this.connectorsById.get(event.connectorId);
594
- const cancelled = await this.options.runtimeRegistry.cancel(convKey);
595
-
596
- this.options.logger.info({ conversationKey: convKey, cancelled }, "Stop requested");
597
- if (!connector) return;
598
-
599
- try {
600
- await connector.send({
601
- ...this.outboundBaseFromControl(event),
602
- mode: "create",
603
- text: cancelled ? "_Cancelled current session tasks._" : "_No active or queued session tasks to cancel._",
604
- });
605
- } catch (error) {
606
- this.options.logger.warn({ err: error, conversationKey: convKey }, "Failed to send stop acknowledgement");
607
- }
608
- }
609
- }