feat(agents): DefaultAgentDispatch orchestrator wiring all four ports #531
Closed
code-lead
wants to merge 2 commits from
boss/518 into main
pull from: boss/518
merge into: charles:main
charles:main
charles:chore/sync-pre-push-from-forge-base
charles:fix/flows-yaml-dispatch-identity
charles:feat/board-tap-to-assign
charles:dev/1107
charles:code-lead/1106
charles:code-lead/1108
charles:dev/1104
charles:code-lead/1103
charles:code-lead/1080
charles:dev/1087
charles:feat/flows-yaml-ci-events
charles:chore/board-drop-stalled-and-density-controls
charles:fix/flows-yaml-routes-always-register
charles:flows-yaml/api-defaults
charles:dev/1023
charles:fix/event-log-history-bleed
charles:fix/janitor-fix-ci-logs-and-cap
charles:dev/1022
charles:fix/board-card-provider
charles:code-lead/1036
charles:dev/1025
charles:code-lead/1020
charles:dev/1017
charles:code-lead/1026
charles:feat/web-shortcut-registry-1018
charles:dev/1015
charles:code-lead/1009
charles:code-lead/1008
charles:dev/975
charles:dev/969
charles:dev/973
charles:dev/967
charles:code-lead/968
charles:code-lead/953
charles:dev/970
charles:dev/976
charles:code-lead/966
charles:code-lead/956
charles:code-lead/951
charles:dev/962
charles:dev/963
charles:dev/977
charles:dev/955
charles:dev/983
charles:dev/961
charles:dev/974
charles:code-lead/950
charles:code-lead/939
charles:dev/941
charles:dev/940
charles:dev/937
charles:dev/938
charles:dev/936
charles:dev/935
charles:feat/web-i18n-fr-locale
charles:feat/spec-editor-ui-polish
charles:chore/drop-legacy-compat
charles:fix/skills-drop-preview-pane
charles:fix/882-skills-safety-rail
charles:dev/911
charles:dev/909
charles:dev/923
charles:dev/917
charles:dev/915
charles:feat/879-sr11-m2-drop-legacy-skill
charles:code-lead/873
charles:dev/881
charles:code-lead/869
charles:dev/867
charles:code-lead/845
charles:code-lead/843
charles:code-lead/844
charles:dev/837
charles:dev/861
charles:dev/849
charles:code-lead/837
charles:code-lead/842
charles:fix/dedup-rebase-inflight
charles:dev/838
charles:code-lead/847
charles:dev/833
charles:code-lead/848
charles:pr/838
charles:code-lead/841
charles:feat/settings-save-bar/836
charles:code-lead/840
charles:dev/846
charles:code-lead/839
charles:dev/832
charles:fix/board-sse-stale-cache
charles:dev/834
charles:dev/835
charles:feat/settings-breadcrumbs
charles:feat/forge-oauth-credentials
charles:refactor/service-config-consolidation
charles:feat/agent-tokens-to-secrets
charles:feat/gitlab-oauth-to-db
charles:feat/authelia-rip-and-voice-fixes
charles:fix/rebase-storm-and-dead-letter
charles:code-lead/797
charles:code-lead/796
charles:dev/811
charles:code-lead/798
charles:dev/810
charles:code-lead/795
charles:dev/808
charles:code-lead/794
charles:dev/805
charles:dev/802
charles:dev/803
charles:feat/avatar-menu-settings-entry
charles:feat/per-agent-token-tracking
charles:dev/793
charles:dev/747
charles:dev/752
charles:code-lead/790
charles:code-lead/759
charles:dev/756
charles:dev/760
charles:dev/741
charles:dev/767
charles:dev/740
charles:dev/709
charles:dev/644
charles:dev/637
charles:boss/614
charles:dev/600
charles:dev/611
charles:dev/585
charles:fix/login-bonus-fixes
charles:boss/544
charles:dev/542
charles:refactor/api-prefix-and-session-gate
charles:dev/489
charles:boss/531
charles:dev/499
charles:boss/516
charles:dev/530
charles:dev/517
charles:dev/519
charles:dev/515
charles:dev/522
charles:dev/503
charles:dev/471
charles:boss/329
charles:dev/417
charles:dev/418
charles:dev/402
charles:boss/327
charles:dev/334
charles:dev/332
charles:boss/326
charles:boss/325
charles:dev/331
charles:boss/324
charles:boss/323
charles:boss/322
charles:dev/294
charles:test/s11-task-analytics
charles:dev/262
charles:boss/270
charles:dev/268
charles:foreman/ui-consolidation-spec
charles:dev/234
charles:boss/196
charles:boss/176
charles:boss/164
charles:fix/124-session-persist-bind
charles:boss/52
charles:dev/87
charles:boss/73
charles:dev/77
charles:dev/81
charles:dev/82
charles:boss/79
charles:dev/42
charles:dev/35
charles:boss/7
No reviewers
Labels
Clear labels
area:agents
Agent types, pool scheduling, per-instance config
area:dashboard
Dashboard UI and observability surfaces
area:database
DB layer — schema, migrations, ORM, raw SQL
area:design
UI/UX mockup work — routes to designer agent
area:design-review
Design review dispatch — routes to design-reviewer agent
area:flows
Flow runner — YAML loader, executor, op registry, expression eval
area:infra
Deployment, isolation, containers, systemd units
area:meta
Tracking, scaffolding, project setup
area:security
Security — routes to reviewer-security (opus)
area:sessions
Session-id store, Claude SDK resume logic
area:webhook
Forgejo webhook routing and handlers
area:workdir
Clone cache, worktrees, git identity
security
Security-sensitive issue
type:bug
Bug
type:chore
Chore
type:meta
Tracking or decisions, not implementation work
type:user-story
User story
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!531
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/518"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #518.
Concrete
AgentDispatchPortadapter atinfrastructure/agents/default-agent-dispatch.tsthat composes the four M25 ports (ClaudeAgentPort,ContainerLifecyclePort,McpRegistryPort,WorkerRegistryPort) plus aSessionStorePortinto a single dispatch surface.Summary
dispatch()pre-flights the container, resolves MCP servers + allowed tools, enqueues a runner closure on the worker registry, and tracks the live task. Per-callforgeTokenoverride beatsdefaultTokenfor both the MCP servers and the env binding.steer()pushes onto a private boundedSteerChannelthe SDK consumes via a streaming-input async iterator yielding the initial prompt then steer messages in arrival order. Empty / closed-task steers return{ok:false, reason:"conflict"}.kill()aborts the controller, closes the steer channel, cancels the registry slot. Idempotent on already-settled tasks.status()lifts queued/running from the live table and settled outcomes from a small in-memory record (DB lift is a follow-up migration).runAgentTask: same FIFO ordering, same(agentType, repo, issue)session key, first-event session capture, container release in the runner'sfinally.domain/ports/carry the contracts forward — to be unified once the parallel #514–#517 PRs merge.Existing callers stay on
runAgentTaskper the issue's out-of-scope note; the migration ticket wires this in.Test plan
bun test src/infrastructure/agents/default-agent-dispatch.test.ts— 4 pass: happy path (events, session capture, container release, settled=success), forgeToken override, kill mid-task (abort propagates, killed outcome, idempotent second kill), steer mid-task (message reaches SDK iterator, post-settle conflict).bun run typecheckclean.bun x biome check apps/server/src/{domain/ports,infrastructure/agents}clean.behavior
apps/server/src/domain/ports/mcp-registry-port.ts:13importsMcpServerSpecfrom../../infrastructure/agent/claude-port. A domain port must not depend on an infrastructure module — this inverts the hexagonal dependency rule and prevents the domain layer from being extracted (or tested) without pulling in the infrastructure layer.McpServerSpecshould be defined indomain/ports/itself or promoted to@claude-hooks/shared, then re-imported from there byclaude-port.ts.behavior
DefaultAgentDispatch.settled(default-agent-dispatch.ts) is an unboundedMap<string, SettledTask>. Every completed task remains in memory for the process lifetime. With no eviction at all (not even a cap) this will grow without bound in a long-running service before the DB migration lands. A simple LRU cap of ~1 000 entries is enough to prevent the leak while keepingstatus()correct for any recently-settled task a caller would realistically query.Aborted rebase on
origin/main. The parallel ports (#514–#517) landed with shapes incompatible with this PR:AgentDispatchRequest: main carriesagentType: string+repo: AgentDispatchRepo+metadata; this PR carriesagent: AgentInstance+ref: IssueOrPrRef+stateless_session?.AgentDispatchHandle: main returns{taskId, worker, containerId?, sessionKey}; this PR returns{taskId}only.AgentDispatchStatus: main is flat (state: "queued"|"running"|"succeeded"|"failed"|"cancelled", ISO timestamps,prUrl?); this PR uses a discriminated union withoutcome+ numeric timestamps.steer/killreturnvoidon main vsSteerVerdict/{ok: boolean}here.McpRegistryPort: main takesRepoBinding; this PR usesMcpBinding{forgeType, token}+ adefaultToken()method that doesn't exist on main.ContainerLifecyclePort: main exposesensureReady → ContainerHandle{mode, container, hostCwd, inContainerCwd, execPath}+buildExecArgs+release(handle, ReleaseReason); this PR usesContainerEnsureResult{containerName, pathToExecutable?, inContainerCwd?}+release(agent, taskId).The orchestrator and test (~600 of 992 added lines) consume the obsolete shapes throughout. Adapting requires new design decisions outside rebase scope (notably: where the
AgentInstancecomes from once the request only carriesagentType).Recommend closing this PR and reopening as a fresh implementation built on main's port surfaces.
Rebase on
origin/mainaborted — design invalidated by intervening merges. Needs human triage.Conflicts (all three are add/add):
apps/server/src/domain/ports/agent-dispatch-port.tsapps/server/src/domain/ports/container-lifecycle-port.tsapps/server/src/domain/ports/mcp-registry-port.tsWhat landed on main since this PR branched (
9e98643):b398770—AgentDispatchPort(canonical hex shape:{ agentType, repo: AgentDispatchRepo, issueOrPr, prompt, forgeToken?, resumeSessionKey?, metadata }; handle exposesworker/containerId/sessionKey; status is a flat record).a92edf1—ContainerLifecyclePort(returnsContainerHandle = { mode, container, hostCwd, inContainerCwd, execPath };release(handle, reason)notrelease(agent, taskId);buildExecEnv(agent, { repo, issue_number, branch, forge?, forgeToken? })).ef7b74e—McpRegistryPort(serversFor(agent, RepoBinding)/allowedTools(agent, RepoBinding)— nodefaultToken; adapter resolves the token from the agent internally).f05eee6/adcdbee—ClaudeAgentPortmigration;infrastructure/agent/default-agent-dispatch.tsalready exists on main as a thin dispatcher (constructor:mcpRegistry,forgePortFactory,runner; syncdispatch(req)that resolves token, builds servers, hands off to an injected runner).Why this isn't a conflict-resolution job:
AgentDispatchOutcome,SteerVerdict,AgentInstancefromagent-dispatch-port— none exist on main's canonical port.req.agent: AgentInstancevs main'sreq.agentType: string— orchestrator would need a new agent-resolver dep.req.ref.{repo, issue_number, forgeType, branch?}vs main'sreq.repo: { forge, owner, name }+req.issueOrPr+req.metadata— translation layer required.mcp.defaultToken(agent, forge)doesn't exist on main; the registry resolves the token internally.container.release(agent, taskId)doesn't exist on main; it'srelease(handle, reason)keyed off theContainerHandlethe orchestrator must thread through.infrastructure/agents/(plural). Main already has a differentDefaultAgentDispatchatinfrastructure/agent/(singular).Effectively the 443-line orchestrator + 494-line test would need to be rewritten against main's port shapes, plus a decision on how to reconcile with the existing thin dispatcher. That's design work, not rebase work — bailing rather than force a bad merge through.
Rebase on
origin/mainaborted — the PR's design has been invalidated by parallel work on main.This PR redefines three domain ports (
AgentDispatchPort,ContainerLifecyclePort,McpRegistryPort) and ships an orchestrator + test file built against those redefined shapes. Since the PR was opened, four commits landed on main shipping the same three ports with materially different surfaces, plus two adapters (DockerContainerLifecycle,HostContainerLifecycle,DefaultMcpRegistry) already wired against main's shapes:b398770AgentDispatchPort domain interfacea92edf1ContainerLifecyclePort + Docker / host adaptersef7b74eMcpRegistryPort + DefaultMcpRegistry adapteradcdbeeper-call forge-token override in AgentDispatchPort.dispatch()Concrete divergence:
agent: AgentInstance+ref: IssueOrPrRef+stateless_session; main usesagentType: string+repo: AgentDispatchRepo+issueOrPr: number+metadata.AgentDispatchHandle,AgentDispatchStatus, and thesteer/killreturn types all differ.ensureReady → ContainerEnsureResultandrelease(agent, taskId). Main'sensureReady → ContainerHandle, adds abuildExecArgsmethod,release(handle, reason). Already consumed by main's two adapters.defaultToken(agent, forgeType): stringand uses anMcpBinding. Main has no such method and usesRepoBindingfromwebhook-config.Per the rebase rules, shared port files take main as the source of truth — but the orchestrator and its tests are wholly built on the PR's redefined shapes. What remains after dropping those redefinitions is a from-scratch reimplementation against main's port surfaces, not a rebase. Forcing it through would silently change the merged port contract.
Recommendation: close-and-reopen this work as a fresh implementation that consumes main's existing
AgentDispatchPort/ContainerLifecyclePort/McpRegistryPortshapes plus a newWorkerRegistryPort(the latter is uncontroversial — keep that file as-is). The orchestrator's intent (single dispatch surface wiring container, MCP registry, worker registry, and Claude agent ports) carries over; only the type plumbing needs to be rebuilt.Rebase aborted — the PR's design has been invalidated by the parallel ports landing on
main. Stopping here per the rebase workflow rather than forcing a bad merge through.The three port files this PR co-authored were also added on
mainvia #514–#517 (b398770,a92edf1,ef7b74e) plusadcdbee(per-call forge-token override) andf05eee6(ClaudeAgentPort migration). Their final shapes are incompatible with the ones this orchestrator was written against:agent-dispatch-port.ts— main hasAgentDispatchRepo+ flatagentType/issueOrPr/metadata; this PR usesAgentInstance+IssueOrPrRef. Main returnsvoidfromsteer/kill; this PR returnsSteerVerdict/{ok}.AgentDispatchHandlerequiresworker+sessionKeyon main; this PR returns just{taskId}.container-lifecycle-port.ts— main hasContainerHandle(mode/hostCwd/execPath) +buildExecArgs+release(handle, reason); this PR usesContainerEnsureResult+release(agent, taskId)with no exec-args wrapper.mcp-registry-port.ts— main hasserversFor(agent, RepoBinding)and nodefaultTokenaccessor; this PR's orchestrator callsmcp.defaultToken(agent, forgeType)to resolve the per-call token fallback. There is no equivalent on the new port.Additionally main already has a small
infrastructure/agent/default-agent-dispatch.ts(different folder) implementing theinfrastructure/agent/agent-dispatch-port.tssurface from #521 — that's a separate, narrower port for forge-token resolution. TwoDefaultAgentDispatchclasses in two folders against two different ports is also a conflict to resolve.The orchestrator's intent (compose all four ports into one dispatch surface) is still valid, but it needs a from-scratch reimplementation against the new shapes, not a rebase. Closing-or-rewriting decision belongs to a human — leaving the branch as-is.
99bd578875ea8ddb4746ea8ddb47469aaed6c689🔀 Unassigned by operator via board (running task cancelled).
Rebase blocked — needs domain decision
Tried to rebase
boss/518on currentmain. One textual conflict ondefault-agent-dispatch.ts(main has a 31-line wrapper from #533, this PR has the 631-line orchestrator). Took--theirsto keep this PR's full impl, but the resulting tree failsbun run typecheckwith semantic mismatches:AgentInstance(this PR) vsResolvedAgent(main) — different shapes, missinggit_author,git_name,git_email,branch_prefix,forgejo_tokenMcpServerSpec.args— main:string[], this PR's port:readonly string[]AgentDispatchPort.dispatchreturn — main:Promise<TaskResult>, this PR:Promise<AgentDispatchHandle>AgentDispatchRequestfield set differs (this PR addsagentType,repo,issueOrPr,prompt; main keepstaskId,task,abort, callbacks)Root cause: this PR was written in parallel with #527, #529, #533 which each tweaked port shapes. The merge train picked the smaller-scoped types; this PR's orchestrator assumes its own.
Two paths forward, both need a human call:
AgentInstance,AgentDispatchHandle, fullAgentDispatchRequest) and update the thin wrapper + callsites to match. Bigger blast radius, cleaner end state.ResolvedAgent,Promise<TaskResult>, currentAgentDispatchRequest). Smaller diff, less invasive.Local rebase reverted;
boss/518upstream untouched. Leaving open for triage.Closing per boss's own recommendation in this thread (4 analyses converged on close-and-rewrite).
Issue #518 stays open and is being updated with:
AgentDispatchPort,ContainerLifecyclePort,McpRegistryPort)worker-registry-port.tsfromboss/518is uncontroversial — port verbatimmcp-registry-port.tsalready lives cleanly indomain/ports/, this PR's parallel copy is the one being discardedBranch
boss/518left in place for reference until the rewrite lands.Pull request closed