feat(hooks): Hook Session Lifecycle & Compression Integration (#14151)

This commit is contained in:
Edilmo Palencia
2025-12-03 09:04:13 -08:00
committed by GitHub
parent 7a6d3067c6
commit 1c12da1fad
27 changed files with 1026 additions and 302 deletions
@@ -220,6 +220,57 @@ function validateNotificationInput(input: Record<string, unknown>): {
};
}
/**
* Validates SessionStart input fields
*/
function validateSessionStartInput(input: Record<string, unknown>): {
source: SessionStartSource;
} {
const source = input['source'];
if (typeof source !== 'string') {
throw new Error(
'Invalid input for SessionStart hook event: source must be a string',
);
}
return {
source: source as SessionStartSource,
};
}
/**
* Validates SessionEnd input fields
*/
function validateSessionEndInput(input: Record<string, unknown>): {
reason: SessionEndReason;
} {
const reason = input['reason'];
if (typeof reason !== 'string') {
throw new Error(
'Invalid input for SessionEnd hook event: reason must be a string',
);
}
return {
reason: reason as SessionEndReason,
};
}
/**
* Validates PreCompress input fields
*/
function validatePreCompressInput(input: Record<string, unknown>): {
trigger: PreCompressTrigger;
} {
const trigger = input['trigger'];
if (typeof trigger !== 'string') {
throw new Error(
'Invalid input for PreCompress hook event: trigger must be a string',
);
}
return {
trigger: trigger as PreCompressTrigger,
};
}
/**
* Hook event bus that coordinates hook execution across the system
*/
@@ -704,6 +755,21 @@ export class HookEventHandler {
);
break;
}
case HookEventName.SessionStart: {
const { source } = validateSessionStartInput(enrichedInput);
result = await this.fireSessionStartEvent(source);
break;
}
case HookEventName.SessionEnd: {
const { reason } = validateSessionEndInput(enrichedInput);
result = await this.fireSessionEndEvent(reason);
break;
}
case HookEventName.PreCompress: {
const { trigger } = validatePreCompressInput(enrichedInput);
result = await this.firePreCompressEvent(trigger);
break;
}
default:
throw new Error(`Unsupported hook event: ${request.eventName}`);
}
+12 -2
View File
@@ -238,8 +238,18 @@ export class HookRunner {
debugLogger.warn(`Hook stdin error: ${err}`);
}
});
child.stdin.write(JSON.stringify(input));
child.stdin.end();
// Wrap write operations in try-catch to handle synchronous EPIPE errors
// that occur when the child process exits before we finish writing
try {
child.stdin.write(JSON.stringify(input));
child.stdin.end();
} catch (err) {
// Ignore EPIPE errors which happen when the child process closes stdin early
if (err instanceof Error && 'code' in err && err.code !== 'EPIPE') {
debugLogger.warn(`Hook stdin write error: ${err}`);
}
}
}
// Collect stdout
+7
View File
@@ -19,3 +19,10 @@ export { HookEventHandler } from './hookEventHandler.js';
export type { HookRegistryEntry, ConfigSource } from './hookRegistry.js';
export type { AggregatedHookResult } from './hookAggregator.js';
export type { HookEventContext } from './hookPlanner.js';
// Export hook trigger functions
export {
fireSessionStartHook,
fireSessionEndHook,
firePreCompressHook,
} from '../core/sessionHookTriggers.js';
-1
View File
@@ -463,7 +463,6 @@ export enum SessionStartSource {
Startup = 'startup',
Resume = 'resume',
Clear = 'clear',
Compress = 'compress',
}
/**