From 25f2d3cf3204248cad6671a8cf7a5817bd58dcea Mon Sep 17 00:00:00 2001 From: bjorger <50590409+bjorger@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:38:58 +0200 Subject: [PATCH] feat(core): Add sub-agent session linkage migration (#31534) Co-authored-by: Cursor --- ...dSubAgentLinkageToAgentExecutionThreads.ts | 86 +++++++++++++++++++ .../db/src/migrations/postgresdb/index.ts | 2 + .../@n8n/db/src/migrations/sqlite/index.ts | 2 + .../entities/agent-execution-thread.entity.ts | 11 +++ .../agents/entities/agent-execution.entity.ts | 5 +- 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 packages/@n8n/db/src/migrations/common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads.ts diff --git a/packages/@n8n/db/src/migrations/common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads.ts b/packages/@n8n/db/src/migrations/common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads.ts new file mode 100644 index 00000000000..5daa7194ccf --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads.ts @@ -0,0 +1,86 @@ +import type { IrreversibleMigration, MigrationContext } from '../migration-types'; + +/** + * Adds the sub-agent session-linkage columns to `agent_execution_threads` + * (parentThreadId / parentAgentId) and widens the agent thread-id columns to + * varchar(128). + * + * Agent thread ids are not bare uuids — several surfaces scope them with a + * prefix and a user id (e.g. the test chat's `test-:`, the + * builder's `builder:`). Those values exceed the original varchar(36) + * of the SDK memory thread (`agents_threads.id`) and the session records + * (`agent_execution_threads.id`, `agent_execution.threadId`), so widen those id + * columns to varchar(128). `parentThreadId` holds such a parent thread id, so it + * is created at varchar(128) directly. Known generated formats stay well below + * 128 chars (for example, `test-:` is about 78 chars with UUIDs). + */ +const COLUMNS_TO_WIDEN: Array<{ table: string; column: string }> = [ + { table: 'agents_threads', column: 'id' }, + { table: 'agent_execution_threads', column: 'id' }, + { table: 'agent_execution', column: 'threadId' }, +]; + +const SQLITE_DECLARED_TYPE_REPLACEMENTS: Array<{ table: string; from: string; to: string }> = [ + { table: 'agents_threads', from: '"id" varchar(36)', to: '"id" varchar(128)' }, + { + table: 'agent_execution_threads', + from: '"id" varchar(36)', + to: '"id" varchar(128)', + }, + { + table: 'agent_execution', + from: '"threadId" varchar(36)', + to: '"threadId" varchar(128)', + }, +]; + +export class AddSubAgentLinkageToAgentExecutionThreads1784000000022 + implements IrreversibleMigration +{ + async up({ + schemaBuilder: { addColumns, column }, + isPostgres, + isSqlite, + runQuery, + escape, + tablePrefix, + }: MigrationContext) { + await addColumns('agent_execution_threads', [ + column('parentThreadId') + .varchar(128) + .comment('Parent session thread id that delegated this subagent run.'), + column('parentAgentId') + .varchar(36) + .comment('Saved agent id of the parent that delegated this subagent run.'), + ]); + + if (isPostgres) { + for (const { table, column: columnName } of COLUMNS_TO_WIDEN) { + await runQuery( + `ALTER TABLE ${escape.tableName(table)} ALTER COLUMN ${escape.columnName(columnName)} TYPE VARCHAR(128);`, + ); + } + } else if (isSqlite) { + // SQLite does not enforce varchar limits, but keep the declared schema in sync for documentation. + await this.widenSqliteDeclaredColumnTypes({ runQuery, tablePrefix }); + } + } + + private async widenSqliteDeclaredColumnTypes({ + runQuery, + tablePrefix, + }: Pick) { + await runQuery('PRAGMA writable_schema = 1;'); + + try { + for (const { table, from, to } of SQLITE_DECLARED_TYPE_REPLACEMENTS) { + await runQuery( + "UPDATE sqlite_master SET sql = replace(sql, :from, :to) WHERE type = 'table' AND name = :tableName", + { from, to, tableName: `${tablePrefix}${table}` }, + ); + } + } finally { + await runQuery('PRAGMA writable_schema = 0;'); + } + } +} diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index 39acf9e6c5b..bccd4c4b029 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -197,6 +197,7 @@ import { CreateAgentFilesTable1784000000018 } from '../common/1784000000018-Crea import { AddCustomTelemetryTagsToProject1784000000019 } from '../common/1784000000019-AddCustomTelemetryTagsToProject'; import { CreateWorkflowPublicationOutboxTable1784000000020 } from '../common/1784000000020-CreateWorkflowPublicationOutboxTable'; import { CreateAgentTaskDefinitionTable1784000000021 } from '../common/1784000000021-CreateAgentTaskDefinitionTable'; +import { AddSubAgentLinkageToAgentExecutionThreads1784000000022 } from '../common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ @@ -399,4 +400,5 @@ export const postgresMigrations: Migration[] = [ AddCustomTelemetryTagsToProject1784000000019, CreateWorkflowPublicationOutboxTable1784000000020, CreateAgentTaskDefinitionTable1784000000021, + AddSubAgentLinkageToAgentExecutionThreads1784000000022, ]; diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts index c4d2fe1e3bd..b18cec49af0 100644 --- a/packages/@n8n/db/src/migrations/sqlite/index.ts +++ b/packages/@n8n/db/src/migrations/sqlite/index.ts @@ -189,6 +189,7 @@ import { AddLastUsedAtToApiKey1784000000017 } from '../common/1784000000017-AddL import { CreateAgentFilesTable1784000000018 } from '../common/1784000000018-CreateAgentFilesTable'; import { AddCustomTelemetryTagsToProject1784000000019 } from '../common/1784000000019-AddCustomTelemetryTagsToProject'; import { CreateWorkflowPublicationOutboxTable1784000000020 } from '../common/1784000000020-CreateWorkflowPublicationOutboxTable'; +import { AddSubAgentLinkageToAgentExecutionThreads1784000000022 } from '../common/1784000000022-AddSubAgentLinkageToAgentExecutionThreads'; import type { Migration } from '../migration-types'; import { CreateAgentTaskDefinitionTable1784000000021 } from './1784000000021-CreateAgentTaskDefinitionTable'; @@ -385,6 +386,7 @@ const sqliteMigrations: Migration[] = [ AddCustomTelemetryTagsToProject1784000000019, CreateWorkflowPublicationOutboxTable1784000000020, CreateAgentTaskDefinitionTable1784000000021, + AddSubAgentLinkageToAgentExecutionThreads1784000000022, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts b/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts index fe35ca78c56..a3af8b3cdbd 100644 --- a/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts +++ b/packages/cli/src/modules/agents/entities/agent-execution-thread.entity.ts @@ -40,6 +40,17 @@ export class AgentExecutionThread extends WithTimestampsAndStringId { @Column({ type: 'varchar', length: 8, nullable: true }) emoji: string | null; + /** + * Parent session thread id that delegated this run, for navigating back to + * it. Holds another thread's id, so it matches the id column width (128). + */ + @Column({ type: 'varchar', length: 128, nullable: true }) + parentThreadId: string | null; + + /** Saved agent id of the parent that delegated this run. */ + @Column({ type: 'varchar', length: 36, nullable: true }) + parentAgentId: string | null; + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'projectId' }) project: Project; diff --git a/packages/cli/src/modules/agents/entities/agent-execution.entity.ts b/packages/cli/src/modules/agents/entities/agent-execution.entity.ts index 1ef02864171..e0c5c002f20 100644 --- a/packages/cli/src/modules/agents/entities/agent-execution.entity.ts +++ b/packages/cli/src/modules/agents/entities/agent-execution.entity.ts @@ -25,7 +25,10 @@ export class AgentExecution extends WithTimestampsAndStringId { @JoinColumn({ name: 'threadId' }) thread: AgentExecutionThread; - @Column({ type: 'varchar', length: 36 }) + // Thread ids are scoped with prefixes/user ids on some surfaces (e.g. + // `test-:`), so they exceed a bare uuid — widened to 128 in + // AddSubAgentLinkageToAgentExecutionThreads1784000000022. + @Column({ type: 'varchar', length: 128 }) threadId: string; @Column({ type: 'varchar', length: 16 })