mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
feat(core): Support multi-main deployments for Instance AI (no-changelog) (#33296)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ export type PubSubEventName =
|
||||
| 'restart-event-bus'
|
||||
| 'relay-execution-lifecycle-event'
|
||||
| 'relay-chat-stream-event'
|
||||
| 'relay-instance-ai-event'
|
||||
| 'relay-instance-ai-task-control'
|
||||
| 'relay-chat-human-message'
|
||||
| 'relay-chat-message-edit'
|
||||
| 'reload-sso-provisioning-configuration'
|
||||
|
||||
@@ -580,6 +580,34 @@ describe('RunStateRegistry', () => {
|
||||
expect(registry.hasSuspendedRun('thread-1')).toBe(true);
|
||||
expect(registry.getSuspendedRun('thread-1')).toBe(suspendedState);
|
||||
});
|
||||
|
||||
it('rehydrates the message-group indexes when they start empty (restart-resumed orphan)', () => {
|
||||
// Simulate a restart: no prior startRun, so threadMessageGroupId /
|
||||
// runIdsByMessageGroup are empty when the orphan is re-suspended.
|
||||
const suspendedState = createSuspendedRunState({
|
||||
threadId: 'thread-1',
|
||||
runId: 'run_orphan',
|
||||
messageGroupId: 'mg_orphan',
|
||||
});
|
||||
|
||||
registry.suspendRun('thread-1', suspendedState);
|
||||
|
||||
expect(registry.getMessageGroupId('thread-1')).toBe('mg_orphan');
|
||||
expect(registry.getRunIdsForMessageGroup('mg_orphan')).toEqual(['run_orphan']);
|
||||
});
|
||||
|
||||
it('does not duplicate the runId in the group when suspending a run started normally', () => {
|
||||
const started = registry.startRun({ threadId: 'thread-1', user: { id: 'u1', name: 'A' } });
|
||||
const suspendedState = createSuspendedRunState({
|
||||
threadId: 'thread-1',
|
||||
runId: started.runId,
|
||||
messageGroupId: started.messageGroupId,
|
||||
});
|
||||
|
||||
registry.suspendRun('thread-1', suspendedState);
|
||||
|
||||
expect(registry.getRunIdsForMessageGroup(started.messageGroupId!)).toEqual([started.runId]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateSuspendedRun', () => {
|
||||
@@ -625,6 +653,24 @@ describe('RunStateRegistry', () => {
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rehydrates the message-group indexes on activation when they start empty', () => {
|
||||
// Restart-resume path: orphan re-suspended then reactivated on a main
|
||||
// that never ran startRun for this thread.
|
||||
const suspendedState = createSuspendedRunState({
|
||||
threadId: 'thread-1',
|
||||
runId: 'run_orphan',
|
||||
messageGroupId: 'mg_orphan',
|
||||
});
|
||||
registry.suspendRun('thread-1', suspendedState);
|
||||
// Clear the group indexes to isolate activation's own rehydration.
|
||||
registry.deleteMessageGroup('mg_orphan');
|
||||
|
||||
registry.activateSuspendedRun('thread-1');
|
||||
|
||||
expect(registry.getMessageGroupId('thread-1')).toBe('mg_orphan');
|
||||
expect(registry.getRunIdsForMessageGroup('mg_orphan')).toEqual(['run_orphan']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSuspendedRun', () => {
|
||||
|
||||
@@ -156,16 +156,29 @@ export class RunStateRegistry<TUser = unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
this.threadMessageGroupId.set(options.threadId, messageGroupId);
|
||||
if (!this.runIdsByMessageGroup.has(messageGroupId)) {
|
||||
this.runIdsByMessageGroup.set(messageGroupId, []);
|
||||
}
|
||||
const groupRunIds = this.runIdsByMessageGroup.get(messageGroupId);
|
||||
if (groupRunIds) groupRunIds.push(runId);
|
||||
this.indexRunInGroup(options.threadId, messageGroupId, runId);
|
||||
|
||||
return { runId, threadId: options.threadId, abortController, messageGroupId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the message-group indexes for a run: map the thread to its current
|
||||
* group and record the run under that group. Idempotent.
|
||||
*
|
||||
* Called on `startRun` and re-applied on `suspendRun`/`activateSuspendedRun`
|
||||
* so a run resumed after a restart (where these maps start empty) repopulates
|
||||
* the group association the SSE bootstrap relies on.
|
||||
*/
|
||||
private indexRunInGroup(threadId: string, messageGroupId: string, runId: string): void {
|
||||
this.threadMessageGroupId.set(threadId, messageGroupId);
|
||||
let groupRunIds = this.runIdsByMessageGroup.get(messageGroupId);
|
||||
if (!groupRunIds) {
|
||||
groupRunIds = [];
|
||||
this.runIdsByMessageGroup.set(messageGroupId, groupRunIds);
|
||||
}
|
||||
if (!groupRunIds.includes(runId)) groupRunIds.push(runId);
|
||||
}
|
||||
|
||||
getThreadStatus(
|
||||
threadId: string,
|
||||
backgroundTasks: BackgroundTaskStatusSnapshot[],
|
||||
@@ -273,6 +286,12 @@ export class RunStateRegistry<TUser = unknown> {
|
||||
state.startedAt = state.startedAt ?? activeRun?.startedAt ?? state.createdAt;
|
||||
state.lastActivityAt = state.lastActivityAt ?? state.createdAt;
|
||||
this.suspendedRuns.set(threadId, state);
|
||||
|
||||
// Re-seed group indexes: on a restart-resumed orphan these maps start
|
||||
// empty, so without this the SSE bootstrap loses the group association.
|
||||
if (state.messageGroupId) {
|
||||
this.indexRunInGroup(threadId, state.messageGroupId, state.runId);
|
||||
}
|
||||
}
|
||||
|
||||
findSuspendedByRequestId(requestId: string): SuspendedRunState<TUser> | undefined {
|
||||
@@ -306,6 +325,11 @@ export class RunStateRegistry<TUser = unknown> {
|
||||
startedAt: suspended.startedAt ?? suspended.createdAt,
|
||||
lastActivityAt: now,
|
||||
});
|
||||
|
||||
// Re-seed group indexes for the reactivated run (empty after a restart).
|
||||
if (suspended.messageGroupId) {
|
||||
this.indexRunInGroup(threadId, suspended.messageGroupId, suspended.runId);
|
||||
}
|
||||
return suspended;
|
||||
}
|
||||
|
||||
|
||||
@@ -363,6 +363,109 @@ describe('InstanceAiController', () => {
|
||||
expect(runSyncFrame).toContain('"planItems"');
|
||||
});
|
||||
|
||||
it('should replay events that arrive while bootstrap snapshot fetches are in flight', async () => {
|
||||
memoryService.checkThreadOwnership.mockResolvedValue('owned');
|
||||
instanceAiService.getThreadStatus.mockReturnValue({
|
||||
hasActiveRun: true,
|
||||
isSuspended: false,
|
||||
backgroundTasks: [],
|
||||
} as never);
|
||||
instanceAiService.getMessageGroupId.mockReturnValue('mg-1');
|
||||
instanceAiService.getRunIdsForMessageGroup.mockReturnValue(['run-1']);
|
||||
eventBus.getEventsForRuns.mockReturnValue([]);
|
||||
eventBus.getEventsAfter.mockReturnValue([]);
|
||||
|
||||
let subscribeHandler: ((stored: { id: number; event: unknown }) => void) | undefined;
|
||||
eventBus.subscribe.mockImplementation((_threadId, handler) => {
|
||||
subscribeHandler = handler as typeof subscribeHandler;
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
// While the persisted snapshot is being fetched, a relayed event arrives:
|
||||
// the early subscription keeps it flowing into the store, and the replay
|
||||
// after the await must pick it up exactly once.
|
||||
const midAwaitEvent = {
|
||||
id: 7,
|
||||
event: { type: 'run-finish', runId: 'run-1', agentId: 'a1', payload: {} },
|
||||
};
|
||||
memoryService.getLatestRunSnapshot.mockImplementation(async () => {
|
||||
subscribeHandler!(midAwaitEvent);
|
||||
eventBus.getEventsAfter.mockReturnValue([midAwaitEvent] as never);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const sseRes = mock<Response & { flush?: () => void }>({
|
||||
setHeader: vi.fn(),
|
||||
flushHeaders: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
flush: vi.fn(),
|
||||
});
|
||||
const sseReq = mock<AuthenticatedRequest>({
|
||||
user: { id: USER_ID },
|
||||
headers: {},
|
||||
once: vi.fn(),
|
||||
});
|
||||
|
||||
await controller.events(sseReq, sseRes, THREAD_ID, { lastEventId: undefined } as never);
|
||||
|
||||
// Subscription must be registered before the async bootstrap starts, so
|
||||
// sibling mains keep relaying events for this thread during the awaits.
|
||||
expect(eventBus.subscribe.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
memoryService.getLatestRunSnapshot.mock.invocationCallOrder[0],
|
||||
);
|
||||
|
||||
const eventFrames = (sseRes.write as Mock).mock.calls
|
||||
.map(([frame]) => String(frame))
|
||||
.filter((frame) => frame.includes('run-finish'));
|
||||
expect(eventFrames).toEqual([`id: 7\ndata: ${JSON.stringify(midAwaitEvent.event)}\n\n`]);
|
||||
});
|
||||
|
||||
it('should clean up the subscription when the client disconnects during bootstrap', async () => {
|
||||
memoryService.checkThreadOwnership.mockResolvedValue('owned');
|
||||
instanceAiService.getThreadStatus.mockReturnValue({
|
||||
hasActiveRun: true,
|
||||
isSuspended: false,
|
||||
backgroundTasks: [],
|
||||
} as never);
|
||||
instanceAiService.getMessageGroupId.mockReturnValue('mg-1');
|
||||
instanceAiService.getRunIdsForMessageGroup.mockReturnValue(['run-1']);
|
||||
eventBus.getEventsForRuns.mockReturnValue([]);
|
||||
eventBus.getEventsAfter.mockReturnValue([
|
||||
{ id: 1, event: { type: 'text-delta', runId: 'run-1', agentId: 'a1', payload: {} } },
|
||||
] as never);
|
||||
|
||||
const unsubscribe = vi.fn();
|
||||
eventBus.subscribe.mockReturnValue(unsubscribe);
|
||||
|
||||
let closeHandler: (() => void) | undefined;
|
||||
const sseReq = mock<AuthenticatedRequest>({
|
||||
user: { id: USER_ID },
|
||||
headers: {},
|
||||
once: vi.fn((event: string, handler: () => void) => {
|
||||
if (event === 'close') closeHandler = handler;
|
||||
}) as never,
|
||||
});
|
||||
const sseRes = mock<Response & { flush?: () => void }>({
|
||||
setHeader: vi.fn(),
|
||||
flushHeaders: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
flush: vi.fn(),
|
||||
});
|
||||
|
||||
// The client disconnects while the persisted snapshot is being fetched.
|
||||
memoryService.getLatestRunSnapshot.mockImplementation(async () => {
|
||||
closeHandler!();
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await controller.events(sseReq, sseRes, THREAD_ID, { lastEventId: undefined } as never);
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(sseRes.write).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close SSE stream when thread ownership changes after pre-creation subscribe', async () => {
|
||||
// Simulate: thread does not exist at connect time
|
||||
memoryService.checkThreadOwnership.mockResolvedValueOnce('not_found');
|
||||
@@ -468,7 +571,7 @@ describe('InstanceAiController', () => {
|
||||
const result = await controller.cancel(req, res, THREAD_ID);
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(instanceAiService.cancelRun).toHaveBeenCalledWith(THREAD_ID);
|
||||
expect(instanceAiService.routeCancelRun).toHaveBeenCalledWith(THREAD_ID);
|
||||
});
|
||||
|
||||
it('should throw ForbiddenError for other user thread', async () => {
|
||||
@@ -698,7 +801,7 @@ describe('InstanceAiController', () => {
|
||||
const result = await controller.cancelTask(req, res, THREAD_ID, 'task-1');
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(instanceAiService.cancelBackgroundTask).toHaveBeenCalledWith(THREAD_ID, 'task-1');
|
||||
expect(instanceAiService.routeCancelBackgroundTask).toHaveBeenCalledWith(THREAD_ID, 'task-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -714,7 +817,7 @@ describe('InstanceAiController', () => {
|
||||
const result = await controller.correctTask(req, res, THREAD_ID, 'task-1', payload);
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(instanceAiService.sendCorrectionToTask).toHaveBeenCalledWith(
|
||||
expect(instanceAiService.routeCorrectionToTask).toHaveBeenCalledWith(
|
||||
THREAD_ID,
|
||||
'task-1',
|
||||
'fix this',
|
||||
@@ -969,7 +1072,7 @@ describe('InstanceAiController', () => {
|
||||
const result = await controller.deleteThread(req, res, THREAD_ID);
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(instanceAiService.clearThreadState).toHaveBeenCalledWith(THREAD_ID);
|
||||
expect(instanceAiService.routeClearThreadState).toHaveBeenCalledWith(THREAD_ID);
|
||||
expect(memoryService.deleteThread).toHaveBeenCalledWith(THREAD_ID);
|
||||
});
|
||||
|
||||
|
||||
@@ -3191,3 +3191,190 @@ describe('InstanceAiService — deterministic workflow setup follow-up', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
type TaskControlInternals = {
|
||||
instanceSettings: { isMultiMain: boolean };
|
||||
publisher: { publishCommand: Mock };
|
||||
backgroundTasks: { getTaskSnapshots: Mock };
|
||||
logger: { error: Mock };
|
||||
sendCorrectionToTask: Mock;
|
||||
cancelBackgroundTask: Mock;
|
||||
cancelRun: Mock;
|
||||
clearThreadState: Mock;
|
||||
routeCorrectionToTask: InstanceAiService['routeCorrectionToTask'];
|
||||
routeCancelBackgroundTask: InstanceAiService['routeCancelBackgroundTask'];
|
||||
routeCancelRun: InstanceAiService['routeCancelRun'];
|
||||
routeClearThreadState: InstanceAiService['routeClearThreadState'];
|
||||
handleRelayTaskControl: InstanceAiService['handleRelayTaskControl'];
|
||||
};
|
||||
|
||||
function buildTaskControlService(isMultiMain: boolean): TaskControlInternals {
|
||||
const service = Object.create(InstanceAiService.prototype) as unknown as TaskControlInternals;
|
||||
service.instanceSettings = { isMultiMain };
|
||||
service.publisher = { publishCommand: vi.fn().mockResolvedValue(undefined) };
|
||||
service.backgroundTasks = { getTaskSnapshots: vi.fn(() => []) };
|
||||
service.logger = { error: vi.fn() };
|
||||
service.sendCorrectionToTask = vi.fn(() => 'queued');
|
||||
service.cancelBackgroundTask = vi.fn();
|
||||
service.cancelRun = vi.fn();
|
||||
service.clearThreadState = vi.fn(async () => {});
|
||||
return service;
|
||||
}
|
||||
|
||||
describe('InstanceAiService — cross-main task-control routing', () => {
|
||||
describe('routeCorrectionToTask', () => {
|
||||
it('broadcasts when the task is not local and multi-main', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
service.sendCorrectionToTask.mockReturnValue('task-not-found');
|
||||
|
||||
await service.routeCorrectionToTask('thread-a', 'task-1', 'try again');
|
||||
|
||||
expect(service.sendCorrectionToTask).toHaveBeenCalledWith('thread-a', 'task-1', 'try again');
|
||||
expect(service.publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'relay-instance-ai-task-control',
|
||||
payload: {
|
||||
threadId: 'thread-a',
|
||||
taskId: 'task-1',
|
||||
action: 'correct',
|
||||
correction: 'try again',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not broadcast when the correction was applied locally', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
service.sendCorrectionToTask.mockReturnValue('queued');
|
||||
|
||||
await service.routeCorrectionToTask('thread-a', 'task-1', 'try again');
|
||||
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not broadcast in single-main even on a local miss', async () => {
|
||||
const service = buildTaskControlService(false);
|
||||
service.sendCorrectionToTask.mockReturnValue('task-not-found');
|
||||
|
||||
await service.routeCorrectionToTask('thread-a', 'task-1', 'try again');
|
||||
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeCancelBackgroundTask', () => {
|
||||
it('broadcasts when the task is not local and multi-main', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
service.backgroundTasks.getTaskSnapshots.mockReturnValue([]);
|
||||
|
||||
await service.routeCancelBackgroundTask('thread-a', 'task-1');
|
||||
|
||||
expect(service.cancelBackgroundTask).toHaveBeenCalledWith('thread-a', 'task-1');
|
||||
expect(service.publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'relay-instance-ai-task-control',
|
||||
payload: { threadId: 'thread-a', taskId: 'task-1', action: 'cancel-task' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not broadcast when the task is local', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
service.backgroundTasks.getTaskSnapshots.mockReturnValue([{ taskId: 'task-1' }]);
|
||||
|
||||
await service.routeCancelBackgroundTask('thread-a', 'task-1');
|
||||
|
||||
expect(service.cancelBackgroundTask).toHaveBeenCalledWith('thread-a', 'task-1');
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeCancelRun / routeClearThreadState', () => {
|
||||
it('routeCancelRun cancels locally and always fans out in multi-main', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
|
||||
await service.routeCancelRun('thread-a');
|
||||
|
||||
expect(service.cancelRun).toHaveBeenCalledWith('thread-a');
|
||||
expect(service.publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'relay-instance-ai-task-control',
|
||||
payload: { threadId: 'thread-a', action: 'cancel-thread' },
|
||||
});
|
||||
});
|
||||
|
||||
it('routeCancelRun does not fan out in single-main', async () => {
|
||||
const service = buildTaskControlService(false);
|
||||
|
||||
await service.routeCancelRun('thread-a');
|
||||
|
||||
expect(service.cancelRun).toHaveBeenCalledWith('thread-a');
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routeClearThreadState clears locally and fans out in multi-main', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
|
||||
await service.routeClearThreadState('thread-a');
|
||||
|
||||
expect(service.clearThreadState).toHaveBeenCalledWith('thread-a');
|
||||
expect(service.publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'relay-instance-ai-task-control',
|
||||
payload: { threadId: 'thread-a', action: 'clear-thread' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRelayTaskControl', () => {
|
||||
it('applies a relayed correction via the local method and never re-broadcasts', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
|
||||
await service.handleRelayTaskControl({
|
||||
threadId: 'thread-a',
|
||||
taskId: 'task-1',
|
||||
action: 'correct',
|
||||
correction: 'fix it',
|
||||
});
|
||||
|
||||
expect(service.sendCorrectionToTask).toHaveBeenCalledWith('thread-a', 'task-1', 'fix it');
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores a correction relay missing its correction text', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
|
||||
await service.handleRelayTaskControl({
|
||||
threadId: 'thread-a',
|
||||
taskId: 'task-1',
|
||||
action: 'correct',
|
||||
});
|
||||
|
||||
expect(service.sendCorrectionToTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes cancel-task / cancel-thread / clear-thread to the local methods', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
|
||||
await service.handleRelayTaskControl({
|
||||
threadId: 'thread-a',
|
||||
taskId: 'task-1',
|
||||
action: 'cancel-task',
|
||||
});
|
||||
await service.handleRelayTaskControl({ threadId: 'thread-a', action: 'cancel-thread' });
|
||||
await service.handleRelayTaskControl({ threadId: 'thread-a', action: 'clear-thread' });
|
||||
|
||||
expect(service.cancelBackgroundTask).toHaveBeenCalledWith('thread-a', 'task-1');
|
||||
expect(service.cancelRun).toHaveBeenCalledWith('thread-a');
|
||||
expect(service.clearThreadState).toHaveBeenCalledWith('thread-a');
|
||||
expect(service.publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('swallows and logs errors from a local action (no unhandled rejection on the sibling main)', async () => {
|
||||
const service = buildTaskControlService(true);
|
||||
service.clearThreadState.mockRejectedValue(new Error('db exploded'));
|
||||
|
||||
await expect(
|
||||
service.handleRelayTaskControl({ threadId: 'thread-a', action: 'clear-thread' }),
|
||||
).resolves.toBeUndefined();
|
||||
expect(service.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to apply relayed Instance AI task-control',
|
||||
expect.objectContaining({ threadId: 'thread-a', action: 'clear-thread' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+105
-1
@@ -1,4 +1,9 @@
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import type { InstanceAiEvent } from '@n8n/api-types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import type { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
|
||||
import { InProcessEventBus } from '../in-process-event-bus';
|
||||
|
||||
@@ -13,9 +18,20 @@ function makeEvent(type: string, runId: string): InstanceAiEvent {
|
||||
|
||||
describe('InProcessEventBus', () => {
|
||||
let bus: InProcessEventBus;
|
||||
let publisher: ReturnType<typeof mock<Publisher>>;
|
||||
let instanceSettings: { isMultiMain: boolean };
|
||||
|
||||
function buildBus() {
|
||||
const logger = mock<Logger>();
|
||||
logger.scoped.mockReturnValue(logger);
|
||||
publisher = mock<Publisher>();
|
||||
publisher.publishCommand.mockResolvedValue(undefined);
|
||||
return new InProcessEventBus(logger, instanceSettings as InstanceSettings, publisher);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
bus = new InProcessEventBus();
|
||||
instanceSettings = { isMultiMain: false };
|
||||
bus = buildBus();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -191,4 +207,92 @@ describe('InProcessEventBus', () => {
|
||||
expect(received).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-main relay', () => {
|
||||
it('does not relay when single-main', () => {
|
||||
bus.publish('thread-1', makeEvent('a', 'run_1'));
|
||||
expect(publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('relays each event via pubsub when multi-main', () => {
|
||||
instanceSettings.isMultiMain = true;
|
||||
bus = buildBus();
|
||||
|
||||
const event = makeEvent('a', 'run_1');
|
||||
bus.publish('thread-1', event);
|
||||
|
||||
expect(publisher.publishCommand).toHaveBeenCalledWith({
|
||||
command: 'relay-instance-ai-event',
|
||||
payload: { threadId: 'thread-1', event },
|
||||
});
|
||||
});
|
||||
|
||||
it('still delivers locally even when relaying', () => {
|
||||
instanceSettings.isMultiMain = true;
|
||||
bus = buildBus();
|
||||
const received: number[] = [];
|
||||
bus.subscribe('thread-1', (e) => received.push(e.id));
|
||||
|
||||
bus.publish('thread-1', makeEvent('a', 'run_1'));
|
||||
|
||||
expect(received).toEqual([1]);
|
||||
expect(publisher.publishCommand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips relay for oversized events but still delivers locally', () => {
|
||||
instanceSettings.isMultiMain = true;
|
||||
bus = buildBus();
|
||||
const received: number[] = [];
|
||||
bus.subscribe('thread-1', (e) => received.push(e.id));
|
||||
const huge = makeEvent('a', 'run_1');
|
||||
(huge.payload as { text: string }).text = 'x'.repeat(6 * 1024 * 1024);
|
||||
|
||||
bus.publish('thread-1', huge);
|
||||
|
||||
// Relay skipped (would bloat pubsub), but the local SSE client still got it
|
||||
// synchronously via the emit (even though the 2 MB store cap then evicts it).
|
||||
expect(publisher.publishCommand).not.toHaveBeenCalled();
|
||||
expect(received).toEqual([1]);
|
||||
});
|
||||
|
||||
it('publishLocalOnly never relays', () => {
|
||||
instanceSettings.isMultiMain = true;
|
||||
bus = buildBus();
|
||||
|
||||
bus.publishLocalOnly('thread-1', makeEvent('a', 'run_1'));
|
||||
|
||||
expect(publisher.publishCommand).not.toHaveBeenCalled();
|
||||
expect(bus.getEventsAfter('thread-1', 0)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRelayInstanceAiEvent', () => {
|
||||
it('re-emits a relayed event when this main holds an SSE subscriber', () => {
|
||||
const received: number[] = [];
|
||||
bus.subscribe('thread-1', (e) => received.push(e.id));
|
||||
|
||||
bus.handleRelayInstanceAiEvent({ threadId: 'thread-1', event: makeEvent('a', 'run_1') });
|
||||
|
||||
expect(received).toEqual([1]);
|
||||
// Re-emit must not re-relay (loop guard): publishLocalOnly path.
|
||||
expect(publisher.publishCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores a relayed event when this main has no subscriber for the thread', () => {
|
||||
bus.handleRelayInstanceAiEvent({ threadId: 'thread-1', event: makeEvent('a', 'run_1') });
|
||||
|
||||
// Nothing stored, since the thread has no local consumer here.
|
||||
expect(bus.getEventsAfter('thread-1', 0)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSubscribers', () => {
|
||||
it('reflects active subscriptions', () => {
|
||||
expect(bus.hasSubscribers('thread-1')).toBe(false);
|
||||
const unsubscribe = bus.subscribe('thread-1', () => {});
|
||||
expect(bus.hasSubscribers('thread-1')).toBe(true);
|
||||
unsubscribe();
|
||||
expect(bus.hasSubscribers('thread-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import type { InstanceAiEvent } from '@n8n/api-types';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { InstanceAiEventBus, StoredEvent } from '@n8n/instance-ai';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import { MAX_PUBSUB_PAYLOAD_BYTES } from '@/scaling/constants';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
|
||||
const MAX_EVENTS_PER_THREAD = 500;
|
||||
const MAX_BYTES_PER_THREAD = 2 * 1024 * 1024; // 2 MB
|
||||
@@ -18,21 +24,47 @@ export class InProcessEventBus implements InstanceAiEventBus {
|
||||
/** Monotonic counter per thread — never resets even after eviction. */
|
||||
private readonly nextId = new Map<string, number>();
|
||||
|
||||
constructor() {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly publisher: Publisher,
|
||||
) {
|
||||
this.logger = this.logger.scoped('instance-ai');
|
||||
// Avoid warnings when many SSE clients connect (each adds a listener per thread)
|
||||
this.emitter.setMaxListeners(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event for a thread: store it, deliver it to local SSE
|
||||
* subscribers, and — in multi-main — relay it to sibling mains so the main
|
||||
* holding the client's SSE connection (which may not be this one) delivers it.
|
||||
*/
|
||||
publish(threadId: string, event: InstanceAiEvent): void {
|
||||
// Serialize once: reused for the store's size accounting and the relay guard.
|
||||
const sizeBytes = Buffer.byteLength(JSON.stringify(event), 'utf8');
|
||||
this.storeAndEmit(threadId, event, sizeBytes);
|
||||
this.relayToSiblings(threadId, event, sizeBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store + deliver locally WITHOUT relaying. Used by the pubsub handler when a
|
||||
* relayed event arrives from another main — re-relaying would loop. The local
|
||||
* `nextId` stamps the SSE id, so this main is the id authority for the
|
||||
* connection it serves.
|
||||
*/
|
||||
publishLocalOnly(threadId: string, event: InstanceAiEvent): void {
|
||||
this.storeAndEmit(threadId, event, Buffer.byteLength(JSON.stringify(event), 'utf8'));
|
||||
}
|
||||
|
||||
private storeAndEmit(threadId: string, event: InstanceAiEvent, eventSizeBytes: number): void {
|
||||
const events = this.getOrCreateStore(threadId);
|
||||
const id = (this.nextId.get(threadId) ?? 0) + 1;
|
||||
this.nextId.set(threadId, id);
|
||||
|
||||
const stored: StoredEvent = { id, event };
|
||||
const eventSize = JSON.stringify(event).length;
|
||||
|
||||
events.push(stored);
|
||||
this.sizeBytes.set(threadId, (this.sizeBytes.get(threadId) ?? 0) + eventSize);
|
||||
this.sizeBytes.set(threadId, (this.sizeBytes.get(threadId) ?? 0) + eventSizeBytes);
|
||||
|
||||
// Evict oldest events if count or size exceeds caps
|
||||
this.evictIfNeeded(threadId, events);
|
||||
@@ -40,11 +72,49 @@ export class InProcessEventBus implements InstanceAiEventBus {
|
||||
this.emitter.emit(threadId, stored);
|
||||
}
|
||||
|
||||
private relayToSiblings(threadId: string, event: InstanceAiEvent, sizeBytes: number): void {
|
||||
if (!this.instanceSettings.isMultiMain) return;
|
||||
|
||||
if (sizeBytes > MAX_PUBSUB_PAYLOAD_BYTES) {
|
||||
this.logger.warn(
|
||||
`Skipping cross-main relay of "${event.type}" event (${sizeBytes} bytes exceeds ${MAX_PUBSUB_PAYLOAD_BYTES})`,
|
||||
{ threadId, runId: event.runId },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void this.publisher
|
||||
.publishCommand({ command: 'relay-instance-ai-event', payload: { threadId, event } })
|
||||
.catch((error: unknown) =>
|
||||
this.logger.error('Failed to relay Instance AI event to sibling mains', {
|
||||
threadId,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** A relayed event from another main: re-emit to this main's SSE clients only
|
||||
* if it actually holds a subscription for the thread (avoids every main
|
||||
* buffering every thread). */
|
||||
@OnPubSubEvent('relay-instance-ai-event', { instanceType: 'main' })
|
||||
handleRelayInstanceAiEvent({
|
||||
threadId,
|
||||
event,
|
||||
}: { threadId: string; event: InstanceAiEvent }): void {
|
||||
if (!this.hasSubscribers(threadId)) return;
|
||||
this.publishLocalOnly(threadId, event);
|
||||
}
|
||||
|
||||
subscribe(threadId: string, handler: (storedEvent: StoredEvent) => void): () => void {
|
||||
this.emitter.on(threadId, handler);
|
||||
return () => this.emitter.off(threadId, handler);
|
||||
}
|
||||
|
||||
/** Whether this main currently holds an SSE subscription for the thread. */
|
||||
hasSubscribers(threadId: string): boolean {
|
||||
return this.emitter.listenerCount(threadId) > 0;
|
||||
}
|
||||
|
||||
getEventsAfter(threadId: string, afterId: number): StoredEvent[] {
|
||||
const events = this.store.get(threadId);
|
||||
if (!events) return [];
|
||||
@@ -90,7 +160,7 @@ export class InProcessEventBus implements InstanceAiEventBus {
|
||||
while (events.length > MAX_EVENTS_PER_THREAD || totalSize > MAX_BYTES_PER_THREAD) {
|
||||
const evicted = events.shift();
|
||||
if (!evicted) break;
|
||||
totalSize -= JSON.stringify(evicted.event).length;
|
||||
totalSize -= Buffer.byteLength(JSON.stringify(evicted.event), 'utf8');
|
||||
}
|
||||
|
||||
this.sizeBytes.set(threadId, Math.max(0, totalSize));
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
Body,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
import type { StoredEvent } from '@n8n/instance-ai';
|
||||
import type { AgentTreeSnapshot, StoredEvent } from '@n8n/instance-ai';
|
||||
import { buildAgentTreeFromEvents } from '@n8n/instance-ai';
|
||||
import { UnsupportedAttachmentError, validateAttachmentMimeTypes } from '@n8n/instance-ai/parsers';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
@@ -213,12 +213,6 @@ export class InstanceAiController {
|
||||
if (ownership === 'other_user') {
|
||||
throw new ForbiddenError('Not authorized for this thread');
|
||||
}
|
||||
if (ownership === 'owned') {
|
||||
await this.instanceAiService.replayUndeliveredTerminalOutcomes(threadId, {
|
||||
delivery: 'event',
|
||||
});
|
||||
}
|
||||
|
||||
// When the thread didn't exist at connect time, another user could create
|
||||
// and own it before events start flowing. We re-check once on the first
|
||||
// event and close the stream if ownership changed. Events are buffered
|
||||
@@ -228,7 +222,74 @@ export class InstanceAiController {
|
||||
const pendingEvents: StoredEvent[] = [];
|
||||
const userId = req.user.id;
|
||||
|
||||
// 1. Set SSE headers.
|
||||
// 1. Subscribe to live events before the async bootstrap below.
|
||||
// hasSubscribers() must be true across the awaits that follow: in
|
||||
// multi-main, sibling mains drop relayed events for threads without a
|
||||
// local subscriber, so a relayed event arriving during an await would
|
||||
// otherwise be lost for good. Events emitted while bootstrapping are
|
||||
// NOT delivered here — they land in the event store and the replay in
|
||||
// step 6 picks them up, avoiding duplicates.
|
||||
let bootstrapping = true;
|
||||
|
||||
const deliver = (stored: StoredEvent) => {
|
||||
if (ownershipVerified) {
|
||||
this.writeSseEvent(res, stored);
|
||||
return;
|
||||
}
|
||||
|
||||
// When the thread was not_found at connect time, re-validate ownership
|
||||
// on the first event. Buffer all events until the check resolves to
|
||||
// avoid leaking data during the async gap.
|
||||
pendingEvents.push(stored);
|
||||
|
||||
if (ownershipCheckInFlight) return;
|
||||
ownershipCheckInFlight = true;
|
||||
|
||||
void this.memoryService
|
||||
.checkThreadOwnership(userId, threadId)
|
||||
.then((currentOwnership) => {
|
||||
if (currentOwnership === 'other_user') {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
ownershipVerified = true;
|
||||
for (const buffered of pendingEvents) {
|
||||
this.writeSseEvent(res, buffered);
|
||||
}
|
||||
pendingEvents.length = 0;
|
||||
})
|
||||
.catch(() => {
|
||||
pendingEvents.length = 0;
|
||||
res.end();
|
||||
});
|
||||
};
|
||||
|
||||
const unsubscribe = this.eventBus.subscribe(threadId, (stored) => {
|
||||
if (bootstrapping) return;
|
||||
deliver(stored);
|
||||
});
|
||||
|
||||
// Cleanup is registered before the async bootstrap so a client disconnect
|
||||
// (or an error response) during the awaits below doesn't leak the
|
||||
// subscription.
|
||||
let closed = false;
|
||||
let keepAlive: NodeJS.Timeout | undefined = undefined;
|
||||
const cleanup = () => {
|
||||
closed = true;
|
||||
unsubscribe();
|
||||
if (keepAlive !== undefined) clearInterval(keepAlive);
|
||||
};
|
||||
req.once('close', cleanup);
|
||||
res.once('finish', cleanup);
|
||||
|
||||
// 2. Re-publish any terminal outcomes that never reached the client.
|
||||
if (ownership === 'owned') {
|
||||
await this.instanceAiService.replayUndeliveredTerminalOutcomes(threadId, {
|
||||
delivery: 'event',
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Set SSE headers.
|
||||
// Disable response compression — SSE streams small chunks where compression
|
||||
// overhead exceeds the benefit, and each Brotli compressor retains ~8.6 MB
|
||||
// of native memory for the lifetime of the connection.
|
||||
@@ -239,7 +300,7 @@ export class InstanceAiController {
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.flushHeaders();
|
||||
|
||||
// 2. Determine replay cursor
|
||||
// 4. Determine replay cursor
|
||||
// Last-Event-ID header (browser auto-reconnect) takes precedence over query param.
|
||||
// Both are validated as non-negative integers; invalid values fall back to 0.
|
||||
const headerValue = req.headers['last-event-id'];
|
||||
@@ -247,20 +308,9 @@ export class InstanceAiController {
|
||||
const cursor =
|
||||
Number.isFinite(parsedHeader) && parsedHeader >= 0 ? parsedHeader : (query.lastEventId ?? 0);
|
||||
|
||||
// 3. Replay missed events then subscribe in the same tick.
|
||||
// Since InProcessEventBus is synchronous and single-threaded (Node.js
|
||||
// event loop), there is no window for missed events between replay and
|
||||
// subscribe when done in the same synchronous block.
|
||||
const missed = this.eventBus.getEventsAfter(threadId, cursor);
|
||||
for (const stored of missed) {
|
||||
this.writeSseEvent(res, stored);
|
||||
}
|
||||
|
||||
// 3b. Bootstrap sync: emit one run-sync control frame per live message group.
|
||||
// Multiple groups can be active simultaneously when a background task
|
||||
// from an older turn outlives its original turn. Each frame uses named
|
||||
// SSE event type (event: run-sync) with NO id: field so the browser's
|
||||
// lastEventId is unaffected and replay cursor stays consistent.
|
||||
// 5. Collect live message groups and fetch their persisted snapshots.
|
||||
// Multiple groups can be active simultaneously when a background task
|
||||
// from an older turn outlives its original turn.
|
||||
const threadStatus = this.instanceAiService.getThreadStatus(threadId);
|
||||
|
||||
// Collect all distinct message groups that have live activity.
|
||||
@@ -291,16 +341,41 @@ export class InstanceAiController {
|
||||
}
|
||||
}
|
||||
|
||||
const persistedSnapshots = new Map<string, AgentTreeSnapshot | undefined>();
|
||||
for (const [groupId, group] of liveGroups) {
|
||||
persistedSnapshots.set(
|
||||
groupId,
|
||||
await this.memoryService.getLatestRunSnapshot(threadId, {
|
||||
messageGroupId: groupId,
|
||||
// Use the group's own latest runId — NOT the thread-global
|
||||
// activeRunId, which belongs to the current orchestrator turn and
|
||||
// would be wrong for background groups from older turns.
|
||||
runId: group.runIds.at(-1),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// The client may have disconnected during the awaits above.
|
||||
if (closed) return;
|
||||
|
||||
// 6. Replay missed events, emit run-sync frames, and flip to live delivery
|
||||
// in one synchronous block. The event bus store and emitter are
|
||||
// synchronous, so no event can slip between the replay and the live
|
||||
// handler taking over. Events that arrived during the awaits above are
|
||||
// already in the store (the early subscription in step 1 keeps relayed
|
||||
// events flowing in multi-main) and are included in the replay here.
|
||||
const missed = this.eventBus.getEventsAfter(threadId, cursor);
|
||||
for (const stored of missed) {
|
||||
deliver(stored);
|
||||
}
|
||||
|
||||
// 6b. Bootstrap sync: emit one run-sync control frame per live message
|
||||
// group. Each frame uses a named SSE event type (event: run-sync) with
|
||||
// NO id: field so the browser's lastEventId is unaffected and the
|
||||
// replay cursor stays consistent.
|
||||
for (const [groupId, group] of liveGroups) {
|
||||
const runEvents = this.eventBus.getEventsForRuns(threadId, group.runIds);
|
||||
// Use the group's own latest runId — NOT the thread-global activeRunId,
|
||||
// which belongs to the current orchestrator turn and would be wrong for
|
||||
// background groups from older turns.
|
||||
const groupRunId = group.runIds.at(-1);
|
||||
const persistedSnapshot = await this.memoryService.getLatestRunSnapshot(threadId, {
|
||||
messageGroupId: groupId,
|
||||
runId: groupRunId,
|
||||
});
|
||||
const persistedSnapshot = persistedSnapshots.get(groupId);
|
||||
if (runEvents.length === 0 && !persistedSnapshot) continue;
|
||||
|
||||
const eventTree = buildAgentTreeFromEvents(runEvents);
|
||||
@@ -310,7 +385,7 @@ export class InstanceAiController {
|
||||
);
|
||||
res.write(
|
||||
`event: run-sync\ndata: ${JSON.stringify({
|
||||
runId: groupRunId,
|
||||
runId: group.runIds.at(-1),
|
||||
messageGroupId: groupId,
|
||||
runIds: group.runIds,
|
||||
agentTree,
|
||||
@@ -321,53 +396,13 @@ export class InstanceAiController {
|
||||
}
|
||||
if (liveGroups.size > 0) res.flush?.();
|
||||
|
||||
// 4. Subscribe to live events
|
||||
// When the thread was not_found at connect time, re-validate ownership on
|
||||
// the first event. Buffer all events until the check resolves to avoid
|
||||
// leaking data during the async gap.
|
||||
const unsubscribe = this.eventBus.subscribe(threadId, (stored) => {
|
||||
if (ownershipVerified) {
|
||||
this.writeSseEvent(res, stored);
|
||||
return;
|
||||
}
|
||||
bootstrapping = false;
|
||||
|
||||
pendingEvents.push(stored);
|
||||
|
||||
if (ownershipCheckInFlight) return;
|
||||
ownershipCheckInFlight = true;
|
||||
|
||||
void this.memoryService
|
||||
.checkThreadOwnership(userId, threadId)
|
||||
.then((currentOwnership) => {
|
||||
if (currentOwnership === 'other_user') {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
ownershipVerified = true;
|
||||
for (const buffered of pendingEvents) {
|
||||
this.writeSseEvent(res, buffered);
|
||||
}
|
||||
pendingEvents.length = 0;
|
||||
})
|
||||
.catch(() => {
|
||||
pendingEvents.length = 0;
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
// 5. Keep-alive
|
||||
const keepAlive = setInterval(() => {
|
||||
// 7. Keep-alive
|
||||
keepAlive = setInterval(() => {
|
||||
res.write(': ping\n\n');
|
||||
res.flush?.();
|
||||
}, KEEP_ALIVE_INTERVAL_MS);
|
||||
|
||||
// 6. Cleanup on disconnect
|
||||
const cleanup = () => {
|
||||
unsubscribe();
|
||||
clearInterval(keepAlive);
|
||||
};
|
||||
req.once('close', cleanup);
|
||||
res.once('finish', cleanup);
|
||||
}
|
||||
|
||||
@Post('/confirm/:requestId')
|
||||
@@ -398,7 +433,7 @@ export class InstanceAiController {
|
||||
async cancel(req: AuthenticatedRequest, _res: Response, @Param('threadId') threadId: string) {
|
||||
this.requireInstanceAiEnabled();
|
||||
await this.assertThreadAccess(req.user.id, threadId);
|
||||
this.instanceAiService.cancelRun(threadId);
|
||||
await this.instanceAiService.routeCancelRun(threadId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -432,7 +467,7 @@ export class InstanceAiController {
|
||||
) {
|
||||
this.requireInstanceAiEnabled();
|
||||
await this.assertThreadAccess(req.user.id, threadId);
|
||||
this.instanceAiService.cancelBackgroundTask(threadId, taskId);
|
||||
await this.instanceAiService.routeCancelBackgroundTask(threadId, taskId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -447,7 +482,7 @@ export class InstanceAiController {
|
||||
) {
|
||||
this.requireInstanceAiEnabled();
|
||||
await this.assertThreadAccess(req.user.id, threadId);
|
||||
this.instanceAiService.sendCorrectionToTask(threadId, taskId, payload.message);
|
||||
await this.instanceAiService.routeCorrectionToTask(threadId, taskId, payload.message);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -586,7 +621,7 @@ export class InstanceAiController {
|
||||
) {
|
||||
this.requireInstanceAiEnabled();
|
||||
await this.assertThreadAccess(req.user.id, threadId);
|
||||
await this.instanceAiService.clearThreadState(threadId);
|
||||
await this.instanceAiService.routeClearThreadState(threadId);
|
||||
await this.memoryService.deleteThread(threadId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Logger } from '@n8n/backend-common';
|
||||
import { SsrfProtectionService } from '@n8n/backend-network';
|
||||
import { GlobalConfig, SsrfProtectionConfig, type InstanceAiConfig } from '@n8n/config';
|
||||
import { UserRepository, type User } from '@n8n/db';
|
||||
import { OnLeaderStepdown, OnLeaderTakeover } from '@n8n/decorators';
|
||||
import { OnLeaderStepdown, OnLeaderTakeover, OnPubSubEvent } from '@n8n/decorators';
|
||||
import { Service } from '@n8n/di';
|
||||
import {
|
||||
MAX_STEPS,
|
||||
@@ -100,6 +100,8 @@ import { nanoid } from 'nanoid';
|
||||
import { N8N_VERSION, WORKFLOW_SDK_VERSION } from '@/constants';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { SourceControlPreferencesService } from '@/modules/source-control.ee/source-control-preferences.service.ee';
|
||||
import type { PubSubCommandMap } from '@/scaling/pubsub/pubsub.event-map';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { AiService } from '@/services/ai.service';
|
||||
import { ProxyTokenManager } from '@/services/proxy-token-manager';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
@@ -534,6 +536,7 @@ export class InstanceAiService {
|
||||
runProbe: InstanceAiRunProbe,
|
||||
private readonly modelService: InstanceAiModelService,
|
||||
private readonly creditService: InstanceAiCreditService,
|
||||
private readonly publisher: Publisher,
|
||||
private readonly instanceAiErrorReporter: InstanceAiErrorReporterService,
|
||||
) {
|
||||
this.logger = logger.scoped('instance-ai');
|
||||
@@ -1054,6 +1057,108 @@ export class InstanceAiService {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-main task-control routing ───────────────────────────────────────
|
||||
// User actions (correct/cancel/clear) can land on a different main than the
|
||||
// one running the task/run. Each `route*` method applies the action locally
|
||||
// and, when the target isn't local (or is thread-wide), broadcasts so the
|
||||
// owning main applies it. Broadcast + local-gate — no shared ownership store.
|
||||
// `applyTaskControlLocally` is the single action → local-method mapping,
|
||||
// shared by the route entry points and the `@OnPubSubEvent` relay handler
|
||||
// (which never re-broadcasts, so there's no loop). These wrappers are the
|
||||
// controller entry points; internal callers keep using the local methods
|
||||
// directly so they don't trigger cross-main broadcasts.
|
||||
|
||||
private broadcastTaskControl(payload: PubSubCommandMap['relay-instance-ai-task-control']): void {
|
||||
if (!this.instanceSettings.isMultiMain) return;
|
||||
void this.publisher
|
||||
.publishCommand({ command: 'relay-instance-ai-task-control', payload })
|
||||
.catch((error: unknown) =>
|
||||
this.logger.error('Failed to relay Instance AI task-control to sibling mains', {
|
||||
threadId: payload.threadId,
|
||||
action: payload.action,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Apply a task-control action to this main's local slice of the thread.
|
||||
* Returns whether the action's target was found locally: task-scoped
|
||||
* actions report a local hit to gate re-broadcast, thread-wide actions
|
||||
* always report a miss so they fan out to every main. */
|
||||
private async applyTaskControlLocally({
|
||||
threadId,
|
||||
taskId,
|
||||
action,
|
||||
correction,
|
||||
}: PubSubCommandMap['relay-instance-ai-task-control']): Promise<boolean> {
|
||||
switch (action) {
|
||||
case 'correct':
|
||||
// A relay without its correction text is malformed: nothing to apply.
|
||||
if (!taskId || correction === undefined) return true;
|
||||
return this.sendCorrectionToTask(threadId, taskId, correction) !== 'task-not-found';
|
||||
case 'cancel-task': {
|
||||
if (!taskId) return true;
|
||||
const isLocal = this.backgroundTasks
|
||||
.getTaskSnapshots(threadId)
|
||||
.some((task) => task.taskId === taskId);
|
||||
this.cancelBackgroundTask(threadId, taskId);
|
||||
return isLocal;
|
||||
}
|
||||
case 'cancel-thread':
|
||||
// A thread's run + tasks can be spread across mains, so always fan out.
|
||||
this.cancelRun(threadId);
|
||||
return false;
|
||||
case 'clear-thread':
|
||||
await this.clearThreadState(threadId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async routeTaskControl(
|
||||
payload: PubSubCommandMap['relay-instance-ai-task-control'],
|
||||
): Promise<void> {
|
||||
const foundLocally = await this.applyTaskControlLocally(payload);
|
||||
if (!foundLocally) this.broadcastTaskControl(payload);
|
||||
}
|
||||
|
||||
async routeCorrectionToTask(threadId: string, taskId: string, correction: string): Promise<void> {
|
||||
await this.routeTaskControl({ threadId, taskId, action: 'correct', correction });
|
||||
}
|
||||
|
||||
async routeCancelBackgroundTask(threadId: string, taskId: string): Promise<void> {
|
||||
await this.routeTaskControl({ threadId, taskId, action: 'cancel-task' });
|
||||
}
|
||||
|
||||
async routeCancelRun(threadId: string): Promise<void> {
|
||||
await this.routeTaskControl({ threadId, action: 'cancel-thread' });
|
||||
}
|
||||
|
||||
async routeClearThreadState(threadId: string): Promise<void> {
|
||||
await this.routeTaskControl({ threadId, action: 'clear-thread' });
|
||||
}
|
||||
|
||||
/** Apply a task-control action relayed from another main to this main's local
|
||||
* slice of the thread. Never re-broadcasts. Not self-sent, so this never
|
||||
* fires on the originating main. */
|
||||
@OnPubSubEvent('relay-instance-ai-task-control', { instanceType: 'main' })
|
||||
async handleRelayTaskControl(
|
||||
payload: PubSubCommandMap['relay-instance-ai-task-control'],
|
||||
): Promise<void> {
|
||||
// Guard the whole handler: it runs as a fire-and-forget pubsub listener, so
|
||||
// a throw (e.g. from the async clearThreadState) would surface as an
|
||||
// unhandled rejection on this sibling main instead of being contained.
|
||||
try {
|
||||
await this.applyTaskControlLocally(payload);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to apply relayed Instance AI task-control', {
|
||||
threadId: payload.threadId,
|
||||
taskId: payload.taskId,
|
||||
action: payload.action,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel all background tasks across all threads. Test-only. */
|
||||
cancelAllBackgroundTasks(): number {
|
||||
const cancelled = this.backgroundTasks.cancelAll();
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Server as WSServer } from 'ws';
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { MAX_PUBSUB_PAYLOAD_BYTES } from '@/scaling/constants';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
|
||||
import { validateOriginHeaders } from './origin-validator';
|
||||
@@ -33,12 +34,6 @@ type PushEvents = {
|
||||
message: OnPushMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Max allowed size of a push message in bytes. Events going through the pubsub
|
||||
* channel are trimmed if exceeding this size.
|
||||
*/
|
||||
const MAX_PAYLOAD_SIZE_BYTES = 5 * 1024 * 1024; // 5 MiB
|
||||
|
||||
/**
|
||||
* Push service for uni- or bi-directional communication with frontend clients.
|
||||
* Uses either server-sent events (SSE, unidirectional from backend --> frontend)
|
||||
@@ -245,10 +240,10 @@ export class Push extends TypedEmitter<PushEvents> {
|
||||
if (type === 'nodeExecuteAfterData') {
|
||||
const eventSizeBytes = new TextEncoder().encode(JSON.stringify(pushMsg.data)).length;
|
||||
|
||||
if (eventSizeBytes > MAX_PAYLOAD_SIZE_BYTES) {
|
||||
if (eventSizeBytes > MAX_PUBSUB_PAYLOAD_BYTES) {
|
||||
const toMb = (bytes: number) => (bytes / (1024 * 1024)).toFixed(0);
|
||||
const eventMb = toMb(eventSizeBytes);
|
||||
const maxMb = toMb(MAX_PAYLOAD_SIZE_BYTES);
|
||||
const maxMb = toMb(MAX_PUBSUB_PAYLOAD_BYTES);
|
||||
|
||||
this.logger.warn(
|
||||
`Size of "${type}" (${eventMb} MB) exceeds max size ${maxMb} MB. Skipping...`,
|
||||
|
||||
@@ -13,6 +13,12 @@ export const WORKER_RESPONSE_PUBSUB_CHANNEL = 'n8n.worker-response';
|
||||
/** Pubsub channel for MCP relay messages between main instances in multi-main queue mode. */
|
||||
export const MCP_RELAY_PUBSUB_CHANNEL = 'n8n.mcp-relay';
|
||||
|
||||
/**
|
||||
* Max allowed size in bytes of a message relayed over the pubsub channel. Events
|
||||
* exceeding this are skipped (or trimmed) rather than bloating the channel.
|
||||
*/
|
||||
export const MAX_PUBSUB_PAYLOAD_BYTES = 5 * 1024 * 1024; // 5 MiB
|
||||
|
||||
/**
|
||||
* Commands that should be sent to the sender as well, e.g. during workflow activation and
|
||||
* deactivation in multi-main setup. */
|
||||
@@ -32,6 +38,8 @@ export const IMMEDIATE_COMMANDS = new Set<PubSub.Command['command']>([
|
||||
'remove-triggers-and-pollers',
|
||||
'relay-execution-lifecycle-event',
|
||||
'relay-chat-stream-event',
|
||||
'relay-instance-ai-event',
|
||||
'relay-instance-ai-task-control',
|
||||
'agent-chat-subscription-changed',
|
||||
'cancel-test-run',
|
||||
'stop-execution',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgentIntegrationConfig,
|
||||
ChatHubMessageStatus,
|
||||
InstanceAiEvent,
|
||||
PushMessage,
|
||||
WorkerStatus,
|
||||
} from '@n8n/api-types';
|
||||
@@ -150,6 +151,32 @@ export type PubSubCommandMap = {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Relay an Instance AI stream event to sibling mains.
|
||||
*
|
||||
* The agent runs on whichever main received `POST /chat/:threadId` and emits
|
||||
* events into that main's in-process bus, but the client's `GET /events/:threadId`
|
||||
* SSE connection may be held by a different main. The producing main relays each
|
||||
* event; the main holding the SSE subscription re-emits it locally to its client.
|
||||
*/
|
||||
'relay-instance-ai-event': {
|
||||
threadId: string;
|
||||
event: InstanceAiEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relay an Instance AI task-control action (correction / cancel / clear) to
|
||||
* sibling mains. The action may target a background task or run held
|
||||
* in another main's in-memory state. Broadcast + local-gate: every main applies
|
||||
* it only to its own local slice of the thread.
|
||||
*/
|
||||
'relay-instance-ai-task-control': {
|
||||
threadId: string;
|
||||
taskId?: string;
|
||||
action: 'correct' | 'cancel-task' | 'cancel-thread' | 'clear-thread';
|
||||
correction?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relay human message events between main instances.
|
||||
* Used for cross-client synchronization when a user sends a message
|
||||
|
||||
@@ -59,6 +59,8 @@ export namespace PubSub {
|
||||
export type WorkflowPublishWakeUp = ToCommand<'workflow-publish-wake-up'>;
|
||||
export type RelayExecutionLifecycleEvent = ToCommand<'relay-execution-lifecycle-event'>;
|
||||
export type RelayChatStreamEvent = ToCommand<'relay-chat-stream-event'>;
|
||||
export type RelayInstanceAiEvent = ToCommand<'relay-instance-ai-event'>;
|
||||
export type RelayInstanceAiTaskControl = ToCommand<'relay-instance-ai-task-control'>;
|
||||
export type RelayChatHumanMessage = ToCommand<'relay-chat-human-message'>;
|
||||
export type RelayChatMessageEdit = ToCommand<'relay-chat-message-edit'>;
|
||||
export type ClearTestWebhooks = ToCommand<'clear-test-webhooks'>;
|
||||
@@ -95,6 +97,8 @@ export namespace PubSub {
|
||||
| Commands.WorkflowPublishWakeUp
|
||||
| Commands.RelayExecutionLifecycleEvent
|
||||
| Commands.RelayChatStreamEvent
|
||||
| Commands.RelayInstanceAiEvent
|
||||
| Commands.RelayInstanceAiTaskControl
|
||||
| Commands.RelayChatHumanMessage
|
||||
| Commands.RelayChatMessageEdit
|
||||
| Commands.ClearTestWebhooks
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*Reply with exactly the single word: pong\\. Do not use any tools\\.\\\\n\\\\n<current-date-time>\\\\n## Current Date and Time\\\\n\\\\nThe user'[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-257333ecdc34ed04df1bec188d0604b9-0ad4bf2621dc6d14-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=SFDWYwrKj7nS9SomTXtVjfUIBmcxsbqDuIO8P1pANtY-1782824956.0979054-1.0.1.1-nMZLiQRgm5ffii4i6ydwZPBEqcFn199Kx54m109XSeI; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CcZVqoMyTi1CJuAYzdLTp"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-06-30T13:09:16Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17989000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-06-30T13:09:16Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19999"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-06-30T13:09:16Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-06-30T13:09:16Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14989000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 30 Jun 2026 13:09:21 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a13d67879a1132d9-HEL"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "SFDWYwrKj7nS9SomTXtVjfUIBmcxsbqDuIO8P1pANtY-1782824956.0979054-1.0.1.1-nMZLiQRgm5ffii4i6ydwZPBEqcFn199Kx54m109XSeI"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01BV1ez6mLA33xhnyRdY24zJ\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":17993,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":17993,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"p\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ong\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":17993,\"cache_read_input_tokens\":0,\"output_tokens\":5,\"output_tokens_details\":{\"thinking_tokens\":0}} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxQlYxZXo2bUxBMzN4aG55UmRZMjR6SiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTc5OTMsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjoxNzk5MywiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoicCJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Im9uZyJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjozLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjE3OTkzLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6NSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH19ICAgICAgICB9CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCIgICAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1782824962620-unknown-host-POST-_v1_messages-dcc60945.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*Build a workflow named \\\\\"INS-164 mocked credential guard\\\\\" with a Manual Trigger connected to a Slack node that posts a me[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-096c2c1d2adf050db9c94a20a4f5ea37-cd3d7191e72d2884-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=VB_1tZGEQc003LPoTH4I4a9av.pb.j0Dm_K3jCF8_6Q-1781088435.499604-1.0.1.1-EBbUWJcv6pIpx9.oZyT9lc1PWEokxYgebRIERf2LBCg; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CbuSqPo8u678GaYz8npbW"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-06-10T10:47:15Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26973000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-06-10T10:47:15Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-06-10T10:47:15Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-06-10T10:47:15Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22473000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Wed, 10 Jun 2026 10:47:16 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a097cc01dec8b236-HEL"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "VB_1tZGEQc003LPoTH4I4a9av.pb.j0Dm_K3jCF8_6Q-1781088435.499604-1.0.1.1-EBbUWJcv6pIpx9.oZyT9lc1PWEokxYgebRIERf2LBCg"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01QDLxqEM9e6QAx8A76gahtp\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":15336,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":15336,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Loading\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" the workflow-builder skill before building.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01H8kceqRSsGHmLWsgBvuG4k\",\"name\":\"load_skill\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"name\\\": \\\"workflow-builder\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":15336,\"cache_read_input_tokens\":0,\"output_tokens\":65} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxUURMeHFFTTllNlFBeDhBNzZnYWh0cCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTUzMzYsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjoxNTMzNiwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGV4dCIsInRleHQiOiIifSAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJMb2FkaW5nIn0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgdGhlIHdvcmtmbG93LWJ1aWxkZXIgc2tpbGwgYmVmb3JlIGJ1aWxkaW5nLiJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxSDhrY2VxUlNzR0htTFdzZ0J2dUc0ayIsIm5hbWUiOiJsb2FkX3NraWxsIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wibmFtZVwiOiBcIndvcmtmbG93LWJ1aWxkZXIifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIn0ifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjEgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTUzMzYsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo2NX0gICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1781088457406-unknown-host-POST-_v1_messages-a942b0c9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*Build a workflow named \\\\\"INS-164 mocked credential guard\\\\\" with a Manual Trigger connected to a Slack node that posts a me[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-dd9f24ce6d3493e1a8ad23d7d8701464-e43ece6a6cbee859-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CcfQCZed4mQmgbcXtDFr8"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-07-03T15:58:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17989000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-07-03T15:58:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19999"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-07-03T15:58:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-07-03T15:58:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14989000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Fri, 03 Jul 2026 15:58:52 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a157180f6b9bf468-HEL"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01U73eqSHxFBEZ8DRTkwRq3M\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":18174,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":18174,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":7,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user wants me to build a workflow named \\\"INS-164 mocked credential guard\\\" with a Manual Trigger connected to a Slack node that posts a message using a mocked slackApi credential placeholder. Let me load\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" the workflow-builder skill first.\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"ErkDCmUIDxgCKkCAbaDQERxAdJR7Mv8Zk0L2gBIMqyDk5Xutac9+WomEVOGq3XucU7aBmBLwe2rk7QHGPmry9uJ+HO188mnmbbX+MhFjbGF1ZGUtc29ubmV0LTQtNjgAQgh0aGlua2luZxIMPcsHSWii08djOnBvGgwgJY3kUjOepZFnqrciMG+FK6UpWSyekQmIRG9g7cyr7Ir9pnQyz7uxQMsd5KTTwbPLlVgAWfkgzCwCV6cV1SqBAlBzDkpsQnWzj+S61GLyN7UEqoas6HJwnVydwa65o4pKkkxCXU8XQXDfK6/5LfZ5rjl4DZdacS8CfMhvotCx2inyanHHk/pzKR8yFuTKKjH5LZQ0faK4959/u7tG3yyEvd4kjayOXMMbePlE5OR1wUrze5iyJ8vnRoy5Of7rNdWRcPROzOsyetdW1cSHquSCQg8ZE7hVjuFNnW6tnJTJaJKDtpnIh0WZLYw7OhxnqCFx1XN4SlHTTImi3iiU9x8QBGL5xAvfsbAbJ0jADJbTGVYr/VxSDdUzpb2ezfhvZL3AhLnaLA8BO7PhgF4a4e/EortEVPZ5KwrtPyfwQUkWaLPOGAE=\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Loading the workflow-builder skill before writing any code.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_015MSAudnAxno2DVs6BjXKp3\",\"name\":\"load_skill\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"name\\\": \\\"workflow-builder\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":3,\"cache_creation_input_tokens\":18174,\"cache_read_input_tokens\":0,\"output_tokens\":137,\"output_tokens_details\":{\"thinking_tokens\":67}} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxVTczZXFTSHhGQkVaOERSVGt3UnEzTSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTgxNzQsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjoxODE3NCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjcsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRoaW5raW5nIiwidGhpbmtpbmciOiIiLCJzaWduYXR1cmUiOiIifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IlRoZSB1c2VyIHdhbnRzIG1lIHRvIGJ1aWxkIGEgd29ya2Zsb3cgbmFtZWQgXCJJTlMtMTY0IG1vY2tlZCBjcmVkZW50aWFsIGd1YXJkXCIgd2l0aCBhIE1hbnVhbCBUcmlnZ2VyIGNvbm5lY3RlZCB0byBhIFNsYWNrIG5vZGUgdGhhdCBwb3N0cyBhIG1lc3NhZ2UgdXNpbmcgYSBtb2NrZWQgc2xhY2tBcGkgY3JlZGVudGlhbCBwbGFjZWhvbGRlci4gTGV0IG1lIGxvYWQifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHRoZSB3b3JrZmxvdy1idWlsZGVyIHNraWxsIGZpcnN0LiJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoic2lnbmF0dXJlX2RlbHRhIiwic2lnbmF0dXJlIjoiRXJrRENtVUlEeGdDS2tDQWJhRFFFUnhBZEpSN012OFprMEwyZ0JJTXF5RGs1WHV0YWM5K1dvbUVWT0dxM1h1Y1U3YUJtQkx3ZTJyazdRSEdQbXJ5OXVKK0hPMTg4bW5tYmJYK01oRmpiR0YxWkdVdGMyOXVibVYwTFRRdE5qZ0FRZ2gwYUdsdWEybHVaeElNUGNzSFNXaWkwOGRqT25Cdkdnd2dKWTNrVWpPZXBaRm5xcmNpTUcrRks2VXBXU3lla1FtSVJHOWc3Y3lyN0lyOXBuUXl6N3V4UU1zZDVLVFR3YlBMbFZnQVdma2d6Q3dDVjZjVjFTcUJBbEJ6RGtwc1FuV3pqK1M2MUdMeU43VUVxb2FzNkhKd25WeWR3YTY1bzRwS2treENYVThYUVhEZks2LzVMZlo1cmpsNERaZGFjUzhDZk1odm90Q3gyaW55YW5ISGsvcHpLUjh5RnVUS0tqSDVMWlEwZmFLNDk1OS91N3RHM3l5RXZkNGtqYXlPWE1NYmVQbEU1T1Ixd1VyemU1aXlKOHZuUm95NU9mN3JOZFdSY1BST3pPc3lldGRXMWNTSHF1U0NRZzhaRTdoVmp1Rk5uVzZ0bkpUSmFKS0R0cG5JaDBXWkxZdzdPaHhucUNGeDFYTjRTbEhUVEltaTNpaVU5eDhRQkdMNXhBdmZzYkFiSjBqQURKYlRHVllyL1Z4U0RkVXpwYjJlemZodlpMM0FoTG5hTEE4Qk83UGhnRjRhNGUvRW9ydEVWUFo1S3dydFB5ZndRVWtXYUxQT0dBRT0ifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjoxLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiTG9hZGluZyB0aGUgd29ya2Zsb3ctYnVpbGRlciBza2lsbCBiZWZvcmUgd3JpdGluZyBhbnkgY29kZS4ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjEgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjIsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMTVNU0F1ZG5BeG5vMkRWczZCalhLcDMiLCJuYW1lIjoibG9hZF9za2lsbCIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjIsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjIsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wibmFtZVwiOiBcIndvcmtmbG93LWJ1aWxkZXIifSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MiwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIn0ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjJ9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxODE3NCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjEzNywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6Njd9fSAgICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1783094387250-unknown-host-POST-_v1_messages-a942b0c9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,1500}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,2000}\\\\\"success\\\\\":true[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-f82ed43335128ea318b236481fdf63c0-d7b9cef1f79e85e2-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=bD1JaEtFDBU30TuRQWLnhLWS42SfHn2_F2gIR7Y_c_4-1781088437.9595-1.0.1.1-ydwko9qf2D91tl.MRFl5naSCwiKUphpJuhlPX2ia9VM; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CbuSqaNdYw3TbqCy92oVf"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-06-10T10:47:18Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26973000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-06-10T10:47:18Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-06-10T10:47:18Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-06-10T10:47:18Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22473000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Wed, 10 Jun 2026 10:47:20 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a097cc113efcbc86-HEL"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "bD1JaEtFDBU30TuRQWLnhLWS42SfHn2_F2gIR7Y_c_4-1781088437.9595-1.0.1.1-ydwko9qf2D91tl.MRFl5naSCwiKUphpJuhlPX2ia9VM"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01AgrALjqXcLvuxfe9FJ4Kmx\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":15474,\"cache_read_input_tokens\":15336,\"cache_creation\":{\"ephemeral_5m_input_tokens\":15474,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":47,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01QpcFwdyXg3zcJ9R39RKDdf\",\"name\":\"nodes\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"search\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"query\\\": \\\"Slack\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"limit\\\": 5\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":15474,\"cache_read_input_tokens\":15336,\"output_tokens\":86} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxQWdyQUxqcVhjTHZ1eGZlOUZKNEtteCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTU0NzQsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxNTMzNiwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MTU0NzQsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo0Nywic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0b29sX3VzZSIsImlkIjoidG9vbHVfMDFRcGNGd2R5WGczemNKOVIzOVJLRGRmIiwibmFtZSI6Im5vZGVzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6IFwic2VhcmNoIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiLCBcInF1ZXJ5XCI6IFwiU2xhY2sifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIiwgXCJsaW1pdFwiOiA1In0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6In0ifSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjE1NDc0LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTUzMzYsIm91dHB1dF90b2tlbnMiOjg2fSAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1781088457408-unknown-host-POST-_v1_messages-77bad225.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,15000}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,100000}\\\\\"success\\\\\"\\s*:\\s*true[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-2bc191e87143dee1ca785d6a3dfdad94-9578e76a3aed2054-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CcfQCq55B7WXQ572CF3pE"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-07-03T15:58:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17989000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-07-03T15:58:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19999"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-07-03T15:58:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-07-03T15:58:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14989000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Fri, 03 Jul 2026 15:58:56 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a1571825ee1d1579-HEL"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01N7pz1beAbRrMwJLLxheM3K\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":19755,\"cache_read_input_tokens\":18174,\"cache_creation\":{\"ephemeral_5m_input_tokens\":19755,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Now\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" I'll look up the Slack node type definition to\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" get the exact credential type and parameter shape before writing the source\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" file.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01DSwWbqBwKbk4hxbrtadHBG\",\"name\":\"nodes\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"search\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"query\\\": \\\"Slack\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"limit\\\": 5\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":19755,\"cache_read_input_tokens\":18174,\"output_tokens\":113,\"output_tokens_details\":{\"thinking_tokens\":0}} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxTjdwejFiZUFiUnJNd0pMTHhoZU0zSyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTk3NTUsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxODE3NCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MTk3NTUsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJOb3cifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIEknbGwgbG9vayB1cCB0aGUgU2xhY2sgbm9kZSB0eXBlIGRlZmluaXRpb24gdG8ifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBnZXQgdGhlIGV4YWN0IGNyZWRlbnRpYWwgdHlwZSBhbmQgcGFyYW1ldGVyIHNoYXBlIGJlZm9yZSB3cml0aW5nIHRoZSBzb3VyY2UifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIGZpbGUuIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxRFN3V2JxQndLYms0aHhicnRhZEhCRyIsIm5hbWUiOiJub2RlcyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6IFwic2VhcmNoIn0gICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIiwgXCJxdWVyeVwiOiBcIlNsYWNrIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIiwgXCJsaW1pdFwiOiA1In0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6In0ifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxICAgICAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxOTc1NSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjE4MTc0LCJvdXRwdXRfdG9rZW5zIjoxMTMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9fSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1783094387252-unknown-host-POST-_v1_messages-f808330f.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,1500}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,2000}\\{\\\\\"results\\\\\":\\[\\{\\\\\"name\\\\\":\\\\\"n8n-nodes-base\\.slack\\\\\",\\\\\"displayName\\\\\":\\\\\"Slack\\\\\",\\\\\"description\\\\\":\\\\\"Consume Slack API\\\\\",\\\\\"version\\\\\":2\\.5,\\\\\"inputs[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-cbbd6bc2127819eef3255234d1cf3eeb-de3c3226af054035-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=bHGRW2nTkJiSJh98mLt8z.qj6FEPnfy_3h7qwEEQYOk-1781088441.8658457-1.0.1.1-9tYojpjjr1QvFWicZJWS2C7oH3dSs5R8HcZUphUxOfo; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CbuSqs2jeDyAPpFfuxPSy"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-06-10T10:47:22Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26973000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-06-10T10:47:22Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-06-10T10:47:22Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-06-10T10:47:22Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22473000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Wed, 10 Jun 2026 10:47:24 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a097cc29aa519a02-HEL"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "bHGRW2nTkJiSJh98mLt8z.qj6FEPnfy_3h7qwEEQYOk-1781088441.8658457-1.0.1.1-9tYojpjjr1QvFWicZJWS2C7oH3dSs5R8HcZUphUxOfo"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_014fsT6GbuM5Wp39XArhTB7f\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":788,\"cache_read_input_tokens\":30810,\"cache_creation\":{\"ephemeral_5m_input_tokens\":788,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":48,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01NQ5A7N59J6JMMizNEPB4DX\",\"name\":\"nodes\",\"input\":{},\"caller\":{\"type\":\"direct\"}}}\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"type-definition\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"nodeTypes\\\": [{\\\"nodeType\\\": \\\"n8n-nodes-base.slack\\\", \\\"resource\\\": \\\"message\\\", \\\"operation\\\": \\\"post\\\"}]\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":788,\"cache_read_input_tokens\":30810,\"output_tokens\":99} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNGZzVDZHYnVNNVdwMzlYQXJoVEI3ZiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6Nzg4LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MzA4MTAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjc4OCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjQ4LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMU5RNUE3TjU5SjZKTU1pek5FUEI0RFgiLCJuYW1lIjoibm9kZXMiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19fQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6IFwidHlwZS1kZWZpbml0aW9uIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiLCBcIm5vZGVUeXBlc1wiOiBbe1wibm9kZVR5cGVcIjogXCJuOG4tbm9kZXMtYmFzZS5zbGFja1wiLCBcInJlc291cmNlXCI6IFwibWVzc2FnZVwiLCBcIm9wZXJhdGlvblwiOiBcInBvc3RcIn1dIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ9In0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjo3ODgsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjozMDgxMCwib3V0cHV0X3Rva2VucyI6OTl9IH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1781088457409-unknown-host-POST-_v1_messages-ad09d9a0.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,15000}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,100000}\\{\\\\\"results\\\\\":\\[\\{\\\\\"name\\\\\":\\\\\"n8n-nodes-base\\.slack\\\\\",\\\\\"displayName\\\\\":\\\\\"Slack\\\\\",\\\\\"description\\\\\":\\\\\"Consume Slack API\\\\\",\\\\\"version\\\\\":2\\.5,\\\\\"inputs[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-ad7c731cec5cdc95b19735d4e5c39e3a-dbc0d65479ebb259-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CcfQDBE2Evd9VEgVEsgWZ"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-07-03T15:58:59Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17989000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-07-03T15:58:59Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19999"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-07-03T15:58:59Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-07-03T15:58:59Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14989000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Fri, 03 Jul 2026 15:59:00 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a157184368c95288-HEL"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01VDdWSRoSbASWv9MDyupeCM\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":851,\"cache_read_input_tokens\":37929,\"cache_creation\":{\"ephemeral_5m_input_tokens\":851,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":47,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01Jb8VQ7LBfhwyqVk2mXfsHW\",\"name\":\"nodes\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"type-definition\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"nodeTypes\\\": [{\\\"nodeType\\\": \\\"n8n-nodes-base.slack\\\", \\\"resource\\\": \\\"message\\\", \\\"operation\\\": \\\"post\\\"}\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"]\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":851,\"cache_read_input_tokens\":37929,\"output_tokens\":99,\"output_tokens_details\":{\"thinking_tokens\":0}} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxVkRkV1NSb1NiQVNXdjlNRHl1cGVDTSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6ODUxLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6Mzc5MjksImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjg1MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjQ3LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxSmI4VlE3TEJmaHd5cVZrMm1YZnNIVyIsIm5hbWUiOiJub2RlcyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjdGlvblwiOiBcInR5cGUtZGVmaW5pdGlvbiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIiwgXCJub2RlVHlwZXNcIjogW3tcIm5vZGVUeXBlXCI6IFwibjhuLW5vZGVzLWJhc2Uuc2xhY2tcIiwgXCJyZXNvdXJjZVwiOiBcIm1lc3NhZ2VcIiwgXCJvcGVyYXRpb25cIjogXCJwb3N0XCJ9In0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6Il0ifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6In0ifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjo4NTEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjozNzkyOSwib3V0cHV0X3Rva2VucyI6OTksIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9fSAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1783094387254-unknown-host-POST-_v1_messages-c5c7530c.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
-108
File diff suppressed because one or more lines are too long
+102
File diff suppressed because one or more lines are too long
-108
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,1500}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,2000}\\\\\"success\\\\\":true[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-09ee4379a1d165c5afced684e0aa41a5-52ab64298eb30feb-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=Tx2Dpu4Ih47Wl2zgPKXuE6NCJc3t.JONqQRnncu8XPk-1781088453.0067868-1.0.1.1-CUiyMIX9sqelVrmhsf_r_i4kU1OMZRpaTni.rfhEn1s; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CbuSrghQuDQvrRki9TSbm"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-06-10T10:47:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26972000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-06-10T10:47:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-06-10T10:47:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-06-10T10:47:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22472000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Wed, 10 Jun 2026 10:47:34 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a097cc6f4faf0560-HEL"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "Tx2Dpu4Ih47Wl2zgPKXuE6NCJc3t.JONqQRnncu8XPk-1781088453.0067868-1.0.1.1-CUiyMIX9sqelVrmhsf_r_i4kU1OMZRpaTni.rfhEn1s"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_012ju9RaTdQiLkLWqsEyyGXP\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":676,\"cache_read_input_tokens\":34323,\"cache_creation\":{\"ephemeral_5m_input_tokens\":676,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" build reports `needs_setup` due to unresolved placeholders and a m\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ocked `slackApi` credential. Opening the setup card now.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01PhpA22r9iSt1gt9sMgAhDX\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"setup\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"workflowId\\\": \\\"oZlS9NokcpVHqKR\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"W\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":676,\"cache_read_input_tokens\":34323,\"output_tokens\":117} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxMmp1OVJhVGRRaUxrTFdxc0V5eUdYUCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6Njc2LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MzQzMjMsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjY3NiwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjEsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgICAgICB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlRoZSJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgYnVpbGQgcmVwb3J0cyBgbmVlZHNfc2V0dXBgIGR1ZSB0byB1bnJlc29sdmVkIHBsYWNlaG9sZGVycyBhbmQgYSBtIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Im9ja2VkIGBzbGFja0FwaWAgY3JlZGVudGlhbC4gT3BlbmluZyB0aGUgc2V0dXAgY2FyZCBub3cuIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxUGhwQTIycjlpU3QxZ3Q5c01nQWhEWCIsIm5hbWUiOiJ3b3JrZmxvd3MiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjdGlvblwiOiBcInNldHVwIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiLCBcIndvcmtmbG93SWRcIjogXCJvWmxTOU5va2NwVkhxS1IifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJXIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiXCJ9In19CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6Njc2LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MzQzMjMsIm91dHB1dF90b2tlbnMiOjExN30gICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0004-1781088457412-unknown-host-POST-_v1_messages-77bad225.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*You are the n8n Instance Agent — an AI assistant embedded in an n8n instance\\. Yo[\\s\\S]*\"role\"\\s*:\\s*\"user\"[\\s\\S]{0,15000}\"type\"\\s*:\\s*\"tool_result\"[\\s\\S]{0,100000}\\\\\"success\\\\\"\\s*:\\s*true[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-a6bf722a2818c7538673c9e1fa0df0f2-a9f6a6efb6aeb043-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CcfQFtv5uyC6MypPC4GQh"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-07-03T15:59:36Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17989000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-07-03T15:59:36Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19999"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-07-03T15:59:36Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-07-03T15:59:36Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14989000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Fri, 03 Jul 2026 15:59:38 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"a1571929da2b3b91-HEL"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_018UZac1pHT6eGDs68q2f6oU\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":478,\"cache_read_input_tokens\":41501,\"cache_creation\":{\"ephemeral_5m_input_tokens\":478,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":43,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01TTvoYhLq6YYTAhaxyN2hW5\",\"name\":\"build-workflow\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"filePath\\\": \\\"src/workflows/ins-164-mocked-credential\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"-guard.workflow.ts\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\", \\\"name\\\": \\\"INS-164 mocked credential guard\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":478,\"cache_read_input_tokens\":41501,\"output_tokens\":96,\"output_tokens_details\":{\"thinking_tokens\":0}} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxOFVaYWMxcEhUNmVHRHM2OHEyZjZvVSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6NDc4LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6NDE1MDEsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjQ3OCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjQzLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxVFR2b1loTHE2WVlUQWhheHlOMmhXNSIsIm5hbWUiOiJidWlsZC13b3JrZmxvdyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ7XCJmaWxlUGF0aFwiOiBcInNyYy93b3JrZmxvd3MvaW5zLTE2NC1tb2NrZWQtY3JlZGVudGlhbCJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLWd1YXJkLndvcmtmbG93LnRzIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiLCBcIm5hbWVcIjogXCJJTlMtMTY0IG1vY2tlZCBjcmVkZW50aWFsIGd1YXJkIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiXCJ9In0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjQ3OCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjQxNTAxLCJvdXRwdXRfdG9rZW5zIjo5Niwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH19ICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0004-1783094387259-unknown-host-POST-_v1_messages-f808330f.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
+102
File diff suppressed because one or more lines are too long
+5
-5
File diff suppressed because one or more lines are too long
@@ -342,6 +342,24 @@ export class ApiHelpers {
|
||||
return body.data.thread;
|
||||
}
|
||||
|
||||
/** Start an Instance AI chat run on a thread; returns the started `runId`. */
|
||||
async startInstanceAiChat(
|
||||
threadId: string,
|
||||
message: string,
|
||||
timeZone = 'UTC',
|
||||
): Promise<{ runId: string }> {
|
||||
const response = await this.request.post(`/rest/instance-ai/chat/${threadId}`, {
|
||||
data: { message, timeZone },
|
||||
});
|
||||
if (!response.ok()) {
|
||||
throw new TestError(
|
||||
`POST /rest/instance-ai/chat/${threadId} failed (${response.status()}): ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
const body = (await response.json()) as { data: { runId: string } };
|
||||
return body.data;
|
||||
}
|
||||
|
||||
async renameInstanceAiThread(threadId: string, title: string): Promise<InstanceAiThreadInfo> {
|
||||
const response = await this.request.patch(`/rest/instance-ai/threads/${threadId}`, {
|
||||
data: { title },
|
||||
|
||||
@@ -631,6 +631,22 @@ export const instanceAiTestConfig = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Variant of `instanceAiTestConfig` for the cross-main smoke test. Unlike the
|
||||
* single-main pin above, it deliberately does NOT set `mains`/`workers`, so it
|
||||
* inherits the running project's topology: the `multi-main` project supplies
|
||||
* 2 mains + a worker, while every other project supplies 1 main (the spec then
|
||||
* skips via `mainUrls.length < 2`). It only exercises a conversational turn, so
|
||||
* it never touches the unsupported agent-triggered worker-offload execution path.
|
||||
*/
|
||||
export const instanceAiMultiMainConfig = {
|
||||
timezoneId: instanceAiTestConfig.timezoneId,
|
||||
capability: {
|
||||
services: instanceAiTestConfig.capability.services,
|
||||
env: instanceAiTestConfig.capability.env,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const test = base.extend<InstanceAiFixtures>({
|
||||
anthropicApiKey: async ({}, use) => {
|
||||
await use(ANTHROPIC_API_KEY);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
|
||||
import { test, expect, instanceAiMultiMainConfig } from './fixtures';
|
||||
import { N8N_AUTH_COOKIE } from '../../../config/constants';
|
||||
|
||||
// Inherits the project topology: 2 mains + worker on the `multi-main` project,
|
||||
// 1 main elsewhere (the test then skips). Conversational-only — never hits the
|
||||
// unsupported agent-triggered worker-offload execution path.
|
||||
test.use(instanceAiMultiMainConfig);
|
||||
|
||||
interface SseEvent {
|
||||
id?: number;
|
||||
name?: string;
|
||||
data: { type?: string; runId?: string; payload?: Record<string, unknown> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an Instance AI SSE stream directly from a specific main (bypassing the
|
||||
* load balancer), collecting frames until `until` matches or the timeout fires.
|
||||
* Uses raw `http` like the MCP cross-main tests, since Playwright's request
|
||||
* context buffers the whole response and never returns for a long-lived stream.
|
||||
*/
|
||||
async function collectSseUntil(opts: {
|
||||
baseUrl: string;
|
||||
threadId: string;
|
||||
cookieHeader: string;
|
||||
until: (event: SseEvent) => boolean;
|
||||
timeoutMs: number;
|
||||
/**
|
||||
* Called once the response headers arrive. At that point the server has
|
||||
* already registered the SSE subscription: the `/events` handler subscribes
|
||||
* to the event bus before it flushes headers, so receiving headers is a
|
||||
* race-free "subscribed" signal — no fixed delay needed.
|
||||
*/
|
||||
onSubscribed?: () => void;
|
||||
}): Promise<SseEvent[]> {
|
||||
const { baseUrl, threadId, cookieHeader, until, timeoutMs, onSubscribed } = opts;
|
||||
const url = new URL(`${baseUrl}/rest/instance-ai/events/${threadId}`);
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
const events: SseEvent[] = [];
|
||||
|
||||
return await new Promise<SseEvent[]>((resolve, reject) => {
|
||||
const req = transport.request(
|
||||
url,
|
||||
{ method: 'GET', headers: { Accept: 'text/event-stream', Cookie: cookieHeader } },
|
||||
(res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
reject(new Error(`SSE on ${baseUrl} returned HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
onSubscribed?.();
|
||||
const timer = setTimeout(() => {
|
||||
req.destroy();
|
||||
reject(
|
||||
new Error(
|
||||
`SSE timed out after ${timeoutMs}ms; received: ${events.map((e) => e.name).join(', ') || '(none)'}`,
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
|
||||
let buffer = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk: string) => {
|
||||
buffer += chunk;
|
||||
let sep: number;
|
||||
while ((sep = buffer.indexOf('\n\n')) >= 0) {
|
||||
const frame = buffer.slice(0, sep);
|
||||
buffer = buffer.slice(sep + 2);
|
||||
const event = parseSseFrame(frame);
|
||||
if (!event) continue;
|
||||
events.push(event);
|
||||
if (until(event)) {
|
||||
clearTimeout(timer);
|
||||
req.destroy();
|
||||
resolve(events);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
res.on('end', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(events);
|
||||
});
|
||||
res.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function parseSseFrame(frame: string): SseEvent | undefined {
|
||||
let id: number | undefined;
|
||||
let name: string | undefined;
|
||||
const dataLines: string[] = [];
|
||||
for (const line of frame.split('\n')) {
|
||||
if (line.startsWith(':')) continue; // keep-alive comment
|
||||
if (line.startsWith('id:')) id = Number(line.slice(3).trim());
|
||||
else if (line.startsWith('event:')) name = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
|
||||
}
|
||||
if (dataLines.length === 0) return undefined;
|
||||
try {
|
||||
const data = JSON.parse(dataLines.join('\n')) as SseEvent['data'];
|
||||
return { id, name: name ?? data.type, data };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Instance AI multi-main @capability:proxy',
|
||||
{ annotation: [{ type: 'owner', description: 'instanceAI' }] },
|
||||
() => {
|
||||
test('@mode:multi-main streams a run produced on one main to an SSE held by another', async ({
|
||||
api,
|
||||
mainUrls,
|
||||
createApiForMain,
|
||||
}) => {
|
||||
test.skip(mainUrls.length < 2, 'Requires at least 2 mains');
|
||||
|
||||
// Thread is created via the load balancer; lives in the shared DB.
|
||||
const thread = await api.createInstanceAiThread();
|
||||
|
||||
const mainA = await createApiForMain(0); // holds the SSE connection
|
||||
const mainB = await createApiForMain(1); // runs the agent
|
||||
|
||||
const { cookies } = await mainA.request.storageState();
|
||||
const authCookie = cookies.find((c) => c.name === N8N_AUTH_COOKIE);
|
||||
expect(authCookie, 'auth cookie present for main A').toBeTruthy();
|
||||
const cookieHeader = `${authCookie!.name}=${authCookie!.value}`;
|
||||
|
||||
// Subscribe to the SSE on main A and wait until the subscription is
|
||||
// registered (headers received) BEFORE producing on main B — otherwise
|
||||
// A's relay handler drops B's events (it gates on `hasSubscribers`) and
|
||||
// A never buffers them (it's not the producer). Deterministic, no sleep.
|
||||
let onSubscribed!: () => void;
|
||||
const subscribed = new Promise<void>((resolve) => {
|
||||
onSubscribed = resolve;
|
||||
});
|
||||
const ssePromise = collectSseUntil({
|
||||
baseUrl: mainUrls[0],
|
||||
threadId: thread.id,
|
||||
cookieHeader,
|
||||
until: (event) => event.name === 'run-finish',
|
||||
timeoutMs: 120_000,
|
||||
onSubscribed: () => onSubscribed(),
|
||||
});
|
||||
await subscribed;
|
||||
|
||||
const { runId } = await mainB.startInstanceAiChat(
|
||||
thread.id,
|
||||
'Reply with exactly the single word: pong. Do not use any tools.',
|
||||
);
|
||||
|
||||
const events = await ssePromise;
|
||||
const forRun = events.filter((event) => event.data?.runId === runId);
|
||||
expect(
|
||||
forRun.some((event) => event.name === 'run-start'),
|
||||
'main A received run-start for the run produced on main B',
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
forRun.some((event) => event.name === 'run-finish'),
|
||||
'main A received run-finish for the run produced on main B',
|
||||
).toBeTruthy();
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user