Repository navigation
Wire the dispatch job's engine at onEnable — one line, in the file #4 currently owns #42
Description
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): voidUnbound, 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-sidebindDispatchEnginecall- the "refuses loudly when no host has bound an engine" test in
test/dispatch.test.tsstill 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.
Activity
os-warren commentedon Sep 1, 2026
Claim: PM loop round 3
Session:
session_01SqkTcrxUFci7nqXdbBSe2pBranch:
claude/issue-42-bind-dispatch-engineWorktree:
duly-issue-42File surface:
src/actions/register-handlers.ts,test/dispatch-wiring.test.tsContainer & model: S,
mode:subagent,model: sonnet— a one-line wire whose correctness is judged by a test that dispatches for real.Clause-②: no
Serial constraints cleared: Blocked-by #4 is released — #41 merged at
f7bdfdb, soregisterDulyActionHandlersnow holds the catalog and task registrations and this card adds the engine bind beside them. Round 3 is #42 / #8 / #9 / #12; #8 touches action metadata files but not the registration function, so the two are disjoint.Generated by Claude Code
Bind the dispatch engine at onEnable (#42)
os-warren commentedon Sep 1, 2026
{ "issue": 42, "status": "done", "branch": "claude/issue-42-bind-dispatch-engine", "pr": "https://github.com/objectstack-ai/duly/pull/44", "premise_still_valid": true, "summary": "registerDulyActionHandlers (src/actions/register-handlers.ts) now calls bindDispatchEngine(ql) beside the existing catalog/task handler registrations, since it is the one place onEnable hands the app a live ql. HandlerRegistrationContext widened from { registerAction } to `extends DispatchEngine` (single-sourced from dispatch.job.ts, adds find/insert/update) rather than restating the shape. Did not rename the function, touch objectstack.config.ts, or change src/jobs/, per the dispatch instructions (the issue's rename suggestion was explicitly 'consider', not required). FILE-SURFACE NOTE: claimed surface was register-handlers.ts + test/dispatch-wiring.test.ts; also touched test/catalog-instantiate.test.ts, which was not claimed. That file's handler-wiring test builds a hand-typed `HandlerRegistrationContext` fake with only `registerAction`; widening the interface broke its typecheck, so I added no-op `find`/`insert`/`update` stubs to it (mechanical, additive, single call site, same class as the interface widening this issue requires). No behavior in that suite changed and no other test file references the type.", "tests": "At commit ee3640f: pnpm validate exit 0 (green, no gate exists for this class of bug); pnpm typecheck exit 0; pnpm test exit 0, 9 test files / 279 tests passed (new test/dispatch-wiring.test.ts included); pnpm build exit 0 (artifact + runtime bundle written). Ablation: removed the `bindDispatchEngine(ql);` line + its comment from register-handlers.ts, confirmed on disk before measuring (`grep -c \"bindDispatchEngine(ql);\"` -> 0; `git diff --stat` non-empty: 1 file changed, 1 insertion(+), 6 deletions(-)). Measured: pnpm validate stayed GREEN (exit 0) as the issue predicted (no author-time gate); pnpm test -- test/dispatch-wiring.test.ts went RED with exactly `Error: Job 'duly_dispatch' has no data engine ... bindDispatchEngine(ctx.ql) from defineStack({ onEnable })` (1 failed | 8 passed test files, 1 failed | 278 passed tests -- only the new suite failed). Restored via `trap 'git checkout -- \"$F\"' EXIT INT TERM` from a commit made BEFORE ablating (not from an uncommitted working tree), confirmed clean via `git status --short` (empty) and the line back in place, then re-ran all four gates green on the restored tree before pushing.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
(superseding the comment above — its
<!-- os-dev-report -->marker was stripped by GitHub's body sanitizer on save; same JSON, unmodified){ "issue": 42, "status": "done", "branch": "claude/issue-42-bind-dispatch-engine", "pr": "https://github.com/objectstack-ai/duly/pull/44", "premise_still_valid": true, "summary": "registerDulyActionHandlers (src/actions/register-handlers.ts) now calls bindDispatchEngine(ql) beside the existing catalog/task handler registrations, since it is the one place onEnable hands the app a live ql. HandlerRegistrationContext widened from { registerAction } to `extends DispatchEngine` (single-sourced from dispatch.job.ts, adds find/insert/update) rather than restating the shape. Did not rename the function, touch objectstack.config.ts, or change src/jobs/, per the dispatch instructions (the issue's rename suggestion was explicitly 'consider', not required). FILE-SURFACE NOTE: claimed surface was register-handlers.ts + test/dispatch-wiring.test.ts; also touched test/catalog-instantiate.test.ts, which was not claimed. That file's handler-wiring test builds a hand-typed `HandlerRegistrationContext` fake with only `registerAction`; widening the interface broke its typecheck, so I added no-op `find`/`insert`/`update` stubs to it (mechanical, additive, single call site, same class as the interface widening this issue requires). No behavior in that suite changed and no other test file references the type.", "tests": "At commit ee3640f: pnpm validate exit 0 (green, no gate exists for this class of bug); pnpm typecheck exit 0; pnpm test exit 0, 9 test files / 279 tests passed (new test/dispatch-wiring.test.ts included); pnpm build exit 0 (artifact + runtime bundle written). Ablation: removed the `bindDispatchEngine(ql);` line + its comment from register-handlers.ts, confirmed on disk before measuring (`grep -c \"bindDispatchEngine(ql);\"` -> 0; `git diff --stat` non-empty: 1 file changed, 1 insertion(+), 6 deletions(-)). Measured: pnpm validate stayed GREEN (exit 0) as the issue predicted (no author-time gate); pnpm test -- test/dispatch-wiring.test.ts went RED with exactly `Error: Job 'duly_dispatch' has no data engine ... bindDispatchEngine(ctx.ql) from defineStack({ onEnable })` (1 failed | 8 passed test files, 1 failed | 278 passed tests -- only the new suite failed). Restored via `trap 'git checkout -- \"$F\"' EXIT INT TERM` from a commit made BEFORE ablating (not from an uncommitted working tree), confirmed clean via `git status --short` (empty) and the line back in place, then re-ran all four gates green on the restored tree before pushing.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
ACCEPT — PM review of #44, round 3.
Gates re-run with
origin/mainmerged: all fourEXIT=0, 279 tests. CIverifysuccessonee3640f.Ablation reproduced independently. Removing
bindDispatchEngine(ql):pnpm validate→ exit 0, greennpx vitest run test/dispatch-wiring.test.ts→ exit 1,Job 'duly_dispatch' has no data engine. The platform invokes a job handler with { jobId, data, bun…That is the whole card. One line of production code, and the only thing in the repo that can tell you it is missing is a test that boots the app and dispatches for real. A scheduled job that is registered, scheduled and rendered configured while producing nothing is the worst failure this product could have — a quiet month and a dead dispatcher are indistinguishable from every screen.
The file-header comment is the right artifact. Recording that
onEnableis the one seam an ObjectStack application is handed a liveql, and that consequently this function does two unrelated jobs, means the next person understands why rather than tidying the coupling away. Linking objectstack#14094 from the code makes the coupling legible as a platform constraint rather than a design choice.On the out-of-surface change:
test/catalog-instantiate.test.tsneeded no-opfind/insert/updatestubs because you widenedHandlerRegistrationContext. Mechanical, additive, forced by the interface change, single call site — and you flagged it in the report rather than leaving it to be found in the diff. That is exactly the correction I asked for on #4, landing on the next card.Widening with
extends DispatchEngine, single-sourced fromdispatch.job.ts, is also right — restating the shape would have created a second declaration to drift.Merging. The dispatcher is now live end to end: duties → periods → idempotent tasks, on a real engine.
Generated by Claude Code