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:
@@ -66,6 +66,7 @@ export class SessionStore {
|
||||
this.addSessionPlatformSourceColumn();
|
||||
this.addObservationModelColumns();
|
||||
this.ensureMergedIntoProjectColumns();
|
||||
this.addObservationSubagentColumns();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -975,6 +976,44 @@ export class SessionStore {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add agent_type and agent_id columns to observations and pending_messages (migration 27).
|
||||
* Mirrors MigrationRunner.addObservationSubagentColumns so bundled artifacts that embed
|
||||
* SessionStore (e.g. context-generator.cjs) stay schema-consistent.
|
||||
*/
|
||||
private addObservationSubagentColumns(): void {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(27) as SchemaVersion | undefined;
|
||||
|
||||
const obsCols = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
|
||||
const obsHasAgentType = obsCols.some(col => col.name === 'agent_type');
|
||||
const obsHasAgentId = obsCols.some(col => col.name === 'agent_id');
|
||||
|
||||
if (!obsHasAgentType) {
|
||||
this.db.run('ALTER TABLE observations ADD COLUMN agent_type TEXT');
|
||||
}
|
||||
if (!obsHasAgentId) {
|
||||
this.db.run('ALTER TABLE observations ADD COLUMN agent_id TEXT');
|
||||
}
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_type ON observations(agent_type)');
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_id ON observations(agent_id)');
|
||||
|
||||
const pendingCols = this.db.query('PRAGMA table_info(pending_messages)').all() as TableColumnInfo[];
|
||||
if (pendingCols.length > 0) {
|
||||
const pendingHasAgentType = pendingCols.some(col => col.name === 'agent_type');
|
||||
const pendingHasAgentId = pendingCols.some(col => col.name === 'agent_id');
|
||||
if (!pendingHasAgentType) {
|
||||
this.db.run('ALTER TABLE pending_messages ADD COLUMN agent_type TEXT');
|
||||
}
|
||||
if (!pendingHasAgentId) {
|
||||
this.db.run('ALTER TABLE pending_messages ADD COLUMN agent_id TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
if (!applied) {
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(27, new Date().toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the memory session ID for a session
|
||||
* Called by SDKAgent when it captures the session ID from the first SDK message
|
||||
@@ -1734,6 +1773,8 @@ export class SessionStore {
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
agent_type?: string | null;
|
||||
agent_id?: string | null;
|
||||
},
|
||||
promptNumber?: number,
|
||||
discoveryTokens: number = 0,
|
||||
@@ -1754,9 +1795,9 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(memory_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, agent_type, agent_id, content_hash, created_at, created_at_epoch,
|
||||
generated_by_model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -1772,6 +1813,8 @@ export class SessionStore {
|
||||
JSON.stringify(observation.files_modified),
|
||||
promptNumber || null,
|
||||
discoveryTokens,
|
||||
observation.agent_type ?? null,
|
||||
observation.agent_id ?? null,
|
||||
contentHash,
|
||||
timestampIso,
|
||||
timestampEpoch,
|
||||
@@ -1863,6 +1906,8 @@ export class SessionStore {
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
agent_type?: string | null;
|
||||
agent_id?: string | null;
|
||||
}>,
|
||||
summary: {
|
||||
request: string;
|
||||
@@ -1889,9 +1934,9 @@ export class SessionStore {
|
||||
const obsStmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(memory_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, agent_type, agent_id, content_hash, created_at, created_at_epoch,
|
||||
generated_by_model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const observation of observations) {
|
||||
@@ -1916,6 +1961,8 @@ export class SessionStore {
|
||||
JSON.stringify(observation.files_modified),
|
||||
promptNumber || null,
|
||||
discoveryTokens,
|
||||
observation.agent_type ?? null,
|
||||
observation.agent_id ?? null,
|
||||
contentHash,
|
||||
timestampIso,
|
||||
timestampEpoch,
|
||||
@@ -1993,6 +2040,8 @@ export class SessionStore {
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
agent_type?: string | null;
|
||||
agent_id?: string | null;
|
||||
}>,
|
||||
summary: {
|
||||
request: string;
|
||||
@@ -2021,9 +2070,9 @@ export class SessionStore {
|
||||
const obsStmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(memory_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, agent_type, agent_id, content_hash, created_at, created_at_epoch,
|
||||
generated_by_model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const observation of observations) {
|
||||
@@ -2048,6 +2097,8 @@ export class SessionStore {
|
||||
JSON.stringify(observation.files_modified),
|
||||
promptNumber || null,
|
||||
discoveryTokens,
|
||||
observation.agent_type ?? null,
|
||||
observation.agent_id ?? null,
|
||||
contentHash,
|
||||
timestampIso,
|
||||
timestampEpoch,
|
||||
@@ -2608,6 +2659,8 @@ export class SessionStore {
|
||||
discovery_tokens: number;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
agent_type?: string | null;
|
||||
agent_id?: string | null;
|
||||
}): { imported: boolean; id: number } {
|
||||
// Check if observation already exists
|
||||
const existing = this.db.prepare(`
|
||||
@@ -2623,8 +2676,9 @@ export class SessionStore {
|
||||
INSERT INTO observations (
|
||||
memory_session_id, project, text, type, title, subtitle,
|
||||
facts, narrative, concepts, files_read, files_modified,
|
||||
prompt_number, discovery_tokens, created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
prompt_number, discovery_tokens, agent_type, agent_id,
|
||||
created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -2641,6 +2695,8 @@ export class SessionStore {
|
||||
obs.files_modified,
|
||||
obs.prompt_number,
|
||||
obs.discovery_tokens || 0,
|
||||
obs.agent_type ?? null,
|
||||
obs.agent_id ?? null,
|
||||
obs.created_at,
|
||||
obs.created_at_epoch
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user