Skip to content

fix(coding-agent): keep git state out of the cached system prompt - #354

Merged
ZouR-Ma merged 4 commits into
mainfrom
fix/git-runtime-context
Nov 9, 2026
Merged

ZouR-Ma merged 4 commits into
mainfrom
fix/git-runtime-context

Conversation

@ZouR-Ma

@ZouR-Ma ZouR-Ma commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The Step system prompt's <env> block carried Git branch and Uncommitted changes, read from git whenever the prompt was rebuilt. The prompt is not rebuilt per user message; it is rebuilt when tools or resources change (an MCP catalog refresh or restart, an extension registering tools, /reload). After a checkout, such an unrelated rebuild re-read git and rewrote the system prompt. Anthropic-style prompt caching matches the request prefix (tools, then system, then history), so that one rewrite turned the whole conversation into cache writes. Between rebuilds the model saw a stale branch, and a run that checked out a branch mid-run (the agent itself running git checkout) never saw the change.

Reproduced with the real CLI in RPC mode against a recording Anthropic endpoint: switching branches changed nothing until an MCP server restart with an identical tool catalog, which then changed the system prompt hash while the tool hash stayed the same.

Changes

  • src/step/system-prompt.ts: drop the git lines, the synchronous git reads and their 5 s cache from <env>. encodeEnvironmentValue is exported for reuse.
  • src/features/step-runtime-context.ts (new), registered in step-capabilities.ts:
    • On before_agent_start, read the branch with one git symbolic-ref --quiet --short HEAD (exit 0 branch, 1 detached HEAD, 128 not a repository; anything else, such as a missing binary or timeout, announces nothing). Persist a hidden step-runtime-context custom message beside the prompt only when the branch differs from the newest snapshot in the active context. The snapshot says it supersedes earlier ones, so history stays append-only.
    • Mark the branch stale after a step that ran tools, and for runs started without before_agent_start (goal continuation, cron, background subagent wake-ups, approved plans, retries). The context hook then re-reads git once and, when the request does not already show the current branch, appends the snapshot at the request tail without writing history. The same hook restores a snapshot that compaction summarized away. The next user prompt persists the new branch.
  • src/index.ts: export createStepRuntimeContextExtension for SDK embedders that use buildStepSystemPromptAppendix without the capabilities extension.
  • CHANGELOG.md: entry under Unreleased → Changed.

Behavior and compatibility

  • The system prompt no longer contains Git branch or Uncommitted changes, so it and the tool block stay byte-identical across checkouts and unrelated rebuilds.
  • The model sees the branch as a user-role <system-reminder> snapshot instead. A detached HEAD is stated as such, and leaving a repository after a branch was announced produces a "not inside a git repository" snapshot. Outside a repository, nothing is announced.
  • The uncommitted-file count is removed, not moved: it changes with almost every edit the agent makes, which would append a snapshot nearly every turn, and the prompt already instructs the model to inspect git state before changing files.
  • Existing sessions are unaffected; older sessions simply have no snapshot until the next prompt.
  • SDK hosts that compose only buildStepSystemPromptAppendix lose the branch line unless they add createStepRuntimeContextExtension.

Validation

Commits, each passing the pre-commit pnpm run check:

  1. fix(coding-agent): keep git state out of the cached system prompt: prompt change, per-prompt snapshot, compaction restore, tests.
  2. fix(coding-agent): refresh the git branch within a run: mid-run and promptless-run refresh, tests.
  3. feat(coding-agent): export the runtime-context extension
  4. docs(changelog): record the git runtime-context change
  • CI build-check-test and Check commit attribution: passed.
  • Focused tests from packages/coding-agent at commit 1 (34/34) and at the tip (40/40): pnpm exec vitest --run test/step-runtime-context.test.ts test/suite/regressions/git-runtime-context-cache-prefix.test.ts test/step-system-prompt.test.ts test/step-capabilities-extension.test.ts test/suite/regressions/plan-mode-prompt-cache-prefix.test.ts.
  • An earlier CI run on this branch failed in test/json-mode-durability.test.ts with exit 143 (its 60 s spawnSync cap killed the CLI child). main run 42374504621 failed the same way in that file without this change. Locally the file passes, about 6 s per case even with every core saturated, and a network trace of the same child shows only local IPC connections, so I did not change that test here.
  • test/suite/regressions/git-runtime-context-cache-prefix.test.ts drives a real session with real git:
    • a checkout between prompts plus a forced prompt rebuild keeps the system prompt and tools byte-identical, history append-only, and appends one new snapshot. It fails against the previous system-prompt.ts (not to contain 'Git branch:').
    • a tool that runs git checkout inside one run shows Git branch: dev at the tail of the next request with an unchanged prefix. It fails against commit 1's extension.
  • ./test.sh (macOS):
    • With the default $TMPDIR, test/subagent-invocation.test.ts failed with listen EINVAL: tsx's IPC socket path under the isolated temp root is 113 characters, over the macOS socket path limit. The file passes (4/4) when run directly.
    • With TMPDIR=/tmp ./test.sh: scripts, config, contracts, telemetry, lark-adapter, providers (484 passed), agent-core (560 passed), coding-agent (5342 passed) and tui passed. apps/cli had 1 failure in test/edit-tool-no-full-redraw.test.ts ("keeps the applied change on a Step card whose call failed after writing"), a render-timing assertion in the edit tool card. The file passed 5/5 in three consecutive direct runs; it does not touch the system prompt or this extension.

Limitations

  • Within a run, a new branch is shown at the request tail and persisted only on the next user prompt; a run started without a user prompt never persists one.
  • A branch changed outside the session (another terminal) between steps that ran no tools is noticed at the next user prompt or the next step that runs tools.
  • Each user prompt pays one git symbolic-ref call (bounded by a 1.5 s timeout).
  • Today's date remains in <env>; crossing midnight still rewrites the system prompt on the next rebuild, at most once a day.

The Step system prompt's <env> block carried the git branch and the
uncommitted-file count. Any prompt rebuild after a checkout (an MCP catalog
refresh, a tool registration) re-read git and rewrote the system prompt,
which the provider caches right after the tool block, so the whole
conversation was re-billed as cache writes. Between rebuilds the model saw a
stale branch.

The branch now rides a runtime-context snapshot: a hidden custom message
appended beside the user's prompt only when the branch differs from the last
snapshot in the active context, so history stays append-only and the cached
prefix survives a checkout. The context hook restores a snapshot that
compaction summarized away, at the request tail only. The uncommitted-file
count is dropped: it changes with almost every edit the agent makes, and the
prompt already tells the model to inspect git state.
The runtime-context snapshot was read only on before_agent_start, so it
covered user prompts alone. A run that checked out a branch through a tool,
and runs started without a user prompt (goal continuation, cron, background
subagent wake-ups, an approved plan, retries), kept showing the branch from
the last user prompt.

Mark the branch stale after a step that ran tools, and on agent_start when
before_agent_start did not run. The context hook then re-reads git once and,
when the request does not already show the current branch, appends the
snapshot at the request tail without writing history; the next user prompt
persists it. A user prompt still reads git once for its first request.
SDK hosts that compose buildStepSystemPromptAppendix without
createStepCapabilitiesExtensionInline lost the git branch when it left the
system prompt. Export createStepRuntimeContextExtension and its options so
they can add it directly.
@ZouR-Ma
ZouR-Ma force-pushed the fix/git-runtime-context branch from 4c0adb6 to 30727e3 Compare November 9, 2026 13:28
@ZouR-Ma
ZouR-Ma merged commit 372f1f6 into main Nov 9, 2026
2 checks passed
@ZouR-Ma
ZouR-Ma deleted the fix/git-runtime-context branch November 9, 2026 13:40
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