GCOMPACTION.md — Design & Architecture (TABLED)
**Target path on approval:** `docs/designs/GCOMPACTION.md`
Overview
GCOMPACTION.md — Design & Architecture (TABLED)
Target path on approval: docs/designs/GCOMPACTION.md
This is the preserved design artifact for gstack compact. Everything above the first --- divider below gets extracted verbatim to docs/designs/GCOMPACTION.md on plan approval. Everything after that divider is archived research (office hours + competitive deep-dive + eng-review notes + codex review + research findings) that informed the design.
Status: TABLED (2026-04-17) — pending Anthropic updatedBuiltinToolOutput API
Why tabled. The v1 architecture assumed a Claude Code PostToolUse hook could REPLACE the tool output that enters the model's context for built-in tools (Bash, Read, Grep, Glob, WebFetch). Research on 2026-04-17 confirmed this is not possible today.
Evidence:
- Official docs (https://code.claude.com/docs/en/hooks): The only output-replace field documented for
PostToolUseishookSpecificOutput.updatedMCPToolOutput, and the docs explicitly state: "For MCP tools only: replaces the tool's output with the provided value." No equivalent field exists for built-in tools. - Anthropic issue #36843 (OPEN): Anthropic themselves acknowledge the gap. "PostToolUse hooks can replace MCP tool output via
updatedMCPToolOutput, but there is no equivalent for built-in tools (WebFetch, WebSearch, Bash, Read, etc.)... They can only add warnings viadecision: block(which injects a reason string) oradditionalContext. The original malicious content still reaches the model." - RTK mechanism (source-reviewed at
src/hooks/init.rs:906-912andhooks/claude/rtk-rewrite.sh:83-100): RTK is NOT a PostToolUse compactor. It's a PreToolUse Bash matcher that rewritestool_input.command(e.g.,git status→rtk git status). The wrapped command produces compact stdout itself. RTK README confirms: "the hook only runs on Bash tool calls. Claude Code built-in tools like Read, Grep, and Glob do not pass through the Bash hook, so they are not auto-rewritten." RTK is Bash-only by architectural constraint, not by choice. - tokenjuice mechanism (source-reviewed at
src/core/claude-code.ts:160, 491, 540-549): tokenjuice DOES registerPostToolUsewithmatcher: "Bash"but has no real output-replace API available — it hijacksdecision: "block"+reasonto inject compacted text. Whether this actually reduces model-context tokens or just overlays UI output is disputed. tokenjuice is also Bash-only. - Read/Grep/Glob execute in-process inside Claude Code and bypass hooks entirely. Wedge (ii) "native-tool coverage" was architecturally impossible from day one regardless of replacement API.
Consequence. Both wedges are dead in their original form:
- Wedge (i) "Conditional LLM verifier" — still technically possible, but only for Bash output, via PreToolUse command wrapping (RTK's mechanism). The verifier stops being a differentiator once we're also Bash-only.
- Wedge (ii) "Native-tool coverage" — impossible today. Read/Grep/Glob don't fire hooks. Even if they did, no output-replace field exists.
Decision. Shelve gstack compact entirely. Track Anthropic issue #36843 for the arrival of updatedBuiltinToolOutput (or equivalent). When that API ships, this design doc + the 15 locked decisions below + the research archive at the bottom become the unblocking artifacts for a fresh implementation sprint.
If un-tabling: Start from the "Decisions locked during plan-eng-review" block below — most remain valid. Then re-verify the hooks reference against the newly-shipped API, update the Architecture data-flow diagram to use whatever real output-replacement field exists, and re-run /codex review against the revised plan before coding.
What we're NOT doing:
- Not shipping a Bash-only PreToolUse wrapper. That's RTK's product; they're at 28K stars and 3 years of rule scars. No wedge.
- Not shipping the
decision: block+reasonhack. Undocumented behavior, Anthropic could break it, and the model may still see the raw output alongside the compacted overlay — context savings are disputed. - Not shipping B-series benchmark in isolation. Without a working compactor, there's nothing to benchmark.
Cost of tabling: ~0. No code was written. The design doc + research + decisions remain as a ready-to-unblock artifact.
Decisions locked during plan-eng-review (2026-04-17)
Preserved for the un-tabling sprint if/when Anthropic ships the built-in-tool output-replace API.
Summary of every decision made during the engineering review. Full rationale is preserved throughout the sections below; this block is the single source of truth if anything else drifts.
Scope (Section 0):
- Claude-first v1. Ship compact + rules + verifier on Claude Code only. Codex + OpenClaw land at v1.1 after the wedge is proven on the primary host. Cuts ~2 days of host integration and derisks launch. The original "wedge (ii) native-tool coverage" claim applies to Claude Code at v1; we make no cross-host claim until v1.1.
- 13-rule launch library. v1 ships tests (jest/vitest/pytest/cargo-test/go-test/rspec) + git (diff/log/status) + install (npm/pnpm/pip/cargo). Build/lint/log families defer to v1.1, driven by
gstack compact discovertelemetry from real users. - Verifier default ON at v1.0.
failureCompactiontrigger (exit≠0 AND >50% reduction) is enabled out of the box. The verifier IS the wedge — defaulting it off hides the differentiating feature. Trigger bounds already keep expected fire rate ≤10% of tool calls.
Architecture (Section 1):
4. Exact line-match sanitization for Haiku output. Split raw output by \ , put lines in a set, only append lines from Haiku that appear verbatim in that set. Tightest adversarial contract; prompt-injection attempts cannot slip in novel text.
5. Layered failureCompaction signal. Prefer exitCode from the envelope; if the host omits it, fall back to /FAIL|Error|Traceback|panic/ regex on the output. Log which signal fired in meta.failureSignal ("exit" | "pattern" | "none"). Pre-implementation task #1 still verifies Claude Code's envelope empirically, but the system no longer breaks if it doesn't.
6. Deep-merge rule resolution. User/project rules inherit built-in fields they don't override. Escape hatch: "extends": null in a rule file triggers full replacement semantics. Matches the mental model of eslint/tsconfig/.gitignore — override a piece without losing the rest.
Code quality (Section 2):
7. Per-rule regex timeout, no RE2 dep. Run each rule's regex via a 50ms AbortSignal budget; on timeout, skip the rule and record meta.regexTimedOut: [ruleId]. Avoids a WASM dependency and keeps rule-author syntax unconstrained.
8. Pre-compiled rule bundle. gstack compact install and gstack compact reload produce ~/.gstack/compact/rules.bundle.json (deep-merged, regex-compiled metadata cached). Hook reads that single file instead of parsing N source files.
9. Auto-reload on mtime drift. Hook stats rule source files on startup; if any source file is newer than the bundle, rebuild in-line before applying. Adds ~0.5ms/invocation but eliminates the "I edited a rule and nothing changed" footgun.
10. Expanded v1 redaction set. Tee files redact: AWS keys, GitHub tokens (ghp_/gho_/ghs_/ghu_), GitLab tokens (glpat-), Slack webhooks, generic JWT (three base64 segments), generic bearer tokens, SSH private-key headers (-----BEGIN * PRIVATE KEY-----). Credit cards / SSNs / per-key env-pairs deferred to a full DLP layer in v2.
Testing (Section 3):
11. P-series gate subset. v1 gate-tier P-tests: P1 (binary garbage), P3 (empty output), P6 (RTK-killer critical stack frame), P8 (secrets to tee), P15 (hook timeout), P18 (prompt injection), P26 (malformed user rule JSON), P28 (regex DoS), P30 (Haiku hallucination). Remaining 21 P-cases grow R-series as real bugs hit.
12. Fixture version-stamping. Every golden fixture has a toolVersion: frontmatter. CI warns when fixture toolVersion ≠ currently installed. No more calendar-based rotation.
13. B-series real-world benchmark testbench (hard v1 gate). New component compact/benchmark/ scans ~/.claude/projects/**/*.jsonl, ranks the noisiest tool calls, clusters them into named scenarios, replays the compactor against them, and reports reduction-by-rule-family. v1 cannot ship until B-series on the author's own 30-day corpus shows ≥15% reduction AND zero critical-line loss on planted bugs. Local-only; never uploads. Community-shared corpus is v2.
Performance (Section 4):
14. Revised latency budgets. Bun cold-start on macOS ARM is 15-25ms; the original 10ms p50 target was unrealistic. New budgets: <30ms p50 / <80ms p99 on macOS ARM, <20ms p50 / <60ms p99 on Linux (verifier off). Verifier-fires budget stays <600ms p50 / <2s p99. Daemon mode is a v2 option gated on B-series showing cold-start hurts session savings.
15. Line-oriented streaming pipeline. Readline over stdin → filter → group → dedupe → ring-buffered tail truncation → stdout. Any single line >1MB hits P9 (truncate to 1KB with [... truncated ...] marker). Caps memory at 64MB regardless of total output size.
Every row above is a MUST in the implementation. Drift requires a new eng-review.
Summary
gstack compact was designed as a PostToolUse hook that reduces tool-output noise before it reaches an AI coding agent's context window. Deterministic JSON rules would shrink noisy test runners, build logs, git diffs, and package installs. A conditional Claude Haiku verifier would act as a safety net when over-compaction risk was high.
Current status: TABLED. See "Status" section above. The architecture depends on a Claude Code API (updatedBuiltinToolOutput or equivalent for built-in tools) that does not exist as of 2026-04-17. Anthropic issue #36843 tracks the gap.
Intended goal (preserved for the un-tabling sprint): 15–30% tool-output token reduction per long session, with zero increase in task-failure rate.
Original wedge (vs RTK, the 28K-star incumbent) — both invalidated by research:
Conditional LLM verifier.Still technically viable via PreToolUse command wrapping, but only for Bash. Stops being a differentiator once we're Bash-only. Reconsider if the built-in-tool API arrives.Native-tool coverage.Architecturally impossible today. Read/Grep/Glob execute in-process inside Claude Code and do not fire hooks. Even for tools that do firePostToolUse, no output-replacement field exists for non-MCP tools.
Original positioning (now moot): "RTK is fast. gstack compact is fast AND safe, and it covers every tool in your toolbox, not just Bash."
Non-goals
- Summarizing user messages or prior agent turns (Claude's own Compaction API owns that).
- Compressing agent response output (caveman's layer).
- Caching tool calls to avoid re-execution (token-optimizer-mcp's layer).
- Acting as a general-purpose log analyzer.
- Replacing the agent's own judgement about when to re-run a command with
GSTACK_RAW=1.
Why this is worth building
Problem is measured, not hypothetical.
- Chroma research (2025) tested 18 frontier models. Every model degrades as context grows. Rot starts well before the window limit — a 200K model rots at 50K.
- Coding agents are the worst case: accumulative context + high distractor density + long task horizon. Tool output is explicitly named as a primary noise source.
- The market has voted: Anthropic shipped Opus 4.6 Compaction API; OpenAI shipped a compaction guide; Google ADK shipped context compression; LangChain shipped autonomous compression; sst/opencode has built-in compaction. The hybrid deterministic + LLM pattern is industry consensus.
Existing field (what gstack compact joins and differentiates from):
| Project | Stars | License | Layer | Threat | Note |
|---|---|---|---|---|---|
| RTK (rtk-ai/rtk) | 28K | Apache-2.0 | Tool output | Primary benchmark | Pure Rust, Bash-only, zero LLM |
| caveman | 34.8K | MIT | Output tokens | Different axis | Terse system prompt; pairs WITH us |
| claude-token-efficient | 4.3K | MIT | Response verbosity | Different axis | Single CLAUDE.md |
| token-optimizer-mcp | 49 | MIT | MCP caching | Different axis | Prevents calls rather than compresses output |
| tokenjuice | ~12 | MIT | Tool output | Too new | 2 days old; inspired our JSON envelope |
| 6-Layer Token Savings Stack | — | Public gist | Recipe | Zero | Documentation; validates stacked compaction thesis |
RTK is the only direct competitor. Everything else compresses a different token source.
License compatibility: Every referenced project is permissive-licensed (MIT or Apache-2.0) and compatible with gstack's MIT license. No AGPL, GPL, or other copyleft dependencies. See the "License & attribution" section below for the clean-room policy.
Architecture
Data flow
┌─────────────────────────────────────────────────────────────────┐
│ Host (Claude Code / Codex / OpenClaw) │
│ ───────────────────────────────────────── │
│ 1. Agent requests tool call: Bash|Read|Grep|Glob|MCP │
│ 2. Host executes tool │
│ 3. Host invokes PostToolUse hook with: {tool, input, output} │
└────────────────────┬────────────────────────────────────────────┘
│ stdin (JSON envelope)
▼
┌─────────────────────────────────────────────────────────────────┐
│ gstack-compact hook binary │
│ ─────────────────────────── │
│ a. Parse envelope │
│ b. Match rule by (tool, command, pattern) │
│ c. Apply rule primitives: filter / group / truncate / dedupe │
│ d. Record reduction metadata │
│ e. Evaluate verifier triggers │
│ f. If trigger met: call Haiku, append preserved lines │
│ g. On failure exit code: tee raw to ~/.gstack/compact/tee/... │
│ h. Emit JSON envelope to stdout │
└────────────────────┬────────────────────────────────────────────┘
│ stdout (JSON envelope)
▼
Host substitutes compacted output into agent context
Rule resolution
Three-tier hierarchy (highest precedence wins), same pattern as tokenjuice and gstack's existing host-config-export model:
- Built-in rules:
compact/rules/shipped with gstack - User rules:
~/.config/gstack/compact-rules/ - Project rules:
.gstack/compact-rules/
Rules match tool calls by rule ID. A project rule with ID tests/jest overrides the built-in