Skip to content

fix(templates): publish agent.upsert with the hasStream flag on launch - #1038

Merged
selfcontained merged 1 commit into
mainfrom
agt_c29292e62569/job-debt-collector-cf0194d2
Sep 2, 2026
Merged

fix(templates): publish agent.upsert with the hasStream flag on launch#1038
selfcontained merged 1 commit into
mainfrom
agt_c29292e62569/job-debt-collector-cf0194d2

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

What

apps/server/src/routes/templates.ts was the only agent.upsert publish site in the repo that sent the raw AgentRecord instead of withStreamFlag(...). This threads withStreamFlag into TemplateRouteDeps and wraps the launch publish, plus a colocated test.

Why it's tech debt

There are 31 agent.upsert publish sites (server.ts, server/mcp-handlers.ts x7, server/mcp-review-handlers.ts x2, server/agent-lifecycle-runtime.ts x7, routes/release.ts, routes/agents/{terminal,crud,lifecycle}-routes.ts, routes/templates.ts). Thirty wrapped the agent; this one didn't. Verified with grep -rn --text -A3 'type: "agent.upsert"' apps/server/src/.

Is it a behavior change? Yes, but an unobservable one — and it closes a latent hole

  • Web's applyAgentUpsert (apps/web/src/hooks/use-sse.ts) replaces the cached agent wholesale, preserving only submittedReviewId. So the unflagged payload dropped hasStream from the cached row.
  • The only consumer is agents-view.tsx:275focusedAgent?.hasStream ?? false — so undefined and false were already indistinguishable.
  • Could the launched agent ever already have a stream? Traced it: templateService.launchTemplate always calls agentManager.createAgent, so the record is always brand new. createAgent fires onAgentCreated early (agents/manager.ts:369, which publishes a flagged upsert with hasStream: false), then runs launchWithSetupScript, then re-fetches and returns the final record — which is what this route publishes. A stream only exists once the agent's CLI drives a CDP screencast via StreamManager.startStream, which cannot happen inside that window. So there is no user-visible symptom today; the fix makes the payload correct by construction rather than correct by luck, and stops the second publish from being able to clobber the first one's flag.
  • No release-notes entry: nothing user-visible changed.

Deliberately NOT changed (near-misses)

  • The HTTP response body. return { agent: result.agent } stays unflagged. crud-routes.ts:333 does the same (return reply.code(201).send({ agent }) after a flagged publish); lifecycle-routes.ts:63 is the one that flags its response. That inconsistency is pre-existing and is about the REST payload, not the SSE payload this entry is about — folding it in would widen the blast radius to the launch endpoint's public response shape for no benefit.
  • server.ts:417's onAgentCreated publish. It looks like this route's publish is redundant with it, but it isn't: the listener fires with the record as of insert, before media seeding, pin/prompt assembly and process launch, and the route publishes the re-fetched final record. Removing either one loses information.
  • TemplateRouteDeps.publishUiEvent: (event: unknown) => void. Eight route dep types declare it as unknown and server.ts casts event as UiEvent at 13 wiring sites; only routes/reviews.ts:18 uses the precise (event: UiEvent) => void. That looseness is exactly why an unflagged agent.upsert type-checked, but tightening it touches 9 modules and belongs in its own PR. Backlogged.

What the included set has in common

Exactly one line changed at a publish site: the payload's agent value. The other 30 sites are untouched; the deps type gains one field with the same signature already used by routes/agents/shared.ts:32, routes/release.ts:247, server/mcp-handlers.ts:101, server/mcp-review-handlers.ts:97 and server/agent-lifecycle-runtime.ts:15.

Test

New apps/server/test/template-launch-publish.test.ts — bare Fastify + registerTemplateRoutes with a stubbed templateService and a spy publishUiEvent (the quick-phrases-routes.test.ts shape; the existing template-routes.test.ts boots the full app and can't observe published events). Confirmed non-vacuous: reverting the one-line fix fails the first test and leaves the second passing.

Checks

pnpm run check clean · pnpm run test 2986 passed / 9 skipped, plus 1490 and 60 in the other two suites · pnpm run test:e2e 186 passed / 12 skipped. Server-only change, so finalize:web and web vitest do not apply.

Next run

Backlog entry for AgentRecord (server) vs Agent (web) divergence — the last big server↔web wire-type gap. This PR unblocks it: hasStream is now attached at every agent.upsert publish site, so a shared agent wire type no longer has to keep it optional on that account.

🤖 Generated with Claude Code

The template launch route was the only one of ~31 `agent.upsert` publish
sites that sent the raw AgentRecord instead of wrapping it in
`withStreamFlag`, so its payload omitted `hasStream` entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@selfcontained
selfcontained merged commit ad83708 into main Sep 2, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_c29292e62569/job-debt-collector-cf0194d2 branch September 2, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant