import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives"; import type { ModelProfileErrorDetails } from "../../config/model-profile-contract"; import { type DirectoryMigrationPolicy } from "../session-directory"; import { type BrokerDiscovery, type BrokerPublicationObservation, type RedactedBrokerDiscovery } from "./discovery"; import { type LifecycleDurableEffectsReceipt, LifecycleLedger, type LifecycleStartupFailureReceipt } from "./lifecycle-ledger"; import { SessionIndex } from "./session-index"; export interface BrokerSettings { agentDir: string; packageGeneration?: string; port?: number; heartbeatTtlMs?: number; /** Broker-owned migration policy. Client lifecycle frames cannot select it. */ resolveDirectoryMigration?: (_cwd: string) => Promise; } type ResolvedBrokerSettings = { agentDir: string; packageGeneration: string; port: number; heartbeatTtlMs: number; resolveDirectoryMigration: (_cwd: string) => Promise; }; export type BrokerErrorCode = "idempotency_conflict" | "terminal_uncertain" | "broker_restarting" | "unavailable" | "endpoint_stale" | "resource_gone" | "invalid_input" | "spawn_failed" | "ready_then_exited" | "endpoint_unreadable" | "startup_admission_timeout" | "startup_admission_refused" | "readiness_timeout" | "close_refused" | "not_found" | "live_session" | "cleanup_pending" | (string & {}); export type BrokerCleanupIdentity = { dev: string; ino: string; nlink?: string; size: number; mtimeNs: string; sha256: string; }; /** Exact retry evidence; detached paths are managed-receipt references, never caller authority. */ export type BrokerLifecycleCleanupFile = { /** Original lifecycle-owned path, retained only for exact identity validation. */ path: string; identity: BrokerCleanupIdentity; /** Monotonic append-only cleanup attempt. */ attempt?: number; /** Immutable no-replace quarantine destination persisted before native detach. */ plannedPath: string; /** Native-returned detached path, persisted after a failed post-detach cleanup. */ detachedPath?: string; /** Append-only terminal proof for this exact artifact; completed entries are never retried. */ completed?: true; }; /** Durable root-tree authority for broker artifact cleanup. */ export type BrokerArtifactTree = { identity: BrokerCleanupIdentity; snapshot: NativeDirectoryTreeSnapshot; plannedPath: string; detachedPath?: string; completed?: true; }; export type BrokerCleanupEvidence = { phase: "artifacts" | "transcript" | "metadata" | "lifecycle"; cleanupReceiptVersion?: 1; /** Ledger-bound deletion target; never reconstructed from a retry request. */ sessionsRoot?: string; transcriptPath?: string; cwd?: string; metadataRoot?: string; sessionId?: string; artifactsIdentity?: BrokerCleanupIdentity; transcriptIdentity?: BrokerCleanupIdentity; transcriptParentIdentity?: { dev: string; ino: string; }; /** Identity-bound lifecycle metadata marker retained when exact cleanup is deferred. */ metadataIdentity?: BrokerCleanupIdentity; metadataPath?: string; /** Monotonic append-only cleanup attempt. */ metadataAttempt?: number; /** No-replace quarantine destination persisted before lifecycle metadata detach. */ plannedMetadataPath?: string; /** Native-returned metadata quarantine path retained until identity-bound reconciliation succeeds. */ detachedMetadataPath?: string; /** Append-only terminal proof for lifecycle metadata cleanup. */ metadataCompleted?: true; detachedArtifactsPath?: string; retainedArtifactsSuccessorPath?: string; retainedArtifactsPlaceholderPath?: string; retainedArtifactsUnknownPath?: string; retainedArtifactsSideAuthority?: "none" | "retained"; detachedTranscriptPath?: string; retainedTranscriptSuccessorPath?: string; retainedTranscriptPlaceholderPath?: string; retainedTranscriptUnknownPath?: string; /** Durable proof that artifact cleanup completed before transcript mutation. */ artifactsRemoved?: boolean; artifactsAbsentAtAuthorization?: true; /** Preauthorized no-replace artifact quarantine path persisted before detach. */ plannedArtifactsPath?: string; /** Identity-bound artifact tree authority persisted before broker detach and replayed exactly. */ artifactTree?: BrokerArtifactTree; /** Preauthorized no-replace transcript quarantine path persisted before detach. */ plannedTranscriptPath?: string; /** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */ lifecycleFiles?: BrokerLifecycleCleanupFile[]; lifecycleParentIdentity?: { dev: string; ino: string; }; /** Delete metadata receipts authorize only the canonical marker/ready sibling pair. */ lifecycleDeleteMetadata?: true; }; export type BrokerResponse = { ok: true; result?: unknown; indexSeq?: number; } | { ok: false; error: { code: BrokerErrorCode; message: string; details?: ModelProfileErrorDetails; endpoint?: "unavailable"; cleanup?: BrokerCleanupEvidence; }; indexSeq?: number; durableEffects?: LifecycleDurableEffectsReceipt; startupFailure?: LifecycleStartupFailureReceipt; }; /** Test seam for lifecycle serialization identity. */ export declare function lifecycleTargetForTest(operation: string, input: Record): unknown; /** Tombstone prefix used by {@link Broker.reclaimStaleLock} when a dead owner's lock is renamed aside. */ export declare const BROKER_LOCK_TOMBSTONE_PREFIX = ".broker.lock.stale-"; /** * Recovery directories left beside the lock by manual and older automated broker * restarts. Nothing writes them today, but installs that ever recovered by hand * still carry them, so the reaper owns them alongside its own tombstones. */ export declare const BROKER_LOCK_BACKUP_PREFIXES: readonly ["broker-restart-backup-", "broker-stale-backup-"]; /** * Age bound before a reclaimed lock artifact may be removed. Generous enough * that a broker still settling after a reclaim can never have its own successor * state deleted underneath it. */ export declare const BROKER_LOCK_ARTIFACT_GRACE_MS: number; /** Why a candidate lock artifact survived a reap pass. */ export type BrokerLockArtifactRetentionReason = "within-grace" | "owner-alive" | "owner-record-unreadable" | "owner-record-missing" | "not-a-directory" | "removal-failed"; export interface BrokerLockArtifactRetention { path: string; reason: BrokerLockArtifactRetentionReason; } export interface BrokerLockArtifactReapResult { removed: string[]; retained: BrokerLockArtifactRetention[]; } /** * Remove reclaimed broker lock tombstones and legacy restart backups older than * the grace window. * * `#reclaimStaleLock` renames a dead owner's lock to a tombstone named by a hash * of the lock's dev+ino, so a machine accrues one directory per dead owner and * nothing ever removed them (54 on the install in #3963). Reaping is * best-effort and fail-closed: anything live, unreadable, permission-denied, or * otherwise ambiguous is kept and the reason is logged. */ export declare function reapStaleBrokerLockArtifacts(input: { agentDir: string; now?: number; graceMs?: number; pidAlive?: (pid: number) => boolean; }): Promise; export interface StartupAdmissionTiming { now(): number; sleep(ms: number, signal?: AbortSignal): Promise; } export type StartupAdmissionResult = { status: "completed"; admittedAt: number; value: T; } | { status: "admission_timeout"; reason: "admission_timeout"; } | { status: "admission_refused"; reason: "admission_refused"; }; export declare function sdkHostStartupConcurrency(availableParallelism?: number): number; export declare class StartupAdmissionQueue { #private; readonly limit: number; constructor(limit: number); run(queueWaitMs: number, timing: StartupAdmissionTiming, task: (admittedAt: number) => Promise): Promise>; /** * Refuse every queued startup and every later one. A broker that can no longer * prove it owns the published root must not spawn children through slots that * free up while it is fenced. The epoch also invalidates a waiter that was * granted but has not crossed the task execution boundary yet. */ close(): void; /** Accept later startups after fresh publication ownership has been proven. */ reopen(): void; } export declare class Broker { #private; readonly settings: ResolvedBrokerSettings; readonly index: SessionIndex; readonly ledger: LifecycleLedger; discovery: BrokerDiscovery | null; constructor(settings: BrokerSettings); runStartup(queueWaitMs: number, timing: StartupAdmissionTiming, task: (admittedAt: number) => Promise): Promise>; start(): Promise; get ownsDiscovery(): boolean; get completion(): Promise; status(): RedactedBrokerDiscovery | null; heartbeat(): Promise; /** Re-observes provably live session hosts and checkpoints their liveness. */ heartbeatSessions(now?: number): Promise; /** * Revalidate retained publication ownership and begin one synchronous effect in * the same stack. The callback is the authority boundary: callers must perform * the authorized effect inside it, so no awaited work can separate proof from * the effect it authorizes. */ runSynchronousEffectWithFreshPublicationAuthority(effect: () => T, ..._synchronousOnly: T extends PromiseLike ? [never] : []): { authorized: true; value: T; } | { authorized: false; }; /** * A stop may take the owning path only while it can prove it still owns the root. * Claiming ownership it cannot prove keeps the admission queue open, so a startup * queued behind this broker is granted a slot that frees after completion and * spawns a child the broker has no authority over. */ stop(): Promise; handleRequest(operation: string, input: Record, idempotencyKey?: string): Promise; } /** Test-only hook for simulating a process crash after terminal persistence verification. */ export declare function setTerminalPersistenceHookForTest(broker: Broker, hook: (() => void) | undefined): void; /** Test-only hook for shortening the bounded ambiguity deadline. */ export declare function setAmbiguityGraceForTest(broker: Broker, graceMs: number | undefined): void; /** Test-only hook for shortening the startup lock-artifact reap bound. */ export declare function setLockArtifactGraceForTest(broker: Broker, graceMs: number | undefined): void; /** Test-only hook for shortening the bounded liveness deadline. */ export declare function setLivenessGraceForTest(broker: Broker, graceMs: number | undefined): void; /** * Test-only hook for stalling the heartbeat write, reproducing a publication tick * whose awaited IO does not settle. Clearing it releases the stalled tick the way * recovered IO would, instead of abandoning it forever. */ export declare function setHeartbeatStallForTest(broker: Broker, stalled: boolean): void; /** Test-only hook for forcing the observation the publication watchdog sees. */ export declare function setPublicationObservationForTest(broker: Broker, observation: BrokerPublicationObservation | undefined): void; export {};