Progressive disclosure, final stage: with lazyTools on (default), the eleven
browser_* tool schemas stay out of the system prompt — the browser-skill
catalog entry is the sole advertisement. One successful skill invocation
(model tool call via tools/result, or a /browser-skill user gesture via the
session append feed) registers the suite for the process lifetime, idempotent
on repeats; session resume is covered by scanning durable events for a past
tool/call+tool/result pair (or skill-invocation message) on session entry and
at apply time. lazyTools: false keeps the legacy always-on registration.
Investigation (recorded in the ticket): tools/result carries normalized
name/arguments with an isError discriminant; dsh 0.1 has no official global
visibility switch (ctx.tools.restrict is agent-scoped and throws from a plain
context), making conditional registration the intended pattern; per-step
assembly picks the new suite up on the next step, and visibility changes are
first-class inputs of tool-skill's catalog digest. registerTools now returns
the combined disposer via a prototype-preserving tracking overlay; the skill
prelude tells the model the browser_* tools unlock on skill load.
Tests: +8 (hidden-before/reveal/idempotent/gesture/history hit+miss/dispose/
scanner pairing/two-state apply wiring). 124/124 green.
ctx.get('webServer') at apply time raced the web composition's service
registration (the fallback SPA then answered /bsk-observation/* with
index.html). Ride ctx.inject(['webServer'], …) instead: the callback
runs when the service is provided and never runs in headless
compositions, keeping route registration timing-safe and optional.
Per-session live observation for owned sessions only, feeding the
upcoming client overlay:
- State model: SessionObservation (sessionId/url/action/since/
thumbnailAttachmentId/lastError), one entry per owned session;
added on start, removed on stop, cleared on dispose.
- Instrumentation: the shared runBsk wrapper begins/ends an action
(tool name mapped to a verb) around every model-facing call and tags
the child with its session id; action end triggers an immediate
thumbnail refresh.
- Thumbnail loop: 1.5s cadence while active, 8s idle downclock, x3
failure backoff, silent frame retention on errors; captures run
OUTSIDE the instrumentation (no action events, no current-pointer
movement) and land in the attachment store by reference.
- Interrupt: runner.killFor(tag) kills exactly the tagged in-flight bsk
children — chat-Stop-equivalent semantics for the current or a named
owned session; foreign ids and the no-in-flight case return false.
- Remote seam: dsh 0.1's Typert Remote pipeline and forwarded-event
allowlist are closed to out-of-tree packages, so state/events/
interrupt are served over the documented webServer route seam:
GET /bsk-observation/state, GET /bsk-observation/events (SSE),
POST /bsk-observation/interrupt; routes mount only when a webServer
service exists (headless compositions skip them).
- Config: observationEnabled (true), thumbnailIntervalMs (1500),
idleIntervalMs (8000).
Tests (+13, 72 total): state machine + event stream, cadence fast/idle/
backoff, headless no-store silence, observation-traffic isolation,
interrupt routing (default/specified/none/foreign), HTTP route
handlers, disabled-webServer no-op.
Tighten the shared-daemon boundary from 'cleanup only touches owned
sessions' to 'the plugin only ever sees and operates on sessions it
created':
- browser_session_list no longer queries the daemon at all; it returns
the plugin's own session table (with the current marker), so foreign
sessions cannot even be enumerated through the plugin.
- Every tool's optional session argument must name a plugin-created
session; foreign/unknown ids are rejected with a clear error before
any command reaches the daemon (the reference-adoption channel is
gone).
- Unchanged: dispose stops exactly the owned set, stop requires
ownership, the start reservation protocol, and handle-precise child
kills.
Tests updated (59): explicit-foreign rejection without daemon contact,
registry-only listing, reservation race, stale-handle-tolerant dispose.
e2e: a manually started foreign session was rejected on reference,
absent from session_list, refused on stop, and survived dispose while
both plugin sessions were cleaned up.
Review hardening for daemon-sharing deployments (other agents,
terminals, or dsh instances on the same bsk daemon):
- Ownership split: only sessions created by browser_session_start are
'owned'. Explicitly referenced foreign sessions are still tracked for
the current-session pointer, but browser_session_stop refuses them
and unload cleanup stops exactly the owned set — no path can stop a
session this plugin did not create.
- Start race: the capacity check is now a synchronous
reserveStart/completeStart/abandonStart protocol, so two concurrent
starts can never both pass the cap (check-and-reserve is atomic on
the event loop); a rejected or failed start never leaks a session.
- Stale handles: dispose already swallowed per-stop errors; the owned
set is computed at dispose time so an externally stopped session is a
no-op, not a failure.
Tests (+7): reservation race (two concurrent starts, cap 1 -> exactly
one spawn), foreign-stop refusal (no stop command reaches the daemon),
reference adoption never becoming owned, resolveForStop pointer
stability, and an apply-level dispose test proving only owned sessions
are stopped while stale stops are tolerated. e2e: a manually started
foreign session survived the plugin's full lifecycle while both plugin
sessions were cleaned up.