Phase 2 / PR #3 of the module refactor. Moves the approval and interactive- question flows out of core and into src/modules/, wired through the response dispatcher and delivery action registries. New modules: - src/modules/interactive/ — registers a response handler that claims pending_questions rows, writes question_response to the session DB, wakes the container. createPendingQuestion call stays inline in delivery.ts (guarded by hasTable) per plan. - src/modules/approvals/ — registers 3 delivery actions (install_packages, request_rebuild, add_mcp_server), a response handler for pending_approvals (including OneCLI action fall-through), an adapter-ready hook that boots the OneCLI manual-approval handler, and a shutdown hook that stops it. OneCLI implementation (src/onecli-approvals.ts) moves into the module. Core lifecycle hooks added (narrow, not registries): - onDeliveryAdapterReady(cb) in delivery.ts — fires when setDeliveryAdapter runs (or immediately if already set). Used by approvals for OneCLI boot. - onShutdown(cb) in index.ts — fires on SIGTERM/SIGINT. Used by approvals for OneCLI teardown. - getDeliveryAdapter() getter in delivery.ts — for live-flow adapter access in registered delivery actions. Core shrinks: delivery.ts 911 → 665 lines, index.ts 405 → 224 lines. dispatchResponse now logs "Unclaimed response" instead of falling through to an inline handler — the inline fallback moved into the two modules. Migration files renamed to the module-<name>-<short>.ts convention: - 003-pending-approvals.ts → module-approvals-pending-approvals.ts - 007-pending-approvals-title-options.ts → module-approvals-title-options.ts Migration.name fields unchanged so existing DBs treat them as already-applied. Degradation verified: emptying the modules barrel builds clean and 137/137 tests pass. Actions would log "Unknown system action"; button clicks would log "Unclaimed response". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
/**
|
|
* Interactive module — generic ask_user_question flow.
|
|
*
|
|
* Container-side `ask_user_question` writes a chat-sdk card to outbound.db +
|
|
* polls inbound.db for a `question_response` system message. On the host side
|
|
* this module handles the button-click response: look up the pending_questions
|
|
* row, write the response into the session's inbound.db, wake the container.
|
|
*
|
|
* The `createPendingQuestion` call in `deliverMessage` (delivery.ts) stays
|
|
* inline in core — it's 15 lines guarded by `hasTable('pending_questions')`,
|
|
* modularizing it adds more registry surface than it saves.
|
|
*/
|
|
import { getDb, hasTable } from '../../db/connection.js';
|
|
import { deletePendingQuestion, getPendingQuestion, getSession } from '../../db/sessions.js';
|
|
import { wakeContainer } from '../../container-runner.js';
|
|
import { registerResponseHandler, type ResponsePayload } from '../../index.js';
|
|
import { log } from '../../log.js';
|
|
import { writeSessionMessage } from '../../session-manager.js';
|
|
|
|
async function handleInteractiveResponse(payload: ResponsePayload): Promise<boolean> {
|
|
if (!hasTable(getDb(), 'pending_questions')) return false;
|
|
|
|
const pq = getPendingQuestion(payload.questionId);
|
|
if (!pq) return false;
|
|
|
|
const session = getSession(pq.session_id);
|
|
if (!session) {
|
|
log.warn('Session not found for pending question', { questionId: payload.questionId, sessionId: pq.session_id });
|
|
deletePendingQuestion(payload.questionId);
|
|
return true; // claimed — we owned this questionId even though the session is gone
|
|
}
|
|
|
|
writeSessionMessage(session.agent_group_id, session.id, {
|
|
id: `qr-${payload.questionId}-${Date.now()}`,
|
|
kind: 'system',
|
|
timestamp: new Date().toISOString(),
|
|
platformId: pq.platform_id,
|
|
channelType: pq.channel_type,
|
|
threadId: pq.thread_id,
|
|
content: JSON.stringify({
|
|
type: 'question_response',
|
|
questionId: payload.questionId,
|
|
selectedOption: payload.value,
|
|
userId: payload.userId ?? '',
|
|
}),
|
|
});
|
|
|
|
deletePendingQuestion(payload.questionId);
|
|
log.info('Question response routed', { questionId: payload.questionId, selectedOption: payload.value, sessionId: session.id });
|
|
|
|
await wakeContainer(session);
|
|
return true;
|
|
}
|
|
|
|
registerResponseHandler(handleInteractiveResponse);
|