fix(hermes): project native completion and tool activity

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-09 01:57:28 -05:00
1 parent 300afb3fe3
commit f628d54bc8
12 files changed
+225 -12

No files matched your search

@@ -21,6 +21,8 @@ import {
issues,
nativeRunFinalizations,
nativeRunResults,
routines,
routineRuns,
statusDecisionEffects,
statusDecisions,
workAssessments,
@@ -290,6 +292,8 @@ describe("PaperclipControlPlanePort conformance", () => {
await db.delete(heartbeatRunEvents);
await db.delete(heartbeatRuns);
await db.delete(completionContracts);
await db.delete(routineRuns);
await db.delete(routines);
await db.delete(issues);
await db.delete(agents);
await db.delete(companyMemberships);
@@ -974,7 +978,18 @@ describe("PaperclipControlPlanePort conformance", () => {
expect(response.decision.chosenSource).toBe("final_agent_message");
expect(stored.findIndex((row) => row.eventType === "run.result.accepted"))
.toBeGreaterThan(stored.findIndex((row) => row.eventType === "item.completed"));
const routineId = randomUUID();
const firingId = randomUUID();
await db.insert(routines).values({ id: routineId, companyId: identity.companyId,
title: "Native completion routine", assigneeAgentId: identity.agentId });
await db.insert(routineRuns).values({ id: firingId, companyId: identity.companyId,
routineId, source: "schedule", status: "issue_created", linkedIssueId: taskIssueId });
await db.update(issues).set({ originKind: "routine_execution", originId: routineId,
originRunId: firingId }).where(eq(issues.id, taskIssueId));
await finalizeNativeRun({ db, runId: taskRunId, workspaceFinalizeStatus: "succeeded" });
const [completedFiring] = await db.select().from(routineRuns).where(eq(routineRuns.id, firingId));
expect(completedFiring).toMatchObject({ status: "completed", failureReason: null });
expect(completedFiring.completedAt).not.toBeNull();
await expect(port.completeRun({
result: taskResult,
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
@@ -996,6 +1011,9 @@ describe("PaperclipControlPlanePort conformance", () => {
await expect(db.select().from(issues).where(eq(issues.id, taskIssueId))).resolves.toEqual([
expect.objectContaining({ status: "done", statusVersion: 1 }),
]);
await expect(db.select().from(routineRuns).where(eq(routineRuns.id, firingId))).resolves.toEqual([
expect.objectContaining({ status: "completed", completedAt: completedFiring.completedAt }),
]);
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, taskRunId))).resolves.toHaveLength(1);
await expect(db.select().from(workAssessments).where(eq(workAssessments.runId, taskRunId))).resolves.toHaveLength(1);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, taskIssueId))).resolves.toHaveLength(1);
@@ -2161,6 +2179,14 @@ describe("PaperclipControlPlanePort conformance", () => {
backendKind: "mock",
sourceInstanceId: runnerInstanceId,
});
const routineId = randomUUID();
const firingId = randomUUID();
await db.insert(routines).values({ id: routineId, companyId: identity.companyId,
title: `Atomic routine ${entry.failpoint}`, assigneeAgentId: identity.agentId });
await db.insert(routineRuns).values({ id: firingId, companyId: identity.companyId,
routineId, source: "schedule", status: "issue_created", linkedIssueId: issueId });
await db.update(issues).set({ originKind: "routine_execution", originId: routineId,
originRunId: firingId }).where(eq(issues.id, issueId));
await port.completeRun({ result: entry.result, terminal, callerResultId: `atomic-result-${entry.suffix}` });
await expect(finalizeNativeRun({
db,
@@ -2171,6 +2197,9 @@ describe("PaperclipControlPlanePort conformance", () => {
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
]);
await expect(db.select().from(routineRuns).where(eq(routineRuns.id, firingId))).resolves.toEqual([
expect.objectContaining({ status: "issue_created", completedAt: null, failureReason: null }),
]);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId))).resolves.toHaveLength(0);
await expect(db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, issueId))).resolves.toHaveLength(0);
await expect(db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId))).resolves.toHaveLength(0);
@@ -45,6 +45,7 @@ import {
resolveExternalChatResponseWaitAuthorizationInTransaction,
} from "./chat-attachment-reuse.js";
import { issueService } from "../issues.js";
import { routineService } from "../routines.js";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
import { buildIssueBlockersResolvedWakeIdempotencyKey } from "../issue-dependency-wakeups.js";
@@ -2037,6 +2038,12 @@ export async function commitNativeStatusDecision(input: {
});
}
// Keep the firing projection atomic with the authoritative task status.
// Recovery and normal completion use the same status-decision transaction.
if (updated.originKind === "routine_execution") {
await routineService(tx as unknown as Db).syncRunStatusForIssue(input.issueId);
}
if (input.decision.statusAction === "done" && issue.status !== "done") {
const issueSvc = issueService(tx as unknown as Db);
const dependents = await issueSvc.listWakeableBlockedDependents(