Skip to content

Fix Electric Agents quickstart runner registration#4610

Merged
samwillis merged 4 commits into
mainfrom
fix-agents-quickstart-runner-principal
Jun 18, 2026
Merged

Fix Electric Agents quickstart runner registration#4610
samwillis merged 4 commits into
mainfrom
fix-agents-quickstart-runner-principal

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Executive Summary

Fixes the Electric Agents quickstart path so the printed CLI commands can spawn, route, send to, and observe Horton entities without extra principal or dispatch-policy setup. It also prevents newly-started agent runs from being left in started when a wake handler errors before ending them. The user-visible result is that electric agents quickstart creates a usable local runner-backed environment, electric agents observe uses the same shared timeline live query as the other UIs, interactive terminal sends remain visible while they reconcile with the entity stream, and runtime failures no longer leave the chat UI stuck on "Thinking" for that failure path.

Root Cause

There were several related quickstart gaps:

  • The built-in runner registered with one principal while the CLI used another, so entity type management, runner ownership checks, and default user permissions did not line up.
  • Built-in Horton/Worker type registration intentionally omitted a default dispatch policy, so electric agents spawn /horton/onboarding could create an entity that no pull-wake runner would claim.
  • The CLI-launched built-in runtime installed the optional global Undici fetch cache; in local quickstart this could surface as opaque fetch failures such as invalid onError method during type registration.
  • electric agents observe and the shared UI timeline query were resolving different @tanstack/db package instances. TanStack validates live-query sources with package-local CollectionImpl identity, so valid StreamDB collections were rejected as non-collections.
  • The shared live timeline query concatenated streamed text deltas by row key rather than stream sequence. Once a response reached msg-0:10, lexicographic ordering placed that chunk before msg-0:2, which only surfaced in consumers reading the live query path.
  • Live run item ordering mixed text row creation order with tool-call timeline order. A text row can be created before its first delta arrives, so text could render before an earlier tool call.
  • Interactive observe sends posted successfully but did not await the returned stream txid or send a stable client key, so the optimistic inbox row could disappear before the real stream event reconciled.
  • The CLI entity stream helper constructed a bare StreamDB instead of using the agents-runtime createEntityStreamDB wrapper used by the web UI. That skipped runtime row-offset materialization (_timeline_order), so the shared timeline query fell back to source tie-breakers and grouped inbox rows ahead of run rows.
  • Published CLI builds floated electricax/agents-server:latest, so a released CLI could start a server image with mismatched runtime contracts.
  • A wake handler could create a run row with status: started and then throw before the run was ended. The existing handler failure path wrote an error event, but did not terminally update that run, so the UI continued to treat it as the latest streaming response.

Approach

  • Make quickstart resolve one runner owner principal and force that same principal into the built-in runtime's server headers.
  • Default the CLI principal to user:<ELECTRIC_AGENTS_IDENTITY> so the no-env quickstart commands match the built-in principal_kind=user grants.
  • Register built-in Horton/Worker types with the local pull-wake runner as their default dispatch target.
  • Disable the optional global Durable Streams fetch cache only for the CLI-managed built-in runtime.
  • Align electric-ax with the same TypeScript/TanStack DB peer set as agents-runtime, so both packages resolve the same @tanstack/db instance.
  • Restore CLI observe to createEntityTimelineQuery(db) plus useLiveQuery, then adapt the shared query rows into the existing Ink terminal sections.
  • Order text/reasoning deltas by _seq in the shared live query, matching the materialized timeline view.
  • Order live run child items in one sequence domain: tool calls by _seq, text items by first text-delta _seq with text-row _seq as fallback.
  • Keep interactive observe sends optimistic until the returned txid is observed, using a stable client key and pending timeline order so the optimistic row and synced inbox row reconcile.
  • Make the CLI entity stream helper delegate to the agents-runtime StreamDB helper, matching the web UI stream setup and preserving _timeline_order for shared timeline row ordering.
  • Add shared timeline indexes for joined collections so the live query path is warning-free in both CLI and UI consumers.
  • Inject the matching @electric-ax/agents-server package version into the CLI Docker image tag at build time, and make ci:publish rebuild packages after Changesets updates versions.
  • Add endpoint/cause context around entity type and runner registration fetch failures.
  • Track run status events produced during a wake and, on handler failure, mark the latest new still-started run as failed before writing the error event. The failure path only selects DB-visible runs whose status is still started, with a produced-event fallback for runs that were emitted but not yet visible through the local collection.
  • Clarify browser-only credentials settings copy so it points at the connected runtime, not editable browser-side provider keys.

Key Invariants

  • The runner registration principal, runner owner, runner claim headers, and CLI default principal must agree for the local quickstart path.
  • Built-in agent types must have a runner default dispatch policy when they are registered by a pull-wake built-in runtime.
  • ELECTRIC_AGENTS_PRINCIPAL remains the explicit override for non-default deployments.
  • electric-ax, agents-runtime, and UI consumers must resolve a single TanStack DB module instance for shared live-query collection identity checks.
  • CLI and desktop/web timeline consumers should share createEntityTimelineQuery(db); only the rendering adapter should differ.
  • Live streamed deltas must be ordered by stream sequence, not by human-readable event keys.
  • Live run child items must be ordered by model event sequence, not by when a text container row was created.
  • Terminal optimistic sends must use a stable key and await stream reconciliation before dropping optimistic state.
  • CLI and web UI StreamDB setup must share the runtime helper so row offsets and _timeline_order are materialized consistently.
  • Released electric-ax builds should start an agents-server image matching the release version unless explicitly overridden.
  • Handler failure cleanup must only fail runs that are still started; a completed run must not be regressed to failed if later cleanup throws.

Non-goals

  • This does not make provider keys editable from the browser UI.
  • This does not alter runner ownership authorization rules on the server; quickstart now supplies matching principals instead.
  • This does not mask model provider failures.
  • This does not make the terminal observer render every rich desktop-only timeline row. It maps the shared query output into the existing terminal sections.
  • This does not implement abandoned-run recovery for hard process death, laptop sleep, or restart cases where no in-process catch/finally can run. That second layer is tracked in Recover abandoned started agent runs after runtime shutdown #4633.

Trade-offs

The CLI observer now shares the same timeline query as the desktop/web UIs, which keeps query semantics in one place and prevents the terminal observer from drifting. The terminal still has a small adapter because its Ink renderer expects simpler records than the desktop timeline component.

The timeline query now creates a few BasicIndex indexes before building joins. That is a small setup cost per StreamDB instance, but it avoids repeated TanStack warnings and keeps the shared query suitable for CLI and UI use.

The Docker image pin is injected at build time rather than hard-coded because Changesets owns release version updates. Rebuilding during ci:publish ensures the generated artifacts see the bumped package versions.

Verification

pnpm --filter @electric-ax/agents-runtime build
pnpm --filter electric-ax build
pnpm --filter @electric-ax/agents-runtime exec vitest run test/entity-timeline.test.ts
pnpm --filter electric-ax test -- entity-stream-db.test.ts observe-ui.test.tsx
pnpm --filter electric-ax test -- observe-ui.test.tsx cli.test.ts start.test.ts
pnpm --filter @electric-ax/agents-server-ui typecheck
pnpm --filter @electric-ax/agents exec vitest run test/builtin-pull-wake-registration.test.ts
pnpm --filter @electric-ax/agents-runtime exec vitest run test/process-wake.test.ts --reporter=dot

Manual isolated quickstart verification:

cd /tmp
ELECTRIC_AGENTS_COMPOSE_PROJECT=electric-agents-verify \
ELECTRIC_AGENTS_PORT=4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents quickstart

In a second terminal:

ELECTRIC_AGENTS_URL=http://localhost:4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents types

ELECTRIC_AGENTS_URL=http://localhost:4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents spawn /horton/onboarding

ELECTRIC_AGENTS_URL=http://localhost:4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents inspect /horton/onboarding

ELECTRIC_AGENTS_URL=http://localhost:4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents send /horton/onboarding "Onboard me to Electric Agents"

ELECTRIC_AGENTS_URL=http://localhost:4448 \
node /Users/kylemathews/programs/electric/packages/electric-ax/dist/index.js agents observe /horton/onboarding

Observed results:

  • Horton and Worker type registration succeeded.
  • Pull-wake runner builtin-agents started.
  • agents types listed built-ins without a manual principal override.
  • agents spawn /horton/onboarding succeeded.
  • agents inspect /horton/onboarding showed dispatch_policy.targets[0].runnerId = builtin-agents.
  • agents send /horton/onboarding "Onboard me to Electric Agents" was picked up by the runner.
  • agents observe /horton/onboarding rendered the timeline through the shared live query instead of throwing Invalid source for live query, with no TanStack join-index warning.
  • The terminal observer rendered the full streamed Horton response after _seq ordering replaced key ordering for live deltas.
  • Tool calls now remain before later text when the text row was created before its first delta.

Files Changed

  • .changeset/quickstart-runner-principal.md: patch release note for affected packages.
  • .changeset/fail-started-agent-runs.md: patch release note for the started-run handler failure fix.
  • package.json: rebuild packages before publish/tag so injected version constants are current after Changesets versioning.
  • packages/agents-runtime/src/create-handler.ts: include registration endpoint and fetch cause in entity type registration failures.
  • packages/agents-runtime/src/process-wake.ts, packages/agents-runtime/test/process-wake.test.ts: fail newly-started runs when handler errors escape before run completion, without regressing already-completed runs.
  • packages/agents-runtime/src/entity-timeline.ts: create shared timeline join indexes, keep CLI/UI consumers on the same live query, and order live deltas by stream sequence.
  • packages/agents-runtime/test/create-handler.test.ts, packages/agents-runtime/test/entity-timeline.test.ts: regression coverage for fetch detail, live delta ordering, top-level live timeline row interleaving, and live run child item ordering.
  • packages/agents/src/server.ts, packages/agents/test/builtin-pull-wake-registration.test.ts: default built-in type dispatch to the local runner and cover it.
  • packages/electric-ax/package.json, pnpm-lock.yaml: align electric-ax with the same TanStack DB/TypeScript peer instance as agents-runtime.
  • packages/electric-ax/src/start.ts, packages/electric-ax/src/index.ts: align quickstart runner owner/server headers and default CLI principal behavior.
  • packages/electric-ax/src/entity-stream-db.ts, packages/electric-ax/test/entity-stream-db.test.ts: delegate CLI stream setup to the agents-runtime helper so timeline ordering matches the web UI.
  • packages/electric-ax/src/observe-ui.tsx, packages/electric-ax/test/observe-ui.test.tsx: render observe from the shared live query, fix terminal wrapping, send interactive messages with stable optimistic keys, and await send txids.
  • packages/electric-ax/src/version.ts, packages/electric-ax/tsdown*.config.ts: inject the matching agents-server Docker image tag into release builds.
  • packages/electric-ax/test/cli.test.ts, packages/electric-ax/test/start.test.ts: coverage for principal/default header behavior.
  • packages/agents-server-ui/src/components/settings/pages/CredentialsPage.tsx: clarify browser-only provider credentials copy.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Desktop Builds

Build artifacts for commit a55f149.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.65587% with 112 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.38%. Comparing base (5c60589) to head (a55f149).
⚠️ Report is 13 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/electric-ax/src/observe-ui.tsx 28.57% 99 Missing and 1 partial ⚠️
packages/agents/src/server.ts 36.36% 7 Missing ⚠️
packages/agents-runtime/src/create-handler.ts 91.66% 2 Missing ⚠️
.../src/components/settings/pages/CredentialsPage.tsx 0.00% 1 Missing ⚠️
packages/electric-ax/src/start.ts 92.85% 1 Missing ⚠️
packages/electric-ax/src/version.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4610      +/-   ##
==========================================
+ Coverage   59.00%   59.38%   +0.38%     
==========================================
  Files         385      385              
  Lines       42642    42928     +286     
  Branches    12205    12355     +150     
==========================================
+ Hits        25160    25493     +333     
+ Misses      17407    17359      -48     
- Partials       75       76       +1     
Flag Coverage Δ
packages/agents 72.64% <36.36%> (-0.15%) ⬇️
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.40% <97.18%> (+0.74%) ⬆️
packages/agents-server 75.50% <ø> (+0.21%) ⬆️
packages/agents-server-ui 7.51% <0.00%> (-0.04%) ⬇️
packages/electric-ax 51.06% <37.80%> (+3.44%) ⬆️
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.83% <ø> (ø)
packages/y-electric 56.05% <ø> (ø)
typescript 59.38% <54.65%> (+0.38%) ⬆️
unit-tests 59.38% <54.65%> (+0.38%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Mobile Build

Local mobile checks ran for commit a55f149.

The EAS Android preview build was skipped because the mobile-eas-build label is not present.
Add the mobile-eas-build label to this PR to produce an installable preview build.

Workflow run

@KyleAMathews KyleAMathews force-pushed the fix-agents-quickstart-runner-principal branch 11 times, most recently from 5d35ef3 to f3b6804 Compare June 17, 2026 15:46
@KyleAMathews KyleAMathews force-pushed the fix-agents-quickstart-runner-principal branch from f3b6804 to f13d3b3 Compare June 17, 2026 19:36

@samwillis samwillis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Interactive review with GPT-5.5.

I reviewed PR #4610 in an isolated worktree against origin/main, focusing on the quickstart runner registration changes, principal/header handling, the electric-ax observe live timeline rewrite, and the related tests.

Finding

  • P2: electric-ax observe now reads entityStopped via db.collections.entityStopped.toArray.length outside a live query. If the stop event is the only new data, React/Ink may not rerender, so the observe UI can keep accepting input and fail to show the stream-closed state.

Validation

  • PR CI is green, including Test packages/electric-ax, Test packages/agents-runtime, and Test packages/agents.
  • I attempted targeted local electric-ax tests in the review worktree, but the local shell is on Node v18.20.5 while the repo requires >=20.19.0 || >=22.12.0.

Comment thread packages/electric-ax/src/observe-ui.tsx Outdated
)

const closed = (stopped as Array<EntityStopped>).length > 0
const closed = db.collections.entityStopped.toArray.length > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably stay subscribed like the previous implementation. Since closed is derived from a direct toArray read, a stop event by itself will not trigger a React/Ink rerender, so observe can keep showing the input and miss the "Stream closed" state until some other live timeline row changes. A small useLiveQuery((q) => q.from({ entityStopped: db.collections.entityStopped })) here would preserve the old behavior.

samwillis and others added 3 commits June 18, 2026 12:35
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@samwillis samwillis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The observe stopped-state regression is covered, electric-ax checks are green, and the remaining Codecov uploader issue cleared on rerun.

@samwillis samwillis merged commit ee0da19 into main Jun 18, 2026
101 of 102 checks passed
@samwillis samwillis deleted the fix-agents-quickstart-runner-principal branch June 18, 2026 16:16
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.

2 participants