@poncho-ai/cli 0.32.8 → 0.33.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/cli@0.32.8 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
2
+ > @poncho-ai/cli@0.33.1 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
3
3
  > tsup src/index.ts src/cli.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,10 +9,10 @@
9
9
  ESM Build start
10
10
  ESM dist/cli.js 94.00 B
11
11
  ESM dist/index.js 917.00 B
12
- ESM dist/run-interactive-ink-JOLJ5W33.js 56.86 KB
13
- ESM dist/chunk-VHS3K24F.js 536.15 KB
14
- ESM ⚡️ Build success in 77ms
12
+ ESM dist/run-interactive-ink-R4PHKIQR.js 56.86 KB
13
+ ESM dist/chunk-IDGGF5WH.js 543.84 KB
14
+ ESM ⚡️ Build success in 69ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 4356ms
16
+ DTS ⚡️ Build success in 4592ms
17
17
  DTS dist/cli.d.ts 20.00 B
18
- DTS dist/index.d.ts 6.85 KB
18
+ DTS dist/index.d.ts 7.01 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @poncho-ai/cli
2
2
 
3
+ ## 0.33.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`67424e0`](https://github.com/cesr/poncho-ai/commit/67424e073b2faa28a255781f91a80f4602c745e2)]:
8
+ - @poncho-ai/harness@0.32.1
9
+
10
+ ## 0.33.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [#68](https://github.com/cesr/poncho-ai/pull/68) [`5a7e370`](https://github.com/cesr/poncho-ai/commit/5a7e3700a5ee441ef41cf4dc0ca70ff90e57d282) Thanks [@cesr](https://github.com/cesr)! - Add one-off reminders: agents can dynamically set, list, and cancel reminders that fire at a specific time. Fired reminders are immediately deleted from storage. Includes polling for local dev and Vercel cron integration.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [[`5a7e370`](https://github.com/cesr/poncho-ai/commit/5a7e3700a5ee441ef41cf4dc0ca70ff90e57d282)]:
19
+ - @poncho-ai/harness@0.32.0
20
+
3
21
  ## 0.32.8
4
22
 
5
23
  ### Patch Changes
@@ -7046,7 +7046,8 @@ var buildConfigFromOnboardingAnswers = (answers) => {
7046
7046
  mcp: [],
7047
7047
  auth,
7048
7048
  storage,
7049
- telemetry
7049
+ telemetry,
7050
+ reminders: { enabled: true }
7050
7051
  };
7051
7052
  if (messagingPlatform !== "none") {
7052
7053
  const channelConfig = {
@@ -8159,6 +8160,26 @@ cron:
8159
8160
 
8160
8161
  Add \`channel: telegram\` (or another platform) to have the agent proactively send the response to all known chats on that platform. The bot must have received at least one message from each user first.
8161
8162
 
8163
+ ## Reminders
8164
+
8165
+ One-off reminders are enabled by default. The agent gets \`set_reminder\`, \`list_reminders\`, and \`cancel_reminder\` tools. Users can say things like "remind me tomorrow at 9am to check the report."
8166
+
8167
+ Configure in \`poncho.config.js\`:
8168
+
8169
+ \`\`\`javascript
8170
+ export default {
8171
+ reminders: {
8172
+ enabled: true,
8173
+ pollSchedule: '*/10 * * * *', // how often to check for due reminders
8174
+ },
8175
+ };
8176
+ \`\`\`
8177
+
8178
+ - Reminders fire via a polling loop (same interval locally and on serverless).
8179
+ - On Vercel, \`poncho build vercel\` adds a cron entry for \`/api/reminders/check\`.
8180
+ - Channel reminders (Telegram/Slack) reply in the original conversation.
8181
+ - Non-channel reminders create a new \`[reminder]\` conversation visible in the web UI.
8182
+
8162
8183
  ## Messaging (Slack)
8163
8184
 
8164
8185
  Connect your agent to Slack so it responds to @mentions:
@@ -8455,9 +8476,24 @@ var checkVercelCronDrift = async (projectDir) => {
8455
8476
  for (const [jobName, schedule] of vercelCronMap) {
8456
8477
  diffs.push(` - removed job "${jobName}" (${schedule})`);
8457
8478
  }
8479
+ try {
8480
+ const cfg = await loadPonchoConfig(projectDir);
8481
+ const reminderCron = vercelCrons.find((c) => c.path === "/api/reminders/check");
8482
+ if (cfg?.reminders?.enabled && !reminderCron) {
8483
+ diffs.push(` + missing reminders polling cron`);
8484
+ } else if (!cfg?.reminders?.enabled && reminderCron) {
8485
+ diffs.push(` - reminders polling cron present but reminders disabled`);
8486
+ } else if (cfg?.reminders?.enabled && reminderCron) {
8487
+ const expected = cfg.reminders.pollSchedule ?? "*/10 * * * *";
8488
+ if (reminderCron.schedule !== expected) {
8489
+ diffs.push(` ~ reminders poll schedule changed: "${reminderCron.schedule}" \u2192 "${expected}"`);
8490
+ }
8491
+ }
8492
+ } catch {
8493
+ }
8458
8494
  if (diffs.length > 0) {
8459
8495
  process.stderr.write(
8460
- `\u26A0 vercel.json crons are out of sync with AGENT.md:
8496
+ `\u26A0 vercel.json crons are out of sync with AGENT.md / poncho.config.js:
8461
8497
  ${diffs.join("\n")}
8462
8498
  Run \`poncho build vercel --force\` to update.
8463
8499
 
@@ -8563,6 +8599,15 @@ export default async function handler(req, res) {
8563
8599
  ],
8564
8600
  routes: [{ src: "/(.*)", dest: "/api/index.mjs" }]
8565
8601
  };
8602
+ try {
8603
+ const cfg = await loadPonchoConfig(projectDir);
8604
+ if (cfg?.reminders?.enabled) {
8605
+ const schedule = cfg.reminders.pollSchedule ?? "*/10 * * * *";
8606
+ if (!vercelCrons) vercelCrons = [];
8607
+ vercelCrons.push({ path: "/api/reminders/check", schedule });
8608
+ }
8609
+ } catch {
8610
+ }
8566
8611
  if (vercelCrons && vercelCrons.length > 0) {
8567
8612
  vercelConfig.crons = vercelCrons;
8568
8613
  }
@@ -8616,6 +8661,9 @@ export const handler = async (event = {}) => {
8616
8661
  // Create a rule for each cron job defined in AGENT.md that sends a GET request to:
8617
8662
  // /api/cron/<jobName>
8618
8663
  // Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
8664
+ //
8665
+ // Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
8666
+ // every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
8619
8667
  `,
8620
8668
  { force: options?.force, writtenPaths, baseDir: projectDir }
8621
8669
  );
@@ -11254,7 +11302,7 @@ ${resultBody}`,
11254
11302
  return;
11255
11303
  }
11256
11304
  if (pathname.startsWith("/api/")) {
11257
- const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/");
11305
+ const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/") || pathname === "/api/reminders/check";
11258
11306
  const isInternal = isInternalPath && request.method === "POST" && isValidInternalRequest(request.headers);
11259
11307
  const hasBearerToken = request.headers.authorization?.startsWith("Bearer ");
11260
11308
  const isAuthenticated = isInternal || !requireAuth || session || validateBearerToken(request.headers.authorization);
@@ -12825,8 +12873,116 @@ ${cronJob.task}`;
12825
12873
  }
12826
12874
  return;
12827
12875
  }
12876
+ if (pathname === "/api/reminders/check" && (request.method === "GET" || request.method === "POST")) {
12877
+ const result = await checkAndFireReminders();
12878
+ writeJson(response, 200, result);
12879
+ return;
12880
+ }
12828
12881
  writeJson(response, 404, { error: "Not found" });
12829
12882
  };
12883
+ const DEFAULT_POLL_SCHEDULE = "*/10 * * * *";
12884
+ const pollScheduleToMs = (schedule) => {
12885
+ const m = schedule.match(/^\*\/(\d+)\s/);
12886
+ if (m) return Number(m[1]) * 60 * 1e3;
12887
+ return 10 * 60 * 1e3;
12888
+ };
12889
+ const reminderPollSchedule = config?.reminders?.pollSchedule ?? DEFAULT_POLL_SCHEDULE;
12890
+ const reminderPollWindowMs = pollScheduleToMs(reminderPollSchedule);
12891
+ const checkAndFireReminders = async () => {
12892
+ const reminderStore = harness.reminderStore;
12893
+ if (!reminderStore) return { fired: [], count: 0, duration: 0 };
12894
+ const start = Date.now();
12895
+ const firedIds = [];
12896
+ try {
12897
+ const reminders = await reminderStore.list();
12898
+ const cutoff = Date.now() + reminderPollWindowMs;
12899
+ const due = reminders.filter((r) => r.status === "pending" && r.scheduledAt <= cutoff);
12900
+ for (const reminder of due) {
12901
+ try {
12902
+ await reminderStore.delete(reminder.id);
12903
+ const originConv = reminder.conversationId ? await conversationStore.get(reminder.conversationId) : void 0;
12904
+ const channelMeta = originConv?.channelMeta;
12905
+ const framedMessage = `[Reminder] A reminder you previously set has fired.
12906
+ Task: "${reminder.task}"
12907
+ Originally set at: ${new Date(reminder.createdAt).toISOString()}
12908
+ Scheduled for: ${new Date(reminder.scheduledAt).toISOString()}`;
12909
+ if (channelMeta) {
12910
+ const adapter = messagingAdapters.get(channelMeta.platform);
12911
+ if (adapter && originConv) {
12912
+ const historyMessages = originConv.messages ?? [];
12913
+ let assistantResponse = "";
12914
+ for await (const event of harness.runWithTelemetry({
12915
+ task: framedMessage,
12916
+ conversationId: originConv.conversationId,
12917
+ parameters: { __activeConversationId: originConv.conversationId },
12918
+ messages: historyMessages
12919
+ })) {
12920
+ if (event.type === "model:chunk") {
12921
+ assistantResponse += event.content;
12922
+ }
12923
+ }
12924
+ if (assistantResponse) {
12925
+ try {
12926
+ await adapter.sendReply(
12927
+ {
12928
+ channelId: channelMeta.channelId,
12929
+ platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId
12930
+ },
12931
+ assistantResponse
12932
+ );
12933
+ } catch (sendError) {
12934
+ console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
12935
+ }
12936
+ }
12937
+ const freshConv = await conversationStore.get(originConv.conversationId);
12938
+ if (freshConv) {
12939
+ freshConv.messages = [
12940
+ ...historyMessages,
12941
+ { role: "user", content: framedMessage },
12942
+ ...assistantResponse ? [{ role: "assistant", content: assistantResponse }] : []
12943
+ ];
12944
+ freshConv.updatedAt = Date.now();
12945
+ await conversationStore.update(freshConv);
12946
+ }
12947
+ }
12948
+ } else {
12949
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12950
+ const conversation = await conversationStore.create(
12951
+ reminder.ownerId ?? "local-owner",
12952
+ `[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`
12953
+ );
12954
+ const convId = conversation.conversationId;
12955
+ let assistantResponse = "";
12956
+ for await (const event of harness.runWithTelemetry({
12957
+ task: framedMessage,
12958
+ conversationId: convId,
12959
+ parameters: { __activeConversationId: convId },
12960
+ messages: []
12961
+ })) {
12962
+ if (event.type === "model:chunk") {
12963
+ assistantResponse += event.content;
12964
+ }
12965
+ }
12966
+ const freshConv = await conversationStore.get(convId);
12967
+ if (freshConv) {
12968
+ freshConv.messages = [
12969
+ { role: "user", content: framedMessage },
12970
+ ...assistantResponse ? [{ role: "assistant", content: assistantResponse }] : []
12971
+ ];
12972
+ freshConv.updatedAt = Date.now();
12973
+ await conversationStore.update(freshConv);
12974
+ }
12975
+ }
12976
+ firedIds.push(reminder.id);
12977
+ } catch (err) {
12978
+ console.error(`[reminder] Failed to fire reminder "${reminder.id}":`, err instanceof Error ? err.message : err);
12979
+ }
12980
+ }
12981
+ } catch (err) {
12982
+ console.error("[reminder] Error checking reminders:", err instanceof Error ? err.message : err);
12983
+ }
12984
+ return { fired: firedIds, count: firedIds.length, duration: Date.now() - start };
12985
+ };
12830
12986
  handler._harness = harness;
12831
12987
  handler._cronJobs = cronJobs;
12832
12988
  handler._conversationStore = conversationStore;
@@ -12836,6 +12992,8 @@ ${cronJob.task}`;
12836
12992
  handler._processSubagentCallback = processSubagentCallback;
12837
12993
  handler._broadcastEvent = broadcastEvent;
12838
12994
  handler._finishConversationStream = finishConversationStream;
12995
+ handler._checkAndFireReminders = checkAndFireReminders;
12996
+ handler._reminderPollIntervalMs = reminderPollWindowMs;
12839
12997
  const STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
12840
12998
  try {
12841
12999
  const allSummaries = await conversationStore.listSummaries();
@@ -13139,8 +13297,29 @@ ${config.task}`;
13139
13297
  }
13140
13298
  }, 500);
13141
13299
  });
13300
+ let reminderInterval = null;
13301
+ if (handler._checkAndFireReminders && handler._reminderPollIntervalMs) {
13302
+ const pollMs = handler._reminderPollIntervalMs;
13303
+ const check = handler._checkAndFireReminders;
13304
+ reminderInterval = setInterval(async () => {
13305
+ try {
13306
+ const result = await check();
13307
+ if (result.count > 0) {
13308
+ process.stdout.write(
13309
+ `[reminder] Fired ${result.count} reminder${result.count === 1 ? "" : "s"} (${result.duration}ms)
13310
+ `
13311
+ );
13312
+ }
13313
+ } catch (err) {
13314
+ console.error("[reminder] Poll error:", err instanceof Error ? err.message : err);
13315
+ }
13316
+ }, pollMs);
13317
+ process.stdout.write(`[reminder] Polling every ${Math.round(pollMs / 1e3)}s
13318
+ `);
13319
+ }
13142
13320
  const shutdown = () => {
13143
13321
  watcher.close();
13322
+ if (reminderInterval) clearInterval(reminderInterval);
13144
13323
  for (const job of activeJobs) {
13145
13324
  job.stop();
13146
13325
  }
@@ -13217,7 +13396,7 @@ var runInteractive = async (workingDir, params) => {
13217
13396
  await harness.initialize();
13218
13397
  const identity = await ensureAgentIdentity2(workingDir);
13219
13398
  try {
13220
- const { runInteractiveInk } = await import("./run-interactive-ink-JOLJ5W33.js");
13399
+ const { runInteractiveInk } = await import("./run-interactive-ink-R4PHKIQR.js");
13221
13400
  await runInteractiveInk({
13222
13401
  harness,
13223
13402
  params,
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-VHS3K24F.js";
4
+ } from "./chunk-IDGGF5WH.js";
5
5
 
6
6
  // src/cli.ts
7
7
  void main();
package/dist/index.d.ts CHANGED
@@ -112,6 +112,12 @@ type RequestHandler = ((request: IncomingMessage, response: ServerResponse) => P
112
112
  _processSubagentCallback?: (conversationId: string, skipLockCheck?: boolean) => Promise<void>;
113
113
  _broadcastEvent?: (conversationId: string, event: AgentEvent) => void;
114
114
  _finishConversationStream?: (conversationId: string) => void;
115
+ _checkAndFireReminders?: () => Promise<{
116
+ fired: string[];
117
+ count: number;
118
+ duration: number;
119
+ }>;
120
+ _reminderPollIntervalMs?: number;
115
121
  };
116
122
  declare const createRequestHandler: (options?: {
117
123
  workingDir?: string;
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  runTests,
25
25
  startDevServer,
26
26
  updateAgentGuidance
27
- } from "./chunk-VHS3K24F.js";
27
+ } from "./chunk-IDGGF5WH.js";
28
28
  export {
29
29
  __internalRunOrchestration,
30
30
  addSkill,
@@ -2,7 +2,7 @@ import {
2
2
  consumeFirstRunIntro,
3
3
  inferConversationTitle,
4
4
  resolveHarnessEnvironment
5
- } from "./chunk-VHS3K24F.js";
5
+ } from "./chunk-IDGGF5WH.js";
6
6
 
7
7
  // src/run-interactive-ink.ts
8
8
  import * as readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.32.8",
3
+ "version": "0.33.1",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "react": "^19.2.4",
28
28
  "react-devtools-core": "^6.1.5",
29
29
  "yaml": "^2.8.1",
30
- "@poncho-ai/harness": "0.31.3",
30
+ "@poncho-ai/harness": "0.32.1",
31
31
  "@poncho-ai/sdk": "1.7.1",
32
32
  "@poncho-ai/messaging": "0.7.8"
33
33
  },
package/src/index.ts CHANGED
@@ -984,6 +984,26 @@ cron:
984
984
 
985
985
  Add \`channel: telegram\` (or another platform) to have the agent proactively send the response to all known chats on that platform. The bot must have received at least one message from each user first.
986
986
 
987
+ ## Reminders
988
+
989
+ One-off reminders are enabled by default. The agent gets \`set_reminder\`, \`list_reminders\`, and \`cancel_reminder\` tools. Users can say things like "remind me tomorrow at 9am to check the report."
990
+
991
+ Configure in \`poncho.config.js\`:
992
+
993
+ \`\`\`javascript
994
+ export default {
995
+ reminders: {
996
+ enabled: true,
997
+ pollSchedule: '*/10 * * * *', // how often to check for due reminders
998
+ },
999
+ };
1000
+ \`\`\`
1001
+
1002
+ - Reminders fire via a polling loop (same interval locally and on serverless).
1003
+ - On Vercel, \`poncho build vercel\` adds a cron entry for \`/api/reminders/check\`.
1004
+ - Channel reminders (Telegram/Slack) reply in the original conversation.
1005
+ - Non-channel reminders create a new \`[reminder]\` conversation visible in the web UI.
1006
+
987
1007
  ## Messaging (Slack)
988
1008
 
989
1009
  Connect your agent to Slack so it responds to @mentions:
@@ -1325,9 +1345,26 @@ const checkVercelCronDrift = async (projectDir: string): Promise<void> => {
1325
1345
  for (const [jobName, schedule] of vercelCronMap) {
1326
1346
  diffs.push(` - removed job "${jobName}" (${schedule})`);
1327
1347
  }
1348
+
1349
+ // Check reminder polling cron
1350
+ try {
1351
+ const cfg = await loadPonchoConfig(projectDir);
1352
+ const reminderCron = vercelCrons.find((c) => c.path === "/api/reminders/check");
1353
+ if (cfg?.reminders?.enabled && !reminderCron) {
1354
+ diffs.push(` + missing reminders polling cron`);
1355
+ } else if (!cfg?.reminders?.enabled && reminderCron) {
1356
+ diffs.push(` - reminders polling cron present but reminders disabled`);
1357
+ } else if (cfg?.reminders?.enabled && reminderCron) {
1358
+ const expected = cfg.reminders.pollSchedule ?? "*/10 * * * *";
1359
+ if (reminderCron.schedule !== expected) {
1360
+ diffs.push(` ~ reminders poll schedule changed: "${reminderCron.schedule}" → "${expected}"`);
1361
+ }
1362
+ }
1363
+ } catch { /* best-effort */ }
1364
+
1328
1365
  if (diffs.length > 0) {
1329
1366
  process.stderr.write(
1330
- `\u26A0 vercel.json crons are out of sync with AGENT.md:\n${diffs.join("\n")}\n Run \`poncho build vercel --force\` to update.\n\n`,
1367
+ `\u26A0 vercel.json crons are out of sync with AGENT.md / poncho.config.js:\n${diffs.join("\n")}\n Run \`poncho build vercel --force\` to update.\n\n`,
1331
1368
  );
1332
1369
  }
1333
1370
  };
@@ -1446,6 +1483,16 @@ export default async function handler(req, res) {
1446
1483
  ],
1447
1484
  routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
1448
1485
  };
1486
+ // Add reminder polling cron if reminders are enabled
1487
+ try {
1488
+ const cfg = await loadPonchoConfig(projectDir);
1489
+ if (cfg?.reminders?.enabled) {
1490
+ const schedule = cfg.reminders.pollSchedule ?? "*/10 * * * *";
1491
+ if (!vercelCrons) vercelCrons = [];
1492
+ vercelCrons.push({ path: "/api/reminders/check", schedule });
1493
+ }
1494
+ } catch { /* best-effort */ }
1495
+
1449
1496
  if (vercelCrons && vercelCrons.length > 0) {
1450
1497
  vercelConfig.crons = vercelCrons;
1451
1498
  }
@@ -1498,6 +1545,9 @@ export const handler = async (event = {}) => {
1498
1545
  // Create a rule for each cron job defined in AGENT.md that sends a GET request to:
1499
1546
  // /api/cron/<jobName>
1500
1547
  // Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
1548
+ //
1549
+ // Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
1550
+ // every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
1501
1551
  `,
1502
1552
  { force: options?.force, writtenPaths, baseDir: projectDir },
1503
1553
  );
@@ -1775,6 +1825,8 @@ export type RequestHandler = ((
1775
1825
  _processSubagentCallback?: (conversationId: string, skipLockCheck?: boolean) => Promise<void>;
1776
1826
  _broadcastEvent?: (conversationId: string, event: AgentEvent) => void;
1777
1827
  _finishConversationStream?: (conversationId: string) => void;
1828
+ _checkAndFireReminders?: () => Promise<{ fired: string[]; count: number; duration: number }>;
1829
+ _reminderPollIntervalMs?: number;
1778
1830
  };
1779
1831
 
1780
1832
  export const createRequestHandler = async (options?: {
@@ -4527,7 +4579,7 @@ export const createRequestHandler = async (options?: {
4527
4579
 
4528
4580
  if (pathname.startsWith("/api/")) {
4529
4581
  // Internal self-fetch requests bypass user-facing auth
4530
- const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/");
4582
+ const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/") || pathname === "/api/reminders/check";
4531
4583
  const isInternal = isInternalPath && request.method === "POST" && isValidInternalRequest(request.headers);
4532
4584
 
4533
4585
  // Check authentication: either valid session (Web UI), valid Bearer token (API), or valid internal request
@@ -6309,8 +6361,139 @@ export const createRequestHandler = async (options?: {
6309
6361
  return;
6310
6362
  }
6311
6363
 
6364
+ // ── Reminders check endpoint ────────────────────────────────────
6365
+ if (pathname === "/api/reminders/check" && (request.method === "GET" || request.method === "POST")) {
6366
+ const result = await checkAndFireReminders();
6367
+ writeJson(response, 200, result);
6368
+ return;
6369
+ }
6370
+
6312
6371
  writeJson(response, 404, { error: "Not found" });
6313
6372
  };
6373
+
6374
+ // ── Reminder polling logic ──────────────────────────────────────
6375
+ const DEFAULT_POLL_SCHEDULE = "*/10 * * * *";
6376
+
6377
+ const pollScheduleToMs = (schedule: string): number => {
6378
+ const m = schedule.match(/^\*\/(\d+)\s/);
6379
+ if (m) return Number(m[1]) * 60 * 1000;
6380
+ return 10 * 60 * 1000;
6381
+ };
6382
+
6383
+ const reminderPollSchedule = config?.reminders?.pollSchedule ?? DEFAULT_POLL_SCHEDULE;
6384
+ const reminderPollWindowMs = pollScheduleToMs(reminderPollSchedule);
6385
+
6386
+ const checkAndFireReminders = async (): Promise<{
6387
+ fired: string[];
6388
+ count: number;
6389
+ duration: number;
6390
+ }> => {
6391
+ const reminderStore = harness.reminderStore;
6392
+ if (!reminderStore) return { fired: [], count: 0, duration: 0 };
6393
+
6394
+ const start = Date.now();
6395
+ const firedIds: string[] = [];
6396
+
6397
+ try {
6398
+ const reminders = await reminderStore.list();
6399
+ const cutoff = Date.now() + reminderPollWindowMs;
6400
+ const due = reminders.filter((r) => r.status === "pending" && r.scheduledAt <= cutoff);
6401
+
6402
+ for (const reminder of due) {
6403
+ try {
6404
+ await reminderStore.delete(reminder.id);
6405
+
6406
+ const originConv = reminder.conversationId
6407
+ ? await conversationStore.get(reminder.conversationId)
6408
+ : undefined;
6409
+ const channelMeta = originConv?.channelMeta;
6410
+
6411
+ const framedMessage =
6412
+ `[Reminder] A reminder you previously set has fired.\n` +
6413
+ `Task: "${reminder.task}"\n` +
6414
+ `Originally set at: ${new Date(reminder.createdAt).toISOString()}\n` +
6415
+ `Scheduled for: ${new Date(reminder.scheduledAt).toISOString()}`;
6416
+
6417
+ if (channelMeta) {
6418
+ const adapter = messagingAdapters.get(channelMeta.platform);
6419
+ if (adapter && originConv) {
6420
+ const historyMessages = originConv.messages ?? [];
6421
+ let assistantResponse = "";
6422
+ for await (const event of harness.runWithTelemetry({
6423
+ task: framedMessage,
6424
+ conversationId: originConv.conversationId,
6425
+ parameters: { __activeConversationId: originConv.conversationId },
6426
+ messages: historyMessages,
6427
+ })) {
6428
+ if (event.type === "model:chunk") {
6429
+ assistantResponse += event.content;
6430
+ }
6431
+ }
6432
+ if (assistantResponse) {
6433
+ try {
6434
+ await adapter.sendReply(
6435
+ {
6436
+ channelId: channelMeta.channelId,
6437
+ platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId,
6438
+ },
6439
+ assistantResponse,
6440
+ );
6441
+ } catch (sendError) {
6442
+ console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
6443
+ }
6444
+ }
6445
+ const freshConv = await conversationStore.get(originConv.conversationId);
6446
+ if (freshConv) {
6447
+ freshConv.messages = [
6448
+ ...historyMessages,
6449
+ { role: "user" as const, content: framedMessage },
6450
+ ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
6451
+ ];
6452
+ freshConv.updatedAt = Date.now();
6453
+ await conversationStore.update(freshConv);
6454
+ }
6455
+ }
6456
+ } else {
6457
+ const timestamp = new Date().toISOString();
6458
+ const conversation = await conversationStore.create(
6459
+ reminder.ownerId ?? "local-owner",
6460
+ `[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`,
6461
+ );
6462
+ const convId = conversation.conversationId;
6463
+ let assistantResponse = "";
6464
+ for await (const event of harness.runWithTelemetry({
6465
+ task: framedMessage,
6466
+ conversationId: convId,
6467
+ parameters: { __activeConversationId: convId },
6468
+ messages: [],
6469
+ })) {
6470
+ if (event.type === "model:chunk") {
6471
+ assistantResponse += event.content;
6472
+ }
6473
+ }
6474
+ const freshConv = await conversationStore.get(convId);
6475
+ if (freshConv) {
6476
+ freshConv.messages = [
6477
+ { role: "user" as const, content: framedMessage },
6478
+ ...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
6479
+ ];
6480
+ freshConv.updatedAt = Date.now();
6481
+ await conversationStore.update(freshConv);
6482
+ }
6483
+ }
6484
+
6485
+ firedIds.push(reminder.id);
6486
+ } catch (err) {
6487
+ console.error(`[reminder] Failed to fire reminder "${reminder.id}":`, err instanceof Error ? err.message : err);
6488
+ }
6489
+ }
6490
+ } catch (err) {
6491
+ console.error("[reminder] Error checking reminders:", err instanceof Error ? err.message : err);
6492
+ }
6493
+
6494
+ return { fired: firedIds, count: firedIds.length, duration: Date.now() - start };
6495
+ };
6496
+
6314
6497
  handler._harness = harness;
6315
6498
  handler._cronJobs = cronJobs;
6316
6499
  handler._conversationStore = conversationStore;
@@ -6320,6 +6503,8 @@ export const createRequestHandler = async (options?: {
6320
6503
  handler._processSubagentCallback = processSubagentCallback;
6321
6504
  handler._broadcastEvent = broadcastEvent;
6322
6505
  handler._finishConversationStream = finishConversationStream;
6506
+ handler._checkAndFireReminders = checkAndFireReminders;
6507
+ handler._reminderPollIntervalMs = reminderPollWindowMs;
6323
6508
 
6324
6509
  // Recover stale subagent runs that were "running" when the server last stopped
6325
6510
  // or that have been inactive longer than the staleness threshold.
@@ -6659,8 +6844,29 @@ export const startDevServer = async (
6659
6844
  }, 500);
6660
6845
  });
6661
6846
 
6847
+ // ── Reminder polling ─────────────────────────────────────────────
6848
+ let reminderInterval: ReturnType<typeof setInterval> | null = null;
6849
+ if (handler._checkAndFireReminders && handler._reminderPollIntervalMs) {
6850
+ const pollMs = handler._reminderPollIntervalMs;
6851
+ const check = handler._checkAndFireReminders;
6852
+ reminderInterval = setInterval(async () => {
6853
+ try {
6854
+ const result = await check();
6855
+ if (result.count > 0) {
6856
+ process.stdout.write(
6857
+ `[reminder] Fired ${result.count} reminder${result.count === 1 ? "" : "s"} (${result.duration}ms)\n`,
6858
+ );
6859
+ }
6860
+ } catch (err) {
6861
+ console.error("[reminder] Poll error:", err instanceof Error ? err.message : err);
6862
+ }
6863
+ }, pollMs);
6864
+ process.stdout.write(`[reminder] Polling every ${Math.round(pollMs / 1000)}s\n`);
6865
+ }
6866
+
6662
6867
  const shutdown = () => {
6663
6868
  watcher.close();
6869
+ if (reminderInterval) clearInterval(reminderInterval);
6664
6870
  for (const job of activeJobs) {
6665
6871
  job.stop();
6666
6872
  }
@@ -346,6 +346,7 @@ export const buildConfigFromOnboardingAnswers = (
346
346
  auth,
347
347
  storage,
348
348
  telemetry,
349
+ reminders: { enabled: true },
349
350
  };
350
351
 
351
352
  if (messagingPlatform !== "none") {
@@ -375,7 +376,7 @@ export const isDefaultOnboardingConfig = (
375
376
  return true;
376
377
  }
377
378
  const topLevelKeys = Object.keys(config);
378
- const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry", "messaging"]);
379
+ const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry", "messaging", "reminders"]);
379
380
  if (topLevelKeys.some((key) => !allowedTopLevel.has(key))) {
380
381
  return false;
381
382
  }