fix(recovery): preserve fresh routine owner direction
This commit is contained in:
parent
c1eafb8d9b
commit
6466257908
|
|
@ -5838,12 +5838,14 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
|||
expect(runs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("preserves productive continuation for a routine execution after fresh owner work", async () => {
|
||||
const { agentId, runId, issueId } = await seedStrandedIssueFixture({
|
||||
status: "in_progress",
|
||||
runStatus: "succeeded",
|
||||
livenessState: "advanced",
|
||||
});
|
||||
it("preserves productive continuation when fresh owner direction precedes its asynchronous wake", async () => {
|
||||
const { companyId, agentId, runId, issueId } =
|
||||
await seedStrandedIssueFixture({
|
||||
status: "in_progress",
|
||||
runStatus: "succeeded",
|
||||
livenessState: "advanced",
|
||||
});
|
||||
const recoveryRunAt = new Date(Date.now() - 1_000);
|
||||
await db
|
||||
.update(issues)
|
||||
.set({
|
||||
|
|
@ -5857,11 +5859,25 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
|||
contextSnapshot: {
|
||||
issueId,
|
||||
taskId: issueId,
|
||||
wakeReason: "issue_commented",
|
||||
source: "issue.comment",
|
||||
wakeReason: "source_scoped_recovery_action",
|
||||
recoveryActionId: randomUUID(),
|
||||
recoveryCause: SUCCESSFUL_RUN_MISSING_STATE_REASON,
|
||||
recoveryIntent: "status_only",
|
||||
allowDeliverableWork: false,
|
||||
allowDocumentUpdates: false,
|
||||
resumeRequiresNormalModel: true,
|
||||
},
|
||||
createdAt: recoveryRunAt,
|
||||
})
|
||||
.where(eq(heartbeatRuns.id, runId));
|
||||
await db.insert(issueComments).values({
|
||||
companyId,
|
||||
issueId,
|
||||
authorType: "user",
|
||||
authorUserId: "local-board",
|
||||
body: "Continue with this new owner instruction.",
|
||||
createdAt: new Date(recoveryRunAt.getTime() + 500),
|
||||
});
|
||||
|
||||
const result =
|
||||
await heartbeatService(db).reconcileStrandedAssignedIssues();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
gt,
|
||||
gte,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
not,
|
||||
notInArray,
|
||||
|
|
@ -1022,6 +1023,28 @@ export function recoveryService(
|
|||
.then((rows) => rows[0] ?? null);
|
||||
}
|
||||
|
||||
async function hasFreshUserDirectionAfterRun(
|
||||
issue: Pick<typeof issues.$inferSelect, "companyId" | "id">,
|
||||
latestRun: NonNullable<LatestIssueRun>,
|
||||
) {
|
||||
return db
|
||||
.select({ id: issueComments.id })
|
||||
.from(issueComments)
|
||||
.where(
|
||||
and(
|
||||
eq(issueComments.companyId, issue.companyId),
|
||||
eq(issueComments.issueId, issue.id),
|
||||
isNotNull(issueComments.authorUserId),
|
||||
isNull(issueComments.authorAgentId),
|
||||
isNull(issueComments.createdByRunId),
|
||||
isNull(issueComments.deletedAt),
|
||||
gt(issueComments.createdAt, latestRun.createdAt),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.then((rows) => Boolean(rows[0]));
|
||||
}
|
||||
|
||||
async function summarizeRecentContinuationRetries(
|
||||
companyId: string,
|
||||
issueId: string,
|
||||
|
|
@ -4987,9 +5010,17 @@ export function recoveryService(
|
|||
}
|
||||
continue;
|
||||
}
|
||||
const handoffEvidence =
|
||||
isExhaustedSuccessfulRunHandoff(latestRun) ??
|
||||
const exhaustedHandoffEvidence =
|
||||
isExhaustedSuccessfulRunHandoff(latestRun);
|
||||
const routineRecoveryEvidence =
|
||||
routineMissingDispositionRecoveryEvidence(issue, latestRun);
|
||||
const hasFreshUserDirection =
|
||||
routineRecoveryEvidence && latestRun
|
||||
? await hasFreshUserDirectionAfterRun(issue, latestRun)
|
||||
: false;
|
||||
const handoffEvidence =
|
||||
exhaustedHandoffEvidence ??
|
||||
(hasFreshUserDirection ? null : routineRecoveryEvidence);
|
||||
if (handoffEvidence) {
|
||||
if (isPluginManagedIssueLifecycle(issue)) {
|
||||
result.skipped += 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue