Split out of #2 rather than bundled into it, because the one line lands in src/actions/register-handlers.ts, which #4 is editing right now.
What is missing
src/jobs/dispatch.job.ts (from #2) is complete and tested: the planner, the insert-and-swallow loop, the backfill window, last_dispatched_period. What it does not have is an engine, because the platform does not give a job handler one.
Measured on @objectstack/* 17.2.0, a defineJob handler is invoked with exactly { jobId, data, bundle } — no engine, no service registry, no logger. Filed upstream as objectstack-ai/objectstack#14094. Until that lands, the only place this application is handed ctx.ql is defineStack({ onEnable }), which is already wired through to registerDulyActionHandlers(ql).
So the dispatcher exposes one named seam and nothing calls it yet:
export function bindDispatchEngine(engine: DispatchEngine): void
Unbound, dulyDispatch refuses loudly — Job 'duly_dispatch' has no data engine … No tasks were dispatched. That is deliberate: a dispatcher that quietly does nothing is the worst failure this product can have, so the run fails and says why rather than reporting a clean night. But it does mean the job does not currently dispatch anything at runtime, and this issue is what closes that.
The change
In src/actions/register-handlers.ts, inside the existing function:
import { bindDispatchEngine } from '../jobs/dispatch.job.js';
// …
export function registerDulyActionHandlers(ql: HandlerRegistrationContext): void {
registerCatalogActionHandlers(ql);
registerTaskActionHandlers(ql); // #4
bindDispatchEngine(ql); // this issue
}
HandlerRegistrationContext is declared as { registerAction } — narrowed to actions on purpose. It will need to widen to the find / insert / update slice DispatchEngine names. The value passed is the real ObjectQL engine (AppPlugin builds hostContext = { ...ctx, ql, logger, drivers }), so nothing new has to be threaded; only the type has to stop hiding it.
Consider renaming the function while you are there — it registers more than action handlers now. registerDulyRuntimeHandlers says what it does.
Acceptance
dulyDispatch({ jobId: 'duly_dispatch' }) dispatches against the real engine after a boot, with no test-side bindDispatchEngine call
- the "refuses loudly when no host has bound an engine" test in
test/dispatch.test.ts still passes (it unbinds first, so it should be unaffected)
pnpm validate && pnpm typecheck && pnpm test && pnpm build
Known limit, and why it is not this issue's to fix
This closes the objectstack dev / config-boot path. It does not close the artifact path: objectstack build emits functions into a runtime module exporting only { functions, meta }, the artifact JSON carries no onEnable, and mergeRuntimeModule merges only functions — so an artifact-served boot never runs onEnable and the binding is never made. That is the second half of objectstack#14094 and it affects action handlers identically; it is not something this application can fix from its own side.
Blocked-by
Blocked-by: #4 — same file, in flight. Land #4 first.
Split out of #2 rather than bundled into it, because the one line lands in
src/actions/register-handlers.ts, which #4 is editing right now.What is missing
src/jobs/dispatch.job.ts(from #2) is complete and tested: the planner, the insert-and-swallow loop, the backfill window,last_dispatched_period. What it does not have is an engine, because the platform does not give a job handler one.Measured on
@objectstack/*17.2.0, adefineJobhandler is invoked with exactly{ jobId, data, bundle }— no engine, no service registry, no logger. Filed upstream as objectstack-ai/objectstack#14094. Until that lands, the only place this application is handedctx.qlisdefineStack({ onEnable }), which is already wired through toregisterDulyActionHandlers(ql).So the dispatcher exposes one named seam and nothing calls it yet:
Unbound,
dulyDispatchrefuses loudly —Job 'duly_dispatch' has no data engine … No tasks were dispatched.That is deliberate: a dispatcher that quietly does nothing is the worst failure this product can have, so the run fails and says why rather than reporting a clean night. But it does mean the job does not currently dispatch anything at runtime, and this issue is what closes that.The change
In
src/actions/register-handlers.ts, inside the existing function:HandlerRegistrationContextis declared as{ registerAction }— narrowed to actions on purpose. It will need to widen to thefind/insert/updatesliceDispatchEnginenames. The value passed is the real ObjectQL engine (AppPluginbuildshostContext = { ...ctx, ql, logger, drivers }), so nothing new has to be threaded; only the type has to stop hiding it.Consider renaming the function while you are there — it registers more than action handlers now.
registerDulyRuntimeHandlerssays what it does.Acceptance
dulyDispatch({ jobId: 'duly_dispatch' })dispatches against the real engine after a boot, with no test-sidebindDispatchEnginecalltest/dispatch.test.tsstill passes (it unbinds first, so it should be unaffected)pnpm validate && pnpm typecheck && pnpm test && pnpm buildKnown limit, and why it is not this issue's to fix
This closes the
objectstack dev/ config-boot path. It does not close the artifact path:objectstack buildemitsfunctionsinto a runtime module exporting only{ functions, meta }, the artifact JSON carries noonEnable, andmergeRuntimeModulemerges onlyfunctions— so an artifact-served boot never runsonEnableand the binding is never made. That is the second half of objectstack#14094 and it affects action handlers identically; it is not something this application can fix from its own side.Blocked-by
Blocked-by: #4 — same file, in flight. Land #4 first.