/** * pi-c2c — native c2c integration for the pi coding agent. * * c2c (https://github.com/anomalyco/c2c) is a peer-to-peer messaging broker * between AI coding sessions. This extension makes a pi session a first-class * c2c peer: * * - registers a c2c identity (alias) on `session_start` — self-registered, * no `c2c start` supervisor required; * - exposes c2c send/list/room **tools** the LLM can call; * - exposes `/c2c-*` **slash commands** for the human at the keyboard; * - runs a background **auto-delivery poller** that injects inbound c2c * messages straight into pi's transcript via `pi.sendMessage` — the * native win an MCP polling client can't do. * * Integration is CLI-based: every broker interaction shells out to the `c2c` * binary via `pi.exec` (the same pattern as the c2c OpenCode plugin). No * c2c-side changes are required. * * Architecture: this file is the orchestrator. It owns the closure-scoped * mutable state, lifecycle hooks, and wires together focused modules: * - tools.ts — LLM-callable tool registrations (ToolCtx interface) * - commands.ts — slash-command registrations (CommandCtx interface) * - poller.ts — background drain/inject pipeline (PollerDeps interface) * - common.ts — shared constants and utilities */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { C2cCli, type C2cMessage, type ExecFn, resolveSessionsBrokerRoot } from "./c2c-cli.ts"; import { resolveC2cCommand } from "./c2c-bin.ts"; import { establishIdentity, type Identity } from "./identity.ts"; import { identityWithAlias, refreshAliasFromBroker, registerAliasForIdentity, type AliasUpdateResult, type AliasUpdateSuccess, } from "./alias.ts"; import { DeliveryDedup } from "./delivery.ts"; import { clearSpool, gcStaleSpools, readSpool, writeSpool } from "./spool.ts"; import { formatStatus, formatDisabledStatus, installStatusColorPatch, type PiC2cBarState } from "./status.ts"; import { collectDebugState } from "./debug.ts"; import { computeHostHash, deriveRelayAlias } from "./relay.ts"; import { drainAllSources } from "./routing.ts"; import { BrokerWatcher, startPerRepoWatcher, startSessionsWatcher } from "./broker-watcher.ts"; import { resolvePerRepoBrokerRoot, type PerRepoBrokerRoot } from "./broker-root.ts"; import { RelayWatcher, type RelayWatcherState } from "./relay-watcher.ts"; import { PeerStatusStore } from "./peer-status.ts"; import { createStatusTracker, type StatusTracker } from "./status-sync.ts"; import { createPiStatefileWriter, type PiStatefileWriter, } from "./statefile-writer.ts"; import { registerC2cMessageRenderer } from "./ui/compact-message.ts"; import { registerSubagentRegistrationRenderer } from "./ui/compact-subagent-registration.ts"; import { createLiveTelemetry, type LiveTelemetry } from "./telemetry.ts"; import { appendSubagentPromptContext, buildRegistrationMessageArgs, getParentAlias, notifySubagentRegistered, observeSubagentRegistrations, observeSubagentRegistrationsFor, readSubagentLoadHint, setParentAlias, } from "./subagent.ts"; import type { LocalInfoToolDetails } from "./ui/tool-renderers.ts"; import { PI_C2C_VERSION, SPOOL_DIR, SPOOL_TTL_MS, readAutoJoinRooms, readPollInterval, readStatusInterval, relayToC2c, } from "./common.ts"; import { registerTools, type ToolCtx } from "./tools.ts"; import { registerCommands, type CommandCtx } from "./commands.ts"; import { pollTick as doPollTick, type PollerDeps } from "./poller.ts"; // Re-export for backward compat (tests import these from index.ts) export { relayToC2c, formatDebugTable } from "./common.ts"; export { PI_C2C_VERSION }; export interface C2cExtensionDeps { createStatefileWriter?: typeof createPiStatefileWriter; resolveC2cCommand?: typeof resolveC2cCommand; } const STATUS_KEY = "c2c"; const SESSION_ENV = "C2C_MCP_SESSION_ID"; /** * Process-global state that must survive the extension factory being * re-invoked on an in-process session switch (reload / new / resume / fork). */ interface PiC2cGlobal { hostSessionEnvCaptured: boolean; hostSessionEnv: string | undefined; prevSessionId?: string; prevSessionByScope?: Record; } function gstate(): PiC2cGlobal { const g = globalThis as { __c2cPiState?: PiC2cGlobal }; if (!g.__c2cPiState) { g.__c2cPiState = { hostSessionEnvCaptured: false, hostSessionEnv: undefined, prevSessionByScope: {}, }; } return g.__c2cPiState; } export default function c2cExtension(pi: ExtensionAPI, deps: C2cExtensionDeps = {}): void { const makeStatefileWriter = deps.createStatefileWriter ?? createPiStatefileWriter; const resolveStatefileCommand = deps.resolveC2cCommand ?? resolveC2cCommand; const subagentHint = readSubagentLoadHint(); const subagentParentAliasAtLoad = getParentAlias(); registerC2cMessageRenderer(pi); registerSubagentRegistrationRenderer(pi); // --- per-session mutable state (closure-scoped) --------------------------- const barState: PiC2cBarState = {}; let cli: C2cCli | null = null; let identity: Identity | null = null; let registered = false; let registerError: string | undefined; let ctxRef: ExtensionContext | null = null; let perRepoWatcher: BrokerWatcher | null = null; let perRepoBrokerRoot: PerRepoBrokerRoot | undefined; let sessionsWatcher: BrokerWatcher | null = null; let relayWatcher: RelayWatcher | null = null; let relayWsState: RelayWatcherState | undefined; let pollTimer: ReturnType | null = null; let aliasSyncTimer: ReturnType | null = null; const SAFETY_NET_POLL_MS = 60_000; let shuttingDown = false; let c2cEnabled = (process.env.C2C_PI_ENABLED ?? "1") !== "0"; const dedup = new DeliveryDedup(); const pollIntervalMs = readPollInterval(); let statusTracker: StatusTracker | null = null; let statefileWriter: PiStatefileWriter | null = null; const peerStatusStore = new PeerStatusStore(); const telemetry: LiveTelemetry = createLiveTelemetry(); const crossRepoEnabled = (process.env.C2C_PI_CROSS_REPO ?? "1") !== "0"; const sessionsBrokerRoot = crossRepoEnabled ? resolveSessionsBrokerRoot() : undefined; let crossRepoSessionsRegistered = false; let crossRepoSessionsError: string | undefined; const relayEnabled = (process.env.C2C_PI_RELAY ?? "1") !== "0"; let relayRegistered = false; let relayAddress: string | undefined; let relayError: string | undefined; let relayHostId: string | undefined; let relayHostIdVerified: boolean = false; let queuedSinceMs: number | undefined; let stopSubagentObserver: (() => void) | null = null; let stopSelfObserver: (() => void) | null = null; let relayRegistrationInFlight: Promise | null = null; const safetyNetPollMs = Math.max( 10, Number.parseInt(process.env.C2C_PI_SAFETY_NET_POLL_MS ?? `${SAFETY_NET_POLL_MS}`, 10) || SAFETY_NET_POLL_MS, ); function spoolScopeKey(id: Identity): string { if (!subagentHint) return "parent"; return `subagent:${subagentHint.agentId ?? id.sessionId}`; } // --- drain serialization -------------------------------------------------- let drainChain: Promise = Promise.resolve(); function serializeDrain(fn: () => Promise): Promise { const run = drainChain.then(fn, fn); drainChain = run.then(() => undefined, () => undefined); return run; } // --- drain all three sources (for c2c_pi_poll_inbox tool) ----------------- async function drainAll(): Promise { return drainAllSources(cli!, { sessionsBrokerRoot, relayRegistered: relayRegistered && !!relayAddress, relayAddress, relayToC2c, }); } // --- poller deps (shared by the background poller) ------------------------ const pollerDeps: PollerDeps = { getCli: () => cli, getIdentity: () => identity, isShuttingDown: () => shuttingDown, sessionsBrokerRoot, isRelayRegistered: () => relayRegistered, getRelayAddress: () => relayAddress, dedup, peerStatusStore, telemetry, sendMessage: (args, opts) => pi.sendMessage(args, opts), getQueuedSinceMs: () => queuedSinceMs, setQueuedSinceMs: (v) => { queuedSinceMs = v; }, }; async function pollTick(): Promise { if (!cli || !identity || shuttingDown) return; await syncAliasFromBrokerState(); if (relayEnabled && !relayRegistered) await registerRelayForAlias(identity.alias); await serializeDrain(() => doPollTick(pollerDeps)); } // --- shared helpers used by ToolCtx and CommandCtx ------------------------ function buildDebugState(): string { return collectDebugState({ version: PI_C2C_VERSION, identity, registered, registerError, ctxRef, barState, pollIntervalMs, hostSessionEnv: gstate().hostSessionEnv, prevSessionId: gstate().prevSessionId, autoJoinRooms: readAutoJoinRooms(), piBarPatched: Boolean((globalThis as Record).__piC2cStatusFgPatched), spoolDir: SPOOL_DIR, pid: process.pid, cwdFallback: process.cwd(), env: process.env, crossRepoEnabled, sessionsBrokerRoot, crossRepoSessionsRegistered, crossRepoSessionsError, relayEnabled, relayRegistered, relayAddress, relayHostId, relayHostIdVerified, relayError, relayWsState, peerStatusCount: peerStatusStore.size(), peerStatusSample: peerStatusStore.live().slice(0, 5).map(({ alias, entry }) => ({ alias, state: entry.state, since: entry.since, ttlMs: entry.ttlMs, })), queuedSinceMs, perRepoBrokerRoot: perRepoBrokerRoot?.root, perRepoBrokerSource: perRepoBrokerRoot?.source, statefileInstanceKey: statefileWriter?.instanceKey, }); } async function buildLocalInfoText(): Promise { const alias = identity?.alias ?? "(not registered)"; const sessionId = identity?.sessionId ?? "(none)"; const hostHash = relayEnabled ? computeHostHash() : undefined; const c2cVersion = cli ? await cli.c2cVersion().catch(() => "unknown") : "unknown"; const addr = relayRegistered ? relayAddress ?? "---" : "---"; const xrepo = crossRepoEnabled ? crossRepoSessionsRegistered ? "connected" : crossRepoSessionsError ? `error: ${crossRepoSessionsError}` : "not connected" : "disabled"; const relay = !relayEnabled ? "disabled (C2C_PI_RELAY=0)" : relayRegistered ? "connected" : relayError ? `error: ${relayError}` : "not connected"; const lines = [ "c2c local info", "─".repeat(36), ` pi-c2c ${PI_C2C_VERSION}`, ` c2c ${c2cVersion}`, ` alias ${alias}`, ` session ${sessionId}`, ` host_hash ${hostHash ?? "(n/a)"}`, ` address ${addr}`, "", ` broker ${registered ? "connected" : registerError ?? "not connected"}`, ` cross-repo ${xrepo}`, ` relay ${relay}`, ` relay_ws ${relayWsState ?? "---"}`, ` relay_host ${relayHostId ? `${relayHostId}${relayHostIdVerified ? "" : " (unverified)"}` : "---"}`, ` poll ${pollIntervalMs}ms`, ` statefile ${statefileWriter?.instanceKey ?? "---"}`, ]; if (statefileWriter) { lines.push(` inspect c2c statefile --instance ${statefileWriter.instanceKey} --json`); } if (relayRegistered && cli) { try { const peers = await cli.relayList(); if (peers.length > 0) { lines.push("", " relay peers"); for (const p of peers) lines.push(` ${p.alive ? "●" : "○"} ${p.alias}`); } } catch { /* best-effort */ } } return lines.join("\n"); } async function buildLocalInfoDetails(): Promise { const alias = identity?.alias ?? "(not registered)"; const c2cVersion = cli ? await cli.c2cVersion().catch(() => "unknown") : "unknown"; const sessionId = identity?.sessionId ?? "(none)"; const xrepo = crossRepoEnabled ? crossRepoSessionsRegistered ? "connected" : crossRepoSessionsError ? `error: ${crossRepoSessionsError}` : "not connected" : "disabled"; const relay = !relayEnabled ? "disabled" : relayRegistered ? "connected" : relayError ? `error: ${relayError}` : "not connected"; return { piC2cVersion: PI_C2C_VERSION, c2cVersion, alias, sessionId, broker: registered ? "connected" : registerError ?? "not connected", crossRepo: xrepo, relay, relayWsState: relayWsState ?? "---", relayHost: relayHostId ?? "---", relayHostVerified: relayHostId ? relayHostIdVerified : undefined, address: relayRegistered ? relayAddress : undefined, statefileInstanceKey: statefileWriter?.instanceKey, }; } function installStatusTrackerForAlias(alias: string): void { if (statusTracker) statusTracker.dispose(); statusTracker = createStatusTracker({ alias, minIntervalMs: readStatusInterval() }); statusTracker.setSink((envelope) => { if (!statefileWriter || shuttingDown) return; statefileWriter.record(envelope); }); } function clearRelayState(error?: string): void { if (relayWatcher) { relayWatcher.stop(); relayWatcher = null; } relayWsState = undefined; relayRegistered = false; relayAddress = undefined; relayHostId = undefined; relayHostIdVerified = false; relayError = error; } async function registerRelayForAlias(alias: string): Promise { if (!relayEnabled || !cli) return undefined; if (relayRegistrationInFlight) return relayRegistrationInFlight; // The in-flight promise must NEVER reject: concurrent callers dedup onto it // via the early return above and await it without their own try/catch (e.g. // the fire-and-forget `void pollTick()` triggers). A rejection leaking to // those consumers surfaces as an unhandledRejection → uncaughtException. // So handle failures inside the IIFE and resolve to `undefined` instead. relayRegistrationInFlight = (async () => { try { const relayUrl = (process.env.C2C_PI_RELAY_URL ?? "").trim() || "https://relay.c2c.im"; const relayIdentityCheck = await cli!.relayIdentity().catch(() => null); if (!relayIdentityCheck) { clearRelayState( "no relay identity; run `c2c relay identity init` to create one; " + "pi-c2c will retry on the next safety-net poll or when you run c2c_pi_local_info", ); return undefined; } const existing = await cli!.relaySetupShow().catch(() => null); if (!existing || !existing.url) await cli!.relaySetup({ url: relayUrl }); const hostHash = computeHostHash(); const rAlias = deriveRelayAlias(alias, hostHash); const reg = await cli!.relayRegister(rAlias, { relayUrl }); relayRegistered = true; relayAddress = rAlias; relayError = undefined; relayHostId = reg?.opaqueHostId ?? rAlias.split("@")[1]; relayHostIdVerified = relayHostId === hostHash; try { const peers = await cli!.relayList(); const self = peers.find((p) => p.alias === rAlias); if (self) { const suffix = self.opaqueHostId ?? self.alias.split("@")[1]; if (suffix && suffix !== hostHash) { relayHostIdVerified = false; relayError = `opaque_host_id mismatch: local=${hostHash} relay=${suffix}`; } else if (suffix === hostHash) { relayHostIdVerified = true; } } } catch { /* best-effort */ } if (relayWatcher) relayWatcher.stop(); relayWatcher = new RelayWatcher({ alias: rAlias, relayUrl, onChange: () => void pollTick(), onStateChange: (state) => { relayWsState = state; }, }); relayWatcher.start(); relayWsState = relayWatcher.state; return rAlias; } catch (e: unknown) { clearRelayState(e instanceof Error ? e.message : String(e)); return undefined; } })(); try { return await relayRegistrationInFlight; } finally { relayRegistrationInFlight = null; } } async function refreshRelayForAlias(alias: string): Promise { if (!relayEnabled || !relayRegistered || !cli) return; await registerRelayForAlias(alias); } async function applyAliasSuccess(result: AliasUpdateSuccess): Promise { identity = identity ? identityWithAlias(identity, result.alias) : { alias: result.alias, sessionId: result.sessionId }; registered = true; registerError = undefined; publishSessionEnv(result.sessionId); if (result.crossRepoRegistered !== undefined) crossRepoSessionsRegistered = result.crossRepoRegistered; if (result.crossRepoError !== undefined) crossRepoSessionsError = result.crossRepoError; else if (result.crossRepoRegistered) crossRepoSessionsError = undefined; barState.alias = result.alias; barState.registered = true; barState.reason = undefined; if (ctxRef) ctxRef.ui.setStatus(STATUS_KEY, formatStatus(result.alias, true, ctxRef.ui.theme)); if (!subagentHint) setParentAlias(result.alias); statefileWriter?.updateAlias(result.alias); await refreshRelayForAlias(result.alias); } async function setAlias(rawAlias: string): Promise { if (!cli) return { ok: false, error: "c2c: not registered yet" }; const result = await registerAliasForIdentity(cli, identity, rawAlias, { sessionsBrokerRoot }); if (result.ok) await applyAliasSuccess(result); return result; } async function syncAliasFromBrokerState(): Promise { if (!cli || !identity || !registered || shuttingDown) return null; const result = await refreshAliasFromBroker(cli, identity, { sessionsBrokerRoot }); if (result?.ok) await applyAliasSuccess(result); return result; } function startAliasSyncTimer(): void { if (aliasSyncTimer) return; aliasSyncTimer = setInterval(() => { void syncAliasFromBrokerState(); }, Math.max(1_000, pollIntervalMs)); } async function ensureRelayRegistered(ui: ExtensionContext["ui"]): Promise { if (!relayEnabled) { ui.notify("Relay is disabled. Set C2C_PI_RELAY=1 and restart to enable.", "info"); return undefined; } if (!identity || !cli) { ui.notify("c2c: not registered yet.", "warning"); return undefined; } const choice = await ui.select("Connect to public relay?", ["Connect now", "Cancel"]); if (!choice || choice === "Cancel") return undefined; const registeredAlias = await registerRelayForAlias(identity.alias); if (!registeredAlias) { ui.notify(`Relay registration failed: ${relayError ?? "unknown error"}`, "error"); return undefined; } return registeredAlias; } /** * Publish C2C_MCP_SESSION_ID into the process env so bare shell `c2c whoami` * / `c2c send` from the same pi process agree with extension tools. * * Parent sessions only: in-process subagents share process.env with the * parent, and C2cCli already injects the subagent session per `pi.exec`. * Publishing from a subagent would clobber the parent's shell identity and * clear would restore ambient (not parent) on subagent shutdown. */ let sessionEnvOwned = false; let sessionEnvPrevious: string | undefined; function publishSessionEnv(sessionId: string): void { if (subagentHint) return; if (!sessionEnvOwned) { sessionEnvPrevious = process.env[SESSION_ENV]; sessionEnvOwned = true; } process.env[SESSION_ENV] = sessionId; } function clearPublishedSessionEnv(): void { if (subagentHint || !sessionEnvOwned) return; if (sessionEnvPrevious === undefined) delete process.env[SESSION_ENV]; else process.env[SESSION_ENV] = sessionEnvPrevious; sessionEnvOwned = false; sessionEnvPrevious = undefined; } /** Stop background services (pollers, watchers, trackers). Does not clear identity. */ function stopBackgroundServices(): void { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (aliasSyncTimer) { clearInterval(aliasSyncTimer); aliasSyncTimer = null; } if (perRepoWatcher) { perRepoWatcher.stop(); perRepoWatcher = null; } if (sessionsWatcher) { sessionsWatcher.stop(); sessionsWatcher = null; } if (relayWatcher) { relayWatcher.stop(); relayWatcher = null; } relayWsState = undefined; if (statusTracker) { statusTracker.dispose(); statusTracker = null; } if (statefileWriter) { statefileWriter.dispose(); statefileWriter = null; } } function teardown(): void { shuttingDown = true; stopBackgroundServices(); } async function deregisterFromBrokers(): Promise { // Drop broker registrations so finished peers leave the live list. // In-process subagents inherit the parent PID, so c2c's pid liveness keeps // them "alive" forever unless we deregister explicitly (B002). if (!registered || !identity || !cli) return; const alias = identity.alias; try { await cli.deregister(alias); } catch { /* best-effort */ } if (sessionsBrokerRoot && crossRepoSessionsRegistered) { try { await cli.deregister(alias, { brokerRoot: sessionsBrokerRoot }); } catch { /* best-effort */ } } } function clearIdentityState(): void { barState.alias = undefined; barState.registered = false; barState.reason = undefined; registered = false; registerError = undefined; identity = null; crossRepoSessionsRegistered = false; crossRepoSessionsError = undefined; relayRegistered = false; relayAddress = undefined; relayError = undefined; relayHostId = undefined; relayHostIdVerified = false; peerStatusStore.clear(); clearPublishedSessionEnv(); } /** * Full disable path used by `/c2c-toggle off`: stop services, deregister, * clear identity so tools and status agree (send blocked, not "half-on"). */ async function disableC2c(): Promise { shuttingDown = true; stopBackgroundServices(); await deregisterFromBrokers(); clearIdentityState(); if (!subagentHint) { setParentAlias(undefined); // Keep observer wiring; re-enable will re-register parent alias. } stopSelfObserver?.(); stopSelfObserver = null; barState.alias = "c2c off"; barState.registered = false; shuttingDown = false; } /** * Bootstrap (or re-bootstrap) c2c for the current pi session context. * Shared by `session_start` and `/c2c-toggle on`. */ async function bootstrapSession(ctx: ExtensionContext): Promise { ctxRef = ctx; shuttingDown = false; if (!c2cEnabled) { installStatusColorPatch(ctx.ui.theme, () => barState); ctx.ui.setStatus(STATUS_KEY, formatDisabledStatus(ctx.ui.theme)); barState.alias = "c2c off"; barState.registered = false; return; } telemetry.startSession(); const gs = gstate(); if (!gs.hostSessionEnvCaptured) { // Capture ambient env once, before we publish our own session id. gs.hostSessionEnv = process.env[SESSION_ENV]; gs.hostSessionEnvCaptured = true; } installStatusColorPatch(ctx.ui.theme, () => barState); const exec: ExecFn = (command, args, options) => pi.exec(command, args, { ...options, cwd: ctx.cwd }); cli = new C2cCli({ exec }); perRepoBrokerRoot = resolvePerRepoBrokerRoot(ctx.cwd); const piSessionId = ctx.sessionManager.getSessionId?.() ?? null; const res = await establishIdentity(cli, { piSessionId, configuredAlias: process.env.C2C_PI_ALIAS, sessionIdEnv: subagentHint ? undefined : gs.hostSessionEnv, subagent: subagentHint ? { ...subagentHint, parentAlias: subagentParentAliasAtLoad ?? getParentAlias() ?? process.env.C2C_PI_ALIAS, } : undefined, }); identity = res.identity; registered = res.ok; // Spool migration on session switch const scopeKey = spoolScopeKey(identity); const prevSessionId = gs.prevSessionByScope?.[scopeKey]; if (prevSessionId && prevSessionId !== identity.sessionId) { const carried = readSpool(SPOOL_DIR, prevSessionId); if (carried.length > 0) { writeSpool(SPOOL_DIR, identity.sessionId, [ ...readSpool(SPOOL_DIR, identity.sessionId), ...carried, ]); } clearSpool(SPOOL_DIR, prevSessionId); } if (!gs.prevSessionByScope) gs.prevSessionByScope = {}; gs.prevSessionByScope[scopeKey] = identity.sessionId; gs.prevSessionId = identity.sessionId; gcStaleSpools(SPOOL_DIR, SPOOL_TTL_MS, Date.now()); if (res.ok) { // Make shell `c2c whoami` / send agree with extension tools (B187 fix). publishSessionEnv(identity.sessionId); if (!subagentHint) { setParentAlias(identity.alias); if (!stopSubagentObserver) { stopSubagentObserver = observeSubagentRegistrations((notice, registration) => { try { const args = buildRegistrationMessageArgs(notice, registration); pi.sendMessage(args.message, args.options); } catch { /* best-effort */ } }); } } else { const selfAgentId = subagentHint.agentId; if (selfAgentId) { stopSelfObserver = observeSubagentRegistrationsFor(selfAgentId, (notice, registration) => { try { const args = buildRegistrationMessageArgs(notice, registration); pi.sendMessage(args.message, args.options); } catch { /* best-effort */ } }); } notifySubagentRegistered({ agentId: selfAgentId, alias: identity.alias, parentAgentId: subagentHint.parentAgentId, }); } barState.alias = identity.alias; barState.registered = true; barState.reason = undefined; registerError = undefined; ctx.ui.setStatus(STATUS_KEY, formatStatus(identity.alias, true, ctx.ui.theme)); ctx.ui.notify(`c2c: registered as ${identity.alias}`, "info"); if (statefileWriter) statefileWriter.dispose(); statefileWriter = makeStatefileWriter({ command: resolveStatefileCommand(), brokerRoot: perRepoBrokerRoot.root, sessionId: identity.sessionId, alias: identity.alias, extensionVersion: PI_C2C_VERSION, }); installStatusTrackerForAlias(identity.alias); if (sessionsBrokerRoot) { try { const xsess = await cli!.register(identity.alias, identity.sessionId, { brokerRoot: sessionsBrokerRoot }); crossRepoSessionsRegistered = xsess !== null; crossRepoSessionsError = crossRepoSessionsRegistered ? undefined : "register returned no identity"; } catch (e: unknown) { crossRepoSessionsError = e instanceof Error ? e.message : String(e); } } } else { const reason = res.error ?? "unknown error"; barState.alias = identity.alias; barState.registered = false; barState.reason = reason; registerError = reason; ctx.ui.setStatus(STATUS_KEY, formatStatus(identity.alias, false, ctx.ui.theme, reason)); ctx.ui.notify( `c2c: registration failed (${reason}). Tools blocked until register succeeds; run 'c2c doctor'.`, "warning", ); } // Auto-join configured rooms if (registered && identity) { for (const room of readAutoJoinRooms()) { try { await cli!.joinRoom(room, identity.alias); } catch { ctx.ui.notify(`c2c: could not join room ${room}`, "warning"); } } } // Register with public relay if (relayEnabled && identity) { await registerRelayForAlias(identity.alias); } // Start the auto-delivery poller if (perRepoWatcher) perRepoWatcher.stop(); perRepoWatcher = startPerRepoWatcher(perRepoBrokerRoot.root, identity.sessionId, () => void pollTick()); if (sessionsBrokerRoot) { if (sessionsWatcher) sessionsWatcher.stop(); sessionsWatcher = startSessionsWatcher(sessionsBrokerRoot, identity.sessionId, () => void pollTick()); } if (!pollTimer) { pollTimer = setInterval(() => { void pollTick(); }, safetyNetPollMs); } if (registered) startAliasSyncTimer(); void pollTick(); } async function enableC2c(ui: ExtensionContext["ui"]): Promise { if (!ctxRef) { ui.notify("c2c: enabled. No active session context; restart the session to register.", "info"); return; } // Stop any half-state first, then full bootstrap (re-register). // bootstrapSession already notifies success/failure. stopBackgroundServices(); await bootstrapSession(ctxRef); } // --- wire up ToolCtx → tools.ts, CommandCtx → commands.ts ----------------- const toolCtx: ToolCtx = { get cli() { return cli; }, get identity() { return identity; }, get registered() { return registered; }, get registerError() { return registerError; }, get relayRegistered() { return relayRegistered; }, get relayAddress() { return relayAddress; }, get relayHostId() { return relayHostId; }, get relayHostIdVerified() { return relayHostIdVerified; }, get relayWsState() { return relayWsState; }, get crossRepoEnabled() { return crossRepoEnabled; }, get sessionsBrokerRoot() { return sessionsBrokerRoot; }, get crossRepoSessionsRegistered() { return crossRepoSessionsRegistered; }, get crossRepoSessionsError() { return crossRepoSessionsError; }, get relayEnabled() { return relayEnabled; }, get pollIntervalMs() { return pollIntervalMs; }, get peerStatusStore() { return peerStatusStore; }, get statusTracker() { return statusTracker; }, get telemetry() { return telemetry; }, get dedup() { return dedup; }, get queuedSinceMs() { return queuedSinceMs; }, get shuttingDown() { return shuttingDown; }, serializeDrain, setAlias, drainAllSources: drainAll, buildDebugState, buildLocalInfoText, buildLocalInfoDetails, ensureRelayRegistered, }; const commandCtx: CommandCtx = { ...toolCtx, get c2cEnabled() { return c2cEnabled; }, set c2cEnabled(v: boolean) { c2cEnabled = v; }, get barState() { return barState; }, teardown, disableC2c, enableC2c, setShuttingDown(v: boolean) { shuttingDown = v; }, }; registerTools(pi, toolCtx); registerCommands(pi, commandCtx); // --- lifecycle hooks ------------------------------------------------------ function updateStatusFromIdleCheck(): void { if (!statusTracker) return; statusTracker.transition(ctxRef?.isIdle() ?? true ? "idle" : "processing"); } pi.on("input", () => { statusTracker?.transition("input"); }); pi.on("agent_start", () => { statusTracker?.transition("processing"); }); pi.on("agent_end", () => { updateStatusFromIdleCheck(); }); pi.on("turn_start", () => { statusTracker?.transition("processing"); }); pi.on("turn_end", () => { updateStatusFromIdleCheck(); }); pi.on("tool_execution_start", () => { statusTracker?.transition("tool"); }); pi.on("tool_execution_end", () => { updateStatusFromIdleCheck(); }); pi.on("before_agent_start", async (event) => { if (!subagentHint || !identity) return; const parentAlias = subagentParentAliasAtLoad ?? getParentAlias(); if (!parentAlias) return; return { systemPrompt: appendSubagentPromptContext(event.systemPrompt, { selfAlias: identity.alias, parentAlias, }), }; }); pi.on("session_start", async (_event, ctx) => { await bootstrapSession(ctx); }); pi.on("session_shutdown", async (_event, ctx) => { shuttingDown = true; stopBackgroundServices(); await deregisterFromBrokers(); ctx.ui.setStatus(STATUS_KEY, undefined); clearIdentityState(); if (!subagentHint) { setParentAlias(undefined); stopSubagentObserver?.(); stopSubagentObserver = null; } stopSelfObserver?.(); stopSelfObserver = null; }); }