import { type ChildProcess } from "node:child_process"; import type { SessionLifecycleMcpServer } from "../acp/mcp"; import { type SdkStartupFailure, type SdkStartupRollbackResult } from "../startup-capability"; import type { Broker, BrokerCleanupEvidence, BrokerResponse } from "./broker"; import type { LifecycleDurableEffectsReceipt, LifecycleStartupFailureReceipt } from "./lifecycle-ledger"; import { type ProcessIncarnationCommandRunner, type ProcessIncarnationOptions, parseDarwinProcessIncarnation, processIncarnation } from "./process-incarnation"; import { type SdkInternalSpawnCommand } from "./runtime"; export { type ProcessIncarnationCommandRunner, type ProcessIncarnationOptions, parseDarwinProcessIncarnation, processIncarnation, }; export declare function terminalUncertainStartupMessage(response: BrokerResponse): string; export declare function waitForChildSpawn(spawned: Pick, onPostSpawnError?: (error: Error) => void): Promise; export interface LifecycleDeadlines { receivedAt: number; requestedReadinessTimeoutMs: number; semanticReadyDeadlineAt: number; terminationStartDeadlineAt: number; lifecycleCleanupDeadlineAt: number; } export declare function deriveLifecycleDeadlines(receivedAt: number, requestedReadinessTimeoutMs: number): LifecycleDeadlines; export interface LifecycleTiming { now(): number; sleep(ms: number, signal?: AbortSignal): Promise; } type LifecycleCommand = SdkInternalSpawnCommand | { file: string; args: string[]; }; type LifecycleCommandResolver = () => LifecycleCommand; /** Test-only hook for simulating a crash immediately after one exact lifecycle detach. */ export declare function setLifecycleCleanupHookForTest(broker: Broker, hook: (() => void) | undefined): void; export declare function setLifecycleCommandResolverForTest(broker: Broker, resolver: LifecycleCommandResolver | undefined): void; export declare function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTiming | undefined): void; /** Deterministic platform seam for Windows-only ready-then-exit tolerance. */ export declare function setLifecycleHostPlatformForTest(platform: NodeJS.Platform | undefined): void; export declare function readyThenExitToleranceEnabledForTest(): boolean; export declare function hasValidLifecycleDeadlines(value: LifecycleDeadlines, now?: number): boolean; type Input = Record; export declare const isCanonicalSessionId: (value: string) => boolean; export interface SessionLifecycleWorktreeTarget { enabled: true; name?: string; } export interface SessionLifecycleWorktreeReceipt { enabled: true; cwd: string; created: boolean; reused: boolean; branch?: string; } export interface SessionLifecycleTranscriptIdentity { dev: string; ino: string; size: number; mtimeMs: number; mtimeNs: string; sha256: string; } /** * When a lifecycle-managed session publishes its replayable readiness signal. * * `immediate` is the stock contract. `deferred` prepares the session instead: * the child publishes a distinct prepared signal, keeps `session_ready` * withheld, and stays unusable for input until it is explicitly activated. It * is broker-issued and session-scoped precisely so a prepared session can never * be produced by an inherited process-global flag. */ export type SessionLifecycleReadiness = "immediate" | "deferred"; export interface SessionLifecycleLaunchRequestBase { operation: "session.create" | "session.fork" | "session.resume"; sessionId: string; cwd: string; stateRoot: string; sourceSessionId?: string; sourceSessionPath?: string; sourceSessionIdentity?: SessionLifecycleTranscriptIdentity; sourceCwd?: string; sessionPath?: string; sessionIdentity?: SessionLifecycleTranscriptIdentity; /** Broker-issued effect marker which the child echoes only after host readiness. */ effectMarker?: string; /** * Explicit `provider/model` pin with `gjc --model` grammar (#4707). The * coordinator resolves it before the broker; the session host applies it * exactly like a CLI `--model` selection, so it wins over `modelPreset` * (mirroring CLI precedence where an explicit `--model` overrides * activated profiles). */ modelId?: string; modelPreset?: string; mcpServers?: SessionLifecycleMcpServer[]; worktree?: SessionLifecycleWorktreeTarget; /** Absent means the stock immediate contract; `deferred` prepares the session. */ readiness?: SessionLifecycleReadiness; receivedAt: number; requestedReadinessTimeoutMs: number; semanticReadyDeadlineAt: number; terminationStartDeadlineAt: number; lifecycleCleanupDeadlineAt: number; coordinatorSessionId?: string; coordinatorSessionBranch?: string; } export type SessionLifecycleLaunchRequest = SessionLifecycleLaunchRequestBase & ({ coordinatorStateDir?: undefined; coordinatorSidecarSigningKey?: undefined; coordinatorSidecarKeyId?: undefined; } | { /** Coordinator namespace dir; broker computes the state file path from launch.id. */ coordinatorStateDir: string; /** Public Coordinator signing authority metadata. */ coordinatorSidecarKeyId: string; coordinatorSidecarSigningKey?: undefined; }); export declare function readSessionLifecycleLaunchRequest(value: string | undefined, now?: number): SessionLifecycleLaunchRequest; type CleanupEvidence = BrokerCleanupEvidence; export declare function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse; /** Test seam: the session-host command override as resolved from trusted env. */ export declare function sdkSessionCommandOverrideForTest(): { file: string; args: string[]; } | undefined; type EffectMarker = { pid: number; effectMarker: string; incarnation: string; }; type BrokerIndex = Pick; export declare function setProcessIncarnationForTest(broker: Broker, value: ((pid: number) => string | undefined) | undefined): void; type ProcessObservation = "alive" | "exited" | "uncertain"; /** Test seam for the lifecycle-owned process observation boundary. */ export declare function observeProcessForTest(pid: number, expectedIncarnation: string | undefined, readIncarnation?: (pid: number) => string | undefined): ProcessObservation; /** * Retires stale lifecycle markers only when their exact owner is proven gone. These * files are launch bookkeeping, not authority for a future process: retaining * an abandoned marker indefinitely turns unrelated launch failures into * cleanup uncertainty. Unreadable, malformed, linked, and live markers are * deliberately left untouched. */ export declare function reapDeadLifecycleMarkers(root: string, limit?: number): Promise; /** The child writes this only after its endpoint and semantic ready event are both live. */ export declare function writeSessionLifecycleReady(root: string, id: string, effectMarker: string): Promise; export interface LifecycleTranscriptEvidence { digest: string; identity: SessionLifecycleTranscriptIdentity; } /** Writes bounded startup diagnostics. The child stamps its own pid; the broker may stamp a proven child identity. */ export declare function writeSessionLifecycleFailure(root: string, id: string, effectMarker: string, failure: SdkStartupFailure, rollback: SdkStartupRollbackResult, transcript?: LifecycleTranscriptEvidence, ownerIncarnation?: string, ownerPid?: number): Promise; export declare function readSessionLifecycleFailureForTest(root: string, id: string, expected: { pid: number; effectMarker: string; incarnation: string; }): Promise; /** * Typed ready-then-exit probe for a dead child (#4712 review: the earlier * boolean collapsed EACCES/EIO/malformed JSON into "absent", routing the * teardown decision through the secondary authority and finally surfacing as * a false `spawn_failed` claim that the child "exited before registering * readiness" when the real failure was reading the endpoint). * * A host that died hard leaves its endpoint file on disk; a host that tore * down gracefully removed it first. `matched` requires the owned marker + * ready marker chain to still name exactly this child and the endpoint file * to still name the same pid and session. `absent_indexed` means the endpoint * file is gone (ENOENT only) and the broker's own session index recorded a * host registration for exactly this incarnation — the sole permitted * fallback authority. `malformed` and `io_error` are their own fail-closed * outcomes: they feed no teardown decision and assert nothing about the * child process. */ type ReadyAuthorityProbe = { kind: "matched"; } | { kind: "absent_indexed"; } | { kind: "absent_unindexed"; } | { kind: "not_published"; } | { kind: "malformed"; } | { kind: "io_error"; code: string; }; /** Test seam for the typed ready-authority probe boundary (#4712 review). */ export declare function probePublishedReadyAuthorityForTest(root: string, id: string, expected: EffectMarker): Promise; /** * Test seam for the PID-reuse deletion boundary: a successor launch rewriting * the lifecycle marker must stop the unlink of an endpoint that only matches * by a reused pid (#4712 review). */ export declare function removeOwnedLifecycleArtifactsForTest(root: string, id: string, expected: EffectMarker): Promise; export declare function canonicalDeleteLocatorPath(pathname: string): string; export interface LifecycleExecutionOutcome { response: BrokerResponse; durableEffects?: LifecycleDurableEffectsReceipt; startupFailure?: LifecycleStartupFailureReceipt; deferredArtifactCleanup?: () => Promise; } /** Returns the response together with every durable lifecycle fact needed for truthful replay. */ export declare function executeLifecycle(broker: Broker, operation: string, input: Input, identity: string, cleanup?: CleanupEvidence): Promise; /** * The live client/socket subscription count of the SDK session endpoint this * process serves, or `undefined` while this process serves no endpoint. * * Only the SDK session runtime owns the real socket table, so it publishes a * reader here instead of every consumer re-deriving attachment from the OS. * Consumers must treat `undefined` as "no evidence" and never as "detached". */ export type SessionHostAttachmentReader = () => number; /** What one serving runtime reports about itself while its transport is up. */ export interface SessionHostRuntimeEvidence { /** This runtime's own live SDK client/socket subscription count. */ attachedClients: SessionHostAttachmentReader; /** Whether this runtime currently has agent work in flight. */ workInFlight: () => boolean; } /** One runtime's live publication, retractable only by its owner. */ export interface SessionHostRuntimePublication { /** * Withdraws the evidence this handle published. Idempotent, and never able * to touch a sibling runtime's evidence. */ retract(): void; } /** * Publishes this runtime's own liveness evidence for the lifetime of its * transport. Retraction goes through the returned handle, so a runtime can only * ever withdraw evidence it owns. */ export declare function publishSessionHostRuntimeEvidence(evidence: SessionHostRuntimeEvidence): SessionHostRuntimePublication; /** * This process's currently attached SDK client count, summed across every * serving runtime, or `undefined` when no runtime publishes a readable count * (before startup, after teardown, or when every reader itself fails). Never * guesses: absence of a reader is absence of evidence. */ export declare function sessionHostAttachedClients(): number | undefined; /** * Whether any runtime in this process has agent work in flight right now. * * Positive evidence only: a runtime that publishes nothing, or whose reader * fails, reports no work, so this can never keep an abandoned host alive. */ export declare function sessionHostWorkInFlight(): boolean; /** * How often a live broker re-checks its own session registrations against OS * process liveness. * * This sweep can never disturb healthy work: a registration is dropped only when * `observeProcess` proves its exact published process identity exited. A live * replacement at the same pid retires the stale registration without being * signaled. One minute keeps `gjc_sessions`/`session.get_endpoint` from * advertising a corpse for longer than a single poll while costing one index * refresh per minute on an otherwise idle broker. */ export declare const BROKER_DEAD_REGISTRATION_SWEEP_MS = 60000; /** * Registrations reaped per sweep. Every reap is its own locked index transaction, * so an uncapped sweep over a long-lived index turns one broker into a continuous * holder of the shared session-index lock and starves unrelated `gjc` launches out * of their bounded retry budget. Surplus dead rows are reaped by later sweeps. */ export declare const BROKER_DEAD_REGISTRATION_SWEEP_LIMIT = 64; /** One reaped registration, as recorded by {@link reapDeadSessionRegistrations}. */ export interface ReapedSessionRegistration { sessionId: string; pid: number; endpointGeneration: number; } /** * Drops every indexed session registration whose host process is provably gone. * * Proof of death is positive, never inferred from a missing liveness proof: * `observeProcess` reports "exited" only on ESRCH or on a readable OS process * incarnation that differs from the recorded one (a reused pid). A stale or * missing heartbeat merely makes a session read as not-live — the host may * still be running ahead of the next heartbeat checkpoint pass, so it is never * grounds for a reap. EPERM and unreadable incarnations stay "uncertain", so an * alien or unreadable process is never mistaken for a dead one. Terminal and * terminal-uncertain identities are retained. Identity-level rows let the sweep * retire a dead losing root without disturbing the surviving authority. */ export declare function reapDeadSessionRegistrations(broker: BrokerIndex, limit?: number): Promise; /** * Runs {@link reapDeadSessionRegistrations} on {@link BROKER_DEAD_REGISTRATION_SWEEP_MS}. * The timer is unref'd, so it never keeps an otherwise idle broker process alive. * Returns a disposer. */ export declare function startBrokerDeadRegistrationSweep(broker: Pick, intervalMs?: number): () => void;