feat(dsh-plugin-browserskill): host-side ObservationService for the PiP overlay (Phase 1-A)

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.
This commit is contained in:
BB-fat
2026-08-14 04:59:26 +00:00
parent 3d050e0f26
commit a7bc8bacf6
8 changed files with 989 additions and 31 deletions
+31 -1
View File
@@ -12,6 +12,8 @@
import type { Context } from "@deepseek-ai/cordis";
import Schema from "@deepseek-ai/schemastery";
import { ObservationService } from "./observation";
import { registerObservationRoutes } from "./observation-http";
import { type BskRunner, createBskRunner } from "./runner";
import { SessionRegistry } from "./sessions";
import { type PluginConfig, registerTools } from "./tools";
@@ -30,6 +32,15 @@ export const Config = Schema.object({
maxSessions: Schema.number()
.default(5)
.description("Maximum number of concurrent browser sessions started through this plugin."),
observationEnabled: Schema.boolean()
.default(true)
.description("Track per-session observation state (action/url/thumbnail) for the PiP overlay."),
thumbnailIntervalMs: Schema.number()
.default(1500)
.description("Thumbnail refresh cadence for active sessions (milliseconds)."),
idleIntervalMs: Schema.number()
.default(8000)
.description("Thumbnail refresh cadence for idle sessions; also the recent-activity window."),
});
export type Config = PluginConfig;
@@ -48,11 +59,25 @@ export function apply(
bskPath: config.bskPath ?? "bsk",
defaultTimeoutMs: config.defaultTimeoutMs ?? 120_000,
maxSessions: config.maxSessions ?? 5,
observationEnabled: config.observationEnabled ?? true,
thumbnailIntervalMs: config.thumbnailIntervalMs ?? 1500,
idleIntervalMs: config.idleIntervalMs ?? 8000,
};
const runner = options.runnerFactory?.(resolved.bskPath) ?? createBskRunner(resolved.bskPath);
const registry = new SessionRegistry(resolved.maxSessions);
const observation = new ObservationService({
ctx,
runner,
registry,
options: {
enabled: resolved.observationEnabled,
thumbnailIntervalMs: resolved.thumbnailIntervalMs,
idleIntervalMs: resolved.idleIntervalMs,
},
});
registerTools({ ctx, runner, registry, config: resolved });
registerTools({ ctx, runner, registry, config: resolved, observation });
const removeRoutes = registerObservationRoutes(ctx, observation);
// Non-blocking install probe: warn early when bsk is missing instead of
// failing the first tool call with a bare spawn error.
@@ -73,6 +98,8 @@ export function apply(
// are swallowed so one stale handle cannot abort the rest.
ctx.effect(() => {
return () => {
removeRoutes();
observation.dispose();
runner.killAll();
const stops = registry
.ownedIds()
@@ -84,6 +111,9 @@ export function apply(
});
}
export type { ObservationEvent, ObservationOptions, SessionObservation } from "./observation";
export { ObservationService } from "./observation";
export { registerObservationRoutes } from "./observation-http";
export type { BskRunner, BskRunResult, SpawnImpl } from "./runner";
export { BskError, createBskRunner } from "./runner";
export { SessionRegistry } from "./sessions";