Extract dashboard inbox action route

This commit is contained in:
ejclaw
2026-04-28 19:34:43 +09:00
parent 1bb57f5c1d
commit 23056f7cc7
3 changed files with 655 additions and 303 deletions

View File

@@ -0,0 +1,253 @@
import { describe, expect, it } from 'vitest';
import type { NewMessage, PairedTask } from './types.js';
import {
createInboxDismissTracker,
handleInboxActionRoute,
type InboxActionRouteDependencies,
} from './web-dashboard-inbox-routes.js';
function makePairedTask(overrides: Partial<PairedTask>): PairedTask {
return {
id: 'paired-1',
chat_jid: 'dc:general',
group_folder: 'general',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude-reviewer',
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
arbiter_agent_type: 'codex',
title: 'Dashboard PR',
source_ref: null,
plan_notes: null,
review_requested_at: null,
round_trip_count: 0,
owner_failure_count: 0,
owner_step_done_streak: 0,
finalize_step_done_count: 0,
task_done_then_user_reopen_count: 0,
empty_step_done_streak: 0,
status: 'review_ready',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-04-26T04:00:00.000Z',
updated_at: '2026-04-26T04:30:00.000Z',
...overrides,
};
}
function jsonResponse(value: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(value), {
...init,
headers: { 'content-type': 'application/json', ...init?.headers },
});
}
function inboxRequest(
inboxId: string,
body: unknown,
method = 'POST',
): Request {
return new Request(
`http://localhost/api/inbox/${encodeURIComponent(inboxId)}/actions`,
{
method,
headers: { 'content-type': 'application/json' },
body: method === 'POST' ? JSON.stringify(body) : undefined,
},
);
}
function makeDeps(
task: PairedTask | undefined,
overrides: Partial<InboxActionRouteDependencies> = {},
): InboxActionRouteDependencies {
return {
dismissTracker: createInboxDismissTracker(),
enqueueMessageCheck: () => undefined,
loadPairedTaskById: (id) => (id === task?.id ? task : undefined),
messageExists: () => false,
mutatePairedTaskIfUnchanged: () => true,
schedulePairedFollowUp: () => true,
writeChatMetadata: () => undefined,
writeMessage: () => undefined,
...overrides,
};
}
describe('web dashboard inbox action routes', () => {
it('dismisses inbox items and queues paired follow-up intents', async () => {
const dismissTracker = createInboxDismissTracker();
const task = makePairedTask({ id: 'merge-1', status: 'merge_ready' });
const scheduled: Array<{ taskId: string; intentKind: string }> = [];
const queued: Array<{ chatJid: string; groupFolder: string }> = [];
const deps = makeDeps(task, {
dismissTracker,
enqueueMessageCheck: (chatJid, groupFolder) => {
queued.push({ chatJid, groupFolder });
},
schedulePairedFollowUp: (args) => {
scheduled.push({
taskId: args.task.id,
intentKind: args.intentKind,
});
args.enqueue();
return true;
},
});
const fallThrough = await handleInboxActionRoute({
...deps,
jsonResponse,
request: new Request('http://localhost/api/overview'),
url: new URL('http://localhost/api/overview'),
});
expect(fallThrough).toBeNull();
const dismiss = await handleInboxActionRoute({
...deps,
jsonResponse,
request: inboxRequest('ci:task-1', {
action: 'dismiss',
lastOccurredAt: '2026-04-26T05:00:00.000Z',
}),
url: new URL('http://localhost/api/inbox/ci%3Atask-1/actions'),
});
expect(dismiss?.status).toBe(200);
expect(
dismissTracker.isDismissed({
id: 'ci:task-1',
lastOccurredAt: '2026-04-26T05:00:00.000Z',
}),
).toBe(true);
const run = await handleInboxActionRoute({
...deps,
jsonResponse,
request: inboxRequest('paired:merge-1:merge_ready', { action: 'run' }),
url: new URL(
'http://localhost/api/inbox/paired%3Amerge-1%3Amerge_ready/actions',
),
});
expect(run?.status).toBe(200);
await expect(run?.json()).resolves.toMatchObject({
intentKind: 'finalize-owner-turn',
ok: true,
queued: true,
taskId: 'merge-1',
});
expect(scheduled).toEqual([
{ taskId: 'merge-1', intentKind: 'finalize-owner-turn' },
]);
expect(queued).toEqual([{ chatJid: 'dc:general', groupFolder: 'general' }]);
});
it('declines paired tasks and rejects invalid inbox actions', async () => {
const task = makePairedTask({
id: 'arbiter-1',
status: 'arbiter_requested',
arbiter_requested_at: '2026-04-26T05:00:00.000Z',
});
const messages: NewMessage[] = [];
const updates: unknown[] = [];
const deps = makeDeps(task, {
enqueueMessageCheck: () => undefined,
messageExists: (chatJid, id) =>
messages.some(
(message) => message.chat_jid === chatJid && message.id === id,
),
mutatePairedTaskIfUnchanged: (_id, _expectedUpdatedAt, update) => {
updates.push(update);
return true;
},
writeMessage: (message) => {
messages.push(message);
},
});
const decline = () =>
handleInboxActionRoute({
...deps,
jsonResponse,
now: () => '2026-04-26T05:15:00.000Z',
request: inboxRequest('paired:arbiter-1:arbiter_requested', {
action: 'decline',
requestId: 'decline 1',
}),
url: new URL(
'http://localhost/api/inbox/paired%3Aarbiter-1%3Aarbiter_requested/actions',
),
});
const response = await decline();
expect(response?.status).toBe(200);
await expect(response?.json()).resolves.toMatchObject({
ok: true,
queued: true,
status: 'active',
taskId: 'arbiter-1',
});
expect(updates).toHaveLength(1);
expect(updates[0]).toMatchObject({
arbiter_requested_at: null,
status: 'active',
updated_at: '2026-04-26T05:15:00.000Z',
});
expect(messages[0]).toMatchObject({
id: 'web-inbox-decline-1',
content:
'Dashboard declined arbiter escalation. Continue with the owner turn.',
message_source_kind: 'ipc_injected_human',
});
const duplicate = await decline();
expect(duplicate?.status).toBe(200);
await expect(duplicate?.json()).resolves.toMatchObject({
duplicate: true,
queued: false,
});
expect(messages).toHaveLength(1);
const invalidBody = await handleInboxActionRoute({
...deps,
jsonResponse,
request: inboxRequest('paired:arbiter-1:active', { action: 'approve' }),
url: new URL(
'http://localhost/api/inbox/paired%3Aarbiter-1%3Aactive/actions',
),
});
expect(invalidBody?.status).toBe(400);
const stale = await handleInboxActionRoute({
...deps,
jsonResponse,
request: inboxRequest('paired:arbiter-1:merge_ready', { action: 'run' }),
url: new URL(
'http://localhost/api/inbox/paired%3Aarbiter-1%3Amerge_ready/actions',
),
});
expect(stale?.status).toBe(409);
const notWired = await handleInboxActionRoute({
...deps,
enqueueMessageCheck: undefined,
jsonResponse,
request: inboxRequest('paired:arbiter-1:active', { action: 'run' }),
url: new URL(
'http://localhost/api/inbox/paired%3Aarbiter-1%3Aactive/actions',
),
});
expect(notWired?.status).toBe(503);
const wrongMethod = await handleInboxActionRoute({
...deps,
jsonResponse,
request: inboxRequest('paired:arbiter-1:active', null, 'GET'),
url: new URL(
'http://localhost/api/inbox/paired%3Aarbiter-1%3Aactive/actions',
),
});
expect(wrongMethod?.status).toBe(405);
});
});

View File

@@ -0,0 +1,379 @@
import type { ScheduledPairedFollowUpIntentKind } from './paired-follow-up-scheduler.js';
import type { NewMessage, PairedTask } from './types.js';
type JsonResponse = (
value: unknown,
init?: ResponseInit,
request?: Request,
) => Response;
type InboxAction = 'run' | 'decline' | 'dismiss';
interface InboxActionRequest {
action: InboxAction;
requestId: string | null;
lastOccurredAt: string | null;
}
type InboxFollowUpTask = Pick<
PairedTask,
'id' | 'status' | 'round_trip_count' | 'updated_at'
>;
export type InboxFollowUpScheduler = (args: {
chatJid: string;
runId: string;
task: InboxFollowUpTask;
intentKind: ScheduledPairedFollowUpIntentKind;
enqueue: () => void;
}) => boolean;
export interface InboxDismissTracker {
dismiss: (inboxId: string, lastOccurredAt: string | null) => void;
isDismissed: (item: { id: string; lastOccurredAt: string }) => boolean;
}
export interface InboxActionRouteDependencies {
dismissTracker: InboxDismissTracker;
enqueueMessageCheck?: (chatJid: string, groupFolder: string) => void;
loadPairedTaskById: (id: string) => PairedTask | undefined;
messageExists: (chatJid: string, id: string) => boolean;
mutatePairedTaskIfUnchanged: (
id: string,
expectedUpdatedAt: string,
updates: Partial<
Pick<
PairedTask,
'status' | 'updated_at' | 'arbiter_requested_at' | 'completion_reason'
>
>,
) => boolean;
schedulePairedFollowUp: InboxFollowUpScheduler;
writeChatMetadata: (
chatJid: string,
timestamp: string,
name?: string,
channel?: string,
isGroup?: boolean,
) => void;
writeMessage: (message: NewMessage) => void;
}
interface InboxActionRouteContext extends InboxActionRouteDependencies {
jsonResponse: JsonResponse;
now?: () => string;
request: Request;
url: URL;
}
export function createInboxDismissTracker(): InboxDismissTracker {
const dismissedInboxKeys = new Set<string>();
return {
dismiss: (inboxId, lastOccurredAt) => {
dismissedInboxKeys.add(makeInboxDismissKey(inboxId, lastOccurredAt));
},
isDismissed: (item) =>
dismissedInboxKeys.has(item.id) ||
dismissedInboxKeys.has(makeInboxDismissKey(item.id, item.lastOccurredAt)),
};
}
function parseInboxActionPath(pathname: string): string | null {
const match = pathname.match(/^\/api\/inbox\/([^/]+)\/actions$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
function parsePairedInboxTarget(
inboxId: string,
): { taskId: string; status: PairedTask['status'] } | null {
if (!inboxId.startsWith('paired:')) return null;
const rest = inboxId.slice('paired:'.length);
const separatorIndex = rest.lastIndexOf(':');
if (separatorIndex <= 0 || separatorIndex === rest.length - 1) return null;
const status = rest.slice(separatorIndex + 1);
if (!isPairedTaskStatus(status)) return null;
return {
taskId: rest.slice(0, separatorIndex),
status,
};
}
function isInboxAction(value: unknown): value is InboxAction {
return value === 'run' || value === 'decline' || value === 'dismiss';
}
function isPairedTaskStatus(value: unknown): value is PairedTask['status'] {
return (
value === 'active' ||
value === 'review_ready' ||
value === 'in_review' ||
value === 'merge_ready' ||
value === 'completed' ||
value === 'arbiter_requested' ||
value === 'in_arbitration'
);
}
async function readInboxAction(
request: Request,
): Promise<InboxActionRequest | null> {
try {
const body = (await request.json()) as {
action?: unknown;
requestId?: unknown;
lastOccurredAt?: unknown;
};
if (!isInboxAction(body.action)) return null;
return {
action: body.action,
requestId: sanitizeInboxActionRequestId(body.requestId),
lastOccurredAt:
typeof body.lastOccurredAt === 'string' && body.lastOccurredAt.trim()
? body.lastOccurredAt.trim()
: null,
};
} catch {
return null;
}
}
function makeWebRunId(prefix: string): string {
return `web-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function sanitizeInboxActionRequestId(value: unknown): string | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
const safe = trimmed.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 120);
return safe || null;
}
function makeInboxDismissKey(
inboxId: string,
lastOccurredAt: string | null,
): string {
return lastOccurredAt ? `${inboxId}\0${lastOccurredAt}` : inboxId;
}
function makeWebInboxMessageId(requestId: string | null): string {
return requestId
? `web-inbox-${requestId}`
: `web-inbox-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function declinedInboxMessage(status: PairedTask['status']): string {
if (status === 'review_ready' || status === 'in_review') {
return 'Dashboard declined review. Continue with the owner turn.';
}
if (status === 'merge_ready') {
return 'Dashboard declined finalization. Continue with the owner turn.';
}
if (status === 'arbiter_requested' || status === 'in_arbitration') {
return 'Dashboard declined arbiter escalation. Continue with the owner turn.';
}
return 'Dashboard declined this inbox item. Continue with the owner turn.';
}
function pairedFollowUpIntentForStatus(
status: PairedTask['status'],
): ScheduledPairedFollowUpIntentKind | null {
if (status === 'review_ready' || status === 'in_review') {
return 'reviewer-turn';
}
if (status === 'merge_ready') {
return 'finalize-owner-turn';
}
if (status === 'arbiter_requested' || status === 'in_arbitration') {
return 'arbiter-turn';
}
return null;
}
export async function handleInboxActionRoute({
dismissTracker,
enqueueMessageCheck,
jsonResponse,
loadPairedTaskById,
messageExists,
mutatePairedTaskIfUnchanged,
now,
request,
schedulePairedFollowUp,
url,
writeChatMetadata,
writeMessage,
}: InboxActionRouteContext): Promise<Response | null> {
const actionInboxId = parseInboxActionPath(url.pathname);
if (!actionInboxId) return null;
if (request.method !== 'POST') {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
const inboxRequest = await readInboxAction(request);
if (!inboxRequest) {
return jsonResponse({ error: 'Invalid inbox action' }, { status: 400 });
}
if (inboxRequest.action === 'dismiss') {
dismissTracker.dismiss(actionInboxId, inboxRequest.lastOccurredAt);
return jsonResponse({ ok: true, id: actionInboxId, dismissed: true });
}
const pairedTarget = parsePairedInboxTarget(actionInboxId);
if (!pairedTarget) {
return jsonResponse(
{ error: 'Unsupported inbox action target' },
{ status: 400 },
);
}
if (!enqueueMessageCheck) {
return jsonResponse(
{ error: 'Paired follow-up queue is not configured' },
{ status: 503 },
);
}
const task = loadPairedTaskById(pairedTarget.taskId);
if (!task) {
return jsonResponse({ error: 'Paired task not found' }, { status: 404 });
}
if (inboxRequest.action === 'decline') {
const messageId = makeWebInboxMessageId(inboxRequest.requestId);
if (inboxRequest.requestId && messageExists(task.chat_jid, messageId)) {
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
queued: false,
duplicate: true,
});
}
}
if (task.status !== pairedTarget.status) {
return jsonResponse(
{
error: 'Inbox item is stale',
status: task.status,
},
{ status: 409 },
);
}
if (inboxRequest.action === 'decline') {
return handleDeclineInboxAction({
actionInboxId,
enqueueMessageCheck,
jsonResponse,
messageId: makeWebInboxMessageId(inboxRequest.requestId),
mutatePairedTaskIfUnchanged,
now,
task,
writeChatMetadata,
writeMessage,
});
}
const intentKind = pairedFollowUpIntentForStatus(task.status);
if (!intentKind) {
return jsonResponse(
{ error: 'Paired task is not actionable' },
{ status: 409 },
);
}
const queued = schedulePairedFollowUp({
chatJid: task.chat_jid,
runId: makeWebRunId('inbox'),
task,
intentKind,
enqueue: () => enqueueMessageCheck(task.chat_jid, task.group_folder),
});
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
intentKind,
queued,
});
}
function handleDeclineInboxAction({
actionInboxId,
enqueueMessageCheck,
jsonResponse,
messageId,
mutatePairedTaskIfUnchanged,
now,
task,
writeChatMetadata,
writeMessage,
}: {
actionInboxId: string;
enqueueMessageCheck: (chatJid: string, groupFolder: string) => void;
jsonResponse: JsonResponse;
messageId: string;
mutatePairedTaskIfUnchanged: InboxActionRouteDependencies['mutatePairedTaskIfUnchanged'];
now?: () => string;
task: PairedTask;
writeChatMetadata: InboxActionRouteDependencies['writeChatMetadata'];
writeMessage: InboxActionRouteDependencies['writeMessage'];
}): Response {
const timestamp = now?.() ?? new Date().toISOString();
const updates: Partial<
Pick<
PairedTask,
'status' | 'updated_at' | 'arbiter_requested_at' | 'completion_reason'
>
> = {
status: 'active',
updated_at: timestamp,
};
if (task.status === 'arbiter_requested' || task.status === 'in_arbitration') {
updates.arbiter_requested_at = null;
}
const updated = mutatePairedTaskIfUnchanged(
task.id,
task.updated_at,
updates,
);
if (!updated) {
return jsonResponse(
{ error: 'Inbox item is stale', status: task.status },
{ status: 409 },
);
}
writeChatMetadata(task.chat_jid, timestamp, undefined, 'web-dashboard', true);
writeMessage({
id: messageId,
chat_jid: task.chat_jid,
sender: 'web-dashboard',
sender_name: 'Web Dashboard',
content: declinedInboxMessage(task.status),
timestamp,
is_from_me: false,
is_bot_message: false,
message_source_kind: 'ipc_injected_human',
});
enqueueMessageCheck(task.chat_jid, task.group_folder);
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
status: 'active',
queued: true,
});
}

View File

@@ -26,7 +26,6 @@ import {
} from './db.js';
import { logger } from './logger.js';
import { schedulePairedFollowUpIntent } from './message-runtime-follow-up.js';
import { type ScheduledPairedFollowUpIntentKind } from './paired-follow-up-scheduler.js';
import {
readStatusSnapshots,
type StatusSnapshot,
@@ -38,6 +37,11 @@ import type {
RegisteredGroup,
ScheduledTask,
} from './types.js';
import {
createInboxDismissTracker,
handleInboxActionRoute,
type InboxFollowUpScheduler,
} from './web-dashboard-inbox-routes.js';
import { handleOverviewRoute } from './web-dashboard-overview-routes.js';
import {
createRoomMessageIdCache,
@@ -63,19 +67,6 @@ const MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE =
const UNIT_NOT_FOUND_PATTERN =
/(Unit .* not found|Could not find the requested service|not-found)/i;
type PairedFollowUpTask = Pick<
PairedTask,
'id' | 'status' | 'round_trip_count' | 'updated_at'
>;
type WebPairedFollowUpScheduler = (args: {
chatJid: string;
runId: string;
task: PairedFollowUpTask;
intentKind: ScheduledPairedFollowUpIntentKind;
enqueue: () => void;
}) => boolean;
export type { ServiceRestartRecord } from './web-dashboard-service-routes.js';
export interface WebDashboardHandlerOptions {
@@ -132,7 +123,7 @@ export interface WebDashboardHandlerOptions {
>
>,
) => boolean;
schedulePairedFollowUp?: WebPairedFollowUpScheduler;
schedulePairedFollowUp?: InboxFollowUpScheduler;
getRoomBindings?: () => Record<string, RegisteredGroup>;
storeChatMetadata?: (
chatJid: string,
@@ -244,131 +235,6 @@ function serveStaticFile(staticDir: string, pathname: string): Response {
});
}
type InboxAction = 'run' | 'decline' | 'dismiss';
interface InboxActionRequest {
action: InboxAction;
requestId: string | null;
lastOccurredAt: string | null;
}
function parseInboxActionPath(pathname: string): string | null {
const match = pathname.match(/^\/api\/inbox\/([^/]+)\/actions$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
function parsePairedInboxTarget(
inboxId: string,
): { taskId: string; status: PairedTask['status'] } | null {
if (!inboxId.startsWith('paired:')) return null;
const rest = inboxId.slice('paired:'.length);
const separatorIndex = rest.lastIndexOf(':');
if (separatorIndex <= 0 || separatorIndex === rest.length - 1) return null;
const status = rest.slice(separatorIndex + 1);
if (!isPairedTaskStatus(status)) return null;
return {
taskId: rest.slice(0, separatorIndex),
status,
};
}
function isInboxAction(value: unknown): value is InboxAction {
return value === 'run' || value === 'decline' || value === 'dismiss';
}
function isPairedTaskStatus(value: unknown): value is PairedTask['status'] {
return (
value === 'active' ||
value === 'review_ready' ||
value === 'in_review' ||
value === 'merge_ready' ||
value === 'completed' ||
value === 'arbiter_requested' ||
value === 'in_arbitration'
);
}
async function readInboxAction(
request: Request,
): Promise<InboxActionRequest | null> {
try {
const body = (await request.json()) as {
action?: unknown;
requestId?: unknown;
lastOccurredAt?: unknown;
};
if (!isInboxAction(body.action)) return null;
return {
action: body.action,
requestId: sanitizeInboxActionRequestId(body.requestId),
lastOccurredAt:
typeof body.lastOccurredAt === 'string' && body.lastOccurredAt.trim()
? body.lastOccurredAt.trim()
: null,
};
} catch {
return null;
}
}
function makeWebRunId(prefix: string): string {
return `web-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function sanitizeInboxActionRequestId(value: unknown): string | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
const safe = trimmed.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 120);
return safe || null;
}
function makeInboxDismissKey(
inboxId: string,
lastOccurredAt: string | null,
): string {
return lastOccurredAt ? `${inboxId}\0${lastOccurredAt}` : inboxId;
}
function makeWebInboxMessageId(requestId: string | null): string {
return requestId
? `web-inbox-${requestId}`
: `web-inbox-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function declinedInboxMessage(status: PairedTask['status']): string {
if (status === 'review_ready' || status === 'in_review') {
return 'Dashboard declined review. Continue with the owner turn.';
}
if (status === 'merge_ready') {
return 'Dashboard declined finalization. Continue with the owner turn.';
}
if (status === 'arbiter_requested' || status === 'in_arbitration') {
return 'Dashboard declined arbiter escalation. Continue with the owner turn.';
}
return 'Dashboard declined this inbox item. Continue with the owner turn.';
}
function pairedFollowUpIntentForStatus(
status: PairedTask['status'],
): ScheduledPairedFollowUpIntentKind | null {
if (status === 'review_ready' || status === 'in_review') {
return 'reviewer-turn';
}
if (status === 'merge_ready') {
return 'finalize-owner-turn';
}
if (status === 'arbiter_requested' || status === 'in_arbitration') {
return 'arbiter-turn';
}
return null;
}
function isRoot(): boolean {
return typeof process.getuid === 'function' && process.getuid() === 0;
}
@@ -477,23 +343,12 @@ export function createWebDashboardHandler(
}
const rememberRoomMessageId = createRoomMessageIdCache();
const dismissedInboxKeys = new Set<string>();
const inboxDismissTracker = createInboxDismissTracker();
const recentServiceRestarts: ServiceRestartRecord[] = [];
const activeServiceRestartTargets = new Set<string>();
function isInboxItemDismissed(item: {
id: string;
lastOccurredAt: string;
}): boolean {
return (
dismissedInboxKeys.has(item.id) ||
dismissedInboxKeys.has(makeInboxDismissKey(item.id, item.lastOccurredAt))
);
}
return async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const actionInboxId = parseInboxActionPath(url.pathname);
const scheduledTaskRoute = await handleScheduledTaskRoute({
url,
@@ -509,156 +364,21 @@ export function createWebDashboardHandler(
});
if (scheduledTaskRoute) return scheduledTaskRoute;
if (actionInboxId) {
if (request.method !== 'POST') {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
const inboxRequest = await readInboxAction(request);
if (!inboxRequest) {
return jsonResponse({ error: 'Invalid inbox action' }, { status: 400 });
}
if (inboxRequest.action === 'dismiss') {
dismissedInboxKeys.add(
makeInboxDismissKey(actionInboxId, inboxRequest.lastOccurredAt),
);
return jsonResponse({ ok: true, id: actionInboxId, dismissed: true });
}
const pairedTarget = parsePairedInboxTarget(actionInboxId);
if (!pairedTarget) {
return jsonResponse(
{ error: 'Unsupported inbox action target' },
{ status: 400 },
);
}
if (!enqueueMessageCheck) {
return jsonResponse(
{ error: 'Paired follow-up queue is not configured' },
{ status: 503 },
);
}
const task = loadPairedTaskById(pairedTarget.taskId);
if (!task) {
return jsonResponse(
{ error: 'Paired task not found' },
{ status: 404 },
);
}
if (inboxRequest.action === 'decline') {
const messageId = makeWebInboxMessageId(inboxRequest.requestId);
if (inboxRequest.requestId && messageExists(task.chat_jid, messageId)) {
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
queued: false,
duplicate: true,
});
}
}
if (task.status !== pairedTarget.status) {
return jsonResponse(
{
error: 'Inbox item is stale',
status: task.status,
},
{ status: 409 },
);
}
if (inboxRequest.action === 'decline') {
const timestamp = opts.now?.() ?? new Date().toISOString();
const messageId = makeWebInboxMessageId(inboxRequest.requestId);
const updates: Partial<
Pick<
PairedTask,
| 'status'
| 'updated_at'
| 'arbiter_requested_at'
| 'completion_reason'
>
> = {
status: 'active',
updated_at: timestamp,
};
if (
task.status === 'arbiter_requested' ||
task.status === 'in_arbitration'
) {
updates.arbiter_requested_at = null;
}
const updated = mutatePairedTaskIfUnchanged(
task.id,
task.updated_at,
updates,
);
if (!updated) {
return jsonResponse(
{ error: 'Inbox item is stale', status: task.status },
{ status: 409 },
);
}
writeChatMetadata(
task.chat_jid,
timestamp,
undefined,
'web-dashboard',
true,
);
writeMessage({
id: messageId,
chat_jid: task.chat_jid,
sender: 'web-dashboard',
sender_name: 'Web Dashboard',
content: declinedInboxMessage(task.status),
timestamp,
is_from_me: false,
is_bot_message: false,
message_source_kind: 'ipc_injected_human',
});
enqueueMessageCheck(task.chat_jid, task.group_folder);
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
status: 'active',
queued: true,
});
}
const intentKind = pairedFollowUpIntentForStatus(task.status);
if (!intentKind) {
return jsonResponse(
{ error: 'Paired task is not actionable' },
{ status: 409 },
);
}
const queued = schedulePairedFollowUp({
chatJid: task.chat_jid,
runId: makeWebRunId('inbox'),
task,
intentKind,
enqueue: () => enqueueMessageCheck(task.chat_jid, task.group_folder),
});
return jsonResponse({
ok: true,
id: actionInboxId,
taskId: task.id,
intentKind,
queued,
});
}
const inboxActionRoute = await handleInboxActionRoute({
url,
request,
jsonResponse,
dismissTracker: inboxDismissTracker,
enqueueMessageCheck,
loadPairedTaskById,
messageExists,
mutatePairedTaskIfUnchanged,
schedulePairedFollowUp,
writeChatMetadata,
writeMessage,
now: opts.now,
});
if (inboxActionRoute) return inboxActionRoute;
const roomMessageRoute = await handleRoomMessageRoute({
url,
@@ -716,7 +436,7 @@ export function createWebDashboardHandler(
const overviewRoute = handleOverviewRoute({
url,
jsonResponse,
isInboxItemDismissed,
isInboxItemDismissed: inboxDismissTracker.isDismissed,
loadPairedTasks,
loadTasks,
readSnapshots,