import type { Logger, Role, Channel } from './types.js'; import { type ProcessSignals, type SystemctlRunner } from './systemd-scope.js'; export type RowState = 'live' | 'ended' | 'archived'; /** Task 250 — `detached` is reached only via the boot-seed reconciler. * When the manager restarts and finds a surviving `claude-session-*.scope` * unit whose PID file is on disk, it reattaches the row but cannot read * PTY bytes, inject keystrokes, or kill via the master fd (the manager no * longer owns it). The detached row is observation-only on the manager * side; the operator's claude.ai/code URL still works because Claude * Code's remote-control channel is an outbound WebSocket independent of * the local PTY. End-via-systemctl-stop still works because the scope * unit name is recoverable. */ export type RowStatus = 'idle' | 'busy' | 'gone' | 'detached'; export interface SessionRow { /** Intrinsic Claude session id (UUID). Canonical key. For a top-level * jsonl row, this is the file basename. For a pre-first-turn live row, * this is the sessionId from the PID file. */ sessionId: string; /** Full bridge session id ("session_") from PID file, or null. */ bridgeSessionId: string | null; /** Suffix only (no "session_" prefix). Derived from bridgeSessionId. */ bridgeSuffix: string | null; /** Specialist agent name from PID file (`--agent `), or null. * Always null on archived rows — PID file is gone by then. */ agent: string | null; /** Specialist agent name cached from the JSONL `agent-setting` * record (read once at row-archive transition or boot-seed of an * archived row). Null when the JSONL has no `agent-setting` record * (operator-driven session). UI's resolveAgentLabel falls back to * this when `agent` is null. */ cachedAgentSetting: string | null; cwd: string | null; /** Task 1769 — the account this session belongs to, from its sidecar. * `cwd` is the primary account evidence for a live row (read from claude's * own PID file), but a row synthesised by the all-slug disk fallback has no * PID file and therefore no cwd. The sidecar's accountId survives the * process, so it is the account evidence that remains between turns. * Null when the sidecar is absent or predates Task 983's stamping. */ accountId: string | null; pid: number | null; pidFilePath: string | null; state: RowState; status: RowStatus; /** ms epoch from PID file `updatedAt`/`startedAt`, or 0 if unknown. */ updatedAt: number; /** Task 250 — systemd scope unit token, set by boot-seed when the row * matches a surviving `claude-session-*.scope`. Null for archived rows * and for live rows whose owning manager process is the current * process (those use PtyTracker.scopeUnitToken in pty-spawner's * module-scoped livePtys map). When set on a SessionRow without a * matching tracker, the row is a detached survivor whose End button * should go through this unit token rather than the (missing) * PtyHandle. */ scopeUnitToken: string | null; /** From sidecar. Null when no sidecar on disk for this row. */ senderId: string | null; /** From sidecar. Null when no sidecar on disk for this row. */ role: Role | null; /** From sidecar. Null when no sidecar on disk for this row. */ channel: Channel | null; /** From sidecar; updated by url-capture when the banner lands. Null * for headless spawns and for rows whose live PTY exited before the * banner fired. */ url: string | null; /** From sidecar. ISO 8601 timestamp captured at /spawn. */ startedAt: string | null; /** From sidecar. Null when not requested. Semantically: the mode the * operator picked at spawn. Compare against `effectivePermissionMode` * to detect a runtime downgrade (Task 278). */ permissionMode: string | null; /** Task 278 — from sidecar. The binary's actual runtime mode, derived * by `permission-mode-tail.ts` from the `--debug=auto-mode` capture * in `.permission-mode.log`. Null at spawn until the first * `[auto-mode] verifyAutoModeGateAccess` line resolves. Stays null * forever on non-auto spawns (no signal source for `acceptEdits` / * `plan` / `bypassPermissions` on 2.1.150). When non-null and * `!== permissionMode`, `SessionMetadataPane` renders a downgrade * warning. */ effectivePermissionMode: string | null; /** Task 260 — `hidden` from the sidecar. Defaults to `false` when no * sidecar on disk. */ hidden: boolean; /** From sidecar. Null when no sidecar on disk OR when the spawn was * not a specialist. The `agent` field above is the in-process view * (PID-file `--agent `); this is the request as recorded at * /spawn. They typically agree; divergence flags either a stale * sidecar or a sidecar-less row. */ sidecarSpecialist: string | null; } export interface WatcherEvent { kind: 'create' | 'modify' | 'delete'; source: 'pid' | 'jsonl'; row: SessionRow | null; sessionId: string; } export type Subscriber = (event: WatcherEvent) => void; export interface FsWatcher { bootSeed(): void; arm(): void; subscribe(sub: Subscriber): () => void; getBySessionId(id: string): SessionRow | undefined; getByBridgeSessionId(id: string): SessionRow | undefined; getByBridgeSuffix(suffix: string): SessionRow | undefined; getByPid(pid: number): SessionRow | undefined; all(): SessionRow[]; /** Task 1433 — the all-slug disk fallback resolveRow uses when the in-memory * index (armed to one boot slug) has no row for a listed session. Reuses * findExistingJsonlForSessionId (the same top+archive, every-slug walk the * sidebar list and the transcript use) and returns a row via buildArchivedRow, * identical in shape to an indexed ended/archived row. Returns undefined when * no JSONL exists under any slug. Optional so existing FsWatcher mocks that * never exercise the fallback (e.g. the meta-no-row buildEmptyWatcher, whose * absence of this method keeps its row-absent path) compile unchanged. */ resolveFromDisk?(sessionId: string): SessionRow | undefined; /** Resolve once a PID-file event lands for `pid`. Rejects on `timeoutMs`. */ waitForPid(pid: number, timeoutMs: number): Promise; /** Task 476 — test seam used by the firing-watcher mock so its synthetic * row carries the same sessionId the spawner just minted. Production * fs-watcher ignores the call: it reads claude's PID file as the * source of truth and the spawner asserts equality against this same * minted UUID. Mismatch is a spawn failure either way; this hook only * exists so test mocks don't have to harvest `--session-id` from * argv to align with the assertion. */ registerPidSessionId?(pid: number, sessionId: string): void; /** Task 260 — re-read the sidecar for `sessionId` and re-emit a * `modify` event. fs.watch does not fire for `.meta.json` writes * (the watcher only watches `*.jsonl` and PID files), so this is the * trigger for the http-server's url-capture path: pty-spawner writes * the URL to the sidecar, then calls this to refresh the row index * and emit a row-updated SSE event. * * Returns true when the row exists (the refresh fired); false when * no row matches the sessionId (caller may choose to log; the * url-capture path early-returns on null). */ refreshSidecar(sessionId: string): boolean; close(): void; /** Test seam: drive reconciliation directly, bypassing fs.watch + debounce. * Production code uses the same closures via the watch callback; the seam * exists because fs.watch on macOS coalesces tight create/delete bursts in * vitest, leaving live-event tests timing-dependent. Tests call these * after performing the same fs mutation a real session would emit. */ __internal: { reconcilePidFile(path: string): void; reconcileJsonlFile(path: string): void; }; } export declare function createFsWatcher(opts: { sessionsDir: string; projectsDir: string; logger: Logger; /** Task 1433 — the `` root (`projects/` lives directly under * it). Used only by resolveFromDisk's all-slug scan. Defaults to the parent of * `projectsDir`'s parent, which is correct for the production * `/projects/` layout and the test fixture's `/projects/`, * so no existing call site needs to pass it. Production passes it explicitly. */ claudeConfigDir?: string; /** Task 250 — systemctl runner the boot-seed uses to discover surviving * `claude-session-*.scope` units. Default issues real spawnSync calls; * tests inject a recording/scripted stub. When the env has no systemd * user manager (macOS dev hosts), the default runner returns non-zero * exit codes and the boot-seed degrades to "0 survivors" gracefully. */ runSystemctl?: SystemctlRunner; /** Task 451 — injectable kill/alive-check primitives for the boot-seed * orphan-stop post-flight verify. Default delegates to `process.kill`. * Tests inject a recording stub to assert SIGTERM/SIGKILL escalation. */ processSignals?: ProcessSignals; /** Task 451 — grace period between SIGTERM and SIGKILL during boot-seed * orphan-stop escalation. Default 5_000 ms (matches the scope's * `TimeoutStopSec`). Tests pass small values to keep run time bounded. */ killGraceMs?: number; }): FsWatcher; //# sourceMappingURL=fs-watcher.d.ts.map