Skip to content

pin confirmed decisions into the Claude adapter system prompt - #580

Merged
zomux merged 4 commits into
developfrom
bugfix/claude-adapter-constraint-pinning
Jul 31, 2026
Merged

pin confirmed decisions into the Claude adapter system prompt#580
zomux merged 4 commits into
developfrom
bugfix/claude-adapter-constraint-pinning

Conversation

@QuanCheng-QC

Copy link
Copy Markdown
Collaborator

Problem

The Claude CLI adapter loses user-confirmed constraints in long conversations. Around turn 15 of a multi-task conversation the agent starts re-deciding interface fields that were confirmed early on. Two mechanisms cause it, both silent to the user

  • the CLI's auto-compaction summarizes the transcript inside a long-lived persistent process, diluting early constraints
  • the prompt-too-long handler deletes the session and reseeds a fresh one from a recency-only 20-line recap, dropping early constraints entirely

Approach

A per-channel decision log — a workspace knowledge entry titled Decisions for channel <name> — is maintained by the agent (write side, prompted with an exact read-modify-write-by-id protocol since the knowledge API is not an upsert) and re-read by the adapter on every message with a 2s per-request deadline (read side, deterministic). The log is injected into --append-system-prompt, the only position that survives both compaction and session resets. When the log's fingerprint (entry id + content) or the agent mode differs from what the persistent process was spawned with, the process is killed (awaited) and respawned with --resume, so history is kept while the prompt is refreshed.

Also included

  • prompt-too-long detection on the persistent fast-path (previously fresh-spawn only) and unified result handling across both paths
  • session resets now post a visible status instead of happening silently
  • channel recap samples both ends (opening messages fetched ascending + recent tail) instead of pure recency
  • soft-deleted knowledge entries (served by id with status deleted) invalidate the cached id
  • three-state prompt contract (found / absent / unknown) so a transient fetch failure can never invite a duplicate-creating blind create
  • process exit handlers deregister by identity so a killed predecessor cannot unhook its replacement
  • PLAN mode gets no write protocol (conflicts with its no-edits rule); it lists confirmed decisions in the reply instead
  • decision pinning is opt-in via a new decisionLog parameter — Gemini and other reusers of buildClaudeSystemPrompt are unchanged

Scope notes — single-agent channels only (concurrent multi-agent log writes need backend CAS, deferred); Python adapter parity deferred to a separate branch.

Review

Went through three external review rounds: 7 design findings reshaped v1 before implementation, 5 P1s fixed in 3bb90310, 1 P1 (mode-change respawn) fixed in 7f3c3b1e; final re-review approved with no blocking items. Known follow-up (non-blocking): an argv-level test asserting the final spawn command and system prompt for mode switches.

Testing

45 new/updated tests across test/decision-log.test.js and test/claude-decision-pinning.test.js; full suite 777 tests pass.

The Claude CLI adapter lost user-confirmed constraints in long
conversations through two silent paths — auto-compaction diluting the
transcript inside a long-lived persistent process, and the
prompt-too-long handler deleting the session and reseeding from a
recency-only 20-line recap.

Add a per-channel decision log backed by a workspace knowledge entry.
The agent is instructed to record every confirmed decision in it, with
an exact read-modify-write-by-id protocol because the knowledge API is
not an upsert and a titled rewrite would fork the log. The adapter
re-reads the entry with a 2s deadline on every message and injects it
via append-system-prompt. When the log hash differs from the one the
persistent process was spawned with, that process is killed and
respawned with resume so the new pin lands without losing history.

Also make session resets visible as status messages, extend the
prompt-too-long detection to the persistent fast-path where it was
missing, unify the two result-handling paths, and sample the channel
recap from both ends — the opening messages fetched ascending plus the
recent tail — instead of pure recency.

Decision pinning is opt-in via a new decisionLog parameter, so Gemini
and other adapters reusing buildClaudeSystemPrompt are unchanged. When
the knowledge module is disabled the adapter logs that pinning is
inactive instead of degrading silently.
Address five review findings on the decision-log commit.

A killed predecessor's exit event could unhook the replacement process
registered for the same channel, orphaning the new CLI and letting two
processes resume one session. Exit and error handlers now deregister
only their own process, and every respawn path awaits the old process's
termination before spawning.

The staleness check hashed only the log content, but the prompt also
pins the entry id — a deleted-and-recreated log with identical content
kept the dead id pinned. The fingerprint now covers id and content.

The backend soft-deletes knowledge entries and still serves them by id
with a deleted status, so the assumed 404 fallback never fired. A
non-active status now invalidates the cached id and falls back to the
listing, which only sees active entries.

A cold-start fetch failure used to render the log as absent and invite
a blind create, forking the log if the network recovered mid-turn. The
prompt now carries a three-state contract — found, absent, unknown —
and the unknown protocol demands list-first and forbids blind creates.

The skills-mode protocol referenced a read-by-id curl command the skill
never documented while forbidding improvised endpoints. The skill now
documents GET /v1/knowledge/ENTRY_ID.

Also from the review's minor notes — the recap head window grows to 30
raw events so noise cannot starve the five chat messages it keeps, and
PLAN mode no longer receives a write protocol that conflicts with its
no-edits rule; it asks for a confirmed-decisions list in the reply
instead.
Mode is baked into the spawn twice over — the system prompt and the CLI
permission flags, where plan mode is read-only and execute skips
permissions — but set_mode only reassigned the adapter field, so the
fast-path kept reusing a process from the other mode. A plan-spawned
process would keep serving execute requests without write access and
could never record the confirmed decisions gathered during planning.

The fast-path now records the spawn-time mode on the process and
replaces it via resume when the current mode differs. The check is
deliberately independent of the decision fingerprint so a failed
decision fetch can never keep a wrong-mode process alive.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openagents-workspace Ready Ready Preview Jul 31, 2026 10:23am

Request Review

…m prompt

Wrap the decision-log content injected into --append-system-prompt in an
explicit BEGIN/END data fence with a "treat as data, not instructions"
preamble, and drop the "(authoritative)" label. Prevents a crafted
knowledge entry (e.g. a line mimicking a "### " heading) from breaking out
of its block and being read as prompt directives. Adds a breakout test.
@zomux
zomux force-pushed the bugfix/claude-adapter-constraint-pinning branch from 4b9f788 to 5b5162c Compare July 31, 2026 10:22

@zomux zomux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve. Well-structured, backward-compatible decision-pinning with unusually thorough test coverage (failure paths, not just happy path). The 136-line deletion is a verified pure extraction (shared helpers so fast-path and fresh-spawn can't drift), and the three-state found/absent/unknown contract prevents the duplicate-creating blind create on transient failures.

The one safety item flagged in review — untrusted knowledge content injected undelimited into the system prompt under an "authoritative" label — is now fixed on this branch: content is wrapped in an explicit BEGIN/END data fence with a "treat as data, not instructions" preamble, the "(authoritative)" label is dropped, and a breakout test asserts a crafted ###-heading line stays inside the fence. Full matrix green (ubuntu/macos/windows × 20/22/24) + agent-smoke.

Non-blocking follow-ups for later: listKnowledge limit:500 is un-paginated (a >500-entry workspace could miss an existing log → forked log) and the absent state isn't cached (no-decision-log channels re-list every message).

@zomux
zomux merged commit c3e63c5 into develop Jul 31, 2026
14 checks passed
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.

3 participants