Files
PaperClipAI/server
3d3b8d4371 fix(external-objects): distinguish hidden GitHub repos from genuinely-missing objects (#15906)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issues reference external objects such as GitHub pull requests. The
instance resolves their status and shows it in the UI.
> - GitHub returns 404 both when a pull request or issue does not exist
and when the repository exists but the configured credentials cannot see
it. GitHub hides private repos behind a 404 on purpose.
> - The resolver treated every 404 as a terminal not-found verdict. It
froze references to inaccessible repos forever, and marked them "Not
found (Stale)".
> - This pull request probes the repository metadata to tell the two
cases apart and keep frozen objects re-checkable.
> - The benefit is accurate statuses instead of permanent "Not found"
labels for repos the instance cannot see.

## Linked Issues or Issue Description

No public GitHub issue exists for this report.

**What happened?**
- The issue properties panel and markdown decoration showed many GitHub
pull request and issue references as "Not found (Stale)" even though the
objects exist.
- GitHub returns 404 for objects the configured credentials cannot
access, not only for objects that are truly gone.
- The resolver treated every 404 as a terminal not-found verdict, so
those references were frozen and showed a misleading "stale" liveness.
- Detected-but-unresolved GitHub objects were labeled "Not yet resolved"
in inline markdown, which did not match the pill label "Not yet
refreshed".

**Expected behavior**
- A reference to a repo the credentials cannot see should surface as
authorization-required, not as a permanent "Not found".
- A truly missing object should still be "Not found".
- A terminal object (merged, closed, or confirmed not-found) should not
be labeled "Stale", because it is never refreshed again.
- Inline markdown statuses should match the pill statuses.

**Steps to reproduce**
1. Configure GitHub credentials with no access to a private repo.
2. Add a comment or issue description that links to a pull request in
that repo.
3. Wait for the external object resolver to run.
4. Observe the reference labeled "Not found (Stale)".

**Paperclip version or commit**
- `master` at `e733bdd` (before this change).

**Deployment mode**
- Self-hosted server.

## What Changed

- Added a repository visibility probe to the GitHub external object
provider.
- A 404 for an object now probes the repository metadata with the same
credentials:
- Repository visible: the object genuinely is gone. Keep the not-found
snapshot.
- Repository hidden: return an authorization-required result instead of
a terminal not-found verdict.
- Probe failed (network, rate limit, 5xx): return an unreachable result
and retry later.
- A rate-limited repository probe (403 with `x-ratelimit-remaining: 0`)
is treated as uncertain, not as missing access, so it retries instead of
showing a misleading "Requires auth".
- Re-check previously frozen GitHub not-found objects on their normal
schedule so they self-heal once credentials or repo visibility improve.
A frozen object is only re-checked until a probe confirms the repo is
visible; once confirmed deleted, it is terminal and no longer polled.
- Cleared any terminal verdict when a refresh fails, so a failed refresh
does not freeze an object.
- Kept terminal objects from displaying the "stale" liveness label.
- Matched the inline markdown status label to the pill status label, so
detected-but-unresolved GitHub objects say "Not yet refreshed" instead
of "Not yet resolved".
- Skipped the liveness suffix in markdown links and status icons when it
duplicates the status label (e.g. no more "Requires auth (Requires
auth)").
- Cached repository visibility probes per company+repo + credential
fingerprint, so many objects in the same repo share one probe per
refresh cycle instead of each issuing its own — and a verdict obtained
under one token is never reused by a later refresh running under a
different credential.

## Verification

- Added provider tests for visible, hidden, failed, and rate-limited
repository probes, per-company/per-repo probe caching, and
probe-verified not-found snapshots.
- Added service tests for terminal-object liveness, for re-checking
frozen GitHub not-found objects, and for ensuring probe-verified
not-found objects are never re-queued.
- Added UI tests asserting unresolved GitHub objects render as "Not yet
refreshed" (not "Not yet resolved") and that failed-first-refresh labels
do not repeat the liveness text.
- Ran `npx vitest run src/__tests__/external-objects-service.test.ts` in
`server/`: 34 passed (incl. provider + service suites).
- Ran the external-object markdown/pill/icon suites in `ui/`: 97 passed.
- `ci / Typecheck + Release Registry` and all other CI gates pass on
this head. Local `tsc` could not run to completion in the execution
sandbox (the sandbox memory cap killed it), so CI typecheck is the
authoritative gate.

## Risks

- Low risk. The change is confined to the GitHub external object
provider and the external object service.
- A repo that genuinely does not exist and a private repo both return
404 for the probe. Both now surface authorization-required or a retry
instead of an immediate not-found freeze, which stays re-checkable and
self-heals.
- Re-checking frozen not-found objects adds at most one repository probe
per affected object until the probe produces a definitive verdict, after
which the object is terminal and no longer polled. Probe results are
cached per company+repo+credential, so a credential rotation always
re-probes instead of inheriting a stale verdict.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Provider: DeepSeek via OpenRouter, model ID
`openrouter/~deepseek/deepseek-v4-flash-latest`, running inside the
Paperclip opencode adapter (`opencode_local`).
- The model produced and assisted with the reasoning and code changes;
the execution workspace, test suite, and git/PR steps above were run by
the same agent run.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
— the branch name is assigned by the managed execution workspace and the
harness requires keeping it
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green on the current head (all `ci /
*`, Greptile, Socket, and review checks pass)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-11 11:53:21 -07:00
..