feat: disable subagent summaries, label subagent observations (#2073)
* feat: disable subagent summaries and label subagent observations Detect Claude Code subagent hook context via `agent_id`/`agent_type` on stdin, short-circuit the Stop-hook summary path when present, and thread the subagent identity end-to-end onto observation rows (new `agent_type` and `agent_id` columns, migration 010 at version 27). Main-session rows remain NULL; content-hash dedup is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address PR #2073 review feedback - Narrow summarize subagent guard to agentId only so --agent-started main sessions still own their summary (agentType alone is main-session). - Remove now-dead agentId/agentType spreads from the summarize POST body. - Always overwrite pendingAgentId/pendingAgentType in SDK/Gemini/OpenRouter agents (clears stale subagent identity on main-session messages after a subagent message in the same batch). - Add idx_observations_agent_id index in migration 010 + the mirror migration in SessionStore + the runner. - Replace console.log in migration010 with logger.debug. - Update summarize test: agentType alone no longer short-circuits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit + claude-review iteration 4 feedback - SessionRoutes.handleSummarizeByClaudeId: narrow worker-side guard to agentId only (matches hook-side). agentType alone = --agent main session, which still owns its summary. - ResponseProcessor: wrap storeObservations in try/finally so pendingAgentId/Type clear even if storage throws. Prevents stale subagent identity from leaking into the next batch on error. - SessionStore.importObservation + bulk.importObservation: persist agent_type/agent_id so backup/import round-trips preserve subagent attribution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish: claude-review iteration 5 cleanup - Use ?? not || for nullable subagent fields in PendingMessageStore (prevents treating empty string as null). - Simplify observation.ts body spread — include fields unconditionally; JSON.stringify drops undefined anyway. - Narrow any[] to Array<{ name: string }> in migration010 column checks. - Add trailing newline to migrations.ts. - Document in observations/store.ts why the dedup hash intentionally excludes agent fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish: claude-review iteration 7 feedback - claude-code adapter: add 128-char safety cap on agent_id/agent_type so a malformed Claude Code payload cannot balloon DB rows. Empty strings now also treated as absent. - migration010: state-aware debug log lists only columns actually added; idempotent re-runs log "already present; ensured indexes". - Add 3 adapter tests covering the length cap boundary and empty-string rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: skip subagent summary before worker bootstrap Move the agentId short-circuit above ensureWorkerRunning() so a Stop hook fired inside a subagent does not trigger worker startup just to return early. Addresses CodeRabbit nit on summarize.ts:36-47. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,13 @@ import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.
|
||||
|
||||
// Maps Claude Code stdin format (session_id, cwd, tool_name, etc.)
|
||||
// SessionStart hooks receive no stdin, so we must handle undefined input gracefully
|
||||
|
||||
// Defensive cap: Claude Code's agent identifiers are short (e.g., "agent-abc123", "Explore").
|
||||
// Ignore anything longer than 128 chars so a malformed payload cannot balloon DB rows.
|
||||
const MAX_AGENT_FIELD_LEN = 128;
|
||||
const pickAgentField = (v: unknown): string | undefined =>
|
||||
typeof v === 'string' && v.length > 0 && v.length <= MAX_AGENT_FIELD_LEN ? v : undefined;
|
||||
|
||||
export const claudeCodeAdapter: PlatformAdapter = {
|
||||
normalizeInput(raw) {
|
||||
const r = (raw ?? {}) as any;
|
||||
@@ -13,6 +20,8 @@ export const claudeCodeAdapter: PlatformAdapter = {
|
||||
toolInput: r.tool_input,
|
||||
toolResponse: r.tool_response,
|
||||
transcriptPath: r.transcript_path,
|
||||
agentId: pickAgentField(r.agent_id),
|
||||
agentType: pickAgentField(r.agent_type),
|
||||
};
|
||||
},
|
||||
formatOutput(result) {
|
||||
|
||||
@@ -57,7 +57,9 @@ export const observationHandler: EventHandler = {
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
tool_response: toolResponse,
|
||||
cwd
|
||||
cwd,
|
||||
agentId: input.agentId,
|
||||
agentType: input.agentType
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,20 @@ const MAX_WAIT_FOR_SUMMARY_MS = 110_000; // 110s — fits within Stop hook's 120
|
||||
|
||||
export const summarizeHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Skip summaries in subagent context — subagents do not own the session summary.
|
||||
// Gate on agentId only: that field is present exclusively for Task-spawned subagents.
|
||||
// agentType alone (no agentId) indicates `--agent`-started main sessions, which still
|
||||
// own their summary. Do this BEFORE ensureWorkerRunning() so a subagent Stop hook
|
||||
// does not bootstrap the worker.
|
||||
if (input.agentId) {
|
||||
logger.debug('HOOK', 'Skipping summary: subagent context detected', {
|
||||
sessionId: input.sessionId,
|
||||
agentId: input.agentId,
|
||||
agentType: input.agentType
|
||||
});
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
// Ensure worker is running before any other logic
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface NormalizedHookInput {
|
||||
edits?: unknown[]; // afterFileEdit
|
||||
// Platform-specific metadata (source, reason, trigger, mcp_context, etc.)
|
||||
metadata?: Record<string, unknown>;
|
||||
// Claude Code subagent identity — present only when hook fires inside a subagent.
|
||||
// Main session has both undefined. Discriminator for subagent context.
|
||||
agentId?: string; // Claude Code subagent agent_id (undefined in main session)
|
||||
agentType?: string; // Claude Code subagent agent_type (undefined in main session)
|
||||
}
|
||||
|
||||
export interface HookResult {
|
||||
|
||||
Reference in New Issue
Block a user