Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/dsh-plugin-browserskill/src/lazy-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,23 @@ export function armLazyTools(ctx: Context, registerSuite: () => () => void): ()

// Live gesture/append feed: covers /browser-skill user gestures (no tool
// call happens on that path) landing as skill-invocation messages.
const onSessionEvent = (_session: SessionLike, event: SessionEventLike): void => {
if (isSkillInvocationMessage(event?.data)) ensureSuite();
const onSessionEvent = (session: SessionLike, event: SessionEventLike): void => {
if (suiteDisposer !== undefined) return;
if (isSkillInvocationMessage(event?.data)) {
ensureSuite();
return;
}
// Reload recovery. The history scan below runs once, when the plugin is
// applied, and it covers neither of the reload cases: a session that
// already exists never emits `session/created`, and `ctx.get("sessions")`
// yields nothing when the sessions service is registered after this
// plugin. The suite then stays hidden until the model happens to invoke
// the skill again, which surfaces as `unknown tool "browser_session"`.
//
// The session is already in hand on every event, so let any later event
// re-derive the reveal from durable history. Guarded above, so this scans
// only while still hidden.
if (session !== undefined && session !== null) scanSession(session);
};
disposers.push(ctx.on("session/event" as never, onSessionEvent as never));

Expand Down
59 changes: 59 additions & 0 deletions packages/dsh-plugin-browserskill/tests/lazy-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,65 @@ describe("armLazyTools", () => {
expect(missRegister).not.toHaveBeenCalled();
});

it("re-arms from history on a later event when the boot scan could not see the session", () => {
const hitEvents = [
{
type: "tool/call",
data: { callId: "c1", name: "skill", arguments: '{"name":"browser-skill"}' },
},
{ type: "tool/result", data: { message: { callId: "c1", isError: false } } },
];
// After a plugin reload the sessions service may not be registered yet, so
// ctx.get("sessions") yields nothing and the boot scan covers nothing; the
// session already exists, so session/created never fires for it either.
const { ctx, listeners } = fakeEventCtx();
const registerSuite = vi.fn(() => () => {});
armLazyTools(ctx, registerSuite);
expect(registerSuite).not.toHaveBeenCalled();

// An ordinary event carrying no invocation of its own still hands over the
// session, whose durable history proves the skill already ran.
callListeners(listeners, "session/event", { events: hitEvents }, { type: "message/append" });
expect(registerSuite).toHaveBeenCalledTimes(1);
});

it("does not reveal from an event whose session has no proof", () => {
const { ctx, listeners } = fakeEventCtx();
const registerSuite = vi.fn(() => () => {});
armLazyTools(ctx, registerSuite);
callListeners(
listeners,
"session/event",
{ events: [{ type: "message/append", data: {} }] },
{ type: "message/append" },
);
expect(registerSuite).not.toHaveBeenCalled();
});

it("stops scanning session history once the suite is revealed", () => {
const { ctx, listeners } = fakeEventCtx();
const registerSuite = vi.fn(() => () => {});
armLazyTools(ctx, registerSuite);
callListeners(
listeners,
"tools/result",
{ name: "skill", arguments: { name: "browser-skill" } },
{ isError: false },
);
expect(registerSuite).toHaveBeenCalledTimes(1);

// Events arrive constantly; re-deriving from history on each one after the
// reveal would be pure waste. A session that would throw if read proves
// the history is not touched again.
const exploding = {
get events(): never {
throw new Error("session history must not be read after the reveal");
},
};
callListeners(listeners, "session/event", exploding, { type: "message/append" });
expect(registerSuite).toHaveBeenCalledTimes(1);
});

it("disposes listeners and the revealed suite", () => {
const { ctx, listeners } = fakeEventCtx();
const suiteDispose = vi.fn();
Expand Down