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.
tabs serve's long-poll carries ~85 lines of machinery that a 2-second pollwould 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 watchis already a blocking command the agent calls once. Whether it waits by holding
a socket open or by looping
sleep 2 → GET → checkinternally, the agent sees oneBash 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 linesfor(;;)loops inGET /tabsandGET /decision,MAX_WAIT_MS, status 499 — ~37 linesidleTimeout: 0— load-bearing and non-obvious.Bun.servedefaults to a10-second idle timeout that kills every long-poll; the symptom was
tabs watchreporting "nothing is listening" about a server that was running fine. Anyone
editing the
Bun.serveoptions 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 intabs-watch.tsandtabs-suggest.ts, ~30 more lines.What it becomes
Server handler:
Client:
Every request is short, so a failure is unambiguously "the server is gone" — the
whole
pingBridgeretry path can go.suggest --wait 300becomes ~150 requests over five minutes instead of one. Onloopback, against a handler that does a map lookup, that is free.
Scope
src/commands/tabs-serve.ts— delete the waiter machinery;?wait=becomes ano-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
pingBridgeGET /tabsorGET /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.