SessionService
Defined in: src/service.ts:862
Constructors
Section titled “Constructors”Constructor
Section titled “Constructor”new SessionService(
deps):SessionService
Defined in: src/service.ts:863
Parameters
Section titled “Parameters”Returns
Section titled “Returns”SessionService
Methods
Section titled “Methods”advanceToExecutionOnPr()
Section titled “advanceToExecutionOnPr()”advanceToExecutionOnPr(
id):boolean
Defined in: src/service.ts:2153
Auto-advance a manually-driven (or otherwise un-released) planning session into execution once a PR appears. When the operator reviews the plan then steers the agent instead of clicking Go, the agent writes code and opens a PR while planPhase is still “planning” — leaving the plan-gate badge latched and making autopilot stand down (autopilot.ts eligible() suppresses planning sessions). This method detects that case: called when a PR is observed for a still-planning session, it flips planPhase → “executing” so the plan gate yields and autopilot stops standing down.
Critically, it does NOT send PLAN_GO_STEER — the agent already executed; steering it to “implement the approved plan” would be wrong (the plan may not even be approved). Its caller mirrors autopilot’s hasPr non-”none” semantics (open/merged/closed) so the two don’t drift.
Returns true when a real transition occurred (planPhase was “planning” and is now “executing”), false when the session is unknown or not in the planning phase (idempotent no-op).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”boolean
archive()
Section titled “archive()”archive(
id,reapKeys?):Promise<number>
Defined in: src/service.ts:2483
Close a session: optionally terminate selected leftovers first, then stop the
agent, remove the worktree, and archive the row. reapKeys are leftover keys
the operator chose to kill; we re-detect and intersect by key so a stale/forged
client selection can never make us kill an arbitrary pid. Returns the number of
leftovers actually reaped (the intersection), so bulk callers can report a count
that reflects what was killed rather than what was requested.
Parameters
Section titled “Parameters”string
reapKeys?
Section titled “reapKeys?”string[]
Returns
Section titled “Returns”Promise<number>
archiveMany()
Section titled “archiveMany()”archiveMany(
ids):Promise<{cleared:string[];leftovers:number; }>
Defined in: src/service.ts:2545
Bulk-close sessions (“clear all merged”). Each session’s leftover subprocesses
are auto-detected and reaped before its teardown — unlike the single-session
close (which asks per-process), bulk clear terminates them all so a landed
session can’t leave a dev server orphaned. Returns the ids actually archived
(missing ones are skipped) and the total leftovers terminated — counted from
what archive actually reaped, so the number never overstates. The caller must
restrict ids to a safe set (e.g. merged-only) — this archives what it’s given.
One session’s teardown failing (e.g. worktree.remove throwing) must not abort
the rest, so each is isolated: a failed id is skipped and left out of cleared,
so the caller emits archived events for exactly the rows that really went away.
Parameters
Section titled “Parameters”string[]
Returns
Section titled “Returns”Promise<{ cleared: string[]; leftovers: number; }>
branchExists()
Section titled “branchExists()”branchExists(
repoPath,branch):boolean
Defined in: src/service.ts:1865
Whether a local branch already exists — the server’s pre-flight check before a rename.
Parameters
Section titled “Parameters”repoPath
Section titled “repoPath”string
branch
Section titled “branch”string
Returns
Section titled “Returns”boolean
broadcast()
Section titled “broadcast()”broadcast(
ids,text):object
Defined in: src/service.ts:2026
Fan a steer out to many sessions (human-style). Skips unknown ids and dead panes.
Lists herdr’s live agents ONCE up front rather than per id, so a wide fan-out
doesn’t spawn one blocking herdr agent list per target.
Parameters
Section titled “Parameters”string[]
string
Returns
Section titled “Returns”object
sent:
number
total:
number
clearMerging()
Section titled “clearMerging()”clearMerging(
id):void
Defined in: src/service.ts:2293
Clear one session’s merge-train mark. No-op (no event) when not marked.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”void
clearMergingForTrain()
Section titled “clearMergingForTrain()”clearMergingForTrain(
trainId,now?):void
Defined in: src/service.ts:2342
The train session was archived (run complete). Clear any of its members still
marked (unchanged), then drive the offer. If a merge was already credited →
emit + finalize now. Else DEFER: mark the entry archived and fast-poll each
still-tracked member via refreshPr so a poller-gated merge surfaces within
seconds and routes through resolveMerging → late-credit emit. A deferred
entry whose late credit never arrives is reclaimed by sweepStaleMerging with
no emit, MERGE_STALE_MS after this archive — the await window starts HERE, so
it is independent of how long the run itself took (a slow/long run is never
reclaimed mid-flight; only the post-archive wait is bounded). now injectable.
Parameters
Section titled “Parameters”trainId
Section titled “trainId”string
number = ...
Returns
Section titled “Returns”void
create()
Section titled “create()”create(
input):Promise<Session>
Defined in: src/service.ts:1389
Parameters
Section titled “Parameters”Returns
Section titled “Returns”Promise<Session>
haltAll()
Section titled “haltAll()”haltAll():
object
Defined in: src/service.ts:2051
Fleet-wide emergency stop: interrupt every live, actively-working agent at once.
Sends a single ESC — the Claude Code interrupt key — to each pane whose herdr
agent reports working, halting the current turn WITHOUT clearing its input or
quitting it (a lone ESC, no bracketed paste, no trailing CR — the opposite of a
steer). Idle / blocked / done agents, dead panes, archived sessions and the
ephemeral usage probe are all left untouched: only ACTIVE sessions are matched
against the live agent set (so the probe — never a stored session — and archived
rows fall out), and of those only the ones reporting working are hit. Auto-spawned
(drain) sessions are included BY DESIGN — a misfiring autopilot is exactly what this
stops. Lists herdr’s agents ONCE up front (like broadcast) so a wide fan-out makes a
single agent list call, not one per target. Emits halt:done {halted} so every
connected operator sees the reach.
Throws (→ HTTP 500 → the UI surfaces halt_failed + Retry) when herdr can’t even be
listed: a swallowed failure would emit a success-looking halt:done {halted:0},
indistinguishable from “nothing was working” — a silent no-op at the worst moment.
Returns
Section titled “Returns”object
halted
Section titled “halted”halted:
number
leftovers()
Section titled “leftovers()”leftovers(
id):Leftover[]
Defined in: src/service.ts:2450
Leftover subprocesses/proxies that would survive this session’s close; [] when none.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Leftover[]
liveTrainPrs()
Section titled “liveTrainPrs()”liveTrainPrs():
number[]
Defined in: src/service.ts:2219
The PR numbers currently scoped by any live merge train (the union of every live
train’s prNumbers), deduped + sorted. Empty when no train is live. Cheap + sync —
reads only the in-memory #liveTrains map, no forge round-trip. The Herd Rundown
folds this in as the train’s queued set.
Returns
Section titled “Returns”number[]
markTrainMember()
Section titled “markTrainMember()”markTrainMember(
trainId,sessionId,prNumber,now?):void
Defined in: src/service.ts:2260
Stamp one session as a member of a live train and register it with the
completion tracker. Idempotent and guarded: a no-op when the session IS the
train, when the train is no longer live (a session:git after archive must not
resurrect a member), when the session is unknown, or when already marked. The
tracker entry is lazily created on the FIRST marked member, so a train whose PRs
never open creates no entry. now injectable.
Parameters
Section titled “Parameters”trainId
Section titled “trainId”string
sessionId
Section titled “sessionId”string
prNumber
Section titled “prNumber”number
number = ...
Returns
Section titled “Returns”void
reconcileTrainMarks()
Section titled “reconcileTrainMarks()”reconcileTrainMarks(
trainId?,now?):void
Defined in: src/service.ts:2202
Server-derived participant marking: for each live train (the one trainId, or
all when omitted), mark every ACTIVE same-repo session whose live PR is OPEN and
whose number is in the train’s scoped queue. Reads the live PR snapshot via
deps.prSnapshot (empty when unwired). The train session itself is never marked,
and an already-marked session is left alone (idempotent — markTrainMember
re-guards). Drives both the launch-time reconcile and the per-session:git
one (a PR that opens later is picked up the next time this runs). now injectable.
Parameters
Section titled “Parameters”trainId?
Section titled “trainId?”string
number = ...
Returns
Section titled “Returns”void
registerTrain()
Section titled “registerTrain()”registerTrain(
trainId,repoPath,prNumbers,now?):void
Defined in: src/service.ts:2179
Parameters
Section titled “Parameters”trainId
Section titled “trainId”string
repoPath
Section titled “repoPath”string
prNumbers
Section titled “prNumbers”number[]
number = ...
Returns
Section titled “Returns”void
relaunch()
Section titled “relaunch()”relaunch(
originalId,issueRef?,overrides?):Promise<Session>
Defined in: src/service.ts:1537
Spawn a fresh replacement for an existing task, carrying its prompt and all
per-task settings — including the spawn-baked ones that can’t be changed after
the fact (model, repoPath, baseBranch, planGateEnabled) plus the runtime
toggles (autopilot, auto-merge). Owns ONLY the spawn + override copy: it emits
no events, never archives the original, and does not resolve forge/issues —
those are the route handler’s job. Always auto: false (relaunch is an
explicit operator action).
overrides is an optional bag applied over the original (absent field keeps the
original’s value; a present one — incl. explicit null — replaces it), letting a
caller relaunch into a DIFFERENT repo while carrying prompt/model/base-branch
forward. Image handling forks on whether overrides are present:
- quick relaunch (
overrides == null) → the original’s uploads are auto-carried (copied into staging), byte-for-byte the original spawn. - relaunch WITH overrides →
overrides.imagesis used VERBATIM and the original’s uploads are NOT auto-carried. The composer is the single source of truth here: it seeds the carried originals (viastageRelaunchImages) into the override list and the operator edits that list, so re-merging server-side would double the images.
On the quick-relaunch branch, the original’s uploaded images are COPIED (not
moved) into staging and passed to create, which lands them in the new
worktree — so a spawn failure here
leaves the original’s images intact on disk (the originals are reclaimed only
when the original’s worktree is torn down by the route, after a successful
spawn). On any error AFTER create, the just-created session is best-effort
torn down and the error rethrown, so no orphaned new session leaks.
Parameters
Section titled “Parameters”originalId
Section titled “originalId”string
issueRef?
Section titled “issueRef?”overrides?
Section titled “overrides?”Returns
Section titled “Returns”Promise<Session>
releasePlanGate()
Section titled “releasePlanGate()”releasePlanGate(
id):boolean
Defined in: src/service.ts:2127
The “Go” gate: release an APPROVED planning session into autonomous execution. Strict — only transitions when the session is in the planning phase AND its plan gate is approved (the reviewer signed off). Flips planPhase → “executing”, steers the agent to implement the approved plan, and emits session:plangate so clients update. Returns false (no-op) when the session is unknown, not planning, or not yet approved. Used by the /go route (interactive) and by PlanGateService for an auto session’s auto-release on approval.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”boolean
rename()
Section titled “rename()”rename(
id,slug,opts):Session|null
Defined in: src/service.ts:1905
Parameters
Section titled “Parameters”string
string
renameLocalBranch
Section titled “renameLocalBranch”boolean
Returns
Section titled “Returns”Session | null
reply()
Section titled “reply()”reply(
id,text):boolean
Defined in: src/service.ts:1938
Steer a session’s live PTY (human-style): deliver the text as a bracketed paste, then submit it with a carriage return.
The wrap is load-bearing for multi-line steers (e.g. a pasted-in critic review).
herdr does NOT bracket-wrap injected text, and back-to-back sends coalesce into a
single PTY read — so a multi-line blob with a trailing “\r” reaches Claude Code as
one chunk, trips its paste heuristic, and the CR is swallowed as just another
newline: message typed-but-unsent. (Single-line steers escaped this because no
embedded “\n” trips the heuristic — which is why short steers worked and reviews
didn’t.) Wrapping the text in the bracketed-paste markers (ESC[200~ … ESC[201~)
gives an explicit paste-end, so the following CR is unambiguously Enter regardless
of read boundaries — deterministic, no timing guesswork. Strip any stray paste
markers from the payload first: a leaked end-marker would close the paste early
(turning the rest into live keystrokes), and a leaked start-marker is benign but
dropped for symmetry. Returns false when the session is unknown OR its pane is dead
(claude exited / terminal reaped) — a live store row can still back a dead pane,
which would make herdr.send throw. The up-front liveness check keeps reply an honest,
non-throwing boolean for human steers, and hands the auto-address loop a clean
“not delivered” instead of relying on it to catch the throw downstream.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”boolean
resolveMerging()
Section titled “resolveMerging()”resolveMerging(
id,didMerge):void
Defined in: src/service.ts:2318
A queue member’s PR resolved (session:git merged/closed). Replaces the bare
per-member clearMerging call: clears the UI mark exactly as before, then
credits the completion tracker. Credit is keyed by #memberToTrain (NOT the
session’s mergingTrainId, which archive may already have nulled).
A merge only counts toward the offer when the session is isolated — a
non-isolated session works in the canonical clone, so its fast-forward would
always report wrong_branch (mirrors the parent feature’s guard). We read
isolated/trainId before clearing, since the mark must be cleared either way.
Emits only via the LATE-CREDIT path: if the train already archived awaiting a
merge, this credit completes it. A credit while the train is still live never
emits — the offer fires on run completion, not first merge.
Parameters
Section titled “Parameters”string
didMerge
Section titled “didMerge”boolean
Returns
Section titled “Returns”void
resume()
Section titled “resume()”resume(
id,opts?):Promise<Session|null>
Defined in: src/service.ts:1774
Bring a finished session back: spawn a fresh claude --resume <pinnedId> in
its still-present worktree so the whole conversation is restored and steerable
again. Re-points the session at the new herdr agent and flips it back to running.
Returns the updated session, or null when it can’t be resumed:
- unknown id, or archived (its worktree was already removed), or
- a pre-feature session with no pinned claude session id to resume. If the herdr agent is still live (a “done” session that’s merely idle at the prompt), there’s nothing to respawn — the current session is handed back so the caller just re-attaches, avoiding a duplicate claude process.
force overrides that re-use: it tears down whatever agent currently backs the
worktree and spawns a fresh claude --resume regardless. This is the explicit
“bring claude back” action (header / card-menu button) for the case the re-use
path can’t see — claude exited but its herdr tab survived as a bare shell, so the
agent still lists as live (idle) and a plain resume would only re-adopt the shell.
We force unconditionally rather than only on a detected husk because herdr ≥0.6
agent list exposes no command/liveness field, so a husk shell and an idle
claude are indistinguishable here (see ui canResume). The tradeoff: if invoked on
a genuinely-live idle claude it respawns one needlessly, resetting that pane’s
terminal scrollback — but --resume restores the FULL conversation, so no work is
lost, and the control is only surfaced/clicked when the user believes they’re
stranded. Guaranteeing the husk case works (always respawn) beats preserving
scrollback in the rare misclick-on-live-claude case.
Parameters
Section titled “Parameters”string
force?
Section titled “force?”boolean
Returns
Section titled “Returns”Promise<Session | null>
retryHalted()
Section titled “retryHalted()”retryHalted(
ids,continueText):Promise<{resumed:number;steered:number;total:number; }>
Defined in: src/service.ts:1990
Retry a set of usage-halted sessions. For each id:
- If its pane is live (herdr still lists it) → steer with
continueTextso the agent can continue from where it stopped (live idle/blocked state). - Otherwise →
resume(id)spawns a freshclaude --resumepane. On any success the haltReason flag is cleared so the UI badge disappears. The steer text is supplied by the caller (localized client-side) — the server stays i18n-agnostic, exactly like broadcast.
Parameters
Section titled “Parameters”string[]
continueText
Section titled “continueText”string
Returns
Section titled “Returns”Promise<{ resumed: number; steered: number; total: number; }>
setReadyToMerge()
Section titled “setReadyToMerge()”setReadyToMerge(
id,ready):void
Defined in: src/service.ts:2104
Toggle the manual “ready to merge” flag (parked / done). Persists it and pushes the change live so every client patches the row without a refetch.
Parameters
Section titled “Parameters”string
boolean
Returns
Section titled “Returns”void
stageRelaunchImages()
Section titled “stageRelaunchImages()”stageRelaunchImages(
originalId):object[]
Defined in: src/service.ts:1618
Stage an original session’s uploaded images for a relaunch-WITH-overrides composer to seed. Copies (not moves) the original’s worktree uploads into the repo staging dir — like the quick-relaunch branch and New Task — and returns the staged path plus its basename for each, capped at MAX_IMAGES so the UI never seeds more chips than a spawn accepts. The copies are recoverable on disk; relaunch() then takes the (possibly operator-edited) list back verbatim, so there is no server-side re-merge.
Each open copies fresh into staging, so a cancelled open or a removed chip orphans its copies. Before staging, we reclaim staged uploads past the TTL (the same sweep the server runs at startup, over the shared staging dir) so repeated opens don’t accumulate.
Parameters
Section titled “Parameters”originalId
Section titled “originalId”string
Returns
Section titled “Returns”object[]
startPreview()
Section titled “startPreview()”startPreview(
id,command):boolean
Defined in: src/service.ts:1948
Steer the agent for session id to start its dev server with command running
in the background. The agent’s PTY is a live Claude Code session — we can’t spawn
processes ourselves, so we deliver a directive asking the agent to do it. Returns
false for an unknown id or a dead pane (same semantics as reply()).
Parameters
Section titled “Parameters”string
command
Section titled “command”string
Returns
Section titled “Returns”boolean
stopPreview()
Section titled “stopPreview()”stopPreview(
id,signal?):object
Defined in: src/service.ts:1969
Stop the previewed dev server for session id by SIGNALLING its process to
terminate. UNLIKE startPreview (which steers the agent to start its dev
server — Shepherd can’t start it itself), this really signals the process,
because Shepherd can find the worktree process listening on the dev port.
killed is a signals-SENT count, NOT a death confirmation (a process may
ignore the signal or take time to exit). This method does NOT release the
preview listener — teardown happens via the poller sweep when the port stops
listening (that port-gone event is the only real “RAM-freed” signal). Idle-stop
passes “SIGTERM” then escalates to “SIGKILL”; force-stop passes “SIGKILL”.
Returns:
- { result: “not_found”, killed: 0 } — unknown id, or deps not wired.
- { result: “not_bound”, killed: 0 } — no live preview for this session.
- { result: “stopped”, killed } — signal dispatched to
killedprocess(es).
Parameters
Section titled “Parameters”string
signal?
Section titled “signal?”Signals = "SIGTERM"
Returns
Section titled “Returns”object
killed
Section titled “killed”killed:
number
result
Section titled “result”result:
"stopped"|"not_bound"|"not_found"
sweepEgressTmp()
Section titled “sweepEgressTmp()”sweepEgressTmp():
void
Defined in: src/service.ts:2527
Startup reconcile: remove any orphaned shepherd-egress/<id> temp dir whose id is
not a currently live (non-archived) session — bounds unbounded growth from teardown
removals missed across a crash/restart. Best-effort (never throws). Call once at
server boot. A live session’s dir (incl. its dns.log) is preserved.
Returns
Section titled “Returns”void
sweepStaleMerging()
Section titled “sweepStaleMerging()”sweepStaleMerging(
now?):void
Defined in: src/service.ts:2387
Liveness-based reclaim: marks live with their train, never on a flat age. Runs
three ordered phases each call (order matters — A feeds B and C):
A. Deregister crashed/gone live trains. A train whose session is missing or
archived, or whose last activity (max(registeredAt, updatedAt)) is past
TRAIN_TRACKER_MAX_MS, is dropped from #liveTrains. Last-activity-keyed so a
slow-but-alive train (still touching its row) is never ceiled mid-run.
B. Clear member marks whose train is no longer live — a mark is released exactly
when its train stops running (cleanly via clearMergingForTrain, or via A’s
deregistration here in the same sweep). No standalone age TTL.
C. Reclaim #trainOffers entries, both no-emit (fail-safe no-offer):
- AWAITING (archived, awaiting a late credit): once the post-archive window
(MERGE_STALE_MS, #426) lapses — preserved exactly.
- LIVE-CRASH ORPHAN (awaitingSince null, train no longer live): A having just
dropped a crashed train, its still-live entry is reclaimed here. The
awaitingSince-null gate keeps a cleanly-archived entry on the AWAITING path.
now injectable for tests.
Parameters
Section titled “Parameters”number = ...
Returns
Section titled “Returns”void
syncWorktreeBranch()
Section titled “syncWorktreeBranch()”syncWorktreeBranch(
id):string|null
Defined in: src/service.ts:1886
Reconcile a session’s stored branch with the one actually checked out in its
worktree. An agent that runs git checkout -b / git branch -m renames the
branch out from under us, so the stored branch goes stale and PR detection
(which queries gh pr list --head <branch>) silently misses the opened PR.
Called by the PR poller on a “no PR found” miss. When the live branch differs,
adopt it (re-point branch) — that alone is what restores PR recognition.
Returns the adopted branch (so the poller can re-query), or null when nothing
changed / it can’t be determined.
The display name follows only when it still trivially mirrors the old branch
(i.e. was auto-derived). A name that already diverged is a chosen name — a manual
rename or an LLM refine — and outranks a raw branch slug, the same precedence
refineNameInBackground enforces. When it does follow, it’s de-duped through
uniqueName like the other automatic rename paths so it can’t clash with a
sibling’s tab label.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string | null