Fix owner context loss after finalize; green the test suite

Owner continuity:
- Seed a freshly created owner task with the previous task's latest owner
  final on a cold start after the previous task already closed, so a user
  reply to a finalized TASK_DONE no longer produces a "no context" answer.
  Activated for this deployment via PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL
  (.env); carried text is injected as clearly-marked background only.
- Skip intermediate STEP_DONE outputs when picking the carry-forward anchor.

Single-mode routing:
- enforceRoomModeOnLease strips a stale reviewer/arbiter lease from a room
  switched back to single, preventing single-mode messages from stalling in
  the paired path on a stuck execution lease.

Session auth / credentials:
- Pre-sync Claude credentials into each session dir before the agent spawns.
- Honor CLAUDE_CREDENTIALS_PATH in setup/login.ts (per-service isolation).
- Add a relogin-required gate so a permanently logged-out claude-code room
  asks the user to re-login instead of spawning a doomed agent.

Other:
- Arbiter verdicts written in the user's language (verdict keyword stays EN).
- status-dashboard chatName field; runtime-inventory credential path resolver.

Tests (make suite fully green: 1595 pass / 3 skip):
- service-routing: default owner is now the claude service and reviewer is
  codex-review; update the 7 failover/default expectations accordingly.
- migrate-room-registrations: owner inferred as claude-code (configured
  OWNER_AGENT_TYPE) for a dual legacy room; reviewer becomes codex.
- register: mock paired-workspace provisioning + reload signal (registration
  now provisions a workspace and hot-reloads); assert RELOADED status.
- paired-execution-context: force a claude-code reviewer to exercise the
  Claude read-only branch regardless of the deployment default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-25 18:40:14 +09:00
parent 80df025672
commit 712664ca00
17 changed files with 457 additions and 91 deletions

View File

@@ -229,14 +229,22 @@ function cancelOutstandingFinalizeOwnerTurn(task: PairedTask): void {
);
}
function getLatestTurnOutputByRole(
taskId: string,
role: PairedRoomRole,
): PairedTurnOutput | null {
function isIntermediateStepOutput(outputText: string): boolean {
// STEP_DONE marks an intermediate step that keeps the task active; it is not
// the user-facing finalize summary, so it is a poor carry-forward anchor.
return /^\s*STEP_DONE\b/.test(outputText);
}
function getLatestOwnerFinalOutput(taskId: string): PairedTurnOutput | null {
const ownerOutputs = [...getPairedTurnOutputs(taskId)]
.reverse()
.filter((output) => output.role === 'owner');
return (
[...getPairedTurnOutputs(taskId)]
.reverse()
.find((output) => output.role === role) ?? null
ownerOutputs.find(
(output) => !isIntermediateStepOutput(output.output_text),
) ??
ownerOutputs[0] ??
null
);
}
@@ -248,10 +256,7 @@ function carryForwardLatestOwnerFinal(args: {
return;
}
const latestOwnerFinal = getLatestTurnOutputByRole(
args.sourceTask.id,
'owner',
);
const latestOwnerFinal = getLatestOwnerFinalOutput(args.sourceTask.id);
if (!latestOwnerFinal) {
return;
}
@@ -293,16 +298,27 @@ export function resolveOwnerTaskForHumanMessage(args: {
args.existingTask ?? getLatestOpenPairedTaskForChat(args.chatJid) ?? null;
if (!existing) {
maybeRecordTaskDoneReopen(getLatestPairedTaskForChat(args.chatJid) ?? null);
const previousTask = getLatestPairedTaskForChat(args.chatJid) ?? null;
maybeRecordTaskDoneReopen(previousTask);
const newTask = canonicalWorkDir
? createActiveTaskForRoom({
group: args.group,
chatJid: args.chatJid,
canonicalWorkDir,
roomRoleContext: args.roomRoleContext,
})
: null;
// Cold start after the previous task already closed (completed): seed the
// fresh task with the previous task's latest owner final so the owner keeps
// continuity across sessions instead of answering with "no context".
if (newTask && previousTask) {
carryForwardLatestOwnerFinal({
sourceTask: previousTask,
targetTask: newTask,
});
}
return {
task: canonicalWorkDir
? createActiveTaskForRoom({
group: args.group,
chatJid: args.chatJid,
canonicalWorkDir,
roomRoleContext: args.roomRoleContext,
})
: null,
task: newTask,
supersededTask: null,
};
}