feat(sessions): add daemon notification inbox protocol

This commit is contained in:
Federico Jaramillo Martinez
2026-07-19 01:28:45 +02:00
parent d77ca66f28
commit 6e09df8329
21 changed files with 2290 additions and 59 deletions
@@ -1,12 +1,13 @@
import { ModelRuntime, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { InMemoryCredentialStore, type Credential, type CredentialStore } from "@earendil-works/pi-ai";
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
import type { GlobalSessionEvent, SessionNotificationSummaryEvent, SessionUiEvent } from "../../shared/apiTypes.js";
import { SessionEventHub } from "../realtime/sessionEventHub.js";
import type { PiAgentSession, PiSessionManager, PiSessionRuntime, PiSessionServiceDependencies } from "./piSessionService.js";
export class CapturingSessionEventHub extends SessionEventHub {
readonly sessionEvents: { sessionId: string; event: SessionUiEvent }[] = [];
readonly globalEvents: GlobalSessionEvent[] = [];
readonly notificationSummaryEvents: SessionNotificationSummaryEvent[] = [];
private readonly seqBySessionOverride = new Map<string, number>();
override publish(sessionId: string, event: SessionUiEvent): void {
@@ -17,6 +18,10 @@ export class CapturingSessionEventHub extends SessionEventHub {
this.globalEvents.push(event);
}
override publishNotificationSummary(event: SessionNotificationSummaryEvent): void {
this.notificationSummaryEvents.push(event);
}
/** Test seam: set the per-session watermark returned by {@link currentSeq}. */
setSeq(sessionId: string, value: number): void {
this.seqBySessionOverride.set(sessionId, value);
@@ -30,6 +35,8 @@ export class CapturingSessionEventHub extends SessionEventHub {
export type SessionGateway = NonNullable<PiSessionServiceDependencies["sessionManager"]>;
export type RuntimeCreator = NonNullable<PiSessionServiceDependencies["createAgentRuntime"]>;
type TestExtensionBindings = Parameters<PiAgentSession["bindExtensions"]>[0];
export interface TestSession extends PiAgentSession {
sessionName: string | undefined;
model: PiAgentSession["model"];
@@ -131,6 +138,7 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
const customMessageCalls: { message: { customType: string; content: string; display: boolean; details?: unknown }; options: unknown }[] = [];
const bindExtensionCalls: unknown[] = [];
const listeners: ((event: unknown) => void)[] = [];
let extensionUiContext = testExtensionUiContext;
const calls = { abort: 0, bindExtensions: bindExtensionCalls, clearQueue: 0, dispose: 0, prompt: promptCalls, reload: 0, sendCustomMessage: customMessageCalls };
const session: TestSession = {
sessionId,
@@ -150,7 +158,8 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
scopedModels: [],
extensionRunner: {
getRegisteredCommands: () => [],
getUIContext: () => testExtensionUiContext,
getUIContext: () => extensionUiContext,
setUIContext: (uiContext) => { extensionUiContext = uiContext ?? testExtensionUiContext; },
},
promptTemplates: [],
resourceLoader: { getSkills: () => ({ skills: [] }) },
@@ -161,8 +170,9 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
if (index !== -1) listeners.splice(index, 1);
};
},
bindExtensions: (bindings: unknown) => {
bindExtensions: (bindings: TestExtensionBindings) => {
calls.bindExtensions.push(bindings);
if (bindings.uiContext !== undefined) extensionUiContext = bindings.uiContext;
return Promise.resolve();
},
getSessionStats: () => ({ sessionId, totalMessages: 0, userMessages: 0, assistantMessages: 0, toolCalls: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, cost: 0 }),