@geminixiang/mikan 0.2.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 (316) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE +22 -0
  3. package/README.md +297 -0
  4. package/dist/adapter.d.ts +134 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +2 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/adapters/discord/bot.d.ts +63 -0
  9. package/dist/adapters/discord/bot.d.ts.map +1 -0
  10. package/dist/adapters/discord/bot.js +577 -0
  11. package/dist/adapters/discord/bot.js.map +1 -0
  12. package/dist/adapters/discord/context.d.ts +9 -0
  13. package/dist/adapters/discord/context.d.ts.map +1 -0
  14. package/dist/adapters/discord/context.js +245 -0
  15. package/dist/adapters/discord/context.js.map +1 -0
  16. package/dist/adapters/discord/index.d.ts +3 -0
  17. package/dist/adapters/discord/index.d.ts.map +1 -0
  18. package/dist/adapters/discord/index.js +3 -0
  19. package/dist/adapters/discord/index.js.map +1 -0
  20. package/dist/adapters/shared.d.ts +91 -0
  21. package/dist/adapters/shared.d.ts.map +1 -0
  22. package/dist/adapters/shared.js +191 -0
  23. package/dist/adapters/shared.js.map +1 -0
  24. package/dist/adapters/slack/bot.d.ts +139 -0
  25. package/dist/adapters/slack/bot.d.ts.map +1 -0
  26. package/dist/adapters/slack/bot.js +1272 -0
  27. package/dist/adapters/slack/bot.js.map +1 -0
  28. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  29. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  30. package/dist/adapters/slack/branch-manager.js +117 -0
  31. package/dist/adapters/slack/branch-manager.js.map +1 -0
  32. package/dist/adapters/slack/context.d.ts +12 -0
  33. package/dist/adapters/slack/context.d.ts.map +1 -0
  34. package/dist/adapters/slack/context.js +327 -0
  35. package/dist/adapters/slack/context.js.map +1 -0
  36. package/dist/adapters/slack/index.d.ts +3 -0
  37. package/dist/adapters/slack/index.d.ts.map +1 -0
  38. package/dist/adapters/slack/index.js +3 -0
  39. package/dist/adapters/slack/index.js.map +1 -0
  40. package/dist/adapters/slack/session.d.ts +38 -0
  41. package/dist/adapters/slack/session.d.ts.map +1 -0
  42. package/dist/adapters/slack/session.js +66 -0
  43. package/dist/adapters/slack/session.js.map +1 -0
  44. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  45. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  46. package/dist/adapters/slack/tools/attach.js +40 -0
  47. package/dist/adapters/slack/tools/attach.js.map +1 -0
  48. package/dist/adapters/telegram/bot.d.ts +51 -0
  49. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  50. package/dist/adapters/telegram/bot.js +430 -0
  51. package/dist/adapters/telegram/bot.js.map +1 -0
  52. package/dist/adapters/telegram/context.d.ts +9 -0
  53. package/dist/adapters/telegram/context.d.ts.map +1 -0
  54. package/dist/adapters/telegram/context.js +190 -0
  55. package/dist/adapters/telegram/context.js.map +1 -0
  56. package/dist/adapters/telegram/html.d.ts +3 -0
  57. package/dist/adapters/telegram/html.d.ts.map +1 -0
  58. package/dist/adapters/telegram/html.js +98 -0
  59. package/dist/adapters/telegram/html.js.map +1 -0
  60. package/dist/adapters/telegram/index.d.ts +3 -0
  61. package/dist/adapters/telegram/index.d.ts.map +1 -0
  62. package/dist/adapters/telegram/index.js +3 -0
  63. package/dist/adapters/telegram/index.js.map +1 -0
  64. package/dist/agent.d.ts +36 -0
  65. package/dist/agent.d.ts.map +1 -0
  66. package/dist/agent.js +1147 -0
  67. package/dist/agent.js.map +1 -0
  68. package/dist/commands/auto-reply.d.ts +5 -0
  69. package/dist/commands/auto-reply.d.ts.map +1 -0
  70. package/dist/commands/auto-reply.js +79 -0
  71. package/dist/commands/auto-reply.js.map +1 -0
  72. package/dist/commands/index.d.ts +5 -0
  73. package/dist/commands/index.d.ts.map +1 -0
  74. package/dist/commands/index.js +18 -0
  75. package/dist/commands/index.js.map +1 -0
  76. package/dist/commands/login.d.ts +5 -0
  77. package/dist/commands/login.d.ts.map +1 -0
  78. package/dist/commands/login.js +91 -0
  79. package/dist/commands/login.js.map +1 -0
  80. package/dist/commands/model.d.ts +14 -0
  81. package/dist/commands/model.d.ts.map +1 -0
  82. package/dist/commands/model.js +110 -0
  83. package/dist/commands/model.js.map +1 -0
  84. package/dist/commands/new.d.ts +5 -0
  85. package/dist/commands/new.d.ts.map +1 -0
  86. package/dist/commands/new.js +24 -0
  87. package/dist/commands/new.js.map +1 -0
  88. package/dist/commands/parse.d.ts +7 -0
  89. package/dist/commands/parse.d.ts.map +1 -0
  90. package/dist/commands/parse.js +17 -0
  91. package/dist/commands/parse.js.map +1 -0
  92. package/dist/commands/registry.d.ts +4 -0
  93. package/dist/commands/registry.d.ts.map +1 -0
  94. package/dist/commands/registry.js +9 -0
  95. package/dist/commands/registry.js.map +1 -0
  96. package/dist/commands/sandbox.d.ts +10 -0
  97. package/dist/commands/sandbox.d.ts.map +1 -0
  98. package/dist/commands/sandbox.js +83 -0
  99. package/dist/commands/sandbox.js.map +1 -0
  100. package/dist/commands/session-view.d.ts +5 -0
  101. package/dist/commands/session-view.d.ts.map +1 -0
  102. package/dist/commands/session-view.js +62 -0
  103. package/dist/commands/session-view.js.map +1 -0
  104. package/dist/commands/types.d.ts +41 -0
  105. package/dist/commands/types.d.ts.map +1 -0
  106. package/dist/commands/types.js +2 -0
  107. package/dist/commands/types.js.map +1 -0
  108. package/dist/commands/utils.d.ts +8 -0
  109. package/dist/commands/utils.d.ts.map +1 -0
  110. package/dist/commands/utils.js +14 -0
  111. package/dist/commands/utils.js.map +1 -0
  112. package/dist/config.d.ts +59 -0
  113. package/dist/config.d.ts.map +1 -0
  114. package/dist/config.js +370 -0
  115. package/dist/config.js.map +1 -0
  116. package/dist/context.d.ts +17 -0
  117. package/dist/context.d.ts.map +1 -0
  118. package/dist/context.js +24 -0
  119. package/dist/context.js.map +1 -0
  120. package/dist/conversation-history.d.ts +16 -0
  121. package/dist/conversation-history.d.ts.map +1 -0
  122. package/dist/conversation-history.js +144 -0
  123. package/dist/conversation-history.js.map +1 -0
  124. package/dist/download.d.ts +2 -0
  125. package/dist/download.d.ts.map +1 -0
  126. package/dist/download.js +89 -0
  127. package/dist/download.js.map +1 -0
  128. package/dist/env.d.ts +3 -0
  129. package/dist/env.d.ts.map +1 -0
  130. package/dist/env.js +12 -0
  131. package/dist/env.js.map +1 -0
  132. package/dist/events.d.ts +85 -0
  133. package/dist/events.d.ts.map +1 -0
  134. package/dist/events.js +483 -0
  135. package/dist/events.js.map +1 -0
  136. package/dist/execution-resolver.d.ts +25 -0
  137. package/dist/execution-resolver.d.ts.map +1 -0
  138. package/dist/execution-resolver.js +167 -0
  139. package/dist/execution-resolver.js.map +1 -0
  140. package/dist/file-guards.d.ts +9 -0
  141. package/dist/file-guards.d.ts.map +1 -0
  142. package/dist/file-guards.js +56 -0
  143. package/dist/file-guards.js.map +1 -0
  144. package/dist/fs-atomic.d.ts +10 -0
  145. package/dist/fs-atomic.d.ts.map +1 -0
  146. package/dist/fs-atomic.js +45 -0
  147. package/dist/fs-atomic.js.map +1 -0
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +7 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/instrument.d.ts +2 -0
  153. package/dist/instrument.d.ts.map +1 -0
  154. package/dist/instrument.js +10 -0
  155. package/dist/instrument.js.map +1 -0
  156. package/dist/log.d.ts +36 -0
  157. package/dist/log.d.ts.map +1 -0
  158. package/dist/log.js +206 -0
  159. package/dist/log.js.map +1 -0
  160. package/dist/login/index.d.ts +42 -0
  161. package/dist/login/index.d.ts.map +1 -0
  162. package/dist/login/index.js +239 -0
  163. package/dist/login/index.js.map +1 -0
  164. package/dist/login/portal.d.ts +19 -0
  165. package/dist/login/portal.d.ts.map +1 -0
  166. package/dist/login/portal.js +1544 -0
  167. package/dist/login/portal.js.map +1 -0
  168. package/dist/login/session.d.ts +26 -0
  169. package/dist/login/session.d.ts.map +1 -0
  170. package/dist/login/session.js +56 -0
  171. package/dist/login/session.js.map +1 -0
  172. package/dist/main.d.ts +3 -0
  173. package/dist/main.d.ts.map +1 -0
  174. package/dist/main.js +366 -0
  175. package/dist/main.js.map +1 -0
  176. package/dist/provisioner.d.ts +83 -0
  177. package/dist/provisioner.d.ts.map +1 -0
  178. package/dist/provisioner.js +500 -0
  179. package/dist/provisioner.js.map +1 -0
  180. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  181. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  182. package/dist/runtime/conversation-orchestrator.js +183 -0
  183. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  184. package/dist/runtime/index.d.ts +2 -0
  185. package/dist/runtime/index.d.ts.map +1 -0
  186. package/dist/runtime/index.js +2 -0
  187. package/dist/runtime/index.js.map +1 -0
  188. package/dist/runtime/session-runtime.d.ts +26 -0
  189. package/dist/runtime/session-runtime.d.ts.map +1 -0
  190. package/dist/runtime/session-runtime.js +221 -0
  191. package/dist/runtime/session-runtime.js.map +1 -0
  192. package/dist/sandbox/cloudflare.d.ts +15 -0
  193. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  194. package/dist/sandbox/cloudflare.js +138 -0
  195. package/dist/sandbox/cloudflare.js.map +1 -0
  196. package/dist/sandbox/container.d.ts +16 -0
  197. package/dist/sandbox/container.d.ts.map +1 -0
  198. package/dist/sandbox/container.js +138 -0
  199. package/dist/sandbox/container.js.map +1 -0
  200. package/dist/sandbox/errors.d.ts +6 -0
  201. package/dist/sandbox/errors.d.ts.map +1 -0
  202. package/dist/sandbox/errors.js +11 -0
  203. package/dist/sandbox/errors.js.map +1 -0
  204. package/dist/sandbox/firecracker.d.ts +17 -0
  205. package/dist/sandbox/firecracker.d.ts.map +1 -0
  206. package/dist/sandbox/firecracker.js +212 -0
  207. package/dist/sandbox/firecracker.js.map +1 -0
  208. package/dist/sandbox/host.d.ts +11 -0
  209. package/dist/sandbox/host.d.ts.map +1 -0
  210. package/dist/sandbox/host.js +89 -0
  211. package/dist/sandbox/host.js.map +1 -0
  212. package/dist/sandbox/image.d.ts +5 -0
  213. package/dist/sandbox/image.d.ts.map +1 -0
  214. package/dist/sandbox/image.js +30 -0
  215. package/dist/sandbox/image.js.map +1 -0
  216. package/dist/sandbox/index.d.ts +22 -0
  217. package/dist/sandbox/index.d.ts.map +1 -0
  218. package/dist/sandbox/index.js +54 -0
  219. package/dist/sandbox/index.js.map +1 -0
  220. package/dist/sandbox/path-context.d.ts +4 -0
  221. package/dist/sandbox/path-context.d.ts.map +1 -0
  222. package/dist/sandbox/path-context.js +20 -0
  223. package/dist/sandbox/path-context.js.map +1 -0
  224. package/dist/sandbox/types.d.ts +67 -0
  225. package/dist/sandbox/types.d.ts.map +1 -0
  226. package/dist/sandbox/types.js +2 -0
  227. package/dist/sandbox/types.js.map +1 -0
  228. package/dist/sandbox/utils.d.ts +4 -0
  229. package/dist/sandbox/utils.d.ts.map +1 -0
  230. package/dist/sandbox/utils.js +51 -0
  231. package/dist/sandbox/utils.js.map +1 -0
  232. package/dist/sentry.d.ts +50 -0
  233. package/dist/sentry.d.ts.map +1 -0
  234. package/dist/sentry.js +257 -0
  235. package/dist/sentry.js.map +1 -0
  236. package/dist/session-view/command.d.ts +5 -0
  237. package/dist/session-view/command.d.ts.map +1 -0
  238. package/dist/session-view/command.js +7 -0
  239. package/dist/session-view/command.js.map +1 -0
  240. package/dist/session-view/portal.d.ts +16 -0
  241. package/dist/session-view/portal.d.ts.map +1 -0
  242. package/dist/session-view/portal.js +1822 -0
  243. package/dist/session-view/portal.js.map +1 -0
  244. package/dist/session-view/service.d.ts +34 -0
  245. package/dist/session-view/service.d.ts.map +1 -0
  246. package/dist/session-view/service.js +434 -0
  247. package/dist/session-view/service.js.map +1 -0
  248. package/dist/session-view/store.d.ts +18 -0
  249. package/dist/session-view/store.d.ts.map +1 -0
  250. package/dist/session-view/store.js +36 -0
  251. package/dist/session-view/store.js.map +1 -0
  252. package/dist/sessions/metadata.d.ts +15 -0
  253. package/dist/sessions/metadata.d.ts.map +1 -0
  254. package/dist/sessions/metadata.js +11 -0
  255. package/dist/sessions/metadata.js.map +1 -0
  256. package/dist/sessions/policy.d.ts +13 -0
  257. package/dist/sessions/policy.d.ts.map +1 -0
  258. package/dist/sessions/policy.js +23 -0
  259. package/dist/sessions/policy.js.map +1 -0
  260. package/dist/sessions/store.d.ts +103 -0
  261. package/dist/sessions/store.d.ts.map +1 -0
  262. package/dist/sessions/store.js +349 -0
  263. package/dist/sessions/store.js.map +1 -0
  264. package/dist/store.d.ts +58 -0
  265. package/dist/store.d.ts.map +1 -0
  266. package/dist/store.js +152 -0
  267. package/dist/store.js.map +1 -0
  268. package/dist/tool-diagnostics.d.ts +2 -0
  269. package/dist/tool-diagnostics.d.ts.map +1 -0
  270. package/dist/tool-diagnostics.js +7 -0
  271. package/dist/tool-diagnostics.js.map +1 -0
  272. package/dist/tools/bash.d.ts +10 -0
  273. package/dist/tools/bash.d.ts.map +1 -0
  274. package/dist/tools/bash.js +80 -0
  275. package/dist/tools/bash.js.map +1 -0
  276. package/dist/tools/edit.d.ts +11 -0
  277. package/dist/tools/edit.d.ts.map +1 -0
  278. package/dist/tools/edit.js +133 -0
  279. package/dist/tools/edit.js.map +1 -0
  280. package/dist/tools/event.d.ts +62 -0
  281. package/dist/tools/event.d.ts.map +1 -0
  282. package/dist/tools/event.js +138 -0
  283. package/dist/tools/event.js.map +1 -0
  284. package/dist/tools/index.d.ts +14 -0
  285. package/dist/tools/index.d.ts.map +1 -0
  286. package/dist/tools/index.js +23 -0
  287. package/dist/tools/index.js.map +1 -0
  288. package/dist/tools/read.d.ts +11 -0
  289. package/dist/tools/read.d.ts.map +1 -0
  290. package/dist/tools/read.js +136 -0
  291. package/dist/tools/read.js.map +1 -0
  292. package/dist/tools/truncate.d.ts +57 -0
  293. package/dist/tools/truncate.d.ts.map +1 -0
  294. package/dist/tools/truncate.js +184 -0
  295. package/dist/tools/truncate.js.map +1 -0
  296. package/dist/tools/write.d.ts +10 -0
  297. package/dist/tools/write.d.ts.map +1 -0
  298. package/dist/tools/write.js +33 -0
  299. package/dist/tools/write.js.map +1 -0
  300. package/dist/trigger.d.ts +31 -0
  301. package/dist/trigger.d.ts.map +1 -0
  302. package/dist/trigger.js +98 -0
  303. package/dist/trigger.js.map +1 -0
  304. package/dist/ui-copy.d.ts +12 -0
  305. package/dist/ui-copy.d.ts.map +1 -0
  306. package/dist/ui-copy.js +36 -0
  307. package/dist/ui-copy.js.map +1 -0
  308. package/dist/vault-routing.d.ts +4 -0
  309. package/dist/vault-routing.d.ts.map +1 -0
  310. package/dist/vault-routing.js +16 -0
  311. package/dist/vault-routing.js.map +1 -0
  312. package/dist/vault.d.ts +72 -0
  313. package/dist/vault.d.ts.map +1 -0
  314. package/dist/vault.js +281 -0
  315. package/dist/vault.js.map +1 -0
  316. package/package.json +83 -0
package/dist/events.js ADDED
@@ -0,0 +1,483 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { Cron } from "croner";
3
+ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, watch, } from "fs";
4
+ import { readFile } from "fs/promises";
5
+ import { join } from "path";
6
+ import { ensureDirExists, parseJsonSchemaValue } from "./file-guards.js";
7
+ import * as log from "./log.js";
8
+ import { reportUserFacingError } from "./sentry.js";
9
+ import { inferConversationKind } from "./sessions/policy.js";
10
+ const EventFileSchema = Type.Object({
11
+ type: Type.Optional(Type.Union([Type.Literal("immediate"), Type.Literal("one-shot"), Type.Literal("periodic")])),
12
+ platform: Type.Optional(Type.String()),
13
+ conversationId: Type.Optional(Type.String()),
14
+ channelId: Type.Optional(Type.String()),
15
+ conversationKind: Type.Optional(Type.Union([Type.Literal("direct"), Type.Literal("shared")])),
16
+ userId: Type.Optional(Type.String()),
17
+ text: Type.Optional(Type.String()),
18
+ at: Type.Optional(Type.String()),
19
+ schedule: Type.Optional(Type.String()),
20
+ timezone: Type.Optional(Type.String()),
21
+ });
22
+ const DEBOUNCE_MS = 100;
23
+ const MAX_RETRIES = 3;
24
+ const RETRY_BASE_MS = 100;
25
+ export class EventsWatcher {
26
+ constructor(eventsDir, botsByPlatform) {
27
+ this.eventsDir = eventsDir;
28
+ this.botsByPlatform = botsByPlatform;
29
+ this.timers = new Map();
30
+ this.timerEventTypes = new Map();
31
+ this.crons = new Map();
32
+ this.debounceTimers = new Map();
33
+ this.watcher = null;
34
+ this.knownFiles = new Set();
35
+ this.startTime = Date.now();
36
+ }
37
+ /**
38
+ * Start watching for events. Call this after platform bots are initialized.
39
+ */
40
+ start() {
41
+ // Ensure events directory exists
42
+ ensureDirExists(this.eventsDir);
43
+ log.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);
44
+ // Scan existing files
45
+ this.scanExisting();
46
+ // Watch for changes
47
+ this.watcher = watch(this.eventsDir, (eventType, filename) => {
48
+ if (!filename || !filename.endsWith(".json"))
49
+ return;
50
+ log.logInfo(`Events watcher fs event: ${String(eventType)} ${filename} (exists=${existsSync(join(this.eventsDir, filename))})`);
51
+ this.debounce(filename, () => this.handleFileChange(filename));
52
+ });
53
+ log.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);
54
+ }
55
+ /**
56
+ * Stop watching and cancel all scheduled events.
57
+ */
58
+ stop() {
59
+ // Stop fs watcher
60
+ if (this.watcher) {
61
+ this.watcher.close();
62
+ this.watcher = null;
63
+ }
64
+ // Cancel all debounce timers
65
+ for (const timer of this.debounceTimers.values()) {
66
+ clearTimeout(timer);
67
+ }
68
+ this.debounceTimers.clear();
69
+ // Cancel all scheduled timers
70
+ for (const timer of this.timers.values()) {
71
+ clearTimeout(timer);
72
+ }
73
+ this.timers.clear();
74
+ this.timerEventTypes.clear();
75
+ // Cancel all cron jobs
76
+ for (const cron of this.crons.values()) {
77
+ cron.stop();
78
+ }
79
+ this.crons.clear();
80
+ this.knownFiles.clear();
81
+ log.logInfo("Events watcher stopped");
82
+ }
83
+ /**
84
+ * Return all active periodic (cron) events with their next run time.
85
+ */
86
+ getPeriodicEvents() {
87
+ const results = [];
88
+ for (const [filename, cron] of this.crons) {
89
+ const filePath = join(this.eventsDir, filename);
90
+ try {
91
+ const content = readFileSync(filePath, "utf-8");
92
+ const data = this.parseEvent(content, filename);
93
+ if (!data || data.type !== "periodic") {
94
+ continue;
95
+ }
96
+ const next = cron.nextRun();
97
+ results.push({
98
+ filename,
99
+ platform: data.platform,
100
+ conversationId: data.conversationId,
101
+ conversationKind: data.conversationKind,
102
+ text: data.text,
103
+ schedule: data.schedule,
104
+ timezone: data.timezone,
105
+ nextRun: next?.toISOString() ?? null,
106
+ });
107
+ }
108
+ catch {
109
+ // File may have been deleted or corrupted, skip
110
+ }
111
+ }
112
+ return results;
113
+ }
114
+ debounce(filename, fn) {
115
+ const existing = this.debounceTimers.get(filename);
116
+ if (existing) {
117
+ clearTimeout(existing);
118
+ }
119
+ this.debounceTimers.set(filename, setTimeout(() => {
120
+ this.debounceTimers.delete(filename);
121
+ fn();
122
+ }, DEBOUNCE_MS));
123
+ }
124
+ scanExisting() {
125
+ let files;
126
+ try {
127
+ files = readdirSync(this.eventsDir).filter((f) => f.endsWith(".json"));
128
+ }
129
+ catch (err) {
130
+ log.logWarning("Failed to read events directory", String(err));
131
+ return;
132
+ }
133
+ for (const filename of files) {
134
+ this.handleFile(filename);
135
+ }
136
+ }
137
+ handleFileChange(filename) {
138
+ const filePath = join(this.eventsDir, filename);
139
+ const exists = existsSync(filePath);
140
+ const known = this.knownFiles.has(filename);
141
+ log.logInfo(`Handling event file change: ${filename} (exists=${exists}, known=${known})`);
142
+ if (!exists) {
143
+ // fs.watch can briefly report a file as missing during create/rename churn.
144
+ // Confirm deletion before canceling scheduled events.
145
+ void this.handleDelete(filename);
146
+ }
147
+ else if (known) {
148
+ // File was modified - cancel existing and re-schedule
149
+ this.cancelScheduled(filename, "file-modified");
150
+ void this.handleFile(filename);
151
+ }
152
+ else {
153
+ // New file
154
+ void this.handleFile(filename);
155
+ }
156
+ }
157
+ async handleDelete(filename) {
158
+ if (!this.knownFiles.has(filename))
159
+ return;
160
+ const filePath = join(this.eventsDir, filename);
161
+ for (let i = 0; i < MAX_RETRIES; i++) {
162
+ const delay = RETRY_BASE_MS * 2 ** i;
163
+ await this.sleep(delay);
164
+ const exists = existsSync(filePath);
165
+ log.logInfo(`Confirming event deletion: ${filename} after ${delay}ms (exists=${exists})`);
166
+ if (exists) {
167
+ return;
168
+ }
169
+ }
170
+ if (this.timerEventTypes.get(filename) === "one-shot" && this.timers.has(filename)) {
171
+ log.logInfo(`Ignoring deleted one-shot file after scheduling: ${filename} (timer remains active)`);
172
+ return;
173
+ }
174
+ log.logInfo(`Event file deleted: ${filename}`);
175
+ this.cancelScheduled(filename, "confirmed-delete");
176
+ this.knownFiles.delete(filename);
177
+ }
178
+ cancelScheduled(filename, reason = "unspecified") {
179
+ const timer = this.timers.get(filename);
180
+ const cron = this.crons.get(filename);
181
+ log.logInfo(`Canceling scheduled event: ${filename} (reason=${reason}, timer=${Boolean(timer)}, cron=${Boolean(cron)})`);
182
+ if (timer) {
183
+ clearTimeout(timer);
184
+ this.timers.delete(filename);
185
+ this.timerEventTypes.delete(filename);
186
+ }
187
+ if (cron) {
188
+ cron.stop();
189
+ this.crons.delete(filename);
190
+ }
191
+ }
192
+ async handleFile(filename) {
193
+ const filePath = join(this.eventsDir, filename);
194
+ log.logInfo(`Loading event file: ${filename} from ${filePath}`);
195
+ // Parse with retries
196
+ let event = null;
197
+ let lastError = null;
198
+ for (let i = 0; i < MAX_RETRIES; i++) {
199
+ try {
200
+ const content = await readFile(filePath, "utf-8");
201
+ event = this.parseEvent(content, filename);
202
+ break;
203
+ }
204
+ catch (err) {
205
+ lastError = err instanceof Error ? err : new Error(String(err));
206
+ if (i < MAX_RETRIES - 1) {
207
+ await this.sleep(RETRY_BASE_MS * 2 ** i);
208
+ }
209
+ }
210
+ }
211
+ if (!event) {
212
+ log.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);
213
+ this.deleteFile(filename, "parse-failed");
214
+ return;
215
+ }
216
+ this.knownFiles.add(filename);
217
+ log.logInfo(`Parsed event file: ${filename} (${event.type} for ${event.platform}/${event.conversationId})`);
218
+ // Schedule based on type
219
+ switch (event.type) {
220
+ case "immediate":
221
+ this.handleImmediate(filename, event);
222
+ break;
223
+ case "one-shot":
224
+ this.handleOneShot(filename, event);
225
+ break;
226
+ case "periodic":
227
+ this.handlePeriodic(filename, event);
228
+ break;
229
+ }
230
+ }
231
+ parseEvent(content, filename) {
232
+ const data = parseJsonSchemaValue(content, EventFileSchema, (detail) => detail === "unexpected JSON shape"
233
+ ? `Expected top-level JSON object in ${filename}`
234
+ : `Malformed event file ${filename}: ${detail}`);
235
+ const conversationId = typeof data.conversationId === "string"
236
+ ? data.conversationId
237
+ : typeof data.channelId === "string"
238
+ ? data.channelId
239
+ : undefined;
240
+ const type = typeof data.type === "string" ? data.type : undefined;
241
+ const text = typeof data.text === "string" ? data.text : undefined;
242
+ if (!type || !conversationId || !text) {
243
+ throw new Error(`Missing required fields (type, conversationId, text) in ${filename}`);
244
+ }
245
+ const platform = this.resolvePlatform(data.platform, filename);
246
+ const conversationKind = this.resolveConversationKind(platform, conversationId, data.conversationKind);
247
+ const userId = typeof data.userId === "string" ? data.userId : undefined;
248
+ switch (type) {
249
+ case "immediate":
250
+ return {
251
+ type: "immediate",
252
+ platform,
253
+ conversationId,
254
+ conversationKind,
255
+ userId,
256
+ text,
257
+ };
258
+ case "one-shot":
259
+ if (typeof data.at !== "string" || data.at.length === 0) {
260
+ throw new Error(`Missing 'at' field for one-shot event in ${filename}`);
261
+ }
262
+ return {
263
+ type: "one-shot",
264
+ platform,
265
+ conversationId,
266
+ conversationKind,
267
+ userId,
268
+ text,
269
+ at: data.at,
270
+ };
271
+ case "periodic":
272
+ if (typeof data.schedule !== "string" || data.schedule.length === 0) {
273
+ throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);
274
+ }
275
+ if (typeof data.timezone !== "string" || data.timezone.length === 0) {
276
+ throw new Error(`Missing 'timezone' field for periodic event in ${filename}`);
277
+ }
278
+ return {
279
+ type: "periodic",
280
+ platform,
281
+ conversationId,
282
+ conversationKind,
283
+ userId,
284
+ text,
285
+ schedule: data.schedule,
286
+ timezone: data.timezone,
287
+ };
288
+ default:
289
+ throw new Error(`Unknown event type '${type}' in ${filename}`);
290
+ }
291
+ }
292
+ resolvePlatform(platformValue, filename) {
293
+ const availablePlatforms = Object.keys(this.botsByPlatform);
294
+ if (typeof platformValue === "string" && platformValue.trim().length > 0) {
295
+ const platform = platformValue.trim().toLowerCase();
296
+ if (!this.botsByPlatform[platform]) {
297
+ throw new Error(`Unknown platform '${platformValue}' in ${filename}. Expected one of: ${availablePlatforms.join(", ")}`);
298
+ }
299
+ return platform;
300
+ }
301
+ if (availablePlatforms.length === 1) {
302
+ return availablePlatforms[0];
303
+ }
304
+ throw new Error(`Missing required field 'platform' in ${filename}. Available platforms: ${availablePlatforms.join(", ")}`);
305
+ }
306
+ resolveConversationKind(platform, conversationId, conversationKindValue) {
307
+ if (conversationKindValue === "direct" || conversationKindValue === "shared") {
308
+ return conversationKindValue;
309
+ }
310
+ return inferConversationKind(platform, conversationId);
311
+ }
312
+ handleImmediate(filename, event) {
313
+ const filePath = join(this.eventsDir, filename);
314
+ // Check if stale (created before harness started)
315
+ try {
316
+ const stat = statSync(filePath);
317
+ if (stat.mtimeMs < this.startTime) {
318
+ log.logInfo(`Stale immediate event, deleting: ${filename}`);
319
+ this.deleteFile(filename, "stale-immediate");
320
+ return;
321
+ }
322
+ }
323
+ catch {
324
+ // File may have been deleted
325
+ return;
326
+ }
327
+ log.logInfo(`Executing immediate event: ${filename}`);
328
+ this.execute(filename, event);
329
+ }
330
+ handleOneShot(filename, event) {
331
+ const atTime = new Date(event.at).getTime();
332
+ const now = Date.now();
333
+ if (atTime <= now) {
334
+ // Past - delete without executing
335
+ log.logInfo(`One-shot event in the past, deleting: ${filename}`);
336
+ this.deleteFile(filename, "one-shot-in-past");
337
+ return;
338
+ }
339
+ const delay = atTime - now;
340
+ log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s (at=${event.at}, now=${new Date(now).toISOString()})`);
341
+ const timer = setTimeout(() => {
342
+ this.timers.delete(filename);
343
+ this.timerEventTypes.delete(filename);
344
+ log.logInfo(`Executing one-shot event: ${filename}`);
345
+ this.execute(filename, event);
346
+ }, delay);
347
+ this.timers.set(filename, timer);
348
+ this.timerEventTypes.set(filename, "one-shot");
349
+ log.logInfo(`Stored one-shot timer: ${filename} (active timers=${this.timers.size})`);
350
+ }
351
+ handlePeriodic(filename, event) {
352
+ try {
353
+ const cron = new Cron(event.schedule, { timezone: event.timezone }, () => {
354
+ log.logInfo(`Executing periodic event: ${filename}`);
355
+ this.execute(filename, event, false); // Don't delete periodic events
356
+ });
357
+ this.crons.set(filename, cron);
358
+ const next = cron.nextRun();
359
+ log.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? "unknown"}`);
360
+ }
361
+ catch (err) {
362
+ log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));
363
+ this.deleteFile(filename, "invalid-cron");
364
+ }
365
+ }
366
+ execute(filename, event, deleteAfter = true) {
367
+ const message = this.buildEventPrompt(event);
368
+ const bot = this.botsByPlatform[event.platform];
369
+ if (!bot) {
370
+ log.logWarning(`No bot configured for event platform '${event.platform}'`, filename);
371
+ reportUserFacingError(new Error("Scheduled event delivery failed: missing bot"), {
372
+ domain: "events",
373
+ surface: "event_delivery",
374
+ operation: "event_execute",
375
+ severity: "error",
376
+ platform: event.platform,
377
+ context: {
378
+ failure: "missing_bot",
379
+ filename,
380
+ eventType: event.type,
381
+ conversationId: event.conversationId,
382
+ conversationKind: event.conversationKind,
383
+ deleteAfter,
384
+ triggeredByEventFile: true,
385
+ textLength: event.text.length,
386
+ },
387
+ });
388
+ if (deleteAfter) {
389
+ this.deleteFile(filename, "missing-bot");
390
+ }
391
+ return;
392
+ }
393
+ const eventId = filename.replace(/\.json$/i, "");
394
+ const botEvent = {
395
+ type: "mention",
396
+ conversationId: event.conversationId,
397
+ conversationKind: event.conversationKind,
398
+ user: event.userId ?? "EVENT",
399
+ text: message,
400
+ ts: `event:${eventId}`,
401
+ };
402
+ // Enqueue for processing
403
+ const enqueued = bot.enqueueEvent(botEvent);
404
+ if (enqueued && deleteAfter) {
405
+ // Delete file after successful enqueue (immediate and one-shot)
406
+ this.deleteFile(filename, "executed-and-enqueued");
407
+ }
408
+ else if (!enqueued) {
409
+ log.logWarning(`Event queue full, discarded: ${filename}`);
410
+ reportUserFacingError(new Error("Scheduled event delivery failed: queue full"), {
411
+ domain: "events",
412
+ surface: "event_delivery",
413
+ operation: "event_execute",
414
+ severity: "error",
415
+ platform: event.platform,
416
+ context: {
417
+ failure: "queue_full",
418
+ filename,
419
+ eventType: event.type,
420
+ conversationId: event.conversationId,
421
+ conversationKind: event.conversationKind,
422
+ deleteAfter,
423
+ triggeredByEventFile: true,
424
+ textLength: event.text.length,
425
+ },
426
+ });
427
+ // Still delete immediate/one-shot even if discarded
428
+ if (deleteAfter) {
429
+ this.deleteFile(filename, "queue-full-discarded");
430
+ }
431
+ }
432
+ }
433
+ buildEventPrompt(event) {
434
+ switch (event.type) {
435
+ case "one-shot":
436
+ return [
437
+ "Please deliver the following reminder to the user in a short, natural way.",
438
+ "Do not greet, do not introduce yourself, and do not ask generic follow-up questions.",
439
+ "",
440
+ `Reminder: ${event.text}`,
441
+ ].join("\n");
442
+ case "periodic":
443
+ return [
444
+ "Handle the following recurring task.",
445
+ "Respond concisely. If there is nothing actionable to report, reply with [SILENT].",
446
+ "",
447
+ `Task: ${event.text}`,
448
+ ].join("\n");
449
+ case "immediate":
450
+ return [
451
+ "Handle the following event/update in a concise, context-appropriate way.",
452
+ "If it reads like a reminder or follow-up, deliver it directly without greeting or generic offers to help.",
453
+ "",
454
+ `Event: ${event.text}`,
455
+ ].join("\n");
456
+ }
457
+ }
458
+ deleteFile(filename, reason = "unspecified") {
459
+ const filePath = join(this.eventsDir, filename);
460
+ log.logInfo(`Deleting event file: ${filename} (reason=${reason})`);
461
+ try {
462
+ unlinkSync(filePath);
463
+ }
464
+ catch (err) {
465
+ // ENOENT is fine (file already deleted), other errors are warnings
466
+ if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
467
+ log.logWarning(`Failed to delete event file: ${filename}`, String(err));
468
+ }
469
+ }
470
+ this.knownFiles.delete(filename);
471
+ }
472
+ sleep(ms) {
473
+ return new Promise((resolve) => setTimeout(resolve, ms));
474
+ }
475
+ }
476
+ /**
477
+ * Create an events watcher for all configured platforms.
478
+ */
479
+ export function createEventsWatcher(workspaceDir, botsByPlatform) {
480
+ const eventsDir = join(workspaceDir, "events");
481
+ return new EventsWatcher(eventsDir, botsByPlatform);
482
+ }
483
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { Type, type Static } from \"@sinclair/typebox\";\nimport { Cron } from \"croner\";\nimport {\n existsSync,\n type FSWatcher,\n readdirSync,\n readFileSync,\n statSync,\n unlinkSync,\n watch,\n} from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport type { Bot, BotEvent, ConversationKind } from \"./adapter.js\";\nimport { ensureDirExists, parseJsonSchemaValue } from \"./file-guards.js\";\nimport * as log from \"./log.js\";\nimport { reportUserFacingError } from \"./sentry.js\";\nimport { inferConversationKind } from \"./sessions/policy.js\";\n\nexport interface ImmediateEvent {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n /** Creator userId — routes tool execution to that user's vault selection when fired. */\n userId?: string;\n text: string;\n}\n\nexport interface OneShotEvent {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n at: string; // ISO 8601 with timezone offset\n // No sessionKey or threadTs: reminders fire as top-level messages regardless of where they were created.\n}\n\nexport interface PeriodicEvent {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n userId?: string;\n text: string;\n schedule: string; // cron syntax\n timezone: string; // IANA timezone\n}\n\nexport type MikanEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\nconst EventFileSchema = Type.Object({\n type: Type.Optional(\n Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n ),\n platform: Type.Optional(Type.String()),\n conversationId: Type.Optional(Type.String()),\n channelId: Type.Optional(Type.String()),\n conversationKind: Type.Optional(Type.Union([Type.Literal(\"direct\"), Type.Literal(\"shared\")])),\n userId: Type.Optional(Type.String()),\n text: Type.Optional(Type.String()),\n at: Type.Optional(Type.String()),\n schedule: Type.Optional(Type.String()),\n timezone: Type.Optional(Type.String()),\n});\n\ntype EventFileData = Static<typeof EventFileSchema>;\n\nexport interface PeriodicEventInfo {\n filename: string;\n platform: string;\n conversationId: string;\n conversationKind: ConversationKind;\n text: string;\n schedule: string;\n timezone: string;\n nextRun: string | null; // ISO 8601\n}\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n private timers: Map<string, NodeJS.Timeout> = new Map();\n private timerEventTypes: Map<string, \"one-shot\"> = new Map();\n private crons: Map<string, Cron> = new Map();\n private debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n private startTime: number;\n private watcher: FSWatcher | null = null;\n private knownFiles: Set<string> = new Set();\n\n constructor(\n private eventsDir: string,\n private botsByPlatform: Record<string, Bot>,\n ) {\n this.startTime = Date.now();\n }\n\n /**\n * Start watching for events. Call this after platform bots are initialized.\n */\n start(): void {\n // Ensure events directory exists\n ensureDirExists(this.eventsDir);\n\n log.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n // Scan existing files\n this.scanExisting();\n\n // Watch for changes\n this.watcher = watch(this.eventsDir, (eventType, filename) => {\n if (!filename || !filename.endsWith(\".json\")) return;\n log.logInfo(\n `Events watcher fs event: ${String(eventType)} ${filename} (exists=${existsSync(join(this.eventsDir, filename))})`,\n );\n this.debounce(filename, () => this.handleFileChange(filename));\n });\n\n log.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n }\n\n /**\n * Stop watching and cancel all scheduled events.\n */\n stop(): void {\n // Stop fs watcher\n if (this.watcher) {\n this.watcher.close();\n this.watcher = null;\n }\n\n // Cancel all debounce timers\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n\n // Cancel all scheduled timers\n for (const timer of this.timers.values()) {\n clearTimeout(timer);\n }\n this.timers.clear();\n this.timerEventTypes.clear();\n\n // Cancel all cron jobs\n for (const cron of this.crons.values()) {\n cron.stop();\n }\n this.crons.clear();\n\n this.knownFiles.clear();\n log.logInfo(\"Events watcher stopped\");\n }\n\n /**\n * Return all active periodic (cron) events with their next run time.\n */\n getPeriodicEvents(): PeriodicEventInfo[] {\n const results: PeriodicEventInfo[] = [];\n for (const [filename, cron] of this.crons) {\n const filePath = join(this.eventsDir, filename);\n try {\n const content = readFileSync(filePath, \"utf-8\");\n const data = this.parseEvent(content, filename);\n if (!data || data.type !== \"periodic\") {\n continue;\n }\n const next = cron.nextRun();\n results.push({\n filename,\n platform: data.platform,\n conversationId: data.conversationId,\n conversationKind: data.conversationKind,\n text: data.text,\n schedule: data.schedule,\n timezone: data.timezone,\n nextRun: next?.toISOString() ?? null,\n });\n } catch {\n // File may have been deleted or corrupted, skip\n }\n }\n return results;\n }\n\n private debounce(filename: string, fn: () => void): void {\n const existing = this.debounceTimers.get(filename);\n if (existing) {\n clearTimeout(existing);\n }\n this.debounceTimers.set(\n filename,\n setTimeout(() => {\n this.debounceTimers.delete(filename);\n fn();\n }, DEBOUNCE_MS),\n );\n }\n\n private scanExisting(): void {\n let files: string[];\n try {\n files = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n } catch (err) {\n log.logWarning(\"Failed to read events directory\", String(err));\n return;\n }\n\n for (const filename of files) {\n this.handleFile(filename);\n }\n }\n\n private handleFileChange(filename: string): void {\n const filePath = join(this.eventsDir, filename);\n const exists = existsSync(filePath);\n const known = this.knownFiles.has(filename);\n log.logInfo(`Handling event file change: ${filename} (exists=${exists}, known=${known})`);\n\n if (!exists) {\n // fs.watch can briefly report a file as missing during create/rename churn.\n // Confirm deletion before canceling scheduled events.\n void this.handleDelete(filename);\n } else if (known) {\n // File was modified - cancel existing and re-schedule\n this.cancelScheduled(filename, \"file-modified\");\n void this.handleFile(filename);\n } else {\n // New file\n void this.handleFile(filename);\n }\n }\n\n private async handleDelete(filename: string): Promise<void> {\n if (!this.knownFiles.has(filename)) return;\n\n const filePath = join(this.eventsDir, filename);\n for (let i = 0; i < MAX_RETRIES; i++) {\n const delay = RETRY_BASE_MS * 2 ** i;\n await this.sleep(delay);\n const exists = existsSync(filePath);\n log.logInfo(`Confirming event deletion: ${filename} after ${delay}ms (exists=${exists})`);\n if (exists) {\n return;\n }\n }\n\n if (this.timerEventTypes.get(filename) === \"one-shot\" && this.timers.has(filename)) {\n log.logInfo(\n `Ignoring deleted one-shot file after scheduling: ${filename} (timer remains active)`,\n );\n return;\n }\n\n log.logInfo(`Event file deleted: ${filename}`);\n this.cancelScheduled(filename, \"confirmed-delete\");\n this.knownFiles.delete(filename);\n }\n\n private cancelScheduled(filename: string, reason = \"unspecified\"): void {\n const timer = this.timers.get(filename);\n const cron = this.crons.get(filename);\n log.logInfo(\n `Canceling scheduled event: ${filename} (reason=${reason}, timer=${Boolean(timer)}, cron=${Boolean(cron)})`,\n );\n if (timer) {\n clearTimeout(timer);\n this.timers.delete(filename);\n this.timerEventTypes.delete(filename);\n }\n\n if (cron) {\n cron.stop();\n this.crons.delete(filename);\n }\n }\n\n private async handleFile(filename: string): Promise<void> {\n const filePath = join(this.eventsDir, filename);\n log.logInfo(`Loading event file: ${filename} from ${filePath}`);\n\n // Parse with retries\n let event: MikanEvent | null = null;\n let lastError: Error | null = null;\n\n for (let i = 0; i < MAX_RETRIES; i++) {\n try {\n const content = await readFile(filePath, \"utf-8\");\n event = this.parseEvent(content, filename);\n break;\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n if (i < MAX_RETRIES - 1) {\n await this.sleep(RETRY_BASE_MS * 2 ** i);\n }\n }\n }\n\n if (!event) {\n log.logWarning(\n `Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`,\n lastError?.message,\n );\n this.deleteFile(filename, \"parse-failed\");\n return;\n }\n\n this.knownFiles.add(filename);\n log.logInfo(\n `Parsed event file: ${filename} (${event.type} for ${event.platform}/${event.conversationId})`,\n );\n\n // Schedule based on type\n switch (event.type) {\n case \"immediate\":\n this.handleImmediate(filename, event);\n break;\n case \"one-shot\":\n this.handleOneShot(filename, event);\n break;\n case \"periodic\":\n this.handlePeriodic(filename, event);\n break;\n }\n }\n\n private parseEvent(content: string, filename: string): MikanEvent | null {\n const data: EventFileData = parseJsonSchemaValue(content, EventFileSchema, (detail) =>\n detail === \"unexpected JSON shape\"\n ? `Expected top-level JSON object in ${filename}`\n : `Malformed event file ${filename}: ${detail}`,\n );\n const conversationId =\n typeof data.conversationId === \"string\"\n ? data.conversationId\n : typeof data.channelId === \"string\"\n ? data.channelId\n : undefined;\n const type = typeof data.type === \"string\" ? data.type : undefined;\n const text = typeof data.text === \"string\" ? data.text : undefined;\n\n if (!type || !conversationId || !text) {\n throw new Error(`Missing required fields (type, conversationId, text) in ${filename}`);\n }\n\n const platform = this.resolvePlatform(data.platform, filename);\n const conversationKind = this.resolveConversationKind(\n platform,\n conversationId,\n data.conversationKind,\n );\n const userId = typeof data.userId === \"string\" ? data.userId : undefined;\n switch (type) {\n case \"immediate\":\n return {\n type: \"immediate\",\n platform,\n conversationId,\n conversationKind,\n userId,\n text,\n };\n\n case \"one-shot\":\n if (typeof data.at !== \"string\" || data.at.length === 0) {\n throw new Error(`Missing 'at' field for one-shot event in ${filename}`);\n }\n return {\n type: \"one-shot\",\n platform,\n conversationId,\n conversationKind,\n userId,\n text,\n at: data.at,\n };\n\n case \"periodic\":\n if (typeof data.schedule !== \"string\" || data.schedule.length === 0) {\n throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n }\n if (typeof data.timezone !== \"string\" || data.timezone.length === 0) {\n throw new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n }\n return {\n type: \"periodic\",\n platform,\n conversationId,\n conversationKind,\n userId,\n text,\n schedule: data.schedule,\n timezone: data.timezone,\n };\n\n default:\n throw new Error(`Unknown event type '${type}' in ${filename}`);\n }\n }\n\n private resolvePlatform(platformValue: unknown, filename: string): string {\n const availablePlatforms = Object.keys(this.botsByPlatform);\n\n if (typeof platformValue === \"string\" && platformValue.trim().length > 0) {\n const platform = platformValue.trim().toLowerCase();\n if (!this.botsByPlatform[platform]) {\n throw new Error(\n `Unknown platform '${platformValue}' in ${filename}. Expected one of: ${availablePlatforms.join(\", \")}`,\n );\n }\n return platform;\n }\n\n if (availablePlatforms.length === 1) {\n return availablePlatforms[0];\n }\n\n throw new Error(\n `Missing required field 'platform' in ${filename}. Available platforms: ${availablePlatforms.join(\", \")}`,\n );\n }\n\n private resolveConversationKind(\n platform: string,\n conversationId: string,\n conversationKindValue: unknown,\n ): ConversationKind {\n if (conversationKindValue === \"direct\" || conversationKindValue === \"shared\") {\n return conversationKindValue;\n }\n\n return inferConversationKind(platform, conversationId);\n }\n\n private handleImmediate(filename: string, event: ImmediateEvent): void {\n const filePath = join(this.eventsDir, filename);\n\n // Check if stale (created before harness started)\n try {\n const stat = statSync(filePath);\n if (stat.mtimeMs < this.startTime) {\n log.logInfo(`Stale immediate event, deleting: ${filename}`);\n this.deleteFile(filename, \"stale-immediate\");\n return;\n }\n } catch {\n // File may have been deleted\n return;\n }\n\n log.logInfo(`Executing immediate event: ${filename}`);\n this.execute(filename, event);\n }\n\n private handleOneShot(filename: string, event: OneShotEvent): void {\n const atTime = new Date(event.at).getTime();\n const now = Date.now();\n\n if (atTime <= now) {\n // Past - delete without executing\n log.logInfo(`One-shot event in the past, deleting: ${filename}`);\n this.deleteFile(filename, \"one-shot-in-past\");\n return;\n }\n\n const delay = atTime - now;\n log.logInfo(\n `Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s (at=${event.at}, now=${new Date(now).toISOString()})`,\n );\n\n const timer = setTimeout(() => {\n this.timers.delete(filename);\n this.timerEventTypes.delete(filename);\n log.logInfo(`Executing one-shot event: ${filename}`);\n this.execute(filename, event);\n }, delay);\n\n this.timers.set(filename, timer);\n this.timerEventTypes.set(filename, \"one-shot\");\n log.logInfo(`Stored one-shot timer: ${filename} (active timers=${this.timers.size})`);\n }\n\n private handlePeriodic(filename: string, event: PeriodicEvent): void {\n try {\n const cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n log.logInfo(`Executing periodic event: ${filename}`);\n this.execute(filename, event, false); // Don't delete periodic events\n });\n\n this.crons.set(filename, cron);\n\n const next = cron.nextRun();\n log.logInfo(\n `Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`,\n );\n } catch (err) {\n log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n this.deleteFile(filename, \"invalid-cron\");\n }\n }\n\n private execute(filename: string, event: MikanEvent, deleteAfter: boolean = true): void {\n const message = this.buildEventPrompt(event);\n const bot = this.botsByPlatform[event.platform];\n\n if (!bot) {\n log.logWarning(`No bot configured for event platform '${event.platform}'`, filename);\n reportUserFacingError(new Error(\"Scheduled event delivery failed: missing bot\"), {\n domain: \"events\",\n surface: \"event_delivery\",\n operation: \"event_execute\",\n severity: \"error\",\n platform: event.platform,\n context: {\n failure: \"missing_bot\",\n filename,\n eventType: event.type,\n conversationId: event.conversationId,\n conversationKind: event.conversationKind,\n deleteAfter,\n triggeredByEventFile: true,\n textLength: event.text.length,\n },\n });\n if (deleteAfter) {\n this.deleteFile(filename, \"missing-bot\");\n }\n return;\n }\n\n const eventId = filename.replace(/\\.json$/i, \"\");\n const botEvent: BotEvent = {\n type: \"mention\",\n conversationId: event.conversationId,\n conversationKind: event.conversationKind,\n user: event.userId ?? \"EVENT\",\n text: message,\n ts: `event:${eventId}`,\n };\n\n // Enqueue for processing\n const enqueued = bot.enqueueEvent(botEvent);\n\n if (enqueued && deleteAfter) {\n // Delete file after successful enqueue (immediate and one-shot)\n this.deleteFile(filename, \"executed-and-enqueued\");\n } else if (!enqueued) {\n log.logWarning(`Event queue full, discarded: ${filename}`);\n reportUserFacingError(new Error(\"Scheduled event delivery failed: queue full\"), {\n domain: \"events\",\n surface: \"event_delivery\",\n operation: \"event_execute\",\n severity: \"error\",\n platform: event.platform,\n context: {\n failure: \"queue_full\",\n filename,\n eventType: event.type,\n conversationId: event.conversationId,\n conversationKind: event.conversationKind,\n deleteAfter,\n triggeredByEventFile: true,\n textLength: event.text.length,\n },\n });\n // Still delete immediate/one-shot even if discarded\n if (deleteAfter) {\n this.deleteFile(filename, \"queue-full-discarded\");\n }\n }\n }\n\n private buildEventPrompt(event: MikanEvent): string {\n switch (event.type) {\n case \"one-shot\":\n return [\n \"Please deliver the following reminder to the user in a short, natural way.\",\n \"Do not greet, do not introduce yourself, and do not ask generic follow-up questions.\",\n \"\",\n `Reminder: ${event.text}`,\n ].join(\"\\n\");\n case \"periodic\":\n return [\n \"Handle the following recurring task.\",\n \"Respond concisely. If there is nothing actionable to report, reply with [SILENT].\",\n \"\",\n `Task: ${event.text}`,\n ].join(\"\\n\");\n case \"immediate\":\n return [\n \"Handle the following event/update in a concise, context-appropriate way.\",\n \"If it reads like a reminder or follow-up, deliver it directly without greeting or generic offers to help.\",\n \"\",\n `Event: ${event.text}`,\n ].join(\"\\n\");\n }\n }\n\n private deleteFile(filename: string, reason = \"unspecified\"): void {\n const filePath = join(this.eventsDir, filename);\n log.logInfo(`Deleting event file: ${filename} (reason=${reason})`);\n try {\n unlinkSync(filePath);\n } catch (err) {\n // ENOENT is fine (file already deleted), other errors are warnings\n if (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n log.logWarning(`Failed to delete event file: ${filename}`, String(err));\n }\n }\n this.knownFiles.delete(filename);\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n/**\n * Create an events watcher for all configured platforms.\n */\nexport function createEventsWatcher(\n workspaceDir: string,\n botsByPlatform: Record<string, Bot>,\n): EventsWatcher {\n const eventsDir = join(workspaceDir, \"events\");\n return new EventsWatcher(eventsDir, botsByPlatform);\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import { DockerContainerManager } from "./provisioner.js";
2
+ import { type Executor, type SandboxConfig } from "./sandbox/index.js";
3
+ import { type VaultManager } from "./vault.js";
4
+ export interface ActorContext {
5
+ platform: string;
6
+ userId: string;
7
+ conversationId: string;
8
+ }
9
+ export type ImageWorkspaceMountMode = "private" | "full";
10
+ export declare function readConversationWorkspaceMountMode(workspaceDir: string | undefined, conversationId: string): ImageWorkspaceMountMode;
11
+ export declare class ActorExecutionResolver {
12
+ private baseConfig;
13
+ private vaultManager;
14
+ private provisioner?;
15
+ private workspaceDir?;
16
+ private readonly ensuredConversationDirs;
17
+ constructor(baseConfig: SandboxConfig, vaultManager: VaultManager, provisioner?: DockerContainerManager | undefined, workspaceDir?: string | undefined);
18
+ resolve(context: ActorContext): Promise<Executor>;
19
+ private ensureDefaultSharedVault;
20
+ private resolveSandboxConfig;
21
+ private buildEnsureReadyCallback;
22
+ private resolveMounts;
23
+ private buildImageSandboxMounts;
24
+ }
25
+ //# sourceMappingURL=execution-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution-resolver.d.ts","sourceRoot":"","sources":["../src/execution-resolver.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAuB,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEvF,OAAO,EAAgD,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAG7F,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,wBAAgB,kCAAkC,CAChD,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,cAAc,EAAE,MAAM,GACrB,uBAAuB,CAgBzB;AAyBD,qBAAa,sBAAsB;IAI/B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,WAAW,CAAC;IACpB,OAAO,CAAC,YAAY,CAAC;IANvB,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;IAE7D,YACU,UAAU,EAAE,aAAa,EACzB,YAAY,EAAE,YAAY,EAC1B,WAAW,CAAC,EAAE,sBAAsB,YAAA,EACpC,YAAY,CAAC,EAAE,MAAM,YAAA,EAC3B;IAEE,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAatD;IAED,OAAO,CAAC,wBAAwB;IAehC,OAAO,CAAC,oBAAoB;IAgB5B,OAAO,CAAC,wBAAwB;IA4ChC,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,uBAAuB;CAsBhC","sourcesContent":["import { existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { loadAgentConfig, loadAgentConfigForConversation } from \"./config.js\";\nimport { ensureDirExists, isRecord, readJsonFileIfExists } from \"./file-guards.js\";\nimport { DockerContainerManager, type ContainerMount } from \"./provisioner.js\";\nimport { createExecutor, type Executor, type SandboxConfig } from \"./sandbox/index.js\";\nimport { reportUserFacingError } from \"./sentry.js\";\nimport { normalizeSharedVaultName, type ResolvedVault, type VaultManager } from \"./vault.js\";\nimport { resolveActorVaultKey } from \"./vault-routing.js\";\n\nexport interface ActorContext {\n platform: string;\n userId: string;\n conversationId: string;\n}\n\nexport type ImageWorkspaceMountMode = \"private\" | \"full\";\n\nexport function readConversationWorkspaceMountMode(\n workspaceDir: string | undefined,\n conversationId: string,\n): ImageWorkspaceMountMode {\n const globalDefault = readGlobalWorkspaceMountMode();\n if (!workspaceDir) {\n return globalDefault;\n }\n\n const conversationDir = join(workspaceDir, conversationId);\n try {\n return (\n loadAgentConfigForConversation(conversationDir).sandboxImageWorkspaceMount ?? globalDefault\n );\n } catch {\n const conversationSettingsPath = join(conversationDir, \"settings.json\");\n const raw = readConversationSettingsFallback(conversationSettingsPath);\n return raw?.sandbox?.image?.workspaceMount ?? globalDefault;\n }\n}\n\nfunction readGlobalWorkspaceMountMode(): ImageWorkspaceMountMode {\n try {\n return loadAgentConfig().sandboxImageWorkspaceMount ?? \"private\";\n } catch {\n return \"private\";\n }\n}\n\nfunction readConversationSettingsFallback(\n settingsPath: string,\n): { sandbox?: { image?: { workspaceMount?: ImageWorkspaceMountMode } } } | undefined {\n try {\n return readJsonFileIfExists(\n settingsPath,\n (value): value is { sandbox?: { image?: { workspaceMount?: ImageWorkspaceMountMode } } } =>\n isRecord(value),\n () => \"Ignoring malformed conversation settings file while resolving workspace mount\",\n );\n } catch {\n return undefined;\n }\n}\n\nexport class ActorExecutionResolver {\n private readonly ensuredConversationDirs = new Set<string>();\n\n constructor(\n private baseConfig: SandboxConfig,\n private vaultManager: VaultManager,\n private provisioner?: DockerContainerManager,\n private workspaceDir?: string,\n ) {}\n\n async resolve(context: ActorContext): Promise<Executor> {\n const vaultKey = resolveActorVaultKey(this.baseConfig, context.userId, context.conversationId);\n this.ensureDefaultSharedVault(vaultKey);\n\n const vault = this.vaultManager.resolve(vaultKey);\n const config = this.resolveSandboxConfig(vaultKey);\n const env =\n config.type !== \"host\" && vault && Object.keys(vault.env).length > 0 ? vault.env : undefined;\n return createExecutor(\n config,\n env,\n this.buildEnsureReadyCallback(vaultKey, context.conversationId, config, vault),\n );\n }\n\n private ensureDefaultSharedVault(vaultKey: string): void {\n if (this.baseConfig.type !== \"image\" && this.baseConfig.type !== \"cloudflare\") return;\n if (this.vaultManager.hasEntry(vaultKey)) return;\n\n let profile: string | undefined;\n try {\n profile = loadAgentConfig().defaultSharedVault;\n } catch {\n return;\n }\n if (!profile || normalizeSharedVaultName(profile) !== profile) return;\n\n this.vaultManager.copySharedVaultTo(profile, vaultKey);\n }\n\n private resolveSandboxConfig(vaultKey: string): SandboxConfig {\n const config = this.vaultManager.getSandboxConfig(vaultKey, this.baseConfig);\n if (this.baseConfig.type !== \"image\") {\n return config;\n }\n\n if (config.type === \"container\") {\n return config;\n }\n\n return {\n type: \"container\",\n container: DockerContainerManager.containerName(vaultKey),\n };\n }\n\n private buildEnsureReadyCallback(\n vaultKey: string,\n conversationId: string,\n config: SandboxConfig,\n vault?: ResolvedVault,\n ): (() => Promise<void>) | undefined {\n if (this.baseConfig.type !== \"image\" || config.type !== \"container\") {\n return undefined;\n }\n\n return async () => {\n const expected = config.container || DockerContainerManager.containerName(vaultKey);\n let actual: string | undefined;\n try {\n actual = await this.provisioner?.provision(vaultKey, {\n containerName: expected,\n mounts: this.resolveMounts(conversationId, vault),\n conversationId,\n });\n } catch (err) {\n reportUserFacingError(err, {\n domain: \"sandbox\",\n surface: \"sandbox_provision\",\n operation: \"ensure_image_container_ready\",\n severity: \"error\",\n context: {\n sandboxType: \"image\",\n resolvedSandboxType: config.type,\n conversationId,\n vaultKey,\n expectedContainer: expected,\n hasVault: Boolean(vault),\n },\n });\n throw err;\n }\n if (actual && actual !== expected) {\n throw new Error(\n `Provisioner returned container \"${actual}\" for container key \"${vaultKey}\", expected \"${expected}\"`,\n );\n }\n };\n }\n\n private resolveMounts(conversationId: string, vault?: ResolvedVault): ContainerMount[] {\n const mountsByTarget = new Map<string, ContainerMount>();\n for (const mount of this.buildImageSandboxMounts(conversationId)) {\n mountsByTarget.set(mount.target, mount);\n }\n for (const mount of vault?.mounts ?? []) {\n if (!existsSync(mount.source)) {\n reportUserFacingError(new Error(\"Vault mount source is missing\"), {\n domain: \"sandbox\",\n surface: \"vault_injection\",\n operation: \"resolve_mounts\",\n severity: \"warning\",\n context: {\n sandboxType: \"image\",\n conversationId,\n target: mount.target,\n hasVault: Boolean(vault),\n },\n });\n continue;\n }\n mountsByTarget.set(mount.target, { source: mount.source, target: mount.target });\n }\n return [...mountsByTarget.values()];\n }\n\n private buildImageSandboxMounts(conversationId: string): ContainerMount[] {\n if (!this.workspaceDir) {\n return [];\n }\n\n if (readConversationWorkspaceMountMode(this.workspaceDir, conversationId) === \"full\") {\n return [{ source: this.workspaceDir, target: \"/workspace\" }];\n }\n\n const conversationDir = join(this.workspaceDir, conversationId);\n if (!this.ensuredConversationDirs.has(conversationId)) {\n ensureDirExists(conversationDir);\n this.ensuredConversationDirs.add(conversationId);\n }\n\n return [\n { source: join(this.workspaceDir, \"MEMORY.md\"), target: \"/workspace/MEMORY.md\" },\n { source: join(this.workspaceDir, \"skills\"), target: \"/workspace/skills\" },\n { source: join(this.workspaceDir, \"events\"), target: \"/workspace/events\" },\n { source: conversationDir, target: `/workspace/${conversationId}` },\n ];\n }\n}\n"]}