Skip to content

Release/1.9.0#707

Merged
philmerrell merged 1728 commits into
mainfrom
release/1.9.0
Jul 20, 2026
Merged

Release/1.9.0#707
philmerrell merged 1728 commits into
mainfrom
release/1.9.0

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

v1.9.0 — Prompt-cache stability + observability

Feature release making Bedrock prompt-cache economics stable and measurable.

Prompt-cache stability (#697) — three cache-busting defects fixed in the model-call path:

  • Restored history is byte-stable between turns (persisted truncation_anchor replaces the sliding truncation window that re-mutated history — and re-wrote the whole cached prefix — nearly every turn)
  • Skills inject into the system prompt in deterministic order (sorted at repository, RBAC-union, and injection layers)
  • Requests carry 3 of Bedrock's max-4 cachePoints (toolConfig + system + last-user-message) so a message-level miss still reads the stable prefix

Prompt-cache observability (#697, #699, #700, #701):

  • Prefix fingerprints + write-time cacheStatus (first_write | hit | miss_ttl_expired | miss_avoidable | uncached) + wastedUsd on every cost row; session-level cache rollups; GET /admin/costs/sessions/{id}/calls; per-call EMF metrics; PROMPT_CACHE_OBSERVABILITY_ENABLED kill switch (default ON)
  • Admin Session Cost Anatomy page at /admin/costs/sessions/:id with fingerprint diffing that names the cache-buster
  • PromptCacheObservabilityConstruct: CloudWatch dashboard + console-only alarms over AgentCoreStack/PromptCache
  • first_write no longer misclassified as miss_avoidable after below-threshold calls

Fixes:

Deploy: platform.yml (CDK required — dashboard construct + Runtime env var) → backend.ymlfrontend-deploy.yml. No backfills, no data migration, no dependency changes.

Squash-merge per release convention; backmerge PR into develop follows.

🤖 Generated with Claude Code

philmerrell and others added 30 commits July 7, 2026 10:07
…ness-q2-probe-result

docs(kaizen): managed-Harness Q2 live-probe result — GO-with-boundary
…areable

Reframes the per-user markdown memory spec into the Memory Space primitive:
a named, first-class, bindable, templated, and shareable markdown wiki.
Oliver becomes a Chief-of-Staff template + a bound agent rather than a
special-cased feature.

- Adds the three-layer abstraction (Agent / Memory Space / declarative
  binding) and the structural-config vs. semantic-MEMORY.md split.
- Space-keyed storage (SPACE#{id} + membership records + UserSpacesIndex
  GSI) so sharing is expressible from day one.
- Entry types (entity / episodic / fact), Space Templates, and manifest-
  indexed fields for relational/temporal queries ("who owes what").
- Sharing via owner/editor/viewer grants mirroring assistant collab-edit,
  with run-as-user write attribution.
- Data governance is proportionate: same data class as sessions/artifacts
  behind the same Entra JWT + RBAC — identity-based, no content-inspection
  gate. Deletion-purge + inherited encryption are the only real items.
- 8-PR phasing; PR-1 (data layer) is the next buildout phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…paces-spec

docs(memory): Memory Spaces — bindable, templated, shareable second-brain primitive (F5)
…ess findings

PR #576 merged from a state prior to commit 908f7e1, orphaning the
resourceOauth2ReturnUrl parameter-shape + harness-security cross-check
subsection. This re-applies it onto develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rn-url-recovery

docs(kaizen): recover resourceOauth2ReturnUrl section orphaned by #576 merge timing
Add a first-class "download the entire space as a .zip of raw markdown"
capability (index + all entries, structure preserved, metadata.json),
framed as the user-ownership / zero-lock-in property. Promotes the stubbed
export endpoint into §9 with contents, access, streaming mechanics, and an
import-friendly round-trip note; folds the zip export into PR-5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pace-export-zip

docs(memory): full-ownership zip export of a Memory Space
Adds the F5 Memory Space primitive's data layer — no runtime wiring,
gated by MEMORY_SPACES_ENABLED (default off).

Backend (apis/shared/memory/):
- store.py: S3 content-addressed byte store (sibling of the skills store)
- models.py: MemorySpace / MemoryIndex / MemoryEntryRef / SpaceMember
- templates.py: Blank / Chief-of-Staff / Research-Notebook presets
- repository.py: dedicated memory-spaces table CRUD (META/INDEX/MEMBER
  rows, OwnerIndex + MemberIndex GSIs, Decimal handling)
- service.py: permission-gated lifecycle + sharing + entry/index I/O,
  resolve_permission chokepoint (viewer reads, editor writes, owner
  shares/deletes), content-addressed writes with GC-on-replace
- feature_flags.py: memory_spaces_enabled() (default off)
- 47 moto-backed tests; import boundaries clean

Infrastructure:
- MemorySpacesConstruct: S3 bucket + dedicated memory-spaces DynamoDB
  table (OwnerIndex/MemberIndex GSIs), a per-domain table matching the
  project's actual pattern
- Threaded via PlatformComputeRefs to both compute roles (readwrite S3 +
  DynamoDB); env vars S3_MEMORY_SPACES_BUCKET_NAME /
  DYNAMODB_MEMORY_SPACES_TABLE_NAME / MEMORY_SPACES_ENABLED
- config.ts flag default-off; resource-count assertions updated
- tsc + 429 jest tests pass

Spec updated to reflect the dedicated-table + two-GSI decisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…paces-pr1-data-layer

feat(memory): Memory Spaces data layer (PR-1)
…orkstreams

Splits the plan into two workstreams to keep the Memory Space a clean
bindable primitive: Workstream A (this epic) delivers the primitive + the
user-facing "own your data" surface (data layer, app-api CRUD, export,
sharing, SPA panel, consolidation); Workstream B (Agent/Harness layer)
delivers agent-consumption — the memory_* tools, declarative binding, and
system-prompt index injection — so any run surface can bind the same
primitive rather than welding it to inference-api.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eslice-phasing

docs(memory): re-slice phasing — primitive vs. agent-consumption workstreams
Workstream A2 of the re-sliced memory epic: the user-facing "own your data"
surface over the Memory Space primitive. No agent-consumption (tools /
binding / prompt injection) — that's the Agent/Harness workstream.

app-api (apis/app_api/memory_spaces/):
- routes.py: /memory/spaces CRUD over MemorySpaceService — list (with
  templates + accurate per-space role), create-from-template, get (index +
  entry manifest), delete-or-leave, entry read/list/upsert/delete, index
  read/update. Sync handlers (FastAPI threadpools the sync boto3 service).
- Gated by require_memory_spaces_user: 404 while MEMORY_SPACES_ENABLED off
  (surface behaves as unmounted); cookie auth via get_current_user_from_session.
- Service errors translated NotFound->404, Permission->403, Error->400.
- models.py: camelCase request/response models.
- Mounted before the existing /memory (AgentCore Memory) router; paths are
  non-overlapping (/memory/spaces vs /memory/{record_id}).

shared service:
- leave_space(): a member drops their own grant (the shared-in forget-me
  case); owner cannot leave.
- list_spaces_for_user() now returns (space, role) so shared-in spaces carry
  the member's real viewer/editor grant (only consumer is the new route).

Tests: 12 route tests (moto-backed real service, flag gate, CRUD, 403/404,
member-leaves-via-delete) + leave_space service tests. Full memory suite +
import boundaries green (69 passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2-app-api-surface

feat(memory): Memory Spaces user surface — /memory/spaces CRUD (A2)
The "own your data" leg of Workstream A: a loss-free `.zip` download of a
space's raw markdown (§9). `MemorySpaceService.export_space` gathers the
corpus once (index + every entry's bytes) behind the viewer+ permission
gate, including the member grant list only for editor+ callers (mirrors
`list_members`). The app-api route builds the archive in a
`SpooledTemporaryFile` — spilling to disk beyond 8 MiB so a large space
never pins memory — and streams it back.

Zip mirrors the S3 layout (`{name}/MEMORY.md`, `entries/<type>/<slug>.md`,
`metadata.json`) so it is self-contained and re-importable later. Archive
path components are sanitized against zip-slip. Route tests cover layout,
verbatim frontmatter, owner-vs-viewer member disclosure, 403/404/flag-off,
and the hostile-slug case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3-export-zip

feat(memory): Memory Space zip export — /memory/spaces/{id}/export (A3)
…(A4)

The sharing leg of Workstream A, plus the concurrency guarantee that makes
multi-editor spaces safe.

Sharing surface (app-api, over the existing service grant methods):
- GET    /memory/spaces/{id}/shares          list grants (editor+)
- POST   /memory/spaces/{id}/shares          grant viewer|editor (owner)
- PATCH  /memory/spaces/{id}/shares/{email}  change a grant's role (owner)
- DELETE /memory/spaces/{id}/shares/{email}  revoke (owner, idempotent)
New `MemorySpaceService.update_share` gives PATCH proper not-found semantics
and preserves the grant's original createdAt (distinct from share's upsert).

Optimistic manifest concurrency (the real design content):
- `MemorySpaceRepository.put_index(expected_version=…)` does a conditional
  DynamoDB write on the manifest `version`, raising the repository-local
  `OptimisticLockError` on a mismatch.
- `write_entry`/`delete_entry` route through a new `_mutate_index` helper: a
  bounded read-modify-conditional-write retry loop. Because an entry write
  touches a single slug, re-reading the fresh manifest and re-applying is safe;
  it converges on transient races and raises `MemorySpaceConcurrencyError`
  (→ 409) only on a sustained one. Behavior is unchanged for single-writer
  spaces.

Tests: 6 route tests (share CRUD, member gains access, non-owner 403, viewer
can't list, PATCH-unknown 404, owner-role 422) + 7 service tests (update_share
role/origin/owner-gate + version-increments, stale-write rejected, retry
converges, gives-up-after-max). 76 memory + import-boundary tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4-sharing

feat(memory): Memory Space sharing + optimistic manifest concurrency (A4)
…rt (A5)

The user-facing "Memory" surface for the Memory Space primitive, under
frontend/ai.client/src/app/memory-spaces/. Makes A2–A4 visible to users.

- List page: owned + shared-in spaces as cards with role/template badges;
  per-card open, share (owner), download .zip, delete/leave. Empty, loading,
  error, and feature-unavailable states.
- Detail page: view/edit the MEMORY.md index (editor+) and the entry list;
  entries open in a dialog to view (viewer) or edit/create (editor+); delete
  per entry. Header carries share/download/delete-or-leave.
- Create-from-template dialog and a share dialog (add-by-email + per-row role
  + delta-on-save over the A4 /shares endpoints). Viewer access is read-only
  throughout; the share dialog fails soft for non-owners.
- Signal facade (MemorySpaceService) + thin API service mirror the
  assistants/schedules pattern. The nav entry rides a live accessible$ probe:
  a 404 (MEMORY_SPACES_ENABLED off) hides it, matching showSchedules.
- Routes memory-spaces + memory-spaces/:id (authGuard); redesign-tokens and
  @angular/cdk/dialog conventions throughout.

Facade spec (7 tests) green; dev build + tsc clean; sidenav specs still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rejection)

The A5 sidenav now injects MemorySpaceService and probes `loadSpaces()` in the
auth effect. The sidenav spec mocked ScheduleService but not the new service,
so the authenticated-probe test constructed the real MemorySpaceService, which
fired a real XHR to /memory/spaces (status 0, no backend); `loadSpaces` then
re-threw, surfacing as a Vitest "unhandled rejection" at suite level.

Mock MemorySpaceService exactly as ScheduleService is mocked, and add matching
coverage: probe fires once authenticated (not while unauthenticated) and the
`showMemorySpaces` nav gate resolves null→false, false→false, true→true.

Full suite: 1422 passed, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5-spa-panel

feat(memory): Memory Spaces SPA panel — list/detail/create/share/export (A5)
app-api owns the Memory Spaces CRUD surface (`/memory/spaces/*`) but its
container environment only set MEMORY_SPACES_ENABLED — never the table or
bucket names the service reads (DYNAMODB_MEMORY_SPACES_TABLE_NAME /
S3_MEMORY_SPACES_BUCKET_NAME). Without them the repository falls back to the
default "memory-spaces" table name, which doesn't exist, so every read throws
a boto3 ResourceNotFoundException that the centralized handler maps to a
502 "Upstream service error." (inference-api already sets the identical trio,
but per the service-boundary rule it isn't the one serving these routes.)

Thread `refs.memorySpacesTable`/`.memorySpacesBucket` through AppApiSsmParams
and emit both names next to the flag in buildAppApiEnvironment. Names are
always wired (read lazily); only MEMORY_SPACES_ENABLED gates route mounting,
so flipping the switch on later needs no env change. New unit test guards the
wiring; tsc + 431 infra jest tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold the lesson from the app-api env-wiring fix into the cdk-infrastructure
skill so the next construct-author sees it while wiring, not after a 502.
Adds a "Cross-Construct References" subsection: set a resource's name env var
on every compute that reads it (one doesn't imply the other), the silent-502
failure mode (default-name fallback → ResourceNotFoundException → generic 502,
invisible to synth/CI), and the env-map test guard + service-boundary caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s-app-api-env-wiring

fix(memory): wire memory-spaces table/bucket names onto app-api
The safe, non-LLM slice of Workstream A6. `MemorySpaceService.consolidate`
(editor+) + `POST /memory/spaces/{id}/consolidate` → a `ConsolidationReport`.

Auto-fixes only storage hygiene: orphaned content-addressed objects — keys
under a space's prefix that no manifest entry or the index pointer references
(leaks from crashed/raced writes) — are GC'd (new `MemorySpaceStore.list_keys`
drives it). Everything that needs a judgment call is *reported, not mutated*:
- duplicate content across slugs (same content hash) — which slug survives is
  semantic, so it's flagged, never auto-merged;
- dead `[[slug]]` wikilinks in MEMORY.md — reported; opt-in `stripDeadLinks`
  unlinks them (they point nowhere) while preserving the surrounding prose;
- over-cap entry counts (`MEMORY_SPACE_INDEX_CAP`, default 200) — flagged, never
  auto-evicted.

This deliberately does not merge/evict/rewrite durable memory — that's deferred
to the LLM consolidation pass (Workstream B era), which extends this exact
`consolidate()` seam once agentic writes create real duplication/staleness to
act on. On-demand only for now; scheduler/threshold auto-run and SPA surfacing
are follow-ups.

Tests: 8 service (healthy report, orphan GC + skip, dup-report-no-merge,
dead-link report + strip-keeps-prose, over-cap flag, editor gate) + 4 route
(report shape, no-body, viewer 403, flag-off 404) + 3 store (list_keys prefix
scoping / empty / disabled). 104 memory + boundary tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6-consolidation

feat(memory): deterministic consolidation health pass (A6)
Captures the strategy for the "Agent Designer" (Agent Harness Editor): a new
authoring surface that composes an Agent from RBAC-governed primitives
(instructions, model, KBs, tools, skills, Memory Spaces, + future), replacing
the term/feature "Assistant."

Locks the load-bearing decisions: own a primitive-agnostic Agent contract and
federate AgentCore Registry later rather than build on it (adopt-with-boundary
precedent); evolve the assistant store in place (no parallel table); a uniform
bindings[] model with the model as a governed single-select; RBAC = compose the
five existing per-primitive access checks (incl. ModelAccessService), not a new
system; design-time filter + run-time re-resolution per invoker with block-on-
missing v1; ship memory-consumption as a thin vertical slice before the full
Designer. Phasing 0–5 + later AWS federation; supersedes the memory spec's
"extend the Assistant" §B1 framing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…signer-spec

docs(agent): Agent Designer spec — unified primitive-binding surface
…dels

Phase 1 (PR-1) of the Agent Designer. Pure library, zero behavior change:
legacy Assistants read unchanged and no caller passes the new fields yet.

- AgentModelConfig (D3 governed single-select; field is model_settings/
  alias modelConfig to dodge pydantic's reserved model_config — R3)
- AgentBinding (open kind on read, KNOWN_BINDING_KINDS for request validation)
- optional model_settings + bindings on Assistant (additive)
- compat.effective_bindings/to_agent_view (D2): absent bindings synthesize a
  knowledge_base binding reffing the assistant id (KB's only stable identity,
  F4 deferred — R4); absent model maps to None, never fabricated (R1)
- Decimal-safe serialization for modelConfig.params floats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 (PR-2). The Agent fields now round-trip through the rag-assistants
store and are validated at write time by composing existing RBAC checks (D4).
Legacy clients are unaffected: the SPA sends none of the new fields and the
AssistantResponse surface is unchanged.

- service.create_assistant/update_assistant thread bindings + model_settings;
  to_ddb_safe on write / from_ddb on read so modelConfig.params floats survive
  DynamoDB (Decimal); explicit [] replaces bindings, absent leaves them untouched
- app_api/agents/services/binding_validation.py composes model access
  (ModelAccessService), memory resolve_permission (viewer+/editor+), the
  implicit-KB rejection, and inert shape-only checks for tool/skill (D4/D5)
- assistants POST/PUT validate then pass through; validation raises 4xx outside
  the create handler's generic except so it isn't masked as 500
- tests: validation matrix (incl. inert no-RBAC guarantee), persistence round
  trip, legacy no-field read

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…signer-phase1

feat(agents): Agent Designer Phase 1 — contract + binding persistence (foundation)
philmerrell and others added 23 commits July 19, 2026 12:35
…nto-develop-1.8.0

Backmerge: main into develop (1.8.0)
Three defects in the chat input's auto-sizing:

- The textarea carried `overflow-hidden`, so once content exceeded the
  visible area there was no way to scroll within it.
- `onTextareaInput` set `height = scrollHeight` with no clamp. Past 200px
  the inline height kept growing while `max-height` capped the rendered
  height, leaving the two diverged and the scrollbar unreachable. The
  growth is now clamped in a shared `autoResize()`.
- `submitChatRequest` cleared the value but left the stale inline height,
  so the input stayed expanded after sending. Adds `resetTextareaHeight()`.

Also drops the `.chat-textarea` CSS block: it declared its own
min-height/max-height/field-sizing but the class is applied nowhere in the
template, so the rules were dead while contradicting the real inline
styles. The JS path is now the sole sizing authority. The `isExpanded`
signal goes too — it was written on submit and never read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k prompt-cache hits

Tool-content truncation previously ran on every session restore behind a
sliding protected-turns window, so each new turn re-mutated the turn that
just aged past the window. Bedrock prompt caching requires an exact prefix
match, so this forced a full prefix cache re-write (~$2.5/MTok on a
35k-150k prefix) nearly every turn — costing far more than the read tokens
truncation saved (evidence: prod session aecd387d, inter-turn prefix
shrinkages of -382/-1035/-1513 with cacheRead=0 inside the cache TTL).

Redesign: truncation is now driven only by a persisted truncation_anchor
in the compaction state (sessions-metadata `compaction` attribute):

- The anchor moves when the checkpoint advances (update_after_turn), where
  the slice already pays the single cache re-write — one mutation per
  compaction event instead of one per turn.
- It also advances opportunistically at restore when more than
  cache_ttl_seconds (default 300s, AGENTCORE_MEMORY_COMPACTION_CACHE_TTL_SECONDS)
  have passed since the previous turn: the cache entry has expired anyway,
  so pending truncations are applied for free.
- Legacy state records without the field default the anchor to the
  checkpoint, so retained history stops being mutated immediately on
  upgrade.
- The compaction-failure path in initialize() now resets
  _compaction_state_loaded so update_after_turn re-loads persisted state
  instead of overwriting checkpoint/anchor with defaults.

Removes the now-dead _find_protected_indices sliding-window helper and adds
tests/agents/main_agent/session/test_compaction_stability.py asserting
agent.messages is byte-identical across consecutive restores whenever no
compaction-state change occurs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cache

Skill records reached the AgentSkills <available_skills> system-prompt
block in nondeterministic order, changing the prompt between turns of the
same session and invalidating the Bedrock prompt cache (exact-prefix
match) — forcing full cache re-writes on turns well inside the TTL.

Two sources, fixed at three layers:
- batch_get_skills returned raw DynamoDB batch_get_item response order;
  now sorted by skill_id.
- resolve_user_permissions built grant unions as sets and returned
  list(set) — iteration order varies per process via hash randomization;
  tools/models/skills now sorted.
- build_skills_runtime sorts records before constructing AgentSkills as
  defense in depth at the injection point.

Regression tests force a descending batch_get_item response order and a
reversed fetch order, both verified to fail without the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ad the stable prefix

CacheConfig(strategy="auto") places exactly one message-level cachePoint;
when its lookup misses, nothing is read and the whole prefix re-writes at
the cache-write premium. One proven miss mode is structural: Anthropic's
cache lookback checks only ~20 content blocks behind the breakpoint, so a
wide parallel tool fan-out (18 parallel calls = ~38 new blocks) pushes the
previous checkpoint out of range — prod session aecd387d observed
cacheRead=0 / cacheWrite=134k mid-turn (~$0.34).

Now the request carries 3 of Bedrock's max-4 cachePoints:
- toolConfig tail via cache_tools="default"
- system tail via SystemContentBlock list with trailing cachePoint
  (built in AgentFactory; the cache_prompt config key is deprecated)
- last-user-message point via the existing auto strategy (which strips
  only message-level points, never system/tools ones)

Both new points are gated on ModelConfig.bedrock_cache_points_supported()
(mirrors Strands' _cache_strategy predicate) because unlike auto mode they
would be sent verbatim to non-Anthropic models and rejected.

Verified live on global.anthropic.claude-sonnet-5 via ConverseStream:
simulated message-level miss reads the 6.5k-token system+tools prefix from
cache (cacheRead=6497, cacheWrite=83) instead of re-writing it. CountTokens
accepts the cachePoint-bearing request, so context attribution is
unaffected. Position/budget test asserts exactly 3 cachePoints.

Upstream: strands-agents/harness-sdk#3348 proposes auto mode keep a
rolling pair of message cachePoints so fan-outs stay within the lookback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… call

A $1.60 prod conversation audit (session aecd387d) showed 75% of spend was
avoidable Bedrock prompt-cache re-writes, and diagnosing the causes took
hours of manual forensics against raw DynamoDB rows. This makes the whole
class measurable end to end:

- PrefixFingerprintHook (BeforeModelCallEvent) hashes the three cacheable
  prefix components per model call — toolConfig (order-sensitive canonical
  JSON), effective system prompt (captured after AgentSkills injection),
  and message history excluding the newest message. The stream coordinator
  persists entry N on the turn's Nth assistant-message cost row, so a miss
  is diagnosable with a column diff instead of row forensics.
- Write-time cacheStatus per cost row (first_write | hit | miss_ttl_expired
  | miss_avoidable | uncached) derived from the session's previous C# row
  (one GSI read), plus wastedUsd for avoidable misses priced at the
  cache-write premium over cache-read from the row's own pricingSnapshot.
  Turn rows now write sequentially (was parallel) so each call classifies
  against its true predecessor.
- Session-row rollups next to totalCost: totalCacheReadTokens,
  totalCacheWriteTokens, avoidableMissCount, wastedUsd — cache-efficiency
  ratio for lists/admin without scanning cost rows.
- Admin cost anatomy: GET /admin/costs/sessions/{sessionId}/calls
  (require_admin) returns chronological per-call rows with token splits,
  cost, cacheStatus, and fingerprints, plus session-level cache summary.
- CloudWatch EMF per call (CacheReadTokens / CacheWriteTokens /
  AvoidableMiss / WastedUsd) via a raw-JSON stdout logger — dashboard and
  alarm on fleet cache-write share with no SDK calls or extra IAM.
- CI determinism guard: builds the chat-agent surface twice from shuffled
  skill/role/tool record orders (RBAC merge -> skills runtime -> Agent ->
  AgentSkills injection) and asserts identical system-prompt and toolConfig
  fingerprints; verified to fail when the skill-ordering sort is removed.

Complements the sorting + byte-stability fixes from
feature/prompt-cache-stability (merged in).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PROMPT_CACHE_OBSERVABILITY_ENABLED=false disables the fingerprint hook,
per-call cacheStatus derivation (and its GSI read), and EMF emission —
default ON per house convention; empty string stays enabled. Raw
cacheRead/cacheWrite token rollups are unaffected (usage passthrough,
not derived). CLAUDE.md now encodes the determinism + byte-stability
contract and the fingerprint-based debugging recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ache-stability

fix: preserve Bedrock prompt-cache hits across turns (deterministic ordering + byte-stable compaction)
…extarea-sizing

fix(chat-input): make the textarea scrollable and reset it after submit
- CLAUDE.md: add "token cost effectiveness is a design tenet" bullet under
  Key Conventions — prefix determinism / bounded per-turn payloads / verify
  via the #697 observability, balanced so quality wins on genuine conflict.
- kaizen review-queue: queue the two deferred #697 follow-ups —
  track harness-sdk#3348 (rolling-pair cachePoints; local workaround gated
  on dashboard evidence) and the ContextOffloader S3 adoption spike
  (with its four known gotchas).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
 follow-up)

New cross-service construct area lib/constructs/observability/ with a
PromptCacheObservabilityConstruct composed into PlatformStack. Graphs
the dimension-less EMF metrics both APIs emit into
AgentCoreStack/PromptCache (cache read/write tokens, a cache-efficiency
MathExpression, AvoidableMiss, WastedUsd) plus a Logs Insights widget
over the runtime log group grouped by cacheStatus.

Console-only alarms on AvoidableMiss and WastedUsd Sums (stricter in
prod, NOT_BREACHING on missing data so the
PROMPT_CACHE_OBSERVABILITY_ENABLED kill switch stays quiet). No SNS —
alerting infra is deliberately out of scope, matching kb-sync and
scheduled-runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consumes GET /admin/costs/sessions/{id}/calls (backend PR #697):
- SessionCostAnatomy / SessionCallRow / PrefixFingerprints models + CacheStatus union
- AdminCostHttpService.getSessionCostAnatomy with URL-encoded session id
- New /admin/costs/sessions/:id route (lazy, component input binding)
- Drill-down page: summary rollups (total cost, cache efficiency incl. null,
  avoidable misses, wasted USD, cache read/write tokens), chronological calls
  table with color-coded cacheStatus badges, and prefix-fingerprint diffing
  that flags which hash (tools/system/history) flipped vs the previous
  fingerprinted call — the cache-buster diagnosis on miss_avoidable rows.
  Expandable rows show full hashes + messageCount; 404 renders a
  no-cost-rows empty state.
- Session-id lookup form on the Cost Analytics dashboard as the entry point
- Vitest specs for the diff util, HTTP method, and page states

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he-followup-breadcrumbs

chore: PR #697 follow-up breadcrumbs + token-cost-effectiveness tenet
…ache-dashboard

feat(observability): prompt-cache CloudWatch dashboard + alarms
…st-anatomy-page

feat(admin-costs): per-session cost-anatomy drill-down page
…d calls as miss_avoidable

When every prior call in a session was uncached (prompt below the
model's minimum cacheable prefix, e.g. ~4096 tokens on Claude Haiku 4.5),
the first call that crosses the threshold does cacheWrite>0/cacheRead=0
and was classified miss_avoidable — inflating the AvoidableMiss and
WastedUsd EMF metrics and the admin Session Cost Anatomy page. Verified
live in dev-ai session 9a1f25b2 (calls 1-5 uncached at 3.5-4k tokens,
call 6 falsely flagged with write=4122/read=0).

classify_cache_status now takes the previous call's cached-prefix token
total: when the immediately preceding call had zero cache activity there
was no entry to read from, so the write is classified first_write (the
expected initial population) and excluded from waste pricing. Unknown
(None) keeps the previous behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…voidable-below-threshold

fix(observability): first cache write after below-threshold calls is first_write, not miss_avoidable
The AgentCore Runtime env block set DYNAMODB_USER_FILES_TABLE_NAME but not S3_USER_FILES_BUCKET_NAME, so the Word-document tools' _user_files_bucket() fell back to the literal 'user-files' default and PutObject failed with AccessDenied (and earlier PermanentRedirect against that unrelated bucket). The runtime role's UserFilesBucketAccess already grants Get/Put/Delete/List on the real bucket; this just points the runtime at it. tsc build passes.
…er-files-bucket-env

Set S3_USER_FILES_BUCKET_NAME on inference-api runtime
_user_files_bucket() previously defaulted to a literal 'user-files' bucket when S3_USER_FILES_BUCKET_NAME was unset, which surfaced a missing runtime env var as a confusing S3 PermanentRedirect/AccessDenied. It now raises _StorageNotConfiguredError, and create/modify/read short-circuit before the Code Interpreter run with a clear 'storage is not configured' message.
…storage-config

Fail loudly when Word doc storage bucket is unconfigured
Feature release making Bedrock prompt-cache economics stable and measurable. Fixes three cache-busting defects in the model-call path (per-turn history mutation via the sliding truncation window, nondeterministic skill ordering, single fragile cachePoint) and adds end-to-end observability: prefix fingerprints + cacheStatus classification on every cost row, the admin Session Cost Anatomy page, per-call EMF metrics, and a CloudWatch dashboard with alarms. Also fixes chat-input textarea sizing and points the runtime's Word-document tools at the real user-files bucket.

- Prompt-cache observability layer with PROMPT_CACHE_OBSERVABILITY_ENABLED kill switch (#697)
- Admin Session Cost Anatomy drill-down at /admin/costs/sessions/:id (#700)
- Byte-stable restored history, deterministic skill ordering, 3 cachePoints (#697)
- first_write vs miss_avoidable classification fix after below-threshold calls (#701)
- PromptCacheObservabilityConstruct dashboard + alarms (#699)
- S3_USER_FILES_BUCKET_NAME on the inference-api Runtime + fail-loud storage guard (#702, #706)
- Chat input textarea scroll/clamp/reset fixes (#696)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@philmerrell
philmerrell requested a review from a team July 20, 2026 19:17
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroArrowLeft, heroChevronDown } from '@ng-icons/heroicons/outline';
import { AdminCostHttpService } from '../services/admin-cost-http.service';
import { CacheStatus, SessionCallRow } from '../models';

import pytest

from agents.main_agent.session.compaction_models import CompactionConfig, CompactionState
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroArrowLeft, heroChevronDown } from '@ng-icons/heroicons/outline';
import { AdminCostHttpService } from '../services/admin-cost-http.service';
import { CacheStatus, SessionCallRow } from '../models';
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
@philmerrell
philmerrell merged commit 854b9a6 into main Jul 20, 2026
11 checks passed
@philmerrell
philmerrell deleted the release/1.9.0 branch July 20, 2026 20:03
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.

6 participants