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.
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:
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 step | What it does | Wall clock | Basis |
|---|---|---|---|
| 1. Hand-off read | 3 local file reads — today's scratchpad, latest diary loose ends, next-session-todo memory (~26 KB total) |
38 ms | measured |
| 2a. Delta — issues | gh search issues --owner jwildfire --limit 100 |
919 ms | measured |
| 2b. Delta — PRs | gh search prs --owner jwildfire --limit 100 |
689 ms | measured |
| 2c. Delta — board | gh project item-list 1 --limit 80 (the slowest single call in the init) |
2 385 ms | measured |
| 2d. Delta — subagent | Explore-agent spawn, three calls, digest to one line per item | ~2 min | estimated |
| 2.5a. Inbox — sweep | scripts/ideas-sweep (GraphQL, read-only, no LLM) |
579 ms | measured |
| 2.5b. Inbox — reminders | scripts/reminders-to-ideas (osascript + GraphQL) |
971 ms | measured |
| 2.5c. Inbox — triage | Per-idea classification and in-thread replies; cost scales with inbox size, unbounded | minutes | estimated |
| 3–4. Prioritize + present | Model reads ~26 KB of hand-off, merges three sources, generates a fresh ~40-line list | tens of s | estimated |
| 5. Persist | Rewrite the scratchpad ## Overview section |
< 1 s | estimated |
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.
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.
The same script reads gh-sweep.json and annotates the rendered list without judgment:
repo#N that the sweep reports closed or merged is struck through and moved below the fold.ideas-sweep's exit output — the number, not the triage.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).
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.
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:
| Option | Mechanism | Cost | Notes |
|---|---|---|---|
| A — accept cold | Nothing; pay ~4 s when the cache is stale | none | Still inside the 10 s bar, but with no headroom and a visible pause |
| B — launchd tick | A local agent refreshes gh-sweep.json every N minutes | one plist; N API calls/hour | Mac-local, matches the lid-open constraint already noted in #122 |
| C — session-hub watch | The existing --watch loop already refreshes this cache while running | zero new code | Only 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.
| Step | New placement | Why it is safe |
|---|---|---|
| Delta subagent digest | Tier 2 | Tier 1 already marks closed/merged carried items mechanically; the subagent adds interpretation, which can arrive late |
| Ideas-inbox triage | Tier 2, or out of init entirely | The 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 prose | Removed | The 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) | Unchanged | Local file writes, already sub-second |
| Fallback full sweep | Unchanged | Fires 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.
--autoAn 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.
/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.
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)
--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).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./oneoff to its durable-write half once Tier 0 lands (proposed) · keep it as a full parallel lane · close it into this requirement.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.claude CLI process boot, MCP server startup, and machine-level factors stay out of scope, unchanged from the requirement's assumption 1.| Phase | What | Where | Gate |
|---|---|---|---|
| 1 — instrument | Timing ledger for all init steps; close the three gaps in §3 with real numbers | obot.agent | Numbers posted on #91 |
| 2 — render | --brief mode over the existing collectors: Tier 0 + Tier 1, freshness header, tests against the shipped scratchpad parser | obot.agent PR | @jwildfire review + obot-merge |
| 3 — retier the skill | session-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 neighbours | D4 (inbox placement) and D6 (#127 re-scope) applied; obot.agent#29 contract text updated to carry both numbers | obot.agent + hub | @jwildfire |