@c4t4/heyamigo 0.9.16 → 0.9.17

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/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
+ // Job-kind tag for duration estimation (see src/estimates/). Set
163
+ // at ingest time when a registered estimator matches the message
164
+ // (e.g. 'image-gen', 'browser:ig'). Null otherwise. Queried by
165
+ // the estimator on subsequent invocations to compute past-sample
166
+ // averages.
167
+ kind: text('kind'),
162
168
  // Producer-built worker payload (JSON). Chat worker deserializes
163
169
  // at claim time to reconstruct the Job. Keeps the rebuild logic
164
170
  // out of the worker for Phase 4; later phases may move portions
@@ -179,6 +185,8 @@ export const inbound = sqliteTable('inbound', {
179
185
  byStatusNext: index('inbound_by_status_next').on(t.status, t.nextAttemptAt),
180
186
  byAddress: index('inbound_by_address').on(t.address),
181
187
  byPerson: index('inbound_by_person').on(t.personId, t.receivedAt),
188
+ // Used by the duration estimator: "last N done rows of this kind".
189
+ byKindDone: index('inbound_by_kind_done').on(t.kind, t.status),
182
190
  // Sparse unique on external_msg_id: enforced only when set. Same
183
191
  // pattern as outbound's idempotency_key.
184
192
  uniqExtId: uniqueIndex('inbound_external_msg_id_uq')
@@ -0,0 +1,36 @@
1
+ // Image-generation estimator. Matches when the user message looks
2
+ // like a request to produce an image. Tracks duration of the chat-
3
+ // track turn that handles it (claimedAt → updatedAt on the inbound
4
+ // row).
5
+ import { aggregateMean, registerEstimator } from './registry.js';
6
+ // Conservative regex. Requires a generation verb AND an image-class
7
+ // noun within 80 chars. Prefers false-negative to false-positive —
8
+ // a single mistagged sample drags the average for everyone.
9
+ const IMAGE_GEN_RE = /\b(generate|create|make|draw|render|design|sketch|paint|illustrate)\b[^.?!\n]{0,80}\b(image|picture|drawing|art|artwork|photo|portrait|illustration|sketch|render|painting|wallpaper|logo|icon|graphic)\b/i;
10
+ class ImageGenEstimator {
11
+ kind = 'image-gen';
12
+ // 30s starting point — reasonable ballpark for current
13
+ // image-generation APIs (DALL-E 3, Imagen, Flux, etc.). The very
14
+ // first request shows this; from sample 1 onward it averages real
15
+ // observations.
16
+ defaultMs = 30_000;
17
+ matches(ctx) {
18
+ return IMAGE_GEN_RE.test(ctx.description);
19
+ }
20
+ estimate(samples) {
21
+ return aggregateMean(samples, this.defaultMs);
22
+ }
23
+ format(estimate) {
24
+ if (estimate.rangeMs) {
25
+ return `generating image, anywhere from ~${secs(estimate.rangeMs.lowMs)} to ~${secs(estimate.rangeMs.highMs)}`;
26
+ }
27
+ return `generating image, ~${secs(estimate.pointMs)}`;
28
+ }
29
+ }
30
+ function secs(ms) {
31
+ const s = Math.max(1, Math.round(ms / 1000));
32
+ if (s < 60)
33
+ return `${s}s`;
34
+ return `${Math.round(s / 60)}min`;
35
+ }
36
+ registerEstimator(new ImageGenEstimator());
@@ -0,0 +1,12 @@
1
+ // Estimates module entry point. Importing this side-effect-loads
2
+ // every built-in plugin (each plugin file calls registerEstimator()
3
+ // at module load). Outside callers only need:
4
+ //
5
+ // import { classify, estimate } from './estimates/index.js'
6
+ //
7
+ // Adding a new kind = drop a file alongside image-gen.ts and import
8
+ // it below. No other code in the codebase needs to change.
9
+ import './image-gen.js';
10
+ // future: import './browser-ig.js'
11
+ // future: import './voice-gen.js'
12
+ export { classify, estimate, formatEstimateDefault, humanDur, listEstimators, querySamplesForKind, registerEstimator, } from './registry.js';
@@ -0,0 +1,113 @@
1
+ // Estimator registry + the single entry points the rest of the bot
2
+ // uses: classify() and estimate(). Plugins self-register by importing
3
+ // this module and calling registerEstimator().
4
+ import { and, desc, eq, isNotNull } from 'drizzle-orm';
5
+ import { getDb } from '../db/index.js';
6
+ import { inbound } from '../db/schema.js';
7
+ const REGISTRY = [];
8
+ export function registerEstimator(e) {
9
+ // Idempotent on kind so hot-reload during dev doesn't duplicate.
10
+ const i = REGISTRY.findIndex((x) => x.kind === e.kind);
11
+ if (i >= 0)
12
+ REGISTRY[i] = e;
13
+ else
14
+ REGISTRY.push(e);
15
+ }
16
+ export function listEstimators() {
17
+ return REGISTRY;
18
+ }
19
+ // Find the first estimator whose matches() returns true. First-match
20
+ // wins — order matters when registering. More-specific kinds should
21
+ // register before broad fallbacks.
22
+ export function classify(ctx) {
23
+ for (const e of REGISTRY) {
24
+ if (e.matches(ctx))
25
+ return e;
26
+ }
27
+ return null;
28
+ }
29
+ // Pull the last N completed inbound rows for this kind. Returns
30
+ // newest-first; estimators that care about recency can use that
31
+ // order directly, the mean-based aggregator below doesn't.
32
+ //
33
+ // Limited to N=20 by default. The mean is fast and stable past 5-10
34
+ // samples; older data isn't helpful and risks staleness.
35
+ const SAMPLE_LIMIT = 20;
36
+ export function querySamplesForKind(kind, limit = SAMPLE_LIMIT) {
37
+ const db = getDb();
38
+ const rows = db
39
+ .select({
40
+ claimedAt: inbound.claimedAt,
41
+ updatedAt: inbound.updatedAt,
42
+ })
43
+ .from(inbound)
44
+ .where(and(eq(inbound.kind, kind), eq(inbound.status, 'done'), isNotNull(inbound.claimedAt)))
45
+ .orderBy(desc(inbound.id))
46
+ .limit(limit)
47
+ .all();
48
+ return rows
49
+ .filter((r) => r.claimedAt !== null)
50
+ .map((r) => ({
51
+ durationMs: (r.updatedAt - r.claimedAt) * 1000,
52
+ finishedAt: r.updatedAt,
53
+ }))
54
+ .filter((s) => s.durationMs > 0);
55
+ }
56
+ // Public entry point. Returns the kind + formatted text, or null
57
+ // when no estimator matched (i.e. this isn't a job-kind we estimate).
58
+ // If an estimator matches, the result is ALWAYS non-null — the
59
+ // estimator falls back to its defaultMs when no samples exist.
60
+ export function estimate(ctx) {
61
+ const e = classify(ctx);
62
+ if (!e)
63
+ return null;
64
+ const samples = querySamplesForKind(e.kind);
65
+ const result = e.estimate(samples);
66
+ const text = (e.format ?? formatEstimateDefault)(result);
67
+ return { kind: e.kind, result, text };
68
+ }
69
+ // Default UX-friendly rendering. Each estimator can override.
70
+ export function formatEstimateDefault(r) {
71
+ if (r.rangeMs) {
72
+ return `anywhere from ~${humanDur(r.rangeMs.lowMs)} to ~${humanDur(r.rangeMs.highMs)}`;
73
+ }
74
+ return `~${humanDur(r.pointMs)}`;
75
+ }
76
+ export function humanDur(ms) {
77
+ const s = Math.max(1, Math.round(ms / 1000));
78
+ if (s < 60)
79
+ return `${s}s`;
80
+ const m = Math.round(s / 60);
81
+ if (m < 60)
82
+ return `${m}min`;
83
+ return `${Math.round(m / 60)}h`;
84
+ }
85
+ // Shared aggregator used by built-in estimators. Each estimator may
86
+ // implement its own estimate() but most just call this.
87
+ export function aggregateMean(samples, defaultMs) {
88
+ if (samples.length === 0) {
89
+ return { pointMs: defaultMs, sampleSize: 0, confidence: 'low' };
90
+ }
91
+ const ds = samples.map((s) => s.durationMs);
92
+ const mean = ds.reduce((a, b) => a + b, 0) / ds.length;
93
+ if (samples.length === 1) {
94
+ return { pointMs: mean, sampleSize: 1, confidence: 'low' };
95
+ }
96
+ const variance = ds.reduce((acc, x) => acc + (x - mean) ** 2, 0) / ds.length;
97
+ const std = Math.sqrt(variance);
98
+ const confidence = samples.length >= 10 ? 'high' : samples.length >= 5 ? 'medium' : 'low';
99
+ // Disclose range when stddev is a large fraction of the mean.
100
+ // Threshold chosen at 50% — beyond that, a single point estimate
101
+ // hides too much.
102
+ return std / mean > 0.5
103
+ ? {
104
+ pointMs: mean,
105
+ sampleSize: samples.length,
106
+ confidence,
107
+ rangeMs: {
108
+ lowMs: Math.max(0, mean - std),
109
+ highMs: mean + std,
110
+ },
111
+ }
112
+ : { pointMs: mean, sampleSize: samples.length, confidence };
113
+ }
@@ -0,0 +1,6 @@
1
+ // Job duration estimation interface. The system stays "blackbox" by
2
+ // design — outside callers only touch the registry's classify() /
3
+ // estimate() functions. Each kind plugs in via a self-contained file
4
+ // in src/estimates/<kind>.ts that calls registerEstimator() at module
5
+ // load.
6
+ export {};
@@ -5,6 +5,7 @@ import { getSession } from '../ai/sessions.js';
5
5
  import { formatAddress, jidToAddress } from '../db/address.js';
6
6
  import { personIdForAddress } from '../db/identity-sync.js';
7
7
  import { config } from '../config.js';
8
+ import { estimate as estimateJob } from '../estimates/index.js';
8
9
  import { logger } from '../logger.js';
9
10
  import { buildMemoryPreamble } from '../memory/preamble.js';
10
11
  import { enqueueInbound } from '../queue/inbound.js';
@@ -216,12 +217,30 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
216
217
  const actorPersonId = senderAddress
217
218
  ? personIdForAddress(senderAddress)
218
219
  : null;
219
- // For media-bearing messages, send an immediate "looking…" ack
220
- // via outbound so the user isn't left wondering whether the bot
221
- // saw the image (typing indicator was dropped in Phase 4 —
222
- // followup commit will reinstate via ChannelAdapter.sendTyping).
223
- // The chat worker still processes the actual reply normally.
224
- if (media && config.reply.ackOnMedia !== false) {
220
+ // Estimator: classify this message and, when a kind matches,
221
+ // (a) tag the inbound row so future estimates of the same kind
222
+ // get a fresh sample, and (b) send the estimate text as an
223
+ // immediate ack so the user sees a timeline before the agent
224
+ // even starts.
225
+ const est = estimateJob({
226
+ description: stored.text,
227
+ attachments: media ? [{ kind: media.mediaType }] : undefined,
228
+ senderPersonId: actorPersonId ?? undefined,
229
+ });
230
+ const jobKind = est?.kind ?? null;
231
+ if (est) {
232
+ enqueueOutbound({
233
+ address: chatAddress,
234
+ kind: 'text',
235
+ text: est.text,
236
+ idempotencyKey: `estimate-${msg.key.id}`,
237
+ });
238
+ }
239
+ else if (media && config.reply.ackOnMedia !== false) {
240
+ // Fallback media-ack when no estimator matched — keeps the
241
+ // pre-estimator behavior so image messages still get the
242
+ // "looking…" hint. A future MediaIncomingEstimator can replace
243
+ // this with a real average.
225
244
  enqueueOutbound({
226
245
  address: chatAddress,
227
246
  kind: 'text',
@@ -238,6 +257,7 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
238
257
  text: stored.text,
239
258
  pushName: stored.pushName ?? null,
240
259
  triggerReason,
260
+ kind: jobKind,
241
261
  receivedAt: stored.timestamp,
242
262
  payload: job,
243
263
  });
@@ -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
+ kind: input.kind ?? null,
40
41
  payload: input.payload === undefined ? null : JSON.stringify(input.payload),
41
42
  status: 'pending',
42
43
  attempts: 0,
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `inbound` ADD `kind` text;--> statement-breakpoint
2
+ CREATE INDEX `inbound_by_kind_done` ON `inbound` (`kind`,`status`);
@@ -0,0 +1,924 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "6ac269f9-97a4-4ab9-a44d-de8b487add11",
5
+ "prevId": "4858c530-a743-4923-8a2a-eccbfa960447",
6
+ "tables": {
7
+ "browser_tasks": {
8
+ "name": "browser_tasks",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "integer",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": true
16
+ },
17
+ "address": {
18
+ "name": "address",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "actor_person_id": {
25
+ "name": "actor_person_id",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "description": {
32
+ "name": "description",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "originating_message": {
39
+ "name": "originating_message",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": true,
43
+ "autoincrement": false
44
+ },
45
+ "sender_number": {
46
+ "name": "sender_number",
47
+ "type": "text",
48
+ "primaryKey": false,
49
+ "notNull": true,
50
+ "autoincrement": false
51
+ },
52
+ "sender_name": {
53
+ "name": "sender_name",
54
+ "type": "text",
55
+ "primaryKey": false,
56
+ "notNull": false,
57
+ "autoincrement": false
58
+ },
59
+ "allowed_tools": {
60
+ "name": "allowed_tools",
61
+ "type": "text",
62
+ "primaryKey": false,
63
+ "notNull": false,
64
+ "autoincrement": false
65
+ },
66
+ "status": {
67
+ "name": "status",
68
+ "type": "text",
69
+ "primaryKey": false,
70
+ "notNull": true,
71
+ "autoincrement": false
72
+ },
73
+ "attempts": {
74
+ "name": "attempts",
75
+ "type": "integer",
76
+ "primaryKey": false,
77
+ "notNull": true,
78
+ "autoincrement": false,
79
+ "default": 0
80
+ },
81
+ "next_attempt_at": {
82
+ "name": "next_attempt_at",
83
+ "type": "integer",
84
+ "primaryKey": false,
85
+ "notNull": false,
86
+ "autoincrement": false
87
+ },
88
+ "last_error": {
89
+ "name": "last_error",
90
+ "type": "text",
91
+ "primaryKey": false,
92
+ "notNull": false,
93
+ "autoincrement": false
94
+ },
95
+ "claimed_by": {
96
+ "name": "claimed_by",
97
+ "type": "text",
98
+ "primaryKey": false,
99
+ "notNull": false,
100
+ "autoincrement": false
101
+ },
102
+ "claimed_at": {
103
+ "name": "claimed_at",
104
+ "type": "integer",
105
+ "primaryKey": false,
106
+ "notNull": false,
107
+ "autoincrement": false
108
+ },
109
+ "created_at": {
110
+ "name": "created_at",
111
+ "type": "integer",
112
+ "primaryKey": false,
113
+ "notNull": true,
114
+ "autoincrement": false
115
+ },
116
+ "updated_at": {
117
+ "name": "updated_at",
118
+ "type": "integer",
119
+ "primaryKey": false,
120
+ "notNull": true,
121
+ "autoincrement": false
122
+ }
123
+ },
124
+ "indexes": {
125
+ "btasks_by_status_next": {
126
+ "name": "btasks_by_status_next",
127
+ "columns": [
128
+ "status",
129
+ "next_attempt_at"
130
+ ],
131
+ "isUnique": false
132
+ }
133
+ },
134
+ "foreignKeys": {},
135
+ "compositePrimaryKeys": {},
136
+ "uniqueConstraints": {},
137
+ "checkConstraints": {}
138
+ },
139
+ "control": {
140
+ "name": "control",
141
+ "columns": {
142
+ "key": {
143
+ "name": "key",
144
+ "type": "text",
145
+ "primaryKey": true,
146
+ "notNull": true,
147
+ "autoincrement": false
148
+ },
149
+ "value": {
150
+ "name": "value",
151
+ "type": "text",
152
+ "primaryKey": false,
153
+ "notNull": false,
154
+ "autoincrement": false
155
+ },
156
+ "requested_by": {
157
+ "name": "requested_by",
158
+ "type": "text",
159
+ "primaryKey": false,
160
+ "notNull": false,
161
+ "autoincrement": false
162
+ },
163
+ "requested_at": {
164
+ "name": "requested_at",
165
+ "type": "integer",
166
+ "primaryKey": false,
167
+ "notNull": true,
168
+ "autoincrement": false
169
+ }
170
+ },
171
+ "indexes": {},
172
+ "foreignKeys": {},
173
+ "compositePrimaryKeys": {},
174
+ "uniqueConstraints": {},
175
+ "checkConstraints": {}
176
+ },
177
+ "crons": {
178
+ "name": "crons",
179
+ "columns": {
180
+ "id": {
181
+ "name": "id",
182
+ "type": "integer",
183
+ "primaryKey": true,
184
+ "notNull": true,
185
+ "autoincrement": true
186
+ },
187
+ "name": {
188
+ "name": "name",
189
+ "type": "text",
190
+ "primaryKey": false,
191
+ "notNull": true,
192
+ "autoincrement": false
193
+ },
194
+ "enqueue_into": {
195
+ "name": "enqueue_into",
196
+ "type": "text",
197
+ "primaryKey": false,
198
+ "notNull": true,
199
+ "autoincrement": false
200
+ },
201
+ "payload": {
202
+ "name": "payload",
203
+ "type": "text",
204
+ "primaryKey": false,
205
+ "notNull": true,
206
+ "autoincrement": false
207
+ },
208
+ "recurrence": {
209
+ "name": "recurrence",
210
+ "type": "text",
211
+ "primaryKey": false,
212
+ "notNull": false,
213
+ "autoincrement": false
214
+ },
215
+ "next_run_at": {
216
+ "name": "next_run_at",
217
+ "type": "integer",
218
+ "primaryKey": false,
219
+ "notNull": true,
220
+ "autoincrement": false
221
+ },
222
+ "last_run_at": {
223
+ "name": "last_run_at",
224
+ "type": "integer",
225
+ "primaryKey": false,
226
+ "notNull": false,
227
+ "autoincrement": false
228
+ },
229
+ "enabled": {
230
+ "name": "enabled",
231
+ "type": "integer",
232
+ "primaryKey": false,
233
+ "notNull": true,
234
+ "autoincrement": false,
235
+ "default": 1
236
+ },
237
+ "created_at": {
238
+ "name": "created_at",
239
+ "type": "integer",
240
+ "primaryKey": false,
241
+ "notNull": true,
242
+ "autoincrement": false
243
+ }
244
+ },
245
+ "indexes": {
246
+ "crons_by_due": {
247
+ "name": "crons_by_due",
248
+ "columns": [
249
+ "enabled",
250
+ "next_run_at"
251
+ ],
252
+ "isUnique": false
253
+ },
254
+ "crons_name_uq": {
255
+ "name": "crons_name_uq",
256
+ "columns": [
257
+ "name"
258
+ ],
259
+ "isUnique": true,
260
+ "where": "\"crons\".\"recurrence\" IS NOT NULL"
261
+ }
262
+ },
263
+ "foreignKeys": {},
264
+ "compositePrimaryKeys": {},
265
+ "uniqueConstraints": {},
266
+ "checkConstraints": {}
267
+ },
268
+ "identities": {
269
+ "name": "identities",
270
+ "columns": {
271
+ "person_id": {
272
+ "name": "person_id",
273
+ "type": "text",
274
+ "primaryKey": false,
275
+ "notNull": true,
276
+ "autoincrement": false
277
+ },
278
+ "address": {
279
+ "name": "address",
280
+ "type": "text",
281
+ "primaryKey": false,
282
+ "notNull": true,
283
+ "autoincrement": false
284
+ },
285
+ "added_at": {
286
+ "name": "added_at",
287
+ "type": "integer",
288
+ "primaryKey": false,
289
+ "notNull": true,
290
+ "autoincrement": false
291
+ }
292
+ },
293
+ "indexes": {
294
+ "identities_address_unique": {
295
+ "name": "identities_address_unique",
296
+ "columns": [
297
+ "address"
298
+ ],
299
+ "isUnique": true
300
+ }
301
+ },
302
+ "foreignKeys": {
303
+ "identities_person_id_persons_id_fk": {
304
+ "name": "identities_person_id_persons_id_fk",
305
+ "tableFrom": "identities",
306
+ "tableTo": "persons",
307
+ "columnsFrom": [
308
+ "person_id"
309
+ ],
310
+ "columnsTo": [
311
+ "id"
312
+ ],
313
+ "onDelete": "no action",
314
+ "onUpdate": "no action"
315
+ }
316
+ },
317
+ "compositePrimaryKeys": {
318
+ "identities_person_id_address_pk": {
319
+ "columns": [
320
+ "person_id",
321
+ "address"
322
+ ],
323
+ "name": "identities_person_id_address_pk"
324
+ }
325
+ },
326
+ "uniqueConstraints": {},
327
+ "checkConstraints": {}
328
+ },
329
+ "inbound": {
330
+ "name": "inbound",
331
+ "columns": {
332
+ "id": {
333
+ "name": "id",
334
+ "type": "integer",
335
+ "primaryKey": true,
336
+ "notNull": true,
337
+ "autoincrement": true
338
+ },
339
+ "address": {
340
+ "name": "address",
341
+ "type": "text",
342
+ "primaryKey": false,
343
+ "notNull": true,
344
+ "autoincrement": false
345
+ },
346
+ "actor_address": {
347
+ "name": "actor_address",
348
+ "type": "text",
349
+ "primaryKey": false,
350
+ "notNull": false,
351
+ "autoincrement": false
352
+ },
353
+ "person_id": {
354
+ "name": "person_id",
355
+ "type": "text",
356
+ "primaryKey": false,
357
+ "notNull": false,
358
+ "autoincrement": false
359
+ },
360
+ "actor_person_id": {
361
+ "name": "actor_person_id",
362
+ "type": "text",
363
+ "primaryKey": false,
364
+ "notNull": false,
365
+ "autoincrement": false
366
+ },
367
+ "external_msg_id": {
368
+ "name": "external_msg_id",
369
+ "type": "text",
370
+ "primaryKey": false,
371
+ "notNull": false,
372
+ "autoincrement": false
373
+ },
374
+ "text": {
375
+ "name": "text",
376
+ "type": "text",
377
+ "primaryKey": false,
378
+ "notNull": true,
379
+ "autoincrement": false
380
+ },
381
+ "media_path": {
382
+ "name": "media_path",
383
+ "type": "text",
384
+ "primaryKey": false,
385
+ "notNull": false,
386
+ "autoincrement": false
387
+ },
388
+ "media_mime": {
389
+ "name": "media_mime",
390
+ "type": "text",
391
+ "primaryKey": false,
392
+ "notNull": false,
393
+ "autoincrement": false
394
+ },
395
+ "media_bytes": {
396
+ "name": "media_bytes",
397
+ "type": "integer",
398
+ "primaryKey": false,
399
+ "notNull": false,
400
+ "autoincrement": false
401
+ },
402
+ "push_name": {
403
+ "name": "push_name",
404
+ "type": "text",
405
+ "primaryKey": false,
406
+ "notNull": false,
407
+ "autoincrement": false
408
+ },
409
+ "trigger_reason": {
410
+ "name": "trigger_reason",
411
+ "type": "text",
412
+ "primaryKey": false,
413
+ "notNull": false,
414
+ "autoincrement": false
415
+ },
416
+ "kind": {
417
+ "name": "kind",
418
+ "type": "text",
419
+ "primaryKey": false,
420
+ "notNull": false,
421
+ "autoincrement": false
422
+ },
423
+ "payload": {
424
+ "name": "payload",
425
+ "type": "text",
426
+ "primaryKey": false,
427
+ "notNull": false,
428
+ "autoincrement": false
429
+ },
430
+ "status": {
431
+ "name": "status",
432
+ "type": "text",
433
+ "primaryKey": false,
434
+ "notNull": true,
435
+ "autoincrement": false
436
+ },
437
+ "attempts": {
438
+ "name": "attempts",
439
+ "type": "integer",
440
+ "primaryKey": false,
441
+ "notNull": true,
442
+ "autoincrement": false,
443
+ "default": 0
444
+ },
445
+ "next_attempt_at": {
446
+ "name": "next_attempt_at",
447
+ "type": "integer",
448
+ "primaryKey": false,
449
+ "notNull": false,
450
+ "autoincrement": false
451
+ },
452
+ "last_error": {
453
+ "name": "last_error",
454
+ "type": "text",
455
+ "primaryKey": false,
456
+ "notNull": false,
457
+ "autoincrement": false
458
+ },
459
+ "claimed_by": {
460
+ "name": "claimed_by",
461
+ "type": "text",
462
+ "primaryKey": false,
463
+ "notNull": false,
464
+ "autoincrement": false
465
+ },
466
+ "claimed_at": {
467
+ "name": "claimed_at",
468
+ "type": "integer",
469
+ "primaryKey": false,
470
+ "notNull": false,
471
+ "autoincrement": false
472
+ },
473
+ "received_at": {
474
+ "name": "received_at",
475
+ "type": "integer",
476
+ "primaryKey": false,
477
+ "notNull": true,
478
+ "autoincrement": false
479
+ },
480
+ "created_at": {
481
+ "name": "created_at",
482
+ "type": "integer",
483
+ "primaryKey": false,
484
+ "notNull": true,
485
+ "autoincrement": false
486
+ },
487
+ "updated_at": {
488
+ "name": "updated_at",
489
+ "type": "integer",
490
+ "primaryKey": false,
491
+ "notNull": true,
492
+ "autoincrement": false
493
+ }
494
+ },
495
+ "indexes": {
496
+ "inbound_by_status_next": {
497
+ "name": "inbound_by_status_next",
498
+ "columns": [
499
+ "status",
500
+ "next_attempt_at"
501
+ ],
502
+ "isUnique": false
503
+ },
504
+ "inbound_by_address": {
505
+ "name": "inbound_by_address",
506
+ "columns": [
507
+ "address"
508
+ ],
509
+ "isUnique": false
510
+ },
511
+ "inbound_by_person": {
512
+ "name": "inbound_by_person",
513
+ "columns": [
514
+ "person_id",
515
+ "received_at"
516
+ ],
517
+ "isUnique": false
518
+ },
519
+ "inbound_by_kind_done": {
520
+ "name": "inbound_by_kind_done",
521
+ "columns": [
522
+ "kind",
523
+ "status"
524
+ ],
525
+ "isUnique": false
526
+ },
527
+ "inbound_external_msg_id_uq": {
528
+ "name": "inbound_external_msg_id_uq",
529
+ "columns": [
530
+ "external_msg_id"
531
+ ],
532
+ "isUnique": true,
533
+ "where": "\"inbound\".\"external_msg_id\" IS NOT NULL"
534
+ }
535
+ },
536
+ "foreignKeys": {},
537
+ "compositePrimaryKeys": {},
538
+ "uniqueConstraints": {},
539
+ "checkConstraints": {}
540
+ },
541
+ "memory_writes": {
542
+ "name": "memory_writes",
543
+ "columns": {
544
+ "id": {
545
+ "name": "id",
546
+ "type": "integer",
547
+ "primaryKey": true,
548
+ "notNull": true,
549
+ "autoincrement": true
550
+ },
551
+ "op": {
552
+ "name": "op",
553
+ "type": "text",
554
+ "primaryKey": false,
555
+ "notNull": true,
556
+ "autoincrement": false
557
+ },
558
+ "payload": {
559
+ "name": "payload",
560
+ "type": "text",
561
+ "primaryKey": false,
562
+ "notNull": true,
563
+ "autoincrement": false
564
+ },
565
+ "idempotency_key": {
566
+ "name": "idempotency_key",
567
+ "type": "text",
568
+ "primaryKey": false,
569
+ "notNull": false,
570
+ "autoincrement": false
571
+ },
572
+ "status": {
573
+ "name": "status",
574
+ "type": "text",
575
+ "primaryKey": false,
576
+ "notNull": true,
577
+ "autoincrement": false
578
+ },
579
+ "attempts": {
580
+ "name": "attempts",
581
+ "type": "integer",
582
+ "primaryKey": false,
583
+ "notNull": true,
584
+ "autoincrement": false,
585
+ "default": 0
586
+ },
587
+ "next_attempt_at": {
588
+ "name": "next_attempt_at",
589
+ "type": "integer",
590
+ "primaryKey": false,
591
+ "notNull": false,
592
+ "autoincrement": false
593
+ },
594
+ "last_error": {
595
+ "name": "last_error",
596
+ "type": "text",
597
+ "primaryKey": false,
598
+ "notNull": false,
599
+ "autoincrement": false
600
+ },
601
+ "claimed_by": {
602
+ "name": "claimed_by",
603
+ "type": "text",
604
+ "primaryKey": false,
605
+ "notNull": false,
606
+ "autoincrement": false
607
+ },
608
+ "claimed_at": {
609
+ "name": "claimed_at",
610
+ "type": "integer",
611
+ "primaryKey": false,
612
+ "notNull": false,
613
+ "autoincrement": false
614
+ },
615
+ "created_at": {
616
+ "name": "created_at",
617
+ "type": "integer",
618
+ "primaryKey": false,
619
+ "notNull": true,
620
+ "autoincrement": false
621
+ },
622
+ "updated_at": {
623
+ "name": "updated_at",
624
+ "type": "integer",
625
+ "primaryKey": false,
626
+ "notNull": true,
627
+ "autoincrement": false
628
+ }
629
+ },
630
+ "indexes": {
631
+ "memwr_by_status_next": {
632
+ "name": "memwr_by_status_next",
633
+ "columns": [
634
+ "status",
635
+ "next_attempt_at"
636
+ ],
637
+ "isUnique": false
638
+ },
639
+ "memwr_idemp_uq": {
640
+ "name": "memwr_idemp_uq",
641
+ "columns": [
642
+ "idempotency_key"
643
+ ],
644
+ "isUnique": true,
645
+ "where": "\"memory_writes\".\"idempotency_key\" IS NOT NULL"
646
+ }
647
+ },
648
+ "foreignKeys": {},
649
+ "compositePrimaryKeys": {},
650
+ "uniqueConstraints": {},
651
+ "checkConstraints": {}
652
+ },
653
+ "outbound": {
654
+ "name": "outbound",
655
+ "columns": {
656
+ "id": {
657
+ "name": "id",
658
+ "type": "integer",
659
+ "primaryKey": true,
660
+ "notNull": true,
661
+ "autoincrement": true
662
+ },
663
+ "address": {
664
+ "name": "address",
665
+ "type": "text",
666
+ "primaryKey": false,
667
+ "notNull": true,
668
+ "autoincrement": false
669
+ },
670
+ "kind": {
671
+ "name": "kind",
672
+ "type": "text",
673
+ "primaryKey": false,
674
+ "notNull": true,
675
+ "autoincrement": false
676
+ },
677
+ "text": {
678
+ "name": "text",
679
+ "type": "text",
680
+ "primaryKey": false,
681
+ "notNull": false,
682
+ "autoincrement": false
683
+ },
684
+ "media_path": {
685
+ "name": "media_path",
686
+ "type": "text",
687
+ "primaryKey": false,
688
+ "notNull": false,
689
+ "autoincrement": false
690
+ },
691
+ "media_mime": {
692
+ "name": "media_mime",
693
+ "type": "text",
694
+ "primaryKey": false,
695
+ "notNull": false,
696
+ "autoincrement": false
697
+ },
698
+ "media_bytes": {
699
+ "name": "media_bytes",
700
+ "type": "integer",
701
+ "primaryKey": false,
702
+ "notNull": false,
703
+ "autoincrement": false
704
+ },
705
+ "quote_msg_id": {
706
+ "name": "quote_msg_id",
707
+ "type": "text",
708
+ "primaryKey": false,
709
+ "notNull": false,
710
+ "autoincrement": false
711
+ },
712
+ "idempotency_key": {
713
+ "name": "idempotency_key",
714
+ "type": "text",
715
+ "primaryKey": false,
716
+ "notNull": false,
717
+ "autoincrement": false
718
+ },
719
+ "status": {
720
+ "name": "status",
721
+ "type": "text",
722
+ "primaryKey": false,
723
+ "notNull": true,
724
+ "autoincrement": false
725
+ },
726
+ "attempts": {
727
+ "name": "attempts",
728
+ "type": "integer",
729
+ "primaryKey": false,
730
+ "notNull": true,
731
+ "autoincrement": false,
732
+ "default": 0
733
+ },
734
+ "next_attempt_at": {
735
+ "name": "next_attempt_at",
736
+ "type": "integer",
737
+ "primaryKey": false,
738
+ "notNull": false,
739
+ "autoincrement": false
740
+ },
741
+ "last_error": {
742
+ "name": "last_error",
743
+ "type": "text",
744
+ "primaryKey": false,
745
+ "notNull": false,
746
+ "autoincrement": false
747
+ },
748
+ "claimed_by": {
749
+ "name": "claimed_by",
750
+ "type": "text",
751
+ "primaryKey": false,
752
+ "notNull": false,
753
+ "autoincrement": false
754
+ },
755
+ "claimed_at": {
756
+ "name": "claimed_at",
757
+ "type": "integer",
758
+ "primaryKey": false,
759
+ "notNull": false,
760
+ "autoincrement": false
761
+ },
762
+ "created_at": {
763
+ "name": "created_at",
764
+ "type": "integer",
765
+ "primaryKey": false,
766
+ "notNull": true,
767
+ "autoincrement": false
768
+ },
769
+ "updated_at": {
770
+ "name": "updated_at",
771
+ "type": "integer",
772
+ "primaryKey": false,
773
+ "notNull": true,
774
+ "autoincrement": false
775
+ }
776
+ },
777
+ "indexes": {
778
+ "outbound_by_status_next": {
779
+ "name": "outbound_by_status_next",
780
+ "columns": [
781
+ "status",
782
+ "next_attempt_at"
783
+ ],
784
+ "isUnique": false
785
+ },
786
+ "outbound_by_address": {
787
+ "name": "outbound_by_address",
788
+ "columns": [
789
+ "address"
790
+ ],
791
+ "isUnique": false
792
+ },
793
+ "outbound_idempotency_key_uq": {
794
+ "name": "outbound_idempotency_key_uq",
795
+ "columns": [
796
+ "idempotency_key"
797
+ ],
798
+ "isUnique": true,
799
+ "where": "\"outbound\".\"idempotency_key\" IS NOT NULL"
800
+ }
801
+ },
802
+ "foreignKeys": {},
803
+ "compositePrimaryKeys": {},
804
+ "uniqueConstraints": {},
805
+ "checkConstraints": {}
806
+ },
807
+ "persons": {
808
+ "name": "persons",
809
+ "columns": {
810
+ "id": {
811
+ "name": "id",
812
+ "type": "text",
813
+ "primaryKey": true,
814
+ "notNull": true,
815
+ "autoincrement": false
816
+ },
817
+ "display_name": {
818
+ "name": "display_name",
819
+ "type": "text",
820
+ "primaryKey": false,
821
+ "notNull": false,
822
+ "autoincrement": false
823
+ },
824
+ "timezone": {
825
+ "name": "timezone",
826
+ "type": "text",
827
+ "primaryKey": false,
828
+ "notNull": false,
829
+ "autoincrement": false
830
+ },
831
+ "created_at": {
832
+ "name": "created_at",
833
+ "type": "integer",
834
+ "primaryKey": false,
835
+ "notNull": true,
836
+ "autoincrement": false
837
+ }
838
+ },
839
+ "indexes": {},
840
+ "foreignKeys": {},
841
+ "compositePrimaryKeys": {},
842
+ "uniqueConstraints": {},
843
+ "checkConstraints": {}
844
+ },
845
+ "workers": {
846
+ "name": "workers",
847
+ "columns": {
848
+ "id": {
849
+ "name": "id",
850
+ "type": "text",
851
+ "primaryKey": true,
852
+ "notNull": true,
853
+ "autoincrement": false
854
+ },
855
+ "kind": {
856
+ "name": "kind",
857
+ "type": "text",
858
+ "primaryKey": false,
859
+ "notNull": true,
860
+ "autoincrement": false
861
+ },
862
+ "status": {
863
+ "name": "status",
864
+ "type": "text",
865
+ "primaryKey": false,
866
+ "notNull": true,
867
+ "autoincrement": false
868
+ },
869
+ "current_job": {
870
+ "name": "current_job",
871
+ "type": "text",
872
+ "primaryKey": false,
873
+ "notNull": false,
874
+ "autoincrement": false
875
+ },
876
+ "last_seen": {
877
+ "name": "last_seen",
878
+ "type": "integer",
879
+ "primaryKey": false,
880
+ "notNull": true,
881
+ "autoincrement": false
882
+ },
883
+ "started_at": {
884
+ "name": "started_at",
885
+ "type": "integer",
886
+ "primaryKey": false,
887
+ "notNull": true,
888
+ "autoincrement": false
889
+ }
890
+ },
891
+ "indexes": {
892
+ "workers_by_kind_status": {
893
+ "name": "workers_by_kind_status",
894
+ "columns": [
895
+ "kind",
896
+ "status"
897
+ ],
898
+ "isUnique": false
899
+ },
900
+ "workers_by_last_seen": {
901
+ "name": "workers_by_last_seen",
902
+ "columns": [
903
+ "last_seen"
904
+ ],
905
+ "isUnique": false
906
+ }
907
+ },
908
+ "foreignKeys": {},
909
+ "compositePrimaryKeys": {},
910
+ "uniqueConstraints": {},
911
+ "checkConstraints": {}
912
+ }
913
+ },
914
+ "views": {},
915
+ "enums": {},
916
+ "_meta": {
917
+ "schemas": {},
918
+ "tables": {},
919
+ "columns": {}
920
+ },
921
+ "internal": {
922
+ "indexes": {}
923
+ }
924
+ }
@@ -50,6 +50,13 @@
50
50
  "when": 1779677301352,
51
51
  "tag": "0006_phase4_browser_tasks",
52
52
  "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1779681183906,
58
+ "tag": "0007_estimates_kind",
59
+ "breakpoints": true
53
60
  }
54
61
  ]
55
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
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",