◉ANTIGRAVITY LABJP
Articles/Antigravity Basics
◉ Antigravity Basics/2026-07-31Advanced

The Flaky Hook Started Failing Every Time — Migrating to Declarative Sessions in the Antigravity SDK

Registering hooks after session init is gone, replaced by declaration at session creation. Here is what happened when I moved four agent definitions across, plus the harness I used to measure ordering non-determinism over 2,000 runs.

Antigravity404SDK5Hook DesignMigration9Reproducibility

✦ Premium Article

The morning after the migration, CI was stuck on red.

The day before, that same test suite passed maybe four runs out of ten. Now it never passed. The only thing I had touched was how hooks were registered.

Here is the conclusion up front: that red board was not a failed migration. It was proof the migration had worked.

What changed is not what you can register, but when it is decided

In the Antigravity SDK, registering hooks and triggers dynamically after session initialization has been removed in favor of declaring them at session creation. Details shift between versions, so check the current wording in the Antigravity changelog before you plan your own move.

What you can register has not changed. What changed is the moment your configuration becomes final.

AspectDynamic registration after initDeclaration at session creation
When config is finalizedAt runtime, as registrations completeThe instant the session is created
Hook execution orderDepends on which registration finished firstThe order of the array you wrote
Where conditionals liveIn whether you register at allInside the hook body
Reproducibility for identical inputCan vary run to runAlways identical
Inspecting the configurationRequires running itReadable statically, before launch

I thought I was migrating for the last row. Being able to read a configuration without executing it is the obvious win. The row that actually mattered turned out to be the one above it.

Find every call site mechanically

My first instinct was grep -rn "register_hook". I only maintain four agent definitions for my own apps, so eyeballing the results felt reasonable.

It was not enough. A register_hook sitting inside an if, inside a loop, or inside a try demands completely different rewrites. Grep hands you lines. It does not hand you the context those lines sit in.

So I wrote a small script that reports both the call site and its syntactic context.

#!/usr/bin/env python3
"""Find post-init hook/trigger registrations and sort them by migration difficulty."""
import ast, os, sys
 
DYNAMIC = {"register_hook", "register_trigger", "add_hook", "add_trigger"}
 
def scan(root):
    rows = []
    for dp, _, fs in os.walk(root):
        if any(seg in dp for seg in (".venv", "node_modules", "__pycache__")):
            continue
        for f in fs:
            if not f.endswith(".py"):
                continue
            p = os.path.join(dp, f)
            try:
                tree = ast.parse(open(p, encoding="utf-8").read(), p)
            except SyntaxError as e:
                # Half-migrated files will land here. Never swallow this silently.
                print(f"skip (syntax): {p}: {e}", file=sys.stderr)
                continue
 
            # ast nodes carry no parent pointer, so build the child -> parent map first
            parent = {}
            for n in ast.walk(tree):
                for c in ast.iter_child_nodes(n):
                    parent[c] = n
 
            for n in ast.walk(tree):
                if not (isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)):
                    continue
                if n.func.attr not in DYNAMIC:
                    continue
                ctx, cur = set(), parent.get(n)
                while cur is not None:
                    if isinstance(cur, ast.If):
                        ctx.add("conditional")
                    elif isinstance(cur, (ast.For, ast.While, ast.comprehension)):
                        ctx.add("loop")
                    elif isinstance(cur, (ast.Try, ast.ExceptHandler)):
                        ctx.add("try")
                    cur = parent.get(cur)
                rows.append((p, n.lineno, n.func.attr, sorted(ctx)))
    # ast.walk is breadth-first, so results are not in source order. Re-sort here.
    return sorted(rows, key=lambda r: (r[0], r[1]))
 
TIER = {
    frozenset(): "A: move straight into an array",
    frozenset({"conditional"}): "B: rewrite as an early return inside the hook",
    frozenset({"loop"}): "B: expand generated functions into the array",
    frozenset({"try"}): "C: decide failure behavior first",
}
 
def main(root):
    rows = scan(root)
    for p, line, call, ctx in rows:
        tier = TIER.get(frozenset(ctx), "C: decide by hand")
        print(f"{p}:{line}  {call:<17} ctx={','.join(ctx) or '-':<12} {tier}")
    print(f"\n{len(rows)} call sites total")
 
if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else ".")

Because ast.walk traverses breadth-first, printing results as they come out scrambles the file order. Migration work goes top-to-bottom through a file, so skipping that re-sort costs you rework. That tripped me up on the first pass.

Running it against my definitions:

sample/agents/build_agent.py:4  register_hook     ctx=-            A: move straight into an array
sample/agents/build_agent.py:6  register_hook     ctx=conditional  B: rewrite as an early return inside the hook
sample/agents/build_agent.py:7  register_trigger  ctx=-            A: move straight into an array
sample/agents/review_agent.py:5  register_hook     ctx=loop         B: expand generated functions into the array
sample/agents/review_agent.py:6  register_trigger  ctx=-            A: move straight into an array

5 call sites total

Three A's, two B's. Small numbers, but the split is what lets you estimate how far you will get in a sitting. The one tier that needs thought before any code moves is C — a registration inside a try forces you to answer whether a failed registration should halt startup or let it continue.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦An AST-based script that finds every post-init register_hook / register_trigger call and sorts them into difficulty tiers A, B, and C — complete and ready to run against your own repository
✦A measured result showing that dynamic registration satisfied hook ordering dependencies only 48.2% of the time across 2,000 runs, along with the minimal harness that produced it
✦The reversal nobody warns you about: declaring hooks does not remove the bug, it makes the bug reproduce every time — and how to read a CI board that turns solid red right after the migration
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

◉ Antigravity2026-10-28
When Antigravity Says Failed to Fetch: Decide the First Three Places to Look
A short triage order for Failed to Fetch in Antigravity: check whether the destination is reachable, whether you are still signed in, and whether a proxy or VPN sits in the middle. Includes copy-paste terminal checks and what each result means.
◉ Antigravity2026-10-16
When Antigravity Says the Server Crashed Unexpectedly: Check the CPU, WSL, or Version First?
The editor opens but the agent dies right away with 'server crashed unexpectedly'. A short triage across three suspects, CPU AES support, WSL or SSH paths, and the app version, built from public forum and issue reports, with the unconfirmed parts marked as unconfirmed.
◉ Antigravity2026-10-15
Before You Press Delete on an Antigravity Project, Can You Name Three Things That Stay?
Does deleting a project in Antigravity 2.0 remove your source code? A community explanation, split into its two storage locations, plus a read-only script and a safe order for freeing disk space.