@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
311 lines (310 loc) • 15.4 kB
TypeScript
import { type PullRequest } from "./return-contract.js";
import { sessionStalls } from "./stalls.js";
import { z } from "zod";
import { type OriginPane, type Receipt } from "./dispatch.js";
import { type BriefArrival } from "./launch-brief.js";
import { type LostTurn } from "./codex-servers.js";
import { findGrant } from "./grants.js";
import { peerRequest, type HookPeer } from "./peers.js";
import { type ApprovalDecision, type Json, type Provider, type Target } from "./protocol.js";
import { type FinalTurn } from "./schedule-watch.js";
import { type TurnRecord } from "./turn-records.js";
import { type MovedWorker, type MoveRecord } from "./session-move.js";
import { type Uncommitted } from "./worker-unfinished.js";
export { truncateUtf8 } from "./turn-records.js";
/**
* The conductor's returns loop. The worker's computer answers what its
* dispatched panes are doing (`workerStates`): from the turn record its
* agent's own hooks wrote (turn-records.ts) when there is one, else from the
* Herdr snapshot its Hook already shares and the transcript. The dispatching
* Hook asks each peer once per poll for all of its open dispatches, records
* transitions in the receipts, and tells an idle dispatching agent that
* returns are waiting (`DispatchReturns`).
*/
/** Longest final reply kept in a receipt, in UTF-8 bytes. */
export declare const REPLY_LIMIT = 4000;
/** How often the dispatching Hook asks peers about open dispatches. */
export declare const POLL_MS = 15000;
/** Allow the new process and its terminal identity to appear before declaring
* an unobserved launch gone. An observed worker never gets this grace. */
export declare const LAUNCH_GRACE_MS = 60000;
/** Shortest gap between two notices typed into the same dispatching pane. */
export declare const NOTICE_MS = 120000;
/** Receipts older than this are no longer watched. */
export declare const WATCH_MS: number;
/** How long a stopped worker whose harness still runs background tasks is
* waited on before it counts as done anyway. Measured from the latest Stop,
* and every task that finishes wakes the worker (a new prompt, a new Stop), so
* it only elapses when no background task has finished for two hours: a dev
* server the worker left running. It is then reported done with the count
* still running. */
export declare const BACKGROUND_WAIT_MS: number;
export declare const workerRequestSchema: z.ZodObject<{
targets: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
dispatch: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
starting: z.ZodLiteral<true>;
startingToken: z.ZodString;
dispatch: z.ZodOptional<z.ZodString>;
}, z.core.$strip>]>>;
}, z.core.$strict>;
/** What a worker pane shows right now, as its own computer reads it. */
export interface WorkerObservation {
/** Herdr's status for the pane, or gone when the pane or its conversation is no longer there. */
state: "working" | "idle" | "done" | "blocked" | "unknown" | "gone" | "closed" | "unavailable";
session?: string;
completed?: boolean;
reply?: string;
truncated?: boolean;
/** The error the harness ended the turn on (Codex's usage limit). */
error?: string;
/** The state comes from the agent's own turn events, not from how the pane looks. */
hook?: true;
/** When the turn's Stop arrived, on the worker's clock; names the turn. */
endedAt?: string;
stopSeq?: number;
/** Background tasks the harness still runs: waited on while `working`;
* when `done`, left running (after BACKGROUND_WAIT_MS, or not awaited), so
* the pane is not closed on them. */
background?: number;
/** The owner stopped the turn in the worker's terminal. */
interrupted?: true;
/** A permission request the pane's worker is waiting on, forwarded by its Hook (`AgentHooks.workerApproval`). */
approval?: Json;
prs?: PullRequest[];
stalled?: boolean;
stalledSince?: string;
stallFor?: number;
/** Why a turn that ended is not done (worker-unfinished.ts): it announced a
* next step, or left uncommitted work and no PR. Returned as needs-you. */
unfinished?: string;
/** The worker's checkout could not be read in time: the turn may be done,
* but it is not closed on that (worker-close.ts). */
unchecked?: true;
/** Where a worker that hit its Claude usage limit was working, for the dispatch that continues it. */
checkout?: {
path: string;
branch?: string;
};
/** The worker is being moved to another agent (session-move.ts): the move's id. */
moving?: string;
/** The worker was moved to another agent; the state above is the new agent's. */
moved?: MovedWorker;
}
export interface WorkerReaders {
snapshot: (server: string) => Promise<Json>;
freshSnapshot?: (server: string) => Promise<Json>;
identity: (server: string, pane: Json) => Promise<string | undefined>;
finalTurn: (source: Provider, session: string) => Promise<FinalTurn | undefined>;
/** The turn record the pane's agent reported through its hooks (or OpenCode's plugin). */
turn?: (server: string, pane: Json, source: Provider) => Promise<TurnRecord | undefined>;
/** What the worker in this conversation is waiting on, from the Hook that runs it. */
approval?: (target: Target) => Json | undefined;
/** Running child agents (sub-agents, teammates, workflows, fan-outs) of the
* worker's conversation, counted like the session rows count them. */
children?: (source: Provider, session: string) => Promise<number>;
now?: () => number;
stall?: typeof sessionStalls.observe;
/** The turn a Codex pane lost with its app-server, which no Stop will end. */
lost?: (server: string, pane: string, session?: string) => LostTurn | undefined;
/** Tracked files with uncommitted changes in the worker's checkout. */
uncommitted?: (directory: string) => Promise<Uncommitted>;
/** Whether other panes' folders share the worker's checkout, whose changes then are not only its own. */
shared?: (directory: string, folders: readonly unknown[]) => Promise<boolean>;
/** The branch checked out in a folder. */
branch?: (directory: string) => Promise<string | undefined>;
/** This computer's session moves (session-move.ts), newest first. */
moves?: () => Promise<MoveRecord[]>;
}
export declare function paneTurn(server: string, pane: Json, source: Provider): Promise<TurnRecord | undefined>;
/** The default readers with the approval reader of the Hook that runs the workers. */
export declare const workerReaders: (approval: NonNullable<WorkerReaders["approval"]>) => WorkerReaders;
/** The receiving side of `/v1/dispatch/workers` on a Hook: leases the panes'
* permission asks for the dispatching Hook's next polls and reads what each
* worker is waiting on. */
export declare function hookWorkers(hooks: {
leaseDispatch: (targets: readonly {
server: string;
pane: string;
source: string;
}[]) => void;
workerApproval: (target: Target) => object | undefined;
}): (input: unknown) => Promise<{
workers: WorkerObservation[];
}>;
/** Receiving side: the state of each dispatched pane, from its agent's turn
* record when its hooks wrote one, else from the shared snapshot and, once
* the agent has stopped, the final reply in its transcript. */
export declare function workerStates(input: unknown, readers?: WorkerReaders): Promise<{
workers: WorkerObservation[];
}>;
/** A worker's forwarded permission request as its Hook sent it; an invalid one is ignored, not a reason to drop the state. */
declare const approvalObservation: z.ZodObject<{
actionId: z.ZodString;
tool: z.ZodDefault<z.ZodString>;
title: z.ZodOptional<z.ZodString>;
request: z.ZodOptional<z.ZodString>;
requestKind: z.ZodOptional<z.ZodEnum<{
command: "command";
edit: "edit";
other: "other";
question: "question";
tool: "tool";
}>>;
terminal: z.ZodOptional<z.ZodBoolean>;
conductor: z.ZodOptional<z.ZodObject<{
action: z.ZodEnum<{
dispatch: "dispatch";
hand_off: "hand_off";
}>;
project: z.ZodOptional<z.ZodString>;
computer: z.ZodOptional<z.ZodString>;
}, z.core.$strict>>;
expiresAt: z.ZodOptional<z.ZodString>;
pushed: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>;
export type ObservedApproval = z.infer<typeof approvalObservation>;
/** What the owner is told when the worker's turn was interrupted in its terminal. */
export declare const INTERRUPTED = "The worker's turn was interrupted in its terminal before it finished.";
export declare function workerTurnKey(seen: {
endedAt?: string;
stopSeq?: number;
reply?: string;
}, error?: string): string | undefined;
/** Apply one observation to a receipt. Returns true when the receipt changed. */
export declare function observe(receipt: Receipt, value: unknown, now: number, peers?: readonly HookPeer[], self?: {
id: string;
name: string;
verifiedHomeMove?: string;
}): boolean;
/** One line for the dispatching agent: who returned, how, and where to read it. */
export declare function noticeLine(receipts: readonly Receipt[]): string;
/** What `dispatch_returns` lists for one unread return. */
export declare function returnRow(receipt: Receipt): Json;
export interface DispatchReturnsOptions {
/** This Hook's stable identity, for a worker moving back home. */
localComputer?: {
id: string;
name: string;
};
peers?: () => Promise<HookPeer[]>;
request?: typeof peerRequest;
forward?: (origin: OriginPane, text: string, deliveryId: string) => Promise<{
delivered: boolean;
} | undefined>;
/** Worker states on this computer, for dispatches placed here without SSH. */
localWorkers?: (input: Json) => Promise<{
workers: unknown[];
}>;
isLocal?: (computer: string) => boolean;
/** A recent Herdr snapshot of a local server, to see whether the dispatching agent is idle. */
snapshot?: (server: string) => Promise<Json>;
identity?: (server: string, pane: Json) => Promise<string | undefined>;
/** Types the notice into the dispatching agent, through the ordinary hand-off
* path; the same notice keeps one delivery id, so a retry is never typed twice. */
deliver?: (target: Target, text: string, deliveryId: string) => Promise<{
delivered: boolean;
}>;
/** What a worker's hooks reported for a brief launched on this computer. */
localArrival?: (id: string) => Promise<BriefArrival | undefined>;
/** The standing grant for a conductor call, on this computer. */
findGrant?: typeof findGrant;
/** Answers a worker's approval placed on this computer, without SSH (the Hook's own `AgentHooks.answer`). */
localAnswer?: (target: Target, actionId: string, decision: ApprovalDecision) => Promise<unknown>;
/** A worker's new forwarded request was recorded. Returns true when this Hook pushed it to its phone. */
onApproval?: (receipt: Receipt, approval: NonNullable<Receipt["approval"]>) => boolean | void;
close?: (receipt: Receipt, verifiedPeer?: HookPeer) => Promise<Json>;
/** Continues Claude workers stopped by their usage limit on another account, after each poll (account-failover.ts). */
failover?: {
run: () => Promise<void>;
};
now?: () => number;
}
/** The same returns to the same origin keep their id on retry. A role
* hand-over gets a fresh delivery instead of reusing the old chat's receipt. */
export declare function noticeDeliveryId(receipts: readonly Receipt[]): string;
/** Dispatching side: follows open dispatches and delivers their returns. */
export declare class DispatchReturns {
private readonly forward?;
private readonly localComputer?;
private readonly peers;
private readonly request;
private readonly localWorkers;
private readonly isLocal;
private readonly snapshot;
private readonly identity;
private readonly deliver;
private readonly localArrival;
private readonly now;
private readonly grant;
private readonly localAnswer?;
private readonly onApproval?;
private readonly close?;
private readonly failover?;
private lastPoll;
private readonly lastNotice;
/** A return is waiting for a dispatching agent that was busy: try again on
* the next tick instead of the next poll. */
private noticesDue;
private running?;
constructor(options?: DispatchReturnsOptions);
private isSelf;
private verifiedMovedPeer;
/** Send a decision for a worker's request to the Hook that runs it. */
private sendAnswer;
/** Answer the permission request a dispatched worker is waiting on. It is
* cleared from the receipt once answered, or once its Hook says it is no
* longer pending (answered in the terminal, or timed out). */
answerApproval(id: string, decision: ApprovalDecision, actionId: string, caller?: OriginPane): Promise<void>;
/** Called from the Hook's activity tick. Polls peers at most every POLL_MS;
* a return waiting for its notice is tried on every tick, so one recorded
* while the dispatching agent was busy reaches it as soon as it stops. */
tick(): Promise<void>;
/**
* A brief that went with the launch but was not confirmed while the dispatch
* was placed (a startup screen held it, the agent was slow): ask the
* worker's computer whether its hook has confirmed it since.
*/
confirmArrivals(peers: HookPeer[]): Promise<void>;
/** Ask each peer, once, about every open dispatch placed on it. */
poll(): Promise<void>;
/** A conductor call the owner's standing grant covers is answered here at
* once, and its request is left off the observation so it never becomes a
* return. Anything else, or an answer that failed, is left for the owner. */
private grantAnswered;
/** Type one line into each idle dispatching agent that has returns it was
* not told about. A pane still working is tried again on the next tick. */
notify(): Promise<void>;
/** Forward a structured report with one stable id; queued is a durable
* acceptance, while uncertain is retained and never replaced. */
forwardPrs(): Promise<void>;
cleanup(): Promise<void>;
private closeRemote;
/** A remote successor acknowledges only the exact return version it received. */
acknowledge(rows: Json[]): Promise<void>;
/** Every unread return, oldest first, marked read as it is handed over. */
take(consume?: boolean): Promise<Json[]>;
}