mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 12:33:05 +01:00
fix(workspaces): cover task waits and reuse canonical recovery causes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e1caab7064
commit
ba18efc7b5
3 files changed
+12
-9
No files matched your search
@@ -206,7 +206,7 @@ finite offline operation adds no idle background work. A resumed rehearsal can
|
||||
measure the fix but cannot replace fresh full-image migration and empty-home
|
||||
recovery proof.
|
||||
|
||||
During offline migration, known task-only assignment, configuration and
|
||||
During offline migration, known task-only assignment, configuration, deliberate-wait and
|
||||
disposition warnings can remain unchanged while the owner saves local files.
|
||||
They do not certify file recovery and are not resolved by migration. An active
|
||||
system recovery, native execution failure marker, declared workspace recovery
|
||||
|
||||
@@ -151,10 +151,11 @@ describe.skipIf(!support.supported)("remote workspace migration", () => {
|
||||
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run!.id)))[0]).toEqual(run);
|
||||
expect(readRemoteWorkspaceAuthority((await workspace(f.workspaceId)).metadata)).not.toBeNull();
|
||||
});
|
||||
it.each(["stranded_assigned_issue", "configuration_validation", "missing_disposition"])("preserves a task-only %s warning through offline file migration", async kind => {
|
||||
it.each([["stranded_assigned_issue", "stranded_assigned_issue"], ["configuration_validation", "configuration_incomplete"],
|
||||
["missing_disposition", "successful_run_missing_state"], ["deliberate_wait_without_target", "deliberate_wait_without_target"]])("preserves a task-only %s warning through offline file migration", async (kind, cause) => {
|
||||
const f = await seed();
|
||||
const [action] = await db.insert(issueRecoveryActions).values({ companyId: f.companyId, sourceIssueId: f.issueId,
|
||||
kind, cause: kind, fingerprint: randomUUID(), nextAction: "Keep the existing task warning", evidence: { latestRunStatus: "failed" } }).returning();
|
||||
kind, cause, fingerprint: randomUUID(), nextAction: "Keep the existing task warning", evidence: { latestRunStatus: "failed" } }).returning();
|
||||
await expect(migrateRemoteWorkspace({ db, storage, ...f, apply: true })).rejects.toThrow("workspace_recovery_unresolved");
|
||||
vi.stubEnv("PAPERCLIP_EPHEMERAL_MIGRATION", "true");
|
||||
await migrateRemoteWorkspace({ db, storage, ...f, apply: true, writersStopped: true });
|
||||
@@ -167,12 +168,13 @@ describe.skipIf(!support.supported)("remote workspace migration", () => {
|
||||
await restoreRemoteWorkspaceArchive({ archivePath, targetDir, expectedManifestDigest: ref!.manifestDigest });
|
||||
expect(await fs.readFile(path.join(targetDir, "saved.txt"), "utf8")).toBe("original saved work\n");
|
||||
});
|
||||
it.each(["workspaceRestoreRecovery", "workspaceRecovery", "historicalWorkspaceSource", "systemRecovery", "nativeFailureBlock", "export-cause", "unknown-kind"])("retains unresolved file or execution recovery as an offline blocker (%s)", async mode => {
|
||||
it.each(["workspaceRestoreRecovery", "workspaceRecovery", "historicalWorkspaceSource", "systemRecovery", "nativeFailureBlock", "export-cause", "unknown-kind", "workspace_validation", "active_run_watchdog"])("retains unresolved file or execution recovery as an offline blocker (%s)", async mode => {
|
||||
const f = await seed(); vi.stubEnv("PAPERCLIP_EPHEMERAL_MIGRATION", "true");
|
||||
await db.insert(issueRecoveryActions).values({ companyId: f.companyId, sourceIssueId: f.issueId,
|
||||
kind: mode === "unknown-kind" ? "future_recovery" : "configuration_validation",
|
||||
cause: mode === "export-cause" ? "native_workspace_sync_out_unrecoverable" : "configuration_incomplete",
|
||||
fingerprint: randomUUID(), nextAction: "Preserve unresolved recovery", evidence: mode === "export-cause" || mode === "unknown-kind" ? {}
|
||||
kind: mode === "unknown-kind" ? "future_recovery" : ["workspace_validation", "active_run_watchdog"].includes(mode) ? mode : "configuration_validation",
|
||||
cause: mode === "export-cause" ? "native_workspace_sync_out_unrecoverable" : mode === "workspace_validation" ? "workspace_validation_failed"
|
||||
: mode === "active_run_watchdog" ? "native_session_interrupted" : "configuration_incomplete",
|
||||
fingerprint: randomUUID(), nextAction: "Preserve unresolved recovery", evidence: ["export-cause", "unknown-kind", "workspace_validation", "active_run_watchdog"].includes(mode) ? {}
|
||||
: { [mode]: mode === "systemRecovery" ? { state: "running" } : { schema: "unverified" } } });
|
||||
await expect(migrateRemoteWorkspace({ db, storage, ...f, apply: true, writersStopped: true })).rejects.toThrow("workspace_recovery_unresolved");
|
||||
expect(readRemoteWorkspaceAuthority((await workspace(f.workspaceId)).metadata)).toBeNull();
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@paperclipai/db";
|
||||
import { readRemoteWorkspaceAuthority, remoteWorkspaceSourceSchema, type RemoteWorkspaceAuthority, type RemoteWorkspaceSource } from "@paperclipai/shared";
|
||||
import { offlineMigrationExecutionWhere } from "./offline-migration-execution.js";
|
||||
import { workspaceExportCauses } from "./workspace-recovery-source.js";
|
||||
import type { StorageService } from "../storage/types.js";
|
||||
import { persistActivity, publishActivity } from "./activity-log.js";
|
||||
import { withExecutionWorkspaceLifecycleLock } from "./execution-workspace-lifecycle-lock.js";
|
||||
@@ -213,8 +214,8 @@ async function inspect(db: ReadDb, input: { companyId: string; workspaceId: stri
|
||||
// These warnings govern task disposition, not file authority. Keep them
|
||||
// unchanged while the offline owner saves the workspace. Unknown recovery
|
||||
// kinds and any declared file source still require explicit preservation.
|
||||
return !["stranded_assigned_issue", "configuration_validation", "missing_disposition"].includes(recovery.kind)
|
||||
|| ["native_workspace_sync_out_unrecoverable", "native_workspace_sync_out_retry_exhausted"].includes(recovery.cause)
|
||||
return !["stranded_assigned_issue", "configuration_validation", "missing_disposition", "deliberate_wait_without_target"].includes(recovery.kind)
|
||||
|| workspaceExportCauses.includes(recovery.cause)
|
||||
|| ["workspaceRestoreRecovery", "workspaceRecovery", "historicalWorkspaceSource", "systemRecovery", "nativeFailureBlock"]
|
||||
.some(key => recovery.evidence?.[key] != null);
|
||||
})) reasons.push("workspace_recovery_unresolved");
|
||||
|
||||
Reference in new issue
Block a user