Skip to content

feat(agent): add durable agent invocations and spawning - #1065

Merged
dcramer merged 27 commits into
mainfrom
codex/durable-agent-invocations
Aug 7, 2026
Merged

feat(agent): add durable agent invocations and spawning#1065
dcramer merged 27 commits into
mainfrom
codex/durable-agent-invocations

Conversation

@dcramer

@dcramer dcramer commented Jul 27, 2026

Copy link
Copy Markdown
Member

Adds durable agent invocations and a spawnAgent tool so a parent agent can schedule named or one-off child work through Junior's existing conversation mailbox and lease worker.

Named and unnamed children are the same work shape. A name only keeps the same child conversation id so later inputs continue that child's history. Unnamed invocations get an invocation-scoped child. Different named agents can run concurrently, repeated tool calls are idempotent, overlapping work for one name is rejected, and one child's failure does not affect its siblings.

Child execution policy

Child runs pin optional capabilities with one list:

disabledFeatures: ["handoff", "interactive-auth", "subagents"]

That replaces one-off disable flags / authorizationFlowMode. interactive-auth is the old interactive-vs-disabled auth pause switch: omit it to allow OAuth pause links, include it to hard-fail instead. TODO: children may later need a way to force interactive auth when a delegated tool requires credentials. Optional reasoning_level is per-invocation only; bindings do not store sticky reasoning or model profile.

The model supplies only the task, optional name, and optional reasoning level. The runtime keeps parent identity, actor, credentials, routing, and idempotency bound to the active run, and the tool returns only a durable invocation handle. Child creation shares the root conversation mutation lock with retention purge, and bindings and invocations are deleted with the conversation tree.

Child work uses the existing provider-neutral mailbox worker. Local chat processes child wakes in-process, while production continues to use Vercel Queues. The schema change is migration 0021_first_warhawk after main's 0020_restrict_task_execution_kinds.

Unrecoverable stranded running children (missing model or no resumable boundary) fail the session immediately and project onto the invocation, so empty resume wakes cannot requeue forever or permanently occupy a named agent.

SQL-backed regression coverage exercises isolated and named agents, history reuse with per-task reasoning, concurrent replay, overlap rejection, sibling failure isolation, validation failure, recovery (including unrecoverable stranded sessions), and retention cleanup. This slice returns invocation handles; parent result delivery, deterministic result recovery, cancellation, and recursive children remain follow-up work on #880.

Fixes #879
Refs #880

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
junior-docs Ready Ready Preview Aug 7, 2026 4:34pm

Request Review

@dcramer dcramer changed the title feat(agent): add durable agent invocations feat(agent): add durable agent invocations and spawning Jul 27, 2026
Comment thread packages/junior/src/chat/agent-invocations/store.ts
Comment thread packages/junior/src/chat/conversations/sql/purge.ts
Comment thread packages/junior/src/chat/agent-invocations/work.ts Outdated
Comment thread packages/junior/src/cli/chat.ts
Comment thread packages/junior/src/chat/agent-invocations/work.ts
@sentry-junior

sentry-junior Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review exercise result: the current recovery path still has one merge blocker. An empty resume wake sets isFinalAttempt false, while recoverRunningSession can throw for a running session with no model or no resumable boundary. That error is rethrown, the worker schedules another empty wake, and the invocation remains running indefinitely—permanently occupying a named agent. Please make unrecoverable stranded-session failures terminal (or give recovery wakes a bounded final-attempt path) and cover the missing-model/no-boundary case with an integration test. This confirms the existing finding in discussion_r3685080709.

sentry-junior Bot and others added 2 commits August 5, 2026 19:48
Empty resume wakes never become final delivery attempts, so stranded
running children with no model or resumable boundary must fail their
session immediately and project onto the invocation instead of throwing
and requeueing forever.

Co-Authored-By: David Cramer <david@sentry.io>
Resolve conflicts with current main while keeping spawn/agent-invocation
wiring, provider conversation bindings, scheduled-task heartbeats, and
destination-aware slack work. Renumber the agent invocation schema
migration to 0020_sour_vargas after main's newer conversation history
and reporting migrations.

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-junior

sentry-junior Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Addressed the stranded-invocation merge blocker and brought the branch current with main.

Fix

  • Unrecoverable stranded running child sessions (missing model / no resumable boundary) now fail the session immediately via failAgentTurnSessionRecord, then project onto the invocation through the normal terminal path.
  • Empty resume wakes never become final delivery attempts, so throwing + requeue is no longer used for this case.
  • Integration coverage: empty-resume wake with isFinalAttempt: false terminalizes the invocation and frees the named agent binding.

Main integration

  • Merge commit (rebase blocked by original human commit authorship under the Junior git identity guard).
  • Agent-invocation schema migration renumbered to 0020_sour_vargas after main's newer conversation-history/reporting migrations.
  • Kept spawn/agent-invocation wiring alongside scheduled-task heartbeats, provider conversation bindings, and destination-aware slack work.

Local integration verification could not run here (no Docker/Postgres). Relying on CI for agent-invocation-work and the rest of the suite.

Main renamed juniorToolResultSchema to juniorToolOutputSchema and dropped
ok/status envelope fields. Update spawnAgent and its unit expectations,
and pass the required Slack destination after requireSlackDestination.

Co-Authored-By: David Cramer <david@sentry.io>
authorizationFlowMode was only interactive vs disabled. Model it as
disabledFeatures: ["interactive-auth"] with the same default-on behavior,
and leave a TODO that child runs may later need a way to force auth.

Co-Authored-By: David Cramer <david@sentry.io>
Link the child hard-fail auth TODO to master plan #883 and operational
controls #881 so the gap is visible outside the PR thread.

Co-Authored-By: David Cramer <david@sentry.io>

@sentry-junior sentry-junior Bot 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.

Second pass after the design lock. This now matches the intended model:

  • named and unnamed are the same work shape; a name only keeps the child conversation id / history
  • bindings are history handles only (no sticky reasoning/model policy)
  • child runs pin disabledFeatures: ["handoff", "interactive-auth", "subagents"] and cannot hand off or recursively spawn
  • stranded unrecoverable children terminalize instead of requeueing forever
  • migration/purge/idempotency/busy locking look coherent, and the integration coverage hits the important paths

Known follow-ups are already tracked outside this slice (getAgentResult / parent delivery on #880, forced child interactive-auth on #881/#883). One residual local gap: CLI wires spawn + the in-process worker but not the production mailbox-append heartbeat repair, so a crash between create and append can leave a named agent busy across local runs. Not a production blocker; worth a small follow-up if local spawn durability matters.

Looks good to ship for the #879 cut.

Keep the model-facing subagent tool off by default so the durable
invocation runtime can ship without exposing incomplete spawn UX.
Enable with JUNIOR_SUBAGENTS_ENABLED=true when ready to roll out.

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-junior

sentry-junior Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Gate the model-facing spawnAgent tool behind bot config so the incomplete subagent surface stays off by default.

Change

  • botConfig.subagentsEnabled defaults to false
  • enable with JUNIOR_SUBAGENTS_ENABLED=true
  • createAgentRunner only binds spawnAgent when bot config allows it and run policy does not disable subagents
  • child runs still pin disabledFeatures: ["handoff", "interactive-auth", "subagents"]

Local tsc --noEmit clean. Vitest still needs Postgres/Docker here.

Production keeps spawnAgent off via botConfig. Opt the junior and
junior-evals test runners in so agent-invocation coverage exercises
the real wiring path without per-test env setup.

Co-Authored-By: David Cramer <david@sentry.io>
Replace the one-off JUNIOR_SUBAGENTS_ENABLED flag with a Next.js-style
createApp({ experimental }) surface so unstable features share one
rollout path. Keep JUNIOR_EXPERIMENTAL as a comma-separated env fallback
for CLI/tests, and gate spawnAgent on experimental.subagents.

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-junior

sentry-junior Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Replaced the one-off subagents flag with a standard experimental opt-in bag.

Pattern (Next.js-style):

await createApp({
  experimental: {
    subagents: true,
  },
});

Details

  • JuniorAppOptions.experimental is the primary config surface
  • known features live in EXPERIMENTAL_FEATURES (subagents today)
  • unknown keys / env names fail closed
  • JUNIOR_EXPERIMENTAL=subagents is the CLI/env fallback; createApp({ experimental }) wins when set
  • spawnAgent gates on isExperimentalFeatureEnabled("subagents")
  • removed botConfig.subagentsEnabled / JUNIOR_SUBAGENTS_ENABLED
  • test suites set JUNIOR_EXPERIMENTAL=subagents

Local tsc --noEmit clean.

Comment thread packages/junior/tests/unit/experimental.test.ts
Drop JUNIOR_EXPERIMENTAL. Opt-in is only createApp({ experimental }).
Test suites enable subagents through the same flag via setupFiles.

Co-Authored-By: David Cramer <david@sentry.io>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f3c9278. Configure here.

Comment thread packages/junior/src/chat/experimental.ts
Local chat wires spawn + child workers outside createApp. Opt into
experimental.subagents there so the model-facing tool matches that path.
sentry-junior Bot and others added 2 commits August 7, 2026 16:22
Bring the branch current with main before rechecking the agent-invocation
migration chain.

Co-Authored-By: David Cramer <david@sentry.io>
Main added this unit fixture without the PR's ConversationStore.createChild
method. Keep the mock complete after merging main.

Co-Authored-By: David Cramer <david@sentry.io>
@sentry-junior

sentry-junior Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Brought the branch current with main.

Note
True history rewrite/rebase still can't preserve original human commit authors under the Junior git identity guard, so this landed as a merge rather than a force-pushed rebase.

Follow-up

  • Migration chain remains 0020_restrict_task_execution_kinds0021_first_warhawk (main added no newer schema migrations).
  • Fixed main's new turn-session-routing store mock to include PR-added createChild.
  • Local tsc --noEmit clean.

Comment thread packages/junior/src/chat/agent-invocations/spawn.ts
Reject new spawnAgent work once a parent already has 8 non-terminal
children in flight so prompt-driven fan-out cannot enqueue unbounded
paid child turns. Named busy-locking is unchanged; idempotent replay
still bypasses the count.
@dcramer
dcramer merged commit a19cfb8 into main Aug 7, 2026
32 checks passed
@dcramer
dcramer deleted the codex/durable-agent-invocations branch August 7, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add durable agent bindings and child execution

1 participant