Self-heal tribunal rooms with no work_dir so the owner never goes silent

A tribunal room whose work_dir was never provisioned (null) made
resolveOwnerTaskForHumanMessage return a null task, so the owner never ran and
the room went completely silent — the user's messages got no reply at all (seen
in the tts_site room, where days of requests were dropped). ensurePairedProject
now provisions the canonical workspace on demand when work_dir is missing,
guarded to tribunal rooms so a single-mode room never gets a spurious paired
workspace. Idempotent via ensurePairedWorkspaceProvisioned. Adds tests for both
the tribunal self-heal and the single-mode no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-25 20:47:00 +09:00
parent 6fad6dcfab
commit 03e6f8bcc6
2 changed files with 69 additions and 1 deletions

View File

@@ -14,6 +14,7 @@ vi.mock('./db.js', () => {
return {
cancelPairedTurn: vi.fn(),
createPairedTask: vi.fn(),
getEffectiveRuntimeRoomMode: vi.fn(() => 'tribunal'),
getLatestPairedTaskForChat: vi.fn(),
getLatestOpenPairedTaskForChat: vi.fn(),
getPairedTaskById: vi.fn(),
@@ -33,6 +34,9 @@ vi.mock('./db.js', () => {
});
vi.mock('./paired-workspace-manager.js', () => ({
ensurePairedWorkspaceProvisioned: vi.fn(
() => '/repo/self-healed-canonical',
),
isOwnerWorkspaceRepairNeededError: vi.fn(() => false),
markPairedTaskReviewReady: vi.fn(),
prepareReviewerWorkspaceForExecution: vi.fn(),
@@ -312,6 +316,54 @@ describe('paired execution context', () => {
expect(db.insertPairedTurnOutput).not.toHaveBeenCalled();
});
it('self-heals a tribunal room with no work_dir by provisioning the canonical workspace', () => {
// Reproduces the silent-room incident: a tribunal room whose work_dir was
// never provisioned would return a null task and never run the owner.
const noWorkDirGroup: RegisteredGroup = { ...group, workDir: undefined };
vi.mocked(db.getEffectiveRuntimeRoomMode).mockReturnValue('tribunal');
vi.mocked(
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
).mockReturnValue('/repo/self-healed-canonical');
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(undefined);
const result = resolveOwnerTaskForHumanMessage({
group: noWorkDirGroup,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
existingTask: null,
});
expect(
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
).toHaveBeenCalledWith({
chatJid: 'dc:test',
groupFolder: noWorkDirGroup.folder,
});
expect(result.task).not.toBeNull();
expect(db.createPairedTask).toHaveBeenCalledTimes(1);
});
it('does not provision a workspace for a single-mode room with no work_dir', () => {
const noWorkDirGroup: RegisteredGroup = { ...group, workDir: undefined };
vi.mocked(db.getEffectiveRuntimeRoomMode).mockReturnValue('single');
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(undefined);
const result = resolveOwnerTaskForHumanMessage({
group: noWorkDirGroup,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
existingTask: null,
});
expect(
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
).not.toHaveBeenCalled();
expect(result.task).toBeNull();
expect(db.createPairedTask).not.toHaveBeenCalled();
});
it('records a quick reopen when a new owner task starts shortly after TASK_DONE completion', () => {
const previousTask = buildPairedTask({
id: 'task-completed',

View File

@@ -19,6 +19,7 @@ import {
import {
cancelPairedTurn,
createPairedTask,
getEffectiveRuntimeRoomMode,
getLatestPairedTaskForChat,
getLatestOpenPairedTaskForChat,
getPairedTaskById,
@@ -48,6 +49,7 @@ import {
} from './paired-task-status.js';
import { resolveCanonicalSourceRef } from './paired-source-ref.js';
import {
ensurePairedWorkspaceProvisioned,
isOwnerWorkspaceRepairNeededError,
prepareReviewerWorkspaceForExecution,
provisionOwnerWorkspaceForPairedTask,
@@ -74,8 +76,22 @@ function ensurePairedProject(
chatJid: string,
): string | null {
if (!group.workDir) {
// Self-heal: a tribunal room whose work_dir was never provisioned (null)
// makes resolveOwnerTaskForHumanMessage return a null task, so the owner
// never runs and the room goes completely silent — the user's messages get
// no reply at all. Provision the canonical workspace on demand so the owner
// can always start. Guarded to tribunal rooms so a single-mode room never
// gets a spurious paired workspace. Idempotent: ensurePairedWorkspace-
// Provisioned skips git init/commit when they already exist and upserts the
// paired project itself.
if (getEffectiveRuntimeRoomMode(chatJid) !== 'tribunal') {
return null;
}
return ensurePairedWorkspaceProvisioned({
chatJid,
groupFolder: group.folder,
});
}
const now = new Date().toISOString();
upsertPairedProject({