@c4t4/heyamigo 0.9.20 → 0.9.21

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.
@@ -4,6 +4,7 @@ import { config } from '../config.js';
4
4
  import { formatAddress, jidToAddress } from '../db/address.js';
5
5
  import { logger } from '../logger.js';
6
6
  import { addDailyTokens } from '../store/usage.js';
7
+ import { getTimezoneForSenderNumber } from '../db/identity-sync.js';
7
8
  import { estimate as estimateJob } from '../estimates/index.js';
8
9
  import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
9
10
  import { isValidSlug } from '../memory/journals.js';
@@ -11,6 +12,7 @@ import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
11
12
  import { enqueueCron } from './crons.js';
12
13
  import { enqueueMemoryWrite } from './memory-writes.js';
13
14
  import { enqueueOutbound } from './outbound.js';
15
+ import { formatLocalTime, resolveTimeExpression } from './time-expr.js';
14
16
  function isStaleSessionError(err) {
15
17
  return (err instanceof Error &&
16
18
  err.message.includes('No conversation found'));
@@ -234,8 +236,12 @@ async function callClaude(job) {
234
236
  }
235
237
  // [CRON: @every X — body] and [REMIND: in Nu — body] create cron
236
238
  // rows that fire into outbound at their scheduled time. The
237
- // originating chat (job.jid) is the destination for both.
239
+ // originating chat (job.jid) is the destination for both. Sender's
240
+ // timezone drives "9am" / "today at..." resolution so the schedule
241
+ // lands in their wall-clock time, not the server's.
238
242
  const chatAddress = formatAddress(jidToAddress(job.jid));
243
+ const senderTz = getTimezoneForSenderNumber(job.senderNumber);
244
+ const nowSec = Math.floor(Date.now() / 1000);
239
245
  const cronBase = `chat-cron-${job.jid}-${Date.now()}`;
240
246
  for (let i = 0; i < crons.length; i++) {
241
247
  const c = crons[i];
@@ -245,24 +251,50 @@ async function callClaude(job) {
245
251
  enqueueInto: 'outbound',
246
252
  payload: { address: chatAddress, kind: 'text', text: c.body },
247
253
  recurrence: c.recurrence,
254
+ // Sender's local timezone so @daily HH:MM fires in their
255
+ // wall-clock time, not the server's.
256
+ timezone: senderTz,
248
257
  });
249
- logger.info({ jid: job.jid, recurrence: c.recurrence, chars: c.body.length }, 'CRON tag scheduled');
258
+ logger.info({
259
+ jid: job.jid,
260
+ recurrence: c.recurrence,
261
+ tz: senderTz,
262
+ chars: c.body.length,
263
+ }, 'CRON tag scheduled');
250
264
  }
251
265
  catch (err) {
252
266
  logger.warn({ err, jid: job.jid, recurrence: c.recurrence }, 'CRON tag failed (bad recurrence?)');
253
267
  }
254
268
  }
269
+ // REMIND resolution uses the same senderTz computed above.
255
270
  const remindBase = `chat-remind-${job.jid}-${Date.now()}`;
256
271
  for (let i = 0; i < reminds.length; i++) {
257
272
  const r = reminds[i];
273
+ let firstRunAt;
274
+ try {
275
+ firstRunAt = resolveTimeExpression(r.when, senderTz, nowSec);
276
+ }
277
+ catch (err) {
278
+ logger.warn({ err, jid: job.jid, when: r.when }, 'REMIND time resolution failed');
279
+ continue;
280
+ }
281
+ if (firstRunAt <= nowSec) {
282
+ logger.warn({ jid: job.jid, when: r.when, firstRunAt }, 'REMIND resolved to past — skipped');
283
+ continue;
284
+ }
258
285
  enqueueCron({
259
286
  name: `${remindBase}-${i}`,
260
287
  enqueueInto: 'outbound',
261
288
  payload: { address: chatAddress, kind: 'text', text: r.body },
262
289
  recurrence: null,
263
- firstRunAt: Math.floor(Date.now() / 1000) + r.whenSecondsFromNow,
290
+ firstRunAt,
264
291
  });
265
- logger.info({ jid: job.jid, inSeconds: r.whenSecondsFromNow, chars: r.body.length }, 'REMIND tag scheduled');
292
+ logger.info({
293
+ jid: job.jid,
294
+ fires: formatLocalTime(firstRunAt, senderTz),
295
+ tz: senderTz,
296
+ chars: r.body.length,
297
+ }, 'REMIND tag scheduled');
266
298
  }
267
299
  return {
268
300
  reply: clean,
@@ -0,0 +1 @@
1
+ ALTER TABLE `crons` ADD `timezone` text;