Skip to content

feat: token-aware context compression policy#104

Open
azalio wants to merge 4 commits intomainfrom
feat/context-compression
Open

feat: token-aware context compression policy#104
azalio wants to merge 4 commits intomainfrom
feat/context-compression

Conversation

@azalio
Copy link
Copy Markdown
Owner

@azalio azalio commented Apr 30, 2026

No description provided.

Copilot AI review requested due to automatic review settings April 30, 2026 09:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces a token-aware context-compression policy for MAP projects by measuring the last assistant turn’s input token usage from Claude Code transcript JSONL and injecting (or emitting) a /compact recommendation when a configured threshold is crossed.

Changes:

  • Added mapify_cli.token_budget module (+ unit tests) to compute last-turn token usage, derive effective thresholds, and format the /compact nudge message.
  • Added a Claude Code UserPromptSubmit hook (context-meter.py) plus a PreCompact cooldown marker to prevent immediate re-nudging after compaction.
  • Extended project config + mapify init flags to configure compression policy/threshold, and added an orchestrator stderr warning path for non-hook providers (e.g., Codex).

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_token_budget.py New unit tests covering token counting, threshold logic, nudge decision, and message formatting.
src/mapify_cli/token_budget.py New core implementation for reading transcript usage and generating /compact nudge text.
src/mapify_cli/templates/settings.json Registers the context-meter.py hook in the shipped template settings.
src/mapify_cli/templates/map/scripts/map_orchestrator.py Emits /compact recommendation to stderr when provided a transcript path and budget is crossed.
src/mapify_cli/templates/hooks/pre-compact-save-transcript.py Writes last-compact.marker cooldown marker after PreCompact transcript save.
src/mapify_cli/templates/hooks/context-meter.py New UserPromptSubmit hook that injects additionalContext with /compact guidance.
src/mapify_cli/config/project_config.py Adds compression config fields, post-load validation, and helper to write overrides into .map/config.yaml.
src/mapify_cli/__init__.py Adds mapify init flags for compression policy/threshold and persists them into .map/config.yaml.
docs/context-compression-plan.md Design/implementation plan doc for the context meter and policy behavior.
docs/USAGE.md Documents policies, defaults, and how the nudge appears/cools down (including orchestrator behavior).
README.md Quick-start mention of compression policy flags.
CHANGELOG.md Unreleased entry describing the new context compression policy feature set.
.map/scripts/map_orchestrator.py Mirrors orchestrator changes in the .map/ script copy.
.claude/settings.json Registers the context-meter.py hook for Claude Code runs in this repo.
.claude/hooks/pre-compact-save-transcript.py Writes cooldown marker after compaction transcript save in the repo hook.
.claude/hooks/context-meter.py Repo hook implementation for injecting additionalContext when threshold is crossed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/context-compression-plan.md Outdated
Comment on lines +3 to +5
Status: approved, not yet started
Owner: TBD
Last updated: 2026-04-29
Comment on lines +51 to +55
| Field | Default |
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
| `compression_policy` | `"auto"` — accepted: `never` / `auto` / `aggressive` |
| `compression_threshold_tokens` | `120_000` (~60% of Sonnet-200k; below Chroma-observed degradation zone) |
| `compression_focus` | `"MAP step state, last 2 monitor verdicts, pending subtasks; drop tool-result bodies older than 3 turns"` |
Comment thread src/mapify_cli/token_budget.py Outdated
Comment on lines +30 to +31
# Valid policy values. ``unknown`` policies are treated as ``never`` (fail
# safe — never inject the nudge if config is wrong).
Comment thread tests/test_token_budget.py Outdated
Comment on lines +232 to +234
# Default focus is the string used in the design doc.
assert "MAP step state" in msg
assert "/compact " in msg
- docs/context-compression-plan.md: mark status as shipped (was "approved, not yet started" after PR #104 already implemented it)
- token_budget.py: align format_compact_instruction default focus with the design-doc Defaults table (adds "drop tool-result bodies older than 3 turns")
- token_budget.py: fix VALID_POLICIES comment to match actual fallback (unknown -> auto, not never; matches existing test)
- test_token_budget.py: tighten test_blank_focus_falls_back_to_default to verify the full default string verbatim, so doc/code drift fails a test instead of a reviewer
- workflow-gate.py (.claude, .codex, both templates): drop pre-existing unused datetime/timezone imports flagged by ruff
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a token-aware context compression (“/compact nudge”) policy to MAP Framework by tracking transcript token usage and warning when a configurable budget is exceeded, across both Claude Code (hook) and Codex (orchestrator stderr).

Changes:

  • Introduces mapify_cli.token_budget utilities (token counting, threshold policy, nudge formatting) with a new dedicated test suite.
  • Extends .map/config.yaml via MapConfig (compression_policy, compression_threshold_tokens, compression_focus) and adds mapify init flags to write overrides.
  • Adds a Claude Code UserPromptSubmit hook (context-meter.py) plus an orchestrator --transcript-path option to emit the same guidance for non-hook providers.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/mapify_cli/token_budget.py New module for token counting + policy thresholding + formatting.
tests/test_token_budget.py Unit tests covering transcript parsing and policy behavior.
src/mapify_cli/config/project_config.py Adds compression config fields, validation, and init-time override writer.
src/mapify_cli/__init__.py Adds mapify init flags for compression policy/threshold and writes them into config.
src/mapify_cli/templates/hooks/context-meter.py New Claude Code hook to inject additionalContext nudge.
.claude/hooks/context-meter.py Repo hook counterpart for local development/templates.
src/mapify_cli/templates/hooks/pre-compact-save-transcript.py Writes cooldown marker to prevent immediate re-nudging post-compact.
.claude/hooks/pre-compact-save-transcript.py Repo hook counterpart writing the same cooldown marker.
src/mapify_cli/templates/map/scripts/map_orchestrator.py Emits nudge to stderr when transcript path is provided (Codex-friendly).
.map/scripts/map_orchestrator.py Template source counterpart updated in lockstep.
src/mapify_cli/templates/settings.json Registers the new UserPromptSubmit hook in templates.
.claude/settings.json Registers the new hook for this repo’s Claude settings.
docs/context-compression-plan.md Design/implementation plan for the feature.
docs/USAGE.md Documents new policy options and init flags.
README.md Quick-start mention of compression flags.
CHANGELOG.md Unreleased entry for the new feature.
src/mapify_cli/templates/hooks/workflow-gate.py Removes unused datetime import.
src/mapify_cli/templates/codex/hooks/workflow-gate.py Removes unused datetime import.
.claude/hooks/workflow-gate.py Removes unused datetime import.
.codex/hooks/workflow-gate.py Removes unused datetime import.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +106 to +107
Append `last-compact.marker` (UTC timestamp) write into the existing PreCompact
hook so that `context-meter.py` does not nudge again immediately after built-in
Comment on lines +177 to +184
# Cooldown marker for context-meter.py - prevents the meter from injecting
# a fresh /compact nudge immediately after Claude Code's built-in
# auto-compact (~83.5%) has just run. mtime is what the meter compares
# against, so the file content is informational only.
marker = branch_dir / "last-compact.marker"
try:
marker.write_text(datetime.now().isoformat(), encoding="utf-8")
except (IOError, OSError):
Comment on lines +177 to +184
# Cooldown marker for context-meter.py - prevents the meter from injecting
# a fresh /compact nudge immediately after Claude Code's built-in
# auto-compact (~83.5%) has just run. mtime is what the meter compares
# against, so the file content is informational only.
marker = branch_dir / "last-compact.marker"
try:
marker.write_text(datetime.now().isoformat(), encoding="utf-8")
except (IOError, OSError):
Comment on lines +165 to +186
# Post-load validation for enum-like fields. We do not raise — a bad
# value falls back to the default so a typo does not break the user's
# workflow.
valid_policies = ("never", "auto", "aggressive")
if cfg.compression_policy not in valid_policies:
logger.warning(
"Invalid compression_policy %r in %s (expected one of %s). "
"Using default 'auto'.",
cfg.compression_policy,
config_file,
", ".join(valid_policies),
)
cfg.compression_policy = "auto"
if cfg.compression_threshold_tokens <= 0:
logger.warning(
"compression_threshold_tokens must be > 0 in %s "
"(got %d). Using default 120000.",
config_file,
cfg.compression_threshold_tokens,
)
cfg.compression_threshold_tokens = 120_000

Comment on lines +30 to +36
# Valid policy values. Unknown policies are treated as ``auto`` (with a debug
# log in ``effective_threshold``) so misconfiguration does not silently
# disable the nudge — see ``effective_threshold`` and the matching test
# ``test_unknown_policy_treated_as_auto``.
VALID_POLICIES = ("never", "auto", "aggressive")


Comment thread tests/test_token_budget.py Outdated
Comment on lines +265 to +266
with pytest.raises(Exception): # noqa: PT011 — dataclasses.FrozenInstanceError
u.input_tokens = 5 # type: ignore[misc]
- pre-compact-save-transcript.py (.claude + template): write last-compact.marker as UTC RFC3339 instead of naive local-time isoformat — matches the design doc's "UTC timestamp" wording and removes timezone ambiguity for cross-machine debugging
- token_budget.VALID_POLICIES is now the single source of truth: project_config.load_map_config and mapify init both import it instead of redeclaring the tuple, so adding a future policy cannot drift across the three sites
- tests/test_token_budget.py: tighten test_is_frozen to assert dataclasses.FrozenInstanceError instead of the bare Exception (the noqa was masking a too-broad raises)
- tests/test_decomposition.py: add coverage for the new compression validation and apply_compression_overrides — invalid policy / non-positive threshold fall back to defaults, and the override helper handles commented placeholder, active entry, missing key, and missing-file cases
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a token-aware context compression policy to MAP so long-running sessions get a proactive /compact nudge before quality degrades, with shared logic across Claude Code hooks and Codex/orchestrator paths.

Changes:

  • Introduces mapify_cli.token_budget utilities for extracting last-turn input token usage and generating /compact instructions.
  • Extends .map/config.yaml (MapConfig) with compression policy/threshold/focus, plus CLI flags in mapify init to set them.
  • Adds a Claude Code UserPromptSubmit hook (context-meter.py), cooldown marker handling, and orchestrator stderr warnings for non-hook environments (e.g., Codex).

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/test_token_budget.py New unit tests covering token counting, threshold policy logic, and message formatting.
tests/test_decomposition.py Adds tests for config validation and apply_compression_overrides.
src/mapify_cli/token_budget.py New shared token budget/policy utilities used by hooks and orchestrator.
src/mapify_cli/templates/settings.json Registers the new context-meter hook in template settings.
src/mapify_cli/templates/map/scripts/map_orchestrator.py Adds transcript-path flag and stderr budget warning emission with cooldown.
src/mapify_cli/templates/hooks/workflow-gate.py Removes unused datetime import in template hook.
src/mapify_cli/templates/hooks/pre-compact-save-transcript.py Writes cooldown marker after compaction in template hook.
src/mapify_cli/templates/hooks/context-meter.py New template hook that injects /compact nudge via additionalContext.
src/mapify_cli/templates/codex/hooks/workflow-gate.py Removes unused datetime import in Codex template hook.
src/mapify_cli/config/project_config.py Adds compression fields, validation, default config entries, and override writer.
src/mapify_cli/init.py Adds mapify init flags and writes compression overrides into .map/config.yaml.
docs/context-compression-plan.md Adds a design/implementation plan doc for the feature.
docs/USAGE.md Documents compression policies, defaults, and how the nudge works.
README.md Mentions init-time compression flags in the quick start.
CHANGELOG.md Adds an Unreleased entry describing the new feature set.
.map/scripts/map_orchestrator.py Mirrors orchestrator changes in the synced .map/scripts version.
.codex/hooks/workflow-gate.py Removes unused datetime import in repo hook.
.claude/settings.json Registers the new context-meter hook in repo Claude settings.
.claude/hooks/workflow-gate.py Removes unused datetime import in repo hook.
.claude/hooks/pre-compact-save-transcript.py Writes cooldown marker after compaction in repo hook.
.claude/hooks/context-meter.py New repo hook that injects /compact nudge via additionalContext.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1879 to +1886
# Get branch
branch = args.branch if args.branch else get_branch_name()

# Provider-agnostic context-budget warning. No-op when no transcript is
# available (Codex without explicit --transcript-path, etc.) or when the
# mapify_cli package is not importable from this script's environment.
_emit_context_budget_warning(branch, args.transcript_path)

Comment on lines +1793 to +1799
# Same cooldown semantic as context-meter.py: skip if a compaction
# marker has been touched in the last 5 minutes.
marker = project_dir / ".map" / branch / "last-compact.marker"
if marker.is_file():
try:
if (time.time() - marker.stat().st_mtime) < 5 * 60:
return
Comment thread .map/scripts/map_orchestrator.py Outdated
Comment on lines +1879 to +1886
# Get branch
branch = args.branch if args.branch else get_branch_name()

# Provider-agnostic context-budget warning. No-op when no transcript is
# available (Codex without explicit --transcript-path, etc.) or when the
# mapify_cli package is not importable from this script's environment.
_emit_context_budget_warning(branch, args.transcript_path)

Comment on lines +1793 to +1799
# Same cooldown semantic as context-meter.py: skip if a compaction
# marker has been touched in the last 5 minutes.
marker = project_dir / ".map" / branch / "last-compact.marker"
if marker.is_file():
try:
if (time.time() - marker.stat().st_mtime) < 5 * 60:
return
Comment thread src/mapify_cli/__init__.py Outdated
Comment on lines +885 to +888
config_path = write_default_config(project_path)
apply_compression_overrides(
config_path, compression, compression_threshold
)
Comment on lines +849 to +858
from mapify_cli.config.project_config import (
apply_compression_overrides,
write_default_config,
)

config_path = write_default_config(project_path)
apply_compression_overrides(
config_path, compression, compression_threshold
)
tracker.complete(
Re-runnable mapify init no longer overwrites compression overrides:

- mapify init's --compression / --compression-threshold flags now default to None.
- apply_compression_overrides accepts None per parameter and is skipped when both are None, so a bare ``mapify init .`` re-run preserves whatever the user has already set in .map/config.yaml. CLI validation only runs when the user actually passed a flag.
- Tests cover: no-op when both flags absent, partial-policy-only, and partial-threshold-only.

--branch is now sanitized before it touches the filesystem:

- map_utils.sanitize_branch_name() is extracted as a public helper (and reused by get_branch_name). The orchestrator routes args.branch through it before building .map/<branch>/last-compact.marker so a malicious "../etc" branch arg can no longer escape the .map/ directory, and "feature/foo" no longer creates a different cooldown directory than the auto-detected "feature-foo".
- Both .map/scripts/ and src/mapify_cli/templates/map/scripts/ kept in sync.
- New tests/test_map_utils_sanitize.py loads the script via importlib and parametrises path-traversal cases.
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