You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reviewed per the repo plan-review skill (AGENTS.md + docs/feature-divide.md loaded;
every baseline claim verified against live main). Verdict: HANDOFF-READY.
All four caps are NEW (generous by default, no change to any existing cap → no human cap-approval gate). Cloud ops N/A is correct (no Production mutate), and the
three phase issues (#541/#540/#539) conform to the phase map below; Depends-on lines are
correct (Phase 2 and Phase 3 each depend only on Phase 1). No Blockers/Majors.
Minor — probe-bind precedence locked (envelope-authoritative).GET /api/harness/status
must resolve the bind from the caller envelope meta.activeSandboxId via the resolveSessionStore → isEnvelopeStore → readEnvelope seam first — the same
server-authoritative rule the merged meta(#532): built-in meta sandbox tools on /api/agent #535/agent-route B1 fix established. The host ?sandboxId= param is a Redis-safe carry only (mirror of parseQuerySandboxId in app/api/sandboxes/route.ts), never an override: a stale/foreign host param must not
cause a probe against a different sandbox. Phase 2 (plan: workspace status bar — phase 2 — git branch/commit probe #540) adopts this exact precedence.
Minor — probe rate-limit is per-instance best-effort. On Vercel serverless the
"bounded in-process per-user clock" is per-instance / ephemeral, not global across
instances. The cap still blocks a single hot loop, and the host cadence is the primary
throttle; the exec stays bounded and per-slot fail-soft holds. Worded below accordingly
(do not present it as a hard global limit).
Nit — reserve primary header controls. The existing header holds lifecycle + model
label + Next (a primary action). Status slots must yield before those on narrow
canvases; the model/lifecycle/Next controls are never in the slot drop pool, so primary
action geometry stays stable (feature-divide / layout-stability).
Nit — % of window condition is safe. The model catalog does not currently carry maxContext/token-limit metadata (verified), so the phase-3 design correctly degrades to absolute tokens only — the "only when known" guard never invents a denominator.
Enforce this in plan: workspace status bar — phase 3 — context/usage slot (implements #327) #539's host fold: never compute a denominator unless catalog metadata
explicitly provides it.
Summary
Ship the workspace status bar from product issue #328 as the authoritative
harness chrome: a compact status strip in the Wasm header band showing sandbox identity · cwd · git branch/commit · context/usage. Product source of
truth is in-canvas (Wasm), per feature-divide; the host may mirror a short summary
but must never host a competing status panel. Context comes from provider usage
(#327), never client token math. Split into 3 implementable phases with distinct
gates (chrome/wire · new server git-probe route · usage plumb).
Goals
#
Goal
Success signal
1
Persistent Wasm status bar with sandbox · cwd · git (when available)
Slots render on-canvas after hydrate + each turn; no secrets; ~390px usable
2
Context/usage slot is provider usage, not a client estimator
Number comes from the API usage field; labeled; missing usage → hides or marks estimated
3
Values refresh across turns / cwd change / session restore without manual refresh
Status updates when RunAgentResult.cwd/activeSandboxId land and after a git probe
4
Graceful degradation
Missing git / no sandbox → muted/hidden slots, never broken layout
Forbidden wiring: the status bar is NOT a second DOM chat/status panel; the
git probe is NOT an unbounded agent exec on every frame; no Gateway/sandbox
secrets (token/base_url) ever leave the server.
Architectural decisions
Decision
Options considered
Choice
Why
Where the status bar lives
A: Wasm header band (reuse existing frame() header row) / B: dedicated thin strip under header / C: DOM-only
A — extend the existing Wasm header band (header_h = TOUCH_H+24 in ui.zig) with right-aligned status slots
Matches #328 "header band or reserved chrome strip"; product truth stays in-canvas per feature-divide; zero new band layout
How status reaches Wasm
A: additive bridge fields + version bump / B: overload a message kind / C: DOM punctuation-only
A — new additive bridge state exports (inv_set_status_slot* / getters), protocol bump 12 → 13
Mirrors the shipped model-catalog push (setModelCatalog → inv_push_model_catalog_entry); additive (old fields intact) so node time is bounded; protocol is additive-only
Git branch/commit source
A: new server read-only probe route against the resolved bind / B: force an agent tool exec in-turn / C: client-side guess
A — a new session-gated GET /api/harness/status route that resolves the active bind and runs a bounded, argv-only read-only git probe
Git is a sandbox fact (server-side per feature-divide); a model turn to probe git is unbounded/expensive; branch+SHA must be authoritative, rate-limited
A — envelope wins; query param is a Redis-safe carry only
Mirrors the merged #535 agent-route precedence (envelope beats body/host carry); a stale/foreign host param must never probe a wrong bind. The ?sandboxId= param follows parseQuerySandboxId exactly as in app/api/sandboxes/route.ts
A — server-side STATUS_PROBE_MIN_INTERVAL_MS cap; per-instance best-effort on Vercel serverless (not a global lock), primary throttle is the host cadence
Host timers are per-tab and resettable; the read-only exec must be bounded server-side so a refresh loop or stale tab can't hammer the sandbox. The cap still blocks single-path hot loops; keep per-slot fail-soft on rate exhaustion
Layer placement
Concern
Layer
Path(s)
Rationale
Status slot paint (sandbox/cwd/git/context)
Wasm
native/harness/src/ui.zig header band + new status.zig
Wasm status strip (phase 1). Extend the header band with a right-aligned
horizontal pack of text slots rendered from additive bridge state: sandbox (backend/short id) · cwd · git (branch@sha + optional * dirty)
· context (N tok). Palette only: TEAL default, WARM for git-dirty/busy,
EMBER only for genuine errors (sandbox down / out-of-date). Each slot
truncates with … + tooltip/expand-on-tap; slots drop in priority order when
the viewport is narrow (~390px: git first, then cwd, then context — sandbox is
last to hide). The existing primary header controls (lifecycle + model label + Next) are never in the drop pool — status slots yield before any of those, so
primary-action geometry stays stable on narrow canvases. Missing value → muted —/hidden, never a broken layout or h-gutter.
Additive bridge v13. Add read/write state exports (e.g. inv_set_status_slot, inv_status_slot_len/copy) for each in bridge.zig + harnessBridge.ts. Old
exports untouched; host clears slots on bridge.reset() / Clear / New session.
Host fold. On hydrate and after each successful turn, the host
(1) resolves sandbox identity from the /api/sandboxes response it already
fetches, (2) copies RunAgentResult.cwd and the effective activeSandboxId,
(3) pushes them into the Wasm status slots. SessionPicker/Clear resets slots.
Context slot updated when a usage summary is present (phase 3).
Git probe (phase 2). New session/user-gated GET /api/harness/status: requireSessionUser + read the caller envelope meta.activeSandboxId via the
existing resolveSessionStore → isEnvelopeStore → readEnvelope seam (this envelope
wins over any ?sandboxId= carry, per the B1/#535 precedence), resolve the bind via resolveAgentSandbox, then run a bounded, argv-only, read-only git rev-parse --abbrev-ref HEAD + git rev-parse --short HEAD + optional git status --porcelain (dirty flag) through the resolved bind client. Output is
truncated to a cap; a non-git repo / no bind / exec error → empty slots (fail
soft). Server-side min-interval rate limit (STATUS_PROBE_MIN_INTERVAL_MS) as a per-instance best-effort clock (primary throttle is the host cadence; the cap
blocks single-path hot loops — return cached last value, never 429-spam, never
exec when limited).
Host calls after turns/cwd changes and on a cadence; not every frame.
Context/usage (phase 3, implements #327). Capture AI SDK usage
(prompt/completion/total, cached when present) at completion time on /api/chat
and /api/agent (JSON result + stream final chunk). Carry a small bounded usage summary on RunAgentResult / AgentSuccess / the stream done event;
host folds it into the context slot (and may mirror). Missing usage → hide the
slot or mark estimated — never present client math as API truth. Absolute
tokens only; % of window only when model max-context is known from catalog, else
no fake denominator (the catalog currently exposes no max-context, so v1 is
absolute-only by default).
Edge cases: empty session (no envelope) → sandbox —; Wasm load fail → host
chips still work (no regression); API 4xx/5xx → probe/usage absent, slots mute;
refresh/restore → slots reseed from envelope + /api/sandboxes; mobile ~390px →
slot priority ordering; Clear/new session → bridge.reset() clears slots.
Cloud ops path
N/A — no Production mutate. New GET /api/harness/status is read-only against
the resolved bind; no schema/DB/env/deploy change. No GHA; no workflow_dispatch.
(Note the git-probe route reuses existing DI-bound sandbox clients — no new
runner/env.)
Living docs plan
Surface
Change
Notes
docs/feature-divide.md
Add Workspace status bar row to the ownership table + a Status bar note in Data flow
Wasm primary, host mirror only; no dual status panel
docs/harness-limits.md
Document status slots, slot-priority order on narrow canvases (primary controls never dropped), git-probe cadence/rates, usage-source rule
Reuses session/sandbox/env seams; no host URL/account hardcoding
Caps table
Cap / ceiling
Value
Rationale
Code location
STATUS_PROBE_MIN_INTERVAL_MS (NEW)
2000
server-side git-probe min-interval so a refresh loop/stale tab cannot exec hammer the sandbox; generous vs real cadence; per-instance best-effort on serverless
lib/sessionCloudCaps.ts
STATUS_SLOT_MAX_BYTES (NEW)
96
per-slot display cap (cwd path / label) — truncate with …; far under argv/bridge ceilings
bounded usage summary JSON carrier (prompt/completion/total/cached) — stays under the stream event / JSON-result payload ceilings
lib/agent/agentStream.ts + lib/agentApi.ts
All caps are NEW (not changes to existing caps), generous by default vs the
transport ceiling of their carriers (bridge state, argv, Runtime.request body/SSE
event). No change to any existing cap → no human cap-approval gate for this
plan.
Open questions
None — all in-scope decisions locked above. (#328's only non-goal — operator-triggered
sandbox switch — remains covered by the shipped meta.sandbox_switch, not this bar.)
Plan header
plan/status-bar(per-phase branch suffix)app/*,lib/*· harnessnative/harness/**(Zig) · Vercelapp/api/**,lib/agent/**docs/feature-divide.md,docs/harness-limits.md,docs/sandbox.md,docs/session-model.md,docs/agent-stream.md,AGENTS.md,README.mdReview notes (2026-08-16)
Reviewed per the repo
plan-reviewskill (AGENTS.md + docs/feature-divide.md loaded;every baseline claim verified against live
main). Verdict: HANDOFF-READY.All four caps are NEW (generous by default, no change to any existing cap →
no human cap-approval gate). Cloud ops N/A is correct (no Production mutate), and the
three phase issues (#541/#540/#539) conform to the phase map below; Depends-on lines are
correct (Phase 2 and Phase 3 each depend only on Phase 1). No Blockers/Majors.
GET /api/harness/statusmust resolve the bind from the caller envelope
meta.activeSandboxIdvia theresolveSessionStore → isEnvelopeStore → readEnvelopeseam first — the sameserver-authoritative rule the merged meta(#532): built-in meta sandbox tools on /api/agent #535/agent-route B1 fix established. The host
?sandboxId=param is a Redis-safe carry only (mirror ofparseQuerySandboxIdinapp/api/sandboxes/route.ts), never an override: a stale/foreign host param must notcause a probe against a different sandbox. Phase 2 (plan: workspace status bar — phase 2 — git branch/commit probe #540) adopts this exact precedence.
"bounded in-process per-user clock" is per-instance / ephemeral, not global across
instances. The cap still blocks a single hot loop, and the host cadence is the primary
throttle; the exec stays bounded and per-slot fail-soft holds. Worded below accordingly
(do not present it as a hard global limit).
label + Next (a primary action). Status slots must yield before those on narrow
canvases; the model/lifecycle/Next controls are never in the slot drop pool, so primary
action geometry stays stable (feature-divide / layout-stability).
% of windowcondition is safe. The model catalog does not currently carrymaxContext/token-limit metadata (verified), so the phase-3 design correctly degrades toabsolute tokens only — the "only when known" guard never invents a denominator.
Enforce this in plan: workspace status bar — phase 3 — context/usage slot (implements #327) #539's host fold: never compute a denominator unless catalog metadata
explicitly provides it.
Summary
Ship the workspace status bar from product issue #328 as the authoritative
harness chrome: a compact status strip in the Wasm header band showing
sandbox identity · cwd · git branch/commit · context/usage. Product source of
truth is in-canvas (Wasm), per feature-divide; the host may mirror a short summary
but must never host a competing status panel. Context comes from provider
usage(#327), never client token math. Split into 3 implementable phases with distinct
gates (chrome/wire · new server git-probe route · usage plumb).
Goals
~390pxusableusagefield; labeled; missing usage → hides or marks estimatedRunAgentResult.cwd/activeSandboxIdland and after a git probeNon-goals / out of scope
/settings/sandbox.$spend — optional later via TPXcost.meta).git probe is NOT an unbounded agent exec on every frame; no Gateway/sandbox
secrets (token/base_url) ever leave the server.
Architectural decisions
frame()header row) / B: dedicated thin strip under header / C: DOM-onlyheader_h = TOUCH_H+24inui.zig) with right-aligned status slotsinv_set_status_slot*/ getters), protocol bump 12 → 13setModelCatalog→inv_push_model_catalog_entry); additive (old fields intact) so node time is bounded; protocol is additive-onlyGET /api/harness/statusroute that resolves the active bind and runs a bounded, argv-only read-only git probemeta.activeSandboxId(server-authoritative) / B: host?sandboxId=query param?sandboxId=param followsparseQuerySandboxIdexactly as inapp/api/sandboxes/route.tsusageplumbed from chat/agent completions (#327) / B: client char×const estimatorusageon/api/chat+/api/agent(JSON + streamdone) and carry a bounded usage summary to host → bridgeGET /api/sandboxesoptions+active(already non-secret) / B: new projection/api/sandboxes; slot showsbackend/name/idfrom that projectionprojectOptionalready excludesbase_url/token; no new identity surface is neededSTATUS_PROBE_MIN_INTERVAL_MScap; per-instance best-effort on Vercel serverless (not a global lock), primary throttle is the host cadenceLayer placement
native/harness/src/ui.zigheader band + newstatus.zignative/harness/src/bridge.zig↔lib/harnessBridge.tsRunAgentResult.cwd/activeSandboxId,/api/sandboxesidentity, usage)app/harness/HarnessHost.tsx,lib/harnessChat.tsapp/api/harness/status/route.ts,lib/agent/statusProbe.tslib/chatServer.ts,lib/agent/runAgent.ts,lib/agent/agentStream.ts,lib/agentApi.tsCurrent baseline (live code)
native/harness/src/ui.zigfn frame()(749),header_h = TOUCH_H+24(778); lifecycle + build-id + model label + Next rendered in the bandlib/harnessBridge.tssetModelCatalog→inv_push_model_catalog_entry(521/528)lib/harnessBridge.ts:13HARNESS_PROTOCOL_VERSION=12;native/harness/src/bridge.zig:20PROTOCOL_VERSION=12RunAgentResultalready carriescwd,sandboxId,activeSandboxIdlib/agent/runAgent.tsRunAgentResult(cwd ~150, sandboxId ~152, activeSandboxId ~161); set on generate+stream pathsactiveSandboxIdapp/api/agent/route.ts(~431-437 envelope read),lib/harnessChat.tssuccess reconcile foldsagentResult.activeSandboxId ?? sandboxIdapp/api/sandboxes/route.tsprojectOption(id/name/slug/backend/status/image/canRead/canWrite/usable/granted) +active.tools;?sandboxId=Redis-safe carry viaparseQuerySandboxId(line 32-54, 87)lib/tenancy/resolveSandbox.tsresolveAgentSandbox(150) +ResolvedAgentSandbox(39); DI-root bound client factories inlib/di/index.tsusageplumbing anywhere yetusage/totalUsageacrosslib/agent/*,lib/chatServer.ts,/api/{chat,agent}→ nonerev-parse/git branch/workspaceProbeacross lib/app → nonemaxContext/token-limitlib/gateway/byokProviders.ts//api/models— no max-context field% of windowonly ever degrades to absolute tokenslogicalCwd+activeSandboxIdAGENTS.mdsession-carrier row;lib/sessionStore.ts/lib/sessionCloudCaps.tsmm:ssBusy chip + lifecycle chipapp/harness/HarnessHost.tsx(busy/setLifecycle,thinking · ${formatElapsedSeconds(...)}),lib/elapsedTime.tsDesign
Wasm status strip (phase 1). Extend the header band with a right-aligned
horizontal pack of text slots rendered from additive bridge state:
sandbox(backend/short id) ·cwd·git(branch@sha+ optional*dirty)·
context(N tok). Palette only: TEAL default, WARM for git-dirty/busy,EMBER only for genuine errors (sandbox down / out-of-date). Each slot
truncates with
…+ tooltip/expand-on-tap; slots drop in priority order whenthe viewport is narrow (~390px: git first, then cwd, then context — sandbox is
last to hide). The existing primary header controls (lifecycle + model label +
Next) are never in the drop pool — status slots yield before any of those, soprimary-action geometry stays stable on narrow canvases. Missing value → muted
—/hidden, never a broken layout or h-gutter.Additive bridge v13. Add read/write state exports (e.g.
inv_set_status_slot,inv_status_slot_len/copy) for each inbridge.zig+harnessBridge.ts. Oldexports untouched; host clears slots on
bridge.reset()/ Clear / New session.Host fold. On hydrate and after each successful turn, the host
(1) resolves sandbox identity from the
/api/sandboxesresponse it alreadyfetches, (2) copies
RunAgentResult.cwdand the effectiveactiveSandboxId,(3) pushes them into the Wasm status slots.
SessionPicker/Clear resets slots.Context slot updated when a usage summary is present (phase 3).
Git probe (phase 2). New session/user-gated
GET /api/harness/status:requireSessionUser+ read the caller envelopemeta.activeSandboxIdvia theexisting
resolveSessionStore → isEnvelopeStore → readEnvelopeseam (this envelopewins over any
?sandboxId=carry, per the B1/#535 precedence), resolve the bind viaresolveAgentSandbox, then run a bounded, argv-only, read-onlygit rev-parse --abbrev-ref HEAD+git rev-parse --short HEAD+ optionalgit status --porcelain(dirty flag) through the resolved bind client. Output istruncated to a cap; a non-git repo / no bind / exec error → empty slots (fail
soft). Server-side min-interval rate limit (
STATUS_PROBE_MIN_INTERVAL_MS) as aper-instance best-effort clock (primary throttle is the host cadence; the cap
blocks single-path hot loops — return cached last value, never 429-spam, never
exec when limited).
Host calls after turns/cwd changes and on a cadence; not every frame.
Context/usage (phase 3, implements #327). Capture AI SDK
usage(prompt/completion/total, cached when present) at completion time on
/api/chatand
/api/agent(JSON result + stream final chunk). Carry a small boundedusagesummary onRunAgentResult/AgentSuccess/ the streamdoneevent;host folds it into the context slot (and may mirror). Missing usage → hide the
slot or mark estimated — never present client math as API truth. Absolute
tokens only;
% of windowonly when model max-context is known from catalog, elseno fake denominator (the catalog currently exposes no max-context, so v1 is
absolute-only by default).
Edge cases: empty session (no envelope) → sandbox
—; Wasm load fail → hostchips still work (no regression); API 4xx/5xx → probe/usage absent, slots mute;
refresh/restore → slots reseed from envelope +
/api/sandboxes; mobile ~390px →slot priority ordering; Clear/new session →
bridge.reset()clears slots.Cloud ops path
N/A — no Production mutate. New
GET /api/harness/statusis read-only againstthe resolved bind; no schema/DB/env/deploy change. No GHA; no
workflow_dispatch.(Note the git-probe route reuses existing DI-bound sandbox clients — no new
runner/env.)
Living docs plan
docs/feature-divide.mddocs/harness-limits.mddocs/sandbox.mddocs/session-model.mdmeta.{logicalCwd,activeSandboxId}+/api/sandboxeson restoredocs/agent-stream.mdusagesummary ondone/JSON resultAGENTS.mdREADME.mdImplementation order
Per-phase branches off
main, each a non-merged PR, landed in order:Each phase builds on the prior (phase 2/3 depend on phase 1's slot bridge).
Testing
lib/harnessBridge.test.tsround-trip; wasm-int loads v13harness.wasmsmokelib/harnessChat.test.tslib/agent/statusProbe.test.ts(store-fakes)/api/harness/statusauth edge + envelope read + resolve; envelope beats?sandboxId=carryapp/api/harness/status/route.test.tsdonerunAgent,chatServer,agentStream,agentApitestsnpm test(= di-gate + vitest run directly),npm run typecheck,npm run build,build-harnesson runnerDefinition of done
usage(or hides/estimates) — harness: show live context size from model usage (not client math) #327 implementedRisks & mitigations
STATUS_PROBE_MIN_INTERVAL_MScap (per-instance best-effort) + host cadence as primary throttle (see Caps)meta.activeSandboxIdwins;?sandboxId=is a Redis-safe carry onlyNext; those are never in the drop poolCaps table
STATUS_PROBE_MIN_INTERVAL_MS(NEW)lib/sessionCloudCaps.tsSTATUS_SLOT_MAX_BYTES(NEW)…; far under argv/bridge ceilingslib/sessionCloudCaps.ts+bridge.zigSTATUS_GIT_PROBE_OUT_MAX_BYTES(NEW)lib/agent/statusProbe.tsUSAGE_SUMMARY_MAX_BYTES(NEW)lib/agent/agentStream.ts+lib/agentApi.tsAll caps are NEW (not changes to existing caps), generous by default vs the
transport ceiling of their carriers (bridge state, argv, Runtime.request body/SSE
event). No change to any existing cap → no human cap-approval gate for this
plan.
Open questions
None — all in-scope decisions locked above. (#328's only non-goal — operator-triggered
sandbox switch — remains covered by the shipped
meta.sandbox_switch, not this bar.)Phase issues
Phase map
References
docs/feature-divide.md,docs/harness-limits.md,docs/sandbox.md,docs/session-model.md