Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.

feat: planner-dispatch mode and dispatcher-mode comparison (#146) - #152

Merged
lukemaj merged 1 commit into
mainfrom
feat/146-planner-dispatch
Sep 29, 2026
Merged

lukemaj merged 1 commit into
mainfrom
feat/146-planner-dispatch

Conversation

@lukemaj

@lukemaj lukemaj commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

What: submit --dispatcher planner runs a Prism job with the planner thread as its dispatcher, and scripts/compare_dispatch.py compares both modes from the runner and Agent Observer ledgers; luna stays the default.
Why: Dispatch took 6-11 minutes before the first worker on most jobs, and nobody has measured whether the planner routing the job itself is faster or cheaper (#146); the planner then also owns #147's router-defect duty.
So what: Review and merge; to expose the mode in Prism, Chromeria's prism_submit needs a dispatcher argument (described below, not in this PR); the user then runs at least five matched jobs per mode and picks the default from the comparison.

Closes #146 (mode and comparison method; the comparison on real jobs and the default stay with the user). Refs #147 (planner-dispatch half).

Change

  • submit --dispatcher luna|planner (CLI and core.submit, default luna). New jobs.dispatcher column; rows from before it read as luna. The mode is part of the idempotent payload, is in the submitted event and the submit ack.
  • Planner mode (runner/controller.py): no dispatcher thread starts. Every decision point a Luna dispatcher gets (first dispatch, worker result, planner answer, refused completion, envelope repair) is posted into the planner thread, headed PRISM DISPATCH DECISION for job <id> (<key>), and the planner's reply carries the envelope. Each decision is keyed by the action seq; its message id is saved before the post (planner_dispatch_posted), so a restarted controller watches the same post instead of asking twice. The reply records planner_dispatch_reply with wait_secs. No reply blocks as planner_dispatch_pending (recover-owned): recover re-watches the same post, or posts afresh after an errored or interrupted turn. Workers and reviewers become children of the planner thread. Cancel never interrupts the planner thread (it is not saved as a job thread). The Prism Dispatcher list is not required in this mode.
  • A planner_question from the planner is for the user: stored, not posted back to the planner, and the job blocks as planner_question_pending until answer plus recover. The runner's own escalation question still reaches the planner as today.
  • runner/adapters.py: PLANNER_ACTION_PROTOCOL shares the worker duties, the ROUTER DEFECTS paragraph (Dispatcher reports router defects as Issues (planner does it in planner-dispatch mode) #147) and the reply shapes with Luna's; the role and the packet-mistake route differ (the planner corrects it in the instructions, or cancels and resubmits). LUNA_ACTION_PROTOCOL is byte-identical to main.
  • scripts/compare_dispatch.py and RUNNER.md "Comparing dispatcher modes": per mode, success rate, median seconds to first worker (submitted to the first impl_* t3_thread event), to a reviewed PR (first review_verdict approve), and dispatch plus question tokens from Observer responses by counter semantics (Luna: the dispatcher thread's session; planner: the planner session inside each decision window).
  • GLOSSARY: "Dispatcher mode".

Pending and gaps

  • Chromeria (not edited, per the brief): prism_submit cannot pass the mode. Needed: an optional dispatcher: "luna" | "planner" in PrismSubmitInput (apps/server/src/mcp/toolkits/prism/tools.ts) and ...(input.dispatcher ? ["--dispatcher", input.dispatcher] : []) in submitArgs (apps/server/src/mcp/toolkits/prism/handlers.ts). Until then the mode is reachable through the CLI only. prism_answer runs answer without recover; answer sets the job back to running, but a job with no live controller still needs recover (true in both modes today).
  • Agent Observer: it imports neither jobs.dispatcher nor event payloads, so mode grouping and decision windows come from jobs.db; task show --task router:<id> reports cost unknown for T3-path jobs, so cost per mode stays unknown until Observer attributes those sessions.
  • Unverified live: T3 behaviour when the first decision is posted while the planner's prism_submit turn still runs is the same path planner questions use today; not exercised live here. The comparison itself (five matched jobs per mode) is behavioral and after release.

Proof

  • Red first: tests/test_issue146.py against the base runner fails at import (module 'runner.adapters' has no attribute 'PLANNER_ACTION_PROTOCOL'); the new behavior did not exist.
  • Head e2ab553: python3 scripts/test.py: Ran 402 tests ... OK; python3 -m compileall -q runner scripts tests clean; versionctl release-check: release candidate OK: v0.44.0 at e2ab553977b6f37d05cbdbfb27147ffff1de23ab.
  • tests/test_issue146.py (17 tests): default and planner submit, invalid mode, cross-mode resubmission conflict, legacy rows, CLI flag and ack; a planner-mode job runs to succeeded with one worker thread under the planner thread, no dispatcher thread, exactly two decision posts carrying the planner protocol; Luna mode still starts its dispatcher thread; no Dispatcher list needed; no-reply block then adoption of the same post; one envelope repair; planner question waits for the user; cancel does not interrupt the planner thread; protocol pins for the router-defect duty and packet mistakes; the comparison script's timings, windowed tokens and summary on synthetic ledgers.
  • scripts/compare_dispatch.py --since 2026-09-25 on the real ledgers: 30 terminal Luna jobs, 10 succeeded, median 439 s to first worker, median 1695 s to a reviewed PR (the Luna baseline).

Elon record

🤖 Generated with Claude Code

`submit --dispatcher planner` runs a job with the planner thread as its
dispatcher. No dispatcher thread starts: each decision point (first
dispatch, worker result, answer, refused completion, envelope repair) is
posted into the planner thread, keyed by the action seq with its message
id saved before the post, so a restarted controller watches the same post.
A reply records planner_dispatch_reply with wait_secs; no reply blocks as
planner_dispatch_pending, which recover continues. The planner's protocol
carries #147's router-defect duty. A planner_question waits for the user
(`answer` plus `recover`). Cancel never interrupts the planner thread.
`luna` stays the default and its protocol text is unchanged.

scripts/compare_dispatch.py reads jobs.db and Agent Observer's ledger
read-only and reports, per mode, success rate, time to first worker and
to a reviewed PR, and dispatch tokens by counter semantics.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lukemaj
lukemaj merged commit 696407e into main Sep 29, 2026
2 checks passed
@lukemaj
lukemaj deleted the feat/146-planner-dispatch branch September 29, 2026 21:43
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optional dispatcher: let the planner dispatch, then compare both modes with Agent Observer

1 participant