HookPayload fallback can still exceed cap on large handoffs #630

Closed
opened 2026-09-22 21:47:39 +00:00 by jared · 2 comments
Owner

Problem

ADR-0206 records a known gap in the #627 hook-payload cap fix.
OsSdlc::Pipeline::HookPayload#to_json (plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb, shipped by #627) drops only the map node's enrich facts when the serialized SubagentStart payload exceeds HOOK_CONTEXT_CAP (10,000 UTF-16 units).

The :handoff key (the pending handoff content from the prior agent) is not an enrich fact, so it survives the fallback. fallback_payload (line 28) already replaces :brief with the fetch instruction, so the brief file body is already covered; :handoff is the only large key left in the fallback envelope. A handoff alone larger than about 10,000 units still pushes the fallback over the cap. Claude Code then persists the string to a file with a 2,000-character preview, and the agent loses the fetch instruction.

This was witnessed live; that observation is how this ticket was opened. (The original body's "Not yet observed live" line was wrong and is corrected here.)

Approach

Whenever the payload falls back at all, drop :handoff unconditionally. One measurement, one fallback shape:

def fallback_payload
  @payload.reject { |key, _| @enrich_keys.include?(key) }
          .except(:handoff)
          .merge(brief: fallback_instruction)
end

Fallback envelope after the change: session_id, implementation_id, dispatch_id, agent, node, instructions, brief (fetch instruction).

Rejected alternative: a second measurement that keeps :handoff when it fits (two fallback shapes for no gain).

Required work:

  1. Code: change fallback_payload in plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb to .except(:handoff) as above.
  2. Test: a payload with a 12,000-unit handoff and no enrich keys serializes under the cap and keeps dispatch_id and the fetch instruction (asserts :handoff is absent from the fallback result).

Acceptance criteria

  • A HookPayload built from a payload containing a :handoff key whose serialized size alone exceeds HOOK_CONTEXT_CAP, with no enrich keys present, serializes to JSON at or under HOOK_CONTEXT_CAP UTF-16 units.
  • The serialized fallback result never contains a :handoff/"handoff" key, regardless of whether the un-fallen-back payload fit without it.
  • The serialized fallback result still contains dispatch_id and a brief value including os-sdlc-runner brief --dispatch-id <id>.
  • Existing hook_payload_test.rb cases (none of which construct a payload containing :handoff) continue to pass unmodified.

Owner sketch

class OsSdlc::Pipeline::HookPayload
  def to_json(*)
  private
  def fallback_payload
end

Blast radius

\1

  • plugins/os-sdlc/tests/pipeline/hook_payload_test.rb — three existing tests (test_payload_at_exactly_the_cap_is_emitted_unchanged_with_enriched_facts_inline, test_payload_over_the_cap_emits_routing_envelope_and_brief_command_only, test_a_non_bmp_character_counts_as_two_utf16_units) build payload hashes with no :handoff key, so .except(:handoff) is a no-op for them; none of their assertions change. A new test case is required (see Required work) to cover the :handoff-present path.
  • StartSubagent#payload (plugins/os-sdlc/lib/os_sdlc/pipeline/start_subagent.rb:40-50) merges brief (from BriefComposer#compose) onto the envelope hash; it does not add :handoff itself.
  • BriefComposer#compose (plugins/os-sdlc/lib/os_sdlc/pipeline/brief_composer.rb) always includes a handoff: key via handoff_json, which returns nil when @handoff is absent — so :handoff is present with a nil value when there is no handoff, not absent from the hash. .except(:handoff) removes the key in both cases, so this is safe either way.
  • docs/adr/0206 (docs/adr/0206-*.md), Consequences section, line 37: "Handoff content is not an enrich fact, so a handoff alone over the cap still overflows. This is a follow-up." — this line names the exact gap this ticket closes and must be updated or superseded by the amending ADR, not left standing as an open follow-up.

Non-goals

  • The amending ADR (ADR-0206 consequences line 37 names this gap): written once for #630, #662, #670 after all three land, outside this run.

  • Trimming or capping :brief/ticket-brief content size independent of the handoff (rejected already in ADR-0206 alternatives).

  • Any change to BriefLookup#call or the os-sdlc-runner brief fetch CLI — it already returns the handoff on request (plugins/os-sdlc/lib/os_sdlc/pipeline/brief_lookup.rb:20), so no seam is lost by removing :handoff from the fallback.

  • Moving HOOK_CONTEXT_CAP to a different limit — separate follow-up per ADR-0206.

Origin

• Trigger: ADR-0206 consequences, review of the #627 diff in session 94320dd4
• Improvised this session: none
• Chain: agent loses fetch instruction ← fallback payload exceeds cap ← handoff/brief not treated as enrich facts ← DESIGN (ADR-0206 consequences)
• Root candidate: this ticket
• Where: plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb
• Session: 94320dd4-8ab4-49a8-ad82-a46504401428
• Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/94320dd4-8ab4-49a8-ad82-a46504401428.jsonl

Skeptic verdict: CREATE — real, documented gap (ADR-0206) in the #627 fix with a clear cause chain ending in a design decision, not improvisation; distinct root (handoff/brief not treated as enrich facts) from the other same-session ticket (behavior-verifier), so no root collision; scoped illustration provided.

## Problem ADR-0206 records a known gap in the #627 hook-payload cap fix. `OsSdlc::Pipeline::HookPayload#to_json` (plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb, shipped by #627) drops only the map node's enrich facts when the serialized SubagentStart payload exceeds `HOOK_CONTEXT_CAP` (10,000 UTF-16 units). The `:handoff` key (the pending handoff content from the prior agent) is not an enrich fact, so it survives the fallback. `fallback_payload` (line 28) already replaces `:brief` with the fetch instruction, so the brief file body is already covered; `:handoff` is the only large key left in the fallback envelope. A handoff alone larger than about 10,000 units still pushes the fallback over the cap. Claude Code then persists the string to a file with a 2,000-character preview, and the agent loses the fetch instruction. This was witnessed live; that observation is how this ticket was opened. (The original body's "Not yet observed live" line was wrong and is corrected here.) ## Approach Whenever the payload falls back at all, drop `:handoff` unconditionally. One measurement, one fallback shape: ```ruby def fallback_payload @payload.reject { |key, _| @enrich_keys.include?(key) } .except(:handoff) .merge(brief: fallback_instruction) end ``` Fallback envelope after the change: `session_id`, `implementation_id`, `dispatch_id`, `agent`, `node`, `instructions`, `brief` (fetch instruction). **Rejected alternative:** a second measurement that keeps `:handoff` when it fits (two fallback shapes for no gain). Required work: 1. Code: change `fallback_payload` in plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb to `.except(:handoff)` as above. 2. Test: a payload with a 12,000-unit handoff and no enrich keys serializes under the cap and keeps `dispatch_id` and the fetch instruction (asserts `:handoff` is absent from the fallback result). ## Acceptance criteria - A `HookPayload` built from a payload containing a `:handoff` key whose serialized size alone exceeds `HOOK_CONTEXT_CAP`, with no enrich keys present, serializes to JSON at or under `HOOK_CONTEXT_CAP` UTF-16 units. - The serialized fallback result never contains a `:handoff`/`"handoff"` key, regardless of whether the un-fallen-back payload fit without it. - The serialized fallback result still contains `dispatch_id` and a `brief` value including `os-sdlc-runner brief --dispatch-id <id>`. - Existing `hook_payload_test.rb` cases (none of which construct a payload containing `:handoff`) continue to pass unmodified. ## Owner sketch ```ruby class OsSdlc::Pipeline::HookPayload def to_json(*) private def fallback_payload end ``` ## Blast radius \1 - plugins/os-sdlc/tests/pipeline/hook_payload_test.rb — three existing tests (`test_payload_at_exactly_the_cap_is_emitted_unchanged_with_enriched_facts_inline`, `test_payload_over_the_cap_emits_routing_envelope_and_brief_command_only`, `test_a_non_bmp_character_counts_as_two_utf16_units`) build payload hashes with no `:handoff` key, so `.except(:handoff)` is a no-op for them; none of their assertions change. A new test case is required (see Required work) to cover the `:handoff`-present path. - `StartSubagent#payload` (plugins/os-sdlc/lib/os_sdlc/pipeline/start_subagent.rb:40-50) merges `brief` (from `BriefComposer#compose`) onto the envelope hash; it does not add `:handoff` itself. - `BriefComposer#compose` (plugins/os-sdlc/lib/os_sdlc/pipeline/brief_composer.rb) always includes a `handoff:` key via `handoff_json`, which returns `nil` when `@handoff` is absent — so `:handoff` is present with a `nil` value when there is no handoff, not absent from the hash. `.except(:handoff)` removes the key in both cases, so this is safe either way. - docs/adr/0206 (docs/adr/0206-*.md), Consequences section, line 37: "Handoff content is not an `enrich` fact, so a handoff alone over the cap still overflows. This is a follow-up." — this line names the exact gap this ticket closes and must be updated or superseded by the amending ADR, not left standing as an open follow-up. ## Non-goals - The amending ADR (ADR-0206 consequences line 37 names this gap): written once for #630, #662, #670 after all three land, outside this run. - Trimming or capping `:brief`/ticket-brief content size independent of the handoff (rejected already in ADR-0206 alternatives). - Any change to `BriefLookup#call` or the `os-sdlc-runner brief` fetch CLI — it already returns the handoff on request (plugins/os-sdlc/lib/os_sdlc/pipeline/brief_lookup.rb:20), so no seam is lost by removing `:handoff` from the fallback. - Moving `HOOK_CONTEXT_CAP` to a different limit — separate follow-up per ADR-0206. ## Origin • Trigger: ADR-0206 consequences, review of the #627 diff in session 94320dd4 • Improvised this session: none • Chain: agent loses fetch instruction ← fallback payload exceeds cap ← handoff/brief not treated as enrich facts ← DESIGN (ADR-0206 consequences) • Root candidate: this ticket • Where: plugins/os-sdlc/lib/os_sdlc/pipeline/hook_payload.rb • Session: 94320dd4-8ab4-49a8-ad82-a46504401428 • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/94320dd4-8ab4-49a8-ad82-a46504401428.jsonl Skeptic verdict: CREATE — real, documented gap (ADR-0206) in the #627 fix with a clear cause chain ending in a design decision, not improvisation; distinct root (handoff/brief not treated as enrich facts) from the other same-session ticket (behavior-verifier), so no root collision; scoped illustration provided.
Author
Owner

Work started on branch ticket-630 via /os-sdlc:implement.

Work started on branch ticket-630 via /os-sdlc:implement.
Author
Owner

Resolution

Done: HookPayload#fallback_payload drops :handoff whenever the payload falls back over HOOK_CONTEXT_CAP; the fetch CLI already returns it. One fallback shape: session_id, implementation_id, dispatch_id, agent, node, instructions, brief.

Evidence: Branch ticket-630 commit f4e8634c via os-sdlc implementation 22: 8 dispatches, 0 retries, all pass; new test test_handoff_alone_over_cap_with_no_enrich_keys_is_dropped_and_dispatch_id_and_brief_kept; suite green (dispatch 255).

Follow-ups: Amending ADR for ADR-0206 (envelope shape) written once for #630/#662/#670 after all three land; tracked in #662 and #670 bodies. No other follow-ups.

## Resolution **Done:** HookPayload#fallback_payload drops :handoff whenever the payload falls back over HOOK_CONTEXT_CAP; the fetch CLI already returns it. One fallback shape: session_id, implementation_id, dispatch_id, agent, node, instructions, brief. **Evidence:** Branch ticket-630 commit f4e8634c via os-sdlc implementation 22: 8 dispatches, 0 retries, all pass; new test test_handoff_alone_over_cap_with_no_enrich_keys_is_dropped_and_dispatch_id_and_brief_kept; suite green (dispatch 255). **Follow-ups:** Amending ADR for ADR-0206 (envelope shape) written once for #630/#662/#670 after all three land; tracked in #662 and #670 bodies. No other follow-ups.
jared closed this issue 2026-09-24 16:12:08 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#630
No description provided.