Skip to content

fix(agents): tell the truth about inbox-lite status filter (BLO-18858) - #1117

Merged
kkroo merged 1 commit into
masterfrom
codex/reopen-pr-821
Aug 11, 2026
Merged

fix(agents): tell the truth about inbox-lite status filter (BLO-18858)#1117
kkroo merged 1 commit into
masterfrom
codex/reopen-pr-821

Conversation

@kkroo

@kkroo kkroo commented Aug 6, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work.
  • Agent selection starts from paperclipInboxLite, which is intentionally the routine heartbeat inbox rather than a full assignment sweep.
  • BLO-18858 showed the tool description overstated that inbox by saying in_review work was returned.
  • That mismatch caused an agent to treat a valid empty inbox as platform failure and bypass checkout with an unscoped list sweep.
  • Checkout is the atomic run-scoped lock boundary, so the correct fix is to align the agent contract and allow named in_review wakes to claim work through checkout.
  • This pull request pins that contract in docs, MCP/OpenAPI descriptions, and focused regression tests.
  • The benefit is less duplicate agent work without weakening the lock that protects concurrent runs.

Linked Issues or Issue Description

Paperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-18858
Related incident: https://paperclip.blockcast.net/PCL/issues/PCL-2063

What happened?

paperclipInboxLite described in_review assignments as part of its returned work, but the route correctly filters routine heartbeat selection to todo, in_progress, and blocked. An agent with only in_review assignments received [], interpreted that as a bad platform response, then used an unscoped issue-list sweep that had no checkout-lock awareness.

Expected behavior

Agents should understand that an empty routine inbox is actionable only for unscoped heartbeat selection. Named task, comment, interaction, approval, monitor, continuation, and recovery wakes should process their named issue and claim it through checkout before work.

Steps to reproduce

  1. Assign an agent only in_review work.
  2. Trigger an unscoped heartbeat inbox fetch.
  3. Observe that paperclipInboxLite returns [] even though the stale description implied in_review work would be listed.
  4. Trigger a named in_review wake and verify the agent contract directs a scoped read plus checkout instead of an unscoped sweep.

Paperclip version or commit

Regression documented against the PR base/current master around 0b099719; the branch is now updated against current master.

Deployment mode

Self-hosted server / local dev routes. This is not adapter-specific; it affects the Paperclip agent heartbeat contract and MCP tool description.

What Changed

  • Corrected paperclipInboxLite documentation in the skill, MCP tool description, and OpenAPI summary so it names the actual routine status set and distinguishes unscoped heartbeat selection from named issue wakes.
  • Updated the bundled issue-triage skill and regenerated the skills catalog manifest so triage can use a read-only, explicitly in_review-scoped list before checkout.
  • Removed the unlocked in_review checkout rejection so assigned in_review issues can be atomically claimed into in_progress with checkout and execution run IDs.
  • Added route/service coverage for the inbox status contract, unlocked in_review checkout, same-agent live-lock conflicts, and expected-status conflict behavior.

Verification

  • pnpm exec vitest run server/src/__tests__/agent-inbox-lite-status-contract.test.ts server/src/__tests__/issues-service.test.ts server/src/__tests__/issue-stale-execution-lock-routes.test.ts --reporter=dot passed non-DB assertions locally; embedded Postgres suites skipped on this host because embedded Postgres cannot initialize here.
  • pnpm --filter @paperclipai/server typecheck
  • pnpm --filter @paperclipai/skills-catalog exec vitest run src/shipped-catalog.test.ts --reporter=dot
  • pnpm --filter @paperclipai/skills-catalog typecheck
  • git diff --check

Risks

Low to moderate risk. The behavioral change is intentionally narrow: unlocked assigned in_review issues become checkoutable so the documented lock-aware claim path works. The main risk is any caller that relied on the old 422 issue_in_review_not_checkoutable response, but the updated contract and tests make checkout the required concurrency boundary.

Model Used

OpenAI Codex, GPT-5, with repository file access, GitHub CLI/API tool use, and local command execution.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

Replacement PR

@allyblockcast

allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: PCL-2063
🔗 Paperclip issue: BLO-18858

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@allyblockcast

allyblockcast Bot commented Aug 6, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: PCL-2063
🔗 Paperclip issue: BLO-18858

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved the independently authored replacement. It carries #821 unchanged, including the reviewed lock-aware inbox/checkout behavior and full green CI from the exact code tree.

@kkroo
kkroo force-pushed the codex/reopen-pr-821 branch from e6fcbcb to 17dcc3f Compare August 6, 2026 22:41
@kkroo
kkroo enabled auto-merge August 6, 2026 23:05

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved the rebased independent replacement on this exact head. The skills catalog was regenerated and validated; inbox contract tests pass 4/4.

@kkroo
kkroo disabled auto-merge August 8, 2026 00:02
@kkroo
kkroo enabled auto-merge August 8, 2026 00:03

@allyblockcast allyblockcast Bot 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8d39f06

Critical Issues (1)

  • [native-codex] server/src/services/issues.ts:9232 — Checkout can seize an in_review issue assigned to a human. The predicate permits every row where assigneeAgentId is null, even when assigneeUserId is set; the update then assigns the caller and clears assigneeUserId at lines 9213-9214. Thus the new in_review checkout path silently removes a human review handoff. The added route test expecting a 409 for this case should expose the failure.
    • Require both assignee columns to be null before claiming an unassigned issue, or otherwise explicitly reject rows with assigneeUserId set; retain a regression test for the atomic update path.

Important Issues (0)

Suggestions (0)

Strengths

  • The status-filter documentation, MCP descriptions, and focused contract tests consistently distinguish routine inbox selection from named wakes.
  • The pending execution-policy stage preserves in_review through checkout, avoiding an accidental transition before its decision is recorded.

Recommended Action

  1. Fix the Critical issue before merge.
  2. Address Important issues this cycle.
  3. Consider Suggestions opportunistically.

@allyblockcast allyblockcast Bot 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 82325d6

Prior Findings Dispositioned (1)

  • prior:8d39f06 critical 1 — fixed — server/src/services/issues.ts:9192 — The checkout predicate now requires both assigneeAgentId and assigneeUserId to be null before claiming an unassigned row, so the atomic update cannot remove a human review handoff.

Critical Issues (0)

Important Issues (1)

  • [gstack/review] server/src/__tests__/agent-inbox-lite-status-contract.test.ts:85 — The newly added general-server suite is absent from the checked-in duration manifest. This drops coverage to 359/399 (89.97%), below the 90% assertion in scripts/__tests__/run-vitest-stable-shard.test.mjs:134; policy fails and skips all build, typecheck, and test lanes.
    • Regenerate and commit the Vitest duration manifest so this suite has a recorded duration and the required lanes can run.

Suggestions (0)

Strengths

  • The inbox-lite contract is now consistent across MCP, OpenAPI, skills, and focused route coverage.
  • The human-assignee guard fixes the prior atomic checkout regression without weakening named-review wake handling.

Recommended Action

  1. Fix Critical issues before merge.
  2. Address Important issues this cycle.
  3. Consider Suggestions opportunistically.

@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown

Heads-up on an overlap neither of us could have seen at the time: #1117 and #1116 were opened one minute apart (22:35Z and 22:36Z on 2026-08-06) and both target BLO-18858. I only found #1117 today, doing the dedup search that #1116's review gate demanded.

I am not touching this PR — flagging the collision so whoever sequences it does so deliberately.

They contradict each other on one point

Both are defensible designs. They are not simultaneously true, and whichever lands second leaves the skills describing behaviour the service does not have — which is precisely the defect BLO-18858 was filed about.

They also collide textually: both touch skills/paperclip/SKILL.md, issue-triage/SKILL.md, generated/catalog.json, packages/mcp-server/src/tools.ts, server/src/routes/openapi.ts, and both add server/src/__tests__/agent-inbox-lite-status-contract.test.ts.

Current state of each, for whoever decides

#1117 #1116
author kkroo (human — can carry an Ally approval) app/allyblockcast (cannot)
mergeable CONFLICTING / DIRTY MERGEABLE as of last computation
Ally at exact head COMMENTED 82325d6d — 1 Important open (duration manifest) COMMENTED 2572749230 Critical / 0 Important / 0 Suggestions
approvals 2 × APPROVED, both stale (e6fcbcb5, 8d39f067) none
checks policy red → build/typecheck/test/e2e all skipped 4 red, reruns in flight
scope full tree of #821, incl. runtime + ReviewQueueCard.tsx docs/contract only, no runtime source

One concrete thing that may unblock your policy failure

Ally's Important here is the Vitest duration manifest: agent-inbox-lite-status-contract.test.ts is absent from scripts/general-server-shard-durations.json, dropping manifest coverage to 359/399 (89.97%) under the 90% floor asserted in scripts/__tests__/run-vitest-stable-shard.test.mjs:134, which fails policy and skips every downstream lane. #1116 adds the same suite and policy passes there, so the difference is base-suite count, not the suite itself — #1117's older base has one more suite than mine. Adding the entry (plus rebasing off the conflict) should clear it.

Separately and not blocking either PR: server/src/__tests__/heartbeat-queued-backlog-convergence.test.ts is also missing from that manifest and one of its tests runs ~138s, so it takes the median weight while being one of the heaviest suites in the lane. That is a latent shard-balance hazard on master, independent of both PRs.

My proposal, if it helps: land #1116 first (docs-only, no runtime source, Ally-clean at head), then rebase #1117 on top so the runtime change and the documentation that describes it move together and the skills are never describing a service that does not exist. Happy to do it the other way round — I would just rather it be a decision than an ordering accident. Raised on BLO-18858 too.

Rebase the approved inbox-lite, checkout, and stale-lock safeguards onto current master while preserving atomic checkout ownership and human assignment boundaries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@kkroo
kkroo force-pushed the codex/reopen-pr-821 branch from 82325d6 to ad27950 Compare August 10, 2026 23:05
@allyblockcast

allyblockcast Bot commented Aug 11, 2026

Copy link
Copy Markdown

Follow-up to my sequencing note above — I've now diffed the two properly, and the conclusion is simpler than "sequence them": #1117 supersedes #1116, and I'm closing #1116. Six of #1116's seven files are also in #1117, and #1117 carries the Step 3 inbox-lite prose essentially verbatim. #1116's one unique file is a doc-contract test that asserts the docs must never list in_review in expectedStatuses — which your removal of the 422 makes false. Merging it would fight this PR. Details on BLO-18858; branch is preserved and the PR is reopenable if you'd rather have it.

Before this lands, one residual defect in #1117 that I'd fix here, because it's the exact practice half of BLO-18858's root cause.

Step 4 still tells agents to hand-roll the lock check, and this PR now forbids that

This PR adds to Critical Rules:

Checkout before any work; never hand-roll the lock check. Inspecting executionRunId/executionLockedAt instead of calling checkout is a race, not a guardrail. See Step 5.

…and the matching Step 5 paragraph. But Step 4 is untouched, so after this merges the same document still says:

Before working an issue, confirm you are the run that holds it. Compare your own $PAPERCLIP_RUN_ID against the issue's executionRunId (or activeRun.id) […]

  • They differ and activeRun.status is running → […] Cede. […] Post a short comment noting the duplicate selection […]
  • activeRun is null → nobody is holding it.

That is the racy read your own new rule calls "a race, not a guardrail", and activeRun is null → nobody is holding it is precisely the false-authorization that produced the PCL-2063 duplicate. BLO-18858's first acceptance criterion is that lock-check-before-work is either documented or declared unnecessary — a contract that says both in one file satisfies neither.

Two smaller things in that same block: the advice to "post a short comment noting the duplicate selection" has the losing run mutate an issue another run owns (bumps lastActivityAt, can trigger a further wake, can enter the approval-comment state machine); and "inbox-lite withholds these issues server-side" stops being true for the reason stated once in_review is checkoutable.

Suggested replacement for that Step 4 block

**Selecting an issue is not claiming it — checkout is (Step 5).** Do not decide ownership by
comparing `$PAPERCLIP_RUN_ID` against the issue's `executionRunId` / `activeRun.id`. That read is
a race: the lock can be taken between your read and your first write, so a "nobody holds it"
answer can be stale by the time you act on it. Those fields are the lock's storage, not its API.

So: pick a candidate, then **let checkout be the ownership decision**. On `409`, you lost —
including to another live run of your own agent. Re-fetch the issue for diagnostics only and
**exit without mutating it**; do not post a "duplicate selection" comment, because a comment
bumps activity, can trigger another wake, and can enter the approval-comment state machine on an
issue another run owns. Read those fields when you want to *explain* a 409 in your own logs —
never to pre-authorize work.

Duplicate work is destructive rather than merely wasteful: under a shared worktree both runs edit
the same tree, and a routine `rm -rf node_modules` in one destroys the other's state mid-task.
Note that "no comments yet" is **not** evidence an issue is unworked — the holding run may be
minutes into its first pass and not have commented yet.

That text is lifted from #1116 (where it was already reviewed clean), and it drops cleanly into this PR — it makes no claim about the 422 either way, so it stays correct under your behavioural change.

Your call whether to take it here or let me open a follow-up once this lands; I'm not pushing to your branch either way. Flagging one more thing you may already know: this PR's checks are mid-run, and #1116 has been hitting an unrelated flake — issues-service.test.ts > issueService.addComment idempotency, Hook timed out in 20000ms, in a file #1116 doesn't touch. #1117 does touch that file (+213), so if you see it there it's worth a second look rather than an automatic re-run.

@kkroo
kkroo merged commit 8d586d4 into master Aug 11, 2026
12 of 17 checks passed
@kkroo
kkroo deleted the codex/reopen-pr-821 branch August 11, 2026 00:23
kkroo pushed a commit that referenced this pull request Aug 16, 2026
…tage (BLO-22666)

AC2: run-ownership was never asserted on an `in_review` row. ensureAgentCheckoutOwnership
early-returns on `status !== "in_progress"`, and the approval-shaped-comment branch gates
on identity only, so the SAME agent's second run could decide the stage run A was sitting
on — the BLO-18858 duplicate-work defect, reintroduced through the review path that #1117
opened up.

New `assertPendingReviewRunOwnership` is the `in_review` counterpart of
assertCheckoutOwner, kept separate rather than widening that one: assertCheckoutOwner is
keyed on `in_progress` in eight places and owns the stale-lock adoption ladder.

The fence is deliberately narrow — live-locked AND `assigneeAgentId === actor` AND the
stage is pending. By the time control reaches it the actor may legitimately be a
mention-granted peer, a manager-chain actor, a recovery owner, a human, or a drifted
`currentParticipant`; none hold the checkout and all must still approve. Widening past
the assignee term would break the approval-by-comment path this issue exists to protect,
which is a worse regression than the bug.

AC3: a pending stage pinned to a `currentParticipant` whose id has drifted off
`assigneeAgentId` could never take the lock, so its already-authorized stage decision
raced whoever held it. The claim writes ONLY the lock columns — assignee, status and
startedAt are untouched — so it cannot become a back door to general issue ownership, and
the participant pin is re-asserted inside the UPDATE as a compare-and-swap.
allyblockcast Bot pushed a commit that referenced this pull request Aug 19, 2026
…erage (BLO-24241)

scripts/general-server-shard-durations.json was missing entries for 3
suites (99.25% coverage) and the >=90% coverage assertion that gated
`policy` was a cliff: crossing it fails the whole job, which skips
build, typecheck, e2e and every test lane over a single missing JSON
entry (#1117 hit this at 89.97%).

- Measure and backfill the 3 missing suites, restoring 100% coverage.
- Replace the bare >=90% assertion with a much more generous
  HARD_FAIL_COVERAGE_FLOOR (0.75) that still warns (naming every
  missing suite and the one-line fix) on any drift without failing.
- Add check-shard-manifest-freshness.{mjs,test.mjs}: a strict
  100%-coverage assertion wired into pr.yml with continue-on-error, so
  drift stays visible as its own red X without cascading into `policy`.
- Add measure-general-server-shard-durations.mjs to actually remeasure
  suites (used by both the new check's one-line fix and the scheduled
  workflow below).
- Add .github/workflows/refresh-shard-manifest.yml: a weekly 4-shard
  measurement run that opens a PR with fresh durations, so the
  manifest no longer depends on a human noticing drift.
- Extract shared suite-enumeration logic (walk/toRepoPath/route-authz
  exclusion) out of run-vitest-stable.mjs into run-vitest-stable-suites.mjs
  so the new diagnostics enumerate suites the same way the real runner does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kkroo pushed a commit that referenced this pull request Aug 22, 2026
…erage (BLO-24241)

scripts/general-server-shard-durations.json was missing entries for 3
suites (99.25% coverage) and the >=90% coverage assertion that gated
`policy` was a cliff: crossing it fails the whole job, which skips
build, typecheck, e2e and every test lane over a single missing JSON
entry (#1117 hit this at 89.97%).

- Measure and backfill the 3 missing suites, restoring 100% coverage.
- Replace the bare >=90% assertion with a much more generous
  HARD_FAIL_COVERAGE_FLOOR (0.75) that still warns (naming every
  missing suite and the one-line fix) on any drift without failing.
- Add check-shard-manifest-freshness.{mjs,test.mjs}: a strict
  100%-coverage assertion wired into pr.yml with continue-on-error, so
  drift stays visible as its own red X without cascading into `policy`.
- Add measure-general-server-shard-durations.mjs to actually remeasure
  suites (used by both the new check's one-line fix and the scheduled
  workflow below).
- Add .github/workflows/refresh-shard-manifest.yml: a weekly 4-shard
  measurement run that opens a PR with fresh durations, so the
  manifest no longer depends on a human noticing drift.
- Extract shared suite-enumeration logic (walk/toRepoPath/route-authz
  exclusion) out of run-vitest-stable.mjs into run-vitest-stable-suites.mjs
  so the new diagnostics enumerate suites the same way the real runner does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants