fix: route master agent through codex device pool
This commit is contained in:
@@ -1333,6 +1333,7 @@ export interface MasterAgentTask {
|
||||
deviceId: string;
|
||||
accountId?: string;
|
||||
accountLabel?: string;
|
||||
modelChannelAttemptedDeviceIds?: string[];
|
||||
attachmentId?: string;
|
||||
attachmentFileName?: string;
|
||||
attachmentDownloadToken?: string;
|
||||
@@ -9915,6 +9916,123 @@ function isTerminalMasterAgentTaskStatus(status: MasterAgentTaskStatus) {
|
||||
return status === "completed" || status === "failed" || status === "timed_out" || status === "canceled";
|
||||
}
|
||||
|
||||
function codexModelChannelAccountSummarySignedIn(device: Device) {
|
||||
const metadata = device.capabilities?.codexAppServer?.metadata;
|
||||
const accountSummary =
|
||||
metadata && typeof metadata === "object"
|
||||
? (metadata.accountSummary as { signedIn?: unknown } | undefined)
|
||||
: undefined;
|
||||
return accountSummary?.signedIn !== false;
|
||||
}
|
||||
|
||||
function hasUsableCodexModelChannelInState(device: Device | undefined) {
|
||||
if (!device || device.status !== "online" || isDeviceRevoked(device)) {
|
||||
return false;
|
||||
}
|
||||
const capabilities = device.capabilities;
|
||||
const hasCodexTransport = Boolean(
|
||||
capabilities?.codexAppServer?.connected ||
|
||||
capabilities?.cli?.connected ||
|
||||
capabilities?.gui?.connected,
|
||||
);
|
||||
return hasCodexTransport && codexModelChannelAccountSummarySignedIn(device);
|
||||
}
|
||||
|
||||
function isMasterCodexNodeTask(task: MasterAgentTask, state: BossState) {
|
||||
if (task.taskType !== "conversation_reply") {
|
||||
return false;
|
||||
}
|
||||
if (task.accountId?.startsWith("master-codex-device-")) {
|
||||
return true;
|
||||
}
|
||||
const account = task.accountId
|
||||
? state.aiAccounts.find((item) => item.accountId === task.accountId)
|
||||
: undefined;
|
||||
return account?.provider === "master_codex_node";
|
||||
}
|
||||
|
||||
function isTaskAuthorizedForDevice(task: MasterAgentTask, deviceId: string) {
|
||||
return (
|
||||
!task.authorizedDeviceIds ||
|
||||
task.authorizedDeviceIds.length === 0 ||
|
||||
task.authorizedDeviceIds.includes(deviceId)
|
||||
);
|
||||
}
|
||||
|
||||
function sortMasterCodexFailoverDevices(left: Device, right: Device) {
|
||||
return (right.lastSeenAt ?? "").localeCompare(left.lastSeenAt ?? "");
|
||||
}
|
||||
|
||||
function findNextMasterCodexNodeCandidateForFailedTask(
|
||||
state: BossState,
|
||||
task: MasterAgentTask,
|
||||
failedDeviceId: string,
|
||||
) {
|
||||
const excludedDeviceIds = new Set([failedDeviceId, ...(task.modelChannelAttemptedDeviceIds ?? [])]);
|
||||
const seenDeviceIds = new Set<string>();
|
||||
|
||||
const configuredCandidates = state.aiAccounts
|
||||
.filter((account) => {
|
||||
const deviceId = account.nodeId?.trim();
|
||||
if (
|
||||
!account.enabled ||
|
||||
account.provider !== "master_codex_node" ||
|
||||
(account.status !== "ready" && account.status !== "degraded") ||
|
||||
!deviceId ||
|
||||
excludedDeviceIds.has(deviceId) ||
|
||||
!isTaskAuthorizedForDevice(task, deviceId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const device = state.devices.find((item) => item.id === deviceId);
|
||||
return hasUsableCodexModelChannelInState(device);
|
||||
})
|
||||
.sort((left, right) => {
|
||||
if (left.isActive !== right.isActive) {
|
||||
return left.isActive ? -1 : 1;
|
||||
}
|
||||
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
||||
});
|
||||
|
||||
const configured = configuredCandidates[0];
|
||||
if (configured?.nodeId) {
|
||||
return {
|
||||
deviceId: configured.nodeId,
|
||||
accountId: configured.accountId,
|
||||
accountLabel: configured.label,
|
||||
};
|
||||
}
|
||||
|
||||
for (const account of configuredCandidates) {
|
||||
if (account.nodeId) {
|
||||
seenDeviceIds.add(account.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const device = state.devices
|
||||
.filter((item) => {
|
||||
if (
|
||||
excludedDeviceIds.has(item.id) ||
|
||||
seenDeviceIds.has(item.id) ||
|
||||
!isTaskAuthorizedForDevice(task, item.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return hasUsableCodexModelChannelInState(item);
|
||||
})
|
||||
.sort(sortMasterCodexFailoverDevices)[0];
|
||||
|
||||
if (!device) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId: device.id,
|
||||
accountId: `master-codex-device-${device.id}`,
|
||||
accountLabel: `Codex · ${device.name || device.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
function masterAgentTaskLeaseMs(task: MasterAgentTask) {
|
||||
return task.taskType === "conversation_reply"
|
||||
? MASTER_AGENT_TASK_CONVERSATION_LEASE_MS
|
||||
@@ -10376,6 +10494,46 @@ export async function completeMasterAgentTask(payload: {
|
||||
dispatchExecution: undefined,
|
||||
};
|
||||
}
|
||||
if (payload.status === "failed" && isMasterCodexNodeTask(task, state)) {
|
||||
const failover = findNextMasterCodexNodeCandidateForFailedTask(state, task, payload.deviceId);
|
||||
if (failover) {
|
||||
const failedAt = nowIso();
|
||||
const failedAccount = task.accountId
|
||||
? state.aiAccounts.find((item) => item.accountId === task.accountId)
|
||||
: undefined;
|
||||
if (failedAccount?.provider === "master_codex_node") {
|
||||
failedAccount.status = "degraded";
|
||||
failedAccount.lastError = payload.errorMessage?.trim() || "MASTER_CODEX_NODE_EXEC_FAILED";
|
||||
failedAccount.lastValidatedAt = failedAt;
|
||||
failedAccount.updatedAt = failedAt;
|
||||
}
|
||||
task.status = "queued";
|
||||
task.deviceId = failover.deviceId;
|
||||
task.accountId = failover.accountId;
|
||||
task.accountLabel = failover.accountLabel;
|
||||
task.modelChannelAttemptedDeviceIds = [
|
||||
...new Set([...(task.modelChannelAttemptedDeviceIds ?? []), payload.deviceId]),
|
||||
];
|
||||
task.completedAt = undefined;
|
||||
task.canceledAt = undefined;
|
||||
task.canceledBy = undefined;
|
||||
task.cancelReason = undefined;
|
||||
task.leaseExpiresAt = undefined;
|
||||
task.claimedAt = undefined;
|
||||
task.lastClaimedAt = undefined;
|
||||
task.lastErrorKind = "model_channel_failover";
|
||||
task.errorMessage = payload.errorMessage?.trim() || "MASTER_CODEX_NODE_EXEC_FAILED";
|
||||
task.replyBody = undefined;
|
||||
task.requestId = undefined;
|
||||
upsertTaskExecutionProgressMessageInState(state, task, "queued", payload.executionProgress);
|
||||
return {
|
||||
...task,
|
||||
dispatchPlan: undefined,
|
||||
dispatchExecution: undefined,
|
||||
dialogGuardIntervention: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
task.status = payload.status;
|
||||
task.completedAt = nowIso();
|
||||
task.leaseExpiresAt = undefined;
|
||||
|
||||
Reference in New Issue
Block a user