Skip to content

Commit f87e5cf

Browse files
committed
fix(runtime): retain session proof for lifecycle cleanup
1 parent 80a8dd2 commit f87e5cf

3 files changed

Lines changed: 36 additions & 9 deletions

File tree

docs/architecture.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ stable home and can be torn down on `hack down`.
125125
Lifecycle state is checkout-local at `.hack/.internal/lifecycle/state.json`. Each compose instance
126126
has its own entry, so starting a branch instance does not replace the base instance's entry in the
127127
same checkout. Cleanup reconciles the saved process group with the live process table; if the group
128-
leader exited while members remain, `hack doctor` reports the orphan and `hack down` terminates the
129-
persisted leaderless group. A live group leader without its saved pane PID is not trusted as lifecycle
130-
ownership because the PID may have been reused.
128+
leader exited while members remain, `hack doctor` reports the orphan. `hack down` terminates the
129+
persisted leaderless group only while a matching lifecycle mux session still proves ownership; without
130+
that session, cleanup stays non-destructive because the numeric PGID may have been reused. A live group
131+
leader without its saved pane PID is likewise not trusted as lifecycle ownership.
131132

132133
For fixed-port helpers such as SSM/database/search tunnels, lifecycle config can also declare a
133134
`singleton` listener set. This lets Hack reuse an already-running equivalent helper or fail fast on

src/lib/project-lifecycle-processes.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,14 @@ export function resolvePersistedLifecycleProcessGroupIds(opts: {
160160
});
161161
}
162162

163-
/** Trust all live roots with a session, or only leaderless persisted groups without one. */
163+
/** Only trust persisted lifecycle metadata when a matching mux session was observed live. */
164164
export function resolveLifecycleStopProcessGroupIds(opts: {
165165
readonly matchedLiveSession: boolean;
166166
readonly lifecycleEntry: LifecycleStateEntry | null;
167167
readonly snapshot: readonly ProcessSnapshotRow[];
168168
}): number[] {
169169
if (!opts.matchedLiveSession) {
170-
return resolveLeaderlessPersistedLifecycleProcessGroupIds({
171-
lifecycleEntry: opts.lifecycleEntry,
172-
snapshot: opts.snapshot,
173-
});
170+
return [];
174171
}
175172
return resolvePersistedLifecycleProcessGroupIds({
176173
lifecycleEntry: opts.lifecycleEntry,

tests/project-lifecycle-processes.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ test("resolveLifecycleStopProcessGroupIds skips persisted cleanup without a live
274274
expect(groups).toEqual([]);
275275
});
276276

277-
test("resolveLifecycleStopProcessGroupIds recovers a leaderless persisted group without a mux session", () => {
277+
test("resolveLifecycleStopProcessGroupIds skips a leaderless persisted group without a mux session", () => {
278278
const groups = resolveLifecycleStopProcessGroupIds({
279279
matchedLiveSession: false,
280280
lifecycleEntry: {
@@ -300,6 +300,35 @@ test("resolveLifecycleStopProcessGroupIds recovers a leaderless persisted group
300300
],
301301
});
302302

303+
expect(groups).toEqual([]);
304+
});
305+
306+
test("resolveLifecycleStopProcessGroupIds recovers a leaderless persisted group with a live mux session", () => {
307+
const groups = resolveLifecycleStopProcessGroupIds({
308+
matchedLiveSession: true,
309+
lifecycleEntry: {
310+
composeProject: "event-agent",
311+
projectName: "event-agent",
312+
branch: "feature-cleanup",
313+
sessionName: "event-agent--lifecycle-feature-cleanup",
314+
backend: "tmux",
315+
updatedAt: "2026-04-01T14:00:00.000Z",
316+
processes: [
317+
{
318+
name: "proxy",
319+
windowName: "proxy",
320+
logPath: "/tmp/event-agent.log",
321+
panePid: 500,
322+
processGroupId: 500,
323+
},
324+
],
325+
},
326+
snapshot: [
327+
{ pid: 501, ppid: 1, processGroupId: 500 },
328+
{ pid: 502, ppid: 501, processGroupId: 500 },
329+
],
330+
});
331+
303332
expect(groups).toEqual([500]);
304333
});
305334

0 commit comments

Comments
 (0)