Skip to content

feat(server): durable PR monitor feedback delivery and reporting - #182

Merged
ronak-guliani merged 3 commits into
base/pr-monitor-v2from
feat/pr-monitor-b-feedback-reporting
Aug 12, 2026
Merged

feat(server): durable PR monitor feedback delivery and reporting#182
ronak-guliani merged 3 commits into
base/pr-monitor-v2from
feat/pr-monitor-b-feedback-reporting

Conversation

@ronak-guliani

Copy link
Copy Markdown
Owner

Summary

Stack PR B (phases 3–4), stacked on A (feat/pr-monitor-a-contracts-observe).

Behavior

  • Durable feedback items, revisions, deliveries, reports, and per-monitor debounce/circuit state (migration 051)
  • Ingest actionable monitor events into stable feedback keys with revisions
  • Debounced batching (15s) + queue-mode V2 delivery via ThreadManagementService.sendToThread (mode: "queue") — never steers active turns
  • Fresh revalidation before delivery (open state, head SHA, monitor status, owner thread)
  • Deterministic command/message/delivery IDs for logical exactly-once receipts
  • Circuit breaker after consecutive delivery failures
  • MCP tools: t3_pr_monitor_context, t3_pr_monitor_report (accepted/rejected/resolved/needs-human) with immediate recheck
  • Status RPC + UI strip audit for open feedback, deliveries, reports

Migrations

  • 051_PullRequestMonitorFeedback

Tests

  • apps/server/src/pullRequestMonitor/* including wakePrompt + service status feedback fields

Stack

  1. A: feat(server): durable observe-only PR monitor foundation #181
  2. This PR (B) → base A
  3. C ownership + handoff (next)
  4. D fallback threads (next)

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 11, 2026
Base automatically changed from feat/pr-monitor-a-contracts-observe to base/pr-monitor-v2 August 11, 2026 19:37
Add feedback items/revisions/deliveries with debounce and circuit breakers,
queue-mode V2 delivery via ThreadManagementService, MCP context/report tools,
status audit fields, and migration 051. Never steers active turns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ronak-guliani
ronak-guliani force-pushed the feat/pr-monitor-b-feedback-reporting branch from 65d74d3 to 6805485 Compare August 11, 2026 19:37
@ronak-guliani
ronak-guliani requested a balanced review from Copilot August 11, 2026 19:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds durable PR-monitor feedback ingestion, queued delivery, reporting, and audit visibility across server, MCP, contracts, and web UI.

Changes:

  • Persists feedback revisions, deliveries, reports, debounce, and circuit state.
  • Adds queued thread delivery plus MCP context/report tools.
  • Exposes feedback status through RPC and the web monitor strip.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/contracts/src/rpc.ts Adds context/report RPC contracts.
packages/contracts/src/pullRequestMonitor.ts Defines feedback schemas and status fields.
packages/client-runtime/src/state/pullRequests.ts Adds context/report client operations.
apps/web/src/components/pullRequest/PullRequestMonitorStrip.tsx Displays feedback audit summaries.
apps/server/src/ws.ts Registers new RPC handlers.
apps/server/src/server.ts Wires the feedback service layer.
apps/server/src/pullRequestMonitor/wakePrompt.ts Builds bounded delivery prompts.
apps/server/src/pullRequestMonitor/wakePrompt.test.ts Tests prompt formatting.
apps/server/src/pullRequestMonitor/PullRequestMonitorService.ts Integrates feedback and reporting.
apps/server/src/pullRequestMonitor/PullRequestMonitorService.test.ts Updates service status tests.
apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackStore.ts Implements feedback persistence.
apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts Implements ingestion and delivery workflow.
apps/server/src/persistence/Migrations/051_PullRequestMonitorFeedback.ts Creates feedback tables and indexes.
apps/server/src/persistence/Migrations.ts Registers migration 051.
apps/server/src/mcp/toolkits/orchestrator/tools.ts Declares monitor MCP tools.
apps/server/src/mcp/toolkits/orchestrator/handlers.ts Implements monitor MCP handlers.
apps/server/src/auth/RpcAuthorization.ts Assigns RPC authorization scopes.
Suppressed comments (7)

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:156

  • The primary web start path sends no ownerThreadId (PullRequestMonitorStrip.tsx:83-86), so this return silently discards every actionable event for those monitors. Ownership should gate materialization/delivery, not durable ingestion; otherwise the new feedback audit remains empty and later ownership cannot recover events already consumed by the monitor cursor.
        if (!input.monitor.ownerThreadId) return;

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:454

  • This uses the monitor's current head SHA rather than the head_sha recorded on each pending revision. If the head changes during the debounce window, a later poll updates monitor.headSha, this batch is keyed to the new SHA, and fresh revalidation passes while still delivering revisions from the old head. Load the revision heads and suppress or split batches whose revisions do not match the fresh head.
        const ids = stableDeliveryIds({
          monitorId: monitor.id,
          threadId: monitor.ownerThreadId,
          revisionIds,
          headSha: monitor.headSha ?? "unknown",
        });

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:511

  • This starts a second background fiber for the same non-claimed flush already run every five seconds above. The fibers can select the same due delivery concurrently; a late failure can overwrite a successful delivered update with failed, causing needless retries. Keep one flusher or add an atomic claim/lease before allowing overlapping workers.
    yield* Stream.fromSchedule(Schedule.spaced(Duration.seconds(20))).pipe(
      Stream.mapEffect(() => flushDueDeliveries),
      Stream.runDrain,
      Effect.forkScoped,
      Effect.interruptible,

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:357

  • The snapshot was freshly fetched, but the prompt status comes from the previously polled monitor record. Checks, reviews, or mergeability can change without a head change, so delivery can tell the agent a stale blocker/readiness state. Recompute readiness from snapshot (with the appropriate thread-version baseline) before building the prompt.
        const readiness = monitor.readiness ?? {
          ready: false,
          label: "blocked" as const,
          blockers: [{ kind: "checks-missing" as const }],
        };

apps/server/src/mcp/toolkits/orchestrator/handlers.ts:125

  • This mutation is not scoped to the invoking MCP thread/project, and the caller-provided reporterThreadId can override scope.threadId. An agent can therefore mutate another monitor's feedback and forge the audit attribution. Validate the monitor against the invocation scope and always derive the reporter ID from scope.
      const scope = yield* McpInvocationContext;
      const monitors = yield* PullRequestMonitorService;
      return yield* monitors.report({
        ...input,
        reporterThreadId: input.reporterThreadId ?? scope.threadId,
      });

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:558

  • The disposition update commits before the required audit report is inserted. If the insert fails, the feedback item has changed state with no report, while the caller receives an error and may retry. Persist the disposition and report in one transaction to preserve the promised durable audit trail.
        yield* feedbackStore.setDisposition({
          itemId: item.id,
          disposition: input.disposition,
          note: input.note ?? null,
          at: now,

apps/server/src/pullRequestMonitor/PullRequestMonitorFeedbackService.ts:494

  • The due list can contain multiple batches for one monitor, but they are delivered concurrently while sharing one deliveryFailureCount/circuit state. Two failures can both read the same count and collapse into one, or a success and failure can race to leave either final state; newer feedback may also be queued before older feedback. Serialize deliveries per monitor while retaining concurrency across different monitors.
          const due = yield* feedbackStore.listDueDeliveries(now, 16);
          yield* Effect.forEach(due, deliverOne, { concurrency: 2 });

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +360 to +364
yield* feedback
.ingestSnapshot({
monitor: updated,
snapshot,
readiness,
Comment on lines +422 to +426
yield* store.update({
...monitor,
nextPollAt: now,
updatedAt: now,
});
Comment on lines +113 to +116
t3_pr_monitor_context: (input) =>
Effect.gen(function* () {
const monitors = yield* PullRequestMonitorService;
return yield* monitors.context(input);
export const PullRequestMonitorContextResult = Schema.Struct({
monitor: Schema.NullOr(PullRequestMonitorRecord),
latestSnapshot: Schema.NullOr(PullRequestMonitorSnapshot),
openItems: Schema.Array(PullRequestMonitorFeedbackItem),
Comment on lines +151 to +154
const ingestSnapshot: (typeof PullRequestMonitorFeedbackService.Service)["ingestSnapshot"] = (
input,
) =>
Effect.gen(function* () {
Comment on lines 190 to +193
assert.isNotNull(status.monitor);
assert.isNotNull(status.latestSnapshot);
assert.strictEqual(status.latestSnapshot?.headSha, "deadbeef");

const stopped = yield* service.stop({ monitorId: started.monitor.id });
assert.strictEqual(stopped.monitor.enabled, false);
assert.strictEqual(stopped.monitor.status, "stopped");
assert.isArray(status.openFeedback);
assert.isArray(status.recentDeliveries);
assert.isArray(status.recentReports);
@ronak-guliani

Copy link
Copy Markdown
Owner Author

Review follow-up

Addressed on tip branch feat/pr-monitor-d-fallback-maintenance (commit stacked on D so the full stack tip is correct):

  • Feedback reopen now clears disposition fields on conflict upsert
  • Cursor advances only after successful feedback ingest
  • Post-report recheck uses ownership-safe scheduleRecheck (no full-row clobber)
  • MCP context/report scoped to invoker project; context result field renamed items
  • Added reopen/disposition coverage in monitor service tests
  • Restored start/status/stop snapshot assertions

Stack tip: #184

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ronak-guliani

Copy link
Copy Markdown
Owner Author

PR B review disposition (assessed against stack design + heads)

# Finding Class Notes
1 Cursor advances before ingest (1) B blocker Still on feat/pr-monitor-b-feedback-reporting. Fixed on tip D (ingest then updatePollState).
2 No ownerThreadId skips ingest (1) B blocker Delivery correctly waits for owner (C), but items/revisions must still durably ingest. Fixed on tip D.
3 Pending batch uses monitor head vs revision head (3) not a blocker Fresh revalidation intentionally uses current head before delivery. Batch key including current head is acceptable; mixed-head pending is edge polish, not lost-event correctness.
4 MCP context/report lack project scope / attribution (1) B blocker (not C) B introduces these MCP tools; C ownership does not provide project ACL. Cross-project read is B’s surface. Fixed on tip D (invoker project gate; report defaults reporter to invoker).
5 Disposition + report non-transactional (1) B blocker Phase-4 audit integrity. Fixed on tip D (reportDisposition transaction).
6 Recheck full-row store.update (1) B blocker Poll race. Fixed on tip D (scheduleRecheck / poll-only columns).
7 Concurrent due deliveries race circuit (1) B blocker concurrency: 2 across monitors is fine; same-monitor races are not. Fixed on tip D (serial per monitor, parallel across).
8 Missing focused tests / dropped snapshot+stop asserts (1) soft B / quality B has reopen + pending-revision atomicity tests. Broader delivery/circuit suite still thin. Snapshot/stop asserts restored on tip. Not a functional correctness hole if (1)(2)(5)(6) hold.
9 openItems includes closed (1) B blocker Contract lie. Fixed on tip D → items.
10 Reopen disposition clear (8aacbca) (3) resolved on B B head already clears disposition columns on upsert + has store test.

Deferred?

  • Not deferred to C: MCP scope, durable ingest without owner, cursor/ingest ordering, report transaction, recheck clobber, items rename, delivery serialization — all phase 3–4 / B.
  • C still owns: auto-associate owner on PR create, transfer/handoff, single modifying owner, review-thread link (not a substitute for MCP project scope).
  • D still owns: fallback launch/worktree/force policy.

Transactional-state / PR size

  • Real blockers are the durability races above, not “make the whole poll path one mega-transaction.”
  • Snapshot save + ingest + cursor should be failure-ordered (cursor last); full single-TX across provider fetch is unnecessary.
  • PR size guidance is slicing advice, not a defect.

Where code is

  • PR B head (feat/pr-monitor-b-feedback-reporting): still has 1,2,4,5,6,7,9 as landed in the original B commits (+10 fixed in 8aacbca).
  • Stack tip D includes review fixes: 6127ae4f8, 9f4be1c1c.

Recommend rebasing C→D onto an updated B (or merging tip-first) so intermediate PR diffs match tip correctness.

Cursor advances only after feedback ingest, durable ingest without
owner, MCP project scope, transactional disposition reports, poll-only
recheck updates, per-monitor delivery serialization, and items rename.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ronak-guliani

Copy link
Copy Markdown
Owner Author

Back-port landed

cec92c6dd on feat/pr-monitor-b-feedback-reporting addresses the remaining B blockers (cursor-after-ingest, owner-less durable ingest, MCP project scope, transactional reports, poll-only recheck, per-monitor delivery serialization, items rename, restored snapshot/stop asserts).

C and D were rebuilt onto this tip. #182 is ready to merge once CI is green (merge into its base / stack order A→B).

@ronak-guliani
ronak-guliani merged commit 3d4214e into base/pr-monitor-v2 Aug 12, 2026
7 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants