Design #91 — responsive session startup: render first, reconcile after

Requirement jwildfire/obot.roadmap#91 · goal #73 (increased autonomy) · related #127 (/oneoff lane), obot.agent#29 (lean bookends) · Draft — awaiting @jwildfire review

Drafted 2026-07-31 with fresh measurements of the current init path (§3). The requirement's original bar was “sub-30-second”; @jwildfire retargeted it to under 10 seconds on 2026-07-31, which is what this design is built against.

1. Summary

Today /session-init takes roughly ten minutes to produce a prioritized list. The instinct is to blame the network — three gh searches, a project-board query, a GraphQL Ideas sweep. The measurements say otherwise. Every network call the init makes, run cold and serially, totals about 4 seconds; with the cache the workspace already maintains, it totals 2 milliseconds. Essentially the entire ten minutes is language-model work: a subagent spawn, a digest, per-idea triage reasoning, and the synthesis of a fresh list.

That reframes the problem. You cannot reach a 10-second bar by making the API calls faster, batching them harder, or fanning them out — they are already noise. You reach it only by removing generated tokens from the critical path.

The design is therefore render first, reconcile after, in three tiers:

The enabling insight is that the machinery for Tiers 0 and 1 already exists and is tested: obot.agent/tools/session-hub/lib/collect.mjs ships collectScratchpad(), collectNextSession(), and collectGhSweep() — exactly the three hand-off sources plus the cached delta. What is missing is a terminal-shaped, non-LLM entry point over them.

2. What “responsive” means here

The bar is time-to-first-actionable-output: from @jwildfire typing /session-init to a usable, link-bearing, prioritized list being on screen. Under 10 seconds.

Three clarifications the target needs to survive contact with reality:

3. Where the time actually goes

Measured on @jwildfire's Mac, 2026-07-31, against the live workspace. Every row marked measured is a real wall-clock timing of the exact command the skill runs today; rows marked estimated are attribution by subtraction and remain to be instrumented.

Init stepWhat it doesWall clockBasis
1. Hand-off read 3 local file reads — today's scratchpad, latest diary loose ends, next-session-todo memory (~26 KB total) 38 msmeasured
2a. Delta — issues gh search issues --owner jwildfire --limit 100 919 msmeasured
2b. Delta — PRs gh search prs --owner jwildfire --limit 100 689 msmeasured
2c. Delta — board gh project item-list 1 --limit 80 (the slowest single call in the init) 2 385 msmeasured
2d. Delta — subagent Explore-agent spawn, three calls, digest to one line per item ~2 minestimated
2.5a. Inbox — sweep scripts/ideas-sweep (GraphQL, read-only, no LLM) 579 msmeasured
2.5b. Inbox — reminders scripts/reminders-to-ideas (osascript + GraphQL) 971 msmeasured
2.5c. Inbox — triage Per-idea classification and in-thread replies; cost scales with inbox size, unbounded minutesestimated
3–4. Prioritize + present Model reads ~26 KB of hand-off, merges three sources, generates a fresh ~40-line list tens of sestimated
5. Persist Rewrite the scratchpad ## Overview section < 1 sestimated
The headline measurement. The session-hub collector collectGhSweep() — the same three batched searches plus a release poll across five repos — runs in 3 767 ms cold and 2 ms warm (5-minute TTL disk cache at .claude/session-hub/cache/gh-sweep.json). Composing all three hand-off collectors plus the sweep in one Node process: 3 995 ms cold, 1–2 ms warm.

So: the entire data-gathering surface of /session-init is ~4 seconds cold and ~0 warm. Against a ~10-minute observed init, that is under 1% of the time. The other 99% is model work — spawning, digesting, triaging, and writing prose. Optimizations aimed at the network are rounding error; the only lever that moves the number is what the model is asked to generate before @jwildfire sees anything.

4. The design: three tiers

Tier 0 — deterministic render (the ≤ 5 s deliverable)

A new non-LLM entry point — session-hub.mjs --brief, or a thin scripts/session-brief wrapper over the same library — reads the three hand-off sources and prints the carried list verbatim: the scratchpad's ## Overview with its check state and grouping (Agent-actionable / Waiting on @jwildfire), plus any unchecked ## Todo stragglers, plus the diary's “Next session: loose ends” and the next-session-todo memory where they add items the scratchpad lacks.

It also prints a freshness header: hand-off source and age, delta-cache age, and whether Tier 2 is running. That header is what makes a provisional list trustworthy rather than misleading.

Crucially, nothing here is synthesized. The previous wrapup already did the prioritization work — that is the entire point of the lean-bookends contract. Init's job at first paint is to show that list, not to re-derive it.

Tier 1 — mechanical reconciliation (inside the ≤ 10 s bar)

The same script reads gh-sweep.json and annotates the rendered list without judgment:

Matching is on repo#N tokens already present in the carried lines, so it is exact string work. When the sweep cache is warm this costs ~2 ms; cold it costs ~4 s, which still fits the bar but leaves no margin — hence pre-warming (§5).

Tier 2 — the judgment pass (async, unbounded, non-blocking)

Everything that needs a model runs after the first paint: the delta subagent's drill-downs and “what does this actually mean” digest, the ideas-inbox triage with its in-thread replies and watermark advance, and any re-ranking of the carried list against what the delta found. Its output is a revision, not a list: a short “since the first paint” message plus a rewrite of the scratchpad ## Overview.

If Tier 2 finds nothing — the common case on a morning after a clean wrapup — it says so in one line and the session never notices it ran.

5. Pre-warming the delta cache

Tier 1 is 2 ms warm and 4 s cold, and the cache TTL is 5 minutes — so at a typical session start the cache is cold and the whole latency budget goes to one gh project item-list call. Three ways to keep it warm, in increasing order of infrastructure:

OptionMechanismCostNotes
A — accept coldNothing; pay ~4 s when the cache is stalenoneStill inside the 10 s bar, but with no headroom and a visible pause
B — launchd tickA local agent refreshes gh-sweep.json every N minutesone plist; N API calls/hourMac-local, matches the lid-open constraint already noted in #122
C — session-hub watchThe existing --watch loop already refreshes this cache while runningzero new codeOnly warm when the dashboard is up; free when it is

The pragmatic answer is C now, B if the pause is still felt — C is already built, and the cache write is best-effort and self-healing either way. A GitHub Action writing a committed snapshot is deliberately not proposed: the sweep is derived state and the session-hub design already ruled that it is never committed.

6. What leaves the critical path — and what that costs

StepNew placementWhy it is safe
Delta subagent digestTier 2Tier 1 already marks closed/merged carried items mechanically; the subagent adds interpretation, which can arrive late
Ideas-inbox triageTier 2, or out of init entirelyThe ideas-triage Action already files most inbox items within minutes of capture; init's sweep is a backstop, not the primary lane (see D4)
Prioritization proseRemovedThe wrapup already ranked the list; re-ranking at init duplicates it. Tier 2 re-ranks only if the delta changed something material
Session identity (step 0)UnchangedLocal file writes, already sub-second
Fallback full sweepUnchangedFires only when there is no usable hand-off; when it fires there is nothing to render, so the bar does not apply — it should say so explicitly

The honest cost: with the judgment pass deferred, the first list @jwildfire sees can be wrong in one specific way — an item whose meaning changed without its state changing (a PR that went red, a review comment that reframes the work). Tier 1 catches state; only Tier 2 catches meaning. The freshness header is what keeps that from being a silent failure.

7. What this means for --auto

An unattended session has no human to present to, so the latency bar does not apply to it — but the tiering still helps, because an --auto run currently spends its first minutes on the same delta agent before it can select an increment.

The rule should be asymmetric and explicit: an interactive session presents at Tier 1 and lets Tier 2 revise; an --auto session waits for Tier 2 before selecting. Selection eligibility depends on judgment the mechanical tier cannot supply — whether a design is signed off, whether a repo is inside the grant matrix, whether an item is gated on @jwildfire. Selecting from a provisional list would let an unattended session start work on something that closed overnight. The tiering makes --auto's init no slower and no faster; it just stops it from being the design's driving constraint.

8. Relationship to #127 (/oneoff)

#127 proposes a separate lane that skips session startup entirely for quick bounded work, and its own Business Requirement names this issue as the tension: “if /session-init were genuinely fast, some of this need would shrink.”

If Tier 0 lands, most of it does shrink. A 3-second render of a list @jwildfire was going to want anyway is not a tax worth building a bypass around. What survives of #127 is the other half of its ask — a durable trace for work done outside a full session — which is about the closing bookend, not the opening one.

Proposed position (D6): land Tier 0/1 first, then re-scope #127 to its wrapup half (“a bounded durable write for short work”) rather than a parallel startup lane. Decided together, per #127's own framing — not silently.

9. Instrumentation and the acceptance test

The requirement asks for per-step timing before any optimization merges. §3 satisfies part of that for the mechanical steps; three gaps remain, and they are exactly the steps this design moves off the critical path:

The implementation should emit a small timing ledger per init — one JSON line per step to .claude/session-hub/cache/init-timings.jsonl, appended, never committed — so the claim “first paint under 10 seconds” is a fact the next session can check rather than a target nobody re-measures. Suggested acceptance test, run three times on a warm workspace and three times cold:

t0  /session-init typed
t1  first paint (Tier 0 + Tier 1 rendered)     — assert t1 - t0 < 10 s
t2  Tier 2 revision posted (or "no changes")   — assert t2 - t0 < 2 min  (obot.agent#29)
    context consumed at t2                     — assert < 10% (obot.agent#29)

10. Decisions for @jwildfire

D1 — the bar's meaning. “Under 10 seconds” = first paint of the carried, mechanically-reconciled list (proposed), with the judgment pass arriving after. The alternative reading — 10 seconds to a fully reconciled list — is not reachable with a subagent in the loop, and would mean cutting the judgment pass entirely rather than deferring it.
D2 — who renders the first paint. A deterministic script whose output is relayed verbatim (proposed) · the model reading the three files and writing the list itself (today's behaviour; the measurements say this is what costs the time) · the script writing the existing session-hub HTML view and opening it in Chrome, so the terminal is not the surface at all.
D3 — pre-warming. Rely on the session-hub --watch loop when it is running and accept ~4 s otherwise (proposed: option C) · add a launchd tick (option B) · accept cold always (option A).
D4 — the ideas inbox. Move the sweep to Tier 2 (proposed) · remove it from init entirely and rely on the ideas-triage Action, with init printing only a pending count · leave it on the critical path. Note that obot.agent#74 currently degrades the unattended posting lane to a no-op, which is an argument for the Action being the primary lane regardless.
D5 — how revisions surface. A short “since the first paint” message plus a silent scratchpad rewrite (proposed) · re-print the whole list · scratchpad only, with nothing said in chat.
D6 — #127. Re-scope /oneoff to its durable-write half once Tier 0 lands (proposed) · keep it as a full parallel lane · close it into this requirement.
D7 — where the code lives. Extend tools/session-hub with a --brief mode, reusing its tested collectors (proposed) · a standalone scripts/session-brief · inline the logic in the skill as bash. The first keeps one parser for the scratchpad format; the last has no home for tests.

11. Risks and non-goals

12. Rollout

PhaseWhatWhereGate
1 — instrumentTiming ledger for all init steps; close the three gaps in §3 with real numbersobot.agentNumbers posted on #91
2 — render--brief mode over the existing collectors: Tier 0 + Tier 1, freshness header, tests against the shipped scratchpad parserobot.agent PR@jwildfire review + obot-merge
3 — retier the skillsession-init rewritten around the three tiers; --auto pinned to wait for Tier 2 (§7)obot.agent PR@jwildfire review; measured first paint reported in the PR
4 — settle the neighboursD4 (inbox placement) and D6 (#127 re-scope) applied; obot.agent#29 contract text updated to carry both numbersobot.agent + hub@jwildfire

This design document was drafted by Claude Code using Opus 5 and reviewed by @jwildfire.