You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AI: Article Series planner (folder of documents) #13
Live-verified corrections from the service layer (#9 / PR #137) — read before building this
Five things contradict what the AI epic and these issues assume.
1. quota is not the same object on both endpoints. GET /api/ai/status → {usedToday, dailyLimit, remaining}. POST /api/ai/suggest → {usedToday, dailyLimit} with no remaining (verified twice). So AiQuota.Remaining is int?, and a post-suggest quota chip cannot read .Remaining — bind RemainingOrComputed instead. #10's "remaining quota is visible wherever an AI action is offered" has to account for that.
2. There is no provider picker to build.
AI is Anthropic-only, app-wide; per-user provider keys were removed server-side 2026-09-05. The provider field on /generate is audit-ledger-only and does not select anything, and the openai/gemini entries in defaultModels are dormant and unreachable. Don't surface a provider choice.
3. crossPost has six fields, not two — crossPostToBluesky, selectedMastodonIds, crossPostToLinkedIn, selectedLinkedInTargets, crossPostToTwitter, linkedInLinkAsFirstComment. Both crossPost and scheduleImmediately are message_series-only (relevant to #12).
4. A sixth 422 code exists that the epic didn't list: refused. Treat it like invalid_ai_output. Modelled and marked retryable.
5. Quota accounting is asymmetric, and it shapes the UI.
Input-validation 422s cost nothing (usedToday stayed 0 across two), but any call that reaches the model costs a unit — including failures. So client-side pre-flight is load-bearing, not politeness: enforce the word caps, the 10-word series gate (#12/#13), a missing powered_document source ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.
Also: context.spacingMinutes is accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.
Note on the exception type
The code is surfaced on a new AiApiException, not on InterlinedApiException — the latter is sealed and was being rewritten in #130. Folding them together is flagged as a follow-up in the code. Catch AiApiException in these UI issues.
Unverified
POST /api/ai/generate was never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the raw created element for read-after-write. Whichever of #12–#15 lands first should verify it against a real account and tighten the model.
⚠️ Correction to the quota rule I posted earlier on this issue
My earlier comment said: "input-validation 422s cost nothing … but any call that reaches the model costs a unit." The first half is too optimistic, and #10/#14/#15's probing proved it (PRs #173/#174/#177).
An unrecognized context.mode is not cheap-rejected — it reaches the model and bills a unit, returning 422 invalid_ai_output.
So the accurate rule is narrower: a 422 is free only for validation the server actually performs. Anything it doesn't recognise as invalid gets forwarded to the model and charged. Observed on the real quota counter:
call
cost
powered_template /suggest
1
powered_document /suggest (mode: from_article)
1
mode: "not_a_mode"
1 — unexpectedly billed
4 reference/scheme rejection probes
0
The rejections that were free came back with prose specific enough to show verbatim: "List not found.", "A list must be selected.", "Document not found.", "Only http(s) URLs are supported."
Practical consequence for this issue: client-side pre-flight must validate against a closed set of known-good values, not merely catch obvious omissions. Sending an unknown enum value is not a free mistake. And still no auto-retry.
Also useful here
The powered_template artifact shape is now pinned (it was contract-transcribed only):
kind: "list"
{ title, description,
dsl: { name, description,
fields: [{ key, type, label, required, displayOrder,
options?, defaultValue?,
visibility?{ condition{ field, operator, value } } }] },
rows: [ { <fieldKey>: value } ] } // flat maps; null is a legal cell value
Four members were on record nowhere in the repo: the dsl wrapper's name/description, and per-field displayOrder/options/defaultValue/visibility. The model attaches rules the editor doesn't surface (e.g. visibility.condition), which makes preserving unparsed members across an edit load-bearing — a naive re-serialize would silently drop them.
powered_document returns {kind, title, markdown, outline[], isPublic} and matched the transcribed model exactly.
Still unverified for all AI issues
POST /api/ai/generate has never been called (it persists to a shared account), so {listId}/{documentId} field names are unconfirmed — every confirm path does read-after-write plus an explicit "don't retry, you'd duplicate" when no id comes back. Also unobserved: subscriber:false, providers:[], either 429, and a real research_url fetch.
Activity
Adron commentedon Sep 16, 2026
Live-verified corrections from the service layer (#9 / PR #137) — read before building this
Five things contradict what the AI epic and these issues assume.
1.
quotais not the same object on both endpoints.GET /api/ai/status→{usedToday, dailyLimit, remaining}.POST /api/ai/suggest→{usedToday, dailyLimit}with noremaining(verified twice). SoAiQuota.Remainingisint?, and a post-suggest quota chip cannot read.Remaining— bindRemainingOrComputedinstead. #10's "remaining quota is visible wherever an AI action is offered" has to account for that.2. There is no provider picker to build.
AI is Anthropic-only, app-wide; per-user provider keys were removed server-side 2026-09-05. The
providerfield on/generateis audit-ledger-only and does not select anything, and theopenai/geminientries indefaultModelsare dormant and unreachable. Don't surface a provider choice.3.
crossPosthas six fields, not two —crossPostToBluesky,selectedMastodonIds,crossPostToLinkedIn,selectedLinkedInTargets,crossPostToTwitter,linkedInLinkAsFirstComment. BothcrossPostandscheduleImmediatelyaremessage_series-only (relevant to #12).4. A sixth
422code exists that the epic didn't list:refused. Treat it likeinvalid_ai_output. Modelled and marked retryable.5. Quota accounting is asymmetric, and it shapes the UI.
Input-validation
422s cost nothing (usedTodaystayed 0 across two), but any call that reaches the model costs a unit — including failures. So client-side pre-flight is load-bearing, not politeness: enforce the word caps, the 10-word series gate (#12/#13), a missingpowered_documentsource ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.Also:
context.spacingMinutesis accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.Note on the exception type
The
codeis surfaced on a newAiApiException, not onInterlinedApiException— the latter issealedand was being rewritten in #130. Folding them together is flagged as a follow-up in the code. CatchAiApiExceptionin these UI issues.Unverified
POST /api/ai/generatewas never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the rawcreatedelement for read-after-write. Whichever of #12–#15 lands first should verify it against a real account and tighten the model.Adron commentedon Sep 16, 2026
My earlier comment said: "input-validation
422s cost nothing … but any call that reaches the model costs a unit." The first half is too optimistic, and #10/#14/#15's probing proved it (PRs #173/#174/#177).An unrecognized
context.modeis not cheap-rejected — it reaches the model and bills a unit, returning422 invalid_ai_output.So the accurate rule is narrower: a
422is free only for validation the server actually performs. Anything it doesn't recognise as invalid gets forwarded to the model and charged. Observed on the real quota counter:powered_template/suggestpowered_document/suggest (mode: from_article)mode: "not_a_mode"The rejections that were free came back with prose specific enough to show verbatim: "List not found.", "A list must be selected.", "Document not found.", "Only http(s) URLs are supported."
Practical consequence for this issue: client-side pre-flight must validate against a closed set of known-good values, not merely catch obvious omissions. Sending an unknown enum value is not a free mistake. And still no auto-retry.
Also useful here
The
powered_templateartifact shape is now pinned (it was contract-transcribed only):Four members were on record nowhere in the repo: the
dslwrapper'sname/description, and per-fielddisplayOrder/options/defaultValue/visibility. The model attaches rules the editor doesn't surface (e.g.visibility.condition), which makes preserving unparsed members across an edit load-bearing — a naive re-serialize would silently drop them.powered_documentreturns{kind, title, markdown, outline[], isPublic}and matched the transcribed model exactly.Still unverified for all AI issues
POST /api/ai/generatehas never been called (it persists to a shared account), so{listId}/{documentId}field names are unconfirmed — every confirm path does read-after-write plus an explicit "don't retry, you'd duplicate" when no id comes back. Also unobserved:subscriber:false,providers:[], either429, and a realresearch_urlfetch.