Spawn, keep working, pick up the answer later.
A parent agent in loomcycle used to have two ways to spawn a sub-agent. It could spawn and wait (block until the child returned the answer). Or it could spawn and detach forever (fire the child, never see the answer, never know what happened). Both are useful. Neither is what you want when the parent has three things to do in parallel.
Imagine an orchestrator that has to dispatch five code-review tasks and then produce a summary. If it blocks on each child, the five reviews run sequentially even though they could run in parallel. If it detaches them, it has no way to assemble the summary. The parent is forced to choose between throughput and visibility.
That is the gap this release closes.
The short version. Agent op=spawn and Agent op=parallel_spawn now take a mode: "poll". In poll mode they return immediately with {child_run_id, state: "running"}; the parent collects results later with Agent op=poll. Children survive pause, restart and replica move. Three new knobs landed alongside: idempotency_key (retry a spawn safely; the second call joins the first run's stream and is marked deduplicated), max_wall_seconds (per-run lifetime limit, applies to detached runs too), and result: "object" (structured child outcome: {agent_id, run_id, status, final_text, structured, state, usage} instead of text behind a header line). And scheduled runs stopped being capped at 10 minutes.
The three modes
flowchart LR
subgraph WAIT["spawn + wait (default)"]
P1["Parent"] -->|spawn| C1["Child runs"]
C1 -->|answer returned| P1
end
subgraph DETACH["spawn + detach"]
P2["Parent"] -->|spawn, mode: detach| C2["Child runs independently"]
P2 -->|keeps going| X1["parent never learns the result"]
end
subgraph POLL["spawn + poll (new)"]
P3["Parent"] -->|spawn, mode: poll| C3["Child runs"]
P3 -->|other work| W["parent does something else"]
P3 -->|op=poll later| R["child's answer"]
end
style WAIT fill:#fff5f5
style DETACH fill:#fff8e6
style POLL fill:#f0fff4
style R fill:#e8ffef,stroke:#4a9e60
Here is poll in use. An orchestrator dispatches five code reviews, continues to prepare the summary scaffold, then collects the reviews when the scaffold is ready.
// dispatch (the call returns immediately)
const reviews = await Agent.parallel_spawn({
mode: "poll",
spawns: files.map(f => ({ agent: "code-review", prompt: `review ${f}` })),
})
// do something else while the reviews run
const summary = await prepareSummaryScaffold()
// collect
const results = await Agent.poll({
child_run_ids: reviews.map(r => r.child_run_id),
timeout_ms: 60_000, // wait up to 60s for stragglers
})
Durability is what makes poll mode usable in practice. A parent that spawns five children in poll mode and then crashes is in a bad spot if the children stop with it. In loomcycle they do not. Children survive pause (the parent sits at awaiting_children until polled or until wall-clock expires), restart (the children keep running and the parent picks them up when it comes back), and replica move (a parent that resumes on a different replica can still poll children that started elsewhere).
The runtime caps active children per run at LOOMCYCLE_MAX_LIVE_CHILDREN_PER_RUN=32 by default, to keep a runaway parent from amplifying load. A spawn above the cap errors the caller rather than silently queuing.
Idempotency keys
A caller that lost network between its spawn call and the response has no safe retry today. The retry might create a second run. The second run burns tokens, writes memory, and produces a duplicate side effect. The caller had asked for one.
An idempotency_key on POST /v1/runs and on Agent op=spawn fixes that. The runtime stores the key against the run id on first use. A later call carrying the same key does not create a second run. Instead it joins the first run's stream, and the server marks the response deduplicated: true so the caller can tell the difference between "started a new run" and "re-joined one in flight."
Keys scope to the caller's identity (tenant, user). Two tenants using the same key create two independent runs; they cannot collide. The server stores keys for 24 hours by default (configurable per run), after which a repeat key starts a fresh run as if it were new.
Wall-clock lifetime limits
A detached run that enters a loop is not visible to the caller, by definition. Before this release a stuck detached run just kept burning tokens until an operator noticed. max_wall_seconds on a run (per call, with a server-side ceiling) gives it a lifetime. When the clock expires, the runtime ends the run with status timed_out and stops billing. The limit applies to detached and poll-mode runs just as it applies to wait-mode runs.
Structured child results
A sub-agent's result used to arrive at the parent as text with a header line: [child agent-id run-id status]\n\n. The parent agent had to parse the header to know which child the result came from, and had no access to the structured fields the child produced.
result: "object" on a spawn returns the child's outcome as structured data: {agent_id, run_id, status, final_text, structured, state, usage}. The structured field is whatever the child's output_format parsed into. The state field is the child's final Σ for a stateful child. The usage field is tokens and cost. A parent that was fragile against header-line formatting is no longer fragile; it reads a field.
Scheduled runs that outlive 10 minutes
Scheduled runs used to be capped at 10 minutes of wall clock. That is fine for a status poll or a sync job. It is not fine for a nightly consolidation that reads a quarter million rows.
The cap is removed. Every replica can run the scheduler now, each slot claimed in the database so two replicas do not fire the same schedule twice. The concurrency_policy field on a schedule (forbid | allow | replace) and the catch_up_max field (how many missed firings to catch up on after a downtime window) are honoured for real. A nightly job that takes an hour runs for an hour; a per-minute poll stays at a minute.
What this bought
A concrete orchestration pattern that used to need three workarounds now needs none. An agent that fans out N sub-agents can poll for whichever ones finish first, bank the fast ones' results while the slow ones keep running, and get a lifetime cap on the whole operation without having to wait on the slowest child.
A caller using HTTP from a flaky network can retry a run start safely. A test harness that re-runs a scenario does not accumulate orphaned runs.
A code-js orchestrator that reads child results no longer has to parse a text header. It reads fields.
What this does not fix
Three limits worth naming.
A run that hits max_iterations with pending tools makes one more billed model call before ending. This is a closing-turn behaviour. The runtime has to flush pending tool calls to a terminal state, and that costs one call. If an operator is watching usage with tight thresholds, the terminal call is where "ran out of iterations" shows up on the bill. Not a surprise; worth knowing.
Hit the LOOMCYCLE_MAX_LIVE_CHILDREN_PER_RUN=32 cap and your spawn fails with the answer kept. Earlier releases reported it as completed and silently discarded the overflow spawn. Now the caller gets an error, and the already-dispatched children keep running. Preferable, but a code path that assumed "completed means dispatched" needs updating.
A child's reply is cut to a quarter of the parent's context window. Not configurable yet. A child that produces a very long reply gets truncated at the parent-visible boundary. The child's own transcript keeps the full reply; the parent sees the first slice. If you need the whole thing, use result: "object" and read structured or state instead of final_text.
What comes next
Poll mode and durable scheduled runs together make the runtime stop being blocking. Next is the primitive that uses that: a Decision tool that answers typed questions with calibrated probabilities (story in the next post). Then the team-definition work that lets a whole state-machine workflow ship as one file. Then the canvas you build those workflows on.