@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.10

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 (273) hide show
  1. package/README.md +171 -334
  2. package/dist/adapter.d.ts +36 -10
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +10 -5
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +349 -114
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +102 -31
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +71 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +168 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +29 -22
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +620 -186
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +22 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +97 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +1 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +136 -71
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +3 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +16 -0
  32. package/dist/adapters/slack/session.js.map +1 -0
  33. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  34. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  35. package/dist/adapters/slack/tools/attach.js.map +1 -1
  36. package/dist/adapters/telegram/bot.d.ts +2 -0
  37. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  38. package/dist/adapters/telegram/bot.js +190 -123
  39. package/dist/adapters/telegram/bot.js.map +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +57 -59
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/adapters/telegram/html.d.ts +3 -0
  44. package/dist/adapters/telegram/html.d.ts.map +1 -0
  45. package/dist/adapters/telegram/html.js +98 -0
  46. package/dist/adapters/telegram/html.js.map +1 -0
  47. package/dist/agent.d.ts +9 -10
  48. package/dist/agent.d.ts.map +1 -1
  49. package/dist/agent.js +645 -555
  50. package/dist/agent.js.map +1 -1
  51. package/dist/commands/auto-reply.d.ts +16 -0
  52. package/dist/commands/auto-reply.d.ts.map +1 -0
  53. package/dist/commands/auto-reply.js +69 -0
  54. package/dist/commands/auto-reply.js.map +1 -0
  55. package/dist/commands/index.d.ts +5 -0
  56. package/dist/commands/index.d.ts.map +1 -0
  57. package/dist/commands/index.js +19 -0
  58. package/dist/commands/index.js.map +1 -0
  59. package/dist/commands/login.d.ts +5 -0
  60. package/dist/commands/login.d.ts.map +1 -0
  61. package/dist/commands/login.js +76 -0
  62. package/dist/commands/login.js.map +1 -0
  63. package/dist/commands/model.d.ts +14 -0
  64. package/dist/commands/model.d.ts.map +1 -0
  65. package/dist/commands/model.js +112 -0
  66. package/dist/commands/model.js.map +1 -0
  67. package/dist/commands/new.d.ts +9 -0
  68. package/dist/commands/new.d.ts.map +1 -0
  69. package/dist/commands/new.js +28 -0
  70. package/dist/commands/new.js.map +1 -0
  71. package/dist/commands/registry.d.ts +7 -0
  72. package/dist/commands/registry.d.ts.map +1 -0
  73. package/dist/commands/registry.js +14 -0
  74. package/dist/commands/registry.js.map +1 -0
  75. package/dist/commands/sandbox.d.ts +10 -0
  76. package/dist/commands/sandbox.d.ts.map +1 -0
  77. package/dist/commands/sandbox.js +88 -0
  78. package/dist/commands/sandbox.js.map +1 -0
  79. package/dist/commands/session-view.d.ts +5 -0
  80. package/dist/commands/session-view.d.ts.map +1 -0
  81. package/dist/commands/session-view.js +62 -0
  82. package/dist/commands/session-view.js.map +1 -0
  83. package/dist/commands/types.d.ts +41 -0
  84. package/dist/commands/types.d.ts.map +1 -0
  85. package/dist/commands/types.js +2 -0
  86. package/dist/commands/types.js.map +1 -0
  87. package/dist/commands/utils.d.ts +8 -0
  88. package/dist/commands/utils.d.ts.map +1 -0
  89. package/dist/commands/utils.js +14 -0
  90. package/dist/commands/utils.js.map +1 -0
  91. package/dist/config.d.ts +53 -7
  92. package/dist/config.d.ts.map +1 -1
  93. package/dist/config.js +320 -55
  94. package/dist/config.js.map +1 -1
  95. package/dist/context.d.ts +10 -42
  96. package/dist/context.d.ts.map +1 -1
  97. package/dist/context.js +15 -128
  98. package/dist/context.js.map +1 -1
  99. package/dist/events.d.ts +16 -5
  100. package/dist/events.d.ts.map +1 -1
  101. package/dist/events.js +127 -58
  102. package/dist/events.js.map +1 -1
  103. package/dist/execution-resolver.d.ts +24 -0
  104. package/dist/execution-resolver.d.ts.map +1 -0
  105. package/dist/execution-resolver.js +115 -0
  106. package/dist/execution-resolver.js.map +1 -0
  107. package/dist/file-guards.d.ts +6 -0
  108. package/dist/file-guards.d.ts.map +1 -0
  109. package/dist/file-guards.js +48 -0
  110. package/dist/file-guards.js.map +1 -0
  111. package/dist/fs-atomic.d.ts +10 -0
  112. package/dist/fs-atomic.d.ts.map +1 -0
  113. package/dist/fs-atomic.js +45 -0
  114. package/dist/fs-atomic.js.map +1 -0
  115. package/dist/index.d.ts +7 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +4 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/instrument.d.ts.map +1 -1
  120. package/dist/instrument.js +3 -3
  121. package/dist/instrument.js.map +1 -1
  122. package/dist/log.d.ts +3 -7
  123. package/dist/log.d.ts.map +1 -1
  124. package/dist/log.js +20 -45
  125. package/dist/log.js.map +1 -1
  126. package/dist/login/index.d.ts +41 -0
  127. package/dist/login/index.d.ts.map +1 -0
  128. package/dist/login/index.js +202 -0
  129. package/dist/login/index.js.map +1 -0
  130. package/dist/login/portal.d.ts +19 -0
  131. package/dist/login/portal.d.ts.map +1 -0
  132. package/dist/login/portal.js +1453 -0
  133. package/dist/login/portal.js.map +1 -0
  134. package/dist/login/session.d.ts +33 -0
  135. package/dist/login/session.d.ts.map +1 -0
  136. package/dist/login/session.js +68 -0
  137. package/dist/login/session.js.map +1 -0
  138. package/dist/main.d.ts.map +1 -1
  139. package/dist/main.js +229 -264
  140. package/dist/main.js.map +1 -1
  141. package/dist/provisioner.d.ts +79 -0
  142. package/dist/provisioner.d.ts.map +1 -0
  143. package/dist/provisioner.js +437 -0
  144. package/dist/provisioner.js.map +1 -0
  145. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  146. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  147. package/dist/runtime/conversation-orchestrator.js +150 -0
  148. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  149. package/dist/runtime/index.d.ts +2 -0
  150. package/dist/runtime/index.d.ts.map +1 -0
  151. package/dist/runtime/index.js +2 -0
  152. package/dist/runtime/index.js.map +1 -0
  153. package/dist/runtime/session-runtime.d.ts +27 -0
  154. package/dist/runtime/session-runtime.d.ts.map +1 -0
  155. package/dist/runtime/session-runtime.js +211 -0
  156. package/dist/runtime/session-runtime.js.map +1 -0
  157. package/dist/sandbox/cloudflare.d.ts +15 -0
  158. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  159. package/dist/sandbox/cloudflare.js +137 -0
  160. package/dist/sandbox/cloudflare.js.map +1 -0
  161. package/dist/sandbox/container.d.ts +16 -0
  162. package/dist/sandbox/container.d.ts.map +1 -0
  163. package/dist/sandbox/container.js +126 -0
  164. package/dist/sandbox/container.js.map +1 -0
  165. package/dist/sandbox/errors.d.ts +6 -0
  166. package/dist/sandbox/errors.d.ts.map +1 -0
  167. package/dist/sandbox/errors.js +11 -0
  168. package/dist/sandbox/errors.js.map +1 -0
  169. package/dist/sandbox/firecracker.d.ts +17 -0
  170. package/dist/sandbox/firecracker.d.ts.map +1 -0
  171. package/dist/sandbox/firecracker.js +212 -0
  172. package/dist/sandbox/firecracker.js.map +1 -0
  173. package/dist/sandbox/host.d.ts +11 -0
  174. package/dist/sandbox/host.d.ts.map +1 -0
  175. package/dist/sandbox/host.js +89 -0
  176. package/dist/sandbox/host.js.map +1 -0
  177. package/dist/sandbox/image.d.ts +5 -0
  178. package/dist/sandbox/image.d.ts.map +1 -0
  179. package/dist/sandbox/image.js +30 -0
  180. package/dist/sandbox/image.js.map +1 -0
  181. package/dist/sandbox/index.d.ts +22 -0
  182. package/dist/sandbox/index.d.ts.map +1 -0
  183. package/dist/sandbox/index.js +54 -0
  184. package/dist/sandbox/index.js.map +1 -0
  185. package/dist/sandbox/path-context.d.ts +4 -0
  186. package/dist/sandbox/path-context.d.ts.map +1 -0
  187. package/dist/sandbox/path-context.js +20 -0
  188. package/dist/sandbox/path-context.js.map +1 -0
  189. package/dist/sandbox/types.d.ts +67 -0
  190. package/dist/sandbox/types.d.ts.map +1 -0
  191. package/dist/sandbox/types.js +2 -0
  192. package/dist/sandbox/types.js.map +1 -0
  193. package/dist/sandbox/utils.d.ts +4 -0
  194. package/dist/sandbox/utils.d.ts.map +1 -0
  195. package/dist/sandbox/utils.js +51 -0
  196. package/dist/sandbox/utils.js.map +1 -0
  197. package/dist/sandbox.d.ts +1 -39
  198. package/dist/sandbox.d.ts.map +1 -1
  199. package/dist/sandbox.js +1 -286
  200. package/dist/sandbox.js.map +1 -1
  201. package/dist/sentry.d.ts +2 -2
  202. package/dist/sentry.d.ts.map +1 -1
  203. package/dist/sentry.js +6 -4
  204. package/dist/sentry.js.map +1 -1
  205. package/dist/session-policy.d.ts +13 -0
  206. package/dist/session-policy.d.ts.map +1 -0
  207. package/dist/session-policy.js +23 -0
  208. package/dist/session-policy.js.map +1 -0
  209. package/dist/session-store.d.ts +35 -8
  210. package/dist/session-store.d.ts.map +1 -1
  211. package/dist/session-store.js +182 -23
  212. package/dist/session-store.js.map +1 -1
  213. package/dist/session-view/command.d.ts +5 -0
  214. package/dist/session-view/command.d.ts.map +1 -0
  215. package/dist/session-view/command.js +11 -0
  216. package/dist/session-view/command.js.map +1 -0
  217. package/dist/session-view/portal.d.ts +16 -0
  218. package/dist/session-view/portal.d.ts.map +1 -0
  219. package/dist/session-view/portal.js +1742 -0
  220. package/dist/session-view/portal.js.map +1 -0
  221. package/dist/session-view/service.d.ts +34 -0
  222. package/dist/session-view/service.d.ts.map +1 -0
  223. package/dist/session-view/service.js +427 -0
  224. package/dist/session-view/service.js.map +1 -0
  225. package/dist/session-view/store.d.ts +18 -0
  226. package/dist/session-view/store.d.ts.map +1 -0
  227. package/dist/session-view/store.js +39 -0
  228. package/dist/session-view/store.js.map +1 -0
  229. package/dist/store.d.ts +4 -7
  230. package/dist/store.d.ts.map +1 -1
  231. package/dist/store.js +26 -52
  232. package/dist/store.js.map +1 -1
  233. package/dist/tool-diagnostics.d.ts +2 -0
  234. package/dist/tool-diagnostics.d.ts.map +1 -0
  235. package/dist/tool-diagnostics.js +7 -0
  236. package/dist/tool-diagnostics.js.map +1 -0
  237. package/dist/tools/bash.d.ts +1 -1
  238. package/dist/tools/bash.d.ts.map +1 -1
  239. package/dist/tools/bash.js.map +1 -1
  240. package/dist/tools/edit.d.ts +1 -1
  241. package/dist/tools/edit.d.ts.map +1 -1
  242. package/dist/tools/edit.js.map +1 -1
  243. package/dist/tools/event.d.ts +62 -0
  244. package/dist/tools/event.d.ts.map +1 -0
  245. package/dist/tools/event.js +138 -0
  246. package/dist/tools/event.js.map +1 -0
  247. package/dist/tools/index.d.ts +8 -2
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +5 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/read.d.ts +1 -1
  252. package/dist/tools/read.d.ts.map +1 -1
  253. package/dist/tools/read.js.map +1 -1
  254. package/dist/tools/write.d.ts +1 -1
  255. package/dist/tools/write.d.ts.map +1 -1
  256. package/dist/tools/write.js.map +1 -1
  257. package/dist/trigger.d.ts +31 -0
  258. package/dist/trigger.d.ts.map +1 -0
  259. package/dist/trigger.js +98 -0
  260. package/dist/trigger.js.map +1 -0
  261. package/dist/ui-copy.d.ts +12 -0
  262. package/dist/ui-copy.d.ts.map +1 -0
  263. package/dist/ui-copy.js +36 -0
  264. package/dist/ui-copy.js.map +1 -0
  265. package/dist/vault-routing.d.ts +4 -0
  266. package/dist/vault-routing.d.ts.map +1 -0
  267. package/dist/vault-routing.js +16 -0
  268. package/dist/vault-routing.js.map +1 -0
  269. package/dist/vault.d.ts +72 -0
  270. package/dist/vault.d.ts.map +1 -0
  271. package/dist/vault.js +264 -0
  272. package/dist/vault.js.map +1 -0
  273. package/package.json +16 -13
package/dist/events.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Cron } from "croner";
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, watch, } from "fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, watch, } from "fs";
3
3
  import { readFile } from "fs/promises";
4
4
  import { join } from "path";
5
+ import { ensureDirExists, isRecord, parseJsonValue } from "./file-guards.js";
5
6
  import * as log from "./log.js";
7
+ import { inferConversationKind } from "./session-policy.js";
6
8
  // ============================================================================
7
9
  // EventsWatcher
8
10
  // ============================================================================
@@ -14,6 +16,7 @@ export class EventsWatcher {
14
16
  this.eventsDir = eventsDir;
15
17
  this.botsByPlatform = botsByPlatform;
16
18
  this.timers = new Map();
19
+ this.timerEventTypes = new Map();
17
20
  this.crons = new Map();
18
21
  this.debounceTimers = new Map();
19
22
  this.watcher = null;
@@ -25,16 +28,15 @@ export class EventsWatcher {
25
28
  */
26
29
  start() {
27
30
  // Ensure events directory exists
28
- if (!existsSync(this.eventsDir)) {
29
- mkdirSync(this.eventsDir, { recursive: true });
30
- }
31
+ ensureDirExists(this.eventsDir);
31
32
  log.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);
32
33
  // Scan existing files
33
34
  this.scanExisting();
34
35
  // Watch for changes
35
- this.watcher = watch(this.eventsDir, (_eventType, filename) => {
36
+ this.watcher = watch(this.eventsDir, (eventType, filename) => {
36
37
  if (!filename || !filename.endsWith(".json"))
37
38
  return;
39
+ log.logInfo(`Events watcher fs event: ${String(eventType)} ${filename} (exists=${existsSync(join(this.eventsDir, filename))})`);
38
40
  this.debounce(filename, () => this.handleFileChange(filename));
39
41
  });
40
42
  log.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);
@@ -58,6 +60,7 @@ export class EventsWatcher {
58
60
  clearTimeout(timer);
59
61
  }
60
62
  this.timers.clear();
63
+ this.timerEventTypes.clear();
61
64
  // Cancel all cron jobs
62
65
  for (const cron of this.crons.values()) {
63
66
  cron.stop();
@@ -83,7 +86,8 @@ export class EventsWatcher {
83
86
  results.push({
84
87
  filename,
85
88
  platform: data.platform,
86
- channelId: data.channelId,
89
+ conversationId: data.conversationId,
90
+ conversationKind: data.conversationKind,
87
91
  text: data.text,
88
92
  schedule: data.schedule,
89
93
  timezone: data.timezone,
@@ -121,34 +125,54 @@ export class EventsWatcher {
121
125
  }
122
126
  handleFileChange(filename) {
123
127
  const filePath = join(this.eventsDir, filename);
124
- if (!existsSync(filePath)) {
125
- // File was deleted
126
- this.handleDelete(filename);
127
- }
128
- else if (this.knownFiles.has(filename)) {
128
+ const exists = existsSync(filePath);
129
+ const known = this.knownFiles.has(filename);
130
+ log.logInfo(`Handling event file change: ${filename} (exists=${exists}, known=${known})`);
131
+ if (!exists) {
132
+ // fs.watch can briefly report a file as missing during create/rename churn.
133
+ // Confirm deletion before canceling scheduled events.
134
+ void this.handleDelete(filename);
135
+ }
136
+ else if (known) {
129
137
  // File was modified - cancel existing and re-schedule
130
- this.cancelScheduled(filename);
131
- this.handleFile(filename);
138
+ this.cancelScheduled(filename, "file-modified");
139
+ void this.handleFile(filename);
132
140
  }
133
141
  else {
134
142
  // New file
135
- this.handleFile(filename);
143
+ void this.handleFile(filename);
136
144
  }
137
145
  }
138
- handleDelete(filename) {
146
+ async handleDelete(filename) {
139
147
  if (!this.knownFiles.has(filename))
140
148
  return;
149
+ const filePath = join(this.eventsDir, filename);
150
+ for (let i = 0; i < MAX_RETRIES; i++) {
151
+ const delay = RETRY_BASE_MS * 2 ** i;
152
+ await this.sleep(delay);
153
+ const exists = existsSync(filePath);
154
+ log.logInfo(`Confirming event deletion: ${filename} after ${delay}ms (exists=${exists})`);
155
+ if (exists) {
156
+ return;
157
+ }
158
+ }
159
+ if (this.timerEventTypes.get(filename) === "one-shot" && this.timers.has(filename)) {
160
+ log.logInfo(`Ignoring deleted one-shot file after scheduling: ${filename} (timer remains active)`);
161
+ return;
162
+ }
141
163
  log.logInfo(`Event file deleted: ${filename}`);
142
- this.cancelScheduled(filename);
164
+ this.cancelScheduled(filename, "confirmed-delete");
143
165
  this.knownFiles.delete(filename);
144
166
  }
145
- cancelScheduled(filename) {
167
+ cancelScheduled(filename, reason = "unspecified") {
146
168
  const timer = this.timers.get(filename);
169
+ const cron = this.crons.get(filename);
170
+ log.logInfo(`Canceling scheduled event: ${filename} (reason=${reason}, timer=${Boolean(timer)}, cron=${Boolean(cron)})`);
147
171
  if (timer) {
148
172
  clearTimeout(timer);
149
173
  this.timers.delete(filename);
174
+ this.timerEventTypes.delete(filename);
150
175
  }
151
- const cron = this.crons.get(filename);
152
176
  if (cron) {
153
177
  cron.stop();
154
178
  this.crons.delete(filename);
@@ -156,6 +180,7 @@ export class EventsWatcher {
156
180
  }
157
181
  async handleFile(filename) {
158
182
  const filePath = join(this.eventsDir, filename);
183
+ log.logInfo(`Loading event file: ${filename} from ${filePath}`);
159
184
  // Parse with retries
160
185
  let event = null;
161
186
  let lastError = null;
@@ -174,10 +199,11 @@ export class EventsWatcher {
174
199
  }
175
200
  if (!event) {
176
201
  log.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);
177
- this.deleteFile(filename);
202
+ this.deleteFile(filename, "parse-failed");
178
203
  return;
179
204
  }
180
205
  this.knownFiles.add(filename);
206
+ log.logInfo(`Parsed event file: ${filename} (${event.type} for ${event.platform}/${event.conversationId})`);
181
207
  // Schedule based on type
182
208
  switch (event.type) {
183
209
  case "immediate":
@@ -192,42 +218,62 @@ export class EventsWatcher {
192
218
  }
193
219
  }
194
220
  parseEvent(content, filename) {
195
- const data = JSON.parse(content);
196
- if (!data.type || !data.channelId || !data.text) {
197
- throw new Error(`Missing required fields (type, channelId, text) in ${filename}`);
221
+ const data = parseJsonValue(content, isRecord, (detail) => detail === "unexpected JSON shape" ? `Expected top-level JSON object in ${filename}` : detail);
222
+ const conversationId = typeof data.conversationId === "string"
223
+ ? data.conversationId
224
+ : typeof data.channelId === "string"
225
+ ? data.channelId
226
+ : undefined;
227
+ const type = typeof data.type === "string" ? data.type : undefined;
228
+ const text = typeof data.text === "string" ? data.text : undefined;
229
+ if (!type || !conversationId || !text) {
230
+ throw new Error(`Missing required fields (type, conversationId, text) in ${filename}`);
198
231
  }
199
232
  const platform = this.resolvePlatform(data.platform, filename);
200
- switch (data.type) {
233
+ const conversationKind = this.resolveConversationKind(platform, conversationId, data.conversationKind);
234
+ const userId = typeof data.userId === "string" ? data.userId : undefined;
235
+ switch (type) {
201
236
  case "immediate":
202
- return { type: "immediate", platform, channelId: data.channelId, text: data.text };
237
+ return {
238
+ type: "immediate",
239
+ platform,
240
+ conversationId,
241
+ conversationKind,
242
+ userId,
243
+ text,
244
+ };
203
245
  case "one-shot":
204
- if (!data.at) {
246
+ if (typeof data.at !== "string" || data.at.length === 0) {
205
247
  throw new Error(`Missing 'at' field for one-shot event in ${filename}`);
206
248
  }
207
249
  return {
208
250
  type: "one-shot",
209
251
  platform,
210
- channelId: data.channelId,
211
- text: data.text,
252
+ conversationId,
253
+ conversationKind,
254
+ userId,
255
+ text,
212
256
  at: data.at,
213
257
  };
214
258
  case "periodic":
215
- if (!data.schedule) {
259
+ if (typeof data.schedule !== "string" || data.schedule.length === 0) {
216
260
  throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);
217
261
  }
218
- if (!data.timezone) {
262
+ if (typeof data.timezone !== "string" || data.timezone.length === 0) {
219
263
  throw new Error(`Missing 'timezone' field for periodic event in ${filename}`);
220
264
  }
221
265
  return {
222
266
  type: "periodic",
223
267
  platform,
224
- channelId: data.channelId,
225
- text: data.text,
268
+ conversationId,
269
+ conversationKind,
270
+ userId,
271
+ text,
226
272
  schedule: data.schedule,
227
273
  timezone: data.timezone,
228
274
  };
229
275
  default:
230
- throw new Error(`Unknown event type '${data.type}' in ${filename}`);
276
+ throw new Error(`Unknown event type '${type}' in ${filename}`);
231
277
  }
232
278
  }
233
279
  resolvePlatform(platformValue, filename) {
@@ -244,6 +290,12 @@ export class EventsWatcher {
244
290
  }
245
291
  throw new Error(`Missing required field 'platform' in ${filename}. Available platforms: ${availablePlatforms.join(", ")}`);
246
292
  }
293
+ resolveConversationKind(platform, conversationId, conversationKindValue) {
294
+ if (conversationKindValue === "direct" || conversationKindValue === "shared") {
295
+ return conversationKindValue;
296
+ }
297
+ return inferConversationKind(platform, conversationId);
298
+ }
247
299
  handleImmediate(filename, event) {
248
300
  const filePath = join(this.eventsDir, filename);
249
301
  // Check if stale (created before harness started)
@@ -251,7 +303,7 @@ export class EventsWatcher {
251
303
  const stat = statSync(filePath);
252
304
  if (stat.mtimeMs < this.startTime) {
253
305
  log.logInfo(`Stale immediate event, deleting: ${filename}`);
254
- this.deleteFile(filename);
306
+ this.deleteFile(filename, "stale-immediate");
255
307
  return;
256
308
  }
257
309
  }
@@ -268,17 +320,20 @@ export class EventsWatcher {
268
320
  if (atTime <= now) {
269
321
  // Past - delete without executing
270
322
  log.logInfo(`One-shot event in the past, deleting: ${filename}`);
271
- this.deleteFile(filename);
323
+ this.deleteFile(filename, "one-shot-in-past");
272
324
  return;
273
325
  }
274
326
  const delay = atTime - now;
275
- log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);
327
+ log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s (at=${event.at}, now=${new Date(now).toISOString()})`);
276
328
  const timer = setTimeout(() => {
277
329
  this.timers.delete(filename);
330
+ this.timerEventTypes.delete(filename);
278
331
  log.logInfo(`Executing one-shot event: ${filename}`);
279
332
  this.execute(filename, event);
280
333
  }, delay);
281
334
  this.timers.set(filename, timer);
335
+ this.timerEventTypes.set(filename, "one-shot");
336
+ log.logInfo(`Stored one-shot timer: ${filename} (active timers=${this.timers.size})`);
282
337
  }
283
338
  handlePeriodic(filename, event) {
284
339
  try {
@@ -292,56 +347,70 @@ export class EventsWatcher {
292
347
  }
293
348
  catch (err) {
294
349
  log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));
295
- this.deleteFile(filename);
350
+ this.deleteFile(filename, "invalid-cron");
296
351
  }
297
352
  }
298
353
  execute(filename, event, deleteAfter = true) {
299
- // Format the message
300
- let scheduleInfo;
301
- switch (event.type) {
302
- case "immediate":
303
- scheduleInfo = "immediate";
304
- break;
305
- case "one-shot":
306
- scheduleInfo = event.at;
307
- break;
308
- case "periodic":
309
- scheduleInfo = event.schedule;
310
- break;
311
- }
312
- const message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;
354
+ const message = this.buildEventPrompt(event);
313
355
  const bot = this.botsByPlatform[event.platform];
314
356
  if (!bot) {
315
357
  log.logWarning(`No bot configured for event platform '${event.platform}'`, filename);
316
358
  if (deleteAfter) {
317
- this.deleteFile(filename);
359
+ this.deleteFile(filename, "missing-bot");
318
360
  }
319
361
  return;
320
362
  }
321
- // Create synthetic BotEvent - use channelId as ts for stable session key
363
+ const eventId = filename.replace(/\.json$/i, "");
322
364
  const syntheticEvent = {
323
365
  type: "mention",
324
- channel: event.channelId,
325
- user: "EVENT",
366
+ conversationId: event.conversationId,
367
+ conversationKind: event.conversationKind,
368
+ user: event.userId ?? "EVENT",
326
369
  text: message,
327
- ts: event.channelId, // Stable key: same channel uses same ts for all events
370
+ ts: `event:${eventId}`,
328
371
  };
329
372
  // Enqueue for processing
330
373
  const enqueued = bot.enqueueEvent(syntheticEvent);
331
374
  if (enqueued && deleteAfter) {
332
375
  // Delete file after successful enqueue (immediate and one-shot)
333
- this.deleteFile(filename);
376
+ this.deleteFile(filename, "executed-and-enqueued");
334
377
  }
335
378
  else if (!enqueued) {
336
379
  log.logWarning(`Event queue full, discarded: ${filename}`);
337
380
  // Still delete immediate/one-shot even if discarded
338
381
  if (deleteAfter) {
339
- this.deleteFile(filename);
382
+ this.deleteFile(filename, "queue-full-discarded");
340
383
  }
341
384
  }
342
385
  }
343
- deleteFile(filename) {
386
+ buildEventPrompt(event) {
387
+ switch (event.type) {
388
+ case "one-shot":
389
+ return [
390
+ "Please deliver the following reminder to the user in a short, natural way.",
391
+ "Do not greet, do not introduce yourself, and do not ask generic follow-up questions.",
392
+ "",
393
+ `Reminder: ${event.text}`,
394
+ ].join("\n");
395
+ case "periodic":
396
+ return [
397
+ "Handle the following recurring task.",
398
+ "Respond concisely. If there is nothing actionable to report, reply with [SILENT].",
399
+ "",
400
+ `Task: ${event.text}`,
401
+ ].join("\n");
402
+ case "immediate":
403
+ return [
404
+ "Handle the following event/update in a concise, context-appropriate way.",
405
+ "If it reads like a reminder or follow-up, deliver it directly without greeting or generic offers to help.",
406
+ "",
407
+ `Event: ${event.text}`,
408
+ ].join("\n");
409
+ }
410
+ }
411
+ deleteFile(filename, reason = "unspecified") {
344
412
  const filePath = join(this.eventsDir, filename);
413
+ log.logInfo(`Deleting event file: ${filename} (reason=${reason})`);
345
414
  try {
346
415
  unlinkSync(filePath);
347
416
  }
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { Cron } from \"croner\";\nimport {\n existsSync,\n type FSWatcher,\n mkdirSync,\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 } from \"./adapter.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n type: \"immediate\";\n platform: string;\n channelId: string;\n text: string;\n}\n\nexport interface OneShotEvent {\n type: \"one-shot\";\n platform: string;\n channelId: string;\n text: string;\n at: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n type: \"periodic\";\n platform: string;\n channelId: string;\n text: string;\n schedule: string; // cron syntax\n timezone: string; // IANA timezone\n}\n\nexport type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\nexport interface PeriodicEventInfo {\n filename: string;\n platform: string;\n channelId: string;\n text: string;\n schedule: string;\n timezone: string;\n nextRun: string | null; // ISO 8601\n}\n\n// ============================================================================\n// EventsWatcher\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 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 if (!existsSync(this.eventsDir)) {\n mkdirSync(this.eventsDir, { recursive: true });\n }\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 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\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 channelId: data.channelId,\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\n if (!existsSync(filePath)) {\n // File was deleted\n this.handleDelete(filename);\n } else if (this.knownFiles.has(filename)) {\n // File was modified - cancel existing and re-schedule\n this.cancelScheduled(filename);\n this.handleFile(filename);\n } else {\n // New file\n this.handleFile(filename);\n }\n }\n\n private handleDelete(filename: string): void {\n if (!this.knownFiles.has(filename)) return;\n\n log.logInfo(`Event file deleted: ${filename}`);\n this.cancelScheduled(filename);\n this.knownFiles.delete(filename);\n }\n\n private cancelScheduled(filename: string): void {\n const timer = this.timers.get(filename);\n if (timer) {\n clearTimeout(timer);\n this.timers.delete(filename);\n }\n\n const cron = this.crons.get(filename);\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\n // Parse with retries\n let event: MamaEvent | 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);\n return;\n }\n\n this.knownFiles.add(filename);\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): MamaEvent | null {\n const data = JSON.parse(content);\n\n if (!data.type || !data.channelId || !data.text) {\n throw new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n }\n\n const platform = this.resolvePlatform(data.platform, filename);\n\n switch (data.type) {\n case \"immediate\":\n return { type: \"immediate\", platform, channelId: data.channelId, text: data.text };\n\n case \"one-shot\":\n if (!data.at) {\n throw new Error(`Missing 'at' field for one-shot event in ${filename}`);\n }\n return {\n type: \"one-shot\",\n platform,\n channelId: data.channelId,\n text: data.text,\n at: data.at,\n };\n\n case \"periodic\":\n if (!data.schedule) {\n throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n }\n if (!data.timezone) {\n throw new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n }\n return {\n type: \"periodic\",\n platform,\n channelId: data.channelId,\n text: data.text,\n schedule: data.schedule,\n timezone: data.timezone,\n };\n\n default:\n throw new Error(`Unknown event type '${data.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 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);\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);\n return;\n }\n\n const delay = atTime - now;\n log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n const timer = setTimeout(() => {\n this.timers.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 }\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);\n }\n }\n\n private execute(filename: string, event: MamaEvent, deleteAfter: boolean = true): void {\n // Format the message\n let scheduleInfo: string;\n switch (event.type) {\n case \"immediate\":\n scheduleInfo = \"immediate\";\n break;\n case \"one-shot\":\n scheduleInfo = event.at;\n break;\n case \"periodic\":\n scheduleInfo = event.schedule;\n break;\n }\n\n const message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n const bot = this.botsByPlatform[event.platform];\n\n if (!bot) {\n log.logWarning(`No bot configured for event platform '${event.platform}'`, filename);\n if (deleteAfter) {\n this.deleteFile(filename);\n }\n return;\n }\n\n // Create synthetic BotEvent - use channelId as ts for stable session key\n const syntheticEvent: BotEvent = {\n type: \"mention\",\n channel: event.channelId,\n user: \"EVENT\",\n text: message,\n ts: event.channelId, // Stable key: same channel uses same ts for all events\n };\n\n // Enqueue for processing\n const enqueued = bot.enqueueEvent(syntheticEvent);\n\n if (enqueued && deleteAfter) {\n // Delete file after successful enqueue (immediate and one-shot)\n this.deleteFile(filename);\n } else if (!enqueued) {\n log.logWarning(`Event queue full, discarded: ${filename}`);\n // Still delete immediate/one-shot even if discarded\n if (deleteAfter) {\n this.deleteFile(filename);\n }\n }\n }\n\n private deleteFile(filename: string): void {\n const filePath = join(this.eventsDir, filename);\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"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { 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, isRecord, parseJsonValue } from \"./file-guards.js\";\nimport * as log from \"./log.js\";\nimport { inferConversationKind } from \"./session-policy.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\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 MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\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\n// ============================================================================\n// EventsWatcher\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: MamaEvent | 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): MamaEvent | null {\n const data = parseJsonValue(content, isRecord, (detail) =>\n detail === \"unexpected JSON shape\" ? `Expected top-level JSON object in ${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: MamaEvent, 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 if (deleteAfter) {\n this.deleteFile(filename, \"missing-bot\");\n }\n return;\n }\n\n const eventId = filename.replace(/\\.json$/i, \"\");\n const syntheticEvent: 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(syntheticEvent);\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 // 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: MamaEvent): 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,24 @@
1
+ import { DockerContainerManager } from "./provisioner.js";
2
+ import { type Executor, type SandboxConfig } from "./sandbox.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 resolveSandboxConfig;
20
+ private buildEnsureReadyCallback;
21
+ private resolveMounts;
22
+ private buildImageSandboxMounts;
23
+ }
24
+ //# 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,cAAc,CAAC;AACjF,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAC;AAG9D,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,CAYtD;IAED,OAAO,CAAC,oBAAoB;IAgB5B,OAAO,CAAC,wBAAwB;IAyBhC,OAAO,CAAC,aAAa;IAYrB,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.js\";\nimport type { ResolvedVault, 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\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 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 const actual = await this.provisioner?.provision(vaultKey, {\n containerName: expected,\n mounts: this.resolveMounts(conversationId, vault),\n conversationId,\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)) continue;\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"]}