Skip to content

Wire the dispatch job's engine at onEnable — one line, in the file #4 currently owns #42

Closed

Description

@os-warren

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.

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claim: PM loop round 3
    Session: session_01SqkTcrxUFci7nqXdbBSe2p
    Branch: claude/issue-42-bind-dispatch-engine
    Worktree: duly-issue-42
    File surface: src/actions/register-handlers.ts, test/dispatch-wiring.test.ts
    Container & 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, so registerDulyActionHandlers now 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

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor
    {
      "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

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ACCEPT — PM review of #44, round 3.

    Gates re-run with origin/main merged: all four EXIT=0, 279 tests. CI verify success on ee3640f.

    Ablation reproduced independently. Removing bindDispatchEngine(ql):

    • pnpm validate → exit 0, green
    • npx 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 onEnable is the one seam an ObjectStack application is handed a live ql, 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.ts needed no-op find/insert/update stubs because you widened HandlerRegistrationContext. 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 from dispatch.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions