diff --git a/CHANGELOG.md b/CHANGELOG.md index 637c3473..58522f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project are documented in this file. Format follows For narrative release notes written for operators and product owners, see [RELEASE_NOTES.md](RELEASE_NOTES.md). +## [1.19.1] - 2026-09-07 + +A patch release. Two MCP Apps defects made an App look broken while the tool behind it had really run: an app-initiated `tools/call` relayed an empty result back to the iframe, and a call made between turns hit a torn-down MCP session and came back as a 502. Both are fixed at the dispatch boundary. The artifact library listing now serves from `UserArtifactsIndex` instead of the base table, which retires the ~3x read amplification and the per-request in-memory sort. **No CDK deploy and no infrastructure change** — but the index that 1.19.0 shipped as groundwork is now on the read path, so its backfill has moved from optional to **required before deploying**. + +### ⚡ Performance + +- **The artifact library listing reads `UserArtifactsIndex`.** `list_for_user` queries the index (`GSI2PK=USER#{uid}`, `GSI2SK` descending) rather than the base table. HEAD and version rows share a base partition, so the old Query scanned roughly 3x the rows it returned and then date-sorted them in memory; only HEAD rows carry the GSI2 keys, so the index holds one row per artifact already newest-first. Ordering now comes from the store instead of being recomputed per request. The response still returns the whole library in one payload, paging the index internally (#989) + +### 🐛 Fixed + +- **App-initiated `tools/call` returned empty content to the iframe.** `_serialize_content` read the result with `getattr`, but Strands' `MCPToolResult` extends `ToolResult`, a `TypedDict` — so `call_tool_sync` returns a plain dict at runtime and the attribute lookup found nothing. The failure was silent end to end: app-api returned 200, inference-api returned 200, and the MCP server had really run the tool, so a write took effect while the App received nothing to render. Any MCP App that re-reads state after an edit appeared frozen (#993) +- **App-initiated tool calls between turns failed with an intermittent 502.** A call arriving after a turn ended resolved to a cached agent whose MCP client sessions Strands had already torn down, raising `MCPClientInitializationError` — which surfaced as `AppToolCallError(502)` in the App. It looked intermittent because a call made while the turn was still streaming found the session alive. The client is now reconnected for the duration of the call and left as it was found; a session already live belongs to an in-flight turn and is used as-is, never stopped, and overlapping calls against the same client share one revived session through a refcount (#994) + +### ⚠️ Changed + +- **`backfill_artifact_user_index_keys.py` now stamps undated rows instead of reporting them.** A HEAD row with no `updated_at` was previously counted and named but left unstamped, on the grounds that a fabricated timestamp would sort wrongly forever. With the index on the read path that choice would drop the artifact from a sparse index — and from its owner's library — silently and permanently. Such a row is now stamped with an empty timestamp segment (`ARTIFACT##{aid}`), which sorts below every real timestamp and so reads last, exactly where the previous in-memory sort put it. **Re-run the script if you ran the 1.19.0 version and it reported any undated rows** (#989) + ## [1.19.0] - 2026-09-06 A correctness release for interrupted turns, plus the share inbox coming out of the dark. Two separate defects made a conversation misreport its own history: a completed response could be labelled **"Response interrupted"** with a Continue button, and an interrupted one could show the model-directed `` in the user's own chat bubble — permanently. Both are fixed at the source rather than patched at the render. The artifact **"Shared with you" inbox now ships on by default** with a kill switch, so a fork gets the finished feature instead of having to discover a variable. Infrastructure adds `UserArtifactsIndex` to the existing `{prefix}-user-artifacts` table (one GSI operation) with a backfill for rows that predate it; nothing reads the index yet. **Requires a CDK deploy**, and two one-shot scripts are available post-deploy. diff --git a/README.md b/README.md index d74ef909..70720035 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **An open-source, production-ready Generative AI platform for institutions** *Built by Boise State University, designed for everyone.* -[![Release](https://img.shields.io/badge/Release-v1.19.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) +[![Release](https://img.shields.io/badge/Release-v1.19.1-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) [![Nightly](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml/badge.svg)](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml) ![Python](https://img.shields.io/badge/Python-3.13+-3776AB?style=flat&logo=python&logoColor=white) @@ -296,7 +296,7 @@ agentcore-public-stack/ See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full changelog, including new features, bug fixes, platform upgrades, and deployment notes for each release. -**Current release:** v1.19.0 +**Current release:** v1.19.1 --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3bf62395..64c2bd30 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,62 @@ +# Release Notes — v1.19.1 + +**Release Date:** September 7, 2026 +**Previous Release:** v1.19.0 (September 6, 2026) + +--- + +> 🏗️ **No CDK deploy required.** No infrastructure changed in this release — `infrastructure/gsi-inventory.json` is byte-identical to `main`, and no table gains or loses an index. +> +> ⚠️ **One prerequisite carried forward from 1.19.0, now mandatory.** `UserArtifactsIndex` shipped in 1.19.0 with nothing reading it. This release puts it on the artifact library's read path. Before deploying, the index must report `ACTIVE` **and** `backfill_artifact_user_index_keys.py` must have been applied — an unstamped row is absent from a sparse index, which means an artifact missing from its owner's library. See Deployment notes. + +--- + +## Highlights + +A patch release with two MCP Apps fixes and one performance change. Both defects had the same shape — the tool behind an App really ran, and the App showed nothing for it. One relayed an empty result to the iframe because the MCP client returns a dict where the code expected an object; the other hit a torn-down MCP session on any call made between turns and surfaced as an intermittent 502. Separately, the artifact library listing moves off the base table onto `UserArtifactsIndex`, retiring the read amplification and the per-request sort that the index was added to remove. + +## 🐛 Bug fixes + +- **An MCP App could issue a tool call, have it succeed, and render nothing.** `_serialize_content` read the tool result's content with `getattr`, but Strands' `MCPToolResult` extends `ToolResult` — a `TypedDict`, so what `call_tool_sync` returns at runtime is a plain dict and the attribute lookup found nothing. Every app-initiated `tools/call` therefore relayed `content: []`. Nothing in the chain reported a problem: app-api returned 200, inference-api returned 200, and the MCP server had genuinely run the tool, so a write took effect while the App received nothing to show for it — any App that re-reads state after an edit simply appeared frozen. The dict shape is now handled alongside the attribute one, mirroring the branch `_is_error` already had. The existing dispatch-test fakes were objects carrying a `.content` attribute, which is exactly why the attribute-only path looked correct; the new test uses the dict shape the client really returns (#993) +- **App-initiated tool calls between turns failed with a 502 that looked intermittent.** Such a call arrives after the turn that built the agent has ended. `routes.py` rebuilds the conversation's agent, but with `cache_write=False` it reads a *cached* agent — and Strands tears that agent's MCP client sessions down when its turn ends. `_resolve_client` then handed back a client whose session was no longer running, `call_tool_sync` raised `MCPClientInitializationError`, and that became an `AppToolCallError(502)` reaching the App as a Bad Gateway. The intermittence was the tell: a call made while the turn was still streaming found the session alive and worked. The call is now wrapped so the client is reconnected for its duration and left as it was found. A session already live belongs to an in-flight turn and is used as-is, never stopped here, and overlapping app calls against the same client share one revived session through a refcount, so no call has the connection closed underneath it. The fix is deliberately kept at the dispatch boundary — resolving the live client from the freshly built agent is the deeper fix, but it reaches into how tool providers are held and cached (#994) + +## ⚡ Performance + +**The artifact library listing now serves from `UserArtifactsIndex`.** 1.19.0 added the index and deliberately left it unread; this release switches the read over. `list_for_user` queries `GSI2PK=USER#{uid}` with `GSI2SK` descending instead of querying the base table. + +The base partition holds both HEAD and version rows, so the old Query spanned roughly three times the rows it returned and then date-sorted them in memory on every request. Only HEAD rows carry the GSI2 keys, so the index holds one row per artifact and already in newest-first order — both the amplification and the sort go away, and the ordering comes from the store rather than being recomputed per call. + +The endpoint still returns the whole library in one response, paging the index internally. Exposing pagination is a larger change than it looks: search and the type filter live in the SPA today, and a filter that can only see the loaded page is worse than no filter because it looks authoritative — both would have to move server-side in the same change. The index makes that possible whenever it is wanted. + +### Two things this turned up + +**The library tests were passing against the old code path.** The test fixture declared no `GlobalSecondaryIndexes` at all, so a suite that should have required an index went green without one. The fixture now declares it, which makes moto raise `ResourceNotFoundException` if the query ever stops using the index — the tests exercise the index rather than silently falling back. + +**Undated rows would have vanished.** A sparse index omits any HEAD row without `GSI2PK`, permanently and silently. Rows predating `updated_at` cannot carry a real timestamp, and the original backfill reported them rather than stamping them — correct while nothing read the index, and a silent data-loss path the moment something did. They are now stamped with an empty timestamp segment (`ARTIFACT##{aid}`): not a fabricated time, but a key that sorts below every digit and so reads last when the index is read descending — exactly where the old in-memory sort put it. Neither dev nor prod holds such a row today; this is the defensive branch, and it preserves a contract `test_undated_legacy_rows_are_returned_and_sort_last` already asserted. + +### Test Coverage + +Backend suites for app_api, architecture and the artifact writer pass at 942 tests, with the library fixture now index-backed. The MCP Apps fixes add dispatch tests using the dict result shape the MCP client actually returns and covering session revival, reuse of an already-live session, and refcounted overlap. + +## 🚀 Deployment notes + +**No CDK deploy is required** — this release changes no infrastructure. `backend.yml` and `frontend-deploy.yml` are sufficient. + +**Before deploying, confirm the 1.19.0 index groundwork is complete.** The artifact library now reads `UserArtifactsIndex`, so two things that were optional last release are prerequisites now: + +1. The index reports `ACTIVE` — `UPDATE_COMPLETE` on the stack is not the same thing: + + ```bash + aws dynamodb describe-table --table-name -user-artifacts \ + --query 'Table.GlobalSecondaryIndexes[].{Name:IndexName,Status:IndexStatus}' + ``` + +2. `backend/scripts/backfill_artifact_user_index_keys.py` has been applied. The index is sparse: an unstamped HEAD row is *absent* from it, not stale, so an artifact written before 2026-09-04 and never backfilled disappears from its owner's library after this deploy. The script is dry-run unless given `--apply`. + +**Re-run the backfill if the 1.19.0 version reported any undated rows.** That version counted rows with no `updated_at` and left them unstamped; this version stamps them with an empty timestamp segment so they sort last instead of dropping out. A re-run is idempotent and skips rows already stamped. + +--- + # Release Notes — v1.19.0 **Release Date:** September 6, 2026 diff --git a/VERSION b/VERSION index 815d5ca0..66e2ae6c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.19.0 +1.19.1 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3add5959..f85da038 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-stack" -version = "1.19.0" +version = "1.19.1" requires-python = ">=3.10" description = "Multi-agent conversational AI system with AWS Bedrock AgentCore" readme = "README.md" diff --git a/backend/uv.lock b/backend/uv.lock index c995f68c..5f2c7f6b 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ [[package]] name = "agentcore-stack" -version = "1.19.0" +version = "1.19.1" source = { editable = "." } dependencies = [ { name = "aiofiles" }, diff --git a/frontend/ai.client/package-lock.json b/frontend/ai.client/package-lock.json index 3fd84055..c8a2b8b8 100644 --- a/frontend/ai.client/package-lock.json +++ b/frontend/ai.client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai.client", - "version": "1.19.0", + "version": "1.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai.client", - "version": "1.19.0", + "version": "1.19.1", "dependencies": { "@angular/cdk": "21.2.14", "@angular/common": "21.2.19", diff --git a/frontend/ai.client/package.json b/frontend/ai.client/package.json index 188c8a95..276bb989 100644 --- a/frontend/ai.client/package.json +++ b/frontend/ai.client/package.json @@ -1,6 +1,6 @@ { "name": "ai.client", - "version": "1.19.0", + "version": "1.19.1", "scripts": { "ng": "ng", "prestart": "tsx scripts/branding/generate-brand-theme.ts && tsx scripts/branding/generate-surface-theme.ts && tsx scripts/branding/generate-surface-colors.ts && tsx scripts/branding/generate-favicons.ts", diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json index f883411b..2d0c39c1 100644 --- a/infrastructure/package-lock.json +++ b/infrastructure/package-lock.json @@ -1,12 +1,12 @@ { "name": "infrastructure", - "version": "1.19.0", + "version": "1.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "infrastructure", - "version": "1.19.0", + "version": "1.19.1", "dependencies": { "aws-cdk-lib": "2.265.0", "constructs": "10.6.0" diff --git a/infrastructure/package.json b/infrastructure/package.json index 5e4e28bb..7899cf9b 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -1,6 +1,6 @@ { "name": "infrastructure", - "version": "1.19.0", + "version": "1.19.1", "bin": { "infrastructure": "bin/infrastructure.js" }, diff --git a/tui/pyproject.toml b/tui/pyproject.toml index c5652247..859f222e 100644 --- a/tui/pyproject.toml +++ b/tui/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-tui" -version = "1.19.0" +version = "1.19.1" requires-python = ">=3.11" description = "Terminal client for the AgentCore Public Stack — streaming AI chat in your terminal" readme = "README.md" diff --git a/tui/src/agentcore_tui/__init__.py b/tui/src/agentcore_tui/__init__.py index bff8d598..70e17e45 100644 --- a/tui/src/agentcore_tui/__init__.py +++ b/tui/src/agentcore_tui/__init__.py @@ -7,6 +7,6 @@ from __future__ import annotations -__version__ = "1.19.0" +__version__ = "1.19.1" __all__ = ["__version__"] diff --git a/tui/uv.lock b/tui/uv.lock index bb665a8d..8b06354f 100644 --- a/tui/uv.lock +++ b/tui/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentcore-tui" -version = "1.19.0" +version = "1.19.1" source = { editable = "." } dependencies = [ { name = "httpx" },