ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-08-17Intermediate

When a Subagent Never Finishes, Look at the Parent's Unapproved Artifacts First

A subagent that runs all night and never returns is usually not the one that is stuck. Here is how I separate a blocked parent from a blocked child using a short log-scanning script, where the new always proceeds mode helps, and where I stop letting approvals happen automatically.

SubagentsApprovalsLogs2Indie developmentAgent operations

Hand the pre-release checks to an agent in the evening, read the results in the morning. As an indie developer maintaining several apps alone, I lean on that rhythm more than I probably should. Split the work into stages, delegate each stage to a subagent, and the whole pass should complete while you sleep.

That morning, the screen still looked alive. The spinner was turning. No errors anywhere. The only clue was the timestamp on the last output: more than thirty minutes old.

I opened the subagent's log and went looking for the failure. Nothing unusual. The model was fine, the tools were fine. Everything was simply waiting.

I had been reading the wrong log. The child was not stuck. The parent was.

A healthy child cannot move while the parent is waiting for approval

When a subagent runs on delegation from a parent, the artifacts it produces are handed back to the parent for disposition. If that parent is sitting behind an approval dialog of its own, the child's request never gets its turn.

The child's log gives you nothing but "waiting." Because it is not an error, timeouts may never fire. The run ends up in the quietest possible failure state and stays there until morning.

Google clearly recognized the shape of this problem. In August 2026, an "always proceeds" mode arrived specifically to address subagents that keep waiting while the parent is blocked, by auto-approving their artifacts. Which tells you something useful in reverse: until then, waiting forever was the default behavior.

Not every stalled turn has this cause. When hooks are involved, the triage order is different, and I wrote that case up separately in 2.6.0 Changed How Hooks Wait. A Triage Order for Turns That Never Finish. What follows happens even when you use no hooks at all.

Find the stalled conversations from the log instead of by eye

Reading logs by eye stops working the moment your hierarchy goes past two levels. So I run the log through a script that lists every conversation that issued an approval request and never produced another event.

The August update helps here. Delegated subagents now carry a subagent_info payload containing conversation_id and log_uri, so parent and child can be matched by identifier rather than by guesswork. Each tool call carries tool_info with a normalized tool name, so you can also see what the run is waiting on.

Start by checking which event names actually appear in your own logs. They change between versions.

# types.py - count the event types present in a log
import json, sys, collections
 
c = collections.Counter()
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        ev = json.loads(line)
    except json.JSONDecodeError:
        continue
    c[ev.get("type", "(no type)")] += 1
 
for k, v in c.most_common():
    print(f"{v:>5}  {k}")

On my machine (Python 3.10.12) it printed this:

    2  approval_request
    2  subagent_start
    2  tool_call
    1  turn_start
    1  result
    1  heartbeat

Feed those names into the checks in the next script. Record every approval request, drop it when a resolution or a result arrives, and whatever is left is your stall.

#!/usr/bin/env python3
"""Read stream-json lines and list conversations stuck waiting for approval."""
import json, sys
from datetime import datetime, timezone
 
PENDING = {}   # conversation_id -> (timestamp, tool being waited on)
LAST    = {}   # conversation_id -> last event timestamp
PARENT  = {}   # child conversation_id -> parent conversation_id
LOGURI  = {}   # conversation_id -> log_uri
 
def ts(v):
    return datetime.fromisoformat(v.replace("Z", "+00:00"))
 
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        ev = json.loads(line)
    except json.JSONDecodeError:
        continue    # truncated lines are normal while a run is still going
 
    sub = ev.get("subagent_info") or {}
    cid = sub.get("conversation_id") or ev.get("conversation_id")
    if not cid:
        continue
    if sub.get("log_uri"):
        LOGURI[cid] = sub["log_uri"]
    if sub.get("parent_conversation_id"):
        PARENT[cid] = sub["parent_conversation_id"]
 
    t = ts(ev["timestamp"])
    LAST[cid] = t
 
    kind = ev.get("type")
    if kind == "approval_request":
        PENDING[cid] = (t, (ev.get("tool_info") or {}).get("name", "unknown"))
    elif kind in ("approval_resolved", "result"):
        PENDING.pop(cid, None)
 
now = max(LAST.values()) if LAST else datetime.now(timezone.utc)
rows = sorted(
    ((round((now - t).total_seconds()), cid, tool,
      PARENT.get(cid, "-"), LOGURI.get(cid, "-"))
     for cid, (t, tool) in PENDING.items()),
    reverse=True,
)
 
if not rows:
    print("No conversations are stalled on approval")
    sys.exit(0)
 
print(f"{'wait_s':>7}  {'conversation_id':<16} {'tool':<12} {'parent':<16} log_uri")
for sec, cid, tool, par, uri in rows:
    print(f"{sec:>7}  {cid:<16} {tool:<12} {par:<16} {uri}")
 
blocked = {r[3] for r in rows if r[3] != "-"}
for cid, _ in PENDING.items():
    if cid in blocked:
        print(f"\n-> {cid} is itself waiting for approval. Its children cannot move until it does.")

Running it against a log shaped like that morning produced this:

 wait_s  conversation_id  tool         parent           log_uri
   2028  conv-parent-01   write_file   -                -
   1977  conv-child-a     write_file   conv-parent-01   file:///logs/conv-child-a.jsonl

-> conv-parent-01 is itself waiting for approval. Its children cannot move until it does.

The ordering is not the interesting part. The last line is. The parent had been stalled for 2,028 seconds and the child for 1,977 — a gap of only 51 seconds. The child had spent almost its entire wait blocked behind the parent.

Add a line resolving the parent's approval, run it again, and the output becomes "No conversations are stalled on approval." Restarting the child alone would not have cleared anything.

You can run this against a log that is still being written. Point it at a copy, or pipe the tail of the file in; the decoder skips the half-written final line rather than aborting. I check it once before I look at anything else, because the answer changes what I open next: a stalled parent means I go resolve a dialog, a stalled child means I go read what the child asked for.

Worth noting what the script deliberately ignores. Heartbeat events keep a conversation's last-seen timestamp fresh without moving the work forward, so "recent activity" is a poor signal on its own. Only the unresolved approval request counts as a stall here.

One disclosure: I assembled that log myself to match the published shape of subagent_info and tool_info as of August. The numbers come from actually running the script, but they were not captured from a production run of the product.

What "always proceeds" actually closes

always proceeds auto-approves artifacts produced by subagents. The child stops waiting even when the parent is tied up with something else.

It helps when the handoff itself is the cause. Nothing was dangerous about the pending approval; the request was simply queued behind another one. In that case a single setting recovers the thirty minutes you lost overnight.

Depth matters too. The same August update made nested subagents visible past the first generation, with tool confirmation requests handled at any depth. Before that, a stall two levels down could sit outside everything you were looking at, which is exactly the case where a scan over identifiers beats scrolling through a transcript.

It does not help with the parent's own dialog. Parent approvals still wait for a human. And if your symptom is the same dialog appearing over and over, the cause lives somewhere else entirely — I covered that in Antigravity Keeps Asking Permission for the Same Command? Fix the Approval Dialog Loop.

SymptomWhat the stall scan showsWhere to intervene
Child stalled, parent movingOnly the child appearsInspect the child's request. Revisit tool permissions
Parent and child stalled togetherBoth appear with close wait timesResolve the parent. Add always proceeds to prevent recurrence
The same prompt keeps returningStalls appear but wait times stay short and rotateRevisit how your approval rules match
Stalls at grandchild depth or deeperRows whose parent column holds a child IDConfirm approvals are handled at every depth

Where I stop letting approvals happen automatically

Convenience here is bought with a piece of oversight, so I did not make this a single global decision.

I drew the line at where the output lands. Anything that stays inside the working directory gets auto-approved: reads and analysis, draft generation, scratch files for comparison. If those break, I throw them away and regenerate.

Working alone, I have no second reviewer to catch a bad artifact, so the boundary has to do that job for me. Anything that leaves the repository — signing, distribution, anything touching credentials — I leave for the version of me who shows up in the morning. Finishing overnight feels good, but being able to undo something at 8 a.m. is worth more.

The genuinely hard part is that approval rules often match more broadly than intended. You can define an auto-approval scope carefully and still have the rule let things through that you never meant to include. I learned that the expensive way, and the audit procedure is in The Line I Thought Matched Nothing Was Approving Everything. Confirm what your rules currently permit before you widen anything.

What to leave in place for tomorrow morning

Three things are enough to keep a stalled morning from turning into a lost one.

  1. Keep the stall scan next to wherever your run logs land. One command in the morning tells you whether to read the parent or the child
  2. Hold on to log_uri from subagent_info. It takes you straight to the stalled child's log instead of hunting for it
  3. Write down your auto-approval scope in terms of where output lands, so you are not re-deciding it every time

Get the location of the stall wrong and you will spend twenty minutes reading a perfectly healthy log. I did exactly that. Next time, start with the parent.

Handing your nights to an agent is a quiet, pleasant way to work when it holds together. I hope this helps you get that quiet back.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-08-16
Android 17 Went Canary-Only, So I Gave My Agent the Silence Rules First
Android 17 dropped Developer Previews in favour of rolling Canary builds, which moved the verification deadline onto my side of the table. Here is how I handed the tracking job to an Antigravity agent by designing when it stays quiet, not when it reports.
Agents & Manager2026-08-07
I Asked the Agent to Pick Tests From My Diff and It Said "None" Nine Times Out of Ten
Change-based test selection returned zero tests for most edits. Measuring how far an import graph actually reaches across three repositories, and rebuilding the selection contract around it.
Agents & Manager2026-08-05
Reading the Same History in 38 Seconds — or 0.4: Handing a Read-Only .git to Your Agent
Antigravity CLI 1.1.10 lets the sandbox read .git without write access. I built a per-session history summary two ways, measured a roughly 100x speed gap, traced it to process spawn cost, and added a guard for shallow clones that silently corrupt the numbers.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →