Repository navigation
Release/1.24.0 - #1244
Merged
Merged
Release/1.24.0#1244
Conversation
…rkdown-dollar-conflict-a4948e fix: stop KaTeX swallowing currency in assistant messages
…paction-followups docs(kaizen): queue the compaction-stack follow-ups for Friday review
…Metadata (offload PR-2) Stacked on PR-1. The digest a later turn will carry instead of the document's bytes (docs/specs/document-context-offload.md §4A), built once, off the model path, when a document upload completes. - apis/shared/files/document_digest.py: a deterministic outline (headings with page/paragraph/line anchors, table and figure mentions, counts, a text sample spread across the document) via pypdfium2 and the PR-1 DOCX extractor, plus a 3-5 sentence abstract from Nova Micro (DOCUMENT_DIGEST_MODEL_ID) that fails open. render_digest emits the <document-digest> block under a hard 1,500-token budget (sections dropped first, then the abstract; the handle always survives) and the estimate is stored as digest.tokens. - FileMetadata.digest + FileUploadRepository.update_file_digest; complete_upload schedules the build as a strong-referenced background task for document uploads only. DOCUMENT_DIGEST_ENABLED=false skips it. - Content policy: digest.abstract / digest.sections denylisted; status, format, count and tokens projected; the attachment profile reports digested count and digest tokens. DocumentDigestGenerated/Tokens/Ms EMF in AgentCoreStack/Compaction. - Nothing in the chat path reads the digest yet (PR-3). Spec: PR-2 row marked built; §8 decisions 12-15. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fload PR-3) Stacked on PR-2. The correctness half of docs/specs/document-context-offload.md §4E: on restore, an inline document block becomes its <document-digest …> block (abstract, outline, upload_id handle for document_read) instead of the contentless placeholder that lost a returning user's document. - session/document_rehydration.py: match each block to the session's upload rows on the sanitized filename PromptBuilder gave it (dot -> underscore, _2/_3 duplicate suffix), byte size as tiebreak, newest first, each row claimed once; render the row's digest. Rows without a digest get an outline-only digest built from the bytes already in the restored message (no S3, no model call on the synchronous restore path) and persisted. Unmatched blocks keep the placeholder verbatim; every failure falls back to it. DOCUMENT_REHYDRATE_ENABLED=false restores the old path. - _strip_document_bytes delegates and records document_rehydrated and document_stripped separately on the ledger. - FileUploadRepository: list_session_files_sync / update_file_digest_sync bodies (initialize() runs under the agent constructor and cannot await). - Restore output is byte-stable across restores given the same rows (tested). - Spec: PR-3 row marked built; §8 decisions 16-20. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… re-write is free (offload PR-4) Stacked on PR-3. The cost half of docs/specs/document-context-offload.md (§4C / §4D): once a document is no longer the active subject, its inline bytes leave the cacheable prefix and its digest (PR-2) plus the document_read handle (PR-1) take their place. - session/document_offload.py: pinning (attach turn + next; the incoming or previous prompt names the document's stem; a recent document_read result), the 5,000-token size floor, the crc32 rollout bucket (DOCUMENT_OFFLOAD_ROLLOUT_PERCENT) and DOCUMENT_OFFLOAD_ENABLED kill switch, in-place replacement, and document_read slice ageing. - TurnBasedSessionManager.apply_document_offload runs head-of-turn right after apply_pending_compaction (the compaction stack's §3.5 predicate is this spec's cache-aware rule): only on cache_expired, prefix_changed or over_ceiling; otherwise nothing moves. The replacement is PR-3's restore transformation, so the live block equals a cold restore's. Records document_offload on the ledger with cacheGapSeconds and emits DocumentOffloaded/DocumentOffloadedTokens/DocumentSlicesAged EMF. - Slices older than DOCUMENT_SLICE_MAX_TURNS are stubbed on the live path and on restore, so a warm agent and a cold restore agree. - CompactionEvent (py + TS) gains digestTokens / slices / sliceTokens. - Spec: PR-4 row marked built; §8 decisions 21-26. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ger (offload PR-5) The spec's PR-5 asked for a cache-live guard on the truncation deferral (July 2026: 34 of 47 truncation events fired inside the 5-minute TTL). That defect was closed by the compaction stack before this spec's PRs began: the anchor moves only with a cut (applied paid-when-free) or in _maybe_advance_truncation_anchor behind _cache_window_expired, and the stability suite pins that a warm cache never advances it. What was missing was the proof from rows. - truncation_anchor ledger event (anchorFrom, anchorTo, cacheGapSeconds measured BEFORE the save re-stamps updated_at) and TruncationAnchorAdvanced / TruncationAnchorCacheGapSeconds EMF. - cacheGapSeconds on the restore-time `applied` event, measured at the top of _apply_compaction, so a restore-time slice or truncation inside the TTL is countable as a rebuild while the cache was live. - No behavior change. Spec: PR-5 row rewritten; §8 decision 27. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ost rows (offload PR-7)
A thumbs up/down on assistant messages, persisted as an F# row on the
sessions-metadata table keyed (sessionId, messageId) beside the C# cost row,
so the admin session profile joins the outcome signal to the call's turn
class in one key lookup. Value ±1, timestamp and an optional reason code
from a closed enum — never free text, by the request model and the storage
layer both.
- apis.shared.sessions.feedback: F#{sessionId}#{messageId} rows (documented
with the C#/D# schemas in metadata.py); a second click replaces; session
rollups thumbsUp/thumbsDown ADDed while COST_DIAGNOSTICS_ENABLED.
- GET /sessions/{id}/messages merges the thumb onto metadata.feedback so a
reload restores the pressed state.
- PUT/DELETE /sessions/{id}/messages/{messageId}/feedback via
get_current_user_from_session; MESSAGE_FEEDBACK_ENABLED kill switch (404).
- Admin profile: feedback {up, down, byTurnClass, unjoined} + dataCoverage.
feedback; byTurnClass is null ("not tracked") until the C# rows carry
hasDocuments / documentDigests / documentReads (#1137).
- content_policy: FEEDBACK_ROW_PROJECTION walked by the existing test;
session projection gains thumbsUp/thumbsDown; call projection gains the
three turn-class fields.
- SPA: thumbs pair with reason chips in the message actions, optimistic with
rollback; Feedback tile with down-thumb rate by turn class on the anatomy
page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nt quota (PR-6)
The stored unit is the message, not the file: a turn's inline attachments
are persisted as one AgentCore Memory event, base64-inflated 4/3 into a
blob payload and bounded by the 10 MB event quota, so ~7.5 MB of raw bytes
per turn is the break point. Past it create_message re-raises
SessionException — a hole in history. The per-file 4 MB gate and the SPA's
5-file cap do not protect: ~1.3–1.4% of prod attachment turns exceed it,
several with only 3–4 files (document-context-offload-validation.md, Claim 7).
- `_apply_inline_byte_budget`: first-fit in attachment order against
INLINE_ATTACHMENTS_MAX_TOTAL_BYTES (default 7.5 MB, derivation in the
constant's comment); images count. Trimmed files join the existing
oversized-note path with their own wording ("send in a follow-up").
- `_apply_message_file_cap`: the server side of the SPA's
MAX_FILES_PER_MESSAGE, applied to both `files` and `file_upload_ids`
before the S3 fetch. The resolver's silent `[:5]` becomes a note to the
user; the app_api's dead `max_files_per_message` now reads the same
shared constant.
- One content-free EMF metric, AttachmentTurnOverQuota (Bytes) in
AgentCoreStack/Compaction, plus a count-only log line.
- ATTACHMENT_TURN_GUARD_ENABLED=false restores the prior behaviour.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sharing an artifact used to be reachable only from the card inside the
conversation that produced it, which meant the library at /artifacts —
the page whose whole premise is that artifacts outlive their chat — had
no way to share one. Both surfaces now open the same
ArtifactShareModalComponent the card does, so there stays exactly one
place in the app that knows what an artifact share is.
The control is gated on `row.owned`, so it appears on your own rows and
never on the "Shared with you" ones: a received artifact has no artifact
id to share (the share id is the only handle you have on it), and
re-sharing someone else's grant is not something this model supports.
The view page deliberately has no ownership check of its own. It
resolves its artifact out of listLibrary(), which reads the caller's own
DynamoDB partition, so an id that is not yours renders "Artifact not
found" before the header exists at all — the boundary is the partition
key, and a comparison in the template would imply the guarantee lives in
the UI. A test pins that the control is absent when nothing resolved.
Both surfaces show HEAD, so they pin HEAD's version. Shares are
immutable and never follow HEAD, so the version rides in each control's
accessible name ("Share Quarterly plan, version 4") as well as the
dialog's caption — the library prints a version badge only when it is
> 1, so the control cannot rely on one being on screen.
Grid footer: raise the label-collapse threshold 19rem -> 21rem.
Found in the browser, not by a test. 19rem was tuned for FOUR controls
and fit with zero slack: at three columns on a 1280px window the
container measures exactly 306px and the labelled row needed exactly
306px. Adding Share pushed the row to 326px while leaving the container
2px ABOVE the collapse threshold, so the labels stayed visible and the
Delete icon was clipped. 21rem (336px) collapses them with 10px to
spare, and the labels still reappear at one column, so they are hidden
where they do not fit rather than removed. Verified against real data on
dev: zero overflow on every card, in both views.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ason buckets, explicit/implicit signal discriminator RESPONSE_FEEDBACK_ENABLED per spec §5; reason codes are the spec's §6 buckets (still a closed enum — free text stays out by decision, recorded in the spec's new §13); rows carry signal="explicit" and every thumb reader filters on it so the §10 implicit phase needs no backfill and never sums in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bs down response-feedback spec §11 PR-1's other half: a down-thumb's reason row offers "Retry with that in mind", which prefills the composer with a correction template for the reason code (new ComposerDraftService; the composer consumes drafts for its session and never submits). When the user sends it, ChatRequestService hands the added user message to MessageFeedbackService.consumePendingRetry, which PUTs retryMessageId — the message's index, never its text — onto the F# row. The write is now an update_item upsert so a later re-thumb keeps the link. Open question 1 settled as a new turn rather than /steer (which targets a running turn). The admin profile reports feedback.retried and reworkUsd (spec §7 "rework cost": the thumbed call rows plus the retry turn's consecutive assistant rows). Spec §13 updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…title formatGap drops a zero seconds remainder, so a 420s cache gap renders as "7m", not "7m 0s". The assertion was written against the latter and was the only red check on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t-offload-pr1 feat(documents): document_read tool + document-context analytics (offload PR-1)
…t-offload-pr2 feat(documents): DocumentDigest built at upload and persisted on FileMetadata (offload PR-2)
…t-offload-pr3 feat(documents): restore rehydrates stripped documents as digests (offload PR-3)
…t-offload-pr4 feat(documents): offload unpinned documents to their digests when the re-write is free (offload PR-4)
…t-offload-pr5 feat(compaction): record the truncation anchor's cache gap on the ledger (offload PR-5)
…t-offload-pr6 feat(attachments): hold a turn's inline attachments to the Memory event quota (PR-6)
Resolves the document-context offload stack (#1137-#1141, #1143) against the feedback work. Every conflict was both sides adding adjacent members to the same model, flag module, allowlist or interface, so the resolution keeps both, except FEEDBACK_ROW_PROJECTION's turn-class comment: PR-1 widened that projection to the full document-context field set, which already carries hasDocuments and documentDigests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The merge resolution appended develop's `documents` member after the interface's closing brace, landing it on FeedbackProfile and breaking the AOT typecheck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-feedback-pr7 feat(feedback): content-free thumbs on assistant messages, joined to cost rows (offload PR-7)
…k-retry-correction feat(feedback): retry-with-correction — the consequence behind a thumbs down
…ck PR-2)
Spec §10: denser than thumbs, no UI cost. Copy and Continue in the message
actions rail POST a kind code to /sessions/{id}/messages/{messageId}/signals
(fire-and-forget, once per message per kind per page load). Rows share the
F# family under their own key — F#{session}#{message}#{kind} — with
signal="implicit", kind, and an ADDed count, so they never collide with the
thumb and every thumb reader keeps filtering them out. The profile reports
feedback.implicit as messages touched per kind, on its own line: explicit
and implicit are never summed.
Edit-and-resend has no SPA affordance to hook; abandonment is deferred by
design (spec §13). Spec status and §13 updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`documentTokens` (offload spec §6.1) and the `DOCUMENT_OFFLOAD_MIN_TOKENS` floor (§4C) both read a document's weight from the compaction estimator's `bytes/4`. That is wrong for PDFs by an order of magnitude: Bedrock dual-encodes each page as an image *and* a text layer — the property §3 of the spec depends on — and the image channel has nothing to do with file size. Measured on dev (session 61de2256, 2026-09-16): a 27,578-byte, 60-page PDF produced a 109.1K-token first write against a 14.6K static prefix, so the document was ~94K tokens. `bytes/4` reported 6,894 — 13.7x low. At that error the spec's "document share of the prefix" reads 6% where the truth is ~87%, which inverts the ship/abandon call the evaluation spec §4.2 reads off these rows, and understates a measured 109.1K -> ~15K prefix drop. New `apis/shared/files/document_tokens.estimate_document_tokens`: pages x PDF_PAGE_TOKEN_ESTIMATE (default 1,500, env-tunable) for PDFs, floored by bytes/4 so a byte-heavy scan can only score higher than before; bytes/4 unchanged for formats Bedrock extracts to text. Page counting costs ~0.1ms even at 200 pages (measured), so no memoization is needed, and an unparseable PDF falls back to today's number. On the dev canary it lands at 90,000 against 94,485 measured — 5% low, deliberately, because this number gates an eviction. Wired into every consumer that reports or decides on that quantity, so the row and the offloader can never disagree: `document_context` (the `C#` row's documentTokens and documentSliceTokens), `document_offload` (Candidate.tokens, the min_tokens floor and the aged-slice ledger figure) and `document_rehydration` (document_stripped). The compaction estimator is deliberately NOT changed — it rescales to measured history and drives cut thresholds, so it needs its own decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e the tool The flag was read in exactly one place — `_document_tools_gate`, which decides whether `document_read` is injected. Neither the restore path nor the live offload consulted it, so pulling the one kill switch an operator would actually reach for (the tool is misbehaving) left two worse-than-before behaviours: 1. restore kept emitting `<document-digest upload_id=…>`, advertising a retrieval handle for a tool that would not be injected — the model is invited to make a call it cannot make; 2. the live offload kept evicting document bytes from the prefix with the only recovery path switched off. That is strictly worse than the pre-offload world, where live bytes never left at all. Spec §5 states the rule this violated: "a digest that points at a tool nobody has is no better than today's placeholder." Fix, split by whether dropping the bytes is optional: - **Restore has to drop them** either way (Bedrock rejects duplicate document names across a conversation), and a digest is still strictly more than the pre-PR-3 placeholder — so it still renders, but `render_digest` gains `include_handle` and the restore path passes `document_read_enabled()`. The outline and abstract survive; the false affordance does not. - **The live offload is optional**, so it stops entirely: `offload_enabled_for` now also requires `document_read_enabled()`. Both callers (the head-of-turn offload and restore-path slice ageing) already go through that single gate, so one check covers both and there is no second flag read to drift. With the flag on, every rendered byte is identical to before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s (response-feedback PR-4) Down-thumbed turns become the evaluation harness's input queue. Thumb rows carry GSI1PK=FEEDBACK#down / GSI1SK=updatedAt on the existing UserTimestampIndex, so the fleet's recent down-thumbs are one query with no new index. POST /admin/feedback/evaluations/run judges up to N not-yet-judged ones in a background task via bedrock_agentcore.evaluation.EvaluationClient over the runtime log group (keyed by the runtime session id the chat proxy pins); GET /admin/feedback/evaluations is the queue with verdicts. Routing is spec §6: wrong → Correctness + Faithfulness, instructions → InstructionFollowing, length → Conciseness, other/none → Helpfulness; tool_failed gets no judge and is corroborated against the call's tool census (ops, not model); outdated has no judge yet. The stored verdict is per-evaluator value / rating / n / tokens — the judge's explanation quotes the conversation, so it is dropped at summarisation, refused at the storage write, and denylisted in the content policy. The profile shows feedback.evaluations. Opt-in: FEEDBACK_EVAL_SAMPLING_ENABLED defaults off (CDK CDK_FEEDBACK_EVAL_SAMPLING_ENABLED=true) because the managed judge reads the sampled conversation's spans — the scoping decision the evaluations spike says to make per environment. CDK wires the runtime log group into app-api and grants Logs Insights on it and aws/spans plus Evaluate/GetEvaluator; inert until an environment opts in. Spec §13 updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(agents): integrate Round 1 template-prefill work (P1 hydration seam, P4 reconcile, P3/P5 picker+content) * feat(agents): Phase 2 template prefill — read localStorage draft in create mode, reconcile tool refs, populate form dirty with flagged/dropped notice * feat(agents): add Q&A, Roleplay and tool-notice demo templates + catalog-wide spec coverage * feat(agents): pin a concrete default model (Claude Sonnet 5) on templates so the picker prefills a selection * feat(agent-templates): admin-managed template catalog backend Backend for the Agent Templates feature per docs/specs/AGENT_TEMPLATES_SPEC.md, mirroring the system_prompts admin-catalog pattern: - shared/agent_templates: models (AgentTemplate + admin/public wire shapes), repository (DynamoDB CRUD, Scan listing), service, and an idempotent seed of three NEUTRAL fork-safe templates (no org tool refs, model_id=null). - app_api/admin/agent_templates: scope-guarded CRUD (admin.agent_templates). - app_api/agent_templates: public GET /templates returning the client TemplateCatalogEntry shape (camelCase draft + pitch), enabled-only, ordered, [] on empty. - admin.agent_templates RBAC scope (GROUP_AI_CONFIG, delegable); routers mounted; AGENT_TEMPLATES config-cache key. - CDK: AgentTemplatesTable in AdminTablesConstruct + SSM params, compute ref, app_api env var + IAM grant, platform-stack wiring + backup list. - Tests: 24 model/repo/service/seed + route tests; scope-coverage map updated. Local only, no push. Client picker still reads the hardcoded array; the data-source switch is the follow-on step. * test(infra): add AgentTemplatesTable CDK assertions Cover the AgentTemplatesTable + wiring already added in 57a7bd5 with infra CDK tests (the backend commit added none): - constructs.test.ts: AdminTablesConstruct 4->5 tables + PK/SK/no-GSI/ AWS_MANAGED assertion for the agent-templates table. - platform-stack.test.ts: shared-table count 27->28; SSM param cap 49->51 for the table name+ARN publishes. - app-api-environment.test.ts: DYNAMODB_AGENT_TEMPLATES_TABLE_NAME wiring. - security-policy.test.ts: scoped AgentTemplatesTableAccess IAM grant (never Action:* / Resource:*). Tests only, no source changes. Local, no push. * feat(agents): source template picker from live backend catalog Swap the 'Start from a template' picker from the hardcoded AGENT_TEMPLATES array to the public GET /templates/ endpoint via a new AgentTemplatesService. Remove the org-specific template DATA from agent-templates.ts (keep the TemplateDraft/TemplateCatalogEntry types + the localStorage contract key) so no Boise/higher-ed content is baked into the client (forkability). The select handoff (write draft to localStorage, route to /agents/new) and the downstream prefill/reconcile/notice path are unchanged. Picker gains loading/empty/error states. Specs updated to mock the service. * feat(admin-ui): agent templates admin console (list + form + service) Add the admin UI for the admin-managed Agent Templates catalog, consuming the /admin/agent-templates CRUD routes on the backend branch: - AdminAgentTemplatesService: list/get/create/update/delete (lazy resource). - Manage list page: name/emoji/enabled/order, enable-disable, delete, reorder (sort_order swap), mirroring manage-system-prompts + manage-models. - Create/edit form page (new / edit/:id): agent-form field set (name, emoji, description, instructions, visibility, tags, starters, model, bindings) plus catalog fields (pitch, status, sort_order). Blank model id -> platform default; model params + binding config preserved on edit. - Routes under admin/agent-templates guarded by adminScopeGuard data:{scope:'admin.agent_templates'}; nav entry in AI Configuration; admin.agent_templates added to the client scope union. - 13 vitest specs (service endpoints, list render/toggle/sort, form buildPayload/create/edit) green via the Angular builder. Local only. * feat(admin): model dropdown for agent-template form Replace the raw model-id text input in the admin Agent Templates create/edit form with a friendlier select: a 'Platform default' option (modelConfig.modelId = null, the default for new templates) plus the RBAC-filtered configured-model catalog, reused from AgentService.loadBindable('model') (GET /agents/bindable?kind=model) — the same source the create-agent form uses. On edit the saved modelId preselects; a saved id no longer in the catalog is shown as a clearly-labeled 'unavailable' option and preserved on save rather than silently dropped. Spec covers platform-default, select-a-model, and edit-preselect (valid + stale). * feat(admin): emoji picker popup for agent-template form Replace the plain emoji text input in the admin Agent Templates form with a real emoji-picker popup. Extract the create-agent builder's inline emoji field (emoji-mart in a CDK connected overlay) into a reusable ControlValueAccessor component, app-emoji-picker (src/app/shared/emoji-picker), so it drops into the reactive form via formControlName="emoji" — the form drives its value (including edit-mode patchValue) and reads selections back; clearing emits ''. agent-form is left as-is and can migrate to the shared component later. * feat(agent-templates): drop template visibility; tool-binding picker Change 1 — remove visibility as a template concept end-to-end. A template's lifecycle is enabled/disabled; visibility (PRIVATE/PUBLIC/SHARED) is an agent concept, meaningless on a template. Dropped from the backend model/repo/seed, the public GET /templates/ TemplateDraft, the admin + client TS types, and the admin form. Prefill no longer sets the create-agent form's visibility from the template — applyAgentToForm falls back to the form's own default (PRIVATE) when a draft carries none, so a created agent stays owner-private by default. Change 2 — admin tool bindings pick from the enabled tool catalog instead of raw ref text. Reuses AgentService.loadBindable('tool') (GET /agents/bindable?kind=tool), same as the model dropdown. A tool binding renders a dropdown (label shown, ref stored); a saved ref no longer in the catalog is kept as a clearly-labelled 'unavailable' option and preserved on save. Other binding kinds keep the text input. Specs updated (backend + frontend), targeted runs green. * chore(agent-templates): drop now-unused BindingPayload import in seed * test(infra): register AgentTemplatesTable in GSI inventory + table count The new AgentTemplatesTable bumps the stack's table inventory, tripping two full-suite infra guards the targeted pre-push run didn't cover: - gsi-inventory.json regenerated to include agent-templates (no GSIs). - observability-dynamodb-alarms table count 27 -> 28 (the per-table throttle alarm is auto-derived, so no construct change needed).
…t-share-from-library-and-view feat(spa): share artifacts from the library and the owner view page
…width The dashboard stacked four blocks down one column and shared a two-column grid between the donut and the trend chart, so each visualisation rendered at half the console's width. The donut's legend ran past the fold, the top-users table lost its tier and quota columns to horizontal scroll, and the trend chart's date axis sat on a 45-degree rotation and still dropped labels. One tab per visualisation instead: Model Usage (default), Cost Trends, Top Users, Conversations. The period KPIs stay pinned above the strip — every tab is a drill-down of those four numbers — and the Session Cost Anatomy lookup moves out of the page header into the Conversations tab, beside the rows it links to. Model Usage leads because the donut answers what the money went to, and it is the only panel complete on load with no further fetch or scan. Tab strip is the documented pill idiom (raised white pill, recessed shell), not an underline: the brand token has no dark variant, so a brand-coloured underline all but disappears on the dark surface. Panels are destroyed on switch rather than hidden, because a hidden canvas cannot size itself and a [hidden] panel would come back to a 0x0 chart; the loaded rows already live in AdminCostStateService, not in the panel. Two defects found on the way: - Whole-dollar axis labels printed "$4 $4 $3 $2 $2 $1 $1 $0" whenever the axis stepped by under a dollar — seven labels for four values against eight distinct gridlines. Both charts now read the precision off the full tick array Chart.js hands the callback, so the column stays uniform rather than mixing "$1" with "$1.50". - Neither chart destroyed its Chart.js instance. That cost nothing while the cards lived for the life of the page; now that they mount and unmount, every tab switch leaked one. Also caps admin content at max-w-[100rem] and centres it. That is a sprawl guard, not the max-w-7xl reading-width cap this shell exists to drop: it is wider than any laptop the console is used on, and only stops a 20-column cost table reaching 3000px on an ultrawide. Verified against dev data across all four tabs, light and dark, at 375 / 1600 / 2600px. Badge contrast 6.51-9.63:1 light and 4.74-5.04:1 dark. Full SPA suite green (296 files / 3701 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cost above the composer swapped silently from one figure to the next, so a turn's spend was only visible to someone already looking at the badge. It now tallies up to the new conversation total whenever the total changes — on entrance, in step with the label's existing fade-in and the ring fill, and again after every turn. The tally is a requestAnimationFrame loop eased over 650ms (easeOutCubic). Details worth knowing: - Counting only ever goes up. A drop means a different conversation is in view, not a refund, so that snaps. - `addTurnCost` fires per LLM call, not per turn, so a multi-tool turn retargets mid-count. A retarget resumes from the running tally rather than restarting, so the number never jumps backwards. - The rendered digits are formatted with the digit count the *target* warrants, not their own: a tally climbing to $1.05 passes through $0.98 and would otherwise reflow from four decimals to two partway up. Plus `tabular-nums` so the digits do not jitter. - The digits are aria-hidden. The span's aria-label already carried the settled figure, so the surrounding polite live region announces the turn's total once instead of ~40 intermediate frames. `costLabel()` still reports the settled total, so the accessible label never lags the animation. - Reduced motion, a missing requestAnimationFrame, and destroy all snap or cancel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ge-animation-18ac03 feat(spa): count the session cost badge up to each turn's new total
The dashboard measured Bedrock token spend and measured it well — against prod's September 1-21 bill our ledger came within 0.50% of Cost Explorer's model SKUs ($1,076.03 vs $1,081.44). It could not see anything that was not a token, which on prod was 38.8% of the bill: $685.96 of $1,767.41. Amazon Bedrock AgentCore alone was $201.54/month, the second-largest line item in the account and on no screen anywhere. With 1,807 active users, "Avg Cost/User" was reporting $0.60 against a true all-in $0.98 — understating unit economics by 1.64x. A daily EventBridge tick runs a Lambda that makes one Cost Explorer call per period and writes PLATFORM#* rows into the EXISTING system-cost-rollup table (own PK namespace — no new table, no GSI, no migration). A new Platform tab and the two headline cards read those rows. Scheduled and never live, because Cost Explorer bills $0.01 per request: a screen that queried it on load would cost ~$10/month per thousand page loads to display costs. ce:GetCostAndUsage is granted to the Lambda role and deliberately NOT to app-api's, so no request path can spend billing dollars however often it is called. Steady state is ~$0.02/day. Two sources, never added twice. Inference stays on our own ledger (per user and per session, where CE is per-account only); infrastructure comes from CE. CE's own inference figure rides along ONLY to reconcile the two — adding it to a total would count every token twice. The Platform tab surfaces that comparison as a pricing regression test: a widening delta means the rates in curated-models.ts have drifted from what AWS actually charged, which CLAUDE.md names as a live risk. Attribution is a service allowlist, not tags. applyStandardTags already puts Project/Environment/Version on every resource, but no cost allocation tag is activated in either account, so GROUP BY TAG Project returns one Project$ bucket holding the whole bill. Activation is an org-management-account action (a Control Tower linked account gets AccessDeniedException listing them) and is not retroactive. Meanwhile the accounts are shared — bsu-prod-backend runs an Aurora cluster we do not provision — so services are bucketed inference/platform/excluded, and excluded rows are persisted and DISPLAYED rather than dropped: a total is only trustworthy if its exclusions are visible. Opt-in, against this repo's usual default-on posture, because this flag reads the account's billing data, needs an IAM action an SCP may deny, and costs money per call. Only the literal "true" enables; an unset workflow variable arrives as an empty string and leaves it off. While off the construct produces zero resources and the tab explains that rather than rendering a $0.00 that would read as "infrastructure is free". Known limitation, stated in the UI and the spec: infrastructure is measured per AWS account, and dev-ai hosts five deployments, so dev's figures cover all of them. Prod is a dedicated account and is sound. Activating the Project tag is the fix; the row shape does not change when it lands. Also fixes two icons that rendered wrong. Utility classes on <ng-icon> do not apply: its :host width/height rule is UNLAYERED so it outranks Tailwind's @layer utilities size-*, and its colour rule sits in @layer ng-icon (registered after Tailwind's layers) resolving to currentColor. Measured size-5 rendering at 16px and text-state-warning-600 resolving to black, while the same class on a plain span resolved correctly. Colour now lives on a wrapper and dimension on ng-icon's own [size] input; the status glyph pairs -700/dark:-400, measured 5.03 light and 8.52 dark. Verified by running the sync Lambda against dev-ai end to end: August came back 519.02 platform + 13.56 inference + 203.74 excluded = $736.32, matching the bill measured independently. Tests: 35 handler, 9 CDK, 233 backend cost, plus full suites green — 9,631 backend, 896 CDK, 3,701 SPA. See docs/specs/platform-cost-visibility.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…count An account is not an application. This stack is open source, so a deployer may share an account with other workloads — and ours does: dev-ai hosts FIVE deployments of this stack plus unrelated apps, and both accounts run a bsu-*-backend Aurora cluster we do not provision. prod-ai happens to hold exactly one deployment today, but that is luck, not design, and no fork should inherit an assumption that only holds for us. The sync now asks for this deployment's own resources first, filtering on the Project tag applyStandardTags already writes. Its value is the stack's projectPrefix, so there is nothing to configure and a fork gets it free. Cost Explorer will not filter by a cost allocation tag until that tag is ACTIVATED in the payer account — an Organizations member gets AccessDenied merely listing them, and activation is not retroactive. So the scope is resolved at runtime rather than assumed: query scoped, and fall back to account-wide if it comes back empty. Verified against prod that an inactive tag returns HTTP 200 with zero groups and $0.00 rather than an error, which is exactly what the probe keys on. We deliberately do NOT call ListCostAllocationTags — that is the call a linked account is denied, and a linked account is the topology most likely to need the fallback. Every row records which answer the deployer got. `scope: "deployment"` means the figures are this stack's; `scope: "account"` means they are a ceiling, and the UI says so in three places that all disappear once the tag is live: a "Whole account" badge, a panel naming the exact fix and the tag value to activate, and the reconciliation copy leading with scope instead of blaming pricing drift. Rows written before scoping existed carry no scope and default to "account" — the pessimistic default is deliberate, because defaulting the other way would relabel an account-wide figure as this app's cost with nothing to reveal the error. Also sets propagateTags: SERVICE on the Fargate service. Fargate bills per TASK and tasks do not inherit a service's tags without it; the live dev service reported NONE, which would have dropped ~17% of prod's infrastructure (~$118/month) out of the tagged scope while everything still looked healthy. That is the kind of gap that makes a scoped total quietly wrong rather than obviously broken. Tag coverage audited across 1,519 dev resources before relying on it: ECR/ELB/Cognito/SQS 100%, DynamoDB 98%, KMS 96%, CloudWatch 91%, and the two largest lines confirmed individually — our NAT gateway and our AgentCore runtime both carry Project=dev-boisestateai-v2, while other stacks' carry theirs or none. Verified end to end against dev: the sync probed the tag, logged an actionable warning naming the fix, fell back, and still produced usable data (scope=account, $321.28 platform). The dashboard renders the badge, the warning panel and the scope-first reconciliation copy in both themes; badge contrast 5.03 light. Tests: 45 handler (10 new for scoping), 11 CDK (2 new), 235 backend cost (2 new), full suites green — 898 CDK, 3,701 SPA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"These figures cover the whole account" describes a condition and leaves the reader to hunt for what to do about it — and the instruction that mattered was buried two paragraphs down in the smallest text on the card. The heading now states the action: activate Project as a cost allocation tag in the payer account. Three changes, all about making it land: - Moved above the figures it qualifies. A caveat printed after the numbers is one people read second, if at all. - Heading carries the fix, with the tag name in a code chip; the "whole account" condition moves to the supporting line where it belongs. - Where / Tag / Then rows replace the small-print paragraph, so the payer account requirement, the exact tag value already on every resource, and the not-retroactive warning each stand on their own instead of running together. Card is now a state-warning tint rather than a white card with a warning border, which is what makes it read as an instruction at a glance. Contrast measured in both themes: icon 4.85 light / 9.53 dark, heading 17.11 / 16.42, body 9.94 / 11.15, code chips 17.75 / 18.89. The icon stays at -700 rather than -600 precisely because it now sits on its own tint, where -600 measures 2.87 and fails. Still conditional, so it disappears the moment the tag is activated and never becomes furniture. Also drops a "see below" that the move made wrong. SPA suite green (296 files / 3,701 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four states observed against dev's real catalog (which carries two `disabled` rows) with local branch code: retiring-and-off renders the badge with a disabled switch; retiring-and-bound renders selected, removable, and named in the section notice; one click removes it and the chip disables in the same frame; `active` controls are untouched. Contrast measured canvas-normalized (the tokens resolve to `oklch()`, which a naive rgb() parse gets wrong) — AA clear in both themes, 4.85:1 the tightest. Also records that `POST /agents/` accepted a binding to a `disabled` tool, which is the design rather than a gap: it is why a throwaway agent could stand in for a fixture here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§7 told a user a tool was going away and nothing else. The Designer notice was
worse than silent — it said "please remove it before the retirement date",
pointing at a fact the UI never carried. The only place a replacement was ever
named was an email template in the runbook.
Two nullable fields on `ToolDefinition`, threaded out through `UserToolAccess`,
`AdminToolResponse` and `BindableItem.meta`:
- `retirementNote` — free text, <=300 chars ("Replaced by Canvas for Faculty",
"No replacement — contact OIT")
- `retiresOn` — ISO date; the day Stage 3 revokes the grant
Free text rather than a `replacedBy` tool id because a retirement often has no
drop-in successor, or splits across several, and an id cannot say so. A
structured pointer can be added underneath this later; it cannot replace it.
Both are display-only — nothing reads them for access, and neither reaches the
model's `toolConfig`, so they cost nothing per turn. Neither is gated on
`status`: `isRetiring` alone decides what renders, so a note typed before the
status is flipped is not discarded. `retiresOn` is stored as a string because it
is displayed, never computed with, which is exactly why it is validated on the
way in — without the guard "soon" would persist and reach the SPA.
`retirementDetail()` composes both into the one sentence every surface appends
after its own lead-in, so the card, the detail page, the Designer notice and the
schedule chip cannot drift into four phrasings. Both fields are independently
optional, so it has four shapes; the empty one renders nothing, because "no
replacement is available" would be a claim invented on the admin's behalf. The
Designer notice is the sole exception, falling back to a sentence that promises
no date it cannot show.
⚠️ The date is parsed at UTC noon: `new Date('2026-10-31')` is midnight UTC and
prints as the 30th everywhere west of Greenwich, and a retirement date that
reads a day early is the one kind of wrong here that costs someone. `Date.UTC`
also rolls over rather than failing — `2026-13-45` becomes February 2027 — so
the parts are re-read and compared after construction. A new test caught that
one before it shipped.
Admin form gets both inputs, revealed when Status leaves `active`. Hidden, not
disabled: toggling Status while drafting must not silently discard what was
typed.
Verified live against dev on all five surfaces, including both notice shapes
side by side (one tool with metadata, one without) and the hide/restore cycle on
the admin form. Backend 9662 passed (9 pre-existing unmocked-AWS errors in
inference/quota fixtures, identical on the main checkout's unrelated branch);
SPA 3659 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Model tiles showed the vendor's corporate logo for every model, so all four Claude rows rendered Anthropic's "A". A user scanning the picker recognises the Claude starburst, not the company mark. `resolveModelIcon` now resolves in the order upload -> explicit slug -> model family -> company -> monogram. The model-family step matches on `modelId` rather than `modelName`: the id is structured and ours (`us.anthropic.claude-sonnet-4-6`), while the name is free text an admin can rewrite. Patterns anchor on a `.` boundary so the CRIS prefix, which differs between dev (`us.`) and prod (`global.`), cannot change which logo a model gets. New slugs: claude, kimi, qwen (family marks) and google (company). `anthropic` is kept rather than replaced -- removing a slug would orphan any record already saved with it, since the backend rejects an unknown slug on the next write, and it stays correct for a non-Claude Anthropic model. Fixes a live gap: Qwen3 Coder 30B matched neither lookup table and rendered no logo at all. Gemma is the one deliberate exception to family-beats-company. Its mark is a gem drawn with construction scaffolding -- guides 0.186 units wide in a 24-unit box, ~0.2px at the 28px tile -- which smears into a blob, and the colour variant fades to #B1C5FF against a near-white tile. Stripping the scaffolding would invent a mark Google does not publish and land a near-twin of the Gemini spark. The exception is commented and pinned by a spec. Also removes `PROVIDER_LOGO_DIR` from the catalog page. The curated cards carried a second vendor->logo table, so adding a slug meant editing two places and a card could disagree with the model it creates; they now call the shared resolver. The admin form's caption distinguishes "Matched from the model ID" from "Matched from the provider name" via a new `via` field, keeping a caption that exists precisely to avoid claiming a match that did not happen. Verified: all 11 curated models resolve correctly (4 Claude -> claude, 5 GPT -> openai, Qwen -> qwen, Kimi -> kimi, none unresolved), all 16 SVGs serve from a running dev server, and Gemma ids resolve to google across provider-name and CRIS-prefix variants. Typecheck clean; 3710 SPA and 22 backend icon tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lytics-tabs-9f8cc9 feat(admin): tabbed cost analytics + all-in platform cost from the AWS bill
…o-light-dark-f8cb67 feat(spa): prefer model-family logos, falling back to company marks
…tirement-metadata feat(tools): tell users what replaces a retiring tool, and when it stops
The offloader has never offloaded anything. `_OffloaderMixin` overrides `ContextOffloader._handle_tool_result` without re-applying Strands' `@hook` decorator, and `strands.plugins._discovery` resolves each MRO name through `getattr(instance, name)` — so the undecorated override answers for the decorated base method, fails the `_hook_event_types` predicate, and the callback is dropped. The plugin still loads and still publishes `retrieve_offloaded_content`; it just silently bounds nothing. Measured in prod: a single `list_events` result put 107,802 tokens into the prompt in one step (11.7k -> 119.5k of a 200k window). Compaction could not recover it — the result sat inside `protected_turns`, so the cut logged `floor_unreachable` and reclaimed a 386-token summary while the payload stayed in the cacheable prefix for 15 more model calls. Four TTL-expired re-writes of that inflated prefix accounted for roughly 75% of the session's spend. Carry the base's registration onto the override rather than re-decorating: `@hook` infers the event type from the annotation via `get_type_hints`, which resolves against the function's module globals, and `AfterToolCallEvent` cannot be imported at this module's scope without pulling in boto3 and the plugin — the thing the lazy build exists to avoid. Copying also tracks upstream if the callback is ever bound to a different event. The existing tests all call `_handle_tool_result` directly, which is why they stayed green through the outage. `TestHookRegistration` asserts the callback is discoverable, bound to `AfterToolCallEvent`, and that the discovered bound method really offloads; all three fail without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1240) * feat(agents): configurable source citations & document download (#111) Per-agent showCitations / allowDocumentDownload flags (both default true, no migration -- legacy rows read as today via Assistant extra="allow"). - Models/compat/service/routes thread the two flags through Assistant, Create/Update requests, AssistantResponse, AgentResponse and to_agent_view. - Inference chat route gates citation SSE emission + persistence on show_citations (single choke point; RAG retrieval/prompt untouched). - Document-download endpoint returns 403 unless BOTH flags are on (downloads implicitly off when citations off). - Agent form gains a Sources section: "Show source citations" + nested "Allow document downloads" (disabled when citations off); citation card hides its download button via an allowDownload input. Also fixes citation fileName showing "Unknown Source" for managed KBs: read metadata["filename"] (managed) before "source" (legacy). Tests: model round-trip/defaults/opt-out, download 403 floor, citation spec. * fix(agents): make #111 Agent flags optional on the client type showCitations/allowDocumentDownload were required on the Agent interface, which broke existing Agent fixtures/mocks that don't set them (TS2739/TS2322/TS2352 in the frontend build). Backend always sends both (default true) and all readers use '?? true', so relax them to optional — matching the Assistant interface.
Managed KBs returned near-identical content two ways that reached both the model prompt and the citation cards: the image-to-text parser repeated a sentence inside one chunk, and 20%-overlap chunking let reranking surface substantially-identical chunks. Both flow from one formatted result list, so dedup lives in the rag_service facade above the backend seam (engine-agnostic, no re-ingest, no chunking change): collapse repeated sentences/lines within a chunk, then drop cross-chunk near-duplicates (exact/substring/high token-overlap), order-preserving and best-first. 18 new unit tests; conservative thresholds leave genuinely distinct overlap-window neighbours in place.
…er-hook-registration fix(agents): register the tool-result offload hook
Composition plan for Claude/ChatGPT-style Shared Projects over the existing Agent record, Memory Spaces, scheduled runs, conversation shares and audit log. Includes the Phase 0 AgentCore Memory audit, data models, memory file format and validation, API surface, harness assembly, maintenance pipeline, configuration, and phased PRs. Decisions recorded: project harnesses degrade with notice on a missing binding (§9.6); memory content lint defaults to warn (§9.3). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… per rung The quota warning tab and the session cost/context badge each took their own line above the composer. They now share a single meta row: the tab centred on the composer, the badge on the right, stacking only when the composer is narrower than 512px. Dismissing a quota warning now acknowledges its rung. The backend re-sends the warning every turn with a new currentUsage, which used to reset the dismissal, so the warning came back on every turn and every new conversation. The dismissed rung is persisted in localStorage and stays hidden until a higher rung fires; it is forgotten when usage drops (new period) or the quota limit changes, and cleared on sign-out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ta-warning-layout-f8fadd fix(spa): quota tab and cost badge on one row; dismissals stick per rung
…-feature-plan-b36fe8 docs(specs): Shared Projects implementation plan
Admin-managed always-on tools, the tool retirement workflow, and fuller cost and quota visibility for users and admins. Also: per-conversation composer drafts, per-agent citation/download controls, Kimi K3, and a fix for the tool-result offloader that 1.23.0 shipped but never ran. - Bump VERSION 1.23.0 -> 1.24.0 and sync all manifests and lockfiles - CHANGELOG.md and RELEASE_NOTES.md entries for 1.24.0 - CDK deploy required; closes Cognito self-signup wherever CDK_COGNITO_SELF_SIGNUP_ENABLED is unset (all environments today) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
| from __future__ import annotations | ||
|
|
||
| from types import SimpleNamespace |
| """ | ||
|
|
||
| from datetime import datetime, timezone | ||
| from types import SimpleNamespace |
config.ts reads it and the platform-cost-visibility spec tells operators to set it, but platform.yml never passed it through, so the GitHub variable had no effect and only cdk.json context could enable the sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 72c8a22)
…TS_ENABLED Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 1.24.0: a minor release covering #1208 through #1243 on
develop.Headline
CDK_COGNITO_SELF_SIGNUP_ENABLEDis not set on any environment, and the default is now closed (feat(cognito): close self-signup by default via config, drop the runtime disable #1211). Federated sign-in and first-boot are unaffected.CDK_PLATFORM_COSTS_ENABLEDGitHub variable. The workflow fix (fix(ci): forward CDK_PLATFORM_COSTS_ENABLED to the platform deploy #1245) is cherry-picked onto this branch.Checklist
VERSIONbumped andsync-version.sh --checkpassesCHANGELOG.mdandRELEASE_NOTES.mdprepended; earlier entries untouchedmainintodevelopwith a merge commit🤖 Generated with Claude Code