/** * subagent-registry.ts — In-process subagent session registry. * * In-process subagent extensions (e.g. `@gotgenes/pi-subagents`) register * each child session here before calling `bindExtensions()` so that * `isSubagentExecutionContext()` and permission-forwarding target resolution * can detect them without relying on environment variables or filesystem * heuristics. * * The registry is keyed by the child's **session id**, which is unique per * child and available to both producer (via `sessionManager.getSessionId()` * after `newSession()` in `create-subagent-session.ts`) and consumer (via * `ctx.sessionManager.getSessionId()`). Two concurrent siblings of the same * parent therefore occupy distinct keys, so one sibling's `disposed` event * cannot evict the entry the others depend on. * * The single registry instance is stored on `globalThis` (via `Symbol.for()`) * so that the parent's permission-system instance (which registers children * on the parent's event bus) and each child's separate jiti instance (which * reads the registry to detect itself and resolve its forwarding target) share * one store across per-session event buses. See `getSubagentSessionRegistry()`. * * When a future code path needs the child's agent name, read it from * `tcc.agentName` (resolved from the `` system-prompt tag) — * not from this registry. */ /** Process-global key for the shared registry slot. */ const SUBAGENT_SESSION_REGISTRY_KEY = Symbol.for( "@gotgenes/pi-permission-system:subagent-registry", ); /** * Return the process-global SubagentSessionRegistry, creating it on first call. * * Backed by `globalThis` + `Symbol.for()` so the parent's permission-system * instance (which registers children on the parent event bus) and each child's * separate jiti instance (which reads the registry to detect itself and resolve * its forwarding target) share one store across per-session event buses. * * Intentionally has no shutdown/unpublish hook — a child's `session_shutdown` * must not be able to wipe the parent's registrations. Entries are added and * removed exclusively by the parent's `subagents:child:session-created` / * `subagents:child:disposed` subscription. */ export function getSubagentSessionRegistry(): SubagentSessionRegistry { const store = globalThis as Record; const existing = store[SUBAGENT_SESSION_REGISTRY_KEY] as | SubagentSessionRegistry | undefined; if (existing) { return existing; } const registry = new SubagentSessionRegistry(); store[SUBAGENT_SESSION_REGISTRY_KEY] = registry; return registry; } /** Signal stored per registered in-process subagent session. */ export interface SubagentSessionInfo { /** Parent session ID for permission forwarding. Omit when unknown. */ parentSessionId?: string; } /** * Registry of active in-process subagent sessions. * * A process-global singleton — obtain it via `getSubagentSessionRegistry()`, * never `new` (see that accessor for why). Written exclusively by * `subscribeSubagentLifecycle` via the `subagents:child:session-created` / * `subagents:child:disposed` event subscription (ADR 0002 — the core * publishes, consumers observe). * * Keyed by child session id. Each concurrent child of the same parent receives * a unique session id from `sessionManager.newSession()`, so siblings occupy * distinct keys and one sibling's `disposed` cannot evict another's entry. */ export class SubagentSessionRegistry { private readonly sessions = new Map(); /** * Register an in-process subagent session. * * If a previous entry exists for `sessionId`, it is overwritten * (last-write-wins; single-writer expected per key). */ register(sessionId: string, info: SubagentSessionInfo): void { this.sessions.set(sessionId, info); } /** Remove a previously registered session. No-op if the key is absent. */ unregister(sessionId: string): void { this.sessions.delete(sessionId); } /** Return the registered info for `sessionId`, or `undefined` if absent. */ get(sessionId: string): SubagentSessionInfo | undefined { return this.sessions.get(sessionId); } /** Return `true` when `sessionId` has a registered entry. */ has(sessionId: string): boolean { return this.sessions.has(sessionId); } }