@c4t4/heyamigo 0.9.24 → 0.10.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.
@@ -10,6 +10,8 @@ import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
10
10
  import { isValidSlug } from '../memory/journals.js';
11
11
  import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
12
12
  import { addCronUsage, enqueueCron } from './crons.js';
13
+ import { compressThread, coolThread, createThread, dropThread, resolveThread, touchThread, updateThread, } from './threads.js';
14
+ import { setCategoryWeight } from './thread-weights.js';
13
15
  import { enqueueMemoryWrite } from './memory-writes.js';
14
16
  import { enqueueOutbound } from './outbound.js';
15
17
  import { formatLocalTime, resolveTimeExpression } from './time-expr.js';
@@ -111,7 +113,7 @@ async function callClaude(job) {
111
113
  addCronUsage(job.cronId, turnInput, turnOutput);
112
114
  }
113
115
  const rawFlags = extractFlags(reply);
114
- const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, crons, reminds, } = filterFlagsByRole(rawFlags, job.allowedTags);
116
+ const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, crons, reminds, threadNews, threadUpdates, threadTouches, threadCools, threadResolves, threadDrops, threadCompresses, threadWeights, } = filterFlagsByRole(rawFlags, job.allowedTags);
115
117
  // Detect any stripped tags so we can log + nudge the role config
116
118
  // if a user is repeatedly hitting the gate.
117
119
  const stripped = [];
@@ -127,6 +129,18 @@ async function callClaude(job) {
127
129
  stripped.push('ASYNC-BROWSER');
128
130
  if (rawFlags.sendTexts.length !== sendTexts.length)
129
131
  stripped.push('SEND-TEXT');
132
+ // Lump all THREAD-* into a single 'THREAD' bucket for the stripped
133
+ // log since they all share the 'THREAD' allowedTag.
134
+ const rawThreadCount = rawFlags.threadNews.length + rawFlags.threadUpdates.length +
135
+ rawFlags.threadTouches.length + rawFlags.threadCools.length +
136
+ rawFlags.threadResolves.length + rawFlags.threadDrops.length +
137
+ rawFlags.threadCompresses.length + rawFlags.threadWeights.length;
138
+ const filteredThreadCount = threadNews.length + threadUpdates.length +
139
+ threadTouches.length + threadCools.length +
140
+ threadResolves.length + threadDrops.length +
141
+ threadCompresses.length + threadWeights.length;
142
+ if (rawThreadCount !== filteredThreadCount)
143
+ stripped.push('THREAD');
130
144
  if (stripped.length > 0) {
131
145
  logger.warn({ jid: job.jid, senderNumber: job.senderNumber, stripped }, 'tags stripped by role gate');
132
146
  }
@@ -339,6 +353,93 @@ async function callClaude(job) {
339
353
  chars: r.body.length,
340
354
  }, 'REMIND tag scheduled');
341
355
  }
356
+ // THREAD-* tag side effects. Each shape lands on its matching
357
+ // CRUD helper in queue/threads.ts. Errors per-tag are logged but
358
+ // don't abort the rest of the reply pipeline.
359
+ if (config.threads?.enabled) {
360
+ for (const t of threadNews) {
361
+ try {
362
+ createThread({
363
+ targetJid: job.jid,
364
+ title: t.title,
365
+ summary: t.summary,
366
+ hotness: t.hotness,
367
+ linkedMemory: t.linkedMemory ?? null,
368
+ category: t.category,
369
+ });
370
+ }
371
+ catch (err) {
372
+ logger.warn({ err, jid: job.jid, title: t.title }, 'THREAD-NEW failed');
373
+ }
374
+ }
375
+ for (const t of threadUpdates) {
376
+ try {
377
+ updateThread({
378
+ id: t.id,
379
+ title: t.title,
380
+ summary: t.summary,
381
+ hotness: t.hotness,
382
+ linkedMemory: t.linkedMemory ?? undefined,
383
+ });
384
+ }
385
+ catch (err) {
386
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-UPDATE failed');
387
+ }
388
+ }
389
+ for (const t of threadTouches) {
390
+ try {
391
+ touchThread(t.id);
392
+ }
393
+ catch (err) {
394
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-TOUCH failed');
395
+ }
396
+ }
397
+ for (const t of threadCools) {
398
+ try {
399
+ coolThread(t.id, t.deferDays);
400
+ }
401
+ catch (err) {
402
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-COOL failed');
403
+ }
404
+ }
405
+ for (const t of threadResolves) {
406
+ try {
407
+ resolveThread(t.id, t.note);
408
+ }
409
+ catch (err) {
410
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-RESOLVE failed');
411
+ }
412
+ }
413
+ for (const t of threadDrops) {
414
+ try {
415
+ dropThread(t.id, t.note);
416
+ }
417
+ catch (err) {
418
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-DROP failed');
419
+ }
420
+ }
421
+ for (const t of threadCompresses) {
422
+ try {
423
+ compressThread(t.id, t.note);
424
+ }
425
+ catch (err) {
426
+ logger.warn({ err, jid: job.jid, id: t.id }, 'THREAD-COMPRESS failed');
427
+ }
428
+ }
429
+ for (const w of threadWeights) {
430
+ try {
431
+ setCategoryWeight(w.category, w.weight);
432
+ }
433
+ catch (err) {
434
+ logger.warn({ err, jid: job.jid, category: w.category }, 'THREAD-WEIGHT failed');
435
+ }
436
+ }
437
+ }
438
+ else if (threadNews.length + threadUpdates.length + threadTouches.length +
439
+ threadCools.length + threadResolves.length + threadDrops.length +
440
+ threadCompresses.length + threadWeights.length > 0) {
441
+ logger.warn({ jid: job.jid }, 'THREAD-* tags emitted but threads feature disabled — ignored');
442
+ }
342
443
  return {
343
444
  reply: clean,
344
445
  stats: {
@@ -353,7 +454,17 @@ async function callClaude(job) {
353
454
  fresh: wasFresh,
354
455
  hasDigest: digest !== null,
355
456
  journalSlugs: journals.map((j) => j.slug),
356
- asyncCount: asyncTasks.length + asyncBrowserTasks.length,
457
+ journalCreateCount: journalCreates.length,
458
+ asyncCount: asyncTasks.length,
459
+ asyncBrowserCount: asyncBrowserTasks.length,
460
+ remindCount: reminds.length,
461
+ cronCount: crons.length,
462
+ sendTextCount: sendTexts.length,
463
+ threadNewCount: threadNews.length,
464
+ threadResolveCount: threadResolves.length,
465
+ threadDropCount: threadDrops.length,
466
+ threadCompressCount: threadCompresses.length,
467
+ threadTouchCount: threadTouches.length,
357
468
  },
358
469
  jobCards: jobCards.length > 0 ? jobCards : undefined,
359
470
  };
@@ -0,0 +1,27 @@
1
+ CREATE TABLE `thread_category_weights` (
2
+ `category` text PRIMARY KEY NOT NULL,
3
+ `weight` integer DEFAULT 50 NOT NULL,
4
+ `samples` integer DEFAULT 0 NOT NULL,
5
+ `updated_at` integer NOT NULL
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE `threads` (
9
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
10
+ `target_jid` text NOT NULL,
11
+ `title` text NOT NULL,
12
+ `summary` text NOT NULL,
13
+ `hotness` integer DEFAULT 50 NOT NULL,
14
+ `status` text DEFAULT 'live' NOT NULL,
15
+ `linked_memory` text,
16
+ `opened_at` integer NOT NULL,
17
+ `last_touched_at` integer NOT NULL,
18
+ `next_review_at` integer NOT NULL,
19
+ `resolution_note` text,
20
+ `total_input_tokens` integer DEFAULT 0 NOT NULL,
21
+ `total_output_tokens` integer DEFAULT 0 NOT NULL,
22
+ `enabled` integer DEFAULT 1 NOT NULL,
23
+ `created_at` integer NOT NULL
24
+ );
25
+ --> statement-breakpoint
26
+ CREATE INDEX `threads_by_jid_hot` ON `threads` (`target_jid`,`status`,`hotness`);--> statement-breakpoint
27
+ CREATE INDEX `threads_by_due` ON `threads` (`enabled`,`status`,`next_review_at`);