Skip to content

Bounded Envelope + Pull Architecture for Delivery-Bound Recipes - #4312

Closed
Trecek wants to merge 25 commits into
developfrom
non-path-verdict-token-unrecoverable-make-plan-lacks-on-cont/4305
Closed

Bounded Envelope + Pull Architecture for Delivery-Bound Recipes#4312
Trecek wants to merge 25 commits into
developfrom
non-path-verdict-token-unrecoverable-make-plan-lacks-on-cont/4305

Conversation

@Trecek

@Trecek Trecek commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Across this branch, the delivery-bound recipe summarization path was hardened in two major passes: Part A fixed the _delivery_bound_summary budget-priority bug (which was starving content to zero bytes), bounded suggestions at source, closed capability fail-open holes (including the ctx is None gap), and added the missing negative-contract and parametrized tests. Part B then eliminated the underlying structural fragility by replacing inline recipe delivery with a bounded envelope + pull architecture — open_kitchen/load_recipe now return a compact, by-construction-bounded envelope with routing metadata, backed by a persisted, sha256-verified artifact retrievable via a new pull tool. Two follow-up remediation rounds closed the gaps this re-architecture initially left open — wiring open_kitchen into the envelope path, restoring unconditional artifact persistence on the exempted-tool backstop, adding the 8 required TDD tests, dropping an errant diagram field from the envelope, and finally extending test_delivery_bound_summary_preserves_operational_fields to assert all 5 new Part B envelope keys — leaving the full suite green (31371 passed) with no known structural or test-coverage gaps remaining.

Individual Group Plans

Group 1: Rectify: Delivery-Bound Summary Self-Starvation and Safety Net Gaps (Part A)

The _delivery_bound_summary function (server/_response_budget.py:375-511) starves content to zero bytes because preserved keys (especially suggestions at 48,262 bytes) consume the entire budget before content gets any allocation, and the fallback that shrinks preserved keys never reallocates freed budget back to content. Additionally, suggestions accumulates unboundedly at source (recipe/_api.py:324), capability resolution silently fails open when backend context is unavailable, and no test asserts minimum content coverage in the summary. This part fixes the budget algorithm, bounds suggestions at source, closes capability holes, and adds the missing negative contracts.

Part B will cover the bounded envelope + pull architecture for full structural immunity — implement as a separate task.

Group 2: Close the ctx-is-None Delivery-Bound Fail-Open Gap and Make the post_prune_step_names Test Contrast Real (Part A Remediation)

This plan remediates the two audit findings from /audit-impl against the Part A rectification branch (commit e55a08917, "Rectify delivery-bound summary self-starvation (Part A)"):

  1. REQ-013 gap (source fix): track_response_size in src/autoskillit/server/_notify.py still fails open when ctx is None. The wrapper initializes backend_inspected = False and only sets it True inside if ctx is not None:; the worst-case fallback block is gated on backend_inspected, so when ctx is None the fallback never runs, effective_delivery_token_limit stays None, and enforce_response_budget treats None as "no delivery bound" — enforcement is silently skipped. The fix removes the backend_inspected gate entirely so the fallback check runs unconditionally, covering BOTH fail-open paths (ctx is None and caps-not-BackendCapabilities) with the same resolve_worst_case_delivery_bound() + logger.warning fallback.

  2. REQ-025 / REQ-007 gap (test fix): _delivery_starvation_payload() in tests/server/test_response_backstop.py never includes a post_prune_step_names key, so the starvation and budget-reallocation tests only ever exercise the empty-list content_floor = 0 path. The fix parametrizes both tests over a with/without post_prune_step_names contrast so each test exercises BOTH the [] fallback path (REQ-025) and the populated skeleton-floor path, with step-name-presence assertions in the WITH variant.

A new regression test (test_capability_fail_closed_on_missing_ctx) captures the ctx is None fail-open gap the existing test_capability_fail_closed_on_missing_backend misses.

Scope boundary (REQ-034): Part B (bounded envelope + pull architecture) remains out of scope and must not be implemented under this plan.

Group 3: Rectify: Bounded Envelope + Pull Architecture for Recipe Delivery (Part B)

Even with Part A's budget-priority fix and suggestions bounding, inline recipe delivery remains structurally fragile: the remediation recipe's step section (~24.3K tokens) is 2.4× the Codex code-mode ceiling, and any recipe growth risks re-triggering starvation. This part eliminates the fragility by re-architecting recipe delivery as "bounded envelope + guaranteed pull access": open_kitchen/load_recipe return a compact envelope (step skeleton + routing metadata) that fits the smallest supported delivery bound by construction, and full step content is retrieved on demand via a new MCP pull tool. The full recipe is always available as a persisted artifact with a deterministic path and sha256 verification.

Part A covers the bounded-summary priority fix, suggestions bounding, and capability hole closure — that part must be implemented first.

Group 4: Part B Remediation — Bounded Envelope + Pull Architecture Gaps (Part A of the gaps plan)

This part closes the structural gaps found by /audit-impl in the Part B (bounded envelope + pull architecture) implementation on this branch:

  1. open_kitchen was never wired to the envelope architecture — both recipe-bearing serve_recipe() paths still returned the full payload without persisting an artifact, setting ToolContext.recipe_artifact_state, or building the compact envelope.
  2. enforce_response_budget's exempted-tool sub-branch (c) does not persist the artifact — the unconditional-persistence requirement was implemented and then reverted; this plan restores it with pre-publication detection so a backstop persist never orphans the tool-layer artifact.
  3. Zero test coverage for the pull architecture — none of the 8 required Part B TDD tests existed; no CI test asserted the envelope fits by construction.
  4. Diagram-forwarding deviationbuild_recipe_envelope forwarded diagram into the envelope despite the plan's requirement that the envelope must NOT include a diagram field; resolved by dropping the forwarding.

Part B of this plan (separate task, requires explicit authorization) covers the presentation and documentation surface: CLI prompt contract, sous-chef SKILL.md, formatter envelope handling, exemption-registry semantic docstring, and the ADR-0005 five-layer update.

Already satisfied on this branch (not re-implemented): build_recipe_envelope, extract_step_routing, deterministic _artifact_path, build_and_record_recipe_envelope, persist_recipe_artifact, ToolContext.recipe_artifact_state, the complete get_recipe_section tool, load_recipe wiring, SERVE_SURFACES S5 entry, _DELIVERY_BOUND_PRESERVED_KEYS envelope fields, serve-surface arch-test expectations, and _serve_helpers.py's "five serve surfaces" docstring.

Group 5: Add the 5 Part B Envelope-Key Assertions to test_delivery_bound_summary_preserves_operational_fields (REQ-126, Round 3)

Round-2 /audit-impl confirmed all six of the previous gaps plan's deliverables are satisfied and task test-check passes (31371 passed, 610 skipped, 49 xfailed). Exactly one MISSING finding survives: the test-assertion half of REQ-126 was never implemented.

_DELIVERY_BOUND_PRESERVED_KEYS correctly contains all 5 Part B envelope keys (step_flow_skeleton, step_index, pull_tool, artifact_path, sha256) — the registry half of REQ-126 is done. But test_delivery_bound_summary_preserves_operational_fields still constructed its payload with, and asserted, only the original 9 keys, so a regression that drops one of the 5 new keys from the _delivery_bound_summary spill-projection path would go undetected.

This plan makes exactly one change: extend that single test's payload and its per-key assertions to cover the 5 new keys, mirroring the existing 9-key pattern, with each new key bound to a distinct, order-detectable sentinel value. No production code changes.

Closes #4305

Implementation Plan

Plan files:

  • /home/talon/projects/autoskillit-runs/remediation-20260720-183035-937540/.autoskillit/temp/rectify/rectify_delivery_bound_starvation_2026-07-20_183500_part_a.md
  • /home/talon/projects/autoskillit-runs/remediation-20260720-183035-937540/.autoskillit/temp/make-plan/remediation_delivery_bound_starvation_part_a_fixes_plan_2026-07-21_071341.md
  • /home/talon/projects/autoskillit-runs/remediation-20260720-183035-937540/.autoskillit/temp/rectify/rectify_delivery_bound_starvation_2026-07-20_183500_part_b.md
  • /home/talon/projects/autoskillit-runs/remediation-20260720-183035-937540/.autoskillit/temp/make-plan/remediation_delivery_bound_starvation_part_b_gaps_plan_2026-07-21_140852_part_a.md
  • /home/talon/projects/autoskillit-runs/remediation-20260720-183035-937540/.autoskillit/temp/make-plan/remediation_delivery_bound_starvation_partB_gaps_round3_plan_2026-07-21_161951.md

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
rectify* opus[1m] 1 16 5.0k 1.1M 162.7k 93 8.6k 34m 10s
dry_walkthrough* sonnet 5 147.2k 196.4k 21.6M 283.0k 421 744.6k 50m 7s
implement* MiniMax-M3 5 557.9k 297.4k 84.6M 537.7k 785 630.2k 1h 25m
retry_worktree* MiniMax-M3 2 742.7k 102.9k 72.3M 183.0k 842 0 2h 35m
audit_impl* sonnet 5 349.9k 315.7k 60.5M 273.8k 784 1.5M 1h 15m
make_plan* fable 3 156.9k 159.8k 14.0M 337.8k 162 928.5k 44m 27s
assess* MiniMax-M3 5 334.5k 114.4k 18.3M 182.7k 402 274.8k 59m 8s
prepare_pr* sonnet 1 9.9k 12.3k 934.4k 133.1k 26 111.5k 2m 4s
compose_pr* sonnet 1 10.2k 13.8k 421.4k 67.6k 16 43.5k 2m 12s
Total 2.3M 1.2M 273.7M 537.7k 4.3M 8h 27m

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
rectify 0
dry_walkthrough 0
implement 2100 40299.3 300.1 141.6
retry_worktree 1682 42960.1 0.0 61.2
audit_impl 0
make_plan 0
assess 132 138674.7 2081.9 866.4
prepare_pr 0
compose_pr 0
Total 3914 69933.9 1090.2 311.1

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
opus[1m] 1 16 5.0k 1.1M 8.6k 34m 10s
sonnet 4 517.2k 538.2k 83.4M 2.4M 2h 9m
MiniMax-M3 3 1.6M 514.6k 175.2M 905.0k 4h 59m
fable 1 156.9k 159.8k 14.0M 928.5k 44m 27s

Trecek and others added 24 commits July 20, 2026 20:47
…iation)

Adds test_capability_fail_closed_on_missing_ctx in
tests/server/test_track_response_size.py — when _get_ctx_or_none() returns
None and the handler returns an 80KB payload, the decorator must fail closed
via bounded_response_budget_failure(cause='context_unavailable') rather than
passing the payload through unbounded.

Mirrors the mocking pattern of the sibling test_capability_fail_closed_on_missing_backend
but returns None itself — the branch the sibling cannot reach. Will turn green
once the backend_inspected gate is removed from track_response_size.
…ediation)

The gate prevented the worst-case fallback from running when ctx is None,
leaving effective_delivery_token_limit as None — which enforce_response_budget
treats as an explicit opt-out, so the delivery bound was silently skipped
and an oversized payload could pass through unbounded.

Removing the gate makes the fallback check run unconditionally, covering BOTH
fail-open paths:
- ctx is None (previously fail-open)
- caps not a BackendCapabilities instance (already working)

The fallback body (logger.warning message + assignment) is unchanged; the
flag is removed entirely to avoid dead state. shape_execution_response in
server/tools/_execution_helpers.py is NOT touched — its tool_ctx parameter is
required/non-Optional, so it has no ctx is None path.

Closes REQ-013 in Part A.
…t (Part A)

The two starvation tests previously never exercised the populated
post_prune_step_names path — _delivery_starvation_payload never inserted
the key, making the test_delivery_bound_summary_budget_reallocation_after_projection
payload.pop("post_prune_step_names", None) line a no-op against a key that
was never present.

Changes:
- _delivery_starvation_payload gains an optional post_prune_step_names
  parameter; when not None, the key is included in the payload
- _STARVATION_STEP_NAMES module-level constant lists the step_0..step_199
  names generated by the helper's content skeleton
- Both tests are parametrized over [None, _STARVATION_STEP_NAMES] with
  ids 'without_post_prune_step_names' / 'with_post_prune_step_names'
- WITH variants assert every supplied step name appears in delivered
  content (matches the contract-level assertion style in
  tests/contracts/test_delivery_bound_fitness.py)
- WITHOUT variants preserve the existing content_floor=0 fallback assertions
- The misleading no-op payload.pop line is removed

Closes REQ-025 / REQ-007 coverage gap.
…ase fallback

The Part A remediation removed the backend_inspected gate so the
worst-case fallback now fires when ctx is None (REQ-013 closure).
Previously, open_kitchen direct-call tests (those that pass a mock_ctx
directly while patching only server._get_ctx) relied on track_response_size
seeing ctx=None and silently letting the unbounded response through. The
new fallback now catches this case and returns a bounded_response_budget_failure
envelope (cause=context_unavailable), causing seven pre-existing tests to
fail with KeyError on parsed['content']/parsed['success'].

Add a conftest helper _make_track_response_ready_mock_ctx that wires
mock_ctx.backend.capabilities to a real BackendCapabilities with a high
effective_delivery_token_limit, and update the seven direct-call tests
to use the helper plus patch autoskillit.server._notify._get_ctx_or_none
(in addition to server._get_ctx) so track_response_size sees the same
ctx as open_kitchen. The seven tests now exercise the production path
(backend capabilities resolve an effective delivery bound) instead of
the previously-incidentally-open fail-open path.
open_kitchen / load_recipe now return a compact step-skeleton envelope
(step_flow_skeleton + routing edges + step_index + artifact_path + sha256 +
pull_tool) that fits the smallest backend delivery bound by construction.
The full recipe content is persisted as a deterministic-path artifact with
sha256 verification, and get_recipe_section (a new bounded pull tool) reads
it on demand — closing ADR-0005 Accepted Gap #6 ("caller needing pruned
collection members must retrieve bounded slices from that artifact").

Key changes:
- src/autoskillit/server/_response_budget.py: add build_recipe_envelope,
  extract_step_routing, deterministic _artifact_path scheme (sha256-based
  rather than uuid4), preserved-keys extension for envelope fields.
- src/autoskillit/server/tools/tools_recipe.py: persist full recipe artifact
  at the tool layer, return envelope via render_served_response, register
  get_recipe_section MCP tool with full never-raises contract and sha256
  verification on the artifact (recreates from load_and_validate if missing).
- src/autoskillit/server/tools/_serve_helpers.py: build_and_record_recipe_envelope
  consolidates the persist + envelope + ctx state update in one helper.
- src/autoskillit/core/types/_type_constants_registries.py: add get_recipe_section
  to GATED_TOOLS, SERVE_SURFACES (5th surface), and TOOL_SUBSET_TAGS.
- src/autoskillit/pipeline/context.py: add recipe_artifact_state field for
  server-side session cache of artifact_path/sha256/recipe-load params.
- tests/: update all tests expecting the old full-payload shape; bump
  kitchen tool count, MCP tool count, and doc references to 60.
- docs/: bump MCP tool count references from 59 to 60.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rding

Restores persistence in enforce_response_budget's exempted-tool sub-branch
(c) with pre-publication detection: reuses the tool-layer artifact when the
payload already carries artifact_path/sha256, persists otherwise when an
artifact dir is available, and skips gracefully when it is not. Threads the
pre-published pair into _spill_for_delivery_bound to avoid a double write.

Also drops build_recipe_envelope's diagram forwarding — diagram retrieval is
exclusively via get_recipe_section(section="diagram").
Both open_kitchen recipe-bearing return paths (deferred-recall and normal)
now route through a single _render_open_kitchen_envelope helper that calls
build_and_record_recipe_envelope, mirroring load_recipe's existing wiring.
The bare-open (name=None) path is untouched.

Also fixes recreation for open_kitchen-originated artifacts: _recreate_artifact
now reapplies build_open_kitchen_recipe_payload before hashing, so a
regenerated artifact byte-matches the originally persisted open_kitchen
payload (which carries success/kitchen/version keys the raw serve_recipe()
result lacks). Without this, get_recipe_section's missing-artifact recreation
path always mismatched for open_kitchen sessions.
open_kitchen(name=...) now returns a compact envelope instead of inline
content, so every affected mock ToolContext needs a real temp_dir for
artifact persistence, and content-reading assertions now go through the
persisted artifact (or the new _open_kitchen_content helper mirroring
_load_recipe_content) instead of indexing "content" directly.
- test_response_backstop.py: artifact-always-persisted, deterministic-path,
  and pre-published-not-rewritten tests for the exempted-branch backstop
  (Tests A1, A2, A2b).
- test_tools_recipe.py: TestGetRecipeSection exercises get_recipe_section
  after an open_kitchen-only session — step resolution, sha256 integrity
  verification, and missing-artifact recreation incl. its failure path
  (Tests A3-A5).
- test_delivery_bound_fitness.py: envelope-fits-by-construction, step_index
  coverage, and largest-step pull-bound fitness for every bundled recipe
  (Tests A6-A8); reworks the old fits-or-spills test into a fits-only
  per-backend contract (Test A9).
…chitecture

Wiring open_kitchen's recipe-bearing paths through build_and_record_recipe_envelope
(previous commit) exposed several latent gaps only reachable once open_kitchen
persists artifacts and returns a compact envelope like load_recipe already did:

- OpenKitchenResult (recipe/_recipe_ingredients.py) didn't declare the five
  compact-envelope fields (step_flow_skeleton, step_index, artifact_path,
  sha256, pull_tool), so the wire response carried undeclared keys.
- The pretty_output open_kitchen formatter's field-coverage contract needed
  the same five fields classified (suppressed pending Part B's dedicated
  formatter work).
- Many tests built mock ToolContexts without a real temp_dir, so envelope
  persistence raised and open_kitchen's outer exception handler turned
  would-be successes into kitchen:"failed" envelopes (test_mcp_overrides,
  test_open_kitchen_deferred_recall, test_pipeline_tracker, plus two more
  sites in test_tools_kitchen_visibility).
- A handful of tests read fields (post_prune_step_names, warnings, valid,
  orchestration_rules absence) that live on the persisted artifact or aren't
  part of the envelope schema at all, rather than on the compact envelope.
- test_serve_idempotence's deferred-recall test asserted artifact identity
  between two calls that legitimately pass different overrides (producing a
  legitimate warnings-key difference) — narrowed to the content-identity
  assertion that's actually guaranteed.
- test_schema_version_convention's allowlist and test_subpackage_isolation's
  line-limit exemption needed updating for tools_kitchen.py's shifted line
  numbers and slightly larger size.
build_recipe_envelope raised unconditionally when the naive full-fidelity
envelope exceeded the delivery bound. Large bundled recipes (implementation,
implementation-groups, remediation) hit this once open_kitchen was wired
through the envelope path, since their suggestions/orchestration_rules/etc.
push the naive envelope past the smallest backend's bound. Advisory
(non-structural) preserved fields now degrade via the existing
_project_value/_minimal_same_type primitives before failing closed;
step_flow_skeleton and step_index (required for full step-pull coverage)
are never touched.
Extends test_delivery_bound_summary_preserves_operational_fields with
the 5 Part B envelope keys (step_flow_skeleton, step_index, pull_tool,
artifact_path, sha256) that _DELIVERY_BOUND_PRESERVED_KEYS already
protects, so a regression dropping one of them from the spill
projection path is now caught (REQ-126 test-assertion half).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…king file

tool_name="open_kitchen" is exempted, so enforce_response_budget treats a
payload-supplied artifact_path+sha256 pair as already published by the tool
layer and reuses it verbatim as the spill metadata's artifact_path instead
of persisting `original` fresh (see test_recipe_artifact_pre_published_not_rewritten).
The round-3 plan's fictional sentinel path was never written to disk, so the
test's own pre-existing artifact round-trip assertion failed with
FileNotFoundError. Point the sentinel at a real file under tmp_path and
write `original` there before invoking enforce_response_budget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… offset

Character-slicing json.dumps(list) then json.loads()-ing the substring
produced invalid JSON on nearly every chunk boundary, silently discarding
content for oversized get_recipe_section(section="suggestions") pulls
while still returning success=True.
tools_recipe.py maintained a private byte-for-byte copy of
_serve_helpers.resolve_envelope_delivery_bound (both added in the same
commit). Import the shared helper instead of two independently
maintained copies of the fallback/multiplier logic.
… strips content

step_index was built unconditionally from post_prune_step_names, but
ingredients_only=True strips "content" from the persisted artifact, so
get_recipe_section(section="step", ...) always fails step_not_found for
those steps despite step_index claiming they're pullable. Guard it the
same way step_flow_skeleton already is.
…ion type

Raising a bare ValueError meant load_and_validate's generic
except ValueError handler reclassified an internal sizing-invariant
failure as "Invalid recipe structure: ...", conflating it with
legitimate recipe-authoring errors — and the fallback _bounded_append
call inside that handler could raise the same ValueError unhandled.
TruncationBoundsError is not caught by that handler, so it now
propagates as a genuine internal error instead.
test_kitchen_tagged_tool_count_is_40 and test_docs_state_59_mcp_tools
asserted 41/60 but kept their old names from before the bounded-envelope
count bump, making the test names misleading.
…ddition

_api.py's TruncationBoundsError import shifted the pre-existing
lazy-registry suppression from line 335 to 336.
…a truncation sentinel

_bounded_append's merge branch stripped the old sentinel via `target[:-1]`
but never concatenated the newly appended `incoming` items into
`candidates`, so once a recipe load tripped the bound once, every
subsequent stage's errors/suggestions for that load were silently
dropped from the list entirely while `original_count` still reported
them as counted.
…eation matches the persisted artifact

recipe_artifact_state never recorded whether the original load used
ingredients_only=True, so _recreate_artifact always re-served the full
unstripped payload. The recreated sha256 then deterministically
mismatched the recorded sha256 for any artifact originally persisted
from an ingredients_only=True load, so get_recipe_section always
failed with artifact_recreation_mismatch instead of recreating.
track_response_size and shape_execution_response each independently
implemented the same "resolve effective delivery limit from backend
capabilities, fall back to the worst-case bound, log if falling back"
sequence (a third copy in tools_recipe.py was already consolidated by
83b3c82). Extract resolve_effective_or_fallback_delivery_bound into
_response_budget.py, which both modules already import from, and have
both call sites use it instead of maintaining independent copies.

@Trecek Trecek left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoSkillit PR Review — Verdict: approved_with_comments

Comment thread tests/server/test_tools_load_recipe.py
…sertions in load_recipe ingredients_only test
@Trecek

Trecek commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #4308, which landed the bounded envelope + pull architecture on develop first. Branch preserved for reference.

@Trecek Trecek closed this Jul 22, 2026
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.

2 participants