mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
fix(server): connect governed response lifecycle notifications
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
53b26e24f9
commit
3b80594f37
8 files changed
+91
-3
No files matched your search
@@ -2391,3 +2391,12 @@ reproduced missed-wake failures and repaired test stub. Interaction producer
|
||||
coverage, discovery cost, startup wiring, periodic removal and full acceptance
|
||||
remain outstanding. No slice 49 PR or whole-instance sleep is claimed. Earlier
|
||||
stack gates, master reconciliation and every remaining original duty stay in scope.
|
||||
|
||||
Slice 49 lifecycle checkpoint: the real agent-resume missed wake was reproduced
|
||||
and repaired through the existing lifecycle hook. Milestone/finalization scopes
|
||||
now feed review recovery, and pending-effect settlement supplies its task identity.
|
||||
The 172-case owner suite and 162 lifecycle/milestone/finalization consumer cases
|
||||
pass, with server types passing. The owner audit lists remaining interaction, task
|
||||
authority and coordinator producer evidence. Registration, periodic removal, query
|
||||
cost, full verification and publication remain outstanding; the full stack scope
|
||||
and older unresolved gates remain unchanged.
|
||||
@@ -194,3 +194,50 @@ and confirmation kinds. Payload-only updates that leave a competing card pending
|
||||
do not reactivate a response. Task ownership changes already carry task invalidation,
|
||||
but direct cleanup writers must be individually accounted for. The polling backstop
|
||||
is retained until this audit and the remaining acceptance requirements are complete.
|
||||
|
||||
## Lifecycle and finalization notification checkpoint
|
||||
|
||||
A real agent-resume regression failed before wiring and passes afterward. The
|
||||
existing lifecycle pre-status-write hook now signals this worker when an agent
|
||||
becomes invokable. Existing task/run milestones and finalization writes also
|
||||
fan out to the new topic. The finalization scope accepts an optional issueId so
|
||||
issue-wide pending-effect settlement can target its affected task instead of
|
||||
reloading all governed responses in the company. Existing pending-effect delivery
|
||||
retains its previous topic semantics. The tool gateway's separate approval link
|
||||
insertion now supplies its known issue identity.
|
||||
|
||||
All 172 owner cases pass, plus 58 agent lifecycle, 72 milestone and 32 native
|
||||
finalization consumer cases. Server typecheck passes. Evidence is in
|
||||
/tmp/sleep49-agent-before.log (reproduced failure),
|
||||
/tmp/sleep49-lifecycle-full.log, /tmp/sleep49-lifecycle-types.log,
|
||||
/tmp/sleep49-agent-consumers.log, /tmp/sleep49-milestone-consumers.log,
|
||||
/tmp/sleep49-finalization-consumers.log and their status files.
|
||||
|
||||
Further producer audit findings, not completed migration claims:
|
||||
|
||||
- Main interaction service: suggest-task accept/reject, item-verdict completion,
|
||||
connection-intent resolve, administrative cancel/withdraw/skip and comment
|
||||
supersession can release a competing pending interaction. Several existing
|
||||
milestone hooks cover only ask_user_questions/request_confirmation. Checkbox
|
||||
confirmation resolution and other kinds need their own governed-response wake
|
||||
without unnecessarily broadening unrelated workers. Some item-verdict comment
|
||||
expiry writes do not currently own a transaction.
|
||||
- Direct interaction writers: satisfied-connection cleanup and OAuth confirmation
|
||||
acceptance/denial need coverage. OAuth connection-intent retry-phase changes
|
||||
leave the interaction pending and cannot by themselves restore presentation.
|
||||
Dismissal also leaves status pending; confirm the owning predicate before
|
||||
introducing a wake for it.
|
||||
- Existing agent-conversation reset, tool-action review, obsolete policy review,
|
||||
automatic completion review and native decision commits already emit task
|
||||
milestones/finalization signals. Verify every alternate branch before marking
|
||||
those writer groups covered.
|
||||
- Issue service currently signals response availability on task status/assignment
|
||||
changes, but that boundary only targets GitHub response queues. Governed-response
|
||||
task status/assignment/execution-state restoration still needs explicit coverage.
|
||||
- Coordinator claim/retry notifications now reach the worker, but add a real-store
|
||||
release test; synthetic late claim/deadline owner tests alone do not prove that
|
||||
producer path. Current authority registration and all publication transitions
|
||||
also remain part of the producer audit.
|
||||
|
||||
The worker remains unregistered and periodic repair is unchanged. These commits
|
||||
are checkpoints, not PR readiness or whole-instance sleep acceptance.
|
||||
@@ -1,3 +1,4 @@
|
||||
import { notifyNativeReviewResponse } from "./native-review-response-notifications.js";
|
||||
import { isAgentStatusInvokable } from "@paperclipai/shared";
|
||||
import { notifyNativeResponsePresentation } from "./native-response-presentation-notifications.js";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
@@ -24,6 +25,7 @@ export function createAgentLifecycleEffects(_hooks: BudgetServiceHooks = {}): li
|
||||
async beforeStatusChange(tx, companyId, agentId, before, after) {
|
||||
if (!isAgentStatusInvokable(before) && isAgentStatusInvokable(after)) {
|
||||
await notifyNativeResponsePresentation(tx, { companyId, agentId });
|
||||
await notifyNativeReviewResponse(tx, { companyId, agentId });
|
||||
}
|
||||
},
|
||||
reportFailure(context, error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { notifyNativeReviewResponse } from "./native-review-response-notifications.js";
|
||||
import { notifyNativeResponsePresentation } from "./native-response-presentation-notifications.js";
|
||||
import { notifyChatConfirmationWork } from "./chat-confirmation-notifications.js";
|
||||
import { notifyChatInboundWakeNotices } from "./chat-inbound-wake-notice-notifications.js";
|
||||
@@ -9,6 +10,8 @@ import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notificatio
|
||||
export type ChatRunMilestoneScope = { companyId: string; issueId?: string; runId?: string };
|
||||
|
||||
export async function notifyChatRunMilestones(tx: object, scope: ChatRunMilestoneScope) {
|
||||
await notifyNativeReviewResponse(tx, { companyId: scope.companyId, issueId: scope.issueId,
|
||||
runId: scope.issueId ? undefined : scope.runId });
|
||||
await notifyNativeResponsePresentation(tx, { companyId: scope.companyId, issueId: scope.issueId,
|
||||
runId: scope.issueId ? undefined : scope.runId });
|
||||
await notifyDeliveryWork(tx, DELIVERY_QUEUES.chatGitHubReviewChecks, JSON.stringify({ companyId: scope.companyId }));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { notifyNativeReviewResponse } from "./native-review-response-notifications.js";
|
||||
import { notifyNativeResponsePresentation } from "./native-response-presentation-notifications.js";
|
||||
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
|
||||
|
||||
export type NativeFinalizationWorkScope = { companyId: string; runId?: string };
|
||||
export type NativeFinalizationWorkScope = { companyId: string; runId?: string; issueId?: string };
|
||||
|
||||
export async function notifyNativeFinalizationWork(transaction: object, scope: NativeFinalizationWorkScope) {
|
||||
await notifyNativeResponsePresentation(transaction, scope);
|
||||
await notifyNativeReviewResponse(transaction, scope);
|
||||
await notifyDeliveryWork(transaction, DELIVERY_QUEUES.nativePendingEffects,
|
||||
JSON.stringify({ companyId: scope.companyId, runId: scope.runId }));
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import type {
|
||||
|
||||
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
||||
import { finalizeNativeRun, recoverCommittedNativeReviewResponse } from "./native-run-finalizer.js";
|
||||
import { configureAgentLifecycle, createAgentLifecycle, processAgentLifecycle } from "../agent-lifecycle.js";
|
||||
import { approvalRecords } from "../approvals.js";
|
||||
import { issueApprovalService } from "../issue-approvals.js";
|
||||
import { nativeReviewResponseWork } from "./native-review-response-work.js";
|
||||
@@ -2937,6 +2938,30 @@ describe("native external-chat response wait", () => {
|
||||
} finally { await coordinator.stop(); }
|
||||
});
|
||||
|
||||
it("wakes dormant governed response recovery after real agent resume", async () => {
|
||||
const fixture = await seedRecoverableReviewResponse();
|
||||
await db.update(agents).set({ status: "paused", lifecycleState: "paused", lifecycleHolds: ["manual"], lifecycleOperation: null })
|
||||
.where(eq(agents.id, fixture.agentId));
|
||||
const errors: unknown[] = [];
|
||||
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError: error => errors.push(error) });
|
||||
const worker = nativeReviewResponseWork(db).register(coordinator, () => true);
|
||||
const lifecycle = configureAgentLifecycle(db, {
|
||||
requiredPluginIds: async () => [], runHost: async () => "complete", runPlugin: async () => "complete",
|
||||
});
|
||||
try {
|
||||
await worker.ready;
|
||||
await expect.poll(() => ({ active: idleWorkSnapshot().active, next: coordinator.nextWakeAt() }),
|
||||
{ timeout: 10_000 }).toEqual({ active: 0, next: null });
|
||||
expect(await db.select().from(issueComments).where(eq(issueComments.createdByRunId, fixture.runId))).toHaveLength(0);
|
||||
await createAgentLifecycle(db).resumeAgent(fixture.agentId);
|
||||
await processAgentLifecycle(db, fixture.agentId);
|
||||
await expect.poll(async () => (await db.select().from(agents).where(eq(agents.id, fixture.agentId)))[0].status).toBe("idle");
|
||||
await expect.poll(async () => (await db.select().from(issueComments).where(eq(issueComments.createdByRunId, fixture.runId))).length,
|
||||
{ timeout: 5_000 }).toBe(1);
|
||||
expect(errors).toEqual([]);
|
||||
} finally { await lifecycle.stop(); await coordinator.stop(); }
|
||||
});
|
||||
|
||||
it("reconstructs scheduled governed responses at startup and leaves their history quiet", async () => {
|
||||
const fixture = await seedRecoverableReviewResponse();
|
||||
const errors: unknown[] = [];
|
||||
|
||||
@@ -1238,7 +1238,7 @@ async function materializeDecisionEffect(input: {
|
||||
)
|
||||
.for("update");
|
||||
if (pending.length === 0) throw new Error("native_pending_effect_missing");
|
||||
await notifyNativeFinalizationWork(input.tx, { companyId: input.companyId });
|
||||
await notifyNativeFinalizationWork(input.tx, { companyId: input.companyId, issueId: input.issue.id });
|
||||
await input.tx
|
||||
.update(statusDecisionEffects)
|
||||
.set({
|
||||
|
||||
@@ -2617,7 +2617,7 @@ export function createToolGatewayService(
|
||||
approvalId: approval.id,
|
||||
linkedByAgentId: input.session.agentId,
|
||||
})
|
||||
.onConflictDoNothing());
|
||||
.onConflictDoNothing(), { issueIds: [approvalIssueId] });
|
||||
}
|
||||
|
||||
const interaction = await interactions.create(
|
||||
|
||||
Reference in new issue
Block a user