Finalize turn-role invariants and legacy migration cutover

This commit is contained in:
ejclaw
2026-04-10 11:44:34 +09:00
parent 7753ee46f0
commit c67bd1c622
45 changed files with 15014 additions and 1381 deletions

View File

@@ -1,21 +1,15 @@
import { Database } from 'bun:sqlite';
import { SERVICE_ID, normalizeServiceId } from '../config.js';
import {
ARBITER_AGENT_TYPE,
CODEX_MAIN_SERVICE_ID,
CODEX_REVIEW_SERVICE_ID,
SERVICE_ID,
normalizeServiceId,
} from '../config.js';
buildPairedTurnIdentity,
resolvePairedTurnRole,
} from '../paired-turn-identity.js';
import { canonicalizePairedTaskMetadata } from './canonical-role-metadata.js';
import {
resolveStablePairedTaskOwnerAgentType,
resolveStableReviewerAgentType,
} from './legacy-rebuilds.js';
import { normalizeStoredAgentType } from './room-registration.js';
import {
inferAgentTypeFromServiceShadow,
resolveRoleServiceShadow,
} from '../role-service-shadow.js';
ensurePairedTurnQueuedInDatabase,
markPairedTurnRunningInDatabase,
} from './paired-turns.js';
import {
AgentType,
PairedProject,
@@ -66,69 +60,31 @@ function computeExecutionLeaseExpiry(now: string): string {
).toISOString();
}
function resolveExecutionLeaseRole(
intentKind: PairedTurnReservationIntentKind,
): PairedRoomRole {
switch (intentKind) {
case 'reviewer-turn':
return 'reviewer';
case 'arbiter-turn':
return 'arbiter';
case 'owner-turn':
case 'owner-follow-up':
case 'finalize-owner-turn':
default:
return 'owner';
}
}
function hydratePairedTaskRow(
database: Database,
row: StoredPairedTaskRow,
): PairedTask {
const persistedOwnerAgentType = normalizeStoredAgentType(
row.owner_agent_type ?? null,
);
const persistedReviewerAgentType = normalizeStoredAgentType(
row.reviewer_agent_type ?? null,
);
const stableOwnerAgentType = resolveStablePairedTaskOwnerAgentType(
database,
row,
);
const ownerAgentType =
stableOwnerAgentType ??
(row.owner_service_id
? inferAgentTypeFromServiceShadow(row.owner_service_id)
: undefined);
const stableReviewerAgentType = resolveStableReviewerAgentType(
stableOwnerAgentType,
row.reviewer_agent_type ?? null,
);
const reviewerAgentType =
persistedReviewerAgentType ??
stableReviewerAgentType ??
(row.reviewer_service_id
? inferAgentTypeFromServiceShadow(row.reviewer_service_id)
: null) ??
resolveStableReviewerAgentType(ownerAgentType, null);
const arbiterAgentType =
normalizeStoredAgentType(row.arbiter_agent_type) ??
ARBITER_AGENT_TYPE ??
null;
const {
ownerAgentType,
reviewerAgentType,
arbiterAgentType,
ownerServiceId,
reviewerServiceId,
} = canonicalizePairedTaskMetadata({
id: row.id,
owner_service_id: row.owner_service_id,
reviewer_service_id: row.reviewer_service_id,
owner_agent_type: row.owner_agent_type,
reviewer_agent_type: row.reviewer_agent_type,
arbiter_agent_type: row.arbiter_agent_type,
});
return {
...row,
owner_service_id:
row.owner_service_id ??
resolveRoleServiceShadow('owner', ownerAgentType) ??
CODEX_MAIN_SERVICE_ID,
reviewer_service_id:
row.reviewer_service_id ??
resolveRoleServiceShadow('reviewer', reviewerAgentType) ??
CODEX_REVIEW_SERVICE_ID,
owner_agent_type: ownerAgentType ?? null,
reviewer_agent_type: reviewerAgentType ?? null,
owner_service_id: ownerServiceId,
reviewer_service_id: reviewerServiceId,
owner_agent_type: ownerAgentType,
reviewer_agent_type: reviewerAgentType,
arbiter_agent_type: arbiterAgentType,
};
}
@@ -176,26 +132,20 @@ export function createPairedTaskInDatabase(
database: Database,
task: PairedTask,
): void {
const ownerAgentType =
normalizeStoredAgentType(task.owner_agent_type) ??
inferAgentTypeFromServiceShadow(task.owner_service_id) ??
null;
const reviewerAgentType =
normalizeStoredAgentType(task.reviewer_agent_type) ??
inferAgentTypeFromServiceShadow(task.reviewer_service_id) ??
resolveStableReviewerAgentType(ownerAgentType ?? undefined, null);
const arbiterAgentType =
normalizeStoredAgentType(task.arbiter_agent_type) ??
ARBITER_AGENT_TYPE ??
null;
const ownerServiceId =
task.owner_service_id ??
resolveRoleServiceShadow('owner', ownerAgentType) ??
CODEX_MAIN_SERVICE_ID;
const reviewerServiceId =
task.reviewer_service_id ??
resolveRoleServiceShadow('reviewer', reviewerAgentType) ??
CODEX_REVIEW_SERVICE_ID;
const {
ownerAgentType,
reviewerAgentType,
arbiterAgentType,
ownerServiceId,
reviewerServiceId,
} = canonicalizePairedTaskMetadata({
id: task.id,
owner_service_id: task.owner_service_id,
reviewer_service_id: task.reviewer_service_id,
owner_agent_type: task.owner_agent_type,
reviewer_agent_type: task.reviewer_agent_type,
arbiter_agent_type: task.arbiter_agent_type,
});
database
.prepare(
@@ -426,6 +376,12 @@ export function reservePairedTurnReservationInDatabase(
},
): boolean {
const now = new Date().toISOString();
const turnIdentity = buildPairedTurnIdentity({
taskId: args.taskId,
taskUpdatedAt: args.taskUpdatedAt,
intentKind: args.intentKind,
role: resolvePairedTurnRole(args.intentKind),
});
const result = database
.prepare(
`
@@ -435,6 +391,10 @@ export function reservePairedTurnReservationInDatabase(
task_status,
round_trip_count,
task_updated_at,
turn_id,
turn_attempt_id,
turn_attempt_no,
turn_role,
intent_kind,
status,
scheduled_run_id,
@@ -443,8 +403,32 @@ export function reservePairedTurnReservationInDatabase(
updated_at,
consumed_at
)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, NULL, ?, ?, NULL)
ON CONFLICT(chat_jid, task_id, task_updated_at, intent_kind) DO NOTHING
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, 'pending', ?, NULL, ?, ?, NULL)
ON CONFLICT(chat_jid, task_id, task_updated_at, intent_kind) DO UPDATE SET
task_status = excluded.task_status,
round_trip_count = excluded.round_trip_count,
turn_id = excluded.turn_id,
turn_attempt_id = NULL,
turn_attempt_no = NULL,
turn_role = excluded.turn_role,
status = 'pending',
scheduled_run_id = excluded.scheduled_run_id,
consumed_run_id = NULL,
updated_at = excluded.updated_at,
consumed_at = NULL
WHERE paired_turn_reservations.status = 'completed'
AND EXISTS (
SELECT 1
FROM paired_turn_attempts latest_attempt
WHERE latest_attempt.turn_id = paired_turn_reservations.turn_id
AND latest_attempt.state = 'failed'
AND NOT EXISTS (
SELECT 1
FROM paired_turn_attempts newer_attempt
WHERE newer_attempt.turn_id = latest_attempt.turn_id
AND newer_attempt.attempt_no > latest_attempt.attempt_no
)
)
`,
)
.run(
@@ -453,13 +437,20 @@ export function reservePairedTurnReservationInDatabase(
args.taskStatus,
args.roundTripCount,
args.taskUpdatedAt,
turnIdentity.turnId,
turnIdentity.role,
args.intentKind,
args.runId,
now,
now,
);
return result.changes > 0;
if (result.changes > 0) {
ensurePairedTurnQueuedInDatabase(database, turnIdentity);
return true;
}
return false;
}
class PairedTurnReservationClaimError extends Error {}
@@ -479,7 +470,12 @@ export function claimPairedTurnReservationInDatabase(
const tx = database.transaction(() => {
const now = new Date().toISOString();
const expiresAt = computeExecutionLeaseExpiry(now);
const leaseRole = resolveExecutionLeaseRole(args.intentKind);
const turnIdentity = buildPairedTurnIdentity({
taskId: args.taskId,
taskUpdatedAt: args.taskUpdatedAt,
intentKind: args.intentKind,
role: resolvePairedTurnRole(args.intentKind),
});
const existingLease = database
.prepare(
`
@@ -501,26 +497,30 @@ export function claimPairedTurnReservationInDatabase(
const insertedLease = database
.prepare(
`
INSERT INTO paired_task_execution_leases (
task_id,
chat_jid,
role,
intent_kind,
claimed_run_id,
claimed_service_id,
INSERT INTO paired_task_execution_leases (
task_id,
chat_jid,
role,
turn_id,
turn_attempt_id,
turn_attempt_no,
intent_kind,
claimed_run_id,
claimed_service_id,
task_status,
task_updated_at,
claimed_at,
updated_at,
expires_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?)
`,
)
.run(
args.taskId,
args.chatJid,
leaseRole,
turnIdentity.role,
turnIdentity.turnId,
args.intentKind,
args.runId,
CURRENT_SERVICE_ID,
@@ -543,6 +543,9 @@ export function claimPairedTurnReservationInDatabase(
UPDATE paired_task_execution_leases
SET chat_jid = ?,
role = ?,
turn_id = ?,
turn_attempt_id = NULL,
turn_attempt_no = NULL,
intent_kind = ?,
claimed_run_id = ?,
claimed_service_id = ?,
@@ -559,7 +562,8 @@ export function claimPairedTurnReservationInDatabase(
)
.run(
args.chatJid,
leaseRole,
turnIdentity.role,
turnIdentity.turnId,
args.intentKind,
args.runId,
CURRENT_SERVICE_ID,
@@ -579,31 +583,6 @@ export function claimPairedTurnReservationInDatabase(
}
}
database
.prepare(
`
UPDATE paired_turn_reservations
SET status = 'completed',
consumed_run_id = ?,
updated_at = ?,
consumed_at = ?
WHERE chat_jid = ?
AND task_id = ?
AND task_updated_at = ?
AND intent_kind = ?
AND status = 'pending'
`,
)
.run(
args.runId,
now,
now,
args.chatJid,
args.taskId,
args.taskUpdatedAt,
args.intentKind,
);
const claimedTask = database
.prepare(
`
@@ -619,6 +598,67 @@ export function claimPairedTurnReservationInDatabase(
if (claimedTask.changes === 0) {
throw new PairedTurnReservationClaimError();
}
const currentAttempt = markPairedTurnRunningInDatabase(database, {
turnIdentity,
executorServiceId: CURRENT_SERVICE_ID,
runId: args.runId,
});
if (!currentAttempt) {
throw new Error(
`paired_turns(${turnIdentity.turnId}) did not materialize a running attempt row`,
);
}
const turnAttemptNo = currentAttempt.attempt_no;
const turnAttemptId = currentAttempt.attempt_id;
database
.prepare(
`
UPDATE paired_task_execution_leases
SET turn_attempt_id = ?,
turn_attempt_no = ?
WHERE task_id = ?
AND claimed_service_id = ?
AND claimed_run_id = ?
`,
)
.run(
turnAttemptId,
turnAttemptNo,
args.taskId,
CURRENT_SERVICE_ID,
args.runId,
);
database
.prepare(
`
UPDATE paired_turn_reservations
SET status = 'completed',
turn_attempt_id = ?,
turn_attempt_no = ?,
consumed_run_id = ?,
updated_at = ?,
consumed_at = ?
WHERE chat_jid = ?
AND task_id = ?
AND task_updated_at = ?
AND intent_kind = ?
AND status = 'pending'
`,
)
.run(
turnAttemptId,
turnAttemptNo,
args.runId,
now,
now,
args.chatJid,
args.taskId,
args.taskUpdatedAt,
args.intentKind,
);
});
try {