에이전트 메모리와 드리밍(dreaming)
AI 에이전트가 세션을 넘어 배우고, 잠든 사이에 스스로 기억을 다듬게 하는 컨텍스트 엔지니어링 이야기다.
목차 (6)
먼저 짚을 점: “밤새 학습한다”는 말은 모델의 가중치(weights)가 바뀐다는 뜻이 아니다. 에이전트가 남긴 기록을 읽고 정리해 다음 세션이 읽을 메모리(문서)를 고치는 것이다. 모델은 그대로이고, 모델에게 건네는 컨텍스트가 좋아진다.
왜 컨텍스트인가
모델이 똑똑해진다고 실제로 배포된 에이전트가 똑똑해지지는 않는다. 에이전트에게는 조직·코드베이스·사용자 선호처럼 그곳에서만 통하는 지식이 필요하고, 발표자는 이 컨텍스트가 모델 지능을 곱해 준다고 말한다. 컨텍스트가 없을 때 생기는 문제는 세 가지다.
- 코드베이스를 어떻게 다니는지, 사용자가 무엇을 좋아하는지 모른다.
- 같은 일을 반복해도 나아지지 않는다.
- 지난 실수에서 배우지 못한다.
컨텍스트 엔지니어링의 변천 (최근 1년)
| 단계 | 방식 | 좋은 점 | 한계 |
|---|---|---|---|
| CLAUDE.md | 세션 시작 때 마크다운 파일 하나를 통째로 넣는다 | 단순하고 효과가 크다 | 파일이 커질수록 컨텍스트 창을 압박한다 |
| 메모리 도구 | 에이전트가 세션 중에 직접 기억을 읽고·쓰고·고친다 (in-band) | 에이전트가 스스로 기억을 관리한다 | 본 작업과 기억 관리 사이에서 주의가 갈린다 |
| Skills | 짧은 요약(front matter)만 보이다가 필요할 때 전체를 불러온다 | 점진적 공개(progressive disclosure)로 컨텍스트가 붓지 않는다 | 절차형 지식에 알맞다 |
| 파일 시스템 메모리 | 마크다운 파일 묶음을 bash·grep 같은 표준 도구로 찾아 읽는다 | 사람이 읽기 쉽고, 자율적이며, 점진적 공개까지 된다 | 현재의 모범 사례로 소개된다 |
발표의 한 줄 요약: “Do the simple thing that works. Markdown is great for reading memories.”
운영에서 필요한 가드레일
- 버전 관리 — 변경마다 계기가 된 세션(transcript), 바꾼 주체(에이전트·사람), 전후 상태의 해시를 남긴다. 되돌리기와 감사 기록이 된다.
- 동시성 — 여러 에이전트가 같은 메모리를 쓸 때 해시로 충돌을 막는다.
- 권한 계층 — 조직 전체 지식(읽기 전용) → 팀·분야 메모리 → 에이전트 개인 작업 공간(읽기·쓰기). 민감한 변경은 사람이 검토한다.
- 이식성 — 깔끔한 API로 만들어 한 도구에 갇히지 않게 한다.
해시 기반 커밋 절차
- 지금 메모리 상태의 해시를 잰다.
- 고칠 내용을 작성한다.
- 커밋 직전에 해시를 다시 잰다.
- 같으면 커밋한다.
- 다르면 다른 누군가가 먼저 고친 것이므로 다시 읽고, 다시 작성하고, 다시 시도한다.
효과로는 반복 작업 정확도 향상, 토큰·지연 감소(한 번에 끝내는 비율 증가), 개발자 시간 절약이 소개된다.
드리밍(dreaming): 세션 밖에서 배우기
세션 안에서 기억을 고치는 방식(in-band)은 에이전트의 주의를 나누고, 자기 세션만 볼 수 있어 여러 세션에 걸친 문제를 놓친다. 드리밍은 이것을 세션 밖(out-of-band)의 비동기 배치 작업으로 옮긴다. 따로 자원을 받고, 모든 세션의 기록을 본다. 에이전트의 답만이 아니라 도구 호출과 메타데이터까지 살핀다.
흐름
- 권한을 지키며 메모리와 에이전트 기록(transcript)을 모은다.
- 여러 분석 에이전트가 기록을 나눠 읽는다.
- 실패·빈틈·설정 오류 같은 패턴을 찾는다.
- 오케스트레이터가 결과를 모아 널리 퍼진 문제를 고른다.
- 근거와 빈도 통계를 붙여 메모리 변경을 제안한다.
- 사람이 변경마다 승인하거나 거절한다.
발표 속 예시
- 모든 학생이 지리 문제 하나를 틀린다 → 커리큘럼에 그 주제가 빠진 것을 찾아 추가한다.
- 수학 답이 모두 도(degree)가 아니라 라디안으로 나온다 → 도구 설정 오류를 찾아 고치자고 제안한다.
- 전체 에이전트가 대시(—)를 지나치게 쓴다 → 조직 전체 컨텍스트를 바꾸자고 제안한다.
드리밍에 드는 추가 토큰은 한 번에 끝내는 비율이 오르고 전체 토큰 사용이 줄면서 상쇄된다고 한다. 아직 아주 새로운 연구 분야이고, 코딩뿐 아니라 발표 자료·고객 응대·분야 전문 지식에도 쓰인다.
확장 단계
| 단계 | 할 일 |
|---|---|
| 단순 | CLAUDE.md(또는 마크다운 메모리), Skills, 에이전트의 자율 메모리 |
| 규모 | 버전 관리, 해시 기반 동시성, 권한 계층 추가 |
| 최적화 | 주기적인 드리밍으로 전체 에이전트가 함께 배우게 한다 |
직접 구현: Codex·Claude Code와 launchd로 로컬 드리밍 만들기
아래 구현은 2026년 10월의 Codex CLI, Claude Code, macOS launchd를 기준으로 한다. runner 하나와 skill 하나를 두 에이전트가
함께 쓰고, 맨 위의 agent 값만 codex나 claude로 바꾼다.
두 도구 모두 예약 기능이 따로 있지만 이 일에는 맞지 않는다.
| 방법 | 동작 | 이 일에 맞지 않는 이유 |
|---|---|---|
| Codex 로컬 scheduled task | 앱이 정해진 시각에 task를 실행한다 | 컴퓨터와 앱이 켜져 있어야 한다 |
| Codex 웹 task · Claude Code 클라우드 예약 | 클라우드에서 정해진 시각에 실행한다 | 컴퓨터가 꺼져 있어도 돌지만 로컬 파일과 session 기록을 읽을 수 없다 |
Claude Code /loop |
열려 있는 session 안에서 일정 간격으로 반복한다 | 터미널을 닫으면 멈춘다 |
launchd + 비대화형 실행 |
macOS가 runner를 깨우고 runner가 codex exec나 claude -p를 부른다 |
로컬 session 기록과 여러 Git 저장소를 읽을 수 있다 |
정확히 02:30에만 예약하면 그때 컴퓨터가 꺼져 있던 날은 건너뛴다. 그래서 로그인할 때와 한 시간마다 runner를 깨운다.
- runner는 02:30 전이거나 오늘 이미 성공했다면 바로 끝난다.
- 실패하면 상태를 갱신하지 않아 다음 시간에 다시 시도한다.
- 며칠 동안 컴퓨터가 꺼져 있었다면, 다음 로그인 뒤 마지막 성공 시점부터 지금까지를 한 번에 분석한다.
파일 구성
~/.dreaming/
├── run-nightly-dreaming.sh # runner (codex·claude 공용)
├── skills/nightly-dreaming/
│ └── SKILL.md # 관찰 범위와 금지 작업
├── logs/
├── reports/
└── state/last-success
~/.codex/skills/nightly-dreaming → ~/.dreaming/skills/nightly-dreaming (심볼릭 링크)
~/.claude/skills/nightly-dreaming → ~/.dreaming/skills/nightly-dreaming (심볼릭 링크)
~/Library/LaunchAgents/io.example.nightly-dreaming.plist
역할은 셋으로 나뉜다.
- skill은 관찰 범위와 금지 작업을 정한다.
- runner는 실행 시점과 성공 상태를 관리한다.
- LaunchAgent는 runner를 깨우기만 한다.
skill은 두 도구가 함께 따르는 Agent Skills 형식(SKILL.md + front matter)이라 파일 하나를 링크로 공유한다.
mkdir -p "${HOME}/.dreaming/skills/nightly-dreaming" "${HOME}/.codex/skills" "${HOME}/.claude/skills"
ln -s "${HOME}/.dreaming/skills/nightly-dreaming" "${HOME}/.codex/skills/nightly-dreaming"
ln -s "${HOME}/.dreaming/skills/nightly-dreaming" "${HOME}/.claude/skills/nightly-dreaming"
한쪽 에이전트만 쓴다면 그쪽 링크만 만든다.
읽기 전용 skill
~/.dreaming/skills/nightly-dreaming/SKILL.md를 만든다.
---
name: nightly-dreaming
description: Analyze recent agent sessions and Git activity to propose evidence-backed memory and documentation improvements. Do not apply proposals automatically.
---
# Nightly Dreaming
최근 작업에서 다음 세션에 재사용할 교훈을 찾는다. 모델이 학습했다고 표현하지 않는다.
결과는 사람이 검토할 컨텍스트 개선 제안이다.
## Source scope
- 마지막 성공 시점부터 현재까지의 에이전트 session 기록과 workspace의 Git 활동을 조사한다.
- Codex: ~/.codex/sessions/
- Claude Code: ~/.claude/projects/
- 마지막 성공 시점이 없을 때만 최근 24시간을 조사한다.
- secret, credential, 환경 변수 값과 Git에서 제외된 파일 내용은 읽거나 출력하지 않는다.
## Selection criteria
- 사용자가 명시적으로 바로잡은 행동이나 규칙
- 두 번 이상 반복된 실패, 우회 또는 재작업
- 여러 프로젝트에 영향을 주는 계약이나 작업 순서
- 재발하면 데이터 손실, 배포 오류 또는 큰 시간 낭비를 일으키는 사건
기존 문서와 skill을 검색해 중복 제안을 만들지 않는다.
## Output
한국어 Markdown 보고서만 반환한다.
- 분석 시작·종료 시각과 조사 범위
- 작업 흐름과 확인된 교훈
- 관찰, 원인, 재발 방지, 근거와 확신도
- 문서 또는 skill의 최소 변경 제안
- 근거가 부족해 보류한 항목
## Hard boundaries
- 파일, Git 상태, schedule, 외부 시스템을 변경하지 않는다.
- build, test, formatter처럼 생성물을 남길 수 있는 명령을 실행하지 않는다.
- commit, push 또는 PR을 만들지 않는다.
- 제안을 자동으로 적용하지 않는다.
지연 실행 runner
~/.dreaming/run-nightly-dreaming.sh를 만든다. 다음 세 가지 실행 방식을 지원한다.
- 기본 예약 실행
- 설정 검사
--check - 예약 조건을 무시하는 수동 실행
--force
맨 위 세 줄을 실제 환경에 맞게 바꾼다.
agent: 쓸 에이전트(codex또는claude)workspace_directory: 분석할 workspace의 절대 경로agent_executable: 그 에이전트 실행 파일의 절대 경로(command -v codex나command -v claude로 확인)
#!/bin/sh
set -u
readonly agent='codex'
readonly workspace_directory='/absolute/path/to/workspace'
readonly agent_executable='/absolute/path/to/codex'
readonly dreaming_directory="${HOME}/.dreaming"
readonly report_directory="${dreaming_directory}/reports"
readonly log_directory="${dreaming_directory}/logs"
readonly state_directory="${dreaming_directory}/state"
readonly last_success_path="${state_directory}/last-success"
umask 077
validate_runtime() {
case "${agent}" in
codex | claude)
;;
*)
printf 'Unknown agent: %s (use codex or claude)\n' "${agent}" >&2
return 1
;;
esac
if [ ! -x "${agent_executable}" ]; then
printf 'Agent executable is unavailable: %s\n' "${agent_executable}" >&2
return 1
fi
if [ ! -d "${workspace_directory}/.git" ]; then
printf 'Workspace is unavailable: %s\n' "${workspace_directory}" >&2
return 1
fi
mkdir -p "${report_directory}" "${log_directory}" "${state_directory}"
chmod 700 \
"${dreaming_directory}" \
"${report_directory}" \
"${log_directory}" \
"${state_directory}"
}
is_before_daily_start() {
current_hour=$(date '+%H')
current_minute=$(date '+%M')
current_hour=${current_hour#0}
current_minute=${current_minute#0}
[ "${current_hour}" -lt 2 ] || \
{ [ "${current_hour}" -eq 2 ] && [ "${current_minute}" -lt 30 ]; }
}
read_last_success() {
last_success=''
if [ -f "${last_success_path}" ]; then
IFS= read -r last_success < "${last_success_path}" || true
fi
}
should_run_today() {
if [ "${force_run}" = 'true' ]; then
return 0
fi
if is_before_daily_start; then
return 1
fi
current_date=$(date '+%Y-%m-%d')
last_success_date=${last_success%%T*}
[ -z "${last_success}" ] || [ "${last_success_date}" != "${current_date}" ]
}
# 에이전트마다 읽기 전용 비대화형 실행 방법이 다르다. 둘 다 최종 답만 첫 인자의 파일에 남긴다.
run_agent() {
agent_output_path=$1
agent_prompt=$2
case "${agent}" in
codex)
"${agent_executable}" exec \
--cd "${workspace_directory}" \
--sandbox read-only \
--ephemeral \
--color never \
--output-last-message "${agent_output_path}" \
"${agent_prompt}" \
> /dev/null
;;
claude)
cd "${workspace_directory}" || return 1
"${agent_executable}" -p \
--tools Read Grep Glob \
--strict-mcp-config \
--permission-mode dontAsk \
--add-dir "${HOME}/.claude/projects" "${log_directory}" \
--no-session-persistence \
--output-format text \
"${agent_prompt}" \
> "${agent_output_path}"
;;
esac
}
force_run='false'
case "${1:-}" in
'')
;;
--check)
validate_runtime || exit 1
printf 'Nightly dreaming runtime is ready (%s).\n' "${agent}"
exit 0
;;
--force)
force_run='true'
;;
*)
printf 'Usage: %s [--check|--force]\n' "$0" >&2
exit 64
;;
esac
validate_runtime || exit 1
read_last_success
if ! should_run_today; then
exit 0
fi
readonly run_timestamp=$(date '+%Y-%m-%dT%H-%M-%S')
readonly report_path="${report_directory}/${run_timestamp}.md"
readonly temporary_report_path="${report_directory}/.${run_timestamp}.tmp"
readonly log_path="${log_directory}/${run_timestamp}.log"
if [ -n "${last_success}" ]; then
analysis_window="from ${last_success} through the current time"
git_since=${last_success}
else
analysis_window='for the last 24 hours'
git_since='24 hours ago'
fi
# Claude에게는 Bash를 주지 않으므로, 분석 구간의 Git 기록을 runner가 미리 파일로 뽑아 둔다
readonly git_activity_path="${log_directory}/${run_timestamp}.git.txt"
git -C "${workspace_directory}" log --all --since="${git_since}" --date=iso --stat \
> "${git_activity_path}" 2>&1
chmod 600 "${git_activity_path}"
if [ "${agent}" = 'codex' ]; then
skill_reference='$nightly-dreaming'
else
skill_reference='the nightly-dreaming skill'
fi
readonly prompt="Use ${skill_reference} to analyze work ${analysis_window}. Git activity for this window is in ${git_activity_path}. Return the Korean Markdown report only. Do not modify files, Git state, schedules, external systems, or memory."
cleanup() {
rm -f "${temporary_report_path}"
}
trap cleanup EXIT HUP INT TERM
if ! run_agent "${temporary_report_path}" "${prompt}" 2> "${log_path}"; then
printf 'Nightly dreaming failed. See %s\n' "${log_path}" >&2
exit 1
fi
if [ ! -s "${temporary_report_path}" ]; then
printf 'Nightly dreaming produced an empty report.\n' >&2
exit 1
fi
mv "${temporary_report_path}" "${report_path}"
completion_timestamp=$(date '+%Y-%m-%dT%H:%M:%S%z')
temporary_state_path="${state_directory}/.last-success.$$"
printf '%s\n' "${completion_timestamp}" > "${temporary_state_path}"
chmod 600 "${temporary_state_path}"
mv "${temporary_state_path}" "${last_success_path}"
chmod 600 "${report_path}" "${log_path}"
trap - EXIT HUP INT TERM
printf 'Nightly dreaming report: %s\n' "${report_path}"
두 에이전트 모두 대화형 화면 없이 실행하는 공식 경로를 쓴다. 읽기 전용으로 묶는 방법은 서로 다르다.
Codex (codex exec) |
Claude Code (claude -p) |
|
|---|---|---|
| 읽기 전용 | --sandbox read-only: OS 샌드박스가 쓰기를 막는다 |
--tools Read Grep Glob으로 쓰기·실행 도구를 아예 주지 않고, --strict-mcp-config로 MCP 도구도 뺀다. --permission-mode dontAsk는 묻는 창 없이 거절한다 |
| Git 기록 | 샌드박스 안에서 git log를 직접 실행한다 |
Bash가 없으므로 runner가 미리 뽑은 logs/*.git.txt를 읽는다 |
| session 기록 읽기 | 샌드박스가 읽기는 허락한다 | --add-dir로 ~/.claude/projects와 logs 폴더를 읽을 수 있게 연다 |
| 이번 실행을 기록에 남기지 않기 | --ephemeral |
--no-session-persistence |
| 최종 답 받기 | --output-last-message 파일 |
표준 출력(--output-format text) |
| skill 부르기 | 프롬프트에 $nightly-dreaming |
프롬프트에 skill 이름. description을 보고 불러온다 |
Claude Code에서 --allowedTools로 Bash(git log *)만 허락하는 방법은 쓰지 않는다. 이 목록은 사용자 설정(~/.claude/settings.json)의
허용 규칙과 합쳐져서, 설정에 Bash(*) 같은 넓은 규칙이 있으면 파일 쓰기까지 통과한다. 실제로 그런 설정에서 echo hi > probe.txt가
실행됐다. 도구 자체를 빼면 설정과 상관없이 쓸 수 없다.
이번 실행을 기록에 남기지 않는 이유가 있다. 남기면 다음 날 드리밍이 자기 보고서 작업까지 분석 대상으로 읽는다. 모델은 최종 답만 임시 파일에 쓰고, runner는 성공한 뒤에만 보고서와 상태를 옮긴다.
LaunchAgent
~/Library/LaunchAgents/io.example.nightly-dreaming.plist를 만든다. launchd는 ProgramArguments의 $HOME이나 ~를
확장하지 않는다. 그래서 모든 __HOME__을 실제 home의 절대 경로로 바꾼다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>io.example.nightly-dreaming</string>
<key>ProgramArguments</key>
<array>
<string>__HOME__/.dreaming/run-nightly-dreaming.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>__HOME__</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>3600</integer>
<key>ProcessType</key>
<string>Background</string>
<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>__HOME__/.dreaming/logs/launchd.stdout.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.dreaming/logs/launchd.stderr.log</string>
</dict>
</plist>
검증하고 등록한다.
chmod 700 "${HOME}/.dreaming/run-nightly-dreaming.sh"
"${HOME}/.dreaming/run-nightly-dreaming.sh" --check
plutil -lint "${HOME}/Library/LaunchAgents/io.example.nightly-dreaming.plist"
launchctl bootstrap \
"gui/$(id -u)" \
"${HOME}/Library/LaunchAgents/io.example.nightly-dreaming.plist"
launchctl enable "gui/$(id -u)/io.example.nightly-dreaming"
launchctl print "gui/$(id -u)/io.example.nightly-dreaming"
설정을 바꿨다면 launchctl bootout 후 다시 bootstrap한다. 바로 시험할 때만 --force를 쓴다. Claude Code는 처음
claude -p를 돌리기 전에 터미널에서 한 번 로그인해 두어야 launchd 실행에서도 인증이 된다.
macOS가 아니라면 같은 runner를 그대로 두고 깨우는 쪽만 바꾼다.
- Linux: systemd user timer(
Persistent=true면 꺼져 있던 동안 놓친 실행을 켠 뒤 한 번 돌린다) - Windows: 작업 스케줄러의 “예약된 시간을 놓친 경우 가능한 빨리 실행”
운영 안전장치
- 보고서에는 근거와 제안만 남긴다. 문서·skill·Git을 자동 수정하거나 commit하지 않는다.
.env, token, credential, 개인 정보는 읽거나 보고서에 싣지 않는다.- 마지막 성공 시각은 성공한 뒤에만 바꾼다. 실패를 성공으로 기록하면 분석 구간이 사라진다.
- 보고서와 로그는
umask 077과 mode600으로 보호한다. - 아침에 사람이 근거와 중복 여부를 확인하고, 필요한 제안만 별도 작업으로 반영한다.