bazilion 0.0.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 (260) hide show
  1. package/.understand-anything/.understandignore +25 -0
  2. package/.understand-anything/fingerprints.json +14267 -0
  3. package/.understand-anything/knowledge-graph.json +18128 -0
  4. package/.understand-anything/meta.json +6 -0
  5. package/CLAUDE.md +164 -0
  6. package/LICENSE +21 -0
  7. package/README.md +195 -0
  8. package/apps/cli/package.json +21 -0
  9. package/apps/cli/src/auth-file.ts +18 -0
  10. package/apps/cli/src/client.ts +37 -0
  11. package/apps/cli/src/columnize.ts +32 -0
  12. package/apps/cli/src/commands/agent.ts +574 -0
  13. package/apps/cli/src/commands/auth.ts +110 -0
  14. package/apps/cli/src/commands/backup.ts +135 -0
  15. package/apps/cli/src/commands/completion.ts +155 -0
  16. package/apps/cli/src/commands/config.ts +95 -0
  17. package/apps/cli/src/commands/doctor.ts +131 -0
  18. package/apps/cli/src/commands/group.ts +132 -0
  19. package/apps/cli/src/commands/inbox.ts +82 -0
  20. package/apps/cli/src/commands/login.ts +73 -0
  21. package/apps/cli/src/commands/memory.ts +106 -0
  22. package/apps/cli/src/commands/profile.ts +259 -0
  23. package/apps/cli/src/commands/provider.ts +170 -0
  24. package/apps/cli/src/commands/send.ts +24 -0
  25. package/apps/cli/src/commands/serve.ts +89 -0
  26. package/apps/cli/src/commands/skill.ts +120 -0
  27. package/apps/cli/src/commands/token.ts +148 -0
  28. package/apps/cli/src/commands/trigger.ts +129 -0
  29. package/apps/cli/src/commands/uninstall.ts +156 -0
  30. package/apps/cli/src/index.ts +196 -0
  31. package/apps/cli/src/paths.ts +12 -0
  32. package/apps/cli/test/agent.test.ts +213 -0
  33. package/apps/cli/test/backup.test.ts +95 -0
  34. package/apps/cli/test/chat.test.ts +539 -0
  35. package/apps/cli/test/columnize.test.ts +29 -0
  36. package/apps/cli/test/completion.test.ts +45 -0
  37. package/apps/cli/test/config-page.test.ts +151 -0
  38. package/apps/cli/test/group.test.ts +74 -0
  39. package/apps/cli/test/helpers.ts +70 -0
  40. package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
  41. package/apps/cli/test/inbox.test.ts +147 -0
  42. package/apps/cli/test/memory.test.ts +69 -0
  43. package/apps/cli/test/profile.test.ts +110 -0
  44. package/apps/cli/test/send.test.ts +30 -0
  45. package/apps/cli/test/server-fixture.ts +212 -0
  46. package/apps/cli/test/session-head.test.ts +45 -0
  47. package/apps/cli/test/skill.test.ts +128 -0
  48. package/apps/cli/test/token.test.ts +109 -0
  49. package/apps/cli/test/trigger.test.ts +245 -0
  50. package/apps/cli/tsconfig.json +4 -0
  51. package/apps/daemon/package.json +31 -0
  52. package/apps/daemon/src/app.ts +41 -0
  53. package/apps/daemon/src/core/agent/archive.ts +8 -0
  54. package/apps/daemon/src/core/agent/delete.ts +30 -0
  55. package/apps/daemon/src/core/agent/resolve.ts +31 -0
  56. package/apps/daemon/src/core/agent/spawn.ts +95 -0
  57. package/apps/daemon/src/core/agent/unarchive.ts +11 -0
  58. package/apps/daemon/src/core/availableModels.ts +58 -0
  59. package/apps/daemon/src/core/db/client.ts +113 -0
  60. package/apps/daemon/src/core/db/migrate.ts +41 -0
  61. package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
  62. package/apps/daemon/src/core/group/delete.ts +21 -0
  63. package/apps/daemon/src/core/group/register.ts +68 -0
  64. package/apps/daemon/src/core/index.ts +71 -0
  65. package/apps/daemon/src/core/paths.ts +56 -0
  66. package/apps/daemon/src/core/profile/create.ts +78 -0
  67. package/apps/daemon/src/core/profile/delete.ts +26 -0
  68. package/apps/daemon/src/core/profile/identity.ts +70 -0
  69. package/apps/daemon/src/core/profile/load.ts +45 -0
  70. package/apps/daemon/src/core/profile/seed.ts +74 -0
  71. package/apps/daemon/src/core/profile/templates.ts +60 -0
  72. package/apps/daemon/src/core/profile/update.ts +57 -0
  73. package/apps/daemon/src/core/profile/validate.ts +9 -0
  74. package/apps/daemon/src/core/repos/agents.ts +202 -0
  75. package/apps/daemon/src/core/repos/config.ts +78 -0
  76. package/apps/daemon/src/core/repos/groups.ts +55 -0
  77. package/apps/daemon/src/core/repos/messages.ts +127 -0
  78. package/apps/daemon/src/core/repos/profiles.ts +83 -0
  79. package/apps/daemon/src/core/repos/providerModels.ts +58 -0
  80. package/apps/daemon/src/core/repos/providerState.ts +37 -0
  81. package/apps/daemon/src/core/repos/secrets.ts +145 -0
  82. package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
  83. package/apps/daemon/src/core/repos/triggers.ts +101 -0
  84. package/apps/daemon/src/core/repos/webTokens.ts +87 -0
  85. package/apps/daemon/src/core/secrets.ts +65 -0
  86. package/apps/daemon/src/core/services.ts +264 -0
  87. package/apps/daemon/src/core/skills/discover.ts +28 -0
  88. package/apps/daemon/src/core/skills/import.ts +136 -0
  89. package/apps/daemon/src/core/skills/parse.ts +52 -0
  90. package/apps/daemon/src/core/skills/resolve.ts +50 -0
  91. package/apps/daemon/src/index.ts +45 -0
  92. package/apps/daemon/src/lib/agent-cancel.ts +48 -0
  93. package/apps/daemon/src/lib/agent-id.ts +13 -0
  94. package/apps/daemon/src/lib/agent-turn.ts +56 -0
  95. package/apps/daemon/src/lib/api-key.ts +56 -0
  96. package/apps/daemon/src/lib/auth.ts +41 -0
  97. package/apps/daemon/src/lib/cron.ts +93 -0
  98. package/apps/daemon/src/lib/ctx.ts +80 -0
  99. package/apps/daemon/src/lib/messaging-host.ts +34 -0
  100. package/apps/daemon/src/lib/middleware-auth.ts +52 -0
  101. package/apps/daemon/src/lib/scheduler.ts +294 -0
  102. package/apps/daemon/src/routes/agents.ts +772 -0
  103. package/apps/daemon/src/routes/auth-login.ts +193 -0
  104. package/apps/daemon/src/routes/config.ts +267 -0
  105. package/apps/daemon/src/routes/groups.ts +133 -0
  106. package/apps/daemon/src/routes/messages.ts +29 -0
  107. package/apps/daemon/src/routes/misc.ts +239 -0
  108. package/apps/daemon/src/routes/profiles.ts +197 -0
  109. package/apps/daemon/src/routes/skills.ts +123 -0
  110. package/apps/daemon/src/routes/triggers.ts +29 -0
  111. package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
  112. package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
  113. package/apps/daemon/src/runtime/index.ts +77 -0
  114. package/apps/daemon/src/runtime/memory/files.ts +103 -0
  115. package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
  116. package/apps/daemon/src/runtime/memory/types.ts +16 -0
  117. package/apps/daemon/src/runtime/pi/events.ts +173 -0
  118. package/apps/daemon/src/runtime/pi/session.ts +536 -0
  119. package/apps/daemon/src/runtime/pi/tools.ts +85 -0
  120. package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
  121. package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
  122. package/apps/daemon/src/runtime/providers/registry.ts +374 -0
  123. package/apps/daemon/src/runtime/providers/retry.ts +176 -0
  124. package/apps/daemon/src/runtime/providers/types.ts +33 -0
  125. package/apps/daemon/src/runtime/session/prompt.ts +83 -0
  126. package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
  127. package/apps/daemon/src/runtime/tools/home.ts +114 -0
  128. package/apps/daemon/src/runtime/tools/memory.ts +81 -0
  129. package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
  130. package/apps/daemon/src/runtime/tools/registry.ts +29 -0
  131. package/apps/daemon/src/runtime/tools/types.ts +13 -0
  132. package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
  133. package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
  134. package/apps/daemon/src/runtime/tools/web.ts +273 -0
  135. package/apps/daemon/src/runtime/worker/entry.ts +221 -0
  136. package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
  137. package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
  138. package/apps/daemon/test/core/agents.test.ts +319 -0
  139. package/apps/daemon/test/core/available-models.test.ts +63 -0
  140. package/apps/daemon/test/core/config.test.ts +99 -0
  141. package/apps/daemon/test/core/groups.test.ts +63 -0
  142. package/apps/daemon/test/core/helpers.ts +50 -0
  143. package/apps/daemon/test/core/identity.test.ts +85 -0
  144. package/apps/daemon/test/core/migrations.test.ts +83 -0
  145. package/apps/daemon/test/core/profiles.test.ts +182 -0
  146. package/apps/daemon/test/core/provider-models.test.ts +57 -0
  147. package/apps/daemon/test/core/provider-state.test.ts +36 -0
  148. package/apps/daemon/test/core/skill-meta.test.ts +45 -0
  149. package/apps/daemon/test/core/skills.test.ts +271 -0
  150. package/apps/daemon/test/core/triggers.test.ts +182 -0
  151. package/apps/daemon/test/core/web-tokens.test.ts +79 -0
  152. package/apps/daemon/test/cron.test.ts +90 -0
  153. package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
  154. package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
  155. package/apps/daemon/test/runtime/memory.test.ts +78 -0
  156. package/apps/daemon/test/runtime/messaging.test.ts +213 -0
  157. package/apps/daemon/test/runtime/mock-server.ts +65 -0
  158. package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
  159. package/apps/daemon/test/runtime/providers.test.ts +306 -0
  160. package/apps/daemon/test/runtime/retry.test.ts +191 -0
  161. package/apps/daemon/test/runtime/session-head.test.ts +90 -0
  162. package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
  163. package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
  164. package/apps/daemon/tsconfig.json +4 -0
  165. package/apps/mobile/README.md +60 -0
  166. package/apps/mobile/app/_layout.tsx +58 -0
  167. package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
  168. package/apps/mobile/app/agents/[id]/index.tsx +166 -0
  169. package/apps/mobile/app/agents/index.tsx +212 -0
  170. package/apps/mobile/app/index.tsx +21 -0
  171. package/apps/mobile/app/pair.tsx +226 -0
  172. package/apps/mobile/app/settings.tsx +419 -0
  173. package/apps/mobile/app.json +49 -0
  174. package/apps/mobile/assets/adaptive-icon.png +0 -0
  175. package/apps/mobile/assets/favicon.png +0 -0
  176. package/apps/mobile/assets/icon.png +0 -0
  177. package/apps/mobile/assets/splash-icon.png +0 -0
  178. package/apps/mobile/babel.config.js +6 -0
  179. package/apps/mobile/metro.config.js +28 -0
  180. package/apps/mobile/package.json +44 -0
  181. package/apps/mobile/src/auth.ts +66 -0
  182. package/apps/mobile/src/pair-url.ts +48 -0
  183. package/apps/mobile/src/theme-context.tsx +88 -0
  184. package/apps/mobile/src/theme.ts +135 -0
  185. package/apps/mobile/test/pair-url.test.ts +46 -0
  186. package/apps/mobile/tsconfig.json +23 -0
  187. package/apps/web/components.json +25 -0
  188. package/apps/web/package.json +39 -0
  189. package/apps/web/public/baziu.svg +8 -0
  190. package/apps/web/src/components/AgentTabs.tsx +45 -0
  191. package/apps/web/src/components/BaziuLogo.tsx +21 -0
  192. package/apps/web/src/components/ChatPane.tsx +1033 -0
  193. package/apps/web/src/components/ConfigTabs.tsx +29 -0
  194. package/apps/web/src/components/CopyButton.tsx +68 -0
  195. package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
  196. package/apps/web/src/components/FieldRow.tsx +94 -0
  197. package/apps/web/src/components/Footer.tsx +10 -0
  198. package/apps/web/src/components/PawIcon.tsx +15 -0
  199. package/apps/web/src/components/Sidebar.tsx +287 -0
  200. package/apps/web/src/components/SpawnDialog.tsx +129 -0
  201. package/apps/web/src/components/ThemeToggle.tsx +75 -0
  202. package/apps/web/src/components/TopNav.tsx +34 -0
  203. package/apps/web/src/components/ui/button.tsx +67 -0
  204. package/apps/web/src/components/ui/card.tsx +103 -0
  205. package/apps/web/src/components/ui/checkbox.tsx +31 -0
  206. package/apps/web/src/components/ui/dialog.tsx +168 -0
  207. package/apps/web/src/components/ui/input.tsx +19 -0
  208. package/apps/web/src/components/ui/label.tsx +22 -0
  209. package/apps/web/src/components/ui/radio-group.tsx +44 -0
  210. package/apps/web/src/components/ui/select.tsx +192 -0
  211. package/apps/web/src/components/ui/separator.tsx +26 -0
  212. package/apps/web/src/components/ui/table.tsx +116 -0
  213. package/apps/web/src/components/ui/tabs.tsx +88 -0
  214. package/apps/web/src/components/ui/textarea.tsx +18 -0
  215. package/apps/web/src/lib/auth.ts +50 -0
  216. package/apps/web/src/lib/daemon-client.ts +34 -0
  217. package/apps/web/src/lib/md.ts +45 -0
  218. package/apps/web/src/lib/utils.ts +6 -0
  219. package/apps/web/src/lib/wire-constants.ts +27 -0
  220. package/apps/web/src/routeTree.gen.ts +408 -0
  221. package/apps/web/src/router.tsx +20 -0
  222. package/apps/web/src/routes/__root.tsx +123 -0
  223. package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
  224. package/apps/web/src/routes/agents/$id/index.tsx +527 -0
  225. package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
  226. package/apps/web/src/routes/agents/index.tsx +265 -0
  227. package/apps/web/src/routes/api/$.ts +88 -0
  228. package/apps/web/src/routes/config/index.tsx +315 -0
  229. package/apps/web/src/routes/config/services.tsx +49 -0
  230. package/apps/web/src/routes/config/tokens.tsx +192 -0
  231. package/apps/web/src/routes/groups/$id/index.tsx +153 -0
  232. package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
  233. package/apps/web/src/routes/groups/index.tsx +191 -0
  234. package/apps/web/src/routes/index.tsx +133 -0
  235. package/apps/web/src/routes/login.tsx +63 -0
  236. package/apps/web/src/routes/profiles/$id.tsx +549 -0
  237. package/apps/web/src/routes/profiles/index.tsx +458 -0
  238. package/apps/web/src/routes/skills/index.tsx +297 -0
  239. package/apps/web/src/routes/welcome.tsx +61 -0
  240. package/apps/web/src/styles.css +449 -0
  241. package/apps/web/tsconfig.json +25 -0
  242. package/apps/web/vite.config.ts +25 -0
  243. package/biome.json +23 -0
  244. package/docs/agent-engine.md +219 -0
  245. package/docs/architecture.md +627 -0
  246. package/docs/backlog/README.md +42 -0
  247. package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
  248. package/docs/openclaw-reference.md +210 -0
  249. package/package.json +38 -0
  250. package/packages/api-types/package.json +11 -0
  251. package/packages/api-types/src/entities.ts +146 -0
  252. package/packages/api-types/src/events.ts +55 -0
  253. package/packages/api-types/src/index.ts +488 -0
  254. package/packages/api-types/src/memory.ts +15 -0
  255. package/packages/client/package.json +13 -0
  256. package/packages/client/src/index.ts +117 -0
  257. package/pnpm-workspace.yaml +3 -0
  258. package/tsconfig.base.json +23 -0
  259. package/tsconfig.json +11 -0
  260. package/vitest.config.ts +22 -0
@@ -0,0 +1,294 @@
1
+ // In-process scheduler for agent triggers (heartbeats + cron) and inbox
2
+ // auto-delivery.
3
+ //
4
+ // Each tick does two jobs:
5
+ //
6
+ // 1. Trigger firing. Loads enabled `agent_triggers` rows and fires whichever
7
+ // are due via `runAgentTurn`. A trigger is "due" when:
8
+ // - interval: last_fired_at + intervalSec*1000 ≤ now (never-fired
9
+ // uses created_at as the baseline)
10
+ // - cron: current minute's wall-clock matches expression AND
11
+ // last_fired_at's minute < current minute
12
+ //
13
+ // 2. Inbox auto-delivery (always on). Scans `messages` for recipients
14
+ // with unread mail. For each idle recipient (not already running /
15
+ // firing), drains all their unread messages in one transaction and
16
+ // fires a turn whose prompt embeds the messages. Marking read happens
17
+ // *inside* the drain transaction so two concurrent ticks can't
18
+ // double-dispatch. To disable both triggers AND auto-delivery, set
19
+ // `BAZILION_SCHEDULER=off` — there is no separate inbox-only knob
20
+ // because free inter-agent messaging is a baseline Bazilion promise.
21
+ //
22
+ // Concurrency: each trigger gets an in-memory "firing" guard so a slow turn
23
+ // can't pile up overlapping runs for the same trigger. Auto-delivery shares
24
+ // the same mechanism keyed on `msg-wake:<agentId>`. The DB is still the
25
+ // source of truth for last_fired_at / read_at — we mark *before* kicking the
26
+ // run, so a server restart won't immediately re-fire.
27
+
28
+ import type { AgentTrigger, Message } from '@bazilion/api-types'
29
+ import { agentRepo, messageRepo, triggerRepo } from '../core/index.ts'
30
+ import { isActiveAgent } from './agent-cancel.ts'
31
+ import { runAgentTurn } from './agent-turn.ts'
32
+ import { matchesCron, type ParsedCron, parseCron } from './cron.ts'
33
+ import { getCtx } from './ctx.ts'
34
+
35
+ const SCHEDULER_KEY = Symbol.for('bazilion.scheduler')
36
+ const TICK_MS = Number(process.env.BAZILION_SCHEDULER_TICK_MS ?? 5_000)
37
+
38
+ interface SchedulerState {
39
+ timer: NodeJS.Timeout | null
40
+ firing: Set<string>
41
+ cronCache: Map<string, ParsedCron>
42
+ /** pinned onStop for graceful shutdown (tests) */
43
+ stopped: boolean
44
+ }
45
+
46
+ function state(): SchedulerState {
47
+ const g = globalThis as unknown as Record<symbol, SchedulerState | undefined>
48
+ let s = g[SCHEDULER_KEY]
49
+ if (!s) {
50
+ s = { timer: null, firing: new Set(), cronCache: new Map(), stopped: false }
51
+ g[SCHEDULER_KEY] = s
52
+ }
53
+ return s
54
+ }
55
+
56
+ function floorToMinute(ms: number): number {
57
+ return Math.floor(ms / 60_000) * 60_000
58
+ }
59
+
60
+ function isDue(t: AgentTrigger, now: number, cronCache: Map<string, ParsedCron>): boolean {
61
+ if (!t.enabled) return false
62
+ if (t.kind === 'interval') {
63
+ const every = (t.intervalSec ?? 0) * 1000
64
+ if (every <= 0) return false
65
+ const baseline = t.lastFiredAt ?? t.createdAt
66
+ return now - baseline >= every
67
+ }
68
+ if (t.kind === 'cron') {
69
+ if (!t.cronExpr) return false
70
+ let parsed = cronCache.get(t.cronExpr)
71
+ if (!parsed) {
72
+ try {
73
+ parsed = parseCron(t.cronExpr)
74
+ } catch {
75
+ return false
76
+ }
77
+ cronCache.set(t.cronExpr, parsed)
78
+ }
79
+ const nowFloor = floorToMinute(now)
80
+ if (t.lastFiredAt && floorToMinute(t.lastFiredAt) === nowFloor) return false
81
+ return matchesCron(parsed, new Date(nowFloor))
82
+ }
83
+ return false
84
+ }
85
+
86
+ async function fireTrigger(t: AgentTrigger): Promise<void> {
87
+ const s = state()
88
+ if (s.firing.has(t.id)) return
89
+ s.firing.add(t.id)
90
+ const ctx = getCtx()
91
+ // Mark fired first — if the agent turn fails, we still don't want to loop
92
+ // on the same trigger every tick. The user will see it in `trigger list`
93
+ // (last_fired_at updated) and the run will be marked failed.
94
+ try {
95
+ triggerRepo.markFired(ctx.db, t.id)
96
+ } catch (err) {
97
+ console.error(`[scheduler] markFired failed for ${t.id}:`, err)
98
+ s.firing.delete(t.id)
99
+ return
100
+ }
101
+ try {
102
+ // Drain the turn; we don't stream to anyone. Errors surface as `fatal`
103
+ // frames which we log but don't throw — the run row in the DB carries
104
+ // the real status.
105
+ for await (const frame of runAgentTurn(t.agentId, t.message)) {
106
+ if (frame.kind === 'fatal') {
107
+ console.error(`[scheduler] trigger ${t.id} fatal:`, frame.error)
108
+ }
109
+ }
110
+ } catch (err) {
111
+ console.error(`[scheduler] trigger ${t.id} unexpected throw:`, err)
112
+ } finally {
113
+ s.firing.delete(t.id)
114
+ }
115
+ }
116
+
117
+ function decodeText(payload: string): string {
118
+ try {
119
+ const obj = JSON.parse(payload) as { text?: unknown }
120
+ if (typeof obj.text === 'string') return obj.text
121
+ } catch {}
122
+ return payload
123
+ }
124
+
125
+ // Sentinel prepended to every inbox-wake prompt. The web chat UI detects it
126
+ // to render the bubble with a distinct "inter-agent" style instead of the
127
+ // default user-message styling. Kept in sync by copy with `chat.ts`'s
128
+ // INBOX_WAKE_PREFIX constant.
129
+ const INBOX_WAKE_PREFIX = '[[bazilion:inbox-wake]]\n'
130
+
131
+ /**
132
+ * Build a wake-up prompt for an agent that has unread mail. The prompt is
133
+ * framed as a user-side system notice — not an agent-style message — so the
134
+ * recipient's LLM understands this is the runtime telling it "here's what
135
+ * arrived for you".
136
+ *
137
+ * Loop prevention: we differentiate by `replyTo`. A NEW message (no
138
+ * `replyTo`) opens a thread, and the sender is waiting for an answer — the
139
+ * agent must respond at least once. A REPLY (`replyTo` set) is already
140
+ * closing a round-trip, so the agent may acknowledge silently. This single
141
+ * asymmetry is enough to terminate conversations after one exchange instead
142
+ * of ping-ponging forever ("you have to answer me" → "no you have to
143
+ * answer me" → …).
144
+ */
145
+ function buildInboxPrompt(
146
+ agentId: string,
147
+ messages: Message[],
148
+ fromNames: Map<string, string>,
149
+ ): string {
150
+ const lines: string[] = [INBOX_WAKE_PREFIX.trimEnd()]
151
+ lines.push(
152
+ `You have ${messages.length} new message${messages.length === 1 ? '' : 's'} in your inbox:`,
153
+ )
154
+ lines.push('')
155
+ const newThread: Message[] = []
156
+ for (const m of messages) {
157
+ const name = fromNames.get(m.fromAgentId)
158
+ const fromLabel = name ? `${name} (${m.fromAgentId})` : m.fromAgentId
159
+ const text = decodeText(m.payload)
160
+ const header = m.replyTo
161
+ ? `--- from ${fromLabel} (message ${m.id}, reply to ${m.replyTo}) ---`
162
+ : `--- from ${fromLabel} (message ${m.id}) ---`
163
+ lines.push(header)
164
+ lines.push(text)
165
+ lines.push('')
166
+ if (!m.replyTo) newThread.push(m)
167
+ }
168
+ if (newThread.length > 0) {
169
+ lines.push(
170
+ `You MUST reply to the ${newThread.length} new message${newThread.length === 1 ? '' : 's'} above ` +
171
+ '(the ones that are NOT marked as a reply). For each, call ' +
172
+ '`send_message(to=<sender agent id>, text=<your reply>, reply_to=<the message id shown in the header>)`. ' +
173
+ 'Give a concrete answer if you can; a brief acknowledgement is fine if the message is purely informational. ' +
174
+ 'Avoid asking follow-up questions — answer with what you already know. ' +
175
+ 'IMPORTANT: do NOT demand a response from the sender in your reply — they already got what they asked for, ' +
176
+ 'and asking them back to reply just creates infinite loops.',
177
+ )
178
+ }
179
+ const replies = messages.filter((m) => m.replyTo)
180
+ if (replies.length > 0) {
181
+ lines.push(
182
+ `The other message${replies.length === 1 ? ' is a reply' : 's are replies'} to something you previously sent. ` +
183
+ 'Read, absorb, and move on — no response is required. Only send a follow-up if there is a ' +
184
+ 'genuinely new question or action that requires it; otherwise this thread is closed.',
185
+ )
186
+ }
187
+ // reference the recipient id so the agent knows which mailbox this is for
188
+ // in case it was spawned without persona context
189
+ lines.push(`(recipient: ${agentId})`)
190
+ return lines.join('\n')
191
+ }
192
+
193
+ async function fireInboxWake(agentId: string): Promise<void> {
194
+ const s = state()
195
+ const key = `msg-wake:${agentId}`
196
+ if (s.firing.has(key)) return
197
+ const ctx = getCtx()
198
+ // Skip agents with an active turn — they'll pick up the messages on their
199
+ // next natural turn or via the next tick after the current one ends.
200
+ if (isActiveAgent(agentId)) return
201
+
202
+ s.firing.add(key)
203
+ try {
204
+ const msgs = messageRepo.drainUnreadForAgent(ctx.db, agentId)
205
+ if (msgs.length === 0) {
206
+ // Raced with another consumer; nothing to do.
207
+ return
208
+ }
209
+ const fromIds = new Set(msgs.map((m) => m.fromAgentId))
210
+ const fromNames = new Map<string, string>()
211
+ for (const fid of fromIds) {
212
+ const sender = agentRepo.get(ctx.db, fid)
213
+ if (sender) fromNames.set(fid, sender.name)
214
+ }
215
+ const prompt = buildInboxPrompt(agentId, msgs, fromNames)
216
+
217
+ try {
218
+ for await (const frame of runAgentTurn(agentId, prompt)) {
219
+ if (frame.kind === 'fatal') {
220
+ console.error(`[scheduler] inbox wake ${agentId} fatal:`, frame.error)
221
+ }
222
+ }
223
+ } catch (err) {
224
+ console.error(`[scheduler] inbox wake ${agentId} unexpected throw:`, err)
225
+ }
226
+ } catch (err) {
227
+ console.error(`[scheduler] inbox drain failed for ${agentId}:`, err)
228
+ } finally {
229
+ s.firing.delete(key)
230
+ }
231
+ }
232
+
233
+ async function tick(): Promise<void> {
234
+ const s = state()
235
+ if (s.stopped) return
236
+ const now = Date.now()
237
+ let triggers: AgentTrigger[]
238
+ let recipients: string[] = []
239
+ try {
240
+ const ctx = getCtx()
241
+ triggers = triggerRepo.listEnabled(ctx.db)
242
+ recipients = messageRepo.listRecipientsWithUnread(ctx.db)
243
+ } catch (err) {
244
+ console.error('[scheduler] tick read failed:', err)
245
+ return
246
+ }
247
+ for (const t of triggers) {
248
+ if (isDue(t, now, s.cronCache)) {
249
+ // Fire async, don't await — the tick itself must stay fast.
250
+ void fireTrigger(t)
251
+ }
252
+ }
253
+ for (const agentId of recipients) {
254
+ // Same fire-and-forget shape as triggers; `fireInboxWake` internally
255
+ // dedup-gates and bails if the agent already has an active run.
256
+ void fireInboxWake(agentId)
257
+ }
258
+ }
259
+
260
+ export function startScheduler(): void {
261
+ const s = state()
262
+ // Replace any existing timer unconditionally so a duplicate startScheduler
263
+ // call (e.g. test harness calling getCtx twice) doesn't leave stale loops.
264
+ if (s.timer) clearInterval(s.timer)
265
+ s.stopped = false
266
+ s.timer = setInterval(() => {
267
+ void tick()
268
+ }, TICK_MS)
269
+ // Unref so the scheduler never blocks process exit in tests.
270
+ if (typeof s.timer.unref === 'function') s.timer.unref()
271
+ }
272
+
273
+ export function stopScheduler(): void {
274
+ const s = state()
275
+ s.stopped = true
276
+ if (s.timer) {
277
+ clearInterval(s.timer)
278
+ s.timer = null
279
+ }
280
+ }
281
+
282
+ // Exposed for tests that want to drive the loop manually rather than wait
283
+ // on wall-clock intervals.
284
+ export async function _tickOnce(): Promise<void> {
285
+ await tick()
286
+ }
287
+
288
+ export function _isDueForTest(
289
+ t: AgentTrigger,
290
+ now: number,
291
+ cronCache: Map<string, ParsedCron> = new Map(),
292
+ ): boolean {
293
+ return isDue(t, now, cronCache)
294
+ }