Keep archived workspaces recent in Done

This commit is contained in:
Mohamed Boudra
2026-06-04 11:46:04 +07:00
parent f139966554
commit 6dcf3ffd6a
2 changed files with 69 additions and 11 deletions

View File

@@ -4356,6 +4356,62 @@ test("buildWorkspaceDescriptorMap computes statusEnteredAt from runtime agent fi
}
});
test("buildWorkspaceDescriptorMap keeps a done workspace recent after its agents are archived", async () => {
const session = createSessionForWorkspaceTests();
const project = createPersistedProjectRecord({
projectId: "proj-archive-status-entered",
rootPath: REPO_CWD,
kind: "git",
displayName: "repo",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const workspace = createPersistedWorkspaceRecord({
workspaceId: "ws-archive-status-entered",
projectId: project.projectId,
cwd: "/tmp/repo/archive-status-entered",
kind: "worktree",
displayName: "feature",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const doneEnteredAt = "2026-05-12T09:30:00.000Z";
const archivedAt = "2026-05-12T09:45:00.000Z";
session.projectRegistry.list = async () => [project];
session.workspaceRegistry.list = async () => [workspace];
session.listAgentPayloads = async () => [
makeAgent({
id: "agent-done",
cwd: workspace.cwd,
status: "idle",
updatedAt: doneEnteredAt,
}),
];
const first = await session.buildWorkspaceDescriptorMap({ includeGitData: false });
expect(first.get(workspace.workspaceId)?.status).toBe("done");
expect(first.get(workspace.workspaceId)?.statusEnteredAt).toBe(doneEnteredAt);
session.listAgentPayloads = async () => [
{
...makeAgent({
id: "agent-done",
cwd: workspace.cwd,
status: "idle",
updatedAt: doneEnteredAt,
}),
archivedAt,
},
];
const second = await session.buildWorkspaceDescriptorMap({ includeGitData: false });
expect(second.get(workspace.workspaceId)).toMatchObject({
status: "done",
statusEnteredAt: doneEnteredAt,
});
});
test("buildWorkspaceDescriptorMap stamps workspace archiving state", async () => {
const session = createSessionForWorkspaceTests();
const archivingAt = "2026-04-30T20:45:00.000Z";

View File

@@ -30,7 +30,7 @@ const FETCH_WORKSPACES_SORT_KEYS = [
* mask to a lower-priority bucket, the new entry time is the unmask time
* (i.e., the moment the higher-priority bucket cleared), not when the
* underlying agent originally entered the lower-priority bucket. Cleared when
* the workspace becomes `done` with no contributing agents.
* the workspace has never had contributing agents.
*/
interface WorkspaceBucketHistoryEntry {
bucket: WorkspaceStateBucket;
@@ -279,7 +279,10 @@ export class WorkspaceDirectory {
// - priority unmasking: when the winning bucket transitions (e.g. a
// higher-priority bucket cleared), the new entry time is "now";
// - same-bucket emits reuse the previous entered-at;
// - empty workspaces (no contributing agents) get `statusEnteredAt: null`.
// - empty workspaces that never had contributing agents get
// `statusEnteredAt: null`.
// - when archived agents leave a previously active workspace empty, keep
// the previous done timestamp or stamp the transition to done now.
private resolveStatusEnteredAt(params: {
workspaceId: string;
winningBucket: WorkspaceStateBucket;
@@ -294,16 +297,15 @@ export class WorkspaceDirectory {
const { winningBucket, contributingAgents, previous, nowIso } = params;
if (contributingAgents.length === 0) {
if (previous && previous.bucket !== "done") {
return { statusEnteredAt: null, recordDelete: true };
if (!previous) {
return { statusEnteredAt: null };
}
if (previous) {
return {
statusEnteredAt: null,
recordUpdate: { bucket: "done", enteredAt: previous.enteredAt },
};
}
return { statusEnteredAt: null };
const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
return {
statusEnteredAt: enteredAt,
recordUpdate: { bucket: "done", enteredAt },
};
}
if (!previous) {