This document is the reference for extending opensessions. It describes the agent event model, watcher interfaces, mux provider capabilities, and the runtime behaviors extension authors need to match.
For end-user setup, start with the docs linked from README.md. For plugin packaging workflow, see PLUGINS.md.
opensessions currently registers four built-in watchers at server startup.
- Watches
~/.local/share/amp/threads/T-*.json. - Also watches
~/.local/share/amp/session.jsonto clear unseen state when a terminal Amp thread becomes seen there. - Uses
fs.watchplus a 2 second polling pass. - Skips stale thread files older than 5 minutes.
- Resolves the project directory from
env.initial.trees[0].uri.
- Watches
~/.claude/projects/<encoded-path>/*.jsonl. - Uses
fs.watchplus a 2 second polling pass. - Reads only appended bytes after the last observed file size.
- Decodes project directories from folder names such as
-Users-me-project.
- Watches
~/.codex/sessions/**/*.jsonlor$CODEX_HOME/sessions/**/*.jsonl. - Reads
$CODEX_HOME/session_index.jsonlfor recent thread titles when available. - Uses recursive
fs.watchplus a 2 second polling pass. - Skips stale transcript files older than 5 minutes.
- Resolves mux sessions from
turn_context.cwdinside the transcript. - Treats
user_message, tool activity, and assistantcommentaryasrunning, assistantfinal_answerandtask_completeasdone, andturn_abortedasinterrupted.
- Polls
~/.local/share/opencode/opencode.dbor$OPENCODE_DB_PATH. - Uses
bun:sqlitein read-only mode. - Polls every 3 seconds.
- Resolves mux sessions from the OpenCode session row's
directoryfield.
type AgentStatus =
| "idle"
| "running"
| "done"
| "error"
| "waiting"
| "interrupted";Terminal states are done, error, and interrupted. The tracker uses those states to decide unseen behavior.
interface AgentEvent {
agent: string;
session: string;
status: AgentStatus;
ts: number;
threadId?: string;
threadName?: string;
unseen?: boolean;
}| Field | Type | Required | Notes |
|---|---|---|---|
agent |
string |
yes | Stable watcher identifier such as amp, claude-code, codex, or opencode |
session |
string |
yes | Resolved mux session name |
status |
AgentStatus |
yes | Current agent state |
ts |
number |
yes | Millisecond timestamp |
threadId |
string |
no | Instance key used to track multiple threads in one session |
threadName |
string |
no | Human-readable label shown in the detail panel |
unseen |
boolean |
no | Added by the tracker when serializing to the TUI |
- The tracker keys instances by
agent:threadIdwhenthreadIdexists, otherwise byagent. - A session can have multiple active agent instances.
- Unseen state is tracked per instance, then derived to the session level.
- Non-terminal updates clear unseen state for that instance.
- Stale
runningevents are pruned after 3 minutes. - Seen terminal instances are pruned after 5 minutes.
interface AgentWatcher {
readonly name: string;
start(ctx: AgentWatcherContext): void;
stop(): void;
}interface AgentWatcherContext {
resolveSession(projectDir: string): string | null;
emit(event: AgentEvent): void;
}resolveSession(projectDir) first checks for an exact directory match across registered mux sessions. If there is no exact match, the server falls back to parent-child prefix matching so nested project paths can still resolve.
import type { AgentWatcher, AgentWatcherContext } from "@opensessions/runtime";
export class MyAgentWatcher implements AgentWatcher {
readonly name = "my-agent";
start(ctx: AgentWatcherContext): void {
const projectDir = "/path/to/project";
const session = ctx.resolveSession(projectDir);
if (!session) return;
ctx.emit({
agent: this.name,
session,
status: "running",
ts: Date.now(),
threadId: "thread-1",
threadName: "Example task",
});
}
stop(): void {
}
}opensessions uses the capability model exported from @opensessions/mux. A provider must implement the required MuxProviderV1 contract and may opt into extra capabilities.
interface MuxSessionInfo {
readonly name: string;
readonly createdAt: number;
readonly dir: string;
readonly windows: number;
}
interface ActiveWindow {
readonly id: string;
readonly sessionName: string;
readonly active: boolean;
}
interface SidebarPane {
readonly paneId: string;
readonly sessionName: string;
readonly windowId: string;
}
type SidebarPosition = "left" | "right";interface MuxProviderV1 {
readonly specificationVersion: "v1";
readonly name: string;
listSessions(): MuxSessionInfo[];
switchSession(name: string, clientTty?: string): void;
getCurrentSession(): string | null;
getSessionDir(name: string): string;
getPaneCount(name: string): number;
getClientTty(): string;
createSession(name?: string, dir?: string): void;
killSession(name: string): void;
setupHooks(serverHost: string, serverPort: number): void;
cleanupHooks(): void;
}interface WindowCapable {
listActiveWindows(): ActiveWindow[];
getCurrentWindowId(): string | null;
}
interface SidebarCapable {
listSidebarPanes(sessionName?: string): SidebarPane[];
spawnSidebar(
sessionName: string,
windowId: string,
width: number,
position: SidebarPosition,
scriptsDir: string,
): string | null;
hideSidebar(paneId: string): void;
killSidebarPane(paneId: string): void;
resizeSidebarPane(paneId: string, width: number): void;
cleanupSidebar(): void;
}
interface BatchCapable {
getAllPaneCounts(): Map<string, number>;
}The server narrows providers with the runtime type guards exported from @opensessions/mux:
isWindowCapable()isSidebarCapable()isBatchCapable()isFullSidebarCapable()
listSessions()should return enough information for the server to sort and render sessions.getCurrentSession()should reflect the session attached to the current client when possible.setupHooks()should install mux-native hooks if the mux supports them. If it does not, a no-op implementation is acceptable.createSession()andkillSession()power the TUI's new-session and kill-session flows.
Plugins are loaded as default-exported factory functions that receive this API:
interface PluginAPI {
registerMux(provider: MuxProvider): void;
registerWatcher(watcher: AgentWatcher): void;
readonly serverPort: number;
readonly serverHost: string;
}The current runtime passes 127.0.0.1:7391 here.
- The server merges sessions from all registered providers into one state payload.
- Session ordering is persisted separately from mux ordering.
- tmux sidebars can be hidden into a stash session instead of being killed.
- tmux is the only supported built-in mux today. Other providers can still target these contracts, but they are currently outside the support bar unless documented otherwise.
- The TUI expects a WebSocket server on
127.0.0.1:7391. - The server exposes HTTP POST endpoints for programmatic metadata (status, progress, logs, notifications). See docs/reference/programmatic-api.md.
- Build a custom watcher: see the
AgentWatchersection above. - Push metadata from scripts: see docs/reference/programmatic-api.md.
- Build a plugin package or local plugin: see PLUGINS.md.
- Understand the end-to-end runtime: see docs/explanation/architecture.md.