Skip to content

feat(agent): serve stale decision packs with freshness marker and background rebuild#26

Merged
JSONbored merged 9 commits into
JSONbored:mainfrom
grnlemon:feat/decision-pack-stale-serving
May 29, 2026
Merged

feat(agent): serve stale decision packs with freshness marker and background rebuild#26
JSONbored merged 9 commits into
JSONbored:mainfrom
grnlemon:feat/decision-pack-stale-serving

Conversation

@grnlemon

Copy link
Copy Markdown
Contributor

Closes #15.

Summary

  • Adds an explicit freshness: "fresh" | "stale" | "rebuilding" | "missing" marker plus rebuildEnqueued: boolean to ContributorDecisionPack, the per-repo decision response, and DecisionPackRefreshNeeded.
  • Centralizes the staleness policy in a new loadContributorDecisionPackForServing helper used by the REST API (GET /v1/contributors/:login/decision-pack, …/repos/:owner/:repo/decision), the MCP server (gittensory_get_decision_pack, gittensory_explain_repo_decision), and the agent orchestrator's executeDecisionPackRun.
  • Stale-but-usable snapshots now return their topActions/repoDecisions with HTTP 200, freshness: "rebuilding" and a backgrounded rebuild. 202 + needs_snapshot_refresh is reserved for the truly-missing case.
  • The agent orchestrator no longer returns an empty needs_snapshot_refresh bundle on staleness — it runs the normal decision/blocker actions against the stale pack, finishes the run completed with dataQualityStatus: "degraded", and writes a freshness warning ("decision pack is stale (age Ns); background rebuild enqueued") to the persisted context snapshot.
  • Worker budget safety preserved: the request path still only reads the latest signal_snapshots row; buildAndPersistContributorDecisionPack is never invoked inline. Queue-send failures are caught — the response degrades to freshness: "stale" / rebuildEnqueued: false instead of failing the whole request.

Acceptance criteria → coverage

  • A contributor gets actionable recommendations from a stale but usable snapshot → integration test (stale-user 200 + non-empty topActions/repoDecisions) and orchestrator test (status: "completed" + actions persisted).
  • Response includes snapshot age, freshness status, and rebuild status → new freshness, rebuildEnqueued, and existing snapshotAgeSeconds/staleSnapshot fields, asserted in unit + integration.
  • Background rebuild is queued when needed → unit test asserts a single JOBS.send per stale request and exactly-once even with multiple serving calls.
  • No broad issue/PR scans happen inline on request paths → all serving paths read only signal_snapshots via loadContributorDecisionPackbuildAndPersistContributorDecisionPack remains the queue-side writer.
  • Fixture contributor no longer receives an empty agent plan solely because the pack is stale → new orchestrator test against a 2026-01-01 stale fixture.

Test plan

  • npm run typecheck
  • npx vitest run test/unit — 250 tests pass
  • npx vitest run test/integration — 16 tests pass
  • npx vitest run --config vitest.workers.config.ts — workers suite green
  • npm run changelog — refreshed CHANGELOG.md

Open questions for review

  • HTTP status for stale: this PR returns 200 for stale-but-usable (data is actionable). Happy to flip to 202 if maintainer prefers "any rebuild in progress" → 202 semantics — one-line change.
  • AgentRunStatus: kept the existing "completed" | "needs_snapshot_refresh" enum (no new value) and used dataQualityStatus: "degraded" + a freshness warning to signal staleness on completed runs. Avoids breaking enum consumers; open to introducing a completed_stale status if you'd rather make it explicit.
  • rebuilding semantics: there's no in-flight queue introspection today, so freshness: "rebuilding" simply means "stale + a rebuild was just enqueued from this request". "stale" is reserved for when the queue refused the job (caught error). Doc'd in site/troubleshooting.md.

Behavioral change to flag for reviewers

  • The previous stale-user integration assertion (needs_snapshot_refresh with empty body and HTTP 202) is intentionally flipped to assert HTTP 200 + actionable payload — this is the contract change the issue is asking for. The legacy missing-snapshot 202 path is unchanged.
  • Pre-existing test "defaults API runs and fails missing or interrupted runs predictably" updated: a queue-send error now yields needs_snapshot_refresh with rebuildEnqueued: false instead of failing the run. The old behavior was an accidental side-effect of JOBS.send throwing unhandled.

…kground rebuild

Closes #15.

`GET /v1/contributors/:login/decision-pack`, the MCP decision-pack tools,
and the agent orchestrator previously discarded a perfectly usable cached
pack whenever it crossed the 6h freshness window and returned an empty
`needs_snapshot_refresh` body. Every MCP `agent plan` call hitting a
slightly stale pack therefore came back with no `topActions` or
`repoDecisions`, exactly when the contributor was asking what to do next.

This change centralizes the staleness policy in a new
`loadContributorDecisionPackForServing` helper used by the REST API,
the MCP server, and the agent orchestrator. The helper:

- returns a `fresh` pack as-is when within the freshness window,
- returns the same pack with `freshness: "rebuilding"` (or `"stale"` if
  the queue refused the job) plus `rebuildEnqueued: true` when the pack
  is past the window, and enqueues a single background rebuild,
- returns a bounded `needs_snapshot_refresh` body with
  `freshness: "missing"` only when no snapshot exists.

The REST routes now return HTTP 200 with the cached `topActions`/
`repoDecisions` when serving a stale-but-usable pack and reserve 202 for
the truly-missing case. The agent orchestrator no longer returns an
empty `needs_snapshot_refresh` bundle on staleness: it runs the normal
decision/blocker path against the cached pack, marks the run
`completed` with `dataQualityStatus: "degraded"`, attaches a freshness
warning to the persisted context snapshot, and records the freshness +
rebuild status on the run payload. MCP tools degrade the same way.

OpenAPI schemas add the `freshness` and `rebuildEnqueued` fields on
`ContributorDecisionPack`, `RepoDecisionResponse`, and
`DecisionPackRefreshNeeded`. Worker budget safety is preserved: the
request path still reads only the latest `signal_snapshots` row and
never invokes broad issue/PR listers inline.

Tests:
- new unit coverage for `loadContributorDecisionPackForServing` across
  fresh, stale-with-enqueue, stale-without-enqueue, missing, and
  queue-failure paths,
- updated stale-user integration test expects 200 + actionable payload,
- new orchestrator test asserts stale-pack runs finish `completed` with
  a `decision pack is stale ...; background rebuild enqueued` freshness
  warning,
- "interrupted run" test updated to assert the new graceful degradation
  (queue errors now yield `needs_snapshot_refresh` with
  `rebuildEnqueued: false` instead of failing the run).
…ild failures

Maintainer-review fixups for #15:

- tryEnqueueDecisionPackRebuild now records a
  decision_pack.rebuild_enqueue_failed audit event when JOBS.send throws,
  so a queue outage no longer silently disables agent.run_failed alerting.
- executeDecisionPackRun emits distinct refreshReason values
  (queue_unavailable / missing_decision_pack /
  stale_decision_pack / stale_decision_pack_queue_unavailable) so an
  outage is distinguishable from "no snapshot yet".
- DecisionPackRefreshNeeded and DecisionPackRefreshNeededSchema drop the
  unreachable reason: "stale_snapshot", freshness: "rebuilding",
  staleSnapshot, dataQuality, and enqueued fields. reason is now exactly
  "missing_snapshot" and freshness is exactly "missing".
- OpenAPI 202 descriptions for both decision-pack routes updated to
  reflect the new contract; 200 description calls out freshness:
  "stale" | "rebuilding".
- Unify on rebuildEnqueued across the orchestrator payload (dropped
  snapshotRefreshEnqueued alias) and across the refresh response (dropped
  enqueued alias).
- Public GitHub bot comment (buildPublicAgentCommandComment) emits a
  one-line "snapshot is stale; rebuild requested" preface when the run's
  payload.freshness is not "fresh".
- Drop unused loadFreshContributorDecisionPack (callers all moved to
  loadContributorDecisionPackForServing).

Tests:
- new perf-regression test asserts the serving path never invokes
  listContributorPullRequests/Issues/RepoStats/Repositories/SyncStates/
  SyncSegments/LatestRepoGithubTotalsSnapshots.
- new integration assertion covers the per-repo stale path
  (GET /v1/contributors/:login/repos/:owner/:repo/decision returns 200
  with freshness: "rebuilding" and a decision body).
- existing needs_snapshot_refresh integration assertions pinned to the
  new freshness + rebuildEnqueued fields.
- interrupted-run test tightened: queue throw now asserts
  refreshReason: "queue_unavailable" AND a persisted
  decision_pack.rebuild_enqueue_failed audit row.
- stale-orchestrator test pins dataQualityStatus: "degraded" against a
  fixture whose signalFidelity.status is "complete", so the assertion
  is no longer trivially satisfied by the fixture default.

@JSONbored JSONbored left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

PR #26 - feat(agent): serve stale decision packs with freshness marker and background rebuild
Signal: REQUEST CHANGES

Required changes:

  • Fix branch coverage drop: CI reports 94.86% branches, below 95%.
  • Add branch tests for stale snapshot + enqueue failure, missing snapshot + enqueue failure, and repo-decision stale serving paths.
  • Prevent repeated stale-pack requests from enqueueing unbounded duplicate rebuild jobs; acceptance asked for a single/background rebuild behavior.
  • Confirm API/MCP responses remain bounded and never trigger broad issue/PR scans inline.

Repeated stale-pack requests for the same login no longer enqueue a
fresh job; tryEnqueueDecisionPackRebuild checks audit_events for a
decision_pack.rebuild_enqueued entry within DECISION_PACK_REBUILD_DEBOUNCE_MS
(15s) and short-circuits when one exists. Success path now records a
decision_pack.rebuild_enqueued audit event so the debounce has a signal
to read.

Adds hasRecentAuditEvent in repositories.ts (indexed lookup on
audit_events_actor_created_idx) and uses it as the dedupe gate.
@dosubot dosubot Bot added the size:L label May 29, 2026
grnlemon and others added 3 commits May 28, 2026 22:20
Adds error-path coverage for backfill-pr-details (no body, empty
repoFullName) and the json-parse .catch arm on build-contributor-evidence
and build-contributor-decision-packs.
@grnlemon

Copy link
Copy Markdown
Contributor Author

Fixups pushed in d2dd9ed.

Addressed the maintainer review findings:

  • fixed stale decision-pack rebuild dedupe for concurrent requests by joining in-flight enqueue attempts and keeping the audit-log debounce as the persistent backstop
  • removed stale/rebuild freshness text from public GitHub comments for the issue feat(agent): return useful decision packs while snapshots refresh #15 public/private boundary
  • made MCP stale summaries distinguish queued rebuilds from queue-unavailable fallbacks
  • removed ordinary-PR changelog entries
  • added focused regression coverage for concurrent stale requests, MCP stale queue-failure summaries, and public-comment non-leakage

Coverage recalculation: local branch coverage is now 4179 / 4398 = 95.02%. The configured threshold needs ceil(0.95 * 4398) = 4179, so this clears the branch gate by one covered branch. The previous 94.86% report was below threshold by 0.14 percentage points.

Validation run locally:

  • npm run typecheck
  • npm run test:unit
  • npm run test:integration
  • npm run test:coverage
  • npm run test:workers
  • npm run actionlint
  • npm run docs:check
  • npm run docs:build
  • npm run docs:smoke
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm audit --audit-level=moderate

GitHub checks on the pushed head are passing: Contributor trust and Superagent Security Scan.

@grnlemon

Copy link
Copy Markdown
Contributor Author

@JSONbored could you please review the update again?

@JSONbored

Copy link
Copy Markdown
Owner

@JSONbored could you please review the update again?

Yes, i'm working on them.

mkdev11 and others added 3 commits May 29, 2026 09:09
…stale-serving

# Conflicts:
#	CHANGELOG.md
#	src/services/decision-pack.ts
…stale-serving

# Conflicts:
#	test/unit/github-commands.test.ts
@JSONbored
JSONbored merged commit d644288 into JSONbored:main May 29, 2026
3 checks passed
@JSONbored JSONbored added gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. and removed gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. labels Jun 10, 2026
JSONbored added a commit that referenced this pull request Jul 11, 2026
…entry (#5046) (#5048)

logSelfHostAiProviderFailed fired console.error on every subscription-
CLI attempt, not just the final one -- contradicting ai-review.ts's
own documented "per-attempt=warn, exhausted=error" policy (#26).
GITTENSORY-K (claude_stalled_no_output) hit 2077+ events: pulled raw
events sharing the same jobId/trace_id at attempt 0, 1, 2, confirming
one review's 3x-retry-per-model loop was independently escalating
each attempt to Sentry instead of just the exhausted outcome.

Thread finalAttempt through AiRunOptions (mirroring the existing
attempt/jobId correlation fields) so the provider's own log can tell
a retried attempt (quiet) from a genuinely exhausted one (loud).
runWorkersOpinion, the dual-AI tie-break judge, the issue planner, and
the AI slop advisory all compute it as "last attempt of the last
model" and pass it through. Two of those callers (planner.ts,
ai-slop.ts) have no logging of their own -- their retry loops rely
entirely on this log for visibility, so a blanket downgrade would have
silenced them; unset stays loud by default for exactly that reason.
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.

feat(agent): return useful decision packs while snapshots refresh

2 participants