discoclaw 0.1.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.
Files changed (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadDurableMemory, saveDurableMemory, addItem, deprecateItems, selectItemsForInjection, formatDurableSection, } from './durable-memory.js';
4
+ import { loadSummary } from './summarizer.js';
5
+ import { durableWriteQueue } from './durable-write-queue.js';
6
+ export function parseMemoryCommand(content) {
7
+ const trimmed = content.trim();
8
+ if (!/^!memory(?:\s|$)/.test(trimmed))
9
+ return null;
10
+ const rest = trimmed.slice('!memory'.length).trim();
11
+ if (!rest || rest === 'show')
12
+ return { action: 'show', args: '' };
13
+ if (rest.startsWith('remember '))
14
+ return { action: 'remember', args: rest.slice('remember '.length).trim() };
15
+ if (rest.startsWith('forget '))
16
+ return { action: 'forget', args: rest.slice('forget '.length).trim() };
17
+ if (rest === 'reset rolling')
18
+ return { action: 'reset-rolling', args: '' };
19
+ return null;
20
+ }
21
+ export async function handleMemoryCommand(cmd, opts) {
22
+ try {
23
+ if (cmd.action === 'show') {
24
+ const store = await loadDurableMemory(opts.durableDataDir, opts.userId);
25
+ const items = store
26
+ ? selectItemsForInjection(store, opts.durableInjectMaxChars)
27
+ : [];
28
+ const durableText = items.length > 0
29
+ ? formatDurableSection(items)
30
+ : '(none)';
31
+ let summaryText = '(none)';
32
+ try {
33
+ const summary = await loadSummary(opts.summaryDataDir, opts.sessionKey);
34
+ if (summary)
35
+ summaryText = summary.summary;
36
+ }
37
+ catch {
38
+ // best-effort
39
+ }
40
+ return `**Durable memory:**\n${durableText}\n\n**Rolling summary:**\n${summaryText}`;
41
+ }
42
+ if (cmd.action === 'remember') {
43
+ return durableWriteQueue.run(opts.userId, async () => {
44
+ const store = await loadOrCreate(opts.durableDataDir, opts.userId);
45
+ const source = { type: 'manual' };
46
+ if (opts.channelId)
47
+ source.channelId = opts.channelId;
48
+ if (opts.messageId)
49
+ source.messageId = opts.messageId;
50
+ if (opts.guildId)
51
+ source.guildId = opts.guildId;
52
+ if (opts.channelName)
53
+ source.channelName = opts.channelName;
54
+ addItem(store, cmd.args, source, opts.durableMaxItems);
55
+ await saveDurableMemory(opts.durableDataDir, opts.userId, store);
56
+ return `Remembered: ${cmd.args}`;
57
+ });
58
+ }
59
+ if (cmd.action === 'forget') {
60
+ return durableWriteQueue.run(opts.userId, async () => {
61
+ const store = await loadOrCreate(opts.durableDataDir, opts.userId);
62
+ const { deprecatedCount } = deprecateItems(store, cmd.args);
63
+ if (deprecatedCount > 0) {
64
+ await saveDurableMemory(opts.durableDataDir, opts.userId, store);
65
+ return `Forgot ${deprecatedCount} item(s).`;
66
+ }
67
+ return 'No matching items found.';
68
+ });
69
+ }
70
+ if (cmd.action === 'reset-rolling') {
71
+ const safeName = opts.sessionKey.replace(/[^a-zA-Z0-9:_-]+/g, '-');
72
+ const filePath = path.join(opts.summaryDataDir, `${safeName}.json`);
73
+ await fs.rm(filePath, { force: true });
74
+ return 'Rolling summary cleared for this session.';
75
+ }
76
+ return 'Unknown memory command.';
77
+ }
78
+ catch (err) {
79
+ return `Memory command error: ${String(err)}`;
80
+ }
81
+ }
82
+ async function loadOrCreate(dir, userId) {
83
+ const store = await loadDurableMemory(dir, userId);
84
+ return store ?? { version: 1, updatedAt: 0, items: [] };
85
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { parseMemoryCommand, handleMemoryCommand } from './memory-commands.js';
6
+ import { saveDurableMemory, addItem } from './durable-memory.js';
7
+ import { saveSummary } from './summarizer.js';
8
+ async function makeTmpDir() {
9
+ return fs.mkdtemp(path.join(os.tmpdir(), 'memory-commands-test-'));
10
+ }
11
+ function baseOpts(overrides = {}) {
12
+ return {
13
+ userId: '12345',
14
+ sessionKey: 'discord:dm:12345',
15
+ durableDataDir: '/tmp/durable',
16
+ durableMaxItems: 200,
17
+ durableInjectMaxChars: 2000,
18
+ summaryDataDir: '/tmp/summaries',
19
+ ...overrides,
20
+ };
21
+ }
22
+ describe('parseMemoryCommand', () => {
23
+ it('returns null for non-commands', () => {
24
+ expect(parseMemoryCommand('hello world')).toBeNull();
25
+ expect(parseMemoryCommand('!other command')).toBeNull();
26
+ expect(parseMemoryCommand('!memoryshow')).toBeNull();
27
+ expect(parseMemoryCommand('')).toBeNull();
28
+ });
29
+ it('parses !memory show', () => {
30
+ expect(parseMemoryCommand('!memory show')).toEqual({ action: 'show', args: '' });
31
+ });
32
+ it('parses bare !memory as show', () => {
33
+ expect(parseMemoryCommand('!memory')).toEqual({ action: 'show', args: '' });
34
+ });
35
+ it('parses !memory remember foo', () => {
36
+ expect(parseMemoryCommand('!memory remember I prefer TypeScript')).toEqual({
37
+ action: 'remember',
38
+ args: 'I prefer TypeScript',
39
+ });
40
+ });
41
+ it('parses !memory forget bar', () => {
42
+ expect(parseMemoryCommand('!memory forget TypeScript')).toEqual({
43
+ action: 'forget',
44
+ args: 'TypeScript',
45
+ });
46
+ });
47
+ it('parses !memory reset rolling', () => {
48
+ expect(parseMemoryCommand('!memory reset rolling')).toEqual({
49
+ action: 'reset-rolling',
50
+ args: '',
51
+ });
52
+ });
53
+ it('ignores extra whitespace', () => {
54
+ expect(parseMemoryCommand(' !memory show ')).toEqual({ action: 'show', args: '' });
55
+ expect(parseMemoryCommand(' !memory remember foo bar ')).toEqual({
56
+ action: 'remember',
57
+ args: 'foo bar',
58
+ });
59
+ });
60
+ });
61
+ describe('handleMemoryCommand', () => {
62
+ it('show — returns formatted durable + rolling', async () => {
63
+ const durableDir = await makeTmpDir();
64
+ const summaryDir = await makeTmpDir();
65
+ const store = { version: 1, updatedAt: 0, items: [] };
66
+ addItem(store, 'User prefers TypeScript', { type: 'manual' }, 200);
67
+ await saveDurableMemory(durableDir, '12345', store);
68
+ await saveSummary(summaryDir, 'discord:dm:12345', {
69
+ summary: 'User is working on memory system.',
70
+ updatedAt: Date.now(),
71
+ });
72
+ const result = await handleMemoryCommand({ action: 'show', args: '' }, baseOpts({ durableDataDir: durableDir, summaryDataDir: summaryDir }));
73
+ expect(result).toContain('**Durable memory:**');
74
+ expect(result).toContain('User prefers TypeScript');
75
+ expect(result).toContain('**Rolling summary:**');
76
+ expect(result).toContain('User is working on memory system.');
77
+ });
78
+ it('show — returns "(none)" when empty', async () => {
79
+ const durableDir = await makeTmpDir();
80
+ const summaryDir = await makeTmpDir();
81
+ const result = await handleMemoryCommand({ action: 'show', args: '' }, baseOpts({ durableDataDir: durableDir, summaryDataDir: summaryDir }));
82
+ expect(result).toContain('(none)');
83
+ });
84
+ it('remember — adds item and saves', async () => {
85
+ const durableDir = await makeTmpDir();
86
+ const result = await handleMemoryCommand({ action: 'remember', args: 'I prefer dark mode' }, baseOpts({ durableDataDir: durableDir }));
87
+ expect(result).toBe('Remembered: I prefer dark mode');
88
+ // Verify it was saved to disk.
89
+ const raw = await fs.readFile(path.join(durableDir, '12345.json'), 'utf8');
90
+ const store = JSON.parse(raw);
91
+ expect(store.items).toHaveLength(1);
92
+ expect(store.items[0].text).toBe('I prefer dark mode');
93
+ });
94
+ it('forget — deprecates matching items', async () => {
95
+ const durableDir = await makeTmpDir();
96
+ // First remember something
97
+ await handleMemoryCommand({ action: 'remember', args: 'TypeScript' }, baseOpts({ durableDataDir: durableDir }));
98
+ // Then forget it
99
+ const result = await handleMemoryCommand({ action: 'forget', args: 'TypeScript' }, baseOpts({ durableDataDir: durableDir }));
100
+ expect(result).toBe('Forgot 1 item(s).');
101
+ });
102
+ it('forget — reports when no match found', async () => {
103
+ const durableDir = await makeTmpDir();
104
+ const result = await handleMemoryCommand({ action: 'forget', args: 'something that does not exist' }, baseOpts({ durableDataDir: durableDir }));
105
+ expect(result).toBe('No matching items found.');
106
+ });
107
+ it('remember — persists channelId and messageId in source', async () => {
108
+ const durableDir = await makeTmpDir();
109
+ await handleMemoryCommand({ action: 'remember', args: 'test fact' }, baseOpts({ durableDataDir: durableDir, channelId: 'ch42', messageId: 'msg99' }));
110
+ const raw = await fs.readFile(path.join(durableDir, '12345.json'), 'utf8');
111
+ const store = JSON.parse(raw);
112
+ expect(store.items[0].source).toEqual({ type: 'manual', channelId: 'ch42', messageId: 'msg99' });
113
+ });
114
+ it('remember persists guildId and channelName in source', async () => {
115
+ const durableDir = await makeTmpDir();
116
+ await handleMemoryCommand({ action: 'remember', args: 'test fact' }, baseOpts({ durableDataDir: durableDir, channelId: 'ch42', messageId: 'msg99', guildId: 'g1', channelName: 'dev' }));
117
+ const raw = await fs.readFile(path.join(durableDir, '12345.json'), 'utf8');
118
+ const store = JSON.parse(raw);
119
+ expect(store.items[0].source).toEqual({
120
+ type: 'manual',
121
+ channelId: 'ch42',
122
+ messageId: 'msg99',
123
+ guildId: 'g1',
124
+ channelName: 'dev',
125
+ });
126
+ });
127
+ it('concurrent remember calls serialize correctly', async () => {
128
+ const durableDir = await makeTmpDir();
129
+ // Fire off concurrent remember commands
130
+ const results = await Promise.all([
131
+ handleMemoryCommand({ action: 'remember', args: 'fact one' }, baseOpts({ durableDataDir: durableDir })),
132
+ handleMemoryCommand({ action: 'remember', args: 'fact two' }, baseOpts({ durableDataDir: durableDir })),
133
+ handleMemoryCommand({ action: 'remember', args: 'fact three' }, baseOpts({ durableDataDir: durableDir })),
134
+ ]);
135
+ expect(results).toEqual([
136
+ 'Remembered: fact one',
137
+ 'Remembered: fact two',
138
+ 'Remembered: fact three',
139
+ ]);
140
+ // Verify all three ended up on disk
141
+ const raw = await fs.readFile(path.join(durableDir, '12345.json'), 'utf8');
142
+ const store = JSON.parse(raw);
143
+ expect(store.items).toHaveLength(3);
144
+ const texts = store.items.map((it) => it.text).sort();
145
+ expect(texts).toEqual(['fact one', 'fact three', 'fact two']);
146
+ });
147
+ it('reset-rolling — deletes summary file', async () => {
148
+ const summaryDir = await makeTmpDir();
149
+ await saveSummary(summaryDir, 'discord:dm:12345', {
150
+ summary: 'some summary',
151
+ updatedAt: Date.now(),
152
+ });
153
+ const result = await handleMemoryCommand({ action: 'reset-rolling', args: '' }, baseOpts({ summaryDataDir: summaryDir }));
154
+ expect(result).toBe('Rolling summary cleared for this session.');
155
+ // Verify file is gone.
156
+ const files = await fs.readdir(summaryDir);
157
+ expect(files).toHaveLength(0);
158
+ });
159
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createMessageCreateHandler } from '../discord.js';
6
+ function makeQueue() {
7
+ return {
8
+ run: vi.fn(async (_key, fn) => fn()),
9
+ };
10
+ }
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+ function makeReply(id) {
15
+ return {
16
+ id,
17
+ channelId: 'chan',
18
+ edit: vi.fn(async () => { }),
19
+ delete: vi.fn(async () => { }),
20
+ react: vi.fn(async () => { }),
21
+ reactions: { resolve: () => ({ remove: async () => { } }) },
22
+ };
23
+ }
24
+ function makeMsg(content, replyId) {
25
+ const replyObj = makeReply(replyId);
26
+ return {
27
+ id: `msg-${replyId}`,
28
+ type: 0,
29
+ author: { id: '123', bot: false, username: 'tester', displayName: 'Tester' },
30
+ guildId: 'guild',
31
+ guild: { roles: { everyone: {} } },
32
+ channelId: 'chan',
33
+ channel: { send: vi.fn(async () => { }), isThread: () => false, name: 'general', id: 'chan' },
34
+ content,
35
+ attachments: new Map(),
36
+ stickers: new Map(),
37
+ embeds: [],
38
+ mentions: { has: () => false },
39
+ client: { user: { id: 'bot-1' }, channels: { cache: new Map() } },
40
+ reply: vi.fn(async () => replyObj),
41
+ };
42
+ }
43
+ function makeParams(runtime, summaryDataDir, overrides = {}) {
44
+ return {
45
+ allowUserIds: new Set(['123']),
46
+ runtime,
47
+ sessionManager: { getOrCreate: vi.fn(async () => 'sess') },
48
+ workspaceCwd: '/tmp',
49
+ projectCwd: '/tmp',
50
+ groupsDir: '/tmp',
51
+ useGroupDirCwd: false,
52
+ runtimeModel: 'fast',
53
+ runtimeTools: [],
54
+ runtimeTimeoutMs: 1000,
55
+ requireChannelContext: false,
56
+ autoIndexChannelContext: false,
57
+ autoJoinThreads: false,
58
+ useRuntimeSessions: true,
59
+ discordActionsEnabled: false,
60
+ discordActionsChannels: false,
61
+ discordActionsMessaging: false,
62
+ discordActionsGuild: false,
63
+ discordActionsModeration: false,
64
+ discordActionsPolls: false,
65
+ discordActionsTasks: false,
66
+ discordActionsBotProfile: false,
67
+ messageHistoryBudget: 0,
68
+ summaryEnabled: true,
69
+ summaryModel: 'fast',
70
+ summaryMaxChars: 2000,
71
+ summaryEveryNTurns: 1,
72
+ summaryDataDir,
73
+ summaryToDurableEnabled: false,
74
+ shortTermMemoryEnabled: false,
75
+ shortTermDataDir: '/tmp/shortterm',
76
+ shortTermMaxEntries: 20,
77
+ shortTermMaxAgeMs: 21600000,
78
+ shortTermInjectMaxChars: 1000,
79
+ durableMemoryEnabled: true,
80
+ durableDataDir: '/tmp/durable',
81
+ durableInjectMaxChars: 2000,
82
+ durableMaxItems: 200,
83
+ memoryCommandsEnabled: true,
84
+ actionFollowupDepth: 0,
85
+ reactionHandlerEnabled: false,
86
+ reactionRemoveHandlerEnabled: false,
87
+ reactionMaxAgeMs: 86400000,
88
+ streamStallWarningMs: 0,
89
+ botDisplayName: 'TestBot',
90
+ ...overrides,
91
+ };
92
+ }
93
+ async function waitForFileSummary(filePath, expected) {
94
+ const deadline = Date.now() + 4000;
95
+ let last = '';
96
+ while (Date.now() < deadline) {
97
+ try {
98
+ const raw = await fs.readFile(filePath, 'utf8');
99
+ last = JSON.parse(raw).summary;
100
+ if (last === expected)
101
+ return;
102
+ }
103
+ catch {
104
+ // keep polling
105
+ }
106
+ await sleep(20);
107
+ }
108
+ throw new Error(`Timed out waiting for summary "${expected}". Last seen: "${last}"`);
109
+ }
110
+ describe('memory timing integration', () => {
111
+ it('serializes summary writes so stale async completions do not overwrite newer summary', async () => {
112
+ let summaryCall = 0;
113
+ const runtime = {
114
+ id: 'test-runtime',
115
+ capabilities: new Set(),
116
+ invoke: vi.fn(async function* (p) {
117
+ const prompt = String(p.prompt ?? '');
118
+ if (prompt.includes('Updated summary:')) {
119
+ summaryCall += 1;
120
+ if (summaryCall === 1)
121
+ await sleep(80);
122
+ if (summaryCall === 2)
123
+ await sleep(10);
124
+ yield { type: 'text_final', text: summaryCall === 1 ? 'summary:first' : 'summary:second' };
125
+ return;
126
+ }
127
+ yield { type: 'text_final', text: 'ok' };
128
+ }),
129
+ };
130
+ const summaryDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-timing-summary-'));
131
+ const handler = createMessageCreateHandler(makeParams(runtime, summaryDir, { summaryEveryNTurns: 1 }), makeQueue());
132
+ await handler(makeMsg('first turn', 'r1'));
133
+ await handler(makeMsg('second turn', 'r2'));
134
+ const summaryFile = path.join(summaryDir, 'discord:channel:chan.json');
135
+ await waitForFileSummary(summaryFile, 'summary:second');
136
+ });
137
+ it('reset rolling clears in-memory turn counter so next turn does not prematurely regenerate summary', async () => {
138
+ const runtime = {
139
+ id: 'test-runtime',
140
+ capabilities: new Set(),
141
+ invoke: vi.fn(async function* (p) {
142
+ const prompt = String(p.prompt ?? '');
143
+ if (prompt.includes('Updated summary:')) {
144
+ yield { type: 'text_final', text: 'summary:generated' };
145
+ return;
146
+ }
147
+ yield { type: 'text_final', text: 'ok' };
148
+ }),
149
+ };
150
+ const summaryDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-reset-counter-'));
151
+ const handler = createMessageCreateHandler(makeParams(runtime, summaryDir, { summaryEveryNTurns: 2 }), makeQueue());
152
+ await handler(makeMsg('turn one', 'r1'));
153
+ await handler(makeMsg('!memory reset rolling', 'r2'));
154
+ await handler(makeMsg('turn two', 'r3'));
155
+ await sleep(200);
156
+ const summaryFile = path.join(summaryDir, 'discord:channel:chan.json');
157
+ await expect(fs.access(summaryFile)).rejects.toThrow();
158
+ });
159
+ });