n8n
Version:
n8n Workflow Automation Tool
443 lines • 19.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstanceAiMemoryService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const di_1 = require("@n8n/di");
const instance_ai_1 = require("@n8n/instance-ai");
const db_snapshot_storage_1 = require("./storage/db-snapshot-storage");
const bad_request_error_1 = require("../../errors/response-errors/bad-request.error");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const durable_log_metrics_1 = require("./event-bus/durable-log-metrics");
const message_parser_1 = require("./message-parser");
const instance_ai_checkpoint_repository_1 = require("./repositories/instance-ai-checkpoint.repository");
const instance_ai_event_log_repository_1 = require("./repositories/instance-ai-event-log.repository");
const instance_ai_pending_confirmation_repository_1 = require("./repositories/instance-ai-pending-confirmation.repository");
const typeorm_agent_memory_1 = require("./storage/typeorm-agent-memory");
function isAgentMessageLike(value) {
return (typeof value === 'object' &&
value !== null &&
typeof value.id === 'string' &&
'role' in value);
}
function isRestorableMessage(value) {
if (typeof value.id !== 'string' || value.id.length === 0)
return false;
if (value.type === 'custom')
return typeof value.data === 'object' && value.data !== null;
return typeof value.role === 'string' && Array.isArray(value.content);
}
function toRestorableMessage(value) {
const rawCreatedAt = value.createdAt;
const createdAt = rawCreatedAt instanceof Date
? rawCreatedAt
: typeof rawCreatedAt === 'string'
? new Date(rawCreatedAt)
: undefined;
if (!createdAt || Number.isNaN(createdAt.getTime()))
return undefined;
const candidate = { ...value, createdAt };
return isRestorableMessage(candidate) ? candidate : undefined;
}
function messageCreatedAtMs(message) {
const at = message.createdAt;
if (at instanceof Date)
return at.getTime();
const parsed = new Date(at).getTime();
return Number.isNaN(parsed) ? 0 : parsed;
}
function collectInFlightCheckpointMessages(checkpoints) {
const merged = [];
const seen = new Set();
for (const checkpoint of checkpoints) {
const stateMessages = checkpoint.state?.messageList?.messages ?? [];
for (const candidate of stateMessages) {
if (!isAgentMessageLike(candidate) || seen.has(candidate.id))
continue;
seen.add(candidate.id);
merged.push({
...candidate,
createdAt: candidate.createdAt instanceof Date ? candidate.createdAt : new Date(candidate.createdAt),
});
}
}
return merged;
}
function mergeMessagesById(stored, extras) {
if (extras.length === 0)
return stored;
const byId = new Map();
for (const message of stored)
byId.set(message.id, message);
for (const message of extras)
if (!byId.has(message.id))
byId.set(message.id, message);
return [...byId.values()].sort((a, b) => messageCreatedAtMs(a) - messageCreatedAtMs(b));
}
function collectUnfinishedRunIds(rows) {
const unfinished = new Set();
for (const row of rows) {
if (row.event.type === 'run-start')
unfinished.add(row.runId);
else if (row.event.type === 'run-finish')
unfinished.delete(row.runId);
}
return unfinished;
}
function collectSuspendedHostRunIds(checkpoints) {
const suspended = new Set();
for (const checkpoint of checkpoints) {
if (checkpoint.state?.status === 'suspended' && checkpoint.hostRunId) {
suspended.add(checkpoint.hostRunId);
}
}
return suspended;
}
function buildLogDerivedSnapshots(rows, skipRunIds, skipGroupIds) {
const groupKeyByRun = new Map();
for (const row of rows) {
if (row.event.type === 'run-start') {
const groupId = row.event.payload.messageGroupId;
if (typeof groupId === 'string' && groupId)
groupKeyByRun.set(row.runId, groupId);
}
}
const skipGroupKeys = new Set(skipGroupIds);
for (const runId of skipRunIds) {
const groupId = groupKeyByRun.get(runId);
if (groupId)
skipGroupKeys.add(groupId);
}
const groups = new Map();
let skippedInFlight = false;
for (const row of rows) {
if (!row.runId)
continue;
const messageGroupId = groupKeyByRun.get(row.runId);
const key = messageGroupId ?? row.runId;
if (skipRunIds.has(row.runId) || skipGroupKeys.has(key)) {
skippedInFlight = true;
continue;
}
let group = groups.get(key);
if (!group) {
group = {
runIds: [],
events: [],
messageGroupId,
anchorAt: row.createdAt,
lastAt: row.createdAt,
};
groups.set(key, group);
}
if (!group.runIds.includes(row.runId))
group.runIds.push(row.runId);
group.events.push(row.event);
if (row.runId === group.runIds[0] && row.createdAt > group.anchorAt) {
group.anchorAt = row.createdAt;
}
if (row.createdAt > group.lastAt)
group.lastAt = row.createdAt;
}
const entries = [];
for (const group of groups.values()) {
const hasContent = group.events.some((e) => e.type !== 'run-start' && e.type !== 'run-finish');
if (!hasContent)
continue;
entries.push({
tree: (0, instance_ai_1.buildAgentTreeFromEvents)(group.events),
runId: group.runIds[group.runIds.length - 1],
messageGroupId: group.messageGroupId,
runIds: group.runIds,
createdAt: group.anchorAt,
updatedAt: group.lastAt,
});
}
return { entries, skippedInFlight };
}
let InstanceAiMemoryService = class InstanceAiMemoryService {
constructor(logger, globalConfig, agentMemory, dbSnapshotStorage, checkpointRepository, pendingConfirmationRepository, eventLogRepository, durableLogMetrics) {
this.logger = logger;
this.agentMemory = agentMemory;
this.dbSnapshotStorage = dbSnapshotStorage;
this.checkpointRepository = checkpointRepository;
this.pendingConfirmationRepository = pendingConfirmationRepository;
this.eventLogRepository = eventLogRepository;
this.durableLogMetrics = durableLogMetrics;
this.instanceAiConfig = globalConfig.instanceAi;
}
async listThreads(userId, page = 0, perPage = 100) {
const result = await this.agentMemory.listThreads({
filter: { resourceId: userId },
perPage,
page,
orderBy: { field: 'updatedAt', direction: 'DESC' },
});
return {
threads: result.threads.map((t) => this.toThreadInfo(t)),
total: result.total,
page: result.page,
hasMore: result.hasMore,
};
}
async ensureThread(userId, threadId, projectId, launchMetadata) {
const existing = await this.agentMemory.getThread(threadId);
if (existing) {
if (existing.resourceId !== userId) {
throw new Error(`Thread ${threadId} is not owned by user ${userId}`);
}
return {
thread: this.toThreadInfo(existing),
created: false,
};
}
const created = await this.agentMemory.saveThreadWithProject({
id: threadId,
resourceId: userId,
title: '',
metadata: {
source: launchMetadata.source,
origin: launchMetadata.origin,
...(launchMetadata.sourceContext ? { sourceContext: launchMetadata.sourceContext } : {}),
},
}, projectId);
return {
thread: this.toThreadInfo(created),
created: true,
};
}
async restoreThreadMessages(userId, threadId, messages) {
const restorable = [];
for (const [index, raw] of messages.entries()) {
const message = toRestorableMessage(raw);
if (!message) {
throw new bad_request_error_1.BadRequestError(`Seed message at index ${index} is not a valid agent message (id, createdAt, and role+content or type:custom+data are required)`);
}
restorable.push(message);
}
await this.agentMemory.saveMessages({ threadId, resourceId: userId, messages: restorable });
return { restored: restorable.length };
}
async getThreadProjectId(threadId) {
return (await this.agentMemory.getThreadProjectId(threadId)) ?? undefined;
}
async getThreadMessages(_userId, threadId, options) {
const result = await this.agentMemory.listMessages({
threadId,
limit: options?.limit ?? 50,
page: options?.page ?? 0,
});
return {
threadId,
messages: result.messages.map((m) => this.toThreadMessage(m)),
};
}
async getRichMessages(_userId, threadId, options) {
const result = await this.agentMemory.listMessages({
threadId,
limit: options?.limit ?? 50,
page: options?.page ?? 0,
});
const loadStoredSnapshots = async () => {
let snapshots = await this.dbSnapshotStorage.getAll(threadId).catch((error) => {
this.logger.warn('Failed to load agent tree snapshots', {
threadId,
error: error instanceof Error ? error.message : String(error),
});
return [];
});
if (options?.excludeRunIds?.length) {
const excluded = new Set(options.excludeRunIds);
snapshots = snapshots.filter((s) => !excluded.has(s.runId));
}
return snapshots;
};
const activeCheckpoints = await this.loadActiveCheckpoints(threadId);
const snapshots = this.instanceAiConfig.durableLog
? await this.foldSnapshotsFromLog(threadId, loadStoredSnapshots, collectSuspendedHostRunIds(activeCheckpoints), options?.excludeRunIds, options?.excludeMessageGroupIds)
: await loadStoredSnapshots();
const checkpointMessages = collectInFlightCheckpointMessages(activeCheckpoints);
const storedMessages = mergeMessagesById(result.messages, checkpointMessages);
const fallbacksBefore = message_parser_1.messageParserStats.fallbackActivations;
const messages = (0, message_parser_1.parseStoredMessages)(storedMessages, snapshots);
this.durableLogMetrics.notifyParserFallbacks(message_parser_1.messageParserStats.fallbackActivations - fallbacksBefore);
await this.flagExpiredConfirmations(messages);
const projectId = await this.agentMemory.getThreadProjectId(threadId);
return { threadId, projectId: projectId ?? undefined, messages };
}
async foldSnapshotsFromLog(threadId, loadStoredSnapshots, suspendedRunIds, excludeRunIds, excludeMessageGroupIds) {
const start = Date.now();
let rows;
try {
rows = await this.eventLogRepository.getForThread(threadId);
}
catch (error) {
this.logger.warn('Failed to read Instance AI event log for history', {
threadId,
error: error instanceof Error ? error.message : String(error),
});
return await loadStoredSnapshots();
}
if (rows.length === 0)
return await loadStoredSnapshots();
const skipRunIds = new Set(excludeRunIds ?? []);
for (const runId of collectUnfinishedRunIds(rows)) {
if (!suspendedRunIds.has(runId))
skipRunIds.add(runId);
}
const { entries, skippedInFlight } = buildLogDerivedSnapshots(rows, skipRunIds, new Set(excludeMessageGroupIds ?? []));
if (entries.length === 0) {
if (skippedInFlight)
return [];
return await loadStoredSnapshots();
}
entries.sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0));
this.durableLogMetrics.recordFoldRead(Date.now() - start, entries.length);
return entries;
}
async flagExpiredConfirmations(messages) {
const requestIds = (0, message_parser_1.collectConfirmationRequestIds)(messages);
if (requestIds.length === 0)
return;
try {
const live = await this.pendingConfirmationRepository.findLiveRequestIds(requestIds, new Date());
(0, message_parser_1.markExpiredConfirmations)(messages, live);
}
catch (error) {
this.logger.warn('Failed to flag expired confirmation cards', {
error: error instanceof Error ? error.message : String(error),
});
}
}
async loadActiveCheckpoints(threadId) {
try {
return await this.checkpointRepository.findActiveByThreadId(threadId);
}
catch (error) {
this.logger.warn('Failed to load in-flight checkpoints', {
threadId,
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
async getLatestRunSnapshot(threadId, options) {
return await this.dbSnapshotStorage.getLatest(threadId, options);
}
async validateThreadOwnership(userId, threadId) {
return (await this.checkThreadOwnership(userId, threadId)) === 'owned';
}
async checkThreadOwnership(userId, threadId) {
const thread = await this.agentMemory.getThread(threadId);
if (!thread)
return 'not_found';
return thread.resourceId === userId ? 'owned' : 'other_user';
}
async deleteThread(threadId) {
await this.agentMemory.deleteThreadsByResourceIdPrefix((0, instance_ai_1.createSubAgentResourceIdPrefix)(threadId));
await this.agentMemory.deleteThread(threadId);
}
async deleteThreadsForUser(userId) {
return await this.agentMemory.deleteThreadsByResourceId(userId);
}
async renameThread(threadId, title) {
return await this.updateThread(threadId, { title });
}
async updateThread(threadId, updates) {
const updated = await (0, instance_ai_1.patchThread)(this.agentMemory, {
threadId,
update: ({ metadata }) => {
const patch = {
metadata: { ...metadata, ...updates.metadata },
};
if (updates.title !== undefined) {
patch.title = updates.title;
patch.metadata.titleRefined = true;
}
return patch;
},
});
if (!updated) {
throw new not_found_error_1.NotFoundError(`Thread ${threadId} not found`);
}
return this.toThreadInfo(updated);
}
async getThreadMetadata(userId, threadId) {
const thread = await this.agentMemory.getThread(threadId);
if (!thread || thread.resourceId !== userId)
return undefined;
return thread.metadata;
}
async cleanupExpiredThreads(onThreadDeleted) {
const ttlDays = this.instanceAiConfig.threadTtlDays;
if (!ttlDays || ttlDays <= 0)
return 0;
const cutoff = new Date(Date.now() - ttlDays * 24 * 60 * 60 * 1000);
let deletedCount = 0;
const perPage = 100;
let hasMore = true;
while (hasMore) {
const result = await this.agentMemory.listThreads({
perPage,
page: 0,
orderBy: { field: 'updatedAt', direction: 'ASC' },
});
let deletedInPage = 0;
for (const thread of result.threads) {
if (thread.updatedAt < cutoff) {
try {
await onThreadDeleted?.(thread.id);
await this.deleteThread(thread.id);
deletedCount++;
deletedInPage++;
}
catch (error) {
this.logger.warn('Failed to delete expired thread', {
threadId: thread.id,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
hasMore = deletedInPage > 0 && result.hasMore;
}
if (deletedCount > 0) {
this.logger.info(`Cleaned up ${deletedCount} expired conversation threads (TTL: ${ttlDays} days)`);
}
return deletedCount;
}
toThreadInfo(thread) {
return {
id: thread.id,
title: thread.title,
resourceId: thread.resourceId,
createdAt: thread.createdAt.toISOString(),
updatedAt: thread.updatedAt.toISOString(),
metadata: thread.metadata,
};
}
toThreadMessage(message) {
return {
id: message.id,
role: 'role' in message ? message.role : 'custom',
content: 'content' in message ? message.content : message.data,
type: message.type,
createdAt: message.createdAt.toISOString(),
};
}
};
exports.InstanceAiMemoryService = InstanceAiMemoryService;
exports.InstanceAiMemoryService = InstanceAiMemoryService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger, config_1.GlobalConfig, typeorm_agent_memory_1.TypeORMAgentMemory, db_snapshot_storage_1.DbSnapshotStorage, instance_ai_checkpoint_repository_1.InstanceAiCheckpointRepository, instance_ai_pending_confirmation_repository_1.InstanceAiPendingConfirmationRepository, instance_ai_event_log_repository_1.InstanceAiEventLogRepository, durable_log_metrics_1.DurableLogMetrics])
], InstanceAiMemoryService);
//# sourceMappingURL=instance-ai-memory.service.js.map