Skip to content

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

Open
dcramer wants to merge 16 commits into
mainfrom
codex/durable-agent-invocations
Open

feat(agent): add durable agent invocations and spawning#1065
dcramer wants to merge 16 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 keep fixed policy for the invocation: no recursive spawn, no model handoff, and no adaptive profile override. Optional reasoning_level is per-invocation only; bindings do not store sticky reasoning or model profile. Model selection stays on the shared runner/session path under that fixed policy.

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 6, 2026 3:13am

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>
Main's local Source requires visibility and dropped type. Align agent
runner and retention fixtures, and expect one more core migration after
0020_sour_vargas.
sentry-junior Bot and others added 2 commits August 6, 2026 01:14
Bring the branch current with main so the agent-invocation schema can be
regenerated after 0020_restrict_task_execution_kinds.

Co-Authored-By: David Cramer <david@sentry.io>
Main claimed 0020_restrict_task_execution_kinds. Drop the stale
0020_sour_vargas agent migration and regenerate as 0021_living_groot
with a snapshot chain rooted on main's latest migration.

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

sentry-junior Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Refreshed the branch against current main and regenerated the agent-invocation migration.

Migration

  • Main now owns 0020_restrict_task_execution_kinds.
  • Removed the stale conflicting 0020_sour_vargas agent migration.
  • Regenerated via pnpm --filter @sentry/junior db:generate as 0021_living_groot.
  • Verified 0021 snapshot prevId matches main's 0020 snapshot id.
  • Re-ran generate: no further schema changes.

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 + clean regenerate rather than a force-pushed rebase. The migration chain is now current with main.

CI should re-run on this push.

@dcramer
dcramer marked this pull request as ready for review August 6, 2026 01:39
@github-actions github-actions Bot added the risk: high PR risk score: high label Aug 6, 2026

@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.

Design review of the subagent model, not just the machinery.

The durable spine is in good shape: named binding → child conversation + history, invocation → one retry-safe task + authority snapshot + terminal result, mailbox/lease/session reuse, stranded recovery, retention. That part matches Junior's conversation runtime well.

The gap is the product identity of a subagent. Right now a "named agent" is really:

  1. a stable child conversation id
  2. optional sticky reasoningLevel
  3. busy-locking across invocations

That is thinner than both the Codex-shaped mental model and #880 (spawnAgent for named/ephemeral agents with optional model/reasoning profiles, plus result subscription / getAgentResult).

1. Agent policy is half-defined

spawnAgent accepts task + optional name + optional reasoning_level. Bindings persist only reasoning. Model profile is chosen later by the shared runner (standard → route/handoff) and lives on conversation projection / turn route / session — never on binding or invocation.

So at run time a child always has profile+reasoning, but only reasoning is agent policy. If named agents are persistent child agents, sticky policy should be coherent: either binding owns full execution policy (modelProfile + reasoningLevel), or binding is only a history handle and reasoning should not be special-cased there either. The current split makes "what is this agent?" hard to answer.

Related inconsistency: children still get handoff. A child can change model mid-run, and profile handoff can override reasoning via profile_reasoning_override, while the binding continues to claim a sticky reasoning policy. Pick one: pin child execution policy, or treat profile/reasoning as fully runtime-owned.

2. Model-facing spawn is incomplete as a subagent API

This slice returns only { invocation_id }. There is no parent mailbox injection, no auto-subscribe on terminal result, and no getAgentResult. The invocation stores a terminal result, but nothing closes the loop to the parent turn.

That means the exposed tool teaches the parent a fire-and-forget contract. In a Codex-like subagent flow, spawn without a result path is not yet a usable subagent — it's durable background work with an opaque handle. Either:

  • keep this PR as #879 runtime-only and don't expose spawnAgent to the model yet, or
  • land the minimal result path (getAgentResult and/or parent terminal notification) before calling the model surface done.

Shipping spawn alone will create parent agents that "successfully" delegate and then never observe the work.

3. Named agent continuity is transcript-only

Reusing a name reuses history. It does not establish:

  • a model profile
  • agent instructions / role
  • tool policy beyond "no recursive spawn"
  • workspace/artifact isolation beyond whatever the shared child state happens to accumulate
  • a stable authority identity (actor/credentials/destination are snapshotted per invocation, not per binding)

That may be fine for v1, but then docs/tool copy should say named persistent child conversation, not imply a full agent config object. Codex-style "researcher/implementer with its own model" is not what this implements yet.

4. Child capability surface is "almost parent"

Child runs through the same AgentRunner with:

  • agentSpawning: "disabled"
  • authorizationFlowMode: "disabled"
  • destinationless conversation ✅
  • otherwise normal tools, including handoff

If subagents are constrained workers, this is too open. If they are full peer agents with continuity, missing policy/result wiring is the problem. Either framing is defensible; the code currently sits between them.

5. Smaller concrete follow-through

  • Local CLI wires spawn + in-process worker but not the mailbox-append heartbeat repair path production uses. A crash between create and append can leave a named agent busy with no local repair.
  • Terminology/docs should explicitly define: binding = named child identity (+ policy if we keep it), invocation = one task attempt, session = resumable execution including resolved model. Right now README says binding owns "reasoning policy" while #880 talks about model/reasoning profiles — align those before merge so the next slice doesn't invent a third shape.

What I'd lock before merge

  1. Decide the noun: is a named agent (a) history handle, or (b) sticky execution policy + history? Implement policy fields accordingly (at least stop the reasoning-only special case unless we commit to it).
  2. Decide the model surface for this PR: runtime-only proof, or spawn+result. Avoid shipping spawn without any parent-visible completion path.
  3. Decide child autonomy: keep/remove handoff on children; if sticky reasoning stays, handoff must not silently override binding policy.

The execution/recovery work is real and mostly right. I would not merge the model-facing half until the subagent identity and result contract are intentional rather than residual from the slice cut.

Named and unnamed children are the same work shape; a name only keeps the
child conversation id so later inputs continue history. Drop sticky
reasoning from bindings, keep per-invocation reasoning, and pin child runs
so they cannot hand off or otherwise override execution policy.

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

sentry-junior Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Addressed the design decisions from review:

Named vs unnamed

  • Same work shape.
  • A name only keeps the same child conversation id so later inputs continue history.
  • Bindings no longer store sticky reasoningLevel.

Child policy is fixed

  • modelHandoff: "disabled" on child runs.
  • No handoff tool, no adaptive profile routing, no resume-after-handoff override path.
  • Optional reasoning_level remains per-invocation only.

Schema

  • Regenerated unshipped migration as 0021_first_warhawk (bindings: parent, name, child only).

Local tsc --noEmit clean. Integration/unit suites need Postgres/Docker, which is unavailable here; relying on CI.

@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 bb5365c. Configure here.

Comment thread packages/junior/tests/integration/agent-invocation-work.test.ts
The new assertion was creating another named researcher invocation while
the previous one was still non-terminal, so it hit busy instead of
validating per-task reasoning on the reused child.
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