botinabox 1.6.0 → 1.8.0

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/README.md CHANGED
@@ -5,6 +5,8 @@ A modular TypeScript framework for building multi-agent bots with LLM orchestrat
5
5
  ## Features
6
6
 
7
7
  - **Multi-agent orchestration** -- Define agents with different models, roles, and execution adapters. Task queue with priority scheduling, retry policies, and followup chains.
8
+ - **Chat response layer** -- Fast (<2s) conversational responses via cheap LLM. Rolling context window, LLM-filtered readability, redundancy suppression. Store-before-respond guarantee for all messages.
9
+ - **Message interpretation** -- Async structured extraction from messages into tasks, memories, files, and user context. Pluggable extractors for custom data types.
8
10
  - **Two-tier agents** -- Deterministic adapter for tasks that don't need LLM reasoning (routing, validation, data fetching). API and CLI adapters for LLM-driven tasks.
9
11
  - **Triage routing** -- Content-aware message routing with keyword/regex matching, priority rules, and LLM fallback for ambiguous messages. Ownership chain logging for every routing decision.
10
12
  - **Loop detection and circuit breakers** -- Pattern-based loop detection (self-loops, ping-pong, blocked re-entry) plus circuit breakers with automatic human escalation when agents fail repeatedly.
package/dist/index.d.ts CHANGED
@@ -1035,6 +1035,340 @@ declare class NotificationQueue {
1035
1035
  private processNext;
1036
1036
  }
1037
1037
 
1038
+ /**
1039
+ * MessageStore — store-before-respond guarantee for all chat interactions.
1040
+ * Story 7.1
1041
+ *
1042
+ * Every inbound message (with attachments) is stored BEFORE the bot responds.
1043
+ * Every outbound message is stored BEFORE it is sent to the user.
1044
+ * Storage confirmation is required before any response flows.
1045
+ */
1046
+
1047
+ interface StoredAttachment {
1048
+ fileType: string;
1049
+ filename?: string;
1050
+ mimeType?: string;
1051
+ sizeBytes?: number;
1052
+ contents?: string;
1053
+ summary?: string;
1054
+ url?: string;
1055
+ }
1056
+ interface StoreResult {
1057
+ messageId: string;
1058
+ attachmentIds: string[];
1059
+ }
1060
+ declare class MessageStore {
1061
+ private db;
1062
+ private hooks;
1063
+ constructor(db: DataStore, hooks: HookBus);
1064
+ /**
1065
+ * Store an inbound message and its attachments.
1066
+ * Must complete successfully before any bot response is generated.
1067
+ */
1068
+ storeInbound(msg: InboundMessage): Promise<StoreResult>;
1069
+ /**
1070
+ * Store an outbound message BEFORE sending it.
1071
+ * Returns the message ID for confirmation tracking.
1072
+ */
1073
+ storeOutbound(opts: {
1074
+ channel: string;
1075
+ text: string;
1076
+ threadId?: string;
1077
+ agentId?: string;
1078
+ agentSlug?: string;
1079
+ taskId?: string;
1080
+ }): Promise<string>;
1081
+ /**
1082
+ * Store an attachment linked to a message.
1083
+ */
1084
+ storeAttachment(messageId: string, att: StoredAttachment): Promise<string>;
1085
+ /**
1086
+ * Get recent messages in a thread for context building.
1087
+ */
1088
+ getThreadHistory(threadId: string, limit?: number): Promise<Array<Record<string, unknown>>>;
1089
+ /**
1090
+ * Get recent outbound messages in a thread for redundancy checking.
1091
+ */
1092
+ getRecentOutbound(threadId: string, limit?: number): Promise<Array<Record<string, unknown>>>;
1093
+ /**
1094
+ * Get attachments for a message.
1095
+ */
1096
+ getAttachments(messageId: string): Promise<Array<Record<string, unknown>>>;
1097
+ }
1098
+
1099
+ /**
1100
+ * ChatResponder — fast conversational layer with LLM-filtered responses.
1101
+ * Story 7.2
1102
+ *
1103
+ * Provides rapid (<2s) conversational responses using a cheap LLM (Haiku).
1104
+ * The responder has awareness of tools and capabilities but does NOT execute
1105
+ * anything — it keeps the conversation going while work happens async.
1106
+ *
1107
+ * All outbound messages (direct, post-interpretation, task execution) are
1108
+ * filtered through this layer for human readability and redundancy checking.
1109
+ */
1110
+
1111
+ interface ChatResponderConfig {
1112
+ /** System prompt for the conversational responder */
1113
+ systemPrompt?: string;
1114
+ /** Max tokens for context window. Default: 4000 */
1115
+ contextWindowTokens?: number;
1116
+ /** Max recent outbound messages to check for redundancy. Default: 10 */
1117
+ redundancyWindow?: number;
1118
+ /** Model to use for responses. Default: 'fast' (resolved via ModelRouter) */
1119
+ model?: string;
1120
+ /** Caller-provided LLM call function */
1121
+ llmCall: (params: {
1122
+ model: string;
1123
+ messages: ChatMessage[];
1124
+ system?: string;
1125
+ maxTokens?: number;
1126
+ }) => Promise<{
1127
+ content: string;
1128
+ }>;
1129
+ }
1130
+ declare class ChatResponder {
1131
+ private db;
1132
+ private hooks;
1133
+ private messageStore;
1134
+ private readonly systemPrompt;
1135
+ private readonly contextWindowTokens;
1136
+ private readonly redundancyWindow;
1137
+ private readonly model;
1138
+ private readonly llmCall;
1139
+ constructor(db: DataStore, hooks: HookBus, messageStore: MessageStore, config: ChatResponderConfig);
1140
+ /**
1141
+ * Generate a fast conversational response to an inbound message.
1142
+ * Uses rolling context window from thread history.
1143
+ */
1144
+ respond(opts: {
1145
+ messageBody: string;
1146
+ threadId: string;
1147
+ channel: string;
1148
+ userName?: string;
1149
+ capabilities?: string;
1150
+ }): Promise<string>;
1151
+ /**
1152
+ * Filter any outbound message through the LLM for human readability.
1153
+ * This is the single funnel ALL responses pass through.
1154
+ */
1155
+ filterResponse(text: string, context?: {
1156
+ channel?: string;
1157
+ threadId?: string;
1158
+ source?: string;
1159
+ }): Promise<string>;
1160
+ /**
1161
+ * Check if a candidate outbound message is redundant with recent messages.
1162
+ * Returns true if the message should be suppressed.
1163
+ */
1164
+ isRedundant(text: string, threadId: string): Promise<boolean>;
1165
+ /**
1166
+ * Full send pipeline: check redundancy → filter → store → deliver.
1167
+ * Returns the message ID, or undefined if suppressed as redundant.
1168
+ */
1169
+ sendResponse(opts: {
1170
+ text: string;
1171
+ channel: string;
1172
+ threadId: string;
1173
+ agentId?: string;
1174
+ agentSlug?: string;
1175
+ taskId?: string;
1176
+ source?: string;
1177
+ skipRedundancyCheck?: boolean;
1178
+ skipFilter?: boolean;
1179
+ }): Promise<string | undefined>;
1180
+ /**
1181
+ * Build a context window from thread history, trimmed to token limit.
1182
+ */
1183
+ private buildContextWindow;
1184
+ }
1185
+
1186
+ /**
1187
+ * MessageInterpreter — async structured extraction from messages.
1188
+ * Story 7.3
1189
+ *
1190
+ * After every message is stored, the interpreter runs async to extract
1191
+ * structured data types: tasks, memories, files, user context, and custom types.
1192
+ *
1193
+ * Uses a cheap LLM (Haiku) for classification and extraction.
1194
+ * Pluggable extractors allow apps to add custom data types.
1195
+ */
1196
+
1197
+ interface ExtractedTask {
1198
+ title: string;
1199
+ description?: string;
1200
+ dueDate?: string;
1201
+ scheduled?: boolean;
1202
+ priority?: number;
1203
+ }
1204
+ interface ExtractedMemory {
1205
+ summary: string;
1206
+ contents: string;
1207
+ tags?: string[];
1208
+ category?: string;
1209
+ }
1210
+ interface ExtractedFile {
1211
+ filename: string;
1212
+ fileType: string;
1213
+ contents: string;
1214
+ summary: string;
1215
+ }
1216
+ interface ExtractedUserContext {
1217
+ trait: string;
1218
+ value: string;
1219
+ }
1220
+ interface InterpretationResult {
1221
+ messageId: string;
1222
+ tasks: ExtractedTask[];
1223
+ memories: ExtractedMemory[];
1224
+ files: ExtractedFile[];
1225
+ userContext: ExtractedUserContext[];
1226
+ custom: Record<string, unknown[]>;
1227
+ isTaskRequest: boolean;
1228
+ }
1229
+ type LLMCallFn = (params: {
1230
+ model: string;
1231
+ messages: ChatMessage[];
1232
+ system?: string;
1233
+ maxTokens?: number;
1234
+ }) => Promise<{
1235
+ content: string;
1236
+ }>;
1237
+ /**
1238
+ * Pluggable extractor interface for custom data types.
1239
+ */
1240
+ interface Extractor {
1241
+ readonly type: string;
1242
+ extract(message: {
1243
+ body: string;
1244
+ attachments?: Array<Record<string, unknown>>;
1245
+ }, llmCall: LLMCallFn): Promise<unknown[]>;
1246
+ }
1247
+ interface MessageInterpreterConfig {
1248
+ /** Additional custom extractors beyond built-in ones */
1249
+ extractors?: Extractor[];
1250
+ /** Model for interpretation LLM calls. Default: 'fast' */
1251
+ model?: string;
1252
+ /** LLM call function */
1253
+ llmCall: LLMCallFn;
1254
+ /** Auto-create tasks from extracted tasks. Default: false */
1255
+ autoCreateTasks?: boolean;
1256
+ }
1257
+ declare class MessageInterpreter {
1258
+ private db;
1259
+ private hooks;
1260
+ private readonly extractors;
1261
+ private readonly model;
1262
+ private readonly llmCall;
1263
+ private readonly autoCreateTasks;
1264
+ constructor(db: DataStore, hooks: HookBus, config: MessageInterpreterConfig);
1265
+ /**
1266
+ * Interpret a stored message asynchronously.
1267
+ * Extracts tasks, memories, files, user context, and custom types.
1268
+ */
1269
+ interpret(messageId: string): Promise<InterpretationResult>;
1270
+ /**
1271
+ * Extract structured data from message text using LLM.
1272
+ */
1273
+ private extractWithLLM;
1274
+ }
1275
+
1276
+ /**
1277
+ * ChatPipeline — configurable 6-layer chat orchestration.
1278
+ * Story 7.4
1279
+ *
1280
+ * Replaces duplicated handler code across apps with a single configurable
1281
+ * pipeline. Apps provide: system prompt, routing rules, LLM call function,
1282
+ * and optional message filter. Everything else is framework-level.
1283
+ *
1284
+ * Layers:
1285
+ * 1. Dedup + Storage (MessageStore)
1286
+ * 2. Fast Response (ChatResponder)
1287
+ * 3. Interpretation (MessageInterpreter)
1288
+ * 4. Post-Interpretation Response
1289
+ * 5. Task Dispatch (TriageRouter)
1290
+ * 6. Task Execution Response
1291
+ */
1292
+
1293
+ interface ChatPipelineConfig {
1294
+ /** LLM call function for chat responses and interpretation */
1295
+ llmCall: ChatResponderConfig['llmCall'];
1296
+ /** System prompt for the conversational responder */
1297
+ systemPrompt: string;
1298
+ /** Agent routing rules for task dispatch */
1299
+ routingRules: RoutingRule[];
1300
+ /** Default agent when no rule matches */
1301
+ fallbackAgent: string;
1302
+ /** Optional message filter — return false to ignore a message */
1303
+ messageFilter?: (msg: InboundMessage) => boolean;
1304
+ /** Optional capabilities description for the responder */
1305
+ capabilities?: string;
1306
+ /** Channel this pipeline handles (default: 'slack') */
1307
+ channel?: string;
1308
+ /** Custom extractors for MessageInterpreter */
1309
+ extractors?: Extractor[];
1310
+ /** Dedup window in ms (default: 300_000 = 5 min) */
1311
+ dedupWindowMs?: number;
1312
+ /** Model for fast responses (default: 'fast') */
1313
+ model?: string;
1314
+ /** Enable LLM fallback routing (default: false) */
1315
+ llmRouting?: boolean;
1316
+ /** TaskQueue instance — required for task dispatch */
1317
+ tasks: {
1318
+ create(task: Record<string, unknown>): Promise<string>;
1319
+ update(id: string, changes: Record<string, unknown>): Promise<void>;
1320
+ get(id: string): Promise<Record<string, unknown> | undefined>;
1321
+ };
1322
+ /** WakeupQueue instance — required for agent waking */
1323
+ wakeups: {
1324
+ enqueue(agentId: string, source: string, context?: Record<string, unknown>): Promise<string>;
1325
+ };
1326
+ }
1327
+ declare class ChatPipeline {
1328
+ private db;
1329
+ private hooks;
1330
+ readonly messageStore: MessageStore;
1331
+ readonly responder: ChatResponder;
1332
+ readonly interpreter: MessageInterpreter;
1333
+ readonly router: TriageRouter;
1334
+ private readonly channel;
1335
+ private readonly messageFilter?;
1336
+ private readonly capabilities?;
1337
+ private readonly dedupWindowMs;
1338
+ private readonly tasks;
1339
+ private readonly wakeups;
1340
+ private readonly threadChannelMap;
1341
+ constructor(db: DataStore, hooks: HookBus, config: ChatPipelineConfig);
1342
+ /**
1343
+ * Resolve the Slack channel ID for a thread (for response delivery).
1344
+ */
1345
+ resolveChannel(threadId: string, taskId?: string): Promise<string | undefined>;
1346
+ /**
1347
+ * Register the 6-layer pipeline on the HookBus.
1348
+ */
1349
+ private registerHandlers;
1350
+ /**
1351
+ * Check and record message dedup (SHA256 hash, configurable window).
1352
+ */
1353
+ private isDuplicate;
1354
+ /**
1355
+ * Async interpretation + task dispatch (Layers 3-5).
1356
+ */
1357
+ private interpretAndDispatch;
1358
+ /**
1359
+ * Route and dispatch extracted tasks.
1360
+ */
1361
+ private dispatchTasks;
1362
+ /**
1363
+ * Resolve Slack channel ID from thread_task_map or in-memory fallback.
1364
+ */
1365
+ private resolveChannelId;
1366
+ /**
1367
+ * Build human-readable interpretation summary.
1368
+ */
1369
+ private buildSummary;
1370
+ }
1371
+
1038
1372
  /**
1039
1373
  * Text chunker — splits long text into chunks at natural boundaries.
1040
1374
  * Story 4.4
@@ -2128,4 +2462,4 @@ declare function isLoginRequired(stdout: string): boolean;
2128
2462
  /** Rewrite local image paths to prevent CLI auto-embedding as vision content. */
2129
2463
  declare function deactivateLocalImagePaths(prompt: string): string;
2130
2464
 
2131
- export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, type ApprovalResponse, type ApprovalStatus, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, BreakerState, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatSessionManager, CircuitBreaker, type CircuitBreakerConfig, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, ConnectorConfig, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, DeterministicAdapter, type DeterministicConfig, type DomainEntityContextOptions, type DomainSchemaOptions, DriftGate, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type FeedbackEntry, type Filter, type GateFinding, type GateInput, type GateResult, GateRunner, type GateVerdict, GovernanceGate, HealthStatus, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, LLMProvider, LearningPipeline, type LearningPipelineConfig, type LoopDetection, LoopDetector, type LoopDetectorConfig, LoopType, MAX_CHAIN_DEPTH, MessagePipeline, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PermissionPrompt, type PermissionProvider, PermissionRelay, type PermissionRelayConfig, type PkLookup, type PlaybookEntry, ProviderRegistry, QAGate, QualityGate, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type RoutingDecision, type RoutingRule, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type Schedule, type ScheduleDef, Scheduler, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SkillEntry, type SqliteAdapter, type StepRef, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, TriageRouter, type TriageRouterConfig, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, autoUpdate, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, truncateAtWord, validateConfig };
2465
+ export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, type ApprovalResponse, type ApprovalStatus, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, BreakerState, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatPipeline, type ChatPipelineConfig, ChatResponder, type ChatResponderConfig, ChatSessionManager, CircuitBreaker, type CircuitBreakerConfig, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, ConnectorConfig, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, DeterministicAdapter, type DeterministicConfig, type DomainEntityContextOptions, type DomainSchemaOptions, DriftGate, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type ExtractedFile, type ExtractedMemory, type ExtractedTask, type ExtractedUserContext, type Extractor, type FeedbackEntry, type Filter, type GateFinding, type GateInput, type GateResult, GateRunner, type GateVerdict, GovernanceGate, HealthStatus, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, type InterpretationResult, type LLMCallFn, LLMProvider, LearningPipeline, type LearningPipelineConfig, type LoopDetection, LoopDetector, type LoopDetectorConfig, LoopType, MAX_CHAIN_DEPTH, MessageInterpreter, type MessageInterpreterConfig, MessagePipeline, MessageStore, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PermissionPrompt, type PermissionProvider, PermissionRelay, type PermissionRelayConfig, type PkLookup, type PlaybookEntry, ProviderRegistry, QAGate, QualityGate, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type RoutingDecision, type RoutingRule, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type Schedule, type ScheduleDef, Scheduler, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SkillEntry, type SqliteAdapter, type StepRef, type StoreResult, type StoredAttachment, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, TriageRouter, type TriageRouterConfig, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, autoUpdate, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, truncateAtWord, validateConfig };
package/dist/index.js CHANGED
@@ -1159,6 +1159,682 @@ var NotificationQueue = class {
1159
1159
  }
1160
1160
  };
1161
1161
 
1162
+ // src/core/chat/message-store.ts
1163
+ var MessageStore = class {
1164
+ constructor(db, hooks) {
1165
+ this.db = db;
1166
+ this.hooks = hooks;
1167
+ }
1168
+ db;
1169
+ hooks;
1170
+ /**
1171
+ * Store an inbound message and its attachments.
1172
+ * Must complete successfully before any bot response is generated.
1173
+ */
1174
+ async storeInbound(msg) {
1175
+ const row = await this.db.insert("messages", {
1176
+ channel: msg.channel,
1177
+ direction: "inbound",
1178
+ from_user: msg.from,
1179
+ user_id: msg.userId,
1180
+ body: msg.body,
1181
+ thread_id: msg.threadId
1182
+ });
1183
+ const messageId = row["id"];
1184
+ const attachmentIds = [];
1185
+ if (msg.attachments && msg.attachments.length > 0) {
1186
+ for (const att of msg.attachments) {
1187
+ const attId = await this.storeAttachment(messageId, {
1188
+ fileType: att.type,
1189
+ filename: att.filename,
1190
+ mimeType: att.mimeType,
1191
+ sizeBytes: att.size,
1192
+ url: att.url
1193
+ });
1194
+ attachmentIds.push(attId);
1195
+ }
1196
+ }
1197
+ await this.hooks.emit("message.stored", {
1198
+ messageId,
1199
+ direction: "inbound",
1200
+ channel: msg.channel,
1201
+ from: msg.from,
1202
+ attachmentCount: attachmentIds.length
1203
+ });
1204
+ return { messageId, attachmentIds };
1205
+ }
1206
+ /**
1207
+ * Store an outbound message BEFORE sending it.
1208
+ * Returns the message ID for confirmation tracking.
1209
+ */
1210
+ async storeOutbound(opts) {
1211
+ const row = await this.db.insert("messages", {
1212
+ channel: opts.channel,
1213
+ direction: "outbound",
1214
+ from_agent: opts.agentSlug,
1215
+ agent_id: opts.agentId,
1216
+ body: opts.text,
1217
+ thread_id: opts.threadId,
1218
+ task_id: opts.taskId
1219
+ });
1220
+ const messageId = row["id"];
1221
+ await this.hooks.emit("message.stored", {
1222
+ messageId,
1223
+ direction: "outbound",
1224
+ channel: opts.channel
1225
+ });
1226
+ return messageId;
1227
+ }
1228
+ /**
1229
+ * Store an attachment linked to a message.
1230
+ */
1231
+ async storeAttachment(messageId, att) {
1232
+ const row = await this.db.insert("message_attachments", {
1233
+ message_id: messageId,
1234
+ file_type: att.fileType,
1235
+ filename: att.filename,
1236
+ mime_type: att.mimeType,
1237
+ size_bytes: att.sizeBytes,
1238
+ contents: att.contents,
1239
+ summary: att.summary,
1240
+ url: att.url
1241
+ });
1242
+ return row["id"];
1243
+ }
1244
+ /**
1245
+ * Get recent messages in a thread for context building.
1246
+ */
1247
+ async getThreadHistory(threadId, limit = 20) {
1248
+ const messages = await this.db.query("messages", {
1249
+ where: { thread_id: threadId },
1250
+ orderBy: "created_at",
1251
+ limit
1252
+ });
1253
+ return messages;
1254
+ }
1255
+ /**
1256
+ * Get recent outbound messages in a thread for redundancy checking.
1257
+ */
1258
+ async getRecentOutbound(threadId, limit = 10) {
1259
+ const messages = await this.db.query("messages", {
1260
+ where: { thread_id: threadId, direction: "outbound" },
1261
+ orderBy: "created_at",
1262
+ limit
1263
+ });
1264
+ return messages;
1265
+ }
1266
+ /**
1267
+ * Get attachments for a message.
1268
+ */
1269
+ async getAttachments(messageId) {
1270
+ return this.db.query("message_attachments", {
1271
+ where: { message_id: messageId }
1272
+ });
1273
+ }
1274
+ };
1275
+
1276
+ // src/core/chat/chat-responder.ts
1277
+ var DEFAULT_SYSTEM_PROMPT = `You are a helpful, enthusiastic AI digital assistant. Your job is to:
1278
+ - Acknowledge the user's message and relay what you understand
1279
+ - Let them know what will happen next (if a task is being worked on)
1280
+ - Answer conversational questions directly
1281
+ - Be honest when you need to look something up \u2014 say "let me find out"
1282
+ - Keep responses concise and friendly
1283
+
1284
+ You are aware of tools and capabilities in the system but you do NOT execute anything.
1285
+ You cannot run code, search databases, or take actions. You are purely conversational.
1286
+ If the user asks you to DO something, acknowledge it and say it will be handled.`;
1287
+ var DEFAULT_CONTEXT_TOKENS = 4e3;
1288
+ var DEFAULT_REDUNDANCY_WINDOW = 10;
1289
+ var APPROX_CHARS_PER_TOKEN = 4;
1290
+ var ChatResponder = class {
1291
+ constructor(db, hooks, messageStore, config) {
1292
+ this.db = db;
1293
+ this.hooks = hooks;
1294
+ this.messageStore = messageStore;
1295
+ this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1296
+ this.contextWindowTokens = config.contextWindowTokens ?? DEFAULT_CONTEXT_TOKENS;
1297
+ this.redundancyWindow = config.redundancyWindow ?? DEFAULT_REDUNDANCY_WINDOW;
1298
+ this.model = config.model ?? "fast";
1299
+ this.llmCall = config.llmCall;
1300
+ }
1301
+ db;
1302
+ hooks;
1303
+ messageStore;
1304
+ systemPrompt;
1305
+ contextWindowTokens;
1306
+ redundancyWindow;
1307
+ model;
1308
+ llmCall;
1309
+ /**
1310
+ * Generate a fast conversational response to an inbound message.
1311
+ * Uses rolling context window from thread history.
1312
+ */
1313
+ async respond(opts) {
1314
+ const history = await this.messageStore.getThreadHistory(
1315
+ opts.threadId,
1316
+ 50
1317
+ // get more, trim by tokens
1318
+ );
1319
+ const messages = this.buildContextWindow(history, opts.messageBody);
1320
+ let system = this.systemPrompt;
1321
+ if (opts.capabilities) {
1322
+ system += `
1323
+
1324
+ System capabilities you are aware of:
1325
+ ${opts.capabilities}`;
1326
+ }
1327
+ if (opts.userName) {
1328
+ system += `
1329
+
1330
+ You are talking to: ${opts.userName}`;
1331
+ }
1332
+ const result = await this.llmCall({
1333
+ model: this.model,
1334
+ messages,
1335
+ system,
1336
+ maxTokens: 500
1337
+ });
1338
+ return result.content;
1339
+ }
1340
+ /**
1341
+ * Filter any outbound message through the LLM for human readability.
1342
+ * This is the single funnel ALL responses pass through.
1343
+ */
1344
+ async filterResponse(text, context) {
1345
+ if (text.length < 100) return text;
1346
+ const result = await this.llmCall({
1347
+ model: this.model,
1348
+ messages: [
1349
+ {
1350
+ role: "user",
1351
+ content: `Rewrite this agent/system message to be human-friendly and conversational. Keep the substance, remove jargon, make it feel like a helpful assistant talking to a person. If it's already readable, return it as-is. Do not add preamble like "Here's the rewritten version". Just output the rewritten text.
1352
+
1353
+ ---
1354
+ ${text}`
1355
+ }
1356
+ ],
1357
+ maxTokens: 1e3
1358
+ });
1359
+ return result.content;
1360
+ }
1361
+ /**
1362
+ * Check if a candidate outbound message is redundant with recent messages.
1363
+ * Returns true if the message should be suppressed.
1364
+ */
1365
+ async isRedundant(text, threadId) {
1366
+ const recent = await this.messageStore.getRecentOutbound(threadId, this.redundancyWindow);
1367
+ if (recent.length === 0) return false;
1368
+ const recentTexts = recent.map((m) => m["body"] ?? "").filter((t) => t.length > 0).slice(-5).join("\n---\n");
1369
+ const result = await this.llmCall({
1370
+ model: this.model,
1371
+ messages: [
1372
+ {
1373
+ role: "user",
1374
+ content: `Does this NEW message duplicate or substantially overlap the RECENT messages already sent? Answer with just "redundant" or "not redundant". If the new message has meaningful new information or updates, it is NOT redundant.
1375
+
1376
+ RECENT MESSAGES:
1377
+ ${recentTexts}
1378
+
1379
+ NEW MESSAGE:
1380
+ ${text}`
1381
+ }
1382
+ ],
1383
+ maxTokens: 10
1384
+ });
1385
+ return result.content.toLowerCase().includes("redundant") && !result.content.toLowerCase().includes("not redundant");
1386
+ }
1387
+ /**
1388
+ * Full send pipeline: check redundancy → filter → store → deliver.
1389
+ * Returns the message ID, or undefined if suppressed as redundant.
1390
+ */
1391
+ async sendResponse(opts) {
1392
+ if (!opts.skipRedundancyCheck) {
1393
+ const redundant = await this.isRedundant(opts.text, opts.threadId);
1394
+ if (redundant) {
1395
+ await this.hooks.emit("response.suppressed", {
1396
+ channel: opts.channel,
1397
+ threadId: opts.threadId,
1398
+ reason: "redundant"
1399
+ });
1400
+ return void 0;
1401
+ }
1402
+ }
1403
+ const filtered = opts.skipFilter ? opts.text : await this.filterResponse(opts.text, {
1404
+ channel: opts.channel,
1405
+ threadId: opts.threadId,
1406
+ source: opts.source
1407
+ });
1408
+ const messageId = await this.messageStore.storeOutbound({
1409
+ channel: opts.channel,
1410
+ text: filtered,
1411
+ threadId: opts.threadId,
1412
+ agentId: opts.agentId,
1413
+ agentSlug: opts.agentSlug,
1414
+ taskId: opts.taskId
1415
+ });
1416
+ await this.hooks.emit("response.ready", {
1417
+ messageId,
1418
+ channel: opts.channel,
1419
+ threadId: opts.threadId,
1420
+ text: filtered,
1421
+ taskId: opts.taskId
1422
+ });
1423
+ return messageId;
1424
+ }
1425
+ /**
1426
+ * Build a context window from thread history, trimmed to token limit.
1427
+ */
1428
+ buildContextWindow(history, currentMessage) {
1429
+ const maxChars = this.contextWindowTokens * APPROX_CHARS_PER_TOKEN;
1430
+ let charCount = currentMessage.length;
1431
+ const messages = [];
1432
+ for (let i = history.length - 1; i >= 0; i--) {
1433
+ const msg = history[i];
1434
+ const body = msg["body"] ?? "";
1435
+ const direction = msg["direction"];
1436
+ if (charCount + body.length > maxChars) break;
1437
+ messages.unshift({
1438
+ role: direction === "inbound" ? "user" : "assistant",
1439
+ content: body
1440
+ });
1441
+ charCount += body.length;
1442
+ }
1443
+ messages.push({ role: "user", content: currentMessage });
1444
+ return messages;
1445
+ }
1446
+ };
1447
+
1448
+ // src/core/chat/message-interpreter.ts
1449
+ var INTERPRET_SYSTEM = `You are a message parser. Extract structured data from the user's message.
1450
+
1451
+ Return a JSON object with these fields:
1452
+ {
1453
+ "tasks": [{ "title": "...", "description": "...", "priority": 1-10 }],
1454
+ "memories": [{ "summary": "one-line", "contents": "full text", "tags": ["tag1"], "category": "..." }],
1455
+ "user_context": [{ "trait": "...", "value": "..." }],
1456
+ "is_task_request": true/false
1457
+ }
1458
+
1459
+ Rules:
1460
+ - "tasks": actionable requests the user wants done. NOT conversational messages.
1461
+ - "memories": information, notes, random thoughts to remember. Parse thematically \u2014 one message can have multiple memories.
1462
+ - "user_context": personality traits, preferences, or learnings about the user.
1463
+ - "is_task_request": true if the message contains at least one actionable task.
1464
+ - If the message is just a greeting or conversation, return empty arrays and is_task_request: false.
1465
+ - Return ONLY valid JSON, no markdown or explanation.`;
1466
+ var MessageInterpreter = class {
1467
+ constructor(db, hooks, config) {
1468
+ this.db = db;
1469
+ this.hooks = hooks;
1470
+ this.extractors = config.extractors ?? [];
1471
+ this.model = config.model ?? "fast";
1472
+ this.llmCall = config.llmCall;
1473
+ this.autoCreateTasks = config.autoCreateTasks ?? false;
1474
+ }
1475
+ db;
1476
+ hooks;
1477
+ extractors;
1478
+ model;
1479
+ llmCall;
1480
+ autoCreateTasks;
1481
+ /**
1482
+ * Interpret a stored message asynchronously.
1483
+ * Extracts tasks, memories, files, user context, and custom types.
1484
+ */
1485
+ async interpret(messageId) {
1486
+ const message = await this.db.get("messages", { id: messageId });
1487
+ if (!message) throw new Error(`Message not found: ${messageId}`);
1488
+ const body = message["body"];
1489
+ const userId = message["user_id"];
1490
+ const attachments = await this.db.query("message_attachments", {
1491
+ where: { message_id: messageId }
1492
+ });
1493
+ const parsed = await this.extractWithLLM(body);
1494
+ for (const memory of parsed.memories) {
1495
+ await this.db.insert("memories", {
1496
+ message_id: messageId,
1497
+ user_id: userId,
1498
+ summary: memory.summary,
1499
+ contents: memory.contents,
1500
+ tags: JSON.stringify(memory.tags ?? []),
1501
+ category: memory.category
1502
+ });
1503
+ }
1504
+ for (const ctx of parsed.userContext) {
1505
+ await this.db.insert("memories", {
1506
+ message_id: messageId,
1507
+ user_id: userId,
1508
+ summary: ctx.trait,
1509
+ contents: ctx.value,
1510
+ tags: JSON.stringify(["user_context"]),
1511
+ category: "user_context"
1512
+ });
1513
+ }
1514
+ const files = [];
1515
+ for (const att of attachments) {
1516
+ if (att["contents"]) {
1517
+ files.push({
1518
+ filename: att["filename"] ?? "unknown",
1519
+ fileType: att["file_type"] ?? "file",
1520
+ contents: att["contents"],
1521
+ summary: att["summary"] ?? ""
1522
+ });
1523
+ }
1524
+ }
1525
+ const custom = {};
1526
+ for (const extractor of this.extractors) {
1527
+ try {
1528
+ const results = await extractor.extract(
1529
+ { body, attachments },
1530
+ this.llmCall
1531
+ );
1532
+ if (results.length > 0) {
1533
+ custom[extractor.type] = results;
1534
+ }
1535
+ } catch {
1536
+ }
1537
+ }
1538
+ const result = {
1539
+ messageId,
1540
+ tasks: parsed.tasks,
1541
+ memories: parsed.memories,
1542
+ files,
1543
+ userContext: parsed.userContext,
1544
+ custom,
1545
+ isTaskRequest: parsed.isTaskRequest
1546
+ };
1547
+ await this.hooks.emit("interpretation.completed", {
1548
+ messageId,
1549
+ taskCount: result.tasks.length,
1550
+ memoryCount: result.memories.length,
1551
+ fileCount: result.files.length,
1552
+ isTaskRequest: result.isTaskRequest
1553
+ });
1554
+ return result;
1555
+ }
1556
+ /**
1557
+ * Extract structured data from message text using LLM.
1558
+ */
1559
+ async extractWithLLM(body) {
1560
+ try {
1561
+ const result = await this.llmCall({
1562
+ model: this.model,
1563
+ messages: [{ role: "user", content: body }],
1564
+ system: INTERPRET_SYSTEM,
1565
+ maxTokens: 1e3
1566
+ });
1567
+ const parsed = JSON.parse(result.content);
1568
+ return {
1569
+ tasks: (parsed.tasks ?? []).map((t) => ({
1570
+ title: t.title,
1571
+ description: t.description,
1572
+ priority: t.priority
1573
+ })),
1574
+ memories: (parsed.memories ?? []).map((m) => ({
1575
+ summary: m.summary,
1576
+ contents: m.contents,
1577
+ tags: m.tags,
1578
+ category: m.category
1579
+ })),
1580
+ userContext: (parsed.user_context ?? []).map((u) => ({
1581
+ trait: u.trait,
1582
+ value: u.value
1583
+ })),
1584
+ isTaskRequest: parsed.is_task_request ?? false
1585
+ };
1586
+ } catch {
1587
+ return {
1588
+ tasks: [],
1589
+ memories: [],
1590
+ userContext: [],
1591
+ isTaskRequest: false
1592
+ };
1593
+ }
1594
+ }
1595
+ };
1596
+
1597
+ // src/core/chat/chat-pipeline.ts
1598
+ import { createHash, randomUUID } from "crypto";
1599
+ var DEFAULT_DEDUP_WINDOW_MS = 5 * 60 * 1e3;
1600
+ var ChatPipeline = class {
1601
+ constructor(db, hooks, config) {
1602
+ this.db = db;
1603
+ this.hooks = hooks;
1604
+ this.channel = config.channel ?? "slack";
1605
+ this.messageFilter = config.messageFilter;
1606
+ this.capabilities = config.capabilities;
1607
+ this.dedupWindowMs = config.dedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS;
1608
+ this.tasks = config.tasks;
1609
+ this.wakeups = config.wakeups;
1610
+ this.messageStore = new MessageStore(db, hooks);
1611
+ this.responder = new ChatResponder(db, hooks, this.messageStore, {
1612
+ llmCall: config.llmCall,
1613
+ model: config.model ?? "fast",
1614
+ systemPrompt: config.systemPrompt
1615
+ });
1616
+ this.interpreter = new MessageInterpreter(db, hooks, {
1617
+ llmCall: config.llmCall,
1618
+ model: config.model ?? "fast",
1619
+ extractors: config.extractors
1620
+ });
1621
+ this.router = new TriageRouter(db, hooks, {
1622
+ rules: config.routingRules,
1623
+ fallbackAgent: config.fallbackAgent,
1624
+ llmFallback: config.llmRouting ?? false,
1625
+ persist: true
1626
+ });
1627
+ this.registerHandlers();
1628
+ }
1629
+ db;
1630
+ hooks;
1631
+ messageStore;
1632
+ responder;
1633
+ interpreter;
1634
+ router;
1635
+ channel;
1636
+ messageFilter;
1637
+ capabilities;
1638
+ dedupWindowMs;
1639
+ tasks;
1640
+ wakeups;
1641
+ // In-memory thread → channel mapping for response routing
1642
+ // (before thread_task_map exists)
1643
+ threadChannelMap = /* @__PURE__ */ new Map();
1644
+ /**
1645
+ * Resolve the Slack channel ID for a thread (for response delivery).
1646
+ */
1647
+ resolveChannel(threadId, taskId) {
1648
+ return this.resolveChannelId(threadId, taskId);
1649
+ }
1650
+ /**
1651
+ * Register the 6-layer pipeline on the HookBus.
1652
+ */
1653
+ registerHandlers() {
1654
+ this.hooks.register("message.inbound", async (ctx) => {
1655
+ const msg = ctx;
1656
+ if (msg.channel !== this.channel) return;
1657
+ if (this.messageFilter && !this.messageFilter(msg)) return;
1658
+ if (await this.isDuplicate(msg)) return;
1659
+ const rawTs = msg.raw?.ts;
1660
+ const threadTs = msg.threadId ?? rawTs ?? msg.id;
1661
+ const channelId = msg.account ?? "";
1662
+ if (threadTs && channelId) {
1663
+ this.threadChannelMap.set(threadTs, channelId);
1664
+ }
1665
+ const { messageId } = await this.messageStore.storeInbound(msg);
1666
+ const ackResponse = await this.responder.respond({
1667
+ messageBody: msg.body,
1668
+ threadId: threadTs,
1669
+ channel: this.channel,
1670
+ capabilities: this.capabilities
1671
+ });
1672
+ await this.responder.sendResponse({
1673
+ text: ackResponse,
1674
+ channel: this.channel,
1675
+ threadId: threadTs,
1676
+ source: "responder",
1677
+ skipFilter: true,
1678
+ skipRedundancyCheck: true
1679
+ });
1680
+ void this.interpretAndDispatch(messageId, msg, threadTs, channelId);
1681
+ });
1682
+ this.hooks.register("run.completed", async (ctx) => {
1683
+ const taskId = ctx.taskId;
1684
+ if (!taskId) return;
1685
+ const task = await this.db.get("tasks", { id: taskId });
1686
+ const output = task?.result;
1687
+ if (!output) return;
1688
+ const mappings = await this.db.query("thread_task_map", {
1689
+ where: { task_id: taskId }
1690
+ });
1691
+ if (mappings.length === 0) return;
1692
+ const mapping = mappings[0];
1693
+ await this.responder.sendResponse({
1694
+ text: output,
1695
+ channel: this.channel,
1696
+ threadId: mapping.thread_ts,
1697
+ agentId: ctx.agentId,
1698
+ taskId,
1699
+ source: "agent"
1700
+ });
1701
+ }, { priority: 90 });
1702
+ }
1703
+ /**
1704
+ * Check and record message dedup (SHA256 hash, configurable window).
1705
+ */
1706
+ async isDuplicate(msg) {
1707
+ const hash = createHash("sha256").update(`${msg.from}:${msg.body}`).digest("hex");
1708
+ const cutoff = new Date(Date.now() - this.dedupWindowMs).toISOString();
1709
+ const recent = await this.db.query("message_dedup", {
1710
+ where: { content_hash: hash }
1711
+ });
1712
+ if (recent.some((r) => r.created_at > cutoff)) {
1713
+ return true;
1714
+ }
1715
+ await this.db.insert("message_dedup", {
1716
+ content_hash: hash,
1717
+ channel_id: msg.account ?? ""
1718
+ });
1719
+ return false;
1720
+ }
1721
+ /**
1722
+ * Async interpretation + task dispatch (Layers 3-5).
1723
+ */
1724
+ async interpretAndDispatch(messageId, msg, threadTs, channelId) {
1725
+ try {
1726
+ const result = await this.interpreter.interpret(messageId);
1727
+ if (result.tasks.length > 0 || result.memories.length > 0) {
1728
+ const summary = this.buildSummary(result);
1729
+ await this.responder.sendResponse({
1730
+ text: summary,
1731
+ channel: this.channel,
1732
+ threadId: threadTs,
1733
+ source: "interpretation"
1734
+ });
1735
+ }
1736
+ if (result.isTaskRequest && result.tasks.length > 0) {
1737
+ await this.dispatchTasks(result, msg, threadTs, channelId);
1738
+ }
1739
+ } catch (err) {
1740
+ await this.hooks.emit("interpretation.error", {
1741
+ messageId,
1742
+ error: err instanceof Error ? err.message : String(err)
1743
+ });
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Route and dispatch extracted tasks.
1748
+ */
1749
+ async dispatchTasks(result, msg, threadTs, channelId) {
1750
+ for (const extractedTask of result.tasks) {
1751
+ const { agentSlug: targetSlug } = await this.router.route(msg);
1752
+ if (!targetSlug) continue;
1753
+ const agents = await this.db.query("agents", { where: { slug: targetSlug } });
1754
+ const targetAgent = agents[0];
1755
+ if (!targetAgent) continue;
1756
+ const handlerAgentId = targetAgent.id;
1757
+ if (threadTs) {
1758
+ const existing = await this.db.query("thread_task_map", {
1759
+ where: { thread_ts: threadTs, channel_id: channelId }
1760
+ });
1761
+ if (existing.length > 0) {
1762
+ const taskId2 = existing[0].task_id;
1763
+ const task = await this.tasks.get(taskId2);
1764
+ if (task && task.status !== "done" && task.status !== "cancelled") {
1765
+ const updatedDesc = `${task.description ?? ""}
1766
+
1767
+ ---
1768
+ **Follow-up (${(/* @__PURE__ */ new Date()).toISOString()}):**
1769
+ ${msg.body}`;
1770
+ await this.tasks.update(taskId2, { description: updatedDesc });
1771
+ await this.wakeups.enqueue(handlerAgentId, "chat_followup", { taskId: taskId2 });
1772
+ return;
1773
+ }
1774
+ }
1775
+ }
1776
+ const description = `## Chat Message
1777
+
1778
+ **Channel:** ${channelId}
1779
+ **Thread:** ${threadTs}
1780
+ **From:** ${msg.from}
1781
+ **Time:** ${msg.receivedAt}
1782
+
1783
+ ---
1784
+
1785
+ ${extractedTask.description ?? msg.body}`;
1786
+ const taskId = randomUUID();
1787
+ if (threadTs) {
1788
+ await this.db.insert("thread_task_map", {
1789
+ thread_ts: threadTs,
1790
+ channel_id: channelId,
1791
+ task_id: taskId
1792
+ });
1793
+ }
1794
+ await this.tasks.create({
1795
+ id: taskId,
1796
+ title: extractedTask.title.slice(0, 120),
1797
+ description,
1798
+ assignee_id: handlerAgentId,
1799
+ priority: extractedTask.priority ?? 5
1800
+ });
1801
+ await this.wakeups.enqueue(handlerAgentId, "chat_dispatch", { taskId });
1802
+ }
1803
+ }
1804
+ /**
1805
+ * Resolve Slack channel ID from thread_task_map or in-memory fallback.
1806
+ */
1807
+ async resolveChannelId(threadId, taskId) {
1808
+ if (taskId) {
1809
+ const mappings = await this.db.query("thread_task_map", {
1810
+ where: { task_id: taskId }
1811
+ });
1812
+ if (mappings.length > 0) return mappings[0].channel_id;
1813
+ }
1814
+ if (threadId) {
1815
+ const mappings = await this.db.query("thread_task_map", {
1816
+ where: { thread_ts: threadId }
1817
+ });
1818
+ if (mappings.length > 0) return mappings[0].channel_id;
1819
+ }
1820
+ return this.threadChannelMap.get(threadId);
1821
+ }
1822
+ /**
1823
+ * Build human-readable interpretation summary.
1824
+ */
1825
+ buildSummary(result) {
1826
+ const parts = [];
1827
+ if (result.tasks.length > 0) {
1828
+ const names = result.tasks.map((t) => t.title).join(", ");
1829
+ parts.push(`I've identified ${result.tasks.length} task${result.tasks.length > 1 ? "s" : ""}: ${names}. Working on ${result.tasks.length > 1 ? "them" : "it"} now.`);
1830
+ }
1831
+ if (result.memories.length > 0) {
1832
+ parts.push(`Noted ${result.memories.length} thing${result.memories.length > 1 ? "s" : ""} to remember.`);
1833
+ }
1834
+ return parts.join(" ");
1835
+ }
1836
+ };
1837
+
1162
1838
  // src/core/chat/text-chunker.ts
1163
1839
  function chunkText(text, maxLen) {
1164
1840
  if (maxLen <= 0) throw new Error("maxLen must be > 0");
@@ -1826,6 +2502,66 @@ function defineCoreTables(db) {
1826
2502
  "FOREIGN KEY (playbook_id) REFERENCES playbooks(id)"
1827
2503
  ]
1828
2504
  });
2505
+ db.define("message_attachments", {
2506
+ columns: {
2507
+ id: "TEXT PRIMARY KEY",
2508
+ message_id: "TEXT NOT NULL",
2509
+ file_type: "TEXT NOT NULL DEFAULT 'file'",
2510
+ filename: "TEXT",
2511
+ mime_type: "TEXT",
2512
+ size_bytes: "INTEGER",
2513
+ contents: "TEXT",
2514
+ summary: "TEXT",
2515
+ url: "TEXT",
2516
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2517
+ },
2518
+ tableConstraints: [
2519
+ "CREATE INDEX IF NOT EXISTS idx_message_attachments_message ON message_attachments(message_id)",
2520
+ "FOREIGN KEY (message_id) REFERENCES messages(id)"
2521
+ ]
2522
+ });
2523
+ db.define("thread_task_map", {
2524
+ columns: {
2525
+ id: "TEXT PRIMARY KEY",
2526
+ thread_ts: "TEXT NOT NULL",
2527
+ channel_id: "TEXT NOT NULL",
2528
+ task_id: "TEXT NOT NULL",
2529
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2530
+ },
2531
+ tableConstraints: [
2532
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_thread_task_map_thread ON thread_task_map(thread_ts, channel_id)",
2533
+ "CREATE INDEX IF NOT EXISTS idx_thread_task_map_task ON thread_task_map(task_id)"
2534
+ ]
2535
+ });
2536
+ db.define("message_dedup", {
2537
+ columns: {
2538
+ id: "TEXT PRIMARY KEY",
2539
+ content_hash: "TEXT NOT NULL",
2540
+ channel_id: "TEXT NOT NULL",
2541
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2542
+ },
2543
+ tableConstraints: [
2544
+ "CREATE INDEX IF NOT EXISTS idx_message_dedup_hash ON message_dedup(content_hash, created_at)"
2545
+ ]
2546
+ });
2547
+ db.define("memories", {
2548
+ columns: {
2549
+ id: "TEXT PRIMARY KEY",
2550
+ message_id: "TEXT",
2551
+ user_id: "TEXT",
2552
+ summary: "TEXT NOT NULL",
2553
+ contents: "TEXT NOT NULL",
2554
+ tags: "TEXT NOT NULL DEFAULT '[]'",
2555
+ category: "TEXT",
2556
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
2557
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
2558
+ deleted_at: "TEXT"
2559
+ },
2560
+ tableConstraints: [
2561
+ "CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id, created_at)",
2562
+ "CREATE INDEX IF NOT EXISTS idx_memories_message ON memories(message_id)"
2563
+ ]
2564
+ });
1829
2565
  }
1830
2566
 
1831
2567
  // src/core/data/core-migrations.ts
@@ -5473,6 +6209,8 @@ export {
5473
6209
  CORE_MIGRATIONS,
5474
6210
  ChannelRegistry,
5475
6211
  ChannelRegistryError,
6212
+ ChatPipeline,
6213
+ ChatResponder,
5476
6214
  ChatSessionManager,
5477
6215
  CircuitBreaker,
5478
6216
  CliExecutionAdapter,
@@ -5491,7 +6229,9 @@ export {
5491
6229
  LoopDetector,
5492
6230
  LoopType,
5493
6231
  MAX_CHAIN_DEPTH,
6232
+ MessageInterpreter,
5494
6233
  MessagePipeline,
6234
+ MessageStore,
5495
6235
  ModelRouter,
5496
6236
  NdjsonLogger,
5497
6237
  NotificationQueue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",