Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 63 additions & 10 deletions apps/controller/src/console/agents/detail.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,13 @@ const DEPLOYMENT_OUTCOME_TONES = {
failed: "danger",
};

// A read failure that a later read can clear: no answer (a network error), a timeout, a rate
// limit or a server error. A refusal or a missing record is an answer, and polling stops on it.
function transientReadError(error) {
const status = error?.status;
return !(status >= 400 && status < 500) || status === 408 || status === 429;
}

// A recovery link opens a version tab far below the banner it sits in. Only the URL changed and
// the page stayed at the top, so the link looked dead (finding 1586). The page the link opens,
// switched in place, rebuilt or restored for Back, brings that tab into view and focuses it.
Expand Down Expand Up @@ -533,6 +540,9 @@ function createDeploymentStatusPanel(context, path, revision, onAgentChange, onS
// Tallest the details have been since a deploy was seen pending; see render().
let reservedHeight = null;
let pollTimer = null;
// The last record read. A transient read failure (a network error, a timeout, a 5xx) does not
// end a pending deploy, so polling goes on from it (finding 2085).
let lastRecord = null;

// A poll that fired while the view was retained stopped; restoring the view re-arms it.
context.onResume?.(() => {
Expand All @@ -541,14 +551,18 @@ function createDeploymentStatusPanel(context, path, revision, onAgentChange, onS
}
});

// Queued and running records are reread until they record a result or a read fails.
// Queued and running records are reread until they record a result or a read is refused.
function schedulePoll() {
clearTimeout(pollTimer);
pollTimer = null;
const record = state.error
? transientReadError(state.error)
? lastRecord
: null
: state.status;
if (
state.error ||
(!PENDING_DEPLOYMENT_STATUSES.has(state.status?.status) &&
!PENDING_DEPLOYMENT_STATUSES.has(state.status?.cleanup?.status))
!PENDING_DEPLOYMENT_STATUSES.has(record?.status) &&
!PENDING_DEPLOYMENT_STATUSES.has(record?.cleanup?.status)
) {
return;
}
Expand All @@ -567,7 +581,8 @@ function createDeploymentStatusPanel(context, path, revision, onAgentChange, onS
}
clearTimeout(pollTimer);
pollTimer = null;
const previousStatus = state.status?.status ?? null;
// After a failed read, the record before it.
const previousStatus = lastRecord?.status ?? null;
state.loading = true;
state.polling = poll;
state.error = null;
Expand All @@ -592,6 +607,7 @@ function createDeploymentStatusPanel(context, path, revision, onAgentChange, onS
}
if (statusResult.status === "fulfilled") {
state.status = statusResult.value;
lastRecord = statusResult.value;
} else {
state.status = null;
state.error = statusResult.reason;
Expand Down Expand Up @@ -626,6 +642,8 @@ function createDeploymentStatusPanel(context, path, revision, onAgentChange, onS
state.error !== null,
state.status?.error?.code ?? null,
state.status,
state.overviewError,
state.error !== null && transientReadError(state.error),
);
if (
state.refreshQueued ||
Expand Down Expand Up @@ -1907,6 +1925,11 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
let latestDeploymentErrorCode = null;
let latestDeploymentErrorMessage = null;
let latestDeploymentRecord = null;
// The newest version's status from its last record read; a failed read keeps it, so one
// failure does not relabel a deploying version Historical (finding 2085).
let latestRecordedStatus = null;
// Set when the Agent reread after a completed deployment failed, until an Agent read lands.
let agentRereadFailed = false;
let versionRecord = null;
const revisionsPromise = readRevisionPage(request, path);
// Every revision summary listed so far, from the newest page and from older pages the viewer
Expand All @@ -1930,7 +1953,9 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
? "Current version · read-only admitted snapshot."
: deployingRevision()?.id === selected
? "Deploying version · read-only admitted snapshot. It becomes the current version only if its deployment completes."
: "Historical version · read-only admitted snapshot. Browsing it does not change the current version.";
: deployedRevision()?.id === selected
? "Deployed version · read-only admitted snapshot. Its deployment completed, but the current version could not be refreshed."
: "Historical version · read-only admitted snapshot. Browsing it does not change the current version.";
}

// The newest version while its deployment is queued or running and it is not yet current. Right
Expand All @@ -1940,7 +1965,24 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
return latest &&
latest.id !== currentRevisionId &&
activityRevisionId === latest.id &&
PENDING_DEPLOYMENT_STATUSES.has(latestDeploymentStatus)
PENDING_DEPLOYMENT_STATUSES.has(latestRecordedStatus)
? latest
: null;
}

// The newest version once its deployment completed, while the Agent reread that would make it
// current failed. It is not Deploying beside "Completed" (finding 2085), nor Historical: the
// page cannot yet tell. Only that failed reread says so; a stopped Agent's last version, say,
// also has a completed record and no current version, and stays Historical, as does one under
// a known newer current version the viewer cannot read.
function deployedRevision() {
const latest = visibleRevisions[0];
return latest &&
latest.id !== currentRevisionId &&
activityRevisionId === latest.id &&
latestRecordedStatus === "succeeded" &&
agentRereadFailed &&
!revisionHidden(currentRevisionId)
? latest
: null;
}
Expand Down Expand Up @@ -2161,6 +2203,7 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
showDeleting();
return;
}
agentRereadFailed = false;
const revisionChanged = freshAgent.activeRevisionId !== currentRevisionId;
const workspaceAvailabilityChanged =
Boolean(freshAgent.activeRevisionId) !== Boolean(currentRevisionId);
Expand Down Expand Up @@ -2574,6 +2617,8 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
// The version's record panel no longer gets this panel's reads.
versionRecord?.release();
latestDeploymentStatus = null;
latestRecordedStatus = null;
agentRereadFailed = false;
latestDeploymentRecord = null;
latestDeploymentError = false;
latestDeploymentErrorCode = null;
Expand Down Expand Up @@ -2605,12 +2650,19 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
{ status: "fulfilled", value: freshRevisions },
snapshot,
),
(status, unavailable, errorCode, record) => {
(status, unavailable, errorCode, record, overviewUnavailable, transientFailure) => {
if (activityRevisionId !== mostRecent.id) {
return;
}
showPanel(nextPanel, status === "failed");
latestDeploymentStatus = status;
// A refused or missing record ends the wait, so it no longer labels the version.
if (record || !transientFailure) {
latestRecordedStatus = status;
}
if (overviewUnavailable) {
agentRereadFailed = true;
}
latestDeploymentError = unavailable;
latestDeploymentErrorCode = errorCode;
latestDeploymentErrorMessage = record?.error?.message || null;
Expand All @@ -2630,8 +2682,9 @@ export async function renderAgentDetail(context, { agent: preloadedAgent = null
}
renderLatestDeployment();
// A success is shown once the Agent reread makes the version current; until then
// the notice keeps "Deploying" rather than flash "Historical".
if (status !== "succeeded") {
// the notice keeps "Deploying" rather than flash "Historical". A reread that failed
// leaves the version labelled from its record (finding 2085).
if (status !== "succeeded" || overviewUnavailable) {
renderVersionNotice();
}
refreshDeployControls();
Expand Down
2 changes: 1 addition & 1 deletion docs/flows/platform-console/agent-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ read error or denial. **Open version draft** (`deploy` only) and **Deploy again*
message) need `deploy` and a successful Configuration read; Deploy again focuses **Deploy new
version**. Credentials recovery needs `update`. Hidden or unlisted
current versions suppress recovery; viewed versions show results. Activity polls every
`DEPLOYMENT_POLL_MS` until a result or read error; a new result also rereads selection. **Refresh deployment** rereads
`DEPLOYMENT_POLL_MS`, through transient read errors, until a result, denial, or missing record; a new result also rereads selection. **Refresh deployment** rereads
activity and selection, after any poll in flight. Polls patch in place (`patchChildren`), so
an unchanged poll writes nothing. A pending deploy lists its result rows before the first attempt;
the details keep their tallest height until Refresh after it settles. **Current observations** requests timestamped
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/console/agent-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ check time. A failed startup model check may also show its **Cause**, such as
the provider reporting a rate limit or the check process exiting with an error
([causes](../../reference/agents/deployment.md#model-check-failure-cause)). Plugin warnings describe that attempt. An unavailable record has
unknown status. While the record is `queued` or `running`, the panel rereads it
every few seconds and stops at `succeeded`, `failed`, or a read error.
every few seconds, through outages, until `succeeded`, `failed`, a denial, or a missing record.
**Refresh deployment** rereads it, the selected version, and its
deployment record without retrying work.

Expand Down
2 changes: 2 additions & 0 deletions scripts/console-storybook/pages-agent-detail.stories.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const DeploymentDeferred = story("deploymentDeferred");
export const DeploymentRetrying = story("deploymentRetrying");
export const DeploymentRunning = story("deploymentRunning");
export const DeploymentRunningActivates = story("deploymentRunningActivates");
export const DeploymentCompletesRereadFails = story("deploymentCompletesRereadFails");
export const DeploymentStatusReadRecovers = story("deploymentStatusReadRecovers");
export const DeploymentActivityLoading = story("deploymentActivityLoading");
export const CurrentVersionDuringDeployment = story("currentVersionDuringDeployment");
export const DeploymentFailed = story("deploymentFailed");
Expand Down
39 changes: 39 additions & 0 deletions scripts/console-storybook/public/scenarios.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4014,6 +4014,45 @@ export const scenarios = {
'Within about 5 s the notice begins "Current version" and Selected version reads v7.',
],
},
deploymentCompletesRereadFails: {
group: "Pages/Agent detail",
name: "New version completes, Agent reread fails",
path: `${candidateVersion}&tab=configuration`,
deployed: true,
candidateDeploymentStatus: "running",
candidateActivates: true,
// The page's own Agent read answers; the reread after the deployment completes fails.
rules: [{ suffix: "/agents/agt_00000000-0000-4000-8000-000000000001", skip: 1, status: 503 }],
description:
"v7's deployment completes, but the Agent reread that would make it current fails. The notice says the deployment completed and that the current version could not be refreshed, rather than Deploying beside Completed. The deployment and the failure are simulated.",
steps: [
'On arrival the notice begins "Deploying version" and Selected version reads v6.',
'Within about 5 s Latest visible deployment reads "v7 · Completed", the page says the current version and version history could not be refreshed, and the notice reads "Deployed version · read-only admitted snapshot. Its deployment completed, but the current version could not be refreshed."',
],
},
deploymentStatusReadRecovers: {
group: "Pages/Agent detail",
name: "New version in progress, one status read fails",
path: `${candidateVersion}&tab=configuration`,
deployed: true,
candidateDeploymentStatus: "running",
candidateActivates: true,
// The first poll after arrival fails once; the next one records success.
rules: [
{
suffix: "/deployments/rev_00000000-0000-4000-8000-000000000007",
skip: 1,
once: true,
status: 503,
},
],
description:
"One deployment status read fails while v7 is deploying. The notice keeps Deploying version rather than turning Historical, polling continues, and the next read records success and makes v7 current. The deployment, failure and activation are simulated.",
steps: [
'On arrival the notice begins "Deploying version". Within about 5 s Latest visible deployment reads "v7 · Unavailable" and the notice still begins "Deploying version".',
'Within about 10 s the notice begins "Current version" and Selected version reads v7, with no "Historical version" in between.',
],
},
deploymentDeferred: {
group: "Pages/Agent detail",
name: "Deployment waiting for runtime",
Expand Down
Loading
Loading