fix(server): keep the agent invokable when a run fails on a workspace sync conflict (#10660)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents on the same project can share one `shared_workspace` clone, and each sandbox run reconciles its git history back into it > - When histories written by different runs genuinely diverge, reconciliation can hit a real merge conflict — a property of the *workspace*, not of whichever agent happened to run last > - That agent nonetheless finalized into a sticky `error` state, removing a healthy agent from rotation while the workspace stayed broken — and on a shared workspace this serially knocks out every agent that touches it > - This pull request classifies workspace-reconciliation failure signatures as workspace-scoped, so the run still fails with the full message but the agent stays invokable > - The benefit is that one bad workspace state no longer disables agents one by one ## Linked Issues or Issue Description Refs #10645 — this addresses the sticky-agent-error clause of that issue. Workspace run serialization / per-agent worktrees remain tracked there (design sketch on the issue). ## What Changed - New exported `isWorkspaceSyncConflictFailure(message)` matching the reconciliation failure signatures: `merge-tree` conflict ("Failed to merge concurrent remote git histories"), integrate-retry exhaustion ("Failed to integrate concurrent remote git history"), and bundle prerequisite failures ("did not send all necessary objects", "lacks these prerequisite commits"). - Both run-failure finalization paths (adapter returned a failed result; adapter threw) pass `keepIdleOnFailure` for these signatures — the same mechanism already used for provider-quota failures — so the agent finalizes to `idle` instead of `error`. The run itself still fails and carries the full message; nothing about run reporting changes. ## Verification - `pnpm vitest run server/src/__tests__/heartbeat-workspace-session.test.ts` — new signature matrix (4 positive signatures, negatives for unrelated adapter failures and null/empty); 121 tests total. - `cd server && pnpm run typecheck`. ## Risks - Low. The only change is which failure families put the agent into `error`; behavior for every other failure is untouched. A workspace stuck in conflict still fails every run against it (visible on the runs surface) — it just no longer takes agents down with it. ## Model Used Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — extended thinking, agentic tool use. No other models involved. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
592cade5a6
commit
e4b0152ca3
|
|
@ -31,6 +31,7 @@ import {
|
|||
resolveNextSessionState,
|
||||
resolveTaskSessionConfigFreshness,
|
||||
issueTextImpliesPrDeliverable,
|
||||
isWorkspaceSyncConflictFailure,
|
||||
requiresPushCapabilityPreflight,
|
||||
resolveWorkspaceAfterLowTrustPreflight,
|
||||
resolveRuntimeSessionParamsForWorkspace,
|
||||
|
|
@ -2642,3 +2643,27 @@ describe("parseSessionCompactionPolicy", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWorkspaceSyncConflictFailure", () => {
|
||||
it("matches the git workspace reconciliation failure signatures", () => {
|
||||
expect(isWorkspaceSyncConflictFailure(
|
||||
"Failed to merge concurrent remote git histories for a5d46a8005b3 and c1042c11774a: Command failed: git merge-tree --write-tree",
|
||||
)).toBe(true);
|
||||
expect(isWorkspaceSyncConflictFailure(
|
||||
"Failed to integrate concurrent remote git history for a5d46a8005b3 after multiple retries.",
|
||||
)).toBe(true);
|
||||
expect(isWorkspaceSyncConflictFailure(
|
||||
"error: /tmp/restore/git-delta.bundle did not send all necessary objects",
|
||||
)).toBe(true);
|
||||
expect(isWorkspaceSyncConflictFailure(
|
||||
"error: Repository lacks these prerequisite commits: 4c631700",
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores unrelated adapter failures", () => {
|
||||
expect(isWorkspaceSyncConflictFailure("Codex exited with code 2")).toBe(false);
|
||||
expect(isWorkspaceSyncConflictFailure("no Codex credentials provisioned for managed home")).toBe(false);
|
||||
expect(isWorkspaceSyncConflictFailure(null)).toBe(false);
|
||||
expect(isWorkspaceSyncConflictFailure("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3907,6 +3907,26 @@ export function describeSessionResetReason(
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Failure signatures from sandbox→host git workspace reconciliation. These
|
||||
* describe the state of the SHARED workspace (divergent histories written by
|
||||
* different runs), not a defect in the agent that happened to run last —
|
||||
* putting the agent into a sticky `error` state over them removes a healthy
|
||||
* agent from rotation while leaving the actual problem (the workspace)
|
||||
* untouched. The run still fails and carries the full message.
|
||||
*/
|
||||
const WORKSPACE_SYNC_CONFLICT_SIGNATURES = [
|
||||
"Failed to merge concurrent remote git histories",
|
||||
"Failed to integrate concurrent remote git history",
|
||||
"did not send all necessary objects",
|
||||
"lacks these prerequisite commits",
|
||||
];
|
||||
|
||||
export function isWorkspaceSyncConflictFailure(message: string | null | undefined): boolean {
|
||||
if (!message) return false;
|
||||
return WORKSPACE_SYNC_CONFLICT_SIGNATURES.some((signature) => message.includes(signature));
|
||||
}
|
||||
|
||||
export function shouldDeferFollowupWakeForSameIssue(input: {
|
||||
activeRunStatus: string | null | undefined;
|
||||
isSameExecutionAgent: boolean;
|
||||
|
|
@ -15139,7 +15159,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
|||
{
|
||||
keepIdleOnFailure:
|
||||
outcome === "failed" &&
|
||||
(finalizedRun ? readHeartbeatRunErrorFamily(finalizedRun) === "provider_quota" : runErrorCode === "provider_quota"),
|
||||
((finalizedRun ? readHeartbeatRunErrorFamily(finalizedRun) === "provider_quota" : runErrorCode === "provider_quota") ||
|
||||
isWorkspaceSyncConflictFailure(adapterResult.errorMessage)),
|
||||
wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run),
|
||||
},
|
||||
);
|
||||
|
|
@ -15266,6 +15287,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
|||
|
||||
await finalizeAgentStatus(agent.id, "failed", message, {
|
||||
wasFirstHeartbeat: timerClaimWasFirstHeartbeat(run),
|
||||
keepIdleOnFailure: isWorkspaceSyncConflictFailure(message),
|
||||
});
|
||||
}
|
||||
} catch (outerErr) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue