fix(hooks): keep a keep-alive turn's hooks out of the run summary

Merge-time fix for #666. `recordHookEvent()` ran for every `stop`, the run
summary files a `stop` as a warning, and the Away digest lists warnings under
Needs Attention, so each keep-alive nudge added a row there and to the
session's Summary timeline. The hook route now skips the run summary while
`keepAliveReason` is set (the keep-alive turn's `stop` and `idle_prompt`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-22 19:10:48 +02:00
1 parent f334b59dd3
commit 2a56b2a997
5 files changed
+37 -5

No files matched your search

+30
View File
@@ -427,6 +427,36 @@ describe('hook-event-routes', () => {
expect(mockTracker.recordHookEvent).toHaveBeenCalledWith('stop', expect.any(Object));
});
/**
* The run summary files every `stop` as a warning, and the Away digest lists warnings under
* Needs Attention, so a keep-alive turn's hooks would add a row there (and to the session's
* Summary timeline) for every nudge. They are Codeman's own turn, not the agent's.
*/
it("keeps a keep-alive turn's stop and idle prompt out of the run summary", async () => {
const sessionId = harness.ctx._sessionId;
const mockTracker = { recordHookEvent: vi.fn() };
harness.ctx.runSummaryTrackers.set(sessionId, mockTracker as never);
harness.ctx._session.keepAliveTurnActive = true;
for (const event of ['stop', 'idle_prompt']) {
const res = await harness.app.inject({
method: 'POST',
url: '/api/hook-event',
payload: { event, sessionId, data: null },
});
expect(res.statusCode).toBe(200);
}
approvalInbox.resolveForSession(sessionId, 'dismissed');
expect(mockTracker.recordHookEvent).not.toHaveBeenCalled();
// The next real turn is recorded as before.
await harness.app.inject({
method: 'POST',
url: '/api/hook-event',
payload: { event: 'stop', sessionId, data: null },
});
expect(mockTracker.recordHookEvent).toHaveBeenCalledWith('stop', expect.any(Object));
});
it('starts transcript watcher when transcript_path is provided', async () => {
const res = await harness.app.inject({
method: 'POST',