|
1 | 1 | import { Bus } from "@/bus" |
2 | 2 | import { BusEvent } from "@/bus/bus-event" |
| 3 | +import { EffectBridge } from "@/effect/bridge" |
3 | 4 | import { Identifier } from "@/id/id" |
4 | 5 | import { Database } from "@/storage/db" |
5 | 6 | import * as Log from "@opencode-ai/core/util/log" |
@@ -129,7 +130,10 @@ export const layer = Layer.effect( |
129 | 130 | Effect.gen(function* () { |
130 | 131 | const bus = yield* Bus.Service |
131 | 132 |
|
132 | | - const timers = new Map<ID, { cron: Cron; sessionID: SessionID }>() |
| 133 | + const timers = new Map< |
| 134 | + ID, |
| 135 | + { cron: Cron; sessionID: SessionID; bridge: EffectBridge.Shape } |
| 136 | + >() |
133 | 137 |
|
134 | 138 | const recordRun: Interface["recordRun"] = Effect.fn("Schedule.recordRun")( |
135 | 139 | function* (scheduleID, sessionID, runStatus, ranAt) { |
@@ -161,43 +165,31 @@ export const layer = Layer.effect( |
161 | 165 | }) |
162 | 166 | }) |
163 | 167 |
|
164 | | - const startTimer = (scheduleID: ID, sessionID: SessionID, expression: string) => { |
| 168 | + const startTimer = ( |
| 169 | + scheduleID: ID, |
| 170 | + sessionID: SessionID, |
| 171 | + expression: string, |
| 172 | + bridge: EffectBridge.Shape, |
| 173 | + ) => { |
165 | 174 | const cron = new Cron(expression, {}, () => { |
166 | | - Effect.runPromise(tick(scheduleID)).catch((e) => |
| 175 | + // Run tick inside the captured instance/workspace context so that |
| 176 | + // services hung off InstanceState (e.g. Bus.publish) resolve cleanly. |
| 177 | + bridge.promise(tick(scheduleID)).catch((e) => |
167 | 178 | log.error("schedule timer error", { |
168 | 179 | scheduleID, |
169 | 180 | error: e instanceof Error ? e.message : String(e), |
170 | 181 | }), |
171 | 182 | ) |
172 | 183 | }) |
173 | | - timers.set(scheduleID, { cron, sessionID }) |
| 184 | + timers.set(scheduleID, { cron, sessionID, bridge }) |
174 | 185 | } |
175 | 186 |
|
176 | | - // Hydrate on startup, deferred via setTimeout so that the layer itself |
177 | | - // can finish constructing without touching the database (the database |
178 | | - // and its surrounding context are not always ready at layer-init time, |
179 | | - // e.g. in tool-registry unit tests). Errors are swallowed; failed |
180 | | - // hydration just means the existing rows won't get timers until the |
181 | | - // next create()/list() touches the data manually. |
182 | | - setTimeout(() => { |
183 | | - try { |
184 | | - const rows = Database.use((db) => db.select().from(ScheduleTable).all()) |
185 | | - for (const row of rows) { |
186 | | - try { |
187 | | - startTimer(row.id as ID, row.session_id as SessionID, row.expression) |
188 | | - } catch (e) { |
189 | | - log.error("failed to hydrate schedule", { |
190 | | - scheduleID: row.id, |
191 | | - error: e instanceof Error ? e.message : String(e), |
192 | | - }) |
193 | | - } |
194 | | - } |
195 | | - } catch (e) { |
196 | | - log.debug?.("schedule hydrate skipped", { |
197 | | - error: e instanceof Error ? e.message : String(e), |
198 | | - }) |
199 | | - } |
200 | | - }, 0) |
| 187 | + // NOTE: Cron timers need an EffectBridge captured inside an active |
| 188 | + // InstanceContext (so that bus.publish / SessionPrompt run in the right |
| 189 | + // instance scope). We only have that during create(); hydrate-on-restart |
| 190 | + // is a TODO that needs per-project instance lookup. Until then, |
| 191 | + // schedules don't survive a sidecar restart — the rows persist in the |
| 192 | + // DB but no timers are reattached. |
201 | 193 |
|
202 | 194 | const list: Interface["list"] = Effect.fn("Schedule.list")(function* (sessionID: SessionID) { |
203 | 195 | const rows = yield* Effect.sync(() => |
@@ -268,7 +260,8 @@ export const layer = Layer.effect( |
268 | 260 | .run() |
269 | 261 | }), |
270 | 262 | ) |
271 | | - startTimer(id, input.sessionID, input.expression) |
| 263 | + const bridge = yield* EffectBridge.make() |
| 264 | + startTimer(id, input.sessionID, input.expression, bridge) |
272 | 265 | yield* bus.publish(Event.Created, { scheduleID: id, sessionID: input.sessionID }) |
273 | 266 | return { |
274 | 267 | id, |
|
0 commit comments