Keep restored merged workspaces active (#2714)
* fix(workspaces): keep restored merged workspaces active Auto-archive previously reacted to every merged pull request snapshot, so restoring a workspace immediately archived it again. Persist the consumed change request with the archive transition so explicit recovery remains authoritative. * test(e2e): clean partial archive scenarios Track workspace resources as soon as creation succeeds so later setup failures archive the worktree and remove the registered project. * test(workspaces): pin archive latch restoration Cover the persisted archive marker across restore in CI and make the real browser assertion observe active workspace stability after refresh. * fix(workspaces): latch restored legacy archives Resolve the current merged change request while restoring records created before the archive marker existed, then persist it with the unarchive mutation. * fix(workspaces): refresh latch on restore When an archived workspace now points at a different merged change request, record that URL during restore so the restored workspace remains active. * test(e2e): make archive latch setup fail loudly Remove authentication-gated skipping and register each external fixture cleanup immediately after allocation so setup failures cannot leak resources. * test(workspaces): include archive latch in reconciliation
This commit is contained in:
@@ -79,6 +79,7 @@ function createLogger(): Logger {
|
||||
|
||||
function createHarness(overrides?: {
|
||||
autoArchiveAfterMerge?: boolean;
|
||||
autoArchivedChangeRequestUrl?: string | null;
|
||||
getSnapshot?: () => Promise<WorkspaceGitRuntimeSnapshot | null>;
|
||||
isPaseoOwnedWorktreeCwd?: ArchiveIfSafeDependencies["isPaseoOwnedWorktreeCwd"];
|
||||
archiveByScope?: ArchiveIfSafeDependencies["archiveByScope"];
|
||||
@@ -105,6 +106,9 @@ function createHarness(overrides?: {
|
||||
terminalManager: {} as AutoArchiveArchiveOptions["terminalManager"],
|
||||
findWorkspaceIdForCwd: vi.fn(async () => "ws-auto-archive"),
|
||||
listActiveWorkspaces: vi.fn(async () => []),
|
||||
getAutoArchivedChangeRequestUrl: vi.fn(
|
||||
async () => overrides?.autoArchivedChangeRequestUrl ?? null,
|
||||
),
|
||||
archiveWorkspaceRecord: vi.fn(),
|
||||
markWorkspaceArchiving: vi.fn(),
|
||||
clearWorkspaceArchiving: vi.fn(),
|
||||
@@ -316,6 +320,7 @@ function createRealOutcomeHarness(input: {
|
||||
},
|
||||
listActiveWorkspaces: async () =>
|
||||
active.filter((workspace) => !input.archivedWorkspaceIds.has(workspace.workspaceId)),
|
||||
getAutoArchivedChangeRequestUrl: async () => null,
|
||||
archiveWorkspaceRecord: async (workspaceId: string) => {
|
||||
input.archivedWorkspaceIds.add(workspaceId);
|
||||
const index = active.findIndex((workspace) => workspace.workspaceId === workspaceId);
|
||||
@@ -496,6 +501,45 @@ describe("archiveIfSafe", () => {
|
||||
expect(harness.inFlight.has(CWD)).toBe(false);
|
||||
});
|
||||
|
||||
test("does not archive a merge event already consumed by this workspace", async () => {
|
||||
const harness = createHarness({
|
||||
autoArchivedChangeRequestUrl: "https://github.com/acme/repo/pull/123",
|
||||
});
|
||||
|
||||
await runArchiveIfSafe(harness);
|
||||
|
||||
expect(harness.deps.archiveByScope).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("archives a different merged change request", async () => {
|
||||
const harness = createHarness({
|
||||
autoArchivedChangeRequestUrl: "https://github.com/acme/repo/pull/122",
|
||||
});
|
||||
|
||||
await runArchiveIfSafe(harness);
|
||||
|
||||
expect(harness.deps.archiveByScope).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("records the consumed change request in the workspace archive mutation", async () => {
|
||||
const harness = createHarness({
|
||||
archiveByScope: async (dependencies) => {
|
||||
await dependencies.archiveWorkspaceRecord("ws-auto-archive");
|
||||
return {
|
||||
archivedAgentIds: [],
|
||||
archivedWorkspaceIds: ["ws-auto-archive"],
|
||||
removedDirectory: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await runArchiveIfSafe(harness);
|
||||
|
||||
expect(harness.options.archiveWorkspaceRecord).toHaveBeenCalledWith("ws-auto-archive", {
|
||||
autoArchivedChangeRequestUrl: "https://github.com/acme/repo/pull/123",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves the merged cwd to a single workspace and does not iterate siblings", async () => {
|
||||
const harness = createHarness({
|
||||
resolveWorkspaceIdAtPath: async () => "ws-merged-worktree",
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
import type { ForgeService } from "../../services/forge-service.js";
|
||||
import type { TerminalManager } from "../../terminal/terminal-manager.js";
|
||||
import { isPaseoOwnedWorktreeCwd } from "../../utils/worktree.js";
|
||||
import type { WorkspaceArchiveContext } from "../workspace-registry.js";
|
||||
|
||||
export interface AutoArchiveArchiveOptions {
|
||||
paseoHome: string;
|
||||
@@ -28,7 +29,8 @@ export interface AutoArchiveArchiveOptions {
|
||||
terminalManager: TerminalManager;
|
||||
findWorkspaceIdForCwd: (cwd: string) => Promise<string | null>;
|
||||
listActiveWorkspaces: () => Promise<ActiveWorkspaceRef[]>;
|
||||
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
|
||||
getAutoArchivedChangeRequestUrl: (workspaceId: string) => Promise<string | null>;
|
||||
archiveWorkspaceRecord: (workspaceId: string, context?: WorkspaceArchiveContext) => Promise<void>;
|
||||
markWorkspaceArchiving: (workspaceIds: Iterable<string>, archivingAt: string) => void;
|
||||
clearWorkspaceArchiving: (workspaceIds: Iterable<string>) => void;
|
||||
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds: Iterable<string>) => Promise<void>;
|
||||
@@ -111,6 +113,11 @@ export async function archiveIfSafe(input: {
|
||||
log.warn({ cwd }, "Auto-archive could not resolve a workspace for cwd; skipping");
|
||||
return;
|
||||
}
|
||||
const autoArchivedChangeRequestUrl =
|
||||
await options.getAutoArchivedChangeRequestUrl(workspaceId);
|
||||
if (autoArchivedChangeRequestUrl === pullRequest.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deps.archiveByScope(
|
||||
{
|
||||
@@ -122,7 +129,10 @@ export async function archiveIfSafe(input: {
|
||||
agentStorage: options.agentStorage,
|
||||
findWorkspaceIdForCwd: options.findWorkspaceIdForCwd,
|
||||
listActiveWorkspaces: options.listActiveWorkspaces,
|
||||
archiveWorkspaceRecord: options.archiveWorkspaceRecord,
|
||||
archiveWorkspaceRecord: (workspaceIdToArchive) =>
|
||||
options.archiveWorkspaceRecord(workspaceIdToArchive, {
|
||||
autoArchivedChangeRequestUrl: pullRequest.url,
|
||||
}),
|
||||
emitWorkspaceUpdatesForWorkspaceIds: options.emitWorkspaceUpdatesForWorkspaceIds,
|
||||
markWorkspaceArchiving: options.markWorkspaceArchiving,
|
||||
clearWorkspaceArchiving: options.clearWorkspaceArchiving,
|
||||
|
||||
@@ -138,7 +138,11 @@ import type { PaseoToolRuntimeContext } from "./agent/tools/types.js";
|
||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
|
||||
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
|
||||
import {
|
||||
FileBackedProjectRegistry,
|
||||
FileBackedWorkspaceRegistry,
|
||||
type WorkspaceArchiveContext,
|
||||
} from "./workspace-registry.js";
|
||||
import { FileBackedChatService } from "./chat/chat-service.js";
|
||||
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||
import { LoopService } from "./loop-service.js";
|
||||
@@ -876,10 +880,14 @@ export async function createPaseoDaemon(
|
||||
paseoHome: config.paseoHome,
|
||||
workspaceGitService,
|
||||
});
|
||||
const archiveWorkspaceRecordExternal = async (workspaceId: string) => {
|
||||
const archiveWorkspaceRecordExternal = async (
|
||||
workspaceId: string,
|
||||
context?: WorkspaceArchiveContext,
|
||||
) => {
|
||||
const existingWorkspace = await archivePersistedWorkspaceRecord({
|
||||
workspaceId,
|
||||
workspaceRegistry,
|
||||
context,
|
||||
});
|
||||
if (!existingWorkspace || existingWorkspace.archivedAt) return;
|
||||
teardownArchivedWorkspaceRuntime(workspaceId);
|
||||
@@ -983,6 +991,8 @@ export async function createPaseoDaemon(
|
||||
logger,
|
||||
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
|
||||
listActiveWorkspaces: listActiveWorkspacesExternal,
|
||||
getAutoArchivedChangeRequestUrl: async (workspaceId) =>
|
||||
(await workspaceRegistry.get(workspaceId))?.autoArchivedChangeRequestUrl ?? null,
|
||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
||||
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
||||
|
||||
+92
-1
@@ -5,7 +5,10 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { afterEach, beforeEach, expect, test } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import { createNoopWorkspaceGitService } from "../../test-utils/workspace-git-service-stub.js";
|
||||
import {
|
||||
createNoGitWorkspaceRuntimeSnapshot,
|
||||
createNoopWorkspaceGitService,
|
||||
} from "../../test-utils/workspace-git-service-stub.js";
|
||||
import {
|
||||
FileBackedProjectRegistry,
|
||||
FileBackedWorkspaceRegistry,
|
||||
@@ -32,6 +35,7 @@ let tmpDir: string;
|
||||
let gitRoots: Set<string>;
|
||||
let gitBranches: Map<string, string | null>;
|
||||
let checkoutFailure: Error | null;
|
||||
let restoredMergedChangeRequestUrl: string | null;
|
||||
let workspaceRegistry: FileBackedWorkspaceRegistry;
|
||||
let projectRegistry: FileBackedProjectRegistry;
|
||||
let provisioning: WorkspaceProvisioningService;
|
||||
@@ -39,6 +43,25 @@ let provisioning: WorkspaceProvisioningService;
|
||||
function gitService() {
|
||||
return createNoopWorkspaceGitService({
|
||||
peekSnapshot: () => null,
|
||||
getSnapshot: async (cwd: string) => {
|
||||
const snapshot = createNoGitWorkspaceRuntimeSnapshot(cwd);
|
||||
if (!restoredMergedChangeRequestUrl) return snapshot;
|
||||
return {
|
||||
...snapshot,
|
||||
forge: {
|
||||
...snapshot.forge,
|
||||
featuresEnabled: true,
|
||||
pullRequest: {
|
||||
url: restoredMergedChangeRequestUrl,
|
||||
title: "Merged change request",
|
||||
state: "merged",
|
||||
baseRefName: "main",
|
||||
headRefName: "workspace-archive-latch",
|
||||
isMerged: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getCheckout: async (cwd: string) => {
|
||||
if (checkoutFailure) throw checkoutFailure;
|
||||
let worktreeRoot: string | null = null;
|
||||
@@ -68,6 +91,7 @@ beforeEach(async () => {
|
||||
gitRoots = new Set();
|
||||
gitBranches = new Map();
|
||||
checkoutFailure = null;
|
||||
restoredMergedChangeRequestUrl = null;
|
||||
workspaceRegistry = new FileBackedWorkspaceRegistry(
|
||||
path.join(tmpDir, "projects", "workspaces.json"),
|
||||
logger,
|
||||
@@ -337,6 +361,73 @@ test("ensureWorkspaceRecordUnarchived restores the owning archived project with
|
||||
expect((await projectRegistry.get(created.projectId))?.archivedAt).toBeNull();
|
||||
});
|
||||
|
||||
test("ensureWorkspaceRecordUnarchived preserves the consumed auto-archive change request", async () => {
|
||||
const repo = path.join(tmpDir, "repo");
|
||||
const changeRequestUrl = "https://github.com/getpaseo/paseo/pull/2714";
|
||||
gitRoots.add(repo);
|
||||
const created = await provisioning.findOrCreateWorkspaceForDirectory(repo);
|
||||
await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT, {
|
||||
autoArchivedChangeRequestUrl: changeRequestUrl,
|
||||
});
|
||||
const archivedWorkspace = await workspaceRegistry.get(created.workspaceId);
|
||||
|
||||
const unarchived = await provisioning.ensureWorkspaceRecordUnarchived(archivedWorkspace!);
|
||||
|
||||
expect(unarchived).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: changeRequestUrl,
|
||||
});
|
||||
expect(await workspaceRegistry.get(created.workspaceId)).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: changeRequestUrl,
|
||||
});
|
||||
});
|
||||
|
||||
test("ensureWorkspaceRecordUnarchived acknowledges a merged change request for a legacy archive", async () => {
|
||||
const repo = path.join(tmpDir, "repo");
|
||||
const changeRequestUrl = "https://github.com/getpaseo/paseo/pull/2714";
|
||||
gitRoots.add(repo);
|
||||
const created = await provisioning.findOrCreateWorkspaceForDirectory(repo);
|
||||
await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT);
|
||||
const archivedWorkspace = await workspaceRegistry.get(created.workspaceId);
|
||||
restoredMergedChangeRequestUrl = changeRequestUrl;
|
||||
|
||||
const unarchived = await provisioning.ensureWorkspaceRecordUnarchived(archivedWorkspace!);
|
||||
|
||||
expect(unarchived).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: changeRequestUrl,
|
||||
});
|
||||
expect(await workspaceRegistry.get(created.workspaceId)).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: changeRequestUrl,
|
||||
});
|
||||
});
|
||||
|
||||
test("ensureWorkspaceRecordUnarchived refreshes the latch for a different merged change request", async () => {
|
||||
const repo = path.join(tmpDir, "repo");
|
||||
const previousChangeRequestUrl = "https://github.com/getpaseo/paseo/pull/2713";
|
||||
const currentChangeRequestUrl = "https://github.com/getpaseo/paseo/pull/2714";
|
||||
gitRoots.add(repo);
|
||||
const created = await provisioning.findOrCreateWorkspaceForDirectory(repo);
|
||||
await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT, {
|
||||
autoArchivedChangeRequestUrl: previousChangeRequestUrl,
|
||||
});
|
||||
const archivedWorkspace = await workspaceRegistry.get(created.workspaceId);
|
||||
restoredMergedChangeRequestUrl = currentChangeRequestUrl;
|
||||
|
||||
const unarchived = await provisioning.ensureWorkspaceRecordUnarchived(archivedWorkspace!);
|
||||
|
||||
expect(unarchived).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: currentChangeRequestUrl,
|
||||
});
|
||||
expect(await workspaceRegistry.get(created.workspaceId)).toMatchObject({
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: currentChangeRequestUrl,
|
||||
});
|
||||
});
|
||||
|
||||
test("does not unarchive either record when checkout refresh fails", async () => {
|
||||
const repo = path.join(tmpDir, "repo");
|
||||
gitRoots.add(repo);
|
||||
|
||||
+20
-1
@@ -88,7 +88,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
serverId?: string;
|
||||
workspaceRegistry: WorkspaceRegistry;
|
||||
projectRegistry: ProjectRegistry;
|
||||
workspaceGitService: Pick<WorkspaceGitService, "getCheckout" | "peekSnapshot">;
|
||||
workspaceGitService: Pick<WorkspaceGitService, "getCheckout" | "getSnapshot" | "peekSnapshot">;
|
||||
logger: Logger;
|
||||
}): WorkspaceProvisioningService {
|
||||
const { serverId, workspaceRegistry, projectRegistry, workspaceGitService, logger } = deps;
|
||||
@@ -323,6 +323,22 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
).workspaceId;
|
||||
}
|
||||
|
||||
async function resolveRestoredAutoArchiveChangeRequestUrl(
|
||||
workspace: PersistedWorkspaceRecord,
|
||||
): Promise<string | null> {
|
||||
if (!workspace.archivedAt) {
|
||||
return workspace.autoArchivedChangeRequestUrl;
|
||||
}
|
||||
const snapshot = await workspaceGitService.getSnapshot(workspace.cwd, {
|
||||
force: true,
|
||||
includeForge: true,
|
||||
reason: "workspace-restore-auto-archive-latch",
|
||||
});
|
||||
return snapshot.forge.pullRequest?.isMerged
|
||||
? snapshot.forge.pullRequest.url
|
||||
: workspace.autoArchivedChangeRequestUrl;
|
||||
}
|
||||
|
||||
async function ensureWorkspaceRecordUnarchived(
|
||||
workspace: PersistedWorkspaceRecord,
|
||||
): Promise<PersistedWorkspaceRecord> {
|
||||
@@ -333,6 +349,8 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
workspace.archivedAt || project.archivedAt
|
||||
? await workspaceGitService.getCheckout(workspace.cwd)
|
||||
: null;
|
||||
const autoArchivedChangeRequestUrl =
|
||||
await resolveRestoredAutoArchiveChangeRequestUrl(workspace);
|
||||
let next: PersistedWorkspaceRecord | null = null;
|
||||
if (workspace.archivedAt && checkout) {
|
||||
const placementUpdate = reconcileWorkspacePlacement({
|
||||
@@ -343,6 +361,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
next = {
|
||||
...(placementUpdate?.workspace ?? workspace),
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
WorktreeTeardownError,
|
||||
} from "../utils/worktree.js";
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "./workspace-registry.js";
|
||||
import type {
|
||||
PersistedWorkspaceRecord,
|
||||
WorkspaceArchiveContext,
|
||||
WorkspaceRegistry,
|
||||
} from "./workspace-registry.js";
|
||||
import { createRealpathAwarePathMatcher } from "../utils/path.js";
|
||||
|
||||
export type ActiveWorkspaceRef = Pick<
|
||||
@@ -526,6 +530,7 @@ export async function archivePersistedWorkspaceRecord(input: {
|
||||
workspaceId: string;
|
||||
workspaceRegistry: Pick<WorkspaceRegistry, "get" | "archive">;
|
||||
archivedAt?: string;
|
||||
context?: WorkspaceArchiveContext;
|
||||
}): Promise<PersistedWorkspaceRecord | null> {
|
||||
const existingWorkspace = await input.workspaceRegistry.get(input.workspaceId);
|
||||
if (!existingWorkspace) {
|
||||
@@ -537,7 +542,7 @@ export async function archivePersistedWorkspaceRecord(input: {
|
||||
}
|
||||
|
||||
const archivedAt = input.archivedAt ?? new Date().toISOString();
|
||||
await input.workspaceRegistry.archive(input.workspaceId, archivedAt);
|
||||
await input.workspaceRegistry.archive(input.workspaceId, archivedAt, input.context);
|
||||
|
||||
return existingWorkspace;
|
||||
}
|
||||
|
||||
@@ -630,6 +630,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
createdAt: timestamp,
|
||||
updatedAt: expect.any(String),
|
||||
archivedAt: expect.any(String),
|
||||
autoArchivedChangeRequestUrl: null,
|
||||
});
|
||||
expect(projects.get("p1")).toEqual(project);
|
||||
});
|
||||
|
||||
@@ -420,6 +420,35 @@ describe("workspace registries", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("persists the consumed change request with the workspace archive", async () => {
|
||||
await workspaceRegistry.initialize();
|
||||
await workspaceRegistry.upsert(
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "workspace-auto-archive",
|
||||
projectId: "project-one",
|
||||
cwd: "/tmp/repo",
|
||||
kind: "worktree",
|
||||
displayName: "feature",
|
||||
createdAt: "2026-03-01T00:00:00.000Z",
|
||||
updatedAt: "2026-03-01T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
await workspaceRegistry.archive("workspace-auto-archive", "2026-03-02T00:00:00.000Z", {
|
||||
autoArchivedChangeRequestUrl: "https://github.com/acme/repo/pull/123",
|
||||
});
|
||||
|
||||
const reloaded = new FileBackedWorkspaceRegistry(
|
||||
path.join(tmpDir, "projects", "workspaces.json"),
|
||||
logger,
|
||||
);
|
||||
await reloaded.initialize();
|
||||
expect(await reloaded.get("workspace-auto-archive")).toMatchObject({
|
||||
archivedAt: "2026-03-02T00:00:00.000Z",
|
||||
autoArchivedChangeRequestUrl: "https://github.com/acme/repo/pull/123",
|
||||
});
|
||||
});
|
||||
|
||||
test("composes concurrent workspace field updates without losing either change", async () => {
|
||||
await workspaceRegistry.initialize();
|
||||
await workspaceRegistry.upsert(
|
||||
|
||||
@@ -80,6 +80,13 @@ const PersistedWorkspaceRecordSchema = z.object({
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
archivedAt: z.string().nullable(),
|
||||
// COMPAT(autoArchivedChangeRequestUrl): added in v0.2.6, remove optional parsing after 2027-01-31.
|
||||
// Records the merged change request whose automatic archive was consumed.
|
||||
autoArchivedChangeRequestUrl: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform((value) => value ?? null),
|
||||
pinnedAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
@@ -101,6 +108,10 @@ export interface WorkspaceMutationContext {
|
||||
expectsInitialAgent?: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceArchiveContext {
|
||||
autoArchivedChangeRequestUrl?: string;
|
||||
}
|
||||
|
||||
export interface ProjectMutation {
|
||||
kind: "upsert" | "archive" | "remove";
|
||||
projectId: string;
|
||||
@@ -140,7 +151,11 @@ export interface WorkspaceRegistry {
|
||||
updater: (record: PersistedWorkspaceRecord) => PersistedWorkspaceRecord,
|
||||
): Promise<PersistedWorkspaceRecord | null>;
|
||||
upsert(record: PersistedWorkspaceRecord, context?: WorkspaceMutationContext): Promise<void>;
|
||||
archive(workspaceId: string, archivedAt: string): Promise<void>;
|
||||
archive(
|
||||
workspaceId: string,
|
||||
archivedAt: string,
|
||||
context?: WorkspaceArchiveContext,
|
||||
): Promise<void>;
|
||||
remove(workspaceId: string): Promise<void>;
|
||||
/** Central lifecycle seam for daemon-global workspace observers. */
|
||||
subscribeToMutations?(
|
||||
@@ -471,8 +486,19 @@ export class FileBackedWorkspaceRegistry
|
||||
});
|
||||
}
|
||||
|
||||
override async archive(workspaceId: string, archivedAt: string): Promise<void> {
|
||||
const workspace = await this.archiveIfPresent(workspaceId, archivedAt);
|
||||
override async archive(
|
||||
workspaceId: string,
|
||||
archivedAt: string,
|
||||
context?: WorkspaceArchiveContext,
|
||||
): Promise<void> {
|
||||
const workspace = await super.update(workspaceId, (existing) => ({
|
||||
...existing,
|
||||
updatedAt: archivedAt,
|
||||
archivedAt,
|
||||
...(context?.autoArchivedChangeRequestUrl
|
||||
? { autoArchivedChangeRequestUrl: context.autoArchivedChangeRequestUrl }
|
||||
: {}),
|
||||
}));
|
||||
if (!workspace) return;
|
||||
await this.notifyMutation({ kind: "archive", workspaceId, workspace });
|
||||
}
|
||||
@@ -528,6 +554,7 @@ export function createPersistedWorkspaceRecord(input: {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
archivedAt?: string | null;
|
||||
autoArchivedChangeRequestUrl?: string | null;
|
||||
pinnedAt?: string | null;
|
||||
}): PersistedWorkspaceRecord {
|
||||
return PersistedWorkspaceRecordSchema.parse({
|
||||
@@ -539,6 +566,7 @@ export function createPersistedWorkspaceRecord(input: {
|
||||
isPaseoOwnedWorktree: input.isPaseoOwnedWorktree ?? false,
|
||||
mainRepoRoot: input.mainRepoRoot ?? null,
|
||||
archivedAt: input.archivedAt ?? null,
|
||||
autoArchivedChangeRequestUrl: input.autoArchivedChangeRequestUrl ?? null,
|
||||
pinnedAt: input.pinnedAt ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user