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:
Alex Newman
2026-04-19 14:58:01 -07:00
committed by GitHub
parent 306a0b1de9
commit 789efe4234
27 changed files with 1381 additions and 361 deletions
+59 -2
View File
@@ -1,5 +1,6 @@
import { Database } from 'bun:sqlite';
import { Migration } from './Database.js';
import { logger } from '../../utils/logger.js';
// Re-export MigrationRunner for SessionStore migration extraction
export { MigrationRunner } from './migrations/runner.js';
@@ -572,6 +573,61 @@ export const migration009: Migration = {
}
};
/**
* Migration 010: Label observations (and their queue rows) with the subagent identity.
*
* Claude Code hooks that fire inside a subagent carry agent_id and agent_type on the
* stdin payload. These flow hook → worker → pending_messages → SDK storage so that
* observation rows can be attributed to the originating subagent. Main-session rows
* keep NULL for both columns.
*/
export const migration010: Migration = {
version: 27,
up: (db: Database) => {
const added: string[] = [];
const obsColumns = db.prepare('PRAGMA table_info(observations)').all() as Array<{ name: string }>;
const obsHasAgentType = obsColumns.some(c => c.name === 'agent_type');
const obsHasAgentId = obsColumns.some(c => c.name === 'agent_id');
if (!obsHasAgentType) {
db.run('ALTER TABLE observations ADD COLUMN agent_type TEXT');
added.push('observations.agent_type');
}
if (!obsHasAgentId) {
db.run('ALTER TABLE observations ADD COLUMN agent_id TEXT');
added.push('observations.agent_id');
}
db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_type ON observations(agent_type)');
db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_id ON observations(agent_id)');
// Also thread the same fields through the pending_messages queue so the label
// survives worker restarts between enqueue and SDK-agent processing.
const pendingColumns = db.prepare('PRAGMA table_info(pending_messages)').all() as Array<{ name: string }>;
if (pendingColumns.length > 0) {
const pendingHasAgentType = pendingColumns.some(c => c.name === 'agent_type');
const pendingHasAgentId = pendingColumns.some(c => c.name === 'agent_id');
if (!pendingHasAgentType) {
db.run('ALTER TABLE pending_messages ADD COLUMN agent_type TEXT');
added.push('pending_messages.agent_type');
}
if (!pendingHasAgentId) {
db.run('ALTER TABLE pending_messages ADD COLUMN agent_id TEXT');
added.push('pending_messages.agent_id');
}
}
logger.debug(
'DB',
added.length > 0
? `[migration010] Added columns: ${added.join(', ')}`
: '[migration010] Subagent identity columns already present; ensured indexes'
);
},
down: (_db: Database) => {
// SQLite DROP COLUMN not fully supported; no-op
}
};
/**
* All migrations in order
*/
@@ -584,5 +640,6 @@ export const migrations: Migration[] = [
migration006,
migration007,
migration008,
migration009
];
migration009,
migration010
];