Skip to content

fix(memory): skip distillation when the team's only credential is for another provider - #131

Merged
escapeboy merged 1 commit into
developfrom
fix/distill-skip-ai-access-unavailable
Aug 1, 2026
Merged

fix(memory): skip distillation when the team's only credential is for another provider#131
escapeboy merged 1 commit into
developfrom
fix/distill-skip-ai-access-unavailable

Conversation

@escapeboy

Copy link
Copy Markdown
Owner

fix(memory): skip distillation when the team's only credential is for another provider

The AI availability gate is provider-agnostic; the distillation call site is
provider-specific, and nothing reconciled them.

DistillTeamEventsJob::handle pre-flight-gates on TeamAiAccessChecker::canUseAi
— "does this team have any path to AI". DistillTeamEventsAction::distil then
demands one specific provider from config('memory.distillation.model'). A BYOK
team whose only credential is for a different provider (openai, google,
custom_endpoint) therefore clears the gate and then throws
AiAccessUnavailableException from PrismAiGateway::resolveCredential, because it
has no credential for the distillation provider and its plan carries no
platform_llm_fallback.

The guard for this already existed but was too narrow: it matched only the
message 'No available providers in fallback chain', while the real throw arrives
as AiAccessUnavailableException — a purpose-built expected-backpressure type that
BeforeSendFilter and BaseStageJob::isExpectedFailure already treat as such.
Result: the hourly cron pushed these teams into failed_jobs and Sentry on every
run, forever.

Widen the existing catch to recognise the exception type. Skip cleanly and
advance the watermark, exactly as the fallback-chain case already does.

Regression test pins the scenario: distillation configured for one provider, the
gateway throwing AiAccessUnavailableException for it — the action must return a
0-stored result and advance the watermark instead of letting the exception
escape. Verified to fail without the fix.

Sentry: #824, #847, #1035, #1064.

… another provider

The AI availability gate is provider-agnostic; the distillation call site is
provider-specific, and nothing reconciled them.

DistillTeamEventsJob::handle pre-flight-gates on TeamAiAccessChecker::canUseAi
— "does this team have *any* path to AI". DistillTeamEventsAction::distil then
demands one specific provider from config('memory.distillation.model'). A BYOK
team whose only credential is for a different provider (openai, google,
custom_endpoint) therefore clears the gate and then throws
AiAccessUnavailableException from PrismAiGateway::resolveCredential, because it
has no credential for the distillation provider and its plan carries no
platform_llm_fallback.

The guard for this already existed but was too narrow: it matched only the
message 'No available providers in fallback chain', while the real throw arrives
as AiAccessUnavailableException — a purpose-built expected-backpressure type that
BeforeSendFilter and BaseStageJob::isExpectedFailure already treat as such.
Result: the hourly cron pushed these teams into failed_jobs and Sentry on every
run, forever.

Widen the existing catch to recognise the exception type. Skip cleanly and
advance the watermark, exactly as the fallback-chain case already does.

Regression test pins the scenario: distillation configured for one provider, the
gateway throwing AiAccessUnavailableException for it — the action must return a
0-stored result and advance the watermark instead of letting the exception
escape. Verified to fail without the fix.

Sentry: #824, #847, #1035, #1064.
@escapeboy
escapeboy merged commit 38a0f8f into develop Aug 1, 2026
3 checks passed
@escapeboy
escapeboy deleted the fix/distill-skip-ai-access-unavailable branch August 1, 2026 09:36
escapeboy added a commit that referenced this pull request Aug 4, 2026
failed_jobs had never been pruned and had grown to 1916 rows on production,
the oldest from 2026-04. Every row is a resolved incident, and the volume
drowns the handful that still matter — `queue:failed` shows only the tail, so
the table reads as "3 old entries" when it is nearly two thousand.

`queue:prune-failed --hours=720` keeps 30 days, which is long enough to
investigate a failure after the fact. Verified the schedule entry registers
(`15 4 * * *`, next due 19h).

Found while validating the current failures rather than assuming they were
stale: of the 17 since 2026-07-24, all but one are DistillTeamEventsJob at
01:31 nightly, which stopped after 2026-07-29 (fix #131, 2026-08-01 — six
clean nights since, with `memory:distill-events` confirmed still scheduled and
due, so the silence is a fix and not a job that quietly stopped running). The
remaining one is the browser_task max_steps 400, fixed in #136.
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.

1 participant