Skip to content

Commit b9fad9a

Browse files
heavygeecursoragent
andcommitted
fix(cli): runner-spawned children use 'Stopped by runner' as default archive reason
Addresses bot review of tiann#923: with the tiann#914 default-archiveReason flip to 'Hub restart', runner-driven SIGTERM paths (`hapi runner stop-session`, webhook-timeout cleanup at run.ts:587, orphan-cleanup at run.ts:267) all mislabel as 'Hub restart' which is also inaccurate audit-trail noise. Smallest defensible change: parameterise the lifecycle default via HAPI_DEFAULT_ARCHIVE_REASON env, and have the runner set 'Stopped by runner' on spawn. Terminal-launched sessions (no runner parent, no env var) still default to 'Hub restart' since hub-restart cascade documented at tiann#915 is the most plausible SIGTERM source for those. Explicit overrides via setArchiveReason (KillSession RPC, SIGINT Ctrl-C, markCrash uncaught exception) still win. Two new unit tests cover the env-var default and the override precedence. Refs tiann#914. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3db4104 commit b9fad9a

3 files changed

Lines changed: 79 additions & 8 deletions

File tree

cli/src/agent/runnerLifecycle.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,58 @@ describe('createRunnerLifecycle archiveReason defaults (tiann/hapi#914)', () =>
7070
archiveReason: 'Session crashed'
7171
})
7272
})
73+
74+
// tiann/hapi#914 review feedback: runner-spawned children should not
75+
// get the 'Hub restart' label when the runner SIGTERMs them via
76+
// stop-session, webhook-timeout cleanup, or orphan cleanup. The runner
77+
// sets HAPI_DEFAULT_ARCHIVE_REASON on the child's spawn env so the
78+
// default reason is path-accurate without changing every kill site.
79+
it('honours HAPI_DEFAULT_ARCHIVE_REASON env as the default reason', async () => {
80+
const session = makeFakeSession()
81+
const original = process.env.HAPI_DEFAULT_ARCHIVE_REASON
82+
process.env.HAPI_DEFAULT_ARCHIVE_REASON = 'Stopped by runner'
83+
try {
84+
const lifecycle = createRunnerLifecycle({
85+
session: session as unknown as Parameters<typeof createRunnerLifecycle>[0]['session'],
86+
logTag: 'test'
87+
})
88+
89+
await lifecycle.cleanup()
90+
91+
expect(session.metadataWrites[0]).toMatchObject({
92+
archiveReason: 'Stopped by runner'
93+
})
94+
} finally {
95+
if (original === undefined) {
96+
delete process.env.HAPI_DEFAULT_ARCHIVE_REASON
97+
} else {
98+
process.env.HAPI_DEFAULT_ARCHIVE_REASON = original
99+
}
100+
}
101+
})
102+
103+
it('setArchiveReason still wins over HAPI_DEFAULT_ARCHIVE_REASON', async () => {
104+
const session = makeFakeSession()
105+
const original = process.env.HAPI_DEFAULT_ARCHIVE_REASON
106+
process.env.HAPI_DEFAULT_ARCHIVE_REASON = 'Stopped by runner'
107+
try {
108+
const lifecycle = createRunnerLifecycle({
109+
session: session as unknown as Parameters<typeof createRunnerLifecycle>[0]['session'],
110+
logTag: 'test'
111+
})
112+
113+
lifecycle.setArchiveReason('User terminated')
114+
await lifecycle.cleanup()
115+
116+
expect(session.metadataWrites[0]).toMatchObject({
117+
archiveReason: 'User terminated'
118+
})
119+
} finally {
120+
if (original === undefined) {
121+
delete process.env.HAPI_DEFAULT_ARCHIVE_REASON
122+
} else {
123+
process.env.HAPI_DEFAULT_ARCHIVE_REASON = original
124+
}
125+
}
126+
})
73127
})

cli/src/agent/runnerLifecycle.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,24 @@ export type RunnerLifecycle = {
2323

2424
export function createRunnerLifecycle(options: RunnerLifecycleOptions): RunnerLifecycle {
2525
let exitCode = 0
26-
// tiann/hapi#914: default reason is 'Hub restart' (parent-driven SIGTERM
27-
// is the most common non-user cause). Genuine user actions (clicking
28-
// Archive in the web UI, or Ctrl-C in a local terminal) explicitly
29-
// reassign this via `setArchiveReason` BEFORE `cleanupAndExit` runs:
26+
// tiann/hapi#914: default reason is parent-driven SIGTERM (the most
27+
// common non-user cause). Genuine user actions (clicking Archive in the
28+
// web UI, or Ctrl-C in a local terminal) explicitly reassign this via
29+
// `setArchiveReason` BEFORE `cleanupAndExit` runs:
3030
// - KillSession RPC handler → 'User terminated' (see registerKillSessionHandler)
3131
// - SIGINT handler → 'User terminated' (Ctrl-C in local terminal)
3232
// - uncaughtException/Reject → 'Session crashed' (via markCrash)
33-
// Out-of-band SIGTERM (hub-restart cascade, `kill <pid>` from host) keeps
34-
// the default and is correctly labelled 'Hub restart' on the audit trail.
35-
let archiveReason = 'Hub restart'
33+
//
34+
// The default is parameterised via `HAPI_DEFAULT_ARCHIVE_REASON` so that
35+
// the runner (cli/src/runner/run.ts) can label children it spawns with
36+
// 'Stopped by runner' — runner-driven SIGTERMs (stop-session command,
37+
// webhook-timeout cleanup, orphan cleanup) are NOT hub restarts, and
38+
// hub-restart cascades for runner-spawned children also flow through
39+
// the runner in production. Terminal-launched sessions (no runner
40+
// parent) fall back to 'Hub restart' since the most plausible
41+
// out-of-band SIGTERM source for those is the systemd hub-restart
42+
// cascade documented at tiann/hapi#915.
43+
let archiveReason = process.env.HAPI_DEFAULT_ARCHIVE_REASON || 'Hub restart'
3644
let sessionEndReason: SessionEndReason = 'terminated'
3745
let cleanupStarted = false
3846
let cleanupPromise: Promise<void> | null = null

cli/src/runner/run.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,16 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
456456
stdio: ['ignore', 'pipe', 'pipe'], // Capture stdout/stderr for debugging
457457
env: {
458458
...process.env,
459-
...extraEnv
459+
...extraEnv,
460+
// tiann/hapi#914 review: runner-spawned children get SIGTERMed
461+
// by three runner paths (stopSession, webhook-timeout cleanup,
462+
// orphan cleanup). All three are runner actions, not hub
463+
// restarts and not user terminations. Override the lifecycle
464+
// default so audit-trail archives accurately attribute to the
465+
// runner. Genuine user actions (web Archive via KillSession
466+
// RPC, terminal Ctrl-C via SIGINT) still override to
467+
// 'User terminated' in runnerLifecycle.
468+
HAPI_DEFAULT_ARCHIVE_REASON: 'Stopped by runner'
460469
}
461470
});
462471

0 commit comments

Comments
 (0)