@c4t4/heyamigo 0.9.4 → 0.9.5

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.
@@ -2,6 +2,8 @@ import fastq from 'fastq';
2
2
  import { config } from '../config.js';
3
3
  import { logger } from '../logger.js';
4
4
  import { prunePrompts } from '../promptlog.js';
5
+ import { registerInternalCronHandler } from '../queue/cron-handlers.js';
6
+ import { deleteCron, enqueueCron } from '../queue/crons.js';
5
7
  import { pruneMedia } from '../store/media.js';
6
8
  import { runDigest } from './digest.js';
7
9
  import { ensureScaffold, getLastDigestedAt, jsonlMtimeFor, loadDigestState, } from './store.js';
@@ -74,7 +76,6 @@ async function sweep() {
74
76
  }
75
77
  }
76
78
  let sweepTimer = null;
77
- let nudgeTimer = null;
78
79
  const NUDGE_TICK_MS = 5 * 60 * 1000; // 5 minutes
79
80
  export function startScheduler() {
80
81
  if (sweepTimer)
@@ -95,13 +96,17 @@ export function startScheduler() {
95
96
  sweepTimer = setInterval(() => {
96
97
  void sweep().catch((err) => logger.error({ err }, 'sweep failed'));
97
98
  }, config.memory.sweepIntervalMs);
98
- // Faster tick just for proactive journal nudges (check-ins, silent-nudges).
99
- // The memory-sweep cycle (default 3h) is too coarse for a "daily 21:00"
100
- // check-in. This tick is cheap: it only spawns Claude when something is
101
- // actually due for a journal.
102
- nudgeTimer = setInterval(() => {
103
- void runNudgeTickSafe();
104
- }, NUDGE_TICK_MS);
99
+ // Proactive journal nudges (check-ins, silent-nudges). Migrated from
100
+ // setInterval to a cron row orchestrator. Same cadence, same body;
101
+ // benefits are: survives restarts, visible in `crons` table, can be
102
+ // paused via control row without code change.
103
+ registerInternalCronHandler('journal-nudge-tick', runNudgeTickSafe);
104
+ enqueueCron({
105
+ name: 'journal-nudge-tick',
106
+ enqueueInto: 'internal',
107
+ payload: { handler: 'journal-nudge-tick' },
108
+ recurrence: `@every ${Math.floor(NUDGE_TICK_MS / 1000)}s`,
109
+ });
105
110
  logger.info({
106
111
  intervalMs: config.memory.sweepIntervalMs,
107
112
  nudgeTickMs: NUDGE_TICK_MS,
@@ -121,11 +126,18 @@ export function stopScheduler() {
121
126
  clearInterval(sweepTimer);
122
127
  sweepTimer = null;
123
128
  }
124
- if (nudgeTimer) {
125
- clearInterval(nudgeTimer);
126
- nudgeTimer = null;
127
- }
129
+ // Nudge cron is owned by the crons table; orchestrator stops on its
130
+ // own. Deleting the cron row here would re-arm itself on next boot,
131
+ // so leave it alone — disabling via the `enabled` column is the
132
+ // user-facing knob.
128
133
  for (const t of pendingTimers.values())
129
134
  clearTimeout(t);
130
135
  pendingTimers.clear();
131
136
  }
137
+ // Exported for callers (CLI, /nudge command) that want to surgically
138
+ // disable nudges without editing config. Use `setCronEnabled` from
139
+ // crons.ts for the on/off switch; this is a hard delete (regenerated
140
+ // on next startScheduler call).
141
+ export function deleteNudgeCron() {
142
+ return deleteCron('journal-nudge-tick');
143
+ }
@@ -5,6 +5,7 @@
5
5
  // in on later phases — when those queues exist, add their dispatch
6
6
  // here and a cron can fire into them with no other changes.
7
7
  import { logger } from '../logger.js';
8
+ import { getInternalCronHandler } from './cron-handlers.js';
8
9
  import { enqueueOutbound } from './outbound.js';
9
10
  export function dispatchCron(row) {
10
11
  let payload;
@@ -19,6 +20,9 @@ export function dispatchCron(row) {
19
20
  case 'outbound':
20
21
  dispatchOutbound(row, payload);
21
22
  return;
23
+ case 'internal':
24
+ dispatchInternal(row, payload);
25
+ return;
22
26
  case 'inbound':
23
27
  case 'async':
24
28
  case 'memory_writes':
@@ -28,6 +32,34 @@ export function dispatchCron(row) {
28
32
  logger.error({ name: row.name, target: row.enqueueInto }, 'cron has unknown target queue');
29
33
  }
30
34
  }
35
+ function dispatchInternal(row, payload) {
36
+ if (!payload || typeof payload !== 'object' || !('handler' in payload)) {
37
+ logger.error({ id: row.id, name: row.name }, "internal cron missing 'handler' in payload");
38
+ return;
39
+ }
40
+ const handlerName = payload.handler;
41
+ if (typeof handlerName !== 'string') {
42
+ logger.error({ id: row.id, name: row.name }, 'internal cron handler name not a string');
43
+ return;
44
+ }
45
+ const handler = getInternalCronHandler(handlerName);
46
+ if (!handler) {
47
+ logger.error({ id: row.id, name: row.name, handler: handlerName }, 'internal cron handler not registered');
48
+ return;
49
+ }
50
+ // Fire-and-forget. Handler errors are caught and logged but the
51
+ // cron row still gets marked fired so we don't stack up retries.
52
+ try {
53
+ const result = handler();
54
+ if (result && typeof result.catch === 'function') {
55
+ ;
56
+ result.catch((err) => logger.error({ err, handler: handlerName }, 'internal cron handler rejected'));
57
+ }
58
+ }
59
+ catch (err) {
60
+ logger.error({ err, handler: handlerName }, 'internal cron handler threw');
61
+ }
62
+ }
31
63
  function dispatchOutbound(row, payload) {
32
64
  if (!isOutboundPayload(payload)) {
33
65
  logger.error({ id: row.id, payload }, 'cron outbound payload malformed');
@@ -0,0 +1,25 @@
1
+ // In-process handler registry for the 'internal' cron target.
2
+ //
3
+ // Not every periodic task fits the queue→worker model. Things like
4
+ // "run the journal observer sweep" or "regenerate compressed memory"
5
+ // are pure in-process work — there's no queue to enqueue into, the
6
+ // orchestrator just needs to call a function.
7
+ //
8
+ // Mechanism: cron rows with enqueue_into='internal' carry a payload
9
+ // with `handler: <name>`. The dispatcher looks up the name in this
10
+ // registry and invokes the function. Registry is populated at boot,
11
+ // before the orchestrator starts polling.
12
+ import { logger } from '../logger.js';
13
+ const registry = new Map();
14
+ export function registerInternalCronHandler(name, handler) {
15
+ if (registry.has(name)) {
16
+ logger.warn({ name }, 'internal cron handler already registered; overwriting');
17
+ }
18
+ registry.set(name, handler);
19
+ }
20
+ export function getInternalCronHandler(name) {
21
+ return registry.get(name);
22
+ }
23
+ export function listInternalCronHandlers() {
24
+ return [...registry.keys()];
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",