@c4t4/heyamigo 0.9.6 → 0.9.7

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/dist/boot.js ADDED
@@ -0,0 +1,67 @@
1
+ // Shared bootstrap for both the library entry (src/index.ts) and the
2
+ // CLI start command (src/cli/start.ts). Single source of truth for
3
+ // startup order — there used to be two parallel main() functions that
4
+ // drifted; this prevents that.
5
+ import { setBaileysSocket } from './channels/index.js';
6
+ import { closeDb, initDb } from './db/index.js';
7
+ import { syncIdentitiesFromAccess } from './db/identity-sync.js';
8
+ import { attachIncoming } from './gateway/incoming.js';
9
+ import { logger } from './logger.js';
10
+ import { startScheduler } from './memory/scheduler.js';
11
+ import { startChatWorkers, stopChatWorkers } from './queue/chat-worker.js';
12
+ import { requestShutdown, startOrchestrator, stopOrchestrator, } from './queue/orchestrator.js';
13
+ import { startSenderWorker, stopSenderWorker } from './queue/sender-worker.js';
14
+ import { startSocket } from './wa/socket.js';
15
+ let booted = false;
16
+ export async function bootBot() {
17
+ if (booted) {
18
+ logger.warn('bootBot called twice; ignoring');
19
+ return;
20
+ }
21
+ booted = true;
22
+ logger.info('heyamigo starting');
23
+ // Migrations + drift check first. Refuses to start on schema mismatch
24
+ // — protects production data from a half-applied schema upgrade.
25
+ initDb();
26
+ // Derived view: persons + identities from access.json (idempotent).
27
+ syncIdentitiesFromAccess();
28
+ // Orchestrator first so workers see a control surface from the moment
29
+ // they register. Drain hook stops everything in reverse order.
30
+ startOrchestrator({
31
+ onShutdownDrained: () => {
32
+ stopChatWorkers();
33
+ stopSenderWorker();
34
+ stopOrchestrator();
35
+ closeDb();
36
+ },
37
+ });
38
+ // Workers next. Inbound table is the source of truth; anything left
39
+ // from a previous crash gets claimed by the new pool automatically.
40
+ // No separate replay step needed.
41
+ startSenderWorker();
42
+ startChatWorkers();
43
+ startScheduler();
44
+ await startSocket((sock) => {
45
+ attachIncoming(sock);
46
+ // Point the Baileys adapter at the live socket. Called on each
47
+ // reconnect with a fresh sock; the adapter just keeps the latest.
48
+ setBaileysSocket(sock);
49
+ });
50
+ }
51
+ // Install once. Both signals trigger the same graceful drain:
52
+ // orchestrator picks up the shutdown control row, waits for busy
53
+ // workers, then runs onShutdownDrained and exits.
54
+ let signalsInstalled = false;
55
+ export function installShutdownSignals() {
56
+ if (signalsInstalled)
57
+ return;
58
+ signalsInstalled = true;
59
+ process.on('SIGINT', () => {
60
+ logger.info('SIGINT received, requesting graceful shutdown');
61
+ requestShutdown('SIGINT');
62
+ });
63
+ process.on('SIGTERM', () => {
64
+ logger.info('SIGTERM received, requesting graceful shutdown');
65
+ requestShutdown('SIGTERM');
66
+ });
67
+ }
package/dist/cli/start.js CHANGED
@@ -1,10 +1,6 @@
1
1
  import { execSync } from 'child_process';
2
- import { attachIncoming } from '../gateway/incoming.js';
3
- import { handleReply } from '../gateway/outgoing.js';
2
+ import { bootBot, installShutdownSignals } from '../boot.js';
4
3
  import { logger } from '../logger.js';
5
- import { startScheduler } from '../memory/scheduler.js';
6
- import { replayPending } from '../queue/queue.js';
7
- import { startSocket } from '../wa/socket.js';
8
4
  export async function main() {
9
5
  try {
10
6
  execSync('which claude', { stdio: 'pipe' });
@@ -14,23 +10,9 @@ export async function main() {
14
10
  ' npm install -g @anthropic-ai/claude-code\n');
15
11
  process.exit(1);
16
12
  }
17
- logger.info('heyamigo starting');
18
- startScheduler();
19
- await startSocket((sock) => {
20
- attachIncoming(sock);
21
- });
22
- void replayPending(async (job, result) => {
23
- await handleReply(job, result, {});
24
- }).catch((err) => logger.error({ err }, 'replay failed'));
13
+ installShutdownSignals();
14
+ await bootBot();
25
15
  }
26
- process.on('SIGINT', () => {
27
- logger.info('SIGINT received, shutting down');
28
- process.exit(0);
29
- });
30
- process.on('SIGTERM', () => {
31
- logger.info('SIGTERM received, shutting down');
32
- process.exit(0);
33
- });
34
16
  main().catch((err) => {
35
17
  logger.error({ err }, 'fatal error during boot');
36
18
  process.exit(1);
package/dist/config.js CHANGED
@@ -36,6 +36,14 @@ const ConfigSchema = z.object({
36
36
  outputFormat: z.enum(['json', 'text', 'stream-json']),
37
37
  contextWindow: z.number(),
38
38
  }),
39
+ chatPool: z
40
+ .object({
41
+ // How many chat workers run in parallel. Per-address
42
+ // serialization means N workers serve up to N different chats
43
+ // concurrently; per-chat ordering is preserved naturally.
44
+ size: z.number().int().positive().default(5),
45
+ })
46
+ .default({ size: 5 }),
39
47
  codex: z
40
48
  .object({
41
49
  // Optional model override. If unset, Codex uses its default. Passed
package/dist/db/schema.js CHANGED
@@ -159,6 +159,12 @@ export const inbound = sqliteTable('inbound', {
159
159
  mediaBytes: integer('media_bytes'),
160
160
  pushName: text('push_name'), // sender's display name at send time
161
161
  triggerReason: text('trigger_reason'), // 'alias'|'mention'|'reply'|'owner'|...
162
+ // Producer-built worker payload (JSON). Chat worker deserializes
163
+ // at claim time to reconstruct the Job. Keeps the rebuild logic
164
+ // out of the worker for Phase 4; later phases may move portions
165
+ // (memory preamble assembly) into the worker for fresh-at-claim
166
+ // semantics. Nullable so non-chat inserters can leave it off.
167
+ payload: text('payload'),
162
168
  // 'pending'|'claimed'|'done'|'failed'|'dlq'
163
169
  status: text('status').notNull(),
164
170
  attempts: integer('attempts').notNull().default(0),
@@ -2,10 +2,12 @@ import { unlink } from 'fs/promises';
2
2
  import { getContentType, isJidGroup, jidDecode, jidNormalizedUser, } from 'baileys';
3
3
  import { getProvider } from '../ai/providers.js';
4
4
  import { getSession } from '../ai/sessions.js';
5
+ import { formatAddress, jidToAddress } from '../db/address.js';
6
+ import { personIdForAddress } from '../db/identity-sync.js';
5
7
  import { config } from '../config.js';
6
8
  import { logger } from '../logger.js';
7
9
  import { buildMemoryPreamble } from '../memory/preamble.js';
8
- import { enqueue } from '../queue/queue.js';
10
+ import { enqueueInbound } from '../queue/inbound.js';
9
11
  import { detectMediaType, downloadAndSave, getMediaSize, mediaPromptTag, } from '../store/media.js';
10
12
  import { append } from '../store/messages.js';
11
13
  import { getDailyTokens } from '../store/usage.js';
@@ -13,7 +15,6 @@ import { sendText } from '../wa/sender.js';
13
15
  import { checkAccess, discoverGroupIfNew, getLimitsForUser, getRoleForContext, } from '../wa/whitelist.js';
14
16
  import { buildInitPayload, buildRecentContext } from './bootstrap.js';
15
17
  import { tryCommand } from './commands.js';
16
- import { handleReply } from './outgoing.js';
17
18
  import { checkTrigger } from './triggers.js';
18
19
  export function attachIncoming(sock) {
19
20
  const ownerJid = sock.user?.id
@@ -201,46 +202,29 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
201
202
  fromMe: stored.fromMe,
202
203
  allowedTools: role.tools,
203
204
  };
204
- // Start typing indicator immediately; refresh every 10s (WA expires ~15s)
205
- let typingHeartbeat = null;
206
- if (config.reply.typingIndicator) {
207
- void sock
208
- .sendPresenceUpdate('composing', stored.jid)
209
- .catch(() => undefined);
210
- typingHeartbeat = setInterval(() => {
211
- void sock
212
- .sendPresenceUpdate('composing', stored.jid)
213
- .catch(() => undefined);
214
- }, 10000);
215
- }
216
- const stopTyping = () => {
217
- if (typingHeartbeat)
218
- clearInterval(typingHeartbeat);
219
- typingHeartbeat = null;
220
- };
221
- // Defense-in-depth: if nothing else clears the heartbeat within 10 min
222
- // (e.g. a code path forgot), force-stop. Prevents runaway "typing..."
223
- // indicators when the pipeline silently fails.
224
- const typingSafetyCap = setTimeout(() => {
225
- if (typingHeartbeat) {
226
- logger.warn({ jid: job.jid }, 'typingHeartbeat safety-cap fired, forcing clear');
227
- stopTyping();
228
- }
229
- }, 10 * 60 * 1000);
230
- typingSafetyCap.unref();
231
- enqueue(job)
232
- .then((result) => handleReply(job, result, msg))
233
- .catch((err) => {
234
- const isTimeout = err instanceof Error && err.name === 'ClaudeTimeoutError';
235
- logger.error({ err, jid: job.jid, isTimeout }, 'pipeline failed');
236
- const replyText = isTimeout
237
- ? 'That request timed out. The task was cancelled, queue is moving.'
238
- : config.reply.errorMessage;
239
- return handleReply(job, { reply: replyText }, msg).catch((e) => logger.error({ err: e, jid: job.jid }, 'failed to send error reply'));
240
- })
241
- .finally(() => {
242
- stopTyping();
243
- clearTimeout(typingSafetyCap);
205
+ // Enqueue into the inbound table; chat worker pool drains and
206
+ // calls processJob + handleReply asynchronously. Typing indicator
207
+ // is temporarily dropped (was tied to the old synchronous flow);
208
+ // re-add via ChannelAdapter.sendTyping() in a follow-up commit.
209
+ const chatAddress = formatAddress(jidToAddress(stored.jid));
210
+ const senderAddress = stored.senderNumber
211
+ ? formatAddress(jidToAddress(`${stored.senderNumber}@s.whatsapp.net`))
212
+ : null;
213
+ const personId = personIdForAddress(chatAddress);
214
+ const actorPersonId = senderAddress
215
+ ? personIdForAddress(senderAddress)
216
+ : null;
217
+ enqueueInbound({
218
+ address: chatAddress,
219
+ actorAddress: senderAddress,
220
+ personId,
221
+ actorPersonId,
222
+ externalMsgId: msg.key.id ?? null,
223
+ text: stored.text,
224
+ pushName: stored.pushName ?? null,
225
+ triggerReason,
226
+ receivedAt: stored.timestamp,
227
+ payload: job,
244
228
  });
245
229
  }
246
230
  catch (err) {
package/dist/index.js CHANGED
@@ -1,59 +1,7 @@
1
- import { setBaileysSocket } from './channels/index.js';
2
- import { closeDb, initDb } from './db/index.js';
3
- import { syncIdentitiesFromAccess } from './db/identity-sync.js';
4
- import { attachIncoming } from './gateway/incoming.js';
5
- import { handleReply } from './gateway/outgoing.js';
1
+ import { bootBot, installShutdownSignals } from './boot.js';
6
2
  import { logger } from './logger.js';
7
- import { startScheduler } from './memory/scheduler.js';
8
- import { requestShutdown, startOrchestrator, stopOrchestrator, } from './queue/orchestrator.js';
9
- import { replayPending } from './queue/queue.js';
10
- import { startSenderWorker, stopSenderWorker } from './queue/sender-worker.js';
11
- import { startSocket } from './wa/socket.js';
12
- async function main() {
13
- logger.info('heyamigo starting');
14
- // Migrations + drift check first; refuses to start on schema mismatch.
15
- initDb();
16
- // Derived view: populate persons + identities from access.json.
17
- syncIdentitiesFromAccess();
18
- // Orchestrator handles cross-cutting bookkeeping: control table
19
- // signals, stuck-claim reclaim, dead-worker detection, cron polling
20
- // (Phase 2.2+). Starts before workers so it can see them register.
21
- startOrchestrator({
22
- onShutdownDrained: () => {
23
- stopSenderWorker();
24
- stopOrchestrator();
25
- closeDb();
26
- },
27
- });
28
- // Sender worker drains outbound queue → channel adapters. Started
29
- // before the socket so it's ready when handleReply enqueues rows.
30
- startSenderWorker();
31
- startScheduler();
32
- await startSocket((sock) => {
33
- attachIncoming(sock);
34
- // Point the Baileys adapter at the live socket. Called on each
35
- // reconnect with a fresh sock; the adapter just keeps the latest.
36
- setBaileysSocket(sock);
37
- });
38
- // Replay any jobs left from a previous crash (no original WAMessage
39
- // available, so replies are sent as plain messages, not quoted).
40
- void replayPending(async (job, result) => {
41
- await handleReply(job, result, {});
42
- }).catch((err) => logger.error({ err }, 'replay failed'));
43
- }
44
- // Graceful shutdown: signal handler writes a 'shutdown' row to the
45
- // control table; orchestrator picks it up, drains in-flight work,
46
- // then runs onShutdownDrained (stops workers, closes DB) and exits.
47
- // A 30s timer inside the orchestrator force-exits if drain hangs.
48
- process.on('SIGINT', () => {
49
- logger.info('SIGINT received, requesting graceful shutdown');
50
- requestShutdown('SIGINT');
51
- });
52
- process.on('SIGTERM', () => {
53
- logger.info('SIGTERM received, requesting graceful shutdown');
54
- requestShutdown('SIGTERM');
55
- });
56
- main().catch((err) => {
3
+ installShutdownSignals();
4
+ bootBot().catch((err) => {
57
5
  logger.error({ err }, 'fatal error during boot');
58
6
  process.exit(1);
59
7
  });
@@ -0,0 +1,183 @@
1
+ // Chat worker pool. N workers drain the inbound queue; per-address
2
+ // serialization ensures one in-flight job per chat while different
3
+ // chats run in parallel (Phase 4's headline feature).
4
+ //
5
+ // Each worker:
6
+ // 1. claimNextInbound (atomic, serialized per address)
7
+ // 2. deserialize the producer-built Job payload
8
+ // 3. call processJob (existing AI + marker handling, unchanged)
9
+ // 4. call handleReply (enqueues outbound rows)
10
+ // 5. markInboundDone — or markInboundRetryOrDlq on error
11
+ //
12
+ // Replaces the in-memory fastq queue (queue/queue.ts). The old
13
+ // queue + persistence files stay in place for now in case they're
14
+ // still referenced anywhere; clean removal lands after we confirm
15
+ // nothing breaks.
16
+ import { hostname } from 'os';
17
+ import { eq } from 'drizzle-orm';
18
+ import { config } from '../config.js';
19
+ import { getDb } from '../db/index.js';
20
+ import { workers } from '../db/schema.js';
21
+ import { handleReply } from '../gateway/outgoing.js';
22
+ import { logger } from '../logger.js';
23
+ import { processJob } from './worker.js';
24
+ import { claimNextInbound, markInboundDone, markInboundFailed, markInboundRetryOrDlq, } from './inbound.js';
25
+ const HEARTBEAT_INTERVAL_MS = 5_000;
26
+ const IDLE_POLL_INTERVAL_MS = 250;
27
+ const BUSY_POLL_INTERVAL_MS = 0; // immediately try next after a successful claim
28
+ const activeWorkers = [];
29
+ let stopping = false;
30
+ let heartbeatTimer = null;
31
+ function newWorkerId(slot) {
32
+ return `${hostname()}-${process.pid}-chat-${slot}`;
33
+ }
34
+ function registerWorker(id) {
35
+ const db = getDb();
36
+ const now = Math.floor(Date.now() / 1000);
37
+ db.insert(workers)
38
+ .values({
39
+ id,
40
+ kind: 'chat',
41
+ status: 'idle',
42
+ currentJob: null,
43
+ lastSeen: now,
44
+ startedAt: now,
45
+ })
46
+ .onConflictDoUpdate({
47
+ target: workers.id,
48
+ set: { status: 'idle', currentJob: null, lastSeen: now, startedAt: now },
49
+ })
50
+ .run();
51
+ }
52
+ function setWorkerStatus(id, status, currentJob = null) {
53
+ const db = getDb();
54
+ db.update(workers)
55
+ .set({
56
+ status,
57
+ currentJob,
58
+ lastSeen: Math.floor(Date.now() / 1000),
59
+ })
60
+ .where(eq(workers.id, id))
61
+ .run();
62
+ }
63
+ function heartbeatAll() {
64
+ if (activeWorkers.length === 0)
65
+ return;
66
+ const db = getDb();
67
+ const now = Math.floor(Date.now() / 1000);
68
+ for (const id of activeWorkers) {
69
+ db.update(workers)
70
+ .set({ lastSeen: now })
71
+ .where(eq(workers.id, id))
72
+ .run();
73
+ }
74
+ }
75
+ // Reconstruct the Job from an inbound row's payload. The payload was
76
+ // built by the producer (gateway/incoming.ts) at enqueue time.
77
+ function jobFromRow(row) {
78
+ if (!row.payload) {
79
+ logger.warn({ id: row.id }, 'inbound row has no payload; cannot reconstruct Job');
80
+ return null;
81
+ }
82
+ try {
83
+ return JSON.parse(row.payload);
84
+ }
85
+ catch (err) {
86
+ logger.error({ err, id: row.id }, 'inbound row payload is not JSON');
87
+ return null;
88
+ }
89
+ }
90
+ async function processOne(workerId, row) {
91
+ setWorkerStatus(workerId, 'busy', `inbound:${row.id}`);
92
+ const job = jobFromRow(row);
93
+ if (!job) {
94
+ markInboundFailed(row.id, workerId, 'invalid payload');
95
+ setWorkerStatus(workerId, 'idle');
96
+ return;
97
+ }
98
+ try {
99
+ const result = await processJob(job);
100
+ // handleReply's third arg was the original WAMessage (used for
101
+ // group quoting). That regression was already deferred in Phase 1;
102
+ // pass an empty stub here too. The compiler type is loose enough.
103
+ await handleReply(job, result, {});
104
+ const ok = markInboundDone(row.id, workerId);
105
+ if (!ok) {
106
+ logger.warn({ id: row.id, workerId }, 'inbound markDone failed (claim lost?). reply was already enqueued.');
107
+ }
108
+ logger.info({
109
+ id: row.id,
110
+ address: row.address,
111
+ chars: result.reply.length,
112
+ dur: result.stats?.durationMs,
113
+ }, 'inbound processed');
114
+ }
115
+ catch (err) {
116
+ const isTimeout = err instanceof Error && err.name === 'ClaudeTimeoutError';
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ const result = markInboundRetryOrDlq(row.id, workerId, msg);
119
+ if (result.deadLettered) {
120
+ logger.error({ err, id: row.id, address: row.address, isTimeout }, 'inbound dead-lettered after max attempts');
121
+ // Send a user-facing failure ack so the chat isn't left hanging.
122
+ try {
123
+ const failText = isTimeout
124
+ ? 'That request timed out. The task was cancelled, queue is moving.'
125
+ : config.reply.errorMessage;
126
+ await handleReply(job, { reply: failText }, {});
127
+ }
128
+ catch (e) {
129
+ logger.error({ err: e, id: row.id }, 'failed to send DLQ-ack reply');
130
+ }
131
+ }
132
+ else if (result.retried) {
133
+ logger.warn({ err, id: row.id, address: row.address, isTimeout }, 'inbound transient fail, will retry');
134
+ }
135
+ }
136
+ finally {
137
+ setWorkerStatus(workerId, 'idle');
138
+ }
139
+ }
140
+ async function loop(workerId) {
141
+ while (!stopping) {
142
+ let processed = false;
143
+ try {
144
+ const row = claimNextInbound(workerId);
145
+ if (row) {
146
+ await processOne(workerId, row);
147
+ processed = true;
148
+ }
149
+ }
150
+ catch (err) {
151
+ logger.error({ err, workerId }, 'chat worker loop error');
152
+ }
153
+ const delay = processed ? BUSY_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
154
+ if (delay > 0) {
155
+ await new Promise((res) => setTimeout(res, delay));
156
+ }
157
+ else {
158
+ // Yield without sleeping so other microtasks (heartbeat etc) run.
159
+ await new Promise((res) => setImmediate(res));
160
+ }
161
+ }
162
+ setWorkerStatus(workerId, 'dead');
163
+ }
164
+ export function startChatWorkers() {
165
+ if (activeWorkers.length > 0) {
166
+ logger.warn('chat workers already started; ignoring');
167
+ return;
168
+ }
169
+ const pool = Math.max(1, config.chatPool?.size ?? 5);
170
+ for (let i = 0; i < pool; i++) {
171
+ const id = newWorkerId(i);
172
+ activeWorkers.push(id);
173
+ registerWorker(id);
174
+ void loop(id).catch((err) => logger.fatal({ err, workerId: id }, 'chat worker loop crashed'));
175
+ }
176
+ heartbeatTimer = setInterval(heartbeatAll, HEARTBEAT_INTERVAL_MS);
177
+ logger.info({ pool }, 'chat worker pool started');
178
+ }
179
+ export function stopChatWorkers() {
180
+ stopping = true;
181
+ if (heartbeatTimer)
182
+ clearInterval(heartbeatTimer);
183
+ }
@@ -37,6 +37,7 @@ export function enqueueInbound(input) {
37
37
  mediaBytes: input.mediaBytes ?? null,
38
38
  pushName: input.pushName ?? null,
39
39
  triggerReason: input.triggerReason ?? null,
40
+ payload: input.payload === undefined ? null : JSON.stringify(input.payload),
40
41
  status: 'pending',
41
42
  attempts: 0,
42
43
  nextAttemptAt: null,
@@ -0,0 +1 @@
1
+ ALTER TABLE `inbound` ADD `payload` text;
@@ -0,0 +1,665 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "dc030b66-cf9b-4fba-bdb6-39680e973b52",
5
+ "prevId": "155b0aa7-93ec-4d7a-a853-e680b47d162c",
6
+ "tables": {
7
+ "control": {
8
+ "name": "control",
9
+ "columns": {
10
+ "key": {
11
+ "name": "key",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "value": {
18
+ "name": "value",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": false,
22
+ "autoincrement": false
23
+ },
24
+ "requested_by": {
25
+ "name": "requested_by",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "requested_at": {
32
+ "name": "requested_at",
33
+ "type": "integer",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ }
38
+ },
39
+ "indexes": {},
40
+ "foreignKeys": {},
41
+ "compositePrimaryKeys": {},
42
+ "uniqueConstraints": {},
43
+ "checkConstraints": {}
44
+ },
45
+ "crons": {
46
+ "name": "crons",
47
+ "columns": {
48
+ "id": {
49
+ "name": "id",
50
+ "type": "integer",
51
+ "primaryKey": true,
52
+ "notNull": true,
53
+ "autoincrement": true
54
+ },
55
+ "name": {
56
+ "name": "name",
57
+ "type": "text",
58
+ "primaryKey": false,
59
+ "notNull": true,
60
+ "autoincrement": false
61
+ },
62
+ "enqueue_into": {
63
+ "name": "enqueue_into",
64
+ "type": "text",
65
+ "primaryKey": false,
66
+ "notNull": true,
67
+ "autoincrement": false
68
+ },
69
+ "payload": {
70
+ "name": "payload",
71
+ "type": "text",
72
+ "primaryKey": false,
73
+ "notNull": true,
74
+ "autoincrement": false
75
+ },
76
+ "recurrence": {
77
+ "name": "recurrence",
78
+ "type": "text",
79
+ "primaryKey": false,
80
+ "notNull": false,
81
+ "autoincrement": false
82
+ },
83
+ "next_run_at": {
84
+ "name": "next_run_at",
85
+ "type": "integer",
86
+ "primaryKey": false,
87
+ "notNull": true,
88
+ "autoincrement": false
89
+ },
90
+ "last_run_at": {
91
+ "name": "last_run_at",
92
+ "type": "integer",
93
+ "primaryKey": false,
94
+ "notNull": false,
95
+ "autoincrement": false
96
+ },
97
+ "enabled": {
98
+ "name": "enabled",
99
+ "type": "integer",
100
+ "primaryKey": false,
101
+ "notNull": true,
102
+ "autoincrement": false,
103
+ "default": 1
104
+ },
105
+ "created_at": {
106
+ "name": "created_at",
107
+ "type": "integer",
108
+ "primaryKey": false,
109
+ "notNull": true,
110
+ "autoincrement": false
111
+ }
112
+ },
113
+ "indexes": {
114
+ "crons_by_due": {
115
+ "name": "crons_by_due",
116
+ "columns": [
117
+ "enabled",
118
+ "next_run_at"
119
+ ],
120
+ "isUnique": false
121
+ },
122
+ "crons_name_uq": {
123
+ "name": "crons_name_uq",
124
+ "columns": [
125
+ "name"
126
+ ],
127
+ "isUnique": true,
128
+ "where": "\"crons\".\"recurrence\" IS NOT NULL"
129
+ }
130
+ },
131
+ "foreignKeys": {},
132
+ "compositePrimaryKeys": {},
133
+ "uniqueConstraints": {},
134
+ "checkConstraints": {}
135
+ },
136
+ "identities": {
137
+ "name": "identities",
138
+ "columns": {
139
+ "person_id": {
140
+ "name": "person_id",
141
+ "type": "text",
142
+ "primaryKey": false,
143
+ "notNull": true,
144
+ "autoincrement": false
145
+ },
146
+ "address": {
147
+ "name": "address",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": true,
151
+ "autoincrement": false
152
+ },
153
+ "added_at": {
154
+ "name": "added_at",
155
+ "type": "integer",
156
+ "primaryKey": false,
157
+ "notNull": true,
158
+ "autoincrement": false
159
+ }
160
+ },
161
+ "indexes": {
162
+ "identities_address_unique": {
163
+ "name": "identities_address_unique",
164
+ "columns": [
165
+ "address"
166
+ ],
167
+ "isUnique": true
168
+ }
169
+ },
170
+ "foreignKeys": {
171
+ "identities_person_id_persons_id_fk": {
172
+ "name": "identities_person_id_persons_id_fk",
173
+ "tableFrom": "identities",
174
+ "tableTo": "persons",
175
+ "columnsFrom": [
176
+ "person_id"
177
+ ],
178
+ "columnsTo": [
179
+ "id"
180
+ ],
181
+ "onDelete": "no action",
182
+ "onUpdate": "no action"
183
+ }
184
+ },
185
+ "compositePrimaryKeys": {
186
+ "identities_person_id_address_pk": {
187
+ "columns": [
188
+ "person_id",
189
+ "address"
190
+ ],
191
+ "name": "identities_person_id_address_pk"
192
+ }
193
+ },
194
+ "uniqueConstraints": {},
195
+ "checkConstraints": {}
196
+ },
197
+ "inbound": {
198
+ "name": "inbound",
199
+ "columns": {
200
+ "id": {
201
+ "name": "id",
202
+ "type": "integer",
203
+ "primaryKey": true,
204
+ "notNull": true,
205
+ "autoincrement": true
206
+ },
207
+ "address": {
208
+ "name": "address",
209
+ "type": "text",
210
+ "primaryKey": false,
211
+ "notNull": true,
212
+ "autoincrement": false
213
+ },
214
+ "actor_address": {
215
+ "name": "actor_address",
216
+ "type": "text",
217
+ "primaryKey": false,
218
+ "notNull": false,
219
+ "autoincrement": false
220
+ },
221
+ "person_id": {
222
+ "name": "person_id",
223
+ "type": "text",
224
+ "primaryKey": false,
225
+ "notNull": false,
226
+ "autoincrement": false
227
+ },
228
+ "actor_person_id": {
229
+ "name": "actor_person_id",
230
+ "type": "text",
231
+ "primaryKey": false,
232
+ "notNull": false,
233
+ "autoincrement": false
234
+ },
235
+ "external_msg_id": {
236
+ "name": "external_msg_id",
237
+ "type": "text",
238
+ "primaryKey": false,
239
+ "notNull": false,
240
+ "autoincrement": false
241
+ },
242
+ "text": {
243
+ "name": "text",
244
+ "type": "text",
245
+ "primaryKey": false,
246
+ "notNull": true,
247
+ "autoincrement": false
248
+ },
249
+ "media_path": {
250
+ "name": "media_path",
251
+ "type": "text",
252
+ "primaryKey": false,
253
+ "notNull": false,
254
+ "autoincrement": false
255
+ },
256
+ "media_mime": {
257
+ "name": "media_mime",
258
+ "type": "text",
259
+ "primaryKey": false,
260
+ "notNull": false,
261
+ "autoincrement": false
262
+ },
263
+ "media_bytes": {
264
+ "name": "media_bytes",
265
+ "type": "integer",
266
+ "primaryKey": false,
267
+ "notNull": false,
268
+ "autoincrement": false
269
+ },
270
+ "push_name": {
271
+ "name": "push_name",
272
+ "type": "text",
273
+ "primaryKey": false,
274
+ "notNull": false,
275
+ "autoincrement": false
276
+ },
277
+ "trigger_reason": {
278
+ "name": "trigger_reason",
279
+ "type": "text",
280
+ "primaryKey": false,
281
+ "notNull": false,
282
+ "autoincrement": false
283
+ },
284
+ "payload": {
285
+ "name": "payload",
286
+ "type": "text",
287
+ "primaryKey": false,
288
+ "notNull": false,
289
+ "autoincrement": false
290
+ },
291
+ "status": {
292
+ "name": "status",
293
+ "type": "text",
294
+ "primaryKey": false,
295
+ "notNull": true,
296
+ "autoincrement": false
297
+ },
298
+ "attempts": {
299
+ "name": "attempts",
300
+ "type": "integer",
301
+ "primaryKey": false,
302
+ "notNull": true,
303
+ "autoincrement": false,
304
+ "default": 0
305
+ },
306
+ "next_attempt_at": {
307
+ "name": "next_attempt_at",
308
+ "type": "integer",
309
+ "primaryKey": false,
310
+ "notNull": false,
311
+ "autoincrement": false
312
+ },
313
+ "last_error": {
314
+ "name": "last_error",
315
+ "type": "text",
316
+ "primaryKey": false,
317
+ "notNull": false,
318
+ "autoincrement": false
319
+ },
320
+ "claimed_by": {
321
+ "name": "claimed_by",
322
+ "type": "text",
323
+ "primaryKey": false,
324
+ "notNull": false,
325
+ "autoincrement": false
326
+ },
327
+ "claimed_at": {
328
+ "name": "claimed_at",
329
+ "type": "integer",
330
+ "primaryKey": false,
331
+ "notNull": false,
332
+ "autoincrement": false
333
+ },
334
+ "received_at": {
335
+ "name": "received_at",
336
+ "type": "integer",
337
+ "primaryKey": false,
338
+ "notNull": true,
339
+ "autoincrement": false
340
+ },
341
+ "created_at": {
342
+ "name": "created_at",
343
+ "type": "integer",
344
+ "primaryKey": false,
345
+ "notNull": true,
346
+ "autoincrement": false
347
+ },
348
+ "updated_at": {
349
+ "name": "updated_at",
350
+ "type": "integer",
351
+ "primaryKey": false,
352
+ "notNull": true,
353
+ "autoincrement": false
354
+ }
355
+ },
356
+ "indexes": {
357
+ "inbound_by_status_next": {
358
+ "name": "inbound_by_status_next",
359
+ "columns": [
360
+ "status",
361
+ "next_attempt_at"
362
+ ],
363
+ "isUnique": false
364
+ },
365
+ "inbound_by_address": {
366
+ "name": "inbound_by_address",
367
+ "columns": [
368
+ "address"
369
+ ],
370
+ "isUnique": false
371
+ },
372
+ "inbound_by_person": {
373
+ "name": "inbound_by_person",
374
+ "columns": [
375
+ "person_id",
376
+ "received_at"
377
+ ],
378
+ "isUnique": false
379
+ },
380
+ "inbound_external_msg_id_uq": {
381
+ "name": "inbound_external_msg_id_uq",
382
+ "columns": [
383
+ "external_msg_id"
384
+ ],
385
+ "isUnique": true,
386
+ "where": "\"inbound\".\"external_msg_id\" IS NOT NULL"
387
+ }
388
+ },
389
+ "foreignKeys": {},
390
+ "compositePrimaryKeys": {},
391
+ "uniqueConstraints": {},
392
+ "checkConstraints": {}
393
+ },
394
+ "outbound": {
395
+ "name": "outbound",
396
+ "columns": {
397
+ "id": {
398
+ "name": "id",
399
+ "type": "integer",
400
+ "primaryKey": true,
401
+ "notNull": true,
402
+ "autoincrement": true
403
+ },
404
+ "address": {
405
+ "name": "address",
406
+ "type": "text",
407
+ "primaryKey": false,
408
+ "notNull": true,
409
+ "autoincrement": false
410
+ },
411
+ "kind": {
412
+ "name": "kind",
413
+ "type": "text",
414
+ "primaryKey": false,
415
+ "notNull": true,
416
+ "autoincrement": false
417
+ },
418
+ "text": {
419
+ "name": "text",
420
+ "type": "text",
421
+ "primaryKey": false,
422
+ "notNull": false,
423
+ "autoincrement": false
424
+ },
425
+ "media_path": {
426
+ "name": "media_path",
427
+ "type": "text",
428
+ "primaryKey": false,
429
+ "notNull": false,
430
+ "autoincrement": false
431
+ },
432
+ "media_mime": {
433
+ "name": "media_mime",
434
+ "type": "text",
435
+ "primaryKey": false,
436
+ "notNull": false,
437
+ "autoincrement": false
438
+ },
439
+ "media_bytes": {
440
+ "name": "media_bytes",
441
+ "type": "integer",
442
+ "primaryKey": false,
443
+ "notNull": false,
444
+ "autoincrement": false
445
+ },
446
+ "quote_msg_id": {
447
+ "name": "quote_msg_id",
448
+ "type": "text",
449
+ "primaryKey": false,
450
+ "notNull": false,
451
+ "autoincrement": false
452
+ },
453
+ "idempotency_key": {
454
+ "name": "idempotency_key",
455
+ "type": "text",
456
+ "primaryKey": false,
457
+ "notNull": false,
458
+ "autoincrement": false
459
+ },
460
+ "status": {
461
+ "name": "status",
462
+ "type": "text",
463
+ "primaryKey": false,
464
+ "notNull": true,
465
+ "autoincrement": false
466
+ },
467
+ "attempts": {
468
+ "name": "attempts",
469
+ "type": "integer",
470
+ "primaryKey": false,
471
+ "notNull": true,
472
+ "autoincrement": false,
473
+ "default": 0
474
+ },
475
+ "next_attempt_at": {
476
+ "name": "next_attempt_at",
477
+ "type": "integer",
478
+ "primaryKey": false,
479
+ "notNull": false,
480
+ "autoincrement": false
481
+ },
482
+ "last_error": {
483
+ "name": "last_error",
484
+ "type": "text",
485
+ "primaryKey": false,
486
+ "notNull": false,
487
+ "autoincrement": false
488
+ },
489
+ "claimed_by": {
490
+ "name": "claimed_by",
491
+ "type": "text",
492
+ "primaryKey": false,
493
+ "notNull": false,
494
+ "autoincrement": false
495
+ },
496
+ "claimed_at": {
497
+ "name": "claimed_at",
498
+ "type": "integer",
499
+ "primaryKey": false,
500
+ "notNull": false,
501
+ "autoincrement": false
502
+ },
503
+ "created_at": {
504
+ "name": "created_at",
505
+ "type": "integer",
506
+ "primaryKey": false,
507
+ "notNull": true,
508
+ "autoincrement": false
509
+ },
510
+ "updated_at": {
511
+ "name": "updated_at",
512
+ "type": "integer",
513
+ "primaryKey": false,
514
+ "notNull": true,
515
+ "autoincrement": false
516
+ }
517
+ },
518
+ "indexes": {
519
+ "outbound_by_status_next": {
520
+ "name": "outbound_by_status_next",
521
+ "columns": [
522
+ "status",
523
+ "next_attempt_at"
524
+ ],
525
+ "isUnique": false
526
+ },
527
+ "outbound_by_address": {
528
+ "name": "outbound_by_address",
529
+ "columns": [
530
+ "address"
531
+ ],
532
+ "isUnique": false
533
+ },
534
+ "outbound_idempotency_key_uq": {
535
+ "name": "outbound_idempotency_key_uq",
536
+ "columns": [
537
+ "idempotency_key"
538
+ ],
539
+ "isUnique": true,
540
+ "where": "\"outbound\".\"idempotency_key\" IS NOT NULL"
541
+ }
542
+ },
543
+ "foreignKeys": {},
544
+ "compositePrimaryKeys": {},
545
+ "uniqueConstraints": {},
546
+ "checkConstraints": {}
547
+ },
548
+ "persons": {
549
+ "name": "persons",
550
+ "columns": {
551
+ "id": {
552
+ "name": "id",
553
+ "type": "text",
554
+ "primaryKey": true,
555
+ "notNull": true,
556
+ "autoincrement": false
557
+ },
558
+ "display_name": {
559
+ "name": "display_name",
560
+ "type": "text",
561
+ "primaryKey": false,
562
+ "notNull": false,
563
+ "autoincrement": false
564
+ },
565
+ "timezone": {
566
+ "name": "timezone",
567
+ "type": "text",
568
+ "primaryKey": false,
569
+ "notNull": false,
570
+ "autoincrement": false
571
+ },
572
+ "created_at": {
573
+ "name": "created_at",
574
+ "type": "integer",
575
+ "primaryKey": false,
576
+ "notNull": true,
577
+ "autoincrement": false
578
+ }
579
+ },
580
+ "indexes": {},
581
+ "foreignKeys": {},
582
+ "compositePrimaryKeys": {},
583
+ "uniqueConstraints": {},
584
+ "checkConstraints": {}
585
+ },
586
+ "workers": {
587
+ "name": "workers",
588
+ "columns": {
589
+ "id": {
590
+ "name": "id",
591
+ "type": "text",
592
+ "primaryKey": true,
593
+ "notNull": true,
594
+ "autoincrement": false
595
+ },
596
+ "kind": {
597
+ "name": "kind",
598
+ "type": "text",
599
+ "primaryKey": false,
600
+ "notNull": true,
601
+ "autoincrement": false
602
+ },
603
+ "status": {
604
+ "name": "status",
605
+ "type": "text",
606
+ "primaryKey": false,
607
+ "notNull": true,
608
+ "autoincrement": false
609
+ },
610
+ "current_job": {
611
+ "name": "current_job",
612
+ "type": "text",
613
+ "primaryKey": false,
614
+ "notNull": false,
615
+ "autoincrement": false
616
+ },
617
+ "last_seen": {
618
+ "name": "last_seen",
619
+ "type": "integer",
620
+ "primaryKey": false,
621
+ "notNull": true,
622
+ "autoincrement": false
623
+ },
624
+ "started_at": {
625
+ "name": "started_at",
626
+ "type": "integer",
627
+ "primaryKey": false,
628
+ "notNull": true,
629
+ "autoincrement": false
630
+ }
631
+ },
632
+ "indexes": {
633
+ "workers_by_kind_status": {
634
+ "name": "workers_by_kind_status",
635
+ "columns": [
636
+ "kind",
637
+ "status"
638
+ ],
639
+ "isUnique": false
640
+ },
641
+ "workers_by_last_seen": {
642
+ "name": "workers_by_last_seen",
643
+ "columns": [
644
+ "last_seen"
645
+ ],
646
+ "isUnique": false
647
+ }
648
+ },
649
+ "foreignKeys": {},
650
+ "compositePrimaryKeys": {},
651
+ "uniqueConstraints": {},
652
+ "checkConstraints": {}
653
+ }
654
+ },
655
+ "views": {},
656
+ "enums": {},
657
+ "_meta": {
658
+ "schemas": {},
659
+ "tables": {},
660
+ "columns": {}
661
+ },
662
+ "internal": {
663
+ "indexes": {}
664
+ }
665
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1779672156371,
30
30
  "tag": "0003_phase4_inbound",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1779672623565,
37
+ "tag": "0004_phase4_inbound_payload",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",