@meetquinn/animactl 0.1.2 → 0.1.3

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.
Files changed (289) hide show
  1. package/README.md +11 -6
  2. package/dist/server/{runtime/activity-payload.d.ts → activities/format.d.ts} +4 -1
  3. package/dist/server/activities/format.d.ts.map +1 -0
  4. package/dist/server/{runtime/activity-payload.js → activities/format.js} +16 -1
  5. package/dist/server/agents/agent-skills.d.ts +7 -3
  6. package/dist/server/agents/agent-skills.d.ts.map +1 -1
  7. package/dist/server/agents/agent-skills.js +243 -38
  8. package/dist/server/agents/agent-slack.service.d.ts +15 -0
  9. package/dist/server/agents/agent-slack.service.d.ts.map +1 -1
  10. package/dist/server/agents/agent-slack.service.js +43 -6
  11. package/dist/server/agents/default-skills.d.ts +9 -0
  12. package/dist/server/agents/default-skills.d.ts.map +1 -0
  13. package/dist/server/agents/default-skills.js +52 -0
  14. package/dist/server/cli/runtime-cli.d.ts +7 -1
  15. package/dist/server/cli/runtime-cli.d.ts.map +1 -1
  16. package/dist/server/cli/runtime-cli.js +107 -3
  17. package/dist/server/cli/services-cli.d.ts.map +1 -1
  18. package/dist/server/cli/services-cli.js +39 -0
  19. package/dist/server/inbox/slack-subscriber.d.ts +2 -0
  20. package/dist/server/inbox/slack-subscriber.d.ts.map +1 -1
  21. package/dist/server/inbox/slack-subscriber.js +28 -2
  22. package/dist/server/inbox/slack-subscription.service.d.ts.map +1 -1
  23. package/dist/server/inbox/slack-subscription.service.js +13 -1
  24. package/dist/server/inbox/wake-queue.service.d.ts +3 -1
  25. package/dist/server/inbox/wake-queue.service.d.ts.map +1 -1
  26. package/dist/server/inbox/wake-queue.service.js +5 -3
  27. package/dist/server/provider-usage/http.d.ts +1 -0
  28. package/dist/server/provider-usage/http.d.ts.map +1 -1
  29. package/dist/server/provider-usage/http.js +23 -1
  30. package/dist/server/provider-usage/provider-usage.service.d.ts +1 -4
  31. package/dist/server/provider-usage/provider-usage.service.d.ts.map +1 -1
  32. package/dist/server/provider-usage/provider-usage.service.js +1 -18
  33. package/dist/server/provider-usage/providers/claude.d.ts +1 -1
  34. package/dist/server/provider-usage/providers/claude.d.ts.map +1 -1
  35. package/dist/server/provider-usage/providers/claude.js +1 -1
  36. package/dist/server/provider-usage/providers/codex.d.ts +1 -1
  37. package/dist/server/provider-usage/providers/codex.d.ts.map +1 -1
  38. package/dist/server/provider-usage/providers/codex.js +1 -1
  39. package/dist/server/provider-usage/providers/kimi.d.ts +1 -1
  40. package/dist/server/provider-usage/providers/kimi.d.ts.map +1 -1
  41. package/dist/server/provider-usage/providers/kimi.js +4 -4
  42. package/dist/server/provider-usage/result.d.ts +5 -0
  43. package/dist/server/provider-usage/result.d.ts.map +1 -0
  44. package/dist/server/provider-usage/result.js +18 -0
  45. package/dist/server/providers/active-runtime.d.ts +1 -1
  46. package/dist/server/providers/active-runtime.d.ts.map +1 -1
  47. package/dist/server/providers/claude-events.d.ts +1 -1
  48. package/dist/server/providers/claude-events.d.ts.map +1 -1
  49. package/dist/server/providers/claude-events.js +2 -2
  50. package/dist/server/providers/claude.d.ts +1 -1
  51. package/dist/server/providers/claude.d.ts.map +1 -1
  52. package/dist/server/providers/claude.js +2 -2
  53. package/dist/server/providers/codex-app-server.d.ts +1 -1
  54. package/dist/server/providers/codex-app-server.d.ts.map +1 -1
  55. package/dist/server/providers/codex-app-server.js +1 -1
  56. package/dist/server/providers/codex-events.js +1 -1
  57. package/dist/server/providers/codex.d.ts +1 -1
  58. package/dist/server/providers/codex.d.ts.map +1 -1
  59. package/dist/server/providers/codex.js +2 -2
  60. package/dist/server/{runtime/provider-contract.d.ts → providers/contract.d.ts} +1 -1
  61. package/dist/server/providers/contract.d.ts.map +1 -0
  62. package/dist/server/providers/factory.d.ts +1 -1
  63. package/dist/server/providers/factory.d.ts.map +1 -1
  64. package/dist/server/providers/kimi-events.d.ts +1 -1
  65. package/dist/server/providers/kimi-events.d.ts.map +1 -1
  66. package/dist/server/providers/kimi-events.js +1 -1
  67. package/dist/server/providers/kimi.d.ts +1 -1
  68. package/dist/server/providers/kimi.d.ts.map +1 -1
  69. package/dist/server/providers/kimi.js +3 -2
  70. package/dist/server/providers/reasoning-events.js +1 -1
  71. package/dist/server/runtime/active-run-control.d.ts +19 -0
  72. package/dist/server/runtime/active-run-control.d.ts.map +1 -0
  73. package/dist/server/runtime/active-run-control.js +89 -0
  74. package/dist/server/runtime/activity.js +1 -1
  75. package/dist/server/runtime/agent-runner.d.ts +1 -1
  76. package/dist/server/runtime/agent-runner.d.ts.map +1 -1
  77. package/dist/server/runtime/delivery-prompt.d.ts +6 -0
  78. package/dist/server/runtime/delivery-prompt.d.ts.map +1 -1
  79. package/dist/server/runtime/delivery-prompt.js +53 -34
  80. package/dist/server/runtime/followup-appender.d.ts +20 -0
  81. package/dist/server/runtime/followup-appender.d.ts.map +1 -0
  82. package/dist/server/runtime/followup-appender.js +128 -0
  83. package/dist/server/runtime/host.d.ts +2 -0
  84. package/dist/server/runtime/host.d.ts.map +1 -1
  85. package/dist/server/runtime/host.js +8 -0
  86. package/dist/server/runtime/provider-runner.d.ts +16 -0
  87. package/dist/server/runtime/provider-runner.d.ts.map +1 -0
  88. package/dist/server/runtime/provider-runner.js +86 -0
  89. package/dist/server/runtime/runtime-bridge.d.ts +1 -1
  90. package/dist/server/runtime/runtime-bridge.d.ts.map +1 -1
  91. package/dist/server/runtime/runtime-session.service.d.ts +1 -1
  92. package/dist/server/runtime/runtime-session.service.d.ts.map +1 -1
  93. package/dist/server/runtime/runtime-session.service.js +1 -1
  94. package/dist/server/runtime/runtime-worker.d.ts +2 -10
  95. package/dist/server/runtime/runtime-worker.d.ts.map +1 -1
  96. package/dist/server/runtime/runtime-worker.js +60 -286
  97. package/dist/server/runtime-management/managed-runtime.d.ts.map +1 -0
  98. package/dist/server/runtime-management/runtime-release.d.ts.map +1 -0
  99. package/dist/server/{runtime → runtime-management}/runtime-upgrade.d.ts +1 -0
  100. package/dist/server/runtime-management/runtime-upgrade.d.ts.map +1 -0
  101. package/dist/server/{runtime → runtime-management}/runtime-upgrade.js +14 -0
  102. package/dist/server/services/system.service.d.ts +41 -0
  103. package/dist/server/services/system.service.d.ts.map +1 -0
  104. package/dist/server/services/system.service.js +154 -0
  105. package/dist/server/settings/settings.service.d.ts +2 -1
  106. package/dist/server/settings/settings.service.d.ts.map +1 -1
  107. package/dist/server/settings/settings.service.js +9 -1
  108. package/dist/server/slack/shortcuts.d.ts +0 -3
  109. package/dist/server/slack/shortcuts.d.ts.map +1 -1
  110. package/dist/server/slack/shortcuts.js +0 -3
  111. package/dist/server/slack-interactions/shortcut-ids.d.ts +4 -0
  112. package/dist/server/slack-interactions/shortcut-ids.d.ts.map +1 -0
  113. package/dist/server/slack-interactions/shortcut-ids.js +3 -0
  114. package/dist/server/slack-interactions/shortcut-views.d.ts +65 -0
  115. package/dist/server/slack-interactions/shortcut-views.d.ts.map +1 -0
  116. package/dist/server/slack-interactions/shortcut-views.js +211 -0
  117. package/dist/server/{slack → slack-interactions}/shortcut.service.d.ts +2 -47
  118. package/dist/server/slack-interactions/shortcut.service.d.ts.map +1 -0
  119. package/dist/server/slack-interactions/shortcut.service.js +227 -0
  120. package/dist/server/storage/jsonl-log.d.ts +9 -0
  121. package/dist/server/storage/jsonl-log.d.ts.map +1 -1
  122. package/dist/server/storage/jsonl-log.js +36 -1
  123. package/dist/server/storage/schema/server.store.d.ts +5 -0
  124. package/dist/server/storage/schema/server.store.d.ts.map +1 -1
  125. package/dist/server/storage/schema/server.store.js +2 -1
  126. package/dist/server/storage/schema/wake-queue.store.d.ts +15 -1
  127. package/dist/server/storage/schema/wake-queue.store.d.ts.map +1 -1
  128. package/dist/server/storage/schema/wake-queue.store.js +5 -3
  129. package/dist/server/tests/agent-config.test.d.ts +2 -0
  130. package/dist/server/tests/agent-config.test.d.ts.map +1 -0
  131. package/dist/server/tests/agent-config.test.js +229 -0
  132. package/dist/server/tests/agent-runtime.test.d.ts +2 -0
  133. package/dist/server/tests/agent-runtime.test.d.ts.map +1 -0
  134. package/dist/server/tests/agent-runtime.test.js +1110 -0
  135. package/dist/server/tests/agent-seed-memory.test.d.ts +2 -0
  136. package/dist/server/tests/agent-seed-memory.test.d.ts.map +1 -0
  137. package/dist/server/tests/agent-seed-memory.test.js +16 -0
  138. package/dist/server/tests/agent-skills.test.d.ts +2 -0
  139. package/dist/server/tests/agent-skills.test.d.ts.map +1 -0
  140. package/dist/server/tests/agent-skills.test.js +140 -0
  141. package/dist/server/tests/anima-home.d.ts +2 -0
  142. package/dist/server/tests/anima-home.d.ts.map +1 -0
  143. package/dist/server/tests/anima-home.js +4 -0
  144. package/dist/server/tests/cli-file.test.d.ts +2 -0
  145. package/dist/server/tests/cli-file.test.d.ts.map +1 -0
  146. package/dist/server/tests/cli-file.test.js +596 -0
  147. package/dist/server/tests/cli-message.test.d.ts +2 -0
  148. package/dist/server/tests/cli-message.test.d.ts.map +1 -0
  149. package/dist/server/tests/cli-message.test.js +1196 -0
  150. package/dist/server/tests/client-error-routes.test.d.ts +2 -0
  151. package/dist/server/tests/client-error-routes.test.d.ts.map +1 -0
  152. package/dist/server/tests/client-error-routes.test.js +163 -0
  153. package/dist/server/tests/config.test.d.ts +2 -0
  154. package/dist/server/tests/config.test.d.ts.map +1 -0
  155. package/dist/server/tests/config.test.js +497 -0
  156. package/dist/server/tests/default-skills.test.d.ts +2 -0
  157. package/dist/server/tests/default-skills.test.d.ts.map +1 -0
  158. package/dist/server/tests/default-skills.test.js +57 -0
  159. package/dist/server/tests/helpers/inbox.d.ts +10 -0
  160. package/dist/server/tests/helpers/inbox.d.ts.map +1 -0
  161. package/dist/server/tests/helpers/inbox.js +21 -0
  162. package/dist/server/tests/helpers/slack.d.ts +11 -0
  163. package/dist/server/tests/helpers/slack.d.ts.map +1 -0
  164. package/dist/server/tests/helpers/slack.js +17 -0
  165. package/dist/server/tests/helpers/state.d.ts +19 -0
  166. package/dist/server/tests/helpers/state.d.ts.map +1 -0
  167. package/dist/server/tests/helpers/state.js +69 -0
  168. package/dist/server/tests/inbox-slack-events.test.d.ts +2 -0
  169. package/dist/server/tests/inbox-slack-events.test.d.ts.map +1 -0
  170. package/dist/server/tests/inbox-slack-events.test.js +271 -0
  171. package/dist/server/tests/inbox.test.d.ts +2 -0
  172. package/dist/server/tests/inbox.test.d.ts.map +1 -0
  173. package/dist/server/tests/inbox.test.js +144 -0
  174. package/dist/server/tests/interactive-ask.test.d.ts +2 -0
  175. package/dist/server/tests/interactive-ask.test.d.ts.map +1 -0
  176. package/dist/server/tests/interactive-ask.test.js +387 -0
  177. package/dist/server/tests/kb.test.d.ts +2 -0
  178. package/dist/server/tests/kb.test.d.ts.map +1 -0
  179. package/dist/server/tests/kb.test.js +371 -0
  180. package/dist/server/tests/message.service.test.d.ts +2 -0
  181. package/dist/server/tests/message.service.test.d.ts.map +1 -0
  182. package/dist/server/tests/message.service.test.js +151 -0
  183. package/dist/server/tests/messages.test.d.ts +2 -0
  184. package/dist/server/tests/messages.test.d.ts.map +1 -0
  185. package/dist/server/tests/messages.test.js +12 -0
  186. package/dist/server/tests/prompt-attachments.test.d.ts +2 -0
  187. package/dist/server/tests/prompt-attachments.test.d.ts.map +1 -0
  188. package/dist/server/tests/prompt-attachments.test.js +274 -0
  189. package/dist/server/tests/provider-usage.test.d.ts +2 -0
  190. package/dist/server/tests/provider-usage.test.d.ts.map +1 -0
  191. package/dist/server/tests/provider-usage.test.js +97 -0
  192. package/dist/server/tests/reminders.test.d.ts +2 -0
  193. package/dist/server/tests/reminders.test.d.ts.map +1 -0
  194. package/dist/server/tests/reminders.test.js +383 -0
  195. package/dist/server/tests/runtime-cli.test.d.ts +2 -0
  196. package/dist/server/tests/runtime-cli.test.d.ts.map +1 -0
  197. package/dist/server/tests/runtime-cli.test.js +21 -0
  198. package/dist/server/tests/runtime-upgrade.test.d.ts +2 -0
  199. package/dist/server/tests/runtime-upgrade.test.d.ts.map +1 -0
  200. package/dist/server/tests/runtime-upgrade.test.js +307 -0
  201. package/dist/server/tests/runtime-worker.test.d.ts +2 -0
  202. package/dist/server/tests/runtime-worker.test.d.ts.map +1 -0
  203. package/dist/server/tests/runtime-worker.test.js +1021 -0
  204. package/dist/server/tests/runtime.test.d.ts +2 -0
  205. package/dist/server/tests/runtime.test.d.ts.map +1 -0
  206. package/dist/server/tests/runtime.test.js +354 -0
  207. package/dist/server/tests/services.test.d.ts +2 -0
  208. package/dist/server/tests/services.test.d.ts.map +1 -0
  209. package/dist/server/tests/services.test.js +636 -0
  210. package/dist/server/tests/slack-files.test.d.ts +2 -0
  211. package/dist/server/tests/slack-files.test.d.ts.map +1 -0
  212. package/dist/server/tests/slack-files.test.js +118 -0
  213. package/dist/server/tests/slack-shortcuts.test.d.ts +2 -0
  214. package/dist/server/tests/slack-shortcuts.test.d.ts.map +1 -0
  215. package/dist/server/tests/slack-shortcuts.test.js +310 -0
  216. package/dist/server/tests/slack.test.d.ts +2 -0
  217. package/dist/server/tests/slack.test.d.ts.map +1 -0
  218. package/dist/server/tests/slack.test.js +213 -0
  219. package/dist/server/tests/state-cache.test.d.ts +2 -0
  220. package/dist/server/tests/state-cache.test.d.ts.map +1 -0
  221. package/dist/server/tests/state-cache.test.js +184 -0
  222. package/dist/server/tests/subscriptions.test.d.ts +2 -0
  223. package/dist/server/tests/subscriptions.test.d.ts.map +1 -0
  224. package/dist/server/tests/subscriptions.test.js +292 -0
  225. package/dist/server/tests/system-service.test.d.ts +2 -0
  226. package/dist/server/tests/system-service.test.d.ts.map +1 -0
  227. package/dist/server/tests/system-service.test.js +20 -0
  228. package/dist/server/tests/url-routes.test.d.ts +2 -0
  229. package/dist/server/tests/url-routes.test.d.ts.map +1 -0
  230. package/dist/server/tests/url-routes.test.js +217 -0
  231. package/dist/server/tests/web-api.test.d.ts +2 -0
  232. package/dist/server/tests/web-api.test.d.ts.map +1 -0
  233. package/dist/server/tests/web-api.test.js +1759 -0
  234. package/dist/server/web/app.d.ts.map +1 -1
  235. package/dist/server/web/app.js +2 -0
  236. package/dist/server/web/client-error-routes.d.ts +3 -0
  237. package/dist/server/web/client-error-routes.d.ts.map +1 -0
  238. package/dist/server/web/client-error-routes.js +101 -0
  239. package/dist/server/web/system-routes.d.ts.map +1 -1
  240. package/dist/server/web/system-routes.js +21 -131
  241. package/dist/shared/agent-config.d.ts +8 -18
  242. package/dist/shared/agent-config.d.ts.map +1 -1
  243. package/dist/shared/agent-config.js +3 -4
  244. package/dist/shared/inbox.d.ts +27 -0
  245. package/dist/shared/inbox.d.ts.map +1 -1
  246. package/dist/shared/inbox.js +1 -0
  247. package/dist/shared/server-info.d.ts +6 -5
  248. package/dist/shared/server-info.d.ts.map +1 -1
  249. package/dist/shared/server-info.js +3 -1
  250. package/dist/shared/server-settings.d.ts +6 -0
  251. package/dist/shared/server-settings.d.ts.map +1 -1
  252. package/dist/shared/server-settings.js +1 -0
  253. package/dist/shared/skills.d.ts +24 -3
  254. package/dist/shared/skills.d.ts.map +1 -1
  255. package/dist/web/assets/activity-DHjip8tY.js +7 -0
  256. package/dist/web/assets/clipboard-D2NSC52v.js +1 -0
  257. package/dist/web/assets/{external-link-Dt0qTpmV.js → external-link-Bhr1WdRS.js} +1 -1
  258. package/dist/web/assets/{file-text-Dd9ys6py.js → file-text-CYMA_SMA.js} +1 -1
  259. package/dist/web/assets/{index-CzAQnbuO.js → index-CVpew409.js} +5 -5
  260. package/dist/web/assets/index-mh0uJTkY.css +2 -0
  261. package/dist/web/assets/kb-DDVpYkT9.js +35 -0
  262. package/dist/web/assets/profile-CBy6GNzp.js +1 -0
  263. package/dist/web/assets/reminders-Bbwb9QNT.js +1 -0
  264. package/dist/web/favicon.svg +1 -1
  265. package/dist/web/index.html +2 -2
  266. package/package.json +1 -1
  267. package/templates/runtime-standing-prompt.md +16 -2
  268. package/templates/skills/find-skills/SKILL.md +133 -0
  269. package/dist/server/runtime/activity-payload.d.ts.map +0 -1
  270. package/dist/server/runtime/activity-text.d.ts +0 -4
  271. package/dist/server/runtime/activity-text.d.ts.map +0 -1
  272. package/dist/server/runtime/activity-text.js +0 -16
  273. package/dist/server/runtime/managed-runtime.d.ts.map +0 -1
  274. package/dist/server/runtime/provider-contract.d.ts.map +0 -1
  275. package/dist/server/runtime/runtime-release.d.ts.map +0 -1
  276. package/dist/server/runtime/runtime-upgrade.d.ts.map +0 -1
  277. package/dist/server/slack/shortcut.service.d.ts.map +0 -1
  278. package/dist/server/slack/shortcut.service.js +0 -493
  279. package/dist/web/assets/activity-BP0C81IV.js +0 -7
  280. package/dist/web/assets/globe-DmDZSgDP.js +0 -1
  281. package/dist/web/assets/index-DKecrvN0.css +0 -2
  282. package/dist/web/assets/kb-3wFeP2Jt.js +0 -32
  283. package/dist/web/assets/profile-D7wPZXW_.js +0 -1
  284. package/dist/web/assets/reminders-DOhroVwo.js +0 -1
  285. /package/dist/server/{runtime/provider-contract.js → providers/contract.js} +0 -0
  286. /package/dist/server/{runtime → runtime-management}/managed-runtime.d.ts +0 -0
  287. /package/dist/server/{runtime → runtime-management}/managed-runtime.js +0 -0
  288. /package/dist/server/{runtime → runtime-management}/runtime-release.d.ts +0 -0
  289. /package/dist/server/{runtime → runtime-management}/runtime-release.js +0 -0
@@ -0,0 +1,383 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { once } from 'node:events';
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import test from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { makeReminderInboxItem } from './helpers/inbox.js';
9
+ import { WakeQueueService } from '../inbox/wake-queue.service.js';
10
+ import { allActivities, loadAgentState, loadState } from './helpers/state.js';
11
+ import { reminderServiceForAgent } from '../reminders/reminder.service.js';
12
+ import { nextDueAtForSchedule, parseRepeatRule } from '../reminders/reminder.helper.js';
13
+ import { AgentRuntimeWorker } from '../runtime/runtime-worker.js';
14
+ import { withAnimaHome } from './anima-home.js';
15
+ const cliPath = resolve('dist/server/cli/anima.js');
16
+ const reminderService = reminderServiceForAgent('scout');
17
+ test('one-shot reminders fire and clear nextDueAt', async () => {
18
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-once-'));
19
+ try {
20
+ await withAnimaHome(stateDir, async () => {
21
+ const reminder = await reminderService.scheduleReminder({
22
+ fireAt: '2026-05-14T09:00:00.000Z',
23
+ instructions: 'Check course comments and send a concise summary only if something changed.',
24
+ now: new Date('2026-05-14T08:00:00.000Z'),
25
+ provenance: {
26
+ channelId: 'C-course-review',
27
+ messageTs: '1770000000.000001',
28
+ },
29
+ title: 'Course review',
30
+ });
31
+ assert.equal(reminder.status, 'scheduled');
32
+ assert.equal(reminder.nextDueAt, '2026-05-14T09:00:00.000Z');
33
+ const tooEarly = await reminderService.dueReminders({
34
+ now: new Date('2026-05-14T08:59:59.000Z'),
35
+ });
36
+ assert.equal(tooEarly.length, 0);
37
+ const due = await reminderService.dueReminders({
38
+ now: new Date('2026-05-14T09:00:01.000Z'),
39
+ });
40
+ assert.equal(due.length, 1);
41
+ assert.equal(due[0]?.reminderId, reminder.reminderId);
42
+ await reminderService.completeReminderFire({
43
+ id: reminder.reminderId,
44
+ now: new Date('2026-05-14T09:00:01.000Z'),
45
+ });
46
+ const state = await loadAgentState('scout');
47
+ assert.equal(state.reminders[reminder.reminderId]?.status, 'fired');
48
+ assert.equal(state.reminders[reminder.reminderId]?.firedCount, 1);
49
+ assert.equal(state.reminders[reminder.reminderId]?.nextDueAt, undefined);
50
+ });
51
+ }
52
+ finally {
53
+ await rm(stateDir, { force: true, recursive: true });
54
+ }
55
+ });
56
+ test('recurring reminders can be snoozed without changing the long-term cadence', async () => {
57
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-recurring-'));
58
+ try {
59
+ await withAnimaHome(stateDir, async () => {
60
+ const reminder = await reminderService.scheduleReminder({
61
+ instructions: 'Review course comments.',
62
+ now: new Date('2026-05-14T08:00:00.000Z'),
63
+ repeat: 'daily@09:00',
64
+ timezone: 'UTC',
65
+ title: 'Daily course review',
66
+ });
67
+ assert.equal(reminder.nextDueAt, '2026-05-14T09:00:00.000Z');
68
+ const snoozed = await reminderService.snoozeReminder({
69
+ by: '2h',
70
+ id: reminder.reminderId,
71
+ now: new Date('2026-05-14T08:30:00.000Z'),
72
+ });
73
+ assert.equal(snoozed.nextDueAt, '2026-05-14T10:30:00.000Z');
74
+ const fired = await reminderService.completeReminderFire({
75
+ id: reminder.reminderId,
76
+ now: new Date('2026-05-14T10:31:00.000Z'),
77
+ });
78
+ assert.equal(fired.status, 'scheduled');
79
+ assert.equal(fired.firedCount, 1);
80
+ assert.equal(fired.nextDueAt, '2026-05-15T09:00:00.000Z');
81
+ });
82
+ }
83
+ finally {
84
+ await rm(stateDir, { force: true, recursive: true });
85
+ }
86
+ });
87
+ test('due reminder delivery enters the inbox and records fire activity', async () => {
88
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-scheduler-'));
89
+ const runtime = new CapturingRuntime();
90
+ const logger = { error: () => { }, log: () => { } };
91
+ try {
92
+ await withAnimaHome(stateDir, async () => {
93
+ await writeConfig(stateDir);
94
+ const reminder = await reminderService.scheduleReminder({
95
+ fireAt: '2026-05-14T09:00:00.000Z',
96
+ instructions: 'Wake up and decide whether to post an update.',
97
+ now: new Date('2026-05-14T08:00:00.000Z'),
98
+ title: 'Wake scout',
99
+ });
100
+ const queue = new WakeQueueService('scout');
101
+ const firedAt = new Date('2026-05-14T09:00:01.000Z');
102
+ const due = await reminderService.dueReminders({ now: firedAt });
103
+ assert.equal(due.length, 1);
104
+ const dueReminder = due[0] ?? assert.fail('expected due reminder');
105
+ const event = makeReminderInboxItem({
106
+ eventId: `reminder:${dueReminder.reminderId}:fire:${dueReminder.firedCount + 1}`,
107
+ reminderId: dueReminder.reminderId,
108
+ timestamp: firedAt.toISOString(),
109
+ });
110
+ const decision = await queue.enqueue(event);
111
+ const firedReminder = await reminderService.completeReminderFire({
112
+ id: dueReminder.reminderId,
113
+ now: firedAt,
114
+ });
115
+ if (!decision.duplicate) {
116
+ await reminderService.recordReminderFire({
117
+ firedAt,
118
+ reminder: firedReminder,
119
+ });
120
+ }
121
+ const worker = new AgentRuntimeWorker({
122
+ agentId: 'scout',
123
+ agentRuntime: runtime,
124
+ pollIntervalMs: 10,
125
+ queue,
126
+ stateDir,
127
+ workerId: 'reminder-test-worker',
128
+ }, logger);
129
+ worker.start();
130
+ try {
131
+ await waitUntil(() => runtime.calls.length === 1);
132
+ }
133
+ finally {
134
+ await worker.close();
135
+ }
136
+ const call = runtime.calls[0];
137
+ assert.match(call?.prompt ?? '', new RegExp(`reminder_id=${reminder.reminderId}`));
138
+ await waitUntil(async () => {
139
+ const state = await loadState();
140
+ return state.reminders[reminder.reminderId]?.status === 'fired';
141
+ });
142
+ await waitUntil(async () => {
143
+ const items = await queue.listRunnable();
144
+ return items.length > 0 && items.every((item) => item.handling.status !== 'running');
145
+ });
146
+ const fireActivity = allActivities(await loadState()).find((activity) => activity.payload?.['tool'] === 'anima.reminder.fire');
147
+ assert.equal(fireActivity?.type, 'tool.call.completed');
148
+ assert.equal(fireActivity?.payload?.['reminderId'], reminder.reminderId);
149
+ assert.equal(fireActivity?.payload?.['title'], 'Wake scout');
150
+ assert.equal(fireActivity?.payload?.['status'], 'fired');
151
+ assert.equal(fireActivity?.payload?.['firedAt'], fireActivity?.payload?.['lastFiredAt']);
152
+ assert.equal(Number.isFinite(Date.parse(String(fireActivity?.payload?.['firedAt']))), true);
153
+ assert.equal(fireActivity?.payload?.['firedCount'], 1);
154
+ assert.equal(fireActivity?.payload?.['scheduleKind'], 'once');
155
+ });
156
+ }
157
+ finally {
158
+ await rm(stateDir, { force: true, recursive: true });
159
+ }
160
+ });
161
+ test('reminder cancel and snooze record agent activity', async () => {
162
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-audit-'));
163
+ try {
164
+ await withAnimaHome(stateDir, async () => {
165
+ const first = await reminderService.scheduleReminder({
166
+ delaySeconds: 600,
167
+ instructions: 'no-op',
168
+ now: new Date('2026-05-14T08:00:00.000Z'),
169
+ title: 'snooze target',
170
+ });
171
+ const snoozed = await reminderService.snoozeReminder({
172
+ by: '30m',
173
+ id: first.reminderId,
174
+ now: new Date('2026-05-14T08:01:00.000Z'),
175
+ });
176
+ assert.equal(snoozed.nextDueAt, '2026-05-14T08:31:00.000Z');
177
+ const second = await reminderService.scheduleReminder({
178
+ delaySeconds: 600,
179
+ instructions: 'no-op',
180
+ now: new Date('2026-05-14T08:00:00.000Z'),
181
+ title: 'cancel target',
182
+ });
183
+ const cancelled = await reminderService.cancelReminder({
184
+ id: second.reminderId,
185
+ now: new Date('2026-05-14T08:02:00.000Z'),
186
+ });
187
+ assert.equal(cancelled.cancelledAt, '2026-05-14T08:02:00.000Z');
188
+ const activities = allActivities(await loadState());
189
+ const snooze = activities.find((activity) => activity.payload?.['tool'] === 'anima.reminder.snooze');
190
+ assert.equal(snooze?.type, 'tool.call.completed');
191
+ assert.equal(snooze?.payload?.['reminderId'], first.reminderId);
192
+ assert.equal(snooze?.payload?.['title'], 'snooze target');
193
+ assert.equal(snooze?.payload?.['nextDueAt'], '2026-05-14T08:31:00.000Z');
194
+ const cancel = activities.find((activity) => activity.payload?.['tool'] === 'anima.reminder.cancel');
195
+ assert.equal(cancel?.type, 'tool.call.completed');
196
+ assert.equal(cancel?.payload?.['reminderId'], second.reminderId);
197
+ assert.equal(cancel?.payload?.['title'], 'cancel target');
198
+ assert.equal(cancel?.payload?.['cancelledAt'], '2026-05-14T08:02:00.000Z');
199
+ });
200
+ }
201
+ finally {
202
+ await rm(stateDir, { force: true, recursive: true });
203
+ }
204
+ });
205
+ test('settled reminders older than 30 days are pruned on writes', async () => {
206
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-retention-'));
207
+ try {
208
+ await withAnimaHome(stateDir, async () => {
209
+ const oldFired = await reminderService.scheduleReminder({
210
+ fireAt: '2026-03-02T08:00:00.000Z',
211
+ instructions: 'old fired',
212
+ now: new Date('2026-03-01T08:00:00.000Z'),
213
+ title: 'old fired',
214
+ });
215
+ await reminderService.completeReminderFire({
216
+ id: oldFired.reminderId,
217
+ now: new Date('2026-03-02T08:00:00.000Z'),
218
+ });
219
+ const oldCancelled = await reminderService.scheduleReminder({
220
+ delaySeconds: 60,
221
+ instructions: 'old cancelled',
222
+ now: new Date('2026-03-01T08:00:00.000Z'),
223
+ title: 'old cancelled',
224
+ });
225
+ await reminderService.cancelReminder({
226
+ id: oldCancelled.reminderId,
227
+ now: new Date('2026-03-03T08:00:00.000Z'),
228
+ });
229
+ const oldScheduled = await reminderService.scheduleReminder({
230
+ fireAt: '2026-06-01T08:00:00.000Z',
231
+ instructions: 'still active',
232
+ now: new Date('2026-03-01T08:00:00.000Z'),
233
+ title: 'old scheduled',
234
+ });
235
+ const recentFired = await reminderService.scheduleReminder({
236
+ fireAt: '2026-05-01T08:00:00.000Z',
237
+ instructions: 'recent fired',
238
+ now: new Date('2026-04-30T08:00:00.000Z'),
239
+ title: 'recent fired',
240
+ });
241
+ await reminderService.completeReminderFire({
242
+ id: recentFired.reminderId,
243
+ now: new Date('2026-05-01T08:00:00.000Z'),
244
+ });
245
+ await reminderService.scheduleReminder({
246
+ delaySeconds: 60,
247
+ instructions: 'trigger retention',
248
+ now: new Date('2026-05-15T08:00:00.000Z'),
249
+ title: 'trigger retention',
250
+ });
251
+ const state = await loadAgentState('scout');
252
+ assert.equal(state.reminders[oldFired.reminderId], undefined);
253
+ assert.equal(state.reminders[oldCancelled.reminderId], undefined);
254
+ assert.equal(state.reminders[oldScheduled.reminderId]?.status, 'scheduled');
255
+ assert.equal(state.reminders[recentFired.reminderId]?.status, 'fired');
256
+ });
257
+ }
258
+ finally {
259
+ await rm(stateDir, { force: true, recursive: true });
260
+ }
261
+ });
262
+ test('parseRepeatRule rejects zero-interval and malformed rules', () => {
263
+ assert.throws(() => parseRepeatRule('every:0h', 'UTC'), /greater than zero/);
264
+ assert.throws(() => parseRepeatRule('every:0m', 'UTC'), /greater than zero/);
265
+ assert.throws(() => parseRepeatRule('weekly:@09:00', 'UTC'), /Invalid repeat rule/);
266
+ assert.throws(() => parseRepeatRule('weekly:funday@09:00', 'UTC'), /Invalid weekly repeat weekdays/);
267
+ });
268
+ test('recurring reminder rules respect IANA timezones', () => {
269
+ const daily = parseRepeatRule('daily@09:00', 'Asia/Shanghai');
270
+ assert.equal(nextDueAtForSchedule(daily, new Date('2026-05-14T00:30:00.000Z')), '2026-05-14T01:00:00.000Z');
271
+ const weekly = parseRepeatRule('weekly:mon@10:00', 'Asia/Shanghai');
272
+ assert.equal(nextDueAtForSchedule(weekly, new Date('2026-05-17T12:00:00.000Z')), '2026-05-18T02:00:00.000Z');
273
+ });
274
+ test('cancelReminder throws on missing id and snoozeReminder throws on cancelled', async () => {
275
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-reminder-errors-'));
276
+ try {
277
+ await withAnimaHome(stateDir, async () => {
278
+ await assert.rejects(reminderService.cancelReminder({ id: 'rem_does_not_exist' }), /Reminder not found/);
279
+ const reminder = await reminderService.scheduleReminder({
280
+ delaySeconds: 60,
281
+ instructions: 'no-op',
282
+ now: new Date('2026-05-14T08:00:00.000Z'),
283
+ title: 'temp',
284
+ });
285
+ await reminderService.cancelReminder({ id: reminder.reminderId });
286
+ await assert.rejects(reminderService.snoozeReminder({ by: '5m', id: reminder.reminderId }), /Cannot snooze cancelled reminder/);
287
+ });
288
+ }
289
+ finally {
290
+ await rm(stateDir, { force: true, recursive: true });
291
+ }
292
+ });
293
+ test('reminder CLI schedules, lists, snoozes, and cancels reminders', async () => {
294
+ const root = await mkdtemp(join(tmpdir(), 'anima-reminder-cli-'));
295
+ const configDir = join(root, '.anima');
296
+ try {
297
+ await writeConfig(configDir);
298
+ const env = { ...process.env, ANIMA_AGENT_ID: 'scout', ANIMA_HOME: configDir, ANIMA_INBOX_ITEM_ID: '' };
299
+ const scheduled = await runNode([
300
+ cliPath,
301
+ 'reminder',
302
+ 'schedule',
303
+ '--title',
304
+ 'Review comments',
305
+ '--in',
306
+ '10m',
307
+ '--instructions',
308
+ 'Read the latest comments and summarize changes.',
309
+ ], { env });
310
+ assert.equal(scheduled.status, 0, scheduled.stderr || scheduled.stdout);
311
+ assert.match(scheduled.stdout, /^scheduled successfully\. reminder_id=rem_/);
312
+ const reminderId = scheduled.stdout.match(/reminder_id=([^,]+)/)?.[1];
313
+ assert.ok(reminderId);
314
+ const aliasScheduled = await runNode([
315
+ cliPath,
316
+ 'reminder',
317
+ 'schedule',
318
+ '--in',
319
+ '5m',
320
+ '--note',
321
+ 'Check prod after restart and report anything odd.',
322
+ ], { env });
323
+ assert.equal(aliasScheduled.status, 0, aliasScheduled.stderr || aliasScheduled.stdout);
324
+ assert.match(aliasScheduled.stdout, /title=Check prod after restart and report anything odd\./);
325
+ const listed = await runNode([cliPath, 'reminder', 'list'], { env });
326
+ assert.equal(listed.status, 0, listed.stderr || listed.stdout);
327
+ assert.match(listed.stdout, new RegExp(`${reminderId} \\[scheduled\\] next=.* Review comments`));
328
+ const snoozed = await runNode([cliPath, 'reminder', 'snooze', '--id', reminderId, '--by', '30m'], { env });
329
+ assert.equal(snoozed.status, 0, snoozed.stderr || snoozed.stdout);
330
+ assert.match(snoozed.stdout, new RegExp(`^snoozed successfully\\. reminder_id=${reminderId}, title=Review comments, next=`));
331
+ const cancelled = await runNode([cliPath, 'reminder', 'cancel', reminderId], { env });
332
+ assert.equal(cancelled.status, 0, cancelled.stderr || cancelled.stdout);
333
+ assert.equal(cancelled.stdout.trim(), `cancelled successfully. reminder_id=${reminderId}, title=Review comments.`);
334
+ }
335
+ finally {
336
+ await rm(root, { force: true, recursive: true });
337
+ }
338
+ });
339
+ class CapturingRuntime {
340
+ kind = 'capturing-runtime';
341
+ calls = [];
342
+ async run(input) {
343
+ this.calls.push(input);
344
+ return { text: 'ok' };
345
+ }
346
+ async appendToActiveRun(_input) {
347
+ return { accepted: false };
348
+ }
349
+ }
350
+ async function waitUntil(predicate, timeoutMs = 1000) {
351
+ const startedAt = Date.now();
352
+ while (Date.now() - startedAt < timeoutMs) {
353
+ if (await predicate())
354
+ return;
355
+ await new Promise((resolve) => setTimeout(resolve, 10));
356
+ }
357
+ throw new Error('Timed out waiting for condition.');
358
+ }
359
+ async function writeConfig(configDir) {
360
+ await mkdir(configDir, { recursive: true });
361
+ await mkdir(join(configDir, 'agents', 'scout'), { recursive: true });
362
+ await writeFile(join(configDir, 'config.json'), `${JSON.stringify({}, null, 2)}\n`, 'utf8');
363
+ await writeFile(join(configDir, 'agents', 'scout', 'config.json'), `${JSON.stringify({ id: 'scout' }, null, 2)}\n`, 'utf8');
364
+ }
365
+ async function runNode(args, options = {}) {
366
+ const child = spawn(process.execPath, args, {
367
+ env: options.env,
368
+ stdio: ['pipe', 'pipe', 'pipe'],
369
+ });
370
+ let stdout = '';
371
+ let stderr = '';
372
+ child.stdout.setEncoding('utf8');
373
+ child.stderr.setEncoding('utf8');
374
+ child.stdout.on('data', (chunk) => {
375
+ stdout += chunk;
376
+ });
377
+ child.stderr.on('data', (chunk) => {
378
+ stderr += chunk;
379
+ });
380
+ child.stdin.end(options.input);
381
+ const [status] = (await once(child, 'exit'));
382
+ return { status, stderr, stdout };
383
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-cli.test.d.ts","sourceRoot":"","sources":["../../../server/tests/runtime-cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { dashboardLaunchCommand, dashboardUrl } from '../cli/runtime-cli.js';
4
+ test('dashboard URL uses localhost for all-interface hosts', () => {
5
+ assert.equal(dashboardUrl('0.0.0.0', 4174), 'http://127.0.0.1:4174');
6
+ assert.equal(dashboardUrl('127.0.0.1', 4199), 'http://127.0.0.1:4199');
7
+ });
8
+ test('dashboard launch command maps to the platform browser launcher', () => {
9
+ assert.deepEqual(dashboardLaunchCommand('http://127.0.0.1:4174', 'darwin'), {
10
+ command: 'open',
11
+ args: ['http://127.0.0.1:4174'],
12
+ });
13
+ assert.deepEqual(dashboardLaunchCommand('http://127.0.0.1:4174', 'win32'), {
14
+ command: 'cmd',
15
+ args: ['/c', 'start', '', 'http://127.0.0.1:4174'],
16
+ });
17
+ assert.deepEqual(dashboardLaunchCommand('http://127.0.0.1:4174', 'linux'), {
18
+ command: 'xdg-open',
19
+ args: ['http://127.0.0.1:4174'],
20
+ });
21
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-upgrade.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-upgrade.test.d.ts","sourceRoot":"","sources":["../../../server/tests/runtime-upgrade.test.ts"],"names":[],"mappings":""}