Skip to content

feat: inject periodic mid-loop reminder every 5 turns to counter instruction drift#211

Merged
zjshen14 merged 2 commits into
mainfrom
feat/issue-19-periodic-reminders
Jun 22, 2026
Merged

feat: inject periodic mid-loop reminder every 5 turns to counter instruction drift#211
zjshen14 merged 2 commits into
mainfrom
feat/issue-19-periodic-reminders

Conversation

@zjshen14

@zjshen14 zjshen14 commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds buildPeriodicReminder(turn) to src/core/prompt.ts that returns a compact [reminder: ...] block every PERIODIC_REMINDER_INTERVAL (5) turns, and empty string otherwise
  • Wires it into applyReminderToLastResult in src/core/agent.ts alongside the existing event-driven reminders
  • Adds 3 unit tests in prompt.test.ts and 3 integration tests in agent.test.ts covering: fires at multiples of the interval, silent before the interval, fires again at the second multiple

Related issue

Part of #19 — item 5: "Mid-loop system-reminder injection"

Why

Event-driven reminders (AGENT_REMINDERS) fire at most once per run and only when specific tools are called (writes, git). In long sessions the model drifts from git rules, edit discipline, and testing requirements. A periodic reminder re-anchors it cheaply (~10 tokens every 5 turns) without disrupting short sessions where the interval is never reached.

Test plan

  • npm run typecheck && npm run lint && npm run format:check && npm test — all pass (654 tests, 0 failures)
  • New buildPeriodicReminder unit tests: fires at turn 5, 10; silent at turns 1–4; silent at turn 0
  • Agent integration tests: reminder appears in the tool_result event at exactly turn 5 and turn 10; absent before

🤖 Generated with Claude Code


Generated by Claude Code

@zjshen14

zjshen14 commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Review: feat: inject periodic mid-loop reminder every 5 turns

What's good: The implementation is clean and well-contained. buildPeriodicReminder is a pure function, correctly placed in prompt.ts, and wired into the existing applyReminderToLastResult path with minimal structural disruption. Tests are thorough: unit tests cover turns 0, 1–4, 5, and 10; integration tests verify the tool_result event carries the reminder at exactly the right turns and not before. Exporting PERIODIC_REMINDER_INTERVAL so tests don't hardcode 5 is the right call. CI is green, no conflicts.


Concerns

1. Wording diverges from AGENT_REMINDERS, creating overlapping-but-inconsistent instructions

PERIODIC_REMINDER_TEXT paraphrases the event-driven reminders rather than reusing their exact text:

AGENT_REMINDERS PERIODIC_REMINDER_TEXT
"never commit or push without an explicit user request" "commit only when explicitly asked"
"verify the change works — find and run the project's test command before reporting done" "run tests after changes"

On any turn where a write call first occurs and it's a multiple-of-5 (e.g. the very first write happens at turn 5), both sets fire in the same tool result and the model receives two slightly different phrasings of the same rule back-to-back. Using the exact text from AGENT_REMINDERS, or deriving PERIODIC_REMINDER_TEXT from that array, would keep the two reminder channels consistent and avoid the potential for the model to treat them as distinct constraints.

2. No opt-out path for custom system prompts

Operators overriding the default instructions via OPENCLI_SYSTEM_MD have no way to suppress or reconfigure the periodic reminder — it fires unconditionally regardless of what the custom prompt says. If the custom prompt already has its own reminder cadence, or deliberately omits certain rules, the hardcoded PERIODIC_REMINDER_TEXT will contradict or duplicate it silently. A PERIODIC_REMINDER_INTERVAL=0 env var (alongside the existing OPENCLI_* family) or a nullable export would make this composable without adding complexity to the hot path.


Recommendation

Merge after addressing concern #1 (wording consistency) or adding a comment explaining why the divergent phrasing is intentional. Concern #2 is lower priority — acceptable to track as a follow-up if the feature ships first.


Generated by Claude Code

Long sessions cause models to forget earlier constraints (git rules,
edit discipline, test requirements). The event-driven reminders in
AGENT_REMINDERS fire at most once per run; they don't re-anchor the
model after many turns. This adds a lightweight periodic reminder
that appends a short [reminder: ...] block to the last tool result
every PERIODIC_REMINDER_INTERVAL (5) turns — cheap (~10 tokens) and
effective at preventing drift without disrupting short sessions.

Part of #19
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zjshen14 zjshen14 force-pushed the feat/issue-19-periodic-reminders branch from 5ac91fe to cf92fdc Compare June 19, 2026 15:10
@zjshen14

Copy link
Copy Markdown
Owner Author

Rebased onto current main (commit 706dceb) to resolve merge conflicts. The rebase was non-trivial: since PR #211 was created, the agentic loop in agent.ts was refactored — the applyReminderToLastResult private method was removed and its logic was inlined back into run(). The conflict resolution:

  • Applied the PR's intended change to the inline reminder code (renamed remindereventReminder, added periodicReminder = buildPeriodicReminder(turns), combined them)
  • Kept HEAD's monolithic run() structure unchanged (did not re-introduce the extracted private methods, which were from the PR's old base)
  • Kept both test suites that were added at the same location (Agent activate_skill — conversation well-formedness from HEAD + Agent periodic reminder injection from this PR)

All 716 tests pass (6 new from this PR). Branch is now conflict-free and ready for review.


Generated by Claude Code

@zjshen14

Copy link
Copy Markdown
Owner Author

Re-review: feat: inject periodic mid-loop reminder every 5 turns (post-rebase, SHA cf92fdc)

This is a re-review following the rebase onto main @ 706dceb. CI is all green (Typecheck+Lint+Test, CodeQL, Analyze) and mergeable_state is clean.

Rebase adaptation

The rebase correctly handles the applyReminderToLastResult refactor that landed on main between the original PR and today: the extracted private method was re-inlined, and the PR's periodicReminder is now combined with eventReminder before the single if (combined && results.length > 0) guard. The logic is equivalent to the original intent and there are no mechanical issues.

buildPeriodicReminder(turns) is called after turns++, so it fires at turns 5, 10, 15, … as the tests verify. No change there.


Prior concerns — still unaddressed

Both concerns from the June 4 review remain open in this rebased state:

1. Wording diverges from AGENT_REMINDERS (prompt.ts:73–82)

AGENT_REMINDERS says:

  • "never commit or push without an explicit user request"
  • "verify the change works — find and run the project's test command before reporting done"

PERIODIC_REMINDER_TEXT says:

  • "commit only when explicitly asked" — omits "or push"; weaker phrasing
  • "run tests after changes" — significantly vaguer than the event-driven version
  • "prefer targeted edits over full rewrites" — new constraint not present in AGENT_REMINDERS at all

The two channels now teach the model three slightly different versions of the same rules. Deriving PERIODIC_REMINDER_TEXT from AGENT_REMINDERS (e.g. AGENT_REMINDERS.map(r => r.text).join('; ')) would keep them in sync mechanically and guarantee they can't diverge again. Alternatively, a short comment explaining why the shorter/different phrasing is intentional (e.g. "compact form for token budget") resolves the concern without a code change.

2. No opt-out path for OPENCLI_SYSTEM_MD overrides

Operators using a custom system prompt via OPENCLI_SYSTEM_MD have no way to suppress or reconfigure the periodic reminder. Still lower priority — still acceptable as a follow-up.


Recommendation

Merge after addressing concern #1 — either align the PERIODIC_REMINDER_TEXT wording with AGENT_REMINDERS, or add a brief inline comment explaining why the intentional divergence is correct. Concern #2 remains a follow-up item.


Generated by Claude Code

The periodic reminder text diverged from AGENT_REMINDERS, teaching the
model slightly different versions of the same rules across two channels
(e.g. "commit only when explicitly asked" vs "never commit or push
without an explicit user request"). Derive PERIODIC_REMINDER_TEXT from
AGENT_REMINDERS so both channels use identical wording and can't drift
apart as reminders are added or changed.

Also moves AGENT_REMINDERS before the periodic reminder block to reflect
the new dependency order.

Part of #19
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zjshen14

Copy link
Copy Markdown
Owner Author

Addressed concern #1 from the re-review (commit 35f9e7c).

What changed:

  • PERIODIC_REMINDER_TEXT is now derived from AGENT_REMINDERS via AGENT_REMINDERS.map((r) => r.text).join("; ") instead of a hand-written string
  • AGENT_REMINDERS was moved before the periodic reminder block to reflect the dependency order
  • Tests in prompt.test.ts and agent.test.ts updated to assert on the actual AGENT_REMINDERS wording

This means both reminder channels now use identical phrasing and can't drift apart as entries are added or changed in the future.

Concern #2 (no opt-out path for OPENCLI_SYSTEM_MD) is unchanged — still a lower-priority follow-up as noted in the review.


Generated by Claude Code

@zjshen14

Copy link
Copy Markdown
Owner Author

Re-review following the wording fix in commit 35f9e7c. PERIODIC_REMINDER_TEXT is now derived from AGENT_REMINDERS.map((r) => r.text).join("; ") — both reminder channels share the same text and can't drift apart as entries are added. The tests correctly assert on the actual AGENT_REMINDERS wording (checking for "never commit or push without an explicit user request"). CI is all green, mergeable_state is clean. Both prior concerns from the June 4 and June 19 reviews are fully resolved. Merging.


Generated by Claude Code

@zjshen14 zjshen14 merged commit e33c39d into main Jun 22, 2026
3 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.

2 participants