diff --git a/doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md b/doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md index eeb159a8e41..bac57a86133 100644 --- a/doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md +++ b/doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md @@ -627,3 +627,38 @@ Fresh origin/master inspected at `8c6bd5a83b`; reconciliation of the stack remai outstanding. Publication/interaction, conversation/configuration and monitor receipt producers remain under audit. Production registration and periodic-loop removal are still pending, so this checkpoint is not a completed migration PR. + +## Monitor receipt ordering checkpoint + +The normal native monitor owner does not need another milestone signal when it +saves a schedule. `PaperclipRunnerToolAuthority.#setTaskMonitor` writes the issue +monitor and its semantic tool receipt inside the same `#withMutationReceipt` +transaction. The future deadline cannot be consumed before that commit. A later +monitor wake's real queue admission already signals the issue scope, and the +projector validates the committed receipt through `readNativeMonitorSource`. +Do not add a redundant signal to the generic semantic tool wrapper merely because +the projector reads a receipt. + +The new test executes the actual `set_task_monitor` tool authority, then admits +the due wake through the real heartbeat queue after the source has finished. It +checks the queued notice and idle quiet. Source completion is fixture-controlled +and provider execution is held by task drain; this is receipt/admission/lineage +coverage, not full monitor scheduler or harness acceptance. Alternate monitor +wake rewrites and recovery paths still require audit before the whole producer +family is declared complete. + +The initial proposed late-receipt test seeded an admitted wake before creating +its future monitor, which did not represent the normal ordering. Its first run +also failed the actual future-deadline validator after advancing the test clock +(`/tmp/sleep28-monitor-receipt-before.log`). That failure was a fixture problem, +not a production regression. The replacement follows the real ownership order +instead of manufacturing a missing signal for an unsupported ordering. + +- `/tmp/sleep28-monitor-admission-tests.log`: targeted real producer path passes. +- `/tmp/sleep28-monitor-final-tests.log`: all 43 milestone worker cases pass. +- No production monitor code changed; `git diff --check` passed. + +Remote gates refreshed at this checkpoint: #17490 at `31bcd1988823` still has +failed E2E shard 3 and aggregate checks; #17590 at `e501d6c99d72` still has queued +Paperclip Review; #17592 at `f53ae3d6d27c` still has neutral Paperclip Review. +These are not claimed ready and no merge was attempted. diff --git a/server/src/__tests__/chat-run-milestone-work.test.ts b/server/src/__tests__/chat-run-milestone-work.test.ts index 15e6bb0d3c7..91d9208cd55 100644 --- a/server/src/__tests__/chat-run-milestone-work.test.ts +++ b/server/src/__tests__/chat-run-milestone-work.test.ts @@ -21,6 +21,7 @@ import { CHAT_CONTROL_RECOVERY_ADMISSION_KEY, CHAT_CONTROL_RECOVERY_UNRESOLVED_C import { heartbeatService } from "../services/heartbeat.js"; import { commitNativeStatusDecision, dispatchPendingNativeStatusEffects } from "../services/native-runtime/status-decision-committer.js"; import { claimQueuedNativeReviewRun } from "../services/native-runtime/native-review-dispatch.js"; +import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js"; import { finalizeNativeRun } from "../services/native-runtime/native-run-finalizer.js"; import { idleWorkSnapshot } from "../services/task-admission.js"; @@ -385,6 +386,36 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu await service.releaseRunClaimedJustBeforeSuppression(seeded.id); await quiet(); }); + it("wakes an admitted monitor through its previously committed source receipt", async () => { + const source = await run(f, { runtimeMode: "native", nativeIssueId: f.issueId }); + await db.update(issues).set({ executionRunId: source.id }).where(eq(issues.id, f.issueId)); + await start(); await flush(); await quiet(); + const nextCheckAt = new Date(Date.now() + 60_000).toISOString(); + const authority = new PaperclipRunnerToolAuthority(db, { ...f, runId: source.id }); + const request = { tool: "set_task_monitor", callId: randomUUID(), arguments: { + idempotencyKey: "milestone-monitor", monitor: { nextCheckAt, notes: "Check the saved task" }, + } }; + await expect(authority.execute(request)).resolves.toMatchObject({ taskId: f.issueId, replayed: false }); + // The monitor and source receipt commit together, before due admission. This + // fixture finishes the source without executing a harness; admission below + // uses the real queue, with execution held by task drain. + await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, source.id)); + await db.update(issues).set({ executionRunId: null }).where(eq(issues.id, f.issueId)); + await quiet(); + const { service, dependencies } = await queue(); + dependencies.getSchedulingSuppression.mockResolvedValue({ suppressed: true, reason: "task_drain" }); + const admitted = await service.enqueueWakeup(f.agentId, { source: "automation", reason: "issue_monitor_due", + idempotencyKey: `issue-monitor:${f.issueId}:${nextCheckAt}`, + payload: { issueId: f.issueId, nextCheckAt }, + contextSnapshot: { issueId: f.issueId, source: "issue.monitor" }, + requestedByActorType: "user", requestedByActorId: f.userId, + }); + expect(admitted).toMatchObject({ status: "queued" }); + await flush(); + expect((await publications()).map(row => row.idempotencyKey)).toContain(`run:${admitted!.id}:queued:${f.endpointId}`); + expect(dependencies.executeRun).not.toHaveBeenCalled(); + await quiet(); + }); async function nativeFixture(phase = "committed") { const contractId = randomUUID(), resultId = randomUUID(); await db.insert(completionContracts).values({