n8n
Version:
n8n Workflow Automation Tool
281 lines • 11.8 kB
JavaScript
;
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 not_found_error_1 = require("../../errors/response-errors/not-found.error");
const message_parser_1 = require("./message-parser");
const instance_ai_checkpoint_repository_1 = require("./repositories/instance-ai-checkpoint.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 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 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));
}
let InstanceAiMemoryService = class InstanceAiMemoryService {
constructor(logger, globalConfig, agentMemory, dbSnapshotStorage, checkpointRepository, pendingConfirmationRepository) {
this.logger = logger;
this.agentMemory = agentMemory;
this.dbSnapshotStorage = dbSnapshotStorage;
this.checkpointRepository = checkpointRepository;
this.pendingConfirmationRepository = pendingConfirmationRepository;
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) {
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.saveThread({
id: threadId,
resourceId: userId,
title: '',
});
return {
thread: this.toThreadInfo(created),
created: true,
};
}
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,
});
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));
}
const checkpointMessages = await this.loadInFlightCheckpointMessages(threadId);
const storedMessages = mergeMessagesById(result.messages, checkpointMessages);
const messages = (0, message_parser_1.parseStoredMessages)(storedMessages, snapshots);
await this.flagExpiredConfirmations(messages);
return { threadId, messages };
}
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 loadInFlightCheckpointMessages(threadId) {
let checkpoints;
try {
checkpoints = await this.checkpointRepository.findActiveByThreadId(threadId);
}
catch (error) {
this.logger.warn('Failed to load in-flight checkpoint messages', {
threadId,
error: error instanceof Error ? error.message : String(error),
});
return [];
}
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;
}
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 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])
], InstanceAiMemoryService);
//# sourceMappingURL=instance-ai-memory.service.js.map