Merge remote-tracking branch 'origin/main' into codex/owner/ejclaw
This commit is contained in:
@@ -73,9 +73,10 @@ export function getTaskByIdFromDatabase(
|
||||
database: Database,
|
||||
id: string,
|
||||
): ScheduledTask | undefined {
|
||||
return database
|
||||
const row = database
|
||||
.prepare('SELECT * FROM scheduled_tasks WHERE id = ?')
|
||||
.get(id) as ScheduledTask | undefined;
|
||||
.get(id) as ScheduledTask | null | undefined;
|
||||
return row ?? undefined;
|
||||
}
|
||||
|
||||
export function findDuplicateCiWatcherInDatabase(
|
||||
|
||||
@@ -387,7 +387,7 @@ async function main(): Promise<void> {
|
||||
editFormattedTrackedChannelMessage(channels, jid, messageId, rawText),
|
||||
});
|
||||
startIpcWatcher({
|
||||
sendMessage: async (jid, text, senderRole, runId) => {
|
||||
sendMessage: async (jid, text, senderRole, runId, attachments) => {
|
||||
if (
|
||||
runId &&
|
||||
(senderRole === 'reviewer' || senderRole === 'arbiter') &&
|
||||
@@ -419,7 +419,7 @@ async function main(): Promise<void> {
|
||||
},
|
||||
'IPC relay routed message to channel',
|
||||
);
|
||||
await route.channel.sendMessage(jid, text);
|
||||
await route.channel.sendMessage(jid, text, { attachments });
|
||||
if (
|
||||
(senderRole === 'reviewer' || senderRole === 'arbiter') &&
|
||||
isTerminalStatusMessage(text)
|
||||
|
||||
@@ -556,6 +556,7 @@ describe('IPC message authorization', () => {
|
||||
'review text',
|
||||
'reviewer',
|
||||
'run-reviewer-ipc',
|
||||
undefined,
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -565,6 +566,39 @@ describe('IPC message authorization', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards structured attachments through authorized IPC messages', async () => {
|
||||
const sendMessage = vi.fn(async () => {});
|
||||
const attachments = [
|
||||
{
|
||||
path: '/tmp/ejclaw-room-mobile-list-390.png',
|
||||
name: 'room.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
];
|
||||
|
||||
const result = await forwardAuthorizedIpcMessage(
|
||||
{
|
||||
type: 'message',
|
||||
chatJid: 'other@g.us',
|
||||
text: '스크린샷입니다.',
|
||||
attachments,
|
||||
},
|
||||
'other-group',
|
||||
false,
|
||||
groups,
|
||||
sendMessage,
|
||||
);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
'other@g.us',
|
||||
'스크린샷입니다.',
|
||||
undefined,
|
||||
undefined,
|
||||
attachments,
|
||||
);
|
||||
expect(result.outcome).toBe('sent');
|
||||
});
|
||||
|
||||
it('injects authorized inbound messages for the source group without routing them as outbound sends', async () => {
|
||||
const sendMessage = vi.fn(async () => {});
|
||||
const injectInboundMessage = vi.fn(async () => {});
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function forwardAuthorizedIpcMessage(
|
||||
text: string,
|
||||
senderRole?: string,
|
||||
runId?: string,
|
||||
attachments?: import('./types.js').OutboundAttachment[],
|
||||
) => Promise<void>,
|
||||
injectInboundMessage?: (payload: {
|
||||
chatJid: string;
|
||||
@@ -79,7 +80,13 @@ export async function forwardAuthorizedIpcMessage(
|
||||
};
|
||||
}
|
||||
|
||||
await sendMessage(msg.chatJid, msg.text, msg.senderRole, msg.runId);
|
||||
await sendMessage(
|
||||
msg.chatJid,
|
||||
msg.text,
|
||||
msg.senderRole,
|
||||
msg.runId,
|
||||
msg.attachments,
|
||||
);
|
||||
return {
|
||||
outcome: 'sent',
|
||||
chatJid: msg.chatJid,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AssignRoomInput } from './db.js';
|
||||
import type {
|
||||
AgentType,
|
||||
MessageSourceKind,
|
||||
OutboundAttachment,
|
||||
PairedTask,
|
||||
RegisteredGroup,
|
||||
RoomMode,
|
||||
@@ -67,6 +68,7 @@ export interface IpcDeps {
|
||||
text: string,
|
||||
senderRole?: string,
|
||||
runId?: string,
|
||||
attachments?: OutboundAttachment[],
|
||||
) => Promise<void>;
|
||||
nudgeScheduler?: () => void;
|
||||
roomBindings: () => Record<string, RegisteredGroup>;
|
||||
@@ -100,6 +102,7 @@ export interface IpcMessagePayload {
|
||||
timestamp?: string;
|
||||
treatAsHuman?: boolean;
|
||||
sourceKind?: MessageSourceKind;
|
||||
attachments?: OutboundAttachment[];
|
||||
}
|
||||
|
||||
export interface IpcMessageForwardResult {
|
||||
|
||||
@@ -12,6 +12,7 @@ const ONE_PIXEL_PNG = Buffer.from(
|
||||
);
|
||||
|
||||
const cleanupDirs: string[] = [];
|
||||
const cleanupFiles: string[] = [];
|
||||
|
||||
function makeTempDir(baseDir: string, prefix: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(baseDir, prefix));
|
||||
@@ -33,6 +34,9 @@ afterEach(() => {
|
||||
for (const dir of cleanupDirs.splice(0)) {
|
||||
fs.rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
for (const file of cleanupFiles.splice(0)) {
|
||||
fs.rmSync(file, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('validateOutboundAttachments', () => {
|
||||
@@ -57,6 +61,31 @@ describe('validateOutboundAttachments', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('accepts direct EJClaw screenshot files under the temp directory', () => {
|
||||
const imagePath = path.join(
|
||||
os.tmpdir(),
|
||||
`ejclaw-room-mobile-list-${Date.now()}.png`,
|
||||
);
|
||||
fs.writeFileSync(imagePath, ONE_PIXEL_PNG);
|
||||
cleanupFiles.push(imagePath);
|
||||
|
||||
const result = validateOutboundAttachments([
|
||||
{
|
||||
path: imagePath,
|
||||
name: 'room-list.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.rejected).toEqual([]);
|
||||
expect(result.files).toEqual([
|
||||
{
|
||||
attachment: fs.realpathSync(imagePath),
|
||||
name: 'room-list.png',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires workspace paths to be explicitly allowlisted', () => {
|
||||
const dir = makeTempDir(process.cwd(), '.ejclaw-attachment-');
|
||||
const imagePath = writeFile(dir, 'workspace-shot.png', ONE_PIXEL_PNG);
|
||||
|
||||
@@ -21,6 +21,7 @@ const DEFAULT_TEMP_ATTACHMENT_DIR_PREFIXES = [
|
||||
'ejclaw-attachment-',
|
||||
'ejclaw-discord-image-',
|
||||
] as const;
|
||||
const DEFAULT_TEMP_ATTACHMENT_FILE_PREFIXES = ['ejclaw-'] as const;
|
||||
|
||||
function unique(values: Array<string | null | undefined>): string[] {
|
||||
return [
|
||||
@@ -83,6 +84,21 @@ function isWithinDefaultTempAttachmentDir(
|
||||
);
|
||||
}
|
||||
|
||||
function isDefaultTempAttachmentFile(
|
||||
realPath: string,
|
||||
tempDir: string | null,
|
||||
): boolean {
|
||||
if (!tempDir) return false;
|
||||
const relative = path.relative(tempDir, realPath);
|
||||
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
return false;
|
||||
}
|
||||
if (relative.includes(path.sep)) return false;
|
||||
return DEFAULT_TEMP_ATTACHMENT_FILE_PREFIXES.some((prefix) =>
|
||||
relative.startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
function detectImageMime(filePath: string): string | null {
|
||||
const handle = fs.openSync(filePath, 'r');
|
||||
try {
|
||||
@@ -171,7 +187,8 @@ export function validateOutboundAttachments(
|
||||
}
|
||||
if (
|
||||
!matchesAllowedBaseDir(realPath, baseDirs) &&
|
||||
!isWithinDefaultTempAttachmentDir(realPath, defaultTempDir)
|
||||
!isWithinDefaultTempAttachmentDir(realPath, defaultTempDir) &&
|
||||
!isDefaultTempAttachmentFile(realPath, defaultTempDir)
|
||||
) {
|
||||
rejected.push({ path: requestedPath, reason: 'outside-allowed-dirs' });
|
||||
continue;
|
||||
|
||||
@@ -356,6 +356,99 @@ describe('web dashboard data', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses Discord live progress messages without exposing them as recent chat', () => {
|
||||
const task = makePairedTask({
|
||||
id: 'paired-room-progress',
|
||||
chat_jid: 'dc:ops',
|
||||
status: 'active',
|
||||
updated_at: '2026-04-26T06:10:00.000Z',
|
||||
});
|
||||
const turn: PairedTurnRecord = {
|
||||
turn_id: 'turn-progress',
|
||||
task_id: task.id,
|
||||
task_updated_at: task.updated_at,
|
||||
role: 'owner',
|
||||
intent_kind: 'owner-turn',
|
||||
state: 'running',
|
||||
executor_service_id: 'codex-main',
|
||||
executor_agent_type: 'codex',
|
||||
attempt_no: 1,
|
||||
created_at: '2026-04-26T06:00:00.000Z',
|
||||
updated_at: '2026-04-26T06:10:00.000Z',
|
||||
completed_at: null,
|
||||
last_error: null,
|
||||
};
|
||||
const statusPrefix = '';
|
||||
const messages: NewMessage[] = [
|
||||
{
|
||||
id: 'msg-latest',
|
||||
chat_jid: 'dc:ops',
|
||||
sender: 'user-1',
|
||||
sender_name: '눈쟁이',
|
||||
content: 'recent human message',
|
||||
timestamp: '2026-04-26T06:09:00.000Z',
|
||||
is_from_me: false,
|
||||
is_bot_message: false,
|
||||
message_source_kind: 'human',
|
||||
},
|
||||
];
|
||||
const progressMessages: NewMessage[] = [
|
||||
{
|
||||
id: 'msg-stale-progress',
|
||||
chat_jid: 'dc:ops',
|
||||
sender: 'bot-1',
|
||||
sender_name: '오너',
|
||||
content: `${statusPrefix}stale progress`,
|
||||
timestamp: '2026-04-26T05:59:00.000Z',
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
message_source_kind: 'bot',
|
||||
},
|
||||
{
|
||||
id: 'msg-live-progress',
|
||||
chat_jid: 'dc:ops',
|
||||
sender: 'bot-1',
|
||||
sender_name: '오너',
|
||||
content: `${statusPrefix}building mobile parity\n\n12s`,
|
||||
timestamp: '2026-04-26T06:08:00.000Z',
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
message_source_kind: 'bot',
|
||||
},
|
||||
];
|
||||
|
||||
const activity = buildWebDashboardRoomActivity({
|
||||
serviceId: 'codex-main',
|
||||
entry: {
|
||||
jid: 'dc:ops',
|
||||
name: '#ops',
|
||||
folder: 'ops',
|
||||
agentType: 'codex',
|
||||
status: 'processing',
|
||||
elapsedMs: 20_000,
|
||||
pendingMessages: false,
|
||||
pendingTasks: 0,
|
||||
},
|
||||
pairedTask: task,
|
||||
turns: [turn],
|
||||
attempts: [],
|
||||
outputs: [],
|
||||
messages,
|
||||
progressMessages,
|
||||
});
|
||||
|
||||
expect(activity.pairedTask?.currentTurn).toMatchObject({
|
||||
progressText: 'building mobile parity',
|
||||
progressUpdatedAt: '2026-04-26T06:08:00.000Z',
|
||||
});
|
||||
expect(activity.messages).toEqual([
|
||||
expect.objectContaining({
|
||||
senderName: '눈쟁이',
|
||||
content: 'recent human message',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds typed inbox items from pending rooms, paired tasks, and CI failures', () => {
|
||||
const snapshots: StatusSnapshot[] = [
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeEjclawStructuredOutput } from './agent-protocol.js';
|
||||
import { isWatchCiTask } from './task-watch-status.js';
|
||||
import type { PairedTurnAttemptRecord, PairedTurnRecord } from './db.js';
|
||||
import type { StatusSnapshot, UsageRowSnapshot } from './status-dashboard.js';
|
||||
@@ -182,6 +183,37 @@ function buildRoomPreview(value: string, maxLength: number): string {
|
||||
return truncateText(redactSensitiveText(value).trim(), maxLength);
|
||||
}
|
||||
|
||||
function decodeCommonHtmlEntities(value: string): string {
|
||||
return value
|
||||
.replace(/"|"|"/gi, '"')
|
||||
.replace(/'|'|'/gi, "'")
|
||||
.replace(/<|<|</gi, '<')
|
||||
.replace(/>|>|>/gi, '>')
|
||||
.replace(/&|&|&/gi, '&');
|
||||
}
|
||||
|
||||
function hasStructuredOutputHint(value: string): boolean {
|
||||
return value.includes('"ejclaw"') || /"ejclaw"/i.test(value);
|
||||
}
|
||||
|
||||
function normalizeRoomMessageContent(value: string): string | null {
|
||||
if (!hasStructuredOutputHint(value)) return value;
|
||||
|
||||
const candidates = [value, decodeCommonHtmlEntities(value)];
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizeEjclawStructuredOutput(candidate);
|
||||
if (normalized.output?.visibility === 'silent') return null;
|
||||
if (
|
||||
normalized.output?.visibility === 'public' &&
|
||||
normalized.output.text !== candidate
|
||||
) {
|
||||
return normalized.output.text;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function stableHash(value: string): string {
|
||||
let hash = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
@@ -405,15 +437,17 @@ export function sanitizeScheduledTask(
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage {
|
||||
function sanitizeRoomMessage(
|
||||
message: NewMessage,
|
||||
): WebDashboardRoomMessage | null {
|
||||
const content = normalizeRoomMessageContent(message.content ?? '');
|
||||
if (content === null) return null;
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
sender: message.sender,
|
||||
senderName: message.sender_name || message.sender,
|
||||
content: buildRoomPreview(
|
||||
message.content ?? '',
|
||||
ROOM_MESSAGE_PREVIEW_MAX_LENGTH,
|
||||
),
|
||||
content: buildRoomPreview(content, ROOM_MESSAGE_PREVIEW_MAX_LENGTH),
|
||||
timestamp: message.timestamp,
|
||||
isFromMe: !!message.is_from_me,
|
||||
isBotMessage: !!message.is_bot_message,
|
||||
@@ -431,6 +465,10 @@ function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage {
|
||||
*/
|
||||
const TASK_STATUS_PREFIX = '';
|
||||
|
||||
function isTaskStatusMessage(message: NewMessage): boolean {
|
||||
return (message.content ?? '').startsWith(TASK_STATUS_PREFIX);
|
||||
}
|
||||
|
||||
function turnRoleFromSenderName(name: string | null | undefined): string {
|
||||
const v = (name ?? '').toLowerCase();
|
||||
if (v.includes('오너') || v.includes('owner')) return 'owner';
|
||||
@@ -533,6 +571,7 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
attempts: PairedTurnAttemptRecord[];
|
||||
outputs: PairedTurnOutput[];
|
||||
messages: NewMessage[];
|
||||
progressMessages?: NewMessage[];
|
||||
outputLimit?: number;
|
||||
}): WebDashboardRoomActivity {
|
||||
const latestAttemptByTurnId = new Map<string, PairedTurnAttemptRecord>();
|
||||
@@ -561,7 +600,12 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
elapsedMs: args.entry.elapsedMs,
|
||||
pendingMessages: args.entry.pendingMessages,
|
||||
pendingTasks: args.entry.pendingTasks,
|
||||
messages: args.messages.map(sanitizeRoomMessage),
|
||||
messages: args.messages
|
||||
.filter((message) => !isTaskStatusMessage(message))
|
||||
.map(sanitizeRoomMessage)
|
||||
.filter((message): message is WebDashboardRoomMessage =>
|
||||
Boolean(message),
|
||||
),
|
||||
pairedTask: args.pairedTask
|
||||
? {
|
||||
id: args.pairedTask.id,
|
||||
@@ -570,7 +614,11 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
roundTripCount: args.pairedTask.round_trip_count,
|
||||
updatedAt: args.pairedTask.updated_at,
|
||||
currentTurn: currentTurn
|
||||
? sanitizeRoomTurn(currentTurn, currentAttempt, args.messages)
|
||||
? sanitizeRoomTurn(
|
||||
currentTurn,
|
||||
currentAttempt,
|
||||
args.progressMessages ?? args.messages,
|
||||
)
|
||||
: null,
|
||||
outputs: args.outputs.slice(-outputLimit).map(sanitizeRoomTurnOutput),
|
||||
}
|
||||
|
||||
77
src/web-dashboard-room-envelope.test.ts
Normal file
77
src/web-dashboard-room-envelope.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { NewMessage } from './types.js';
|
||||
import { buildWebDashboardRoomActivity } from './web-dashboard-data.js';
|
||||
|
||||
describe('web dashboard room envelope rendering', () => {
|
||||
it('unwraps EJClaw structured envelopes in room messages', () => {
|
||||
const envelope = JSON.stringify({
|
||||
ejclaw: {
|
||||
visibility: 'public',
|
||||
text: 'PR #52 모바일/룸 parity 검증 스크린샷입니다.',
|
||||
verdict: 'done',
|
||||
attachments: [
|
||||
{
|
||||
path: '/tmp/ejclaw-room-mobile-list-390.png',
|
||||
name: 'room.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const htmlEscapedEnvelope = envelope.replace(/"/g, '"');
|
||||
const messages: NewMessage[] = [
|
||||
makeMessage('msg-json', envelope, 'owner'),
|
||||
makeMessage('msg-html-json', htmlEscapedEnvelope, 'reviewer'),
|
||||
makeMessage(
|
||||
'msg-silent',
|
||||
'{"ejclaw":{"visibility":"silent","verdict":"silent"}}',
|
||||
'arbiter',
|
||||
),
|
||||
];
|
||||
|
||||
const activity = buildWebDashboardRoomActivity({
|
||||
serviceId: 'codex-main',
|
||||
entry: {
|
||||
jid: 'dc:ops',
|
||||
name: '#ops',
|
||||
folder: 'ops',
|
||||
agentType: 'codex',
|
||||
status: 'processing',
|
||||
elapsedMs: 15_000,
|
||||
pendingMessages: true,
|
||||
pendingTasks: 1,
|
||||
},
|
||||
pairedTask: null,
|
||||
turns: [],
|
||||
attempts: [],
|
||||
outputs: [],
|
||||
messages,
|
||||
});
|
||||
|
||||
expect(activity.messages).toHaveLength(2);
|
||||
expect(activity.messages.map((message) => message.content)).toEqual([
|
||||
'PR #52 모바일/룸 parity 검증 스크린샷입니다.',
|
||||
'PR #52 모바일/룸 parity 검증 스크린샷입니다.',
|
||||
]);
|
||||
expect(JSON.stringify(activity.messages)).not.toContain('"ejclaw"');
|
||||
});
|
||||
});
|
||||
|
||||
function makeMessage(
|
||||
id: string,
|
||||
content: string,
|
||||
senderName: string,
|
||||
): NewMessage {
|
||||
return {
|
||||
id,
|
||||
chat_jid: 'dc:ops',
|
||||
sender: senderName,
|
||||
sender_name: senderName,
|
||||
content,
|
||||
timestamp: '2026-04-26T05:29:00.000Z',
|
||||
is_from_me: false,
|
||||
is_bot_message: true,
|
||||
message_source_kind: 'bot',
|
||||
};
|
||||
}
|
||||
@@ -1506,6 +1506,22 @@ describe('web dashboard server handler', () => {
|
||||
message_source_kind: 'human',
|
||||
},
|
||||
];
|
||||
const statusPrefix = '';
|
||||
const progressMessages: NewMessage[] = [
|
||||
...messages,
|
||||
{
|
||||
id: 'msg-progress',
|
||||
chat_jid: 'dc:ops',
|
||||
sender: 'reviewer-bot',
|
||||
sender_name: '리뷰어',
|
||||
content: `${statusPrefix}checking current output\n\n9s`,
|
||||
timestamp: '2026-04-26T05:20:00.000Z',
|
||||
is_from_me: true,
|
||||
is_bot_message: true,
|
||||
message_source_kind: 'bot',
|
||||
},
|
||||
];
|
||||
const requestedMessageLimits: number[] = [];
|
||||
const handler = createWebDashboardHandler({
|
||||
readStatusSnapshots: () => [
|
||||
{
|
||||
@@ -1535,7 +1551,10 @@ describe('web dashboard server handler', () => {
|
||||
getPairedTurnAttempts: (turnId) =>
|
||||
turnId === turns[0]!.turn_id ? attempts : [],
|
||||
getPairedTurnOutputs: () => outputs,
|
||||
getRecentChatMessages: () => messages,
|
||||
getRecentChatMessages: (_jid, limit) => {
|
||||
requestedMessageLimits.push(limit ?? 20);
|
||||
return limit && limit > 8 ? progressMessages : messages;
|
||||
},
|
||||
});
|
||||
|
||||
const response = await handler(
|
||||
@@ -1555,6 +1574,8 @@ describe('web dashboard server handler', () => {
|
||||
state: string;
|
||||
attemptNo: number;
|
||||
lastError: string;
|
||||
progressText: string;
|
||||
progressUpdatedAt: string;
|
||||
};
|
||||
outputs: Array<{ outputText: string; turnNumber: number }>;
|
||||
};
|
||||
@@ -1568,12 +1589,16 @@ describe('web dashboard server handler', () => {
|
||||
state: 'running',
|
||||
attemptNo: 2,
|
||||
lastError: 'OPENAI_API_KEY=<redacted>',
|
||||
progressText: 'checking current output',
|
||||
progressUpdatedAt: '2026-04-26T05:20:00.000Z',
|
||||
});
|
||||
expect(body.pairedTask.outputs).toMatchObject([
|
||||
{ turnNumber: 1, outputText: 'owner final output' },
|
||||
]);
|
||||
expect(requestedMessageLimits).toEqual([8, 40]);
|
||||
expect(body.messages[0]?.content).toContain('BOT_TOKEN=<redacted>');
|
||||
expect(body.messages[0]?.senderName).toBe('눈쟁이');
|
||||
expect(body.messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns 404 for missing room timelines', async () => {
|
||||
|
||||
@@ -781,6 +781,9 @@ export function createWebDashboardHandler(
|
||||
for (const [jid, { snapshot, entry }] of uniqueByJid) {
|
||||
const pairedTask = loadLatestPairedTaskForChat(jid) ?? null;
|
||||
const messages = loadRecentChatMessages(jid, 8);
|
||||
const progressMessages = pairedTask
|
||||
? loadRecentChatMessages(jid, 40)
|
||||
: messages;
|
||||
const outputs = loadRecentPairedTurnOutputsForChat(jid, 8);
|
||||
if (!pairedTask && messages.length === 0 && outputs.length === 0)
|
||||
continue;
|
||||
@@ -796,6 +799,7 @@ export function createWebDashboardHandler(
|
||||
attempts: [],
|
||||
outputs,
|
||||
messages,
|
||||
progressMessages,
|
||||
outputLimit: 8,
|
||||
});
|
||||
}
|
||||
@@ -1171,6 +1175,7 @@ export function createWebDashboardHandler(
|
||||
attempts,
|
||||
outputs: pairedTask ? loadPairedTurnOutputs(pairedTask.id) : [],
|
||||
messages: loadRecentChatMessages(timelineRoomJid, 8),
|
||||
progressMessages: loadRecentChatMessages(timelineRoomJid, 40),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user