@controlflow-ai/daemon 0.1.2 → 0.1.4

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 (62) hide show
  1. package/README.md +54 -6
  2. package/bin/daemon.js +6 -1
  3. package/package.json +3 -1
  4. package/src/agent-avatar.ts +30 -0
  5. package/src/agent-key.ts +28 -0
  6. package/src/agent-permissions.ts +359 -0
  7. package/src/agent-runtime.ts +795 -28
  8. package/src/agent-workspace.ts +183 -0
  9. package/src/app.ts +1970 -79
  10. package/src/args.ts +54 -7
  11. package/src/cli.ts +873 -14
  12. package/src/client.ts +472 -10
  13. package/src/coco.ts +9 -40
  14. package/src/codex.ts +33 -5
  15. package/src/config.ts +28 -4
  16. package/src/console.ts +230 -20
  17. package/src/daemon-client.ts +116 -3
  18. package/src/daemon.ts +937 -99
  19. package/src/db.ts +3128 -122
  20. package/src/delivery-ws.ts +269 -0
  21. package/src/format.ts +4 -1
  22. package/src/lark/cli.ts +3 -3
  23. package/src/lark/event-router.ts +60 -4
  24. package/src/lark/inbound-events.ts +156 -3
  25. package/src/lark/server-integration.ts +659 -111
  26. package/src/lark/ws-daemon.ts +136 -10
  27. package/src/local-api.ts +545 -15
  28. package/src/local-auth.ts +33 -1
  29. package/src/message-attachments.ts +71 -0
  30. package/src/messaging-cli.ts +741 -0
  31. package/src/messaging-status.ts +669 -0
  32. package/src/migrations/024_agents_model.ts +10 -0
  33. package/src/migrations/025_room_archive.ts +44 -0
  34. package/src/migrations/026_project_archive.ts +44 -0
  35. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  36. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  37. package/src/migrations/029_held_message_drafts.ts +32 -0
  38. package/src/migrations/030_agent_room_read_state.ts +25 -0
  39. package/src/migrations/031_room_tasks.ts +29 -0
  40. package/src/migrations/032_room_reminders.ts +29 -0
  41. package/src/migrations/033_room_saved_messages.ts +25 -0
  42. package/src/migrations/034_agent_activity_events.ts +27 -0
  43. package/src/migrations/035_agent_avatars.ts +17 -0
  44. package/src/migrations/036_project_agent_defaults.ts +21 -0
  45. package/src/migrations/037_message_attachments.ts +36 -0
  46. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  47. package/src/migrations/039_message_attachments_path.ts +34 -0
  48. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  49. package/src/migrations/041_room_system_events.ts +30 -0
  50. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  51. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  52. package/src/migrations/044_workflow_runtime.ts +69 -0
  53. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  54. package/src/migrations.ts +69 -1
  55. package/src/neeko.ts +40 -4
  56. package/src/runtime-env.ts +179 -0
  57. package/src/runtime-registry.ts +83 -13
  58. package/src/server.ts +244 -4
  59. package/src/token-file.ts +13 -6
  60. package/src/types.ts +362 -0
  61. package/src/workflow-runtime.ts +275 -0
  62. package/src/web.ts +0 -904
@@ -1,8 +1,48 @@
1
1
  import { MessageStore } from '../db.js';
2
- import { boundAgents, loadLarkCredentials, type LarkCredential } from './credentials.js';
3
- import { extractMentionOpenIds, ingestLarkMessage, type LarkMessageEnvelope } from './event-router.js';
2
+ import { homeDir } from '../config.js';
3
+ import { boundAgents, defaultLarkConfigPath, loadLarkCredentials, type LarkCredential } from './credentials.js';
4
+ import { extractLarkImageResources, extractMentionOpenIds, ingestLarkMessage, type LarkImageResource, type LarkMessageEnvelope } from './event-router.js';
5
+ import { countInboundParseFailures, countProbeInboundEvents, countProviderInboundEvents, latestInboundEventSummary, latestProbeInboundEventSummary, storeInboundEvent, type InboundRawEventSummary } from './inbound-events.js';
4
6
  import type { ReceivePolicy } from './dispatcher.js';
5
- import { addMessageReaction, createLarkApiClient, getChatInfo, getChatMembers, startLarkDaemon, type LarkDaemonHandle } from './ws-daemon.js';
7
+ import { addMessageReaction, buildLarkEventRawBody, createLarkApiClient, getChatInfo, getChatMembers, startLarkDaemon, type LarkDaemonHandle, type LarkEventCallback, type LarkWebSocketLifecycleCallback } from './ws-daemon.js';
8
+ import type { MessageDelivery } from '../types.js';
9
+ import { larkBotNeedsRestart } from '../messaging-status.js';
10
+
11
+ async function readableToUint8Array(stream: NodeJS.ReadableStream): Promise<Uint8Array> {
12
+ const chunks: Buffer[] = [];
13
+ return new Promise((resolve, reject) => {
14
+ stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
15
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
16
+ stream.on('error', reject);
17
+ });
18
+ }
19
+
20
+ async function larkDownloadToUint8Array(raw: unknown): Promise<Uint8Array> {
21
+ if (raw instanceof Uint8Array) return raw;
22
+ if (Buffer.isBuffer(raw)) return raw;
23
+ if (raw && typeof raw === 'object') {
24
+ const record = raw as { getReadableStream?: () => NodeJS.ReadableStream; data?: unknown };
25
+ if (typeof record.getReadableStream === 'function') return readableToUint8Array(record.getReadableStream());
26
+ if (record.data instanceof Uint8Array) return record.data;
27
+ if (Buffer.isBuffer(record.data)) return record.data;
28
+ }
29
+ throw new Error('unexpected Lark image download response');
30
+ }
31
+
32
+ async function downloadLarkImage(input: { appId: string; appSecret: string; resource: LarkImageResource }): Promise<Uint8Array> {
33
+ const client = createLarkApiClient(input.appId, input.appSecret) as unknown as {
34
+ im: { v1?: { image?: { get(input: { path: { image_key: string } }): Promise<unknown> } } };
35
+ };
36
+ if (!client.im.v1?.image?.get) throw new Error('Lark SDK image download API is unavailable');
37
+ return larkDownloadToUint8Array(await client.im.v1.image.get({ path: { image_key: input.resource.fileKey } }));
38
+ }
39
+
40
+ export interface LarkDeliveryNotifyResult {
41
+ deliveries: number;
42
+ target_connections: number;
43
+ open_sockets: number;
44
+ websocket_frames: number;
45
+ }
6
46
 
7
47
  export interface LarkServerIntegrationOptions {
8
48
  /** Shared MessageStore for DB operations (caller owns lifecycle). */
@@ -11,16 +51,94 @@ export interface LarkServerIntegrationOptions {
11
51
  botOpenIdByApp?: Map<string, string>;
12
52
  logger?: Partial<Pick<Console, 'log' | 'warn' | 'error'>>;
13
53
  configPath?: string;
54
+ dbPath?: string;
14
55
  startDaemon?: typeof startLarkDaemon;
56
+ deliveryNotifier?: {
57
+ notifyDeliveries(deliveries: MessageDelivery[]): LarkDeliveryNotifyResult;
58
+ };
59
+ syncRoomMetadata?: boolean;
60
+ sendDeliveryReaction?: boolean;
15
61
  }
16
62
 
17
63
  export type LarkReloadResult =
18
64
  | { ok: true; bots: string[]; started: string[]; stopped: string[]; restarted: string[] }
19
65
  | { ok: false; bots: string[]; error: string };
20
66
 
67
+ export type LarkRestartResult =
68
+ | { ok: true; bots: string[]; restarted: string[]; missing: string[]; skipped_recent?: string[]; skipped_ineffective?: string[] }
69
+ | { ok: false; bots: string[]; restarted: string[]; missing: string[]; skipped_recent?: string[]; skipped_ineffective?: string[]; error: string };
70
+
71
+ export interface LarkRestartStaleOptions {
72
+ staleAfterMs: number;
73
+ now?: number;
74
+ minRestartIntervalMs?: number;
75
+ }
76
+
77
+ export interface LarkProbeEventInput {
78
+ appId: string;
79
+ envelope?: 'im.message.receive_v1' | 'card.action.trigger' | string;
80
+ data: unknown;
81
+ }
82
+
83
+ export interface LarkProbeEventResult {
84
+ app_id: string;
85
+ envelope: string;
86
+ raw_event_id: string;
87
+ inserted: boolean;
88
+ duplicate: boolean;
89
+ parse_ok: 0 | 1;
90
+ handled: boolean;
91
+ }
92
+
93
+ export interface LarkBotRuntimeStatus {
94
+ app_id: string;
95
+ agent: string | null;
96
+ label: string | null;
97
+ bot_open_id_known: boolean;
98
+ authorized_user_count: number;
99
+ runtime_home: string;
100
+ config_path: string;
101
+ db_path: string | null;
102
+ ws_state: string | null;
103
+ ws_last_connect_at: string | null;
104
+ ws_next_connect_at: string | null;
105
+ ws_reconnect_attempts: number | null;
106
+ ws_last_event_at: string | null;
107
+ ws_last_event_type: string | null;
108
+ ws_last_error: string | null;
109
+ restart_count: number;
110
+ last_restart_at: string | null;
111
+ last_restart_reason: string | null;
112
+ started_at: string;
113
+ event_count: number;
114
+ message_event_count: number;
115
+ provider_event_count: number;
116
+ provider_message_event_count: number;
117
+ last_event_at: string | null;
118
+ last_event_type: string | null;
119
+ last_provider_event_at: string | null;
120
+ last_provider_message_event_at: string | null;
121
+ last_message_event_at: string | null;
122
+ last_raw_event_id: string | null;
123
+ last_ingest_status: string | null;
124
+ last_delivery_count: number | null;
125
+ last_delivery_notify: LarkDeliveryNotifyResult | null;
126
+ db_event_count: number;
127
+ db_provider_event_count: number;
128
+ db_probe_event_count: number;
129
+ db_parse_error_count: number;
130
+ db_last_event: InboundRawEventSummary | null;
131
+ db_last_probe_event: InboundRawEventSummary | null;
132
+ last_error: string | null;
133
+ }
134
+
21
135
  export interface LarkServerIntegrationResult {
22
136
  handles: LarkDaemonHandle[];
137
+ status(): LarkBotRuntimeStatus[];
23
138
  reload(): LarkReloadResult;
139
+ restart(appIds?: string[]): LarkRestartResult;
140
+ restartStale(options: LarkRestartStaleOptions): LarkRestartResult;
141
+ injectProbeEvent(input: LarkProbeEventInput): Promise<LarkProbeEventResult>;
24
142
  stop(): void;
25
143
  }
26
144
 
@@ -51,6 +169,13 @@ function mentionsBotLabel(envelope: LarkMessageEnvelope, bot: LarkCredential): b
51
169
  return (envelope.message?.mentions ?? []).some((mention) => mention.name?.trim().toLowerCase() === expected);
52
170
  }
53
171
 
172
+ function mentionOpenIdForBotLabel(envelope: LarkMessageEnvelope, bot: LarkCredential): string | null {
173
+ if (!bot.label) return null;
174
+ const expected = bot.label.trim().toLowerCase();
175
+ if (!expected) return null;
176
+ const mention = (envelope.message?.mentions ?? []).find((entry) => entry.name?.trim().toLowerCase() === expected);
177
+ return mention?.id?.open_id?.trim() || null;
178
+ }
54
179
 
55
180
  function senderUnionId(envelope: LarkMessageEnvelope): string | null {
56
181
  return envelope.sender?.sender_id?.union_id?.trim() || null;
@@ -64,6 +189,29 @@ function deliveryReactionEmojiType(): string {
64
189
  return process.env.PAL_LARK_ACTION_REACTION_EMOJI?.trim() || 'Typing';
65
190
  }
66
191
 
192
+ function addSingleAgentBotMentionMapping(input: {
193
+ mappings: Map<string, string>;
194
+ bot: LarkCredential;
195
+ botOpenIdByApp: Map<string, string>;
196
+ fallbackBotOpenId?: string | null;
197
+ envelope?: LarkMessageEnvelope;
198
+ }): void {
199
+ const agents = boundAgents(input.bot);
200
+ if (agents.length !== 1) return;
201
+ const openId = input.botOpenIdByApp.get(input.bot.appId)
202
+ ?? input.bot.botOpenId
203
+ ?? (input.envelope ? mentionOpenIdForBotLabel(input.envelope, input.bot) : null)
204
+ ?? input.fallbackBotOpenId
205
+ ?? null;
206
+ if (openId) input.mappings.set(openId, agents[0]!);
207
+ }
208
+
209
+ function hasDeliveryForAgents(deliveries: Array<{ agent: string }>, agents: string[]): boolean {
210
+ if (agents.length === 0) return false;
211
+ const agentSet = new Set(agents);
212
+ return deliveries.some((delivery) => agentSet.has(delivery.agent));
213
+ }
214
+
67
215
  function apiErrorMessage(err: unknown): string {
68
216
  if (err instanceof Error) {
69
217
  const responseData = (err as Error & { response?: { data?: unknown } }).response?.data;
@@ -78,6 +226,78 @@ function apiErrorMessage(err: unknown): string {
78
226
  return String(err);
79
227
  }
80
228
 
229
+ function timestampFromMs(value: number | undefined): string | null {
230
+ return value ? new Date(value).toISOString() : null;
231
+ }
232
+
233
+ function parseInboundReceivedAt(value: string | null | undefined): number | null {
234
+ if (!value) return null;
235
+ const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)
236
+ ? `${value.replace(' ', 'T')}Z`
237
+ : value;
238
+ const time = Date.parse(normalized);
239
+ return Number.isFinite(time) ? time : null;
240
+ }
241
+
242
+ function hasProviderEventAfterRestart(status: { db_last_event?: InboundRawEventSummary | null; last_restart_at?: string | null }): boolean {
243
+ const lastRestartAt = parseInboundReceivedAt(status.last_restart_at);
244
+ const lastProviderEventAt = parseInboundReceivedAt(status.db_last_event?.received_at);
245
+ return lastRestartAt !== null && lastProviderEventAt !== null && lastProviderEventAt > lastRestartAt;
246
+ }
247
+
248
+ function isLocalProbeEvent(storeResult: Pick<LarkProbeEventResult, 'raw_event_id'> | { event_id?: string; event_type?: string }): boolean {
249
+ const eventId = 'raw_event_id' in storeResult ? storeResult.raw_event_id : storeResult.event_id;
250
+ const eventType = 'event_type' in storeResult ? storeResult.event_type : undefined;
251
+ return Boolean(eventId?.startsWith('pal.probe:') || eventType?.startsWith('pal.probe.'));
252
+ }
253
+
254
+ function isLocalProbeChatId(chatId: string | null | undefined): boolean {
255
+ return Boolean(chatId?.includes('pal_probe') || chatId?.startsWith('oc_probe'));
256
+ }
257
+
258
+ function larkWsConnectionStatus(handle: LarkDaemonHandle): Pick<LarkBotRuntimeStatus, 'ws_state' | 'ws_last_connect_at' | 'ws_next_connect_at' | 'ws_reconnect_attempts'> {
259
+ try {
260
+ const status = (handle.wsClient as unknown as {
261
+ getConnectionStatus?: () => {
262
+ state?: string;
263
+ lastConnectTime?: number;
264
+ nextConnectTime?: number;
265
+ reconnectAttempts?: number;
266
+ };
267
+ }).getConnectionStatus?.();
268
+ return {
269
+ ws_state: status?.state ?? null,
270
+ ws_last_connect_at: timestampFromMs(status?.lastConnectTime),
271
+ ws_next_connect_at: timestampFromMs(status?.nextConnectTime),
272
+ ws_reconnect_attempts: typeof status?.reconnectAttempts === 'number' ? status.reconnectAttempts : null,
273
+ };
274
+ } catch {
275
+ return {
276
+ ws_state: null,
277
+ ws_last_connect_at: null,
278
+ ws_next_connect_at: null,
279
+ ws_reconnect_attempts: null,
280
+ };
281
+ }
282
+ }
283
+
284
+ function notifyLarkDeliveries(input: {
285
+ notifier: LarkServerIntegrationOptions['deliveryNotifier'];
286
+ deliveries: MessageDelivery[];
287
+ appId: string;
288
+ log: Pick<Console, 'log' | 'warn'>;
289
+ }): LarkDeliveryNotifyResult | null {
290
+ if (!input.notifier) return null;
291
+ try {
292
+ const notify = input.notifier.notifyDeliveries(input.deliveries);
293
+ input.log.log(`[lark/${input.appId}] notify deliveries=${notify.deliveries} target_connections=${notify.target_connections} open_sockets=${notify.open_sockets} websocket_frames=${notify.websocket_frames}`);
294
+ return notify;
295
+ } catch (err) {
296
+ input.log.warn(`[lark/${input.appId}] notify failed: ${err instanceof Error ? err.message : String(err)}`);
297
+ return null;
298
+ }
299
+ }
300
+
81
301
  async function syncLarkRoomMetadata(input: {
82
302
  bot: LarkCredential;
83
303
  chatId: string;
@@ -155,6 +375,7 @@ async function syncExistingLarkGroupRooms(input: {
155
375
  seen.add(key);
156
376
  const bot = botsByApp.get(mapping.app_id);
157
377
  if (!bot) continue;
378
+ if (isLocalProbeChatId(mapping.external_chat_id)) continue;
158
379
  await syncLarkRoomMetadata({
159
380
  bot,
160
381
  chatId: mapping.external_chat_id,
@@ -214,7 +435,12 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
214
435
  const msgStore = options.store;
215
436
  const botOpenIdByApp = options.botOpenIdByApp ?? new Map<string, string>();
216
437
  const startDaemon = options.startDaemon ?? startLarkDaemon;
217
- const active = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string }>();
438
+ const runtimeHome = homeDir();
439
+ const configPath = options.configPath ?? defaultLarkConfigPath();
440
+ const dbPath = options.dbPath ?? null;
441
+ const syncRoomMetadataEnabled = options.syncRoomMetadata ?? true;
442
+ const sendDeliveryReactionEnabled = options.sendDeliveryReaction ?? true;
443
+ const active = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback }>();
218
444
 
219
445
  const activeBots = (): LarkCredential[] => Array.from(active.values()).map((entry) => entry.bot);
220
446
  const activeHandles = (): LarkDaemonHandle[] => Array.from(active.values()).map((entry) => entry.handle);
@@ -247,135 +473,280 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
247
473
  });
248
474
  };
249
475
 
250
- const startBot = (bot: LarkCredential): LarkDaemonHandle => startDaemon({
251
- appId: bot.appId,
252
- appSecret: bot.appSecret,
253
- db: msgStore.db,
254
- logger: { info: log.log, warn: log.warn, error: log.error },
255
- onEvent: async ({ envelope, data, storeResult }) => {
256
- log.log(`[lark/${bot.appId}] ${envelope} stored id=${storeResult.id} event_id=${storeResult.event_id} parse_ok=${storeResult.parse_ok}`);
257
- if (envelope === 'im.message.receive_v1') {
258
- try {
259
- const larkEnvelope = data as LarkMessageEnvelope;
260
- const identity = await resolveSenderUnionId({ bot, envelope: larkEnvelope, store: msgStore });
261
- if (identity.status === 'pending') {
262
- msgStore.recordPendingInboundEvent({ rawEventId: storeResult.event_id, provider: 'lark', reason: 'missing_lark_sender_user_id', error: identity.reason });
263
- log.warn(`[lark/${bot.appId}] sender union_id lookup pending: ${identity.reason}`);
264
- return;
265
- }
266
- if (identity.status !== 'resolved' || !msgStore.isLarkAuthorizedUser(identity.unionId)) {
267
- log.log(`[lark/${bot.appId}] business ingest skipped sender_union=${identity.status === 'resolved' ? identity.unionId : '-'} authorized=false`);
268
- return;
269
- }
270
- const agents = boundAgents(bot);
271
- const mentionOpenIds = extractMentionOpenIds(larkEnvelope);
272
- const inferredBotOpenId = agents.length === 1 && mentionOpenIds.length === 1 ? mentionOpenIds[0] : null;
273
- const botOpenId = botOpenIdByApp.get(bot.appId) ?? bot.botOpenId ?? inferredBotOpenId;
274
- const recipientByMentionOpenId = new Map<string, string>();
275
- if (botOpenId && agents.length === 1) recipientByMentionOpenId.set(botOpenId, agents[0]!);
276
- const labelMentionedThisBot = mentionsBotLabel(larkEnvelope, bot);
277
- const labelMentionedAnyKnownBot = activeBots().some((candidate) => mentionsBotLabel(larkEnvelope, candidate));
278
- if (labelMentionedAnyKnownBot && !labelMentionedThisBot) {
279
- log.log(`[lark/${bot.appId}] label mention targets another bot, skipping ingest`);
280
- return;
476
+ const startBot = (bot: LarkCredential): { handle: LarkDaemonHandle; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback } => {
477
+ const restartState = msgStore.getLarkWebsocketRestartState(bot.appId);
478
+ const status: LarkBotRuntimeStatus = {
479
+ app_id: bot.appId,
480
+ agent: bot.agent?.trim() || null,
481
+ label: bot.label?.trim() || null,
482
+ bot_open_id_known: Boolean(bot.botOpenId?.trim() || botOpenIdByApp.get(bot.appId)),
483
+ authorized_user_count: 0,
484
+ runtime_home: runtimeHome,
485
+ config_path: configPath,
486
+ db_path: dbPath,
487
+ ws_state: null,
488
+ ws_last_connect_at: null,
489
+ ws_next_connect_at: null,
490
+ ws_reconnect_attempts: null,
491
+ ws_last_event_at: null,
492
+ ws_last_event_type: null,
493
+ ws_last_error: null,
494
+ restart_count: restartState?.restart_count ?? 0,
495
+ last_restart_at: restartState?.last_restart_at ?? null,
496
+ last_restart_reason: restartState?.last_restart_reason ?? null,
497
+ started_at: new Date().toISOString(),
498
+ event_count: 0,
499
+ message_event_count: 0,
500
+ provider_event_count: 0,
501
+ provider_message_event_count: 0,
502
+ last_event_at: null,
503
+ last_event_type: null,
504
+ last_provider_event_at: null,
505
+ last_provider_message_event_at: null,
506
+ last_message_event_at: null,
507
+ last_raw_event_id: null,
508
+ last_ingest_status: null,
509
+ last_delivery_count: null,
510
+ last_delivery_notify: null,
511
+ db_event_count: 0,
512
+ db_provider_event_count: 0,
513
+ db_probe_event_count: 0,
514
+ db_parse_error_count: 0,
515
+ db_last_event: null,
516
+ db_last_probe_event: null,
517
+ last_error: null,
518
+ };
519
+
520
+ const onLifecycle: LarkWebSocketLifecycleCallback = (event) => {
521
+ status.ws_last_event_at = new Date().toISOString();
522
+ status.ws_last_event_type = event.type;
523
+ status.ws_last_error = event.error ?? null;
524
+ };
525
+
526
+ const onEvent: LarkEventCallback = async ({ envelope, data, storeResult }) => {
527
+ const isProbe = isLocalProbeEvent(storeResult);
528
+ status.event_count += 1;
529
+ status.last_event_at = new Date().toISOString();
530
+ status.last_event_type = envelope;
531
+ status.last_raw_event_id = storeResult.event_id;
532
+ status.last_error = null;
533
+ if (!isProbe) {
534
+ status.provider_event_count += 1;
535
+ status.last_provider_event_at = status.last_event_at;
536
+ }
537
+ log.log(`[lark/${bot.appId}] ${envelope} stored id=${storeResult.id} event_id=${storeResult.event_id} parse_ok=${storeResult.parse_ok}`);
538
+ if (envelope === 'im.message.receive_v1') {
539
+ status.message_event_count += 1;
540
+ status.last_message_event_at = status.last_event_at;
541
+ if (!isProbe) {
542
+ status.provider_message_event_count += 1;
543
+ status.last_provider_message_event_at = status.last_event_at;
281
544
  }
282
- const recipientOverride = larkEnvelope.message?.chat_type === 'p2p' && agents.length === 1
283
- ? agents[0]!
284
- : labelMentionedThisBot && agents.length === 1 ? agents[0]! : undefined;
285
-
286
- const result = ingestLarkMessage({
287
- appId: bot.appId,
288
- envelope: larkEnvelope,
289
- store: msgStore,
290
- recipientByMentionOpenId,
291
- recipientOverride,
292
- });
293
- if (result.status === 'ok' && result.message) {
294
- const tag = result.deduped ? 'dup' : result.threadOrphan ? 'orphan' : 'new';
295
- log.log(
296
- `[lark/${bot.appId}] ingest ${tag} lock_msg=${result.message.id} chat=${result.message.chat_name} parent=${result.message.parent_id ?? '-'}`,
297
- );
298
- const larkMessage = (data as LarkMessageEnvelope).message;
299
- if (larkMessage?.chat_id) {
300
- try {
301
- const account = msgStore.registerChannelAccount({
302
- name: bot.label ?? bot.appId,
303
- appId: bot.appId,
304
- botOpenId,
305
- agent: agents.length === 1 ? agents[0]! : null,
306
- });
307
- msgStore.resolveChannelConversation({
308
- accountId: account.id,
309
- chatName: result.message.chat_name,
310
- conversationKey: `${result.message.chat_name}:app:${bot.appId}`,
311
- externalChatId: larkMessage.chat_id,
312
- externalRootId: larkMessage.root_id ?? null,
313
- externalThreadId: larkMessage.thread_id ?? null,
314
- scope: larkMessage.root_id || larkMessage.thread_id ? 'thread' : larkMessage.chat_type === 'p2p' ? 'p2p' : 'chat',
315
- chatType: larkMessage.chat_type === 'p2p' ? 'p2p' : 'group',
316
- auditOnly: false,
317
- });
318
- await syncLarkRoomMetadata({
319
- bot,
320
- chatId: larkMessage.chat_id,
321
- roomId: result.message.chat_id,
322
- store: msgStore,
323
- botOpenId,
324
- agent: agents.length === 1 ? agents[0]! : null,
325
- log,
326
- });
327
- } catch (err) {
328
- log.warn(`[lark/${bot.appId}] channel mapping failed: ${err instanceof Error ? err.message : String(err)}`);
329
- }
545
+ status.last_ingest_status = null;
546
+ status.last_delivery_count = null;
547
+ status.last_delivery_notify = null;
548
+ try {
549
+ const larkEnvelope = data as LarkMessageEnvelope;
550
+ const identity = await resolveSenderUnionId({ bot, envelope: larkEnvelope, store: msgStore });
551
+ if (identity.status === 'pending') {
552
+ msgStore.recordPendingInboundEvent({ rawEventId: storeResult.event_id, provider: 'lark', reason: 'missing_lark_sender_user_id', error: identity.reason });
553
+ status.last_ingest_status = 'pending_sender_identity';
554
+ status.last_delivery_count = 0;
555
+ status.last_delivery_notify = null;
556
+ log.warn(`[lark/${bot.appId}] sender union_id lookup pending: ${identity.reason}`);
557
+ return;
330
558
  }
331
- if (tag === 'new' || result.deduped) {
332
- const deliveries = msgStore.resolveDeliveriesForMessage(result.message.id);
333
- if (deliveries.length === 0) {
334
- log.log(`[lark/${bot.appId}] resolver created no deliveries message=${result.message.id}`);
335
- } else {
336
- for (const delivery of deliveries) {
337
- log.log(`[lark/${bot.appId}] resolver delivery id=${delivery.id} agent=${delivery.agent} message=${result.message.id}`);
338
- }
339
- if (tag === 'new' && larkMessage?.message_id) {
340
- const emojiType = deliveryReactionEmojiType();
559
+ if (identity.status !== 'resolved' || !msgStore.isLarkAuthorizedUser(identity.unionId)) {
560
+ status.last_ingest_status = 'skipped_unauthorized_sender';
561
+ status.last_delivery_count = 0;
562
+ status.last_delivery_notify = null;
563
+ log.log(`[lark/${bot.appId}] business ingest skipped sender_union=${identity.status === 'resolved' ? identity.unionId : '-'} authorized=false`);
564
+ return;
565
+ }
566
+ const agents = boundAgents(bot);
567
+ const mentionOpenIds = extractMentionOpenIds(larkEnvelope);
568
+ const inferredBotOpenId = agents.length === 1 && mentionOpenIds.length === 1 ? mentionOpenIds[0] : null;
569
+ const botOpenId = botOpenIdByApp.get(bot.appId) ?? bot.botOpenId ?? inferredBotOpenId;
570
+ const recipientByMentionOpenId = new Map<string, string>();
571
+ for (const candidate of activeBots()) {
572
+ addSingleAgentBotMentionMapping({
573
+ mappings: recipientByMentionOpenId,
574
+ bot: candidate,
575
+ botOpenIdByApp,
576
+ fallbackBotOpenId: candidate.appId === bot.appId ? inferredBotOpenId : null,
577
+ envelope: larkEnvelope,
578
+ });
579
+ }
580
+ const labelMentionedThisBot = mentionsBotLabel(larkEnvelope, bot);
581
+ const labelMentionedAnyKnownBot = activeBots().some((candidate) => mentionsBotLabel(larkEnvelope, candidate));
582
+ if (labelMentionedAnyKnownBot && !labelMentionedThisBot) {
583
+ status.last_ingest_status = 'skipped_other_bot_label';
584
+ status.last_delivery_count = 0;
585
+ status.last_delivery_notify = null;
586
+ log.log(`[lark/${bot.appId}] label mention targets another bot, skipping ingest`);
587
+ return;
588
+ }
589
+ const hasReplyContext = Boolean(larkEnvelope.message?.root_id || larkEnvelope.message?.parent_id);
590
+ const recipientOverride = agents.length === 1 && (
591
+ larkEnvelope.message?.chat_type === 'p2p' ||
592
+ labelMentionedThisBot ||
593
+ hasReplyContext
594
+ )
595
+ ? agents[0]!
596
+ : undefined;
597
+
598
+ const result = ingestLarkMessage({
599
+ appId: bot.appId,
600
+ envelope: larkEnvelope,
601
+ store: msgStore,
602
+ recipientByMentionOpenId,
603
+ recipientOverride,
604
+ });
605
+ if (result.status === 'ok' && result.message) {
606
+ const tag = result.deduped ? 'dup' : result.threadOrphan ? 'orphan' : 'new';
607
+ status.last_ingest_status = `ok_${tag}`;
608
+ log.log(
609
+ `[lark/${bot.appId}] ingest ${tag} lock_msg=${result.message.id} chat=${result.message.chat_name} parent=${result.message.parent_id ?? '-'}`,
610
+ );
611
+ const larkMessage = (data as LarkMessageEnvelope).message;
612
+ if (!result.deduped) {
613
+ for (const resource of extractLarkImageResources(larkEnvelope)) {
341
614
  try {
342
- const client = createLarkApiClient(bot.appId, bot.appSecret);
343
- const reaction = await addMessageReaction({
344
- client,
345
- messageId: larkMessage.message_id,
346
- emojiType,
615
+ const content = await downloadLarkImage({ appId: bot.appId, appSecret: bot.appSecret, resource });
616
+ const attachment = msgStore.addMessageAttachment(result.message.id, {
617
+ kind: 'image',
618
+ mimeType: resource.mimeType,
619
+ filename: resource.filename,
620
+ content,
621
+ sourceProvider: 'lark',
622
+ sourceRef: resource.fileKey,
347
623
  });
348
- log.log(`[lark/${bot.appId}] added reaction emoji=${emojiType} message=${larkMessage.message_id} reaction=${reaction.reactionId ?? '-'}`);
624
+ log.log(`[lark/${bot.appId}] stored image attachment=${attachment.id} message=${result.message.id} bytes=${attachment.size_bytes}`);
349
625
  } catch (err) {
350
- log.warn(`[lark/${bot.appId}] add reaction failed message=${larkMessage.message_id}: ${apiErrorMessage(err)}`);
626
+ log.warn(`[lark/${bot.appId}] image download failed message=${result.message.id}: ${apiErrorMessage(err)}`);
351
627
  }
352
628
  }
353
629
  }
630
+ if (larkMessage?.chat_id && !isProbe) {
631
+ try {
632
+ const account = msgStore.registerChannelAccount({
633
+ name: bot.label ?? bot.appId,
634
+ appId: bot.appId,
635
+ botOpenId,
636
+ agent: agents.length === 1 ? agents[0]! : null,
637
+ });
638
+ msgStore.resolveChannelConversation({
639
+ accountId: account.id,
640
+ chatName: result.message.chat_name,
641
+ conversationKey: `${result.message.chat_name}:app:${bot.appId}`,
642
+ externalChatId: larkMessage.chat_id,
643
+ externalRootId: larkMessage.root_id ?? null,
644
+ externalThreadId: larkMessage.thread_id ?? null,
645
+ scope: larkMessage.root_id || larkMessage.thread_id ? 'thread' : larkMessage.chat_type === 'p2p' ? 'p2p' : 'chat',
646
+ chatType: larkMessage.chat_type === 'p2p' ? 'p2p' : 'group',
647
+ auditOnly: false,
648
+ });
649
+ if (syncRoomMetadataEnabled) {
650
+ await syncLarkRoomMetadata({
651
+ bot,
652
+ chatId: larkMessage.chat_id,
653
+ roomId: result.message.chat_id,
654
+ store: msgStore,
655
+ botOpenId,
656
+ agent: agents.length === 1 ? agents[0]! : null,
657
+ log,
658
+ });
659
+ }
660
+ } catch (err) {
661
+ log.warn(`[lark/${bot.appId}] channel mapping failed: ${err instanceof Error ? err.message : String(err)}`);
662
+ }
663
+ }
664
+ if (tag === 'new' || result.deduped) {
665
+ const deliveries = msgStore.resolveDeliveriesForMessage(result.message.id);
666
+ status.last_delivery_count = deliveries.length;
667
+ if (deliveries.length === 0) {
668
+ status.last_delivery_notify = null;
669
+ log.log(`[lark/${bot.appId}] resolver created no deliveries message=${result.message.id}`);
670
+ } else {
671
+ for (const delivery of deliveries) {
672
+ log.log(`[lark/${bot.appId}] resolver delivery id=${delivery.id} agent=${delivery.agent} message=${result.message.id}`);
673
+ }
674
+ if (result.deduped) {
675
+ status.last_delivery_notify = null;
676
+ log.log(`[lark/${bot.appId}] duplicate event did not re-notify deliveries message=${result.message.id}`);
677
+ } else {
678
+ status.last_delivery_notify = notifyLarkDeliveries({
679
+ notifier: options.deliveryNotifier,
680
+ deliveries,
681
+ appId: bot.appId,
682
+ log,
683
+ });
684
+ }
685
+ if (!isProbe && sendDeliveryReactionEnabled && hasDeliveryForAgents(deliveries, agents) && larkMessage?.message_id) {
686
+ const emojiType = deliveryReactionEmojiType();
687
+ try {
688
+ const client = createLarkApiClient(bot.appId, bot.appSecret);
689
+ const reaction = await addMessageReaction({
690
+ client,
691
+ messageId: larkMessage.message_id,
692
+ emojiType,
693
+ });
694
+ log.log(`[lark/${bot.appId}] added reaction emoji=${emojiType} message=${larkMessage.message_id} reaction=${reaction.reactionId ?? '-'}`);
695
+ } catch (err) {
696
+ log.warn(`[lark/${bot.appId}] add reaction failed message=${larkMessage.message_id}: ${apiErrorMessage(err)}`);
697
+ }
698
+ }
699
+ }
700
+ }
701
+ } else if (result.status === 'skipped') {
702
+ status.last_ingest_status = `skipped_${result.reason}`;
703
+ status.last_delivery_count = 0;
704
+ status.last_delivery_notify = null;
705
+ log.log(`[lark/${bot.appId}] ingest skipped reason=${result.reason}`);
354
706
  }
355
- } else if (result.status === 'skipped') {
356
- log.log(`[lark/${bot.appId}] ingest skipped reason=${result.reason}`);
707
+ } catch (err) {
708
+ status.last_ingest_status = 'error';
709
+ status.last_delivery_count = 0;
710
+ status.last_delivery_notify = null;
711
+ status.last_error = err instanceof Error ? err.message : String(err);
712
+ log.error(`[lark/${bot.appId}] ingest error:`, err);
357
713
  }
358
- } catch (err) {
359
- log.error(`[lark/${bot.appId}] ingest error:`, err);
360
714
  }
361
- }
362
- },
363
- });
715
+ };
716
+
717
+ const handle = startDaemon({
718
+ appId: bot.appId,
719
+ appSecret: bot.appSecret,
720
+ db: msgStore.db,
721
+ logger: { info: log.log, warn: log.warn, error: log.error },
722
+ onEvent,
723
+ onLifecycle,
724
+ });
725
+ return { handle, status, onEvent, onLifecycle };
726
+ };
364
727
 
365
728
  const applyBots = (bots: LarkCredential[]): LarkReloadResult => {
366
729
  const nextByApp = new Map(bots.map((bot) => [bot.appId, bot]));
367
730
  const started: string[] = [];
368
731
  const stopped: string[] = [];
369
732
  const restarted: string[] = [];
370
- const pending = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; replacing: boolean }>();
733
+ const pending = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback; replacing: boolean }>();
371
734
 
372
735
  try {
373
736
  for (const bot of bots) {
374
737
  const fingerprint = credentialFingerprint(bot);
375
738
  const current = active.get(bot.appId);
376
739
  if (current?.fingerprint === fingerprint) continue;
377
- const handle = startBot(bot);
378
- pending.set(bot.appId, { bot, handle, fingerprint, replacing: Boolean(current) });
740
+ const startedBot = startBot(bot);
741
+ pending.set(bot.appId, {
742
+ bot,
743
+ handle: startedBot.handle,
744
+ fingerprint,
745
+ status: startedBot.status,
746
+ onEvent: startedBot.onEvent,
747
+ onLifecycle: startedBot.onLifecycle,
748
+ replacing: Boolean(current),
749
+ });
379
750
  }
380
751
  } catch (err) {
381
752
  for (const entry of pending.values()) entry.handle.stop();
@@ -392,7 +763,14 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
392
763
  }
393
764
 
394
765
  for (const [appId, entry] of pending.entries()) {
395
- active.set(appId, entry);
766
+ active.set(appId, {
767
+ bot: entry.bot,
768
+ handle: entry.handle,
769
+ fingerprint: entry.fingerprint,
770
+ status: entry.status,
771
+ onEvent: entry.onEvent,
772
+ onLifecycle: entry.onLifecycle,
773
+ });
396
774
  if (entry.replacing) restarted.push(appId);
397
775
  else started.push(appId);
398
776
  }
@@ -418,6 +796,123 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
418
796
  return applyBots(bots);
419
797
  };
420
798
 
799
+ const restart = (appIds?: string[]): LarkRestartResult => restartWithReason(appIds, 'manual');
800
+
801
+ const restartWithReason = (appIds: string[] | undefined, reason: string): LarkRestartResult => {
802
+ const requested = appIds && appIds.length > 0 ? Array.from(new Set(appIds.map((id) => id.trim()).filter(Boolean))) : activeAppIds();
803
+ const missing = requested.filter((appId) => !active.has(appId));
804
+ const restarted: string[] = [];
805
+ for (const appId of requested) {
806
+ const current = active.get(appId);
807
+ if (!current) continue;
808
+ let next: { handle: LarkDaemonHandle; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback };
809
+ try {
810
+ next = startBot(current.bot);
811
+ } catch (err) {
812
+ return {
813
+ ok: false,
814
+ bots: activeAppIds(),
815
+ restarted,
816
+ missing,
817
+ error: err instanceof Error ? err.message : String(err),
818
+ };
819
+ }
820
+ const restartedAt = new Date().toISOString();
821
+ let restartState: ReturnType<MessageStore['recordLarkWebsocketRestart']>;
822
+ try {
823
+ restartState = msgStore.recordLarkWebsocketRestart({ appId, restartedAt, reason });
824
+ } catch (err) {
825
+ next.handle.stop();
826
+ return {
827
+ ok: false,
828
+ bots: activeAppIds(),
829
+ restarted,
830
+ missing,
831
+ error: err instanceof Error ? err.message : String(err),
832
+ };
833
+ }
834
+ next.status.restart_count = restartState.restart_count;
835
+ next.status.last_restart_at = restartState.last_restart_at;
836
+ next.status.last_restart_reason = restartState.last_restart_reason;
837
+ current.handle.stop();
838
+ active.set(appId, {
839
+ bot: current.bot,
840
+ handle: next.handle,
841
+ fingerprint: current.fingerprint,
842
+ status: next.status,
843
+ onEvent: next.onEvent,
844
+ onLifecycle: next.onLifecycle,
845
+ });
846
+ restarted.push(appId);
847
+ }
848
+ if (restarted.length > 0) {
849
+ afterSuccessfulScan(restarted.map((appId) => active.get(appId)?.bot).filter((bot): bot is LarkCredential => Boolean(bot)));
850
+ }
851
+ log.log(`[lark] restarted ${restarted.length} bot websocket(s)${missing.length > 0 ? ` missing=${missing.join(',')}` : ''}`);
852
+ return { ok: true, bots: activeAppIds(), restarted, missing };
853
+ };
854
+
855
+ const restartStale = (restartOptions: LarkRestartStaleOptions): LarkRestartResult => {
856
+ const now = restartOptions.now ?? Date.now();
857
+ const staleAfterMs = restartOptions.staleAfterMs;
858
+ const minRestartIntervalMs = restartOptions.minRestartIntervalMs ?? 0;
859
+ if (!Number.isFinite(staleAfterMs) || staleAfterMs <= 0) {
860
+ return { ok: false, bots: activeAppIds(), restarted: [], missing: [], error: 'staleAfterMs must be positive' };
861
+ }
862
+ if (!Number.isFinite(minRestartIntervalMs) || minRestartIntervalMs < 0) {
863
+ return { ok: false, bots: activeAppIds(), restarted: [], missing: [], error: 'minRestartIntervalMs must be zero or positive' };
864
+ }
865
+ const restartAppIds: string[] = [];
866
+ const skippedRecent: string[] = [];
867
+ const skippedIneffective: string[] = [];
868
+ for (const entry of active.values()) {
869
+ const dbProviderEventCount = countProviderInboundEvents(msgStore.db, entry.bot.appId);
870
+ const status = {
871
+ ...entry.status,
872
+ db_event_count: dbProviderEventCount,
873
+ db_provider_event_count: dbProviderEventCount,
874
+ db_probe_event_count: countProbeInboundEvents(msgStore.db, entry.bot.appId),
875
+ db_parse_error_count: countInboundParseFailures(msgStore.db, entry.bot.appId),
876
+ db_last_event: latestInboundEventSummary(msgStore.db, entry.bot.appId),
877
+ db_last_probe_event: latestProbeInboundEventSummary(msgStore.db, entry.bot.appId),
878
+ ...larkWsConnectionStatus(entry.handle),
879
+ };
880
+ if (!larkBotNeedsRestart(status, { now, staleAfterMs })) continue;
881
+ const lastRestartAt = parseInboundReceivedAt(entry.status.last_restart_at);
882
+ if (lastRestartAt !== null && minRestartIntervalMs > 0 && now - lastRestartAt < minRestartIntervalMs) {
883
+ skippedRecent.push(entry.bot.appId);
884
+ continue;
885
+ }
886
+ if (
887
+ entry.status.last_restart_reason === 'auto_stale'
888
+ && (entry.status.restart_count ?? 0) > 0
889
+ && lastRestartAt !== null
890
+ && !hasProviderEventAfterRestart(status)
891
+ ) {
892
+ skippedIneffective.push(entry.bot.appId);
893
+ continue;
894
+ }
895
+ restartAppIds.push(entry.bot.appId);
896
+ }
897
+ if (restartAppIds.length === 0) {
898
+ return {
899
+ ok: true,
900
+ bots: activeAppIds(),
901
+ restarted: [],
902
+ missing: [],
903
+ ...(skippedRecent.length > 0 ? { skipped_recent: skippedRecent } : {}),
904
+ ...(skippedIneffective.length > 0 ? { skipped_ineffective: skippedIneffective } : {}),
905
+ };
906
+ }
907
+ log.warn(`[lark] unhealthy/stale inbound state for ${restartAppIds.join(',')}; restarting websocket(s)`);
908
+ const result = restartWithReason(restartAppIds, 'auto_stale');
909
+ return {
910
+ ...result,
911
+ ...(skippedRecent.length > 0 ? { skipped_recent: skippedRecent } : {}),
912
+ ...(skippedIneffective.length > 0 ? { skipped_ineffective: skippedIneffective } : {}),
913
+ };
914
+ };
915
+
421
916
  const initial = reload();
422
917
  if (!initial.ok) {
423
918
  log.warn(`[lark] initial load failed: ${initial.error}`);
@@ -427,7 +922,60 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
427
922
  get handles() {
428
923
  return activeHandles();
429
924
  },
925
+ status() {
926
+ return Array.from(active.values()).map((entry) => {
927
+ const dbProviderEventCount = countProviderInboundEvents(msgStore.db, entry.bot.appId);
928
+ return {
929
+ ...entry.status,
930
+ db_event_count: dbProviderEventCount,
931
+ db_provider_event_count: dbProviderEventCount,
932
+ db_probe_event_count: countProbeInboundEvents(msgStore.db, entry.bot.appId),
933
+ db_parse_error_count: countInboundParseFailures(msgStore.db, entry.bot.appId),
934
+ db_last_event: latestInboundEventSummary(msgStore.db, entry.bot.appId),
935
+ db_last_probe_event: latestProbeInboundEventSummary(msgStore.db, entry.bot.appId),
936
+ agent: entry.bot.agent?.trim() || null,
937
+ label: entry.bot.label?.trim() || null,
938
+ bot_open_id_known: Boolean(entry.bot.botOpenId?.trim() || botOpenIdByApp.get(entry.bot.appId)),
939
+ authorized_user_count: msgStore.listLarkAuthorizedUsers().length,
940
+ ...larkWsConnectionStatus(entry.handle),
941
+ };
942
+ });
943
+ },
430
944
  reload,
945
+ restart,
946
+ restartStale,
947
+ async injectProbeEvent(input: LarkProbeEventInput): Promise<LarkProbeEventResult> {
948
+ const appId = input.appId.trim();
949
+ const entry = active.get(appId);
950
+ if (!entry) throw new Error(`Lark bot is not active: ${appId}`);
951
+ const envelope = input.envelope ?? 'im.message.receive_v1';
952
+ const raw = JSON.parse(buildLarkEventRawBody(envelope, input.data)) as {
953
+ header?: { event_id?: string; event_type?: string };
954
+ };
955
+ raw.header = raw.header ?? {};
956
+ raw.header.event_type = `pal.probe.${envelope}`;
957
+ raw.header.event_id = raw.header.event_id ? `pal.probe:${raw.header.event_id}` : `pal.probe:${envelope}:${crypto.randomUUID()}`;
958
+ const rawBody = JSON.stringify(raw);
959
+ const storeResult = await storeInboundEvent(msgStore.db, { appId, rawBody });
960
+ if (!storeResult.duplicate) {
961
+ await entry.onEvent({
962
+ appId,
963
+ envelope,
964
+ data: input.data,
965
+ rawBody,
966
+ storeResult,
967
+ });
968
+ }
969
+ return {
970
+ app_id: appId,
971
+ envelope,
972
+ raw_event_id: storeResult.event_id,
973
+ inserted: storeResult.inserted,
974
+ duplicate: storeResult.duplicate,
975
+ parse_ok: storeResult.parse_ok,
976
+ handled: !storeResult.duplicate,
977
+ };
978
+ },
431
979
  stop() {
432
980
  const handles = activeHandles();
433
981
  for (const h of handles) h.stop();