Skip to content

Replace the bridge long-poll with plain polling #31

Description

@colevels

tabs serve's long-poll carries ~85 lines of machinery that a 2-second poll
would not need, and the latency it buys is invisible in this product.

The reasoning that led to long-poll was wrong

The justification was "an agent tick costs tokens". It doesn't apply: tabs watch
is already a blocking command the agent calls once. Whether it waits by holding
a socket open or by looping sleep 2 → GET → check internally, the agent sees one
Bash call and one result. Identical token cost.

What long-poll actually buys is sub-second latency instead of ≤2s — on a path where
the extension already debounces 2.5s and the other end is a human clicking a button.

What it costs today

In src/commands/tabs-serve.ts:

  • park() / wake() / tabWaiters / verdictWaiters — ~18 lines
  • for(;;) loops in GET /tabs and GET /decision, MAX_WAIT_MS, status 499 — ~37 lines
  • idleTimeout: 0 — load-bearing and non-obvious. Bun.serve defaults to a
    10-second idle timeout that kills every long-poll; the symptom was tabs watch
    reporting "nothing is listening" about a server that was running fine. Anyone
    editing the Bun.serve options can reintroduce it.

And because a held connection can drop without the server going away, both clients
need to tell those two cases apart — pingBridge() plus a retry loop in
tabs-watch.ts and tabs-suggest.ts, ~30 more lines.

What it becomes

Server handler:

if (tabState && tabState.version > since) return json({ ...tabState, changes })
return new Response(null, { status: 204 })

Client:

for (;;) {
  const res = await fetch(`/tabs?since=${since}`)   // always sub-ms
  if (res.status === 200) return await res.json()
  if (Date.now() >= deadline) return null
  await sleep(2000)
}

Every request is short, so a failure is unambiguously "the server is gone" — the
whole pingBridge retry path can go.

suggest --wait 300 becomes ~150 requests over five minutes instead of one. On
loopback, against a handler that does a map lookup, that is free.

Scope

  • src/commands/tabs-serve.ts — delete the waiter machinery; ?wait= becomes a
    no-op accepted for compatibility (or is dropped, since only our own clients send it)
  • src/commands/tabs-watch.ts, src/commands/tabs-suggest.ts — internal poll loop,
    drop pingBridge
  • The extension is unaffected: it never calls GET /tabs or GET /decision.

Keep the E2E checks from the auto-mode work: watch unblocking on a real tab change,
and suggest returning a Deny with its reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions