Merge pull request 'fix: feat: agents flush context to scratch file before compaction (#262)' (#430) from fix/issue-262 into main

This commit is contained in:
johba 2026-03-20 22:33:50 +01:00
commit e4d5058172
7 changed files with 109 additions and 15 deletions

View file

@ -24,6 +24,7 @@ export PROJECT_TOML="${2:-${PROJECT_TOML:-}}"
source "$(dirname "$0")/../lib/env.sh"
source "$(dirname "$0")/../lib/agent-session.sh"
source "$(dirname "$0")/../lib/formula-session.sh"
# shellcheck source=../dev/phase-handler.sh
source "$(dirname "$0")/../dev/phase-handler.sh"
SESSION_NAME="action-${ISSUE}"
@ -41,6 +42,7 @@ WORKTREE="${PROJECT_REPO_ROOT}"
PHASE_FILE="/tmp/action-session-${PROJECT_NAME:-harb}-${ISSUE}.phase"
IMPL_SUMMARY_FILE="/tmp/action-impl-summary-${PROJECT_NAME:-harb}-${ISSUE}.txt"
PREFLIGHT_RESULT="/tmp/action-preflight-${ISSUE}.json"
SCRATCH_FILE="/tmp/action-${ISSUE}-scratch.md"
log() {
printf '[%s] action#%s %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" >> "$LOGFILE"
@ -166,6 +168,10 @@ if [ -n "${_thread_id:-}" ]; then
>> "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true
fi
# --- Read scratch file (compaction survival) ---
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# --- Build initial prompt ---
PRIOR_SECTION=""
if [ -n "$PRIOR_COMMENTS" ]; then
@ -193,7 +199,7 @@ in the issue below.
## Issue #${ISSUE}: ${ISSUE_TITLE}
${ISSUE_BODY}
${SCRATCH_CONTEXT}
${PRIOR_SECTION}## Instructions
1. Read the action formula steps in the issue body carefully.
@ -235,6 +241,8 @@ ${PRIOR_SECTION}## Instructions
If the prior comments above show work already completed, resume from where it
left off.
${SCRATCH_INSTRUCTION}
${PHASE_PROTOCOL_INSTRUCTIONS}"
# --- Create tmux session ---
@ -264,16 +272,16 @@ case "${_MONITOR_LOOP_EXIT:-}" in
# Escalate to supervisor (idle_prompt already escalated via _on_phase_change callback)
echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER:-0},\"reason\":\"idle_timeout\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
>> "${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.jsonl"
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE"
;;
idle_prompt)
# Notification + escalation already handled by _on_phase_change(PHASE:failed) callback
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE"
;;
done)
# Belt-and-suspenders: callback handles primary cleanup,
# but ensure sentinel files are removed if callback was interrupted
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE"
;;
esac

View file

@ -23,6 +23,7 @@ set -euo pipefail
# Load shared environment
source "$(dirname "$0")/../lib/env.sh"
source "$(dirname "$0")/../lib/agent-session.sh"
source "$(dirname "$0")/../lib/formula-session.sh"
# shellcheck source=./phase-handler.sh
source "$(dirname "$0")/phase-handler.sh"
@ -88,6 +89,9 @@ IMPL_SUMMARY_FILE="/tmp/dev-impl-summary-${PROJECT_NAME}-${ISSUE}.txt"
# Matrix thread tracking — one thread per issue for conversational notifications
THREAD_FILE="/tmp/dev-thread-${PROJECT_NAME}-${ISSUE}"
# Scratch file for context compaction survival
SCRATCH_FILE="/tmp/dev-${PROJECT_NAME}-${ISSUE}-scratch.md"
# Timing
export PHASE_POLL_INTERVAL=30 # seconds between phase checks (read by agent-session.sh)
IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long
@ -493,6 +497,12 @@ else
done
fi
# =============================================================================
# READ SCRATCH FILE (compaction survival)
# =============================================================================
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# =============================================================================
# BUILD PROMPT
# =============================================================================
@ -593,7 +603,7 @@ This is issue #${ISSUE} for the ${CODEBERG_REPO} project.
## Issue: ${ISSUE_TITLE}
${ISSUE_BODY}
${SCRATCH_CONTEXT}
## CRASH RECOVERY
Your previous session for this issue was interrupted. Resume from where you left off.
@ -617,6 +627,8 @@ $(if [ -n "$CI_RESULT" ]; then printf '\n### Last CI result:\n%s\n' "$CI_RESULT"
2. Resume from the last known phase.
3. Follow the phase protocol below.
${SCRATCH_INSTRUCTION}
${PHASE_PROTOCOL_INSTRUCTIONS}"
else
# Normal mode: initial implementation prompt
@ -626,7 +638,7 @@ You have been assigned issue #${ISSUE} for the ${CODEBERG_REPO} project.
## Issue: ${ISSUE_TITLE}
${ISSUE_BODY}
${SCRATCH_CONTEXT}
## Other open issues labeled 'backlog' (for context if you need to suggest alternatives):
${OPEN_ISSUES_SUMMARY}
@ -678,6 +690,8 @@ printf 'PHASE:failed\nReason: refused\n' > \"${PHASE_FILE}\"
**Do NOT invent dependencies that aren't real.** If the code compiles and tests pass, that's ready.
${SCRATCH_INSTRUCTION}
${PHASE_PROTOCOL_INSTRUCTIONS}"
fi
@ -745,7 +759,7 @@ case "${_MONITOR_LOOP_EXIT:-}" in
else
cleanup_worktree
fi
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" \
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" \
"/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt"
[ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}"
;;
@ -755,7 +769,7 @@ case "${_MONITOR_LOOP_EXIT:-}" in
done)
# Belt-and-suspenders: callback in phase-handler.sh handles primary cleanup,
# but ensure sentinel files are removed if callback was interrupted
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" \
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" \
"/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt"
[ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}"
CLAIMED=false

View file

@ -525,7 +525,7 @@ Instructions:
cleanup_labels
agent_kill_session "$SESSION_NAME"
cleanup_worktree
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}"
exit 0
else
log "PR #${PR_NUMBER} was closed WITHOUT merge — NOT closing issue"
@ -580,7 +580,7 @@ Instructions:
# Local cleanup
agent_kill_session "$SESSION_NAME"
cleanup_worktree
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" \
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \
"/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt"
[ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}"
CLAIMED=false # Don't unclaim again in cleanup()
@ -679,7 +679,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
CLAIMED=false # Don't unclaim again in cleanup()
agent_kill_session "$SESSION_NAME"
cleanup_worktree
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" \
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \
"/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt"
[ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}"
return 1
@ -708,7 +708,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
else
cleanup_worktree
fi
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" \
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \
"/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt"
[ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}"
return 1

View file

@ -37,6 +37,7 @@ SESSION_NAME="gardener-${PROJECT_NAME}"
PHASE_FILE="/tmp/gardener-session-${PROJECT_NAME}.phase"
RESULT_FILE="/tmp/gardener-result-${PROJECT_NAME}.txt"
DUST_FILE="$SCRIPT_DIR/dust.jsonl"
SCRATCH_FILE="/tmp/gardener-${PROJECT_NAME}-scratch.md"
# shellcheck disable=SC2034 # read by monitor_phase_loop in lib/agent-session.sh
PHASE_POLL_INTERVAL=15
@ -221,13 +222,18 @@ If a choice is unclear, re-escalate that single item with a clarifying question.
${ESCALATION_REPLY}"
fi
# ── Read scratch file (compaction survival) ───────────────────────────────
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# ── Build prompt from formula + dynamic context ────────────────────────────
log "Building gardener prompt from formula"
PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling.
${CONTEXT_SECTION}
## Formula
${SCRATCH_CONTEXT:+${SCRATCH_CONTEXT}
}## Formula
${FORMULA_CONTENT}
## Runtime context (bash pre-analysis)
@ -253,6 +259,8 @@ NEVER echo or include the actual token value in output — always reference \$CO
printf 'ESCALATE\n1. #NNN \"title\" — reason (a) option1 (b) option2\n' >> '${RESULT_FILE}'
echo 'CLEAN' >> '${RESULT_FILE}' # only if truly nothing to do
${SCRATCH_INSTRUCTION}
## Phase protocol (REQUIRED)
When all work is done and verify confirms zero tech-debt:
echo 'PHASE:done' > '${PHASE_FILE}'
@ -462,4 +470,9 @@ Fix all items above in a single PR. Each is a small change (rename, comment, sty
done <<< "$DUST_GROUPS"
fi
# ── Cleanup scratch file on normal exit ──────────────────────────────────
if [ "$FINAL_PHASE" = "PHASE:done" ]; then
rm -f "$SCRATCH_FILE"
fi
log "--- gardener-agent done ---"

View file

@ -126,6 +126,37 @@ formula_phase_callback() {
esac
}
# ── Scratch file helpers (compaction survival) ────────────────────────────
# build_scratch_instruction SCRATCH_FILE
# Returns a prompt block instructing Claude to periodically flush context
# to a scratch file so understanding survives context compaction.
build_scratch_instruction() {
local scratch_file="$1"
cat <<_SCRATCH_EOF_
## Context scratch file (compaction survival)
Periodically (every 10-15 tool calls), write a summary of:
- What you have discovered so far
- Decisions made and why
- What remains to do
to: ${scratch_file}
If this file existed at session start, its contents have already been injected into your prompt above.
This file is ephemeral — not evidence or permanent memory, just a compaction survival mechanism.
_SCRATCH_EOF_
}
# read_scratch_context SCRATCH_FILE
# If the scratch file exists, returns a context block for prompt injection.
# Returns empty string if the file does not exist.
read_scratch_context() {
local scratch_file="$1"
if [ -f "$scratch_file" ]; then
printf '## Previous context (from scratch file)\n%s\n' "$(head -c 8192 "$scratch_file")"
fi
}
# ── Prompt + monitor helpers ──────────────────────────────────────────────
# build_prompt_footer [EXTRA_API_LINES]

View file

@ -31,6 +31,8 @@ PHASE_FILE="/tmp/planner-session-${PROJECT_NAME}.phase"
# shellcheck disable=SC2034 # read by monitor_phase_loop in lib/agent-session.sh
PHASE_POLL_INTERVAL=15
SCRATCH_FILE="/tmp/planner-${PROJECT_NAME}-scratch.md"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
# ── Guards ────────────────────────────────────────────────────────────────
@ -53,6 +55,10 @@ $(cat "$MEMORY_FILE")
"
fi
# ── Read scratch file (compaction survival) ───────────────────────────────
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# ── Build prompt ─────────────────────────────────────────────────────────
build_prompt_footer "
Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
@ -65,12 +71,21 @@ PROMPT="You are the strategic planner for ${CODEBERG_REPO}. Work through the for
## Project context
${CONTEXT_BLOCK}${MEMORY_BLOCK}
${SCRATCH_CONTEXT:+${SCRATCH_CONTEXT}
}
## Formula
${FORMULA_CONTENT}
${SCRATCH_INSTRUCTION}
${PROMPT_FOOTER}"
# ── Run session ──────────────────────────────────────────────────────────
export CLAUDE_MODEL="opus"
run_formula_and_monitor "planner"
# ── Cleanup scratch file on normal exit ──────────────────────────────────
# FINAL_PHASE already set by run_formula_and_monitor
if [ "${FINAL_PHASE:-}" = "PHASE:done" ]; then
rm -f "$SCRATCH_FILE"
fi

View file

@ -33,6 +33,8 @@ PHASE_FILE="/tmp/predictor-session-${PROJECT_NAME}.phase"
# shellcheck disable=SC2034 # read by monitor_phase_loop in lib/agent-session.sh
PHASE_POLL_INTERVAL=15
SCRATCH_FILE="/tmp/predictor-${PROJECT_NAME}-scratch.md"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; }
# ── Guards ────────────────────────────────────────────────────────────────
@ -45,6 +47,10 @@ log "--- Predictor run start ---"
load_formula "$FACTORY_ROOT/formulas/run-predictor.toml"
build_context_block AGENTS.md RESOURCES.md
# ── Read scratch file (compaction survival) ───────────────────────────────
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
# ── Build prompt ─────────────────────────────────────────────────────────
build_prompt_footer
@ -58,12 +64,19 @@ about CI health, issue staleness, agent status, and system conditions.
## Project context
${CONTEXT_BLOCK}
${SCRATCH_CONTEXT}
## Formula
${FORMULA_CONTENT}
${SCRATCH_INSTRUCTION}
${PROMPT_FOOTER}"
# ── Run session ──────────────────────────────────────────────────────────
export CLAUDE_MODEL="sonnet"
run_formula_and_monitor "predictor"
# ── Cleanup scratch file on normal exit ──────────────────────────────────
# FINAL_PHASE already set by run_formula_and_monitor
if [ "${FINAL_PHASE:-}" = "PHASE:done" ]; then
rm -f "$SCRATCH_FILE"
fi