Skip to content

perf(mcp): drop duplicated structured arrays from get_instructions (−51% payload) - #1891

Merged
simple-agent-manager[bot] merged 7 commits into
mainfrom
sam/remove-duplicated-structured-arrays-b6ggy4
Aug 23, 2026
Merged

perf(mcp): drop duplicated structured arrays from get_instructions (−51% payload)#1891
simple-agent-manager[bot] merged 7 commits into
mainfrom
sam/remove-duplicated-structured-arrays-b6ggy4

Conversation

@simple-agent-manager

@simple-agent-manager simple-agent-manager Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

get_instructions — the tool every SAM agent session must call before doing anything — was sending every knowledge observation and every policy twice: once rendered as markdown in knowledgeDirectives/policyDirectives, and again as structured JSON in knowledgeContext/policyContext, labelled "Also include structured data for programmatic use." Nothing has ever consumed the structured arrays.

Verified byte-for-byte on the real SAM payload with jq: 81/81 policy titles and bodies and 50/50 knowledge observations appear verbatim inside the rendered directives.

This PR stops emitting the two arrays. They remain as local variables feeding the formatters and the hasKnowledge/hasPolicies instruction switches.

Real SAM project: 166,814 → ~81,025 chars (−51%, ~21.4k tokens saved on every session bootstrap).

Critical implementation note: policy IDs

policyDirectives rendered - **{title}**: {content} with no id (0 of 81 policy ids appeared anywhere in it), while instructions[14] tells the agent to call update_policy/remove_policy — both of which require policyId. The id existed only in the array being removed.

formatPolicyDirectives now renders it inline: - **{title}** (id: {uuid}): {content}.

The id is rendered in FULL, not elided. The task brief suggested (id: 7d24e435…), but policies.ts:118,136 resolve with WHERE id = ? (exact match), so an abbreviated id would silently fail to address the row. Cost: +3,483 chars against 52,157 saved.

Deliberately not fixed here: observationId is required by update_knowledge/remove_knowledge/confirm_knowledge but has never been in the response — knowledgeContext mapped only entityName/entityType/observation/confidence, though the underlying SELECT o.* returns the id. So this PR loses nothing those tools could have used. Filed as tasks/backlog/2026-08-23-get-instructions-missing-observation-ids.md; fixing it means reshaping the ' | '-joined per-entity grouping, which would collide with in-flight sibling R3.

Rule 44 — consumer enumeration

Location Kind Action
instruction-tools.ts only producer arrays become locals, no longer emitted
apps/web/, packages/vm-agent/, tests, specs/, .claude/, apps/www/ no runtime consumer exists
tasks/archive/2026-04-26-policy-propagation-phase4.md historical record left alone
tasks/backlog/2026-04-13-knowledge-graph-test-coverage.md unstarted criteria naming the removed field updated to knowledgeDirectives

Independently re-verified by architecture-reviewer including packages/vm-agent/**/*.go and apps/web/src/**: the Go/web get_instructions hits all concern the injected system-reminder prompt text, not response parsing.

Coordination with in-flight siblings

origin/main was unchanged at push time, so this lands first as planned. architecture-reviewer confirmed via git merge-tree a guaranteed (mechanical, not design-level) conflict with R2 sam/add-lifecycle-controls-sam-jrnt7n on three spans. The merged template line should read:

`- **${item.title}** (id: ${item.id})${item.lifecycle}: ${item.content}`

Whoever rebases R2 should re-run this PR's renders ids in FULL... test, which asserts the exact (id: ${id}) string shape. R3 does not conflict — this diff never touches the knowledge block.

Validation

  • pnpm lint — 0 errors (3 pre-existing apps/web warnings, untouched)
  • pnpm typecheck — 19/19 tasks clean
  • pnpm testapps/api: 595 files, 8,037 tests, 0 failed, 0 collection errors (8,025 baseline + 12; now 14 after review fixes)
  • Additional: pnpm build 9/9; pnpm check:fast 13/13 with 0 blocking type-boundary findings
  • N/A — no sweep/cron/alarm candidate-selection change

Discriminating-test proof (rules 02/62): re-adding the two structured arrays turns exactly 5 of 14 tests red — the three "exactly once" body/title/observation tests, the "omits structured arrays" test, and the same-entity multi-observation test. Restored → 14/14 green. Verified twice (before and after the review fixes), and independently reproduced by test-engineer.

Note on reporting: a failed import shows up in vitest as 0 passed / 0 failed / success:false. Totals here were read per-file from the JSON reporter with an explicit assertionResults.length check, so a collection error cannot masquerade as a pass.

Staging Verification (REQUIRED for all code changes — merge-blocking)

  • Staging deployment green — run 32648180754, conclusion success
  • Live app verified via Playwrightapp.sammy.party, token-login 200
  • Existing workflows confirmed working — dashboard, /projects (16 listed), /settings all render at 375x667 and 1280x800; GET /health 200; 0 console errors at both viewports
  • New feature/fix verified on staging — see evidence below
  • N/A: no infra changes — does not touch packages/cloud-init/, packages/vm-agent/, scripts/deploy/, DNS, or TLS
  • Mobile and desktop verification notes added (no UI change, but both viewports exercised)

Staging Verification Evidence

Measured through the real deployed Worker via POST https://api.sammy.party/mcp with a live MCP token read from staging KV — the exact code path an agent bootstraps through. Project 01KTKXZ4ZZAT6MJFXRW1ZTQ7RB seeded with 3 marked policies (one per category) + 1 observation; identical data measured either side of the deploy.

BEFORE (pre-fix main) AFTER (this branch)
total payload 6,025 chars 5,230 chars
knowledgeContext 177 chars, 1 item ABSENT
policyContext 652 chars, 3 items ABSENT
policyDirectives 481 chars 610 chars (+129 = the 3 inline ids)
occurrences of each body 2 1
full policy ids rendered 0 of 3 3 of 3

The absolute delta is small only because the fixture is small — fixed context/task/project/instructions overhead (~4,010 chars) dominates at that size. What was eliminated is the duplication, which scales with knowledge + policy volume: on the real SAM project (81 policies, 50 observations) that is 166,814 → ~81,025.

Management round-trip proven end-to-end — the decisive check, and exactly what an elided id would have broken:

  1. Scraped c546f67b-… out of - **...** (id: ...) in the rendered markdown
  2. update_policy with only that id → {"updated":true,...}
  3. Fresh get_instructions → new text present, stale text gone
  4. remove_policy × 3 → {"removed":true,...} each

Incidentally confirmed the filed observationId gap: removing the seeded observation required search_knowledge to obtain the id, because get_instructions still does not expose it.

Cleanup: all seeded data removed; payload returned to exactly the 4,010-char empty-path shape with zero R1VERIFY residue. No VMs or nodes were created (the MCP endpoint was exercised directly), so the zero-VMs-at-rest rule is respected.

UI Compliance Checklist

N/A: no UI changes — diff touches only apps/api/src/routes/mcp/instruction-tools.ts, one test file, and task markdown.

End-to-End Verification

  • Data flow traced with code path citations
  • Capability test exercises the complete happy path across system boundaries
  • Assumptions verified against code, not assumed
  • Gaps documented below

Data Flow Trace

1. Agent calls get_instructions (MCP tools/call)
   -> apps/api/src/index.ts:801  app.route('/mcp', mcpRoutes)
   -> apps/api/src/routes/mcp/index.ts:278  case 'get_instructions'

2. Handler retrieves knowledge + policies
   -> instruction-tools.ts:148  projectDataService.getAllHighConfidenceKnowledge()
   -> instruction-tools.ts:187  projectDataService.getActivePolicies()
       (both unchanged; both still wrapped in try/catch -> [])

3. Rendered to markdown — the SINGLE representation
   -> instruction-tools.ts:169  formatKnowledgeDirectives()
   -> instruction-tools.ts:202  formatPolicyDirectives()   <-- now interpolates item.id

4. Result object omits the structured arrays        <-- THE CHANGE
   -> instruction-tools.ts:289-297

5. Agent reads the id and acts on it
   -> update_policy / remove_policy
   -> durable-objects/project-data/policies.ts:118,136  WHERE id = ?  (exact match)

Step 5 is why the id must be full-length; it was verified live on staging, not just reasoned about.

Untested Gaps

No same-process integration test round-trips the rendered id through the real handleUpdatePolicy. formatPolicyDirectives does a literal ${item.id} interpolation with no transformation, so the id is unmodified by construction — and the round-trip was exercised end-to-end against the deployed staging Worker (evidence above), which is stronger than a mocked test. Raised by task-completion-validator as MEDIUM and resolved that way.

Post-Mortem

N/A: not a bug fix — this is a token/cost optimization (R1 of the token-optimization program, idea 01M0QGZQ15WE9DDQENGV6S9XZ5). No user-visible defect is being corrected, so no process fix is required. One pre-existing gap discovered during research was filed rather than silently inherited: tasks/backlog/2026-08-23-get-instructions-missing-observation-ids.md.

Specialist Review Evidence

  • All local reviewers completed and findings addressed before merge
  • N/A — no reviewer failed to complete, so no needs-human-review label needed
Reviewer Status Outcome
cloudflare-specialist PASS 0 CRITICAL/HIGH. MCP JSON-RPC envelope unchanged and get_instructions declares no outputSchema, so no output contract is violated; DO RPC calls and both try/catch → [] fallbacks byte-for-byte unchanged; 0 new round-trips (rule 60). Proved (id: undefined) is impossibleparsePolicyRow requires v.string(), so a bad id throws upstream and the batch degrades to [] rather than rendering a corrupt line. 2 LOW informational notes, no in-branch action.
architecture-reviewer ADDRESSED 0 CRITICAL/HIGH. Independently re-ran the rule-44 enumeration (incl. vm-agent Go + web) — matched exactly. Endorsed the prose-id design: list_policies/get_policy already provide structured access, so a policyIds array would be a redundant fourth representation and a correlatable {id,title}[] would cost ~2x the inline form. MEDIUM "add a comment so nobody re-adds the array" applied in 62e3bcba1. MEDIUM R2 merge-conflict note is informational (see Coordination above).
test-engineer ADDRESSED 0 CRITICAL/HIGH. Independently reproduced the discriminating check. Real gaps fixed in 62e3bcba1: missing liveness assertion on the absence test (rule 62 req 5); no coverage anywhere in the repo of the same-entity multi-observation `'
task-completion-validator ADDRESSED Returned FAIL with 1 CRITICAL — and was right. The task file had been archived with a blanket sed that ticked every box including the staging criterion before staging had run, and archiving preceded the validator, violating rule 09. Fixed in 62e3bcba1: moved back to tasks/active/, un-ticked the two non-empirical criteria, relabelled the Outcome section. Both MEDIUMs (projected-not-measured AFTER figure; no live id round-trip) are now resolved by the staging evidence above; task archived in 754bcf0bf only after that evidence existed.

Not run, with reason: go-specialist (no Go), ui-ux-specialist (no UI), security-auditor (no auth/credential/token code), env-validator (no env vars), doc-sync-validator (grep confirmed no doc anywhere references these fields), constitution-validator (no hardcoded values; the (id: ...) format string is presentation, not configuration).

Exceptions

  • Scope: task file committed to the feature branch rather than main as /do Phase 1 prescribes.
  • Rationale: branch protection rejects direct pushes to main (Required status check "Durable Object Workers" is expected). Also the safer default per CLAUDE.md's "never commit directly to main."
  • Expiration: n/a — permanent, this is the correct behaviour.

Agent Preflight (Required)

  • Preflight completed before code changes

Classification

  • external-api-change
  • cross-component-change
  • business-logic-change
  • public-surface-change
  • docs-sync-change
  • security-sensitive-change
  • ui-change
  • infra-change

public-surface-change: the MCP get_instructions response shape changes for every agent. business-logic-change: how policy identifiers reach the agent changes.

External References

N/A: no external API involved. Sources were in-repo and empirical — the live get_instructions payload (measured with jq), .claude/rules/44 (enumerate every consumer), .claude/rules/02/62 (discriminating tests), and library research doc /engineering/research/token-optimization-research.md §3.1 and §8/R1.

Codebase Impact Analysis

apps/api/src/routes/mcp/instruction-tools.ts (only runtime file: −2 emitted fields, +1 rendered id, comments), apps/api/tests/unit/routes/mcp-instruction-payload-dedup.test.ts (new, 14 tests), 3 task markdown files. No packages/ change, no apps/web change, no schema/migration, no new dependency.

Documentation & Specs

apps/www/src/content/docs/docs/architecture/overview.md — added an Agent Bootstrap Payload (get_instructions) subsection under Agent Configuration Layers. It documents that knowledge and policies are delivered as rendered markdown only, that there is deliberately no second structured copy (and why the knowledgeContext/policyContext arrays were removed), that list_policies/get_policy and search_knowledge/get_project_knowledge are the machine-readable paths, and that policy lines carry full untruncated ids because WHERE id = ? is an exact match. It also records that observations still lack observationId in this payload.

No prior doc described this response shape — grep across apps/www/, specs/, and .claude/ found zero references to knowledgeContext/policyContext, and the get_instructions tool description does not mention either field. The one stale mention (acceptance criteria in an unstarted backlog task) is also updated in this PR.

Constitution & Risk Check

Principle XI (no hardcoded values): no new URL, timeout, or limit. The (id: ...) template is presentation, not configuration.

Risks / tradeoffs

  1. Removing a field others might depend on — mitigated by the rule-44 enumeration, independently re-verified by a second reviewer.
  2. Elided id would break policy management — the reason full UUIDs are rendered; proven live on staging by scraping an id and mutating with it.
  3. Prose id vs structured field — accepted deliberately; list_policies/get_policy remain the structured path, and a comment in the code prevents a future "fix" from re-adding the array.
  4. R2 merge conflict — guaranteed but mechanical; the resolved line is given above.

Merge to main and production ship for this work were explicitly authorized by Raphaël (2026-08-23), recorded in the SAM task description for 01M0QHHVEQY11VC37Y29B6GGY4.

🤖 Generated with Claude Code

raphaeltm and others added 6 commits August 23, 2026 14:55
Also files the pre-existing missing-observationId gap discovered during research.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
get_instructions emitted every knowledge observation and every policy twice:
once rendered in knowledgeDirectives/policyDirectives and again as structured
JSON in knowledgeContext/policyContext. Verified byte-identical (81/81 policy
titles+bodies, 50/50 observations). Nothing consumed the structured arrays.

On the real SAM project this is 166,814 -> 81,025 chars (-51%, ~21.4k tokens)
on every session bootstrap.

Policy IDs lived ONLY in the structured array, so formatPolicyDirectives now
renders them inline as "(id: <uuid>)". IDs are rendered in FULL because
update_policy/remove_policy resolve with WHERE id = ? (exact match); an
abbreviated id would not address the row. Cost: +3,483 chars vs 52,157 saved.

observationId was never present in knowledgeContext, so the knowledge
management tools lose nothing here. That pre-existing gap is filed separately.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
test-engineer:
- add liveness assertions to the 'omits structured arrays' test (rule 62 req 5) —
  an absence assertion is also satisfied by a payload that rendered nothing
- add same-entity multi-observation test: formatKnowledgeDirectives joins an entity's
  observations with ' | ', and nothing in the repo covered that branch
- add the missing policy-retrieval-failure mirror of the knowledge-failure test
- fix vacuous not.toContain('…') — a [0-9a-f-]+ capture cannot hold an ellipsis, so the
  guard now runs against the whole rendered block
- escape the interpolated policy title before building the id-extraction RegExp
- drop the unused recordActivityEvent mock

architecture-reviewer:
- comment explaining why the policy id is intentionally in prose rather than a structured
  field, so a future reader does not 'fix' it by re-adding an array (list_policies /
  get_policy already serve structured access)

task-completion-validator (CRITICAL, correct):
- the task file had been archived with every box ticked, including the staging acceptance
  criterion that has not run. Moved back to tasks/active/ and un-ticked the two criteria
  that are not yet empirically verified.

Discriminating property re-verified after the test changes: re-adding the structured
arrays now turns 5 of 14 red; restored -> 14/14.

Co-Authored-By: Claude <noreply@anthropic.com>
Staging deploy 32648180754 green. Measured through the real deployed Worker on identical
seeded data: 6,025 -> 5,230 chars, every body 2x -> 1x, both context arrays gone, 3/3 full
policy ids rendered (was 0/3). Scraped-id -> update_policy -> remove_policy round-trip
proven. All seed data cleaned up. Playwright regression pass green, 0 console errors.

Co-Authored-By: Claude <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing sam/remove-duplicated-structured-arrays-b6ggy4 (4b8c2e4) with main (b36f235)

Open in CodSpeed

CI's Preflight Evidence gate correctly rejected 'N/A' for Documentation & Specs on a
public-surface-change: the MCP get_instructions response shape is consumed by every agent,
including in self-hosted forks, and no doc described it.

Adds an Agent Bootstrap Payload subsection to the architecture overview covering: rendered
markdown as the single representation, why the knowledgeContext/policyContext duplicate
arrays were removed, list_policies/get_policy and search_knowledge/get_project_knowledge as
the machine-readable paths, why policy ids are rendered untruncated (WHERE id = ? is exact),
and the still-missing observationId.

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@simple-agent-manager
simple-agent-manager Bot merged commit 69e8d12 into main Aug 23, 2026
51 of 52 checks passed
@simple-agent-manager
simple-agent-manager Bot deleted the sam/remove-duplicated-structured-arrays-b6ggy4 branch August 23, 2026 16:09
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.

1 participant