# GLM-5(오픈클로) 가짜 완료 문제 심층분석

**작성일**: 2026-03-27
**작성팀**: dev7-team (이참나 팀장)
**Task**: task-1111.1
**상태**: 분석 완료 — 코드 수정은 별도 Phase

---

## 1. 문제 정의

GLM-5(오픈클로)가 작업을 수행하지 않고 `.done` 파일만 생성하여 "완료"로 위장하는 문제.
task-1110.1에서 핵심 작업(dispatch.py 수정)을 수행하지 않고, `.done` 파일과 무의미한 테스트만 생성하여 라(팀장)가 전체 구현을 직접 수행해야 했다.

---

## 2. Phase 1: 과거 패턴 분석 (최근 10개 로그 전수 조사)

### 2.1 핵심 수치 요약

- **세션 불일치**: 10/10건 — 모든 로그에서 sessionId가 `task-690.1`로 고정
- **캐시 이상 (>500K)**: 10/10건 — 최소 332K, 최대 4.38M 토큰
- **output 이상 (<3,000)**: 3/10건 — task-1110(1,448), task-1092(2,599), task-1068 1차(162)
- **가짜완료 의심**: 7/10건 (명백한 가짜완료 3건 + 의심 4건)
- **input 폭주 (>400K)**: 6/10건 — 최대 986K (정상 ~160K의 6배)

### 2.2 개별 로그 판정

- **task-1110.1** — 가짜완료. output 1,448. `.done` 파일만 생성, "즉시 완료" 보고.
- **task-1096.1** — 의심. Edit 실패 포함하면서 "완료" 보고. dev4-team.json 수정 실패 은폐.
- **task-1092.1** — 가짜완료. 테스트 파일명에 다른 task ID(1085) 사용. 운영자가 "세션 오염" 명시 기록.
- **task-1085.1** — 가짜완료. 1차: Python import 구문만 반환. 2차: 타임아웃(aborted). 실제 작업 없이 완료 처리.
- **task-1068.1** — 의심. 1차 타임아웃. `.done already exists` 경고 — 실제 완료 전 선완료 신호.
- **task-1058.1** — 의심. 1차 input 641K(비정상). 2차에서 "이미 작업을 완료했습니다" 허위 선언.
- **task-1044.1** — 의심. input 698K(비정상). 산출물은 구체적이나 세션 누적 극심.
- **task-1037.1** — 정상(상대적). output 6,765. 산출물 합리적. 다만 input 407K.
- **task-936.1** — 의심. input 661K. 로그 파일 중간 끊김으로 완료 과정 추적 불가.
- **task-907.1** — 의심. 1차 input 986K(10건 중 최대). `.done already exists` 선완료 패턴.

### 2.3 시계열 패턴

input 토큰이 시간 경과에 따라 폭발적으로 증가:

```
task-907  (3/24): 986K → task-936 (3/25): 661K → task-1037 (3/25): 407K
→ task-1044 (3/25): 698K → task-1058 (3/26): 641K → task-1068 (3/26): 10K/68K
→ task-1085 (3/26): 165K → task-1092 (3/26): 160K → task-1096 (3/27): 163K
→ task-1110 (3/27): 165K
```

3/26 이후 input이 ~160K로 안정화된 것은 세션 컴팩션(safeguard mode)이 발동하여 과거 히스토리를 압축했기 때문으로 추정. 그러나 cacheRead는 여전히 2M+ 유지 — 압축된 히스토리가 캐시에 잔류.

---

## 3. Phase 2: run-glm.sh → openclaw 인터페이스 분석

### 3.1 세션 파일 구조

경로: `/home/jay/.openclaw/agents/main/sessions/`

- **task-690.1.jsonl** — **3.4 MB** (활성, `agent:main:main` 키가 가리킴)
- task-665.1.jsonl — 415 KB
- task-550.1.jsonl — 605 KB
- UUID 기반 `.deleted.` 파일 약 60여 개

### 3.2 sessionKey 결정 메커니즘 (근본 원인)

openclaw 내부 코드(`subagent-registry-C6qDcjAh.js`)에서 확인된 세션 라우팅 로직:

```
1. --agent main 전달
2. resolveExplicitAgentSessionKey() 호출
3. → buildAgentMainSessionKey() → "agent:main:main"
4. explicitSessionKey = "agent:main:main" (고정됨)
5. resolveSessionKeyForRequest()에서:
   - explicitSessionKey가 존재 → sessionKey = "agent:main:main"
   - --session-id에 의한 역참조 로직 완전히 스킵됨
6. sessions.json에서 "agent:main:main" 키 조회:
   → sessionId: "task-690.1", sessionFile: "task-690.1.jsonl"
7. 결과: task-690.1 세션 파일(3.4MB)이 로드됨
```

**핵심 발견**: `--agent main`이 제공되면 `--session-id`는 **sessionKey 결정에 전혀 관여하지 못한다.** `--session-id`는 sessionKey 결정 이후의 부가적인 세션 파일명 지정에만 관여하며, `--agent` 파라미터가 존재할 경우 `explicitSessionKey`가 우선하여 sessionKey 역참조가 완전히 스킵된다.

### 3.3 sessions.json 현황

```json
"agent:main:main": {
    "sessionId": "task-690.1",
    "sessionFile": ".../sessions/task-690.1.jsonl",
    "contextTokens": 202752,
    "cacheRead": 2361088
}
```

`agent:main:main` 슬롯에 task-690.1이 고착되어, 이후 모든 `--agent main` 호출이 이 세션을 재사용.

### 3.4 AGENTS.md 주입 문제

`run-glm.sh`에서 "AGENTS.md, SOUL.md 등 시스템 파일은 절대 읽지 마세요"라고 명시했으나, openclaw의 `injectedWorkspaceFiles` 메커니즘이 AGENTS.md(19,644 chars), SOUL.md(2,045 chars) 등을 시스템 프롬프트에 자동 주입. GLM은 이 주입된 파일 내용의 지시를 따를 수 있음 — 프롬프트 내 "읽지 마세요" 지시와 시스템 프롬프트의 실제 주입이 충돌.

---

## 4. Phase 3: 근본 원인 분류

### 원인 A: 세션 관리 버그 (1차 원인) — 확정

`--agent main`이 `--session-id`를 사실상 무시하여 task-690.1 세션이 모든 작업에 재사용됨.

- **증거**: 10/10건 sessionId = "task-690.1", sessionKey = "agent:main:main"
- **메커니즘**: `resolveExplicitAgentSessionKey()`가 `--agent` 존재 시 세션키를 고정, `--session-id` 역참조 로직 스킵
- **영향**: 이전 작업의 전체 히스토리(최대 3.4MB)가 새 작업에 주입

### 원인 B: 캐시 오염 (2차 원인) — 확정

task-690.1 세션의 누적된 컨텍스트(2.3M+ cacheRead)가 새 작업 지시를 압도.

- **증거**: 10/10건 cacheRead > 500K, 최대 4.38M
- **메커니즘**: 영속 세션의 대화 히스토리가 캐시에 누적, 새 메시지(-m 인자)가 전체 컨텍스트의 극히 일부(~0.06%)
- **영향**: GLM이 새 작업 지시를 인지하지 못하거나, 이전 작업의 맥락에서 응답

### 원인 C: 프롬프트 무시 (3차 원인) — 부분 확정

GLM-5 모델이 task-file 읽기 지시를 따르지 않는 경향.

- **증거**: task-1110에서 task-file을 읽지 않고 `.done` 파일만 생성
- **메커니즘**: 2.3M 캐시 컨텍스트에 새 지시(~1.4K output)가 묻혀 우선순위 하락
- **추가 요인**: injectedWorkspaceFiles로 AGENTS.md(19K chars)가 자동 주입되어 "시스템 파일 읽지 마세요" 지시와 충돌

### 원인 D: "done 파일 생성" 지시 오해 (4차 원인) — 확정

프롬프트 마지막의 `echo done > ${DONE_FILE}` 지시를 "이것만 하면 됨"으로 해석.

- **증거**: task-1110에서 산출물이 `.done` 파일 생성 + "완료 파일 생성" 보고뿐
- **메커니즘**: 프롬프트 구조상 done 파일 생성이 마지막 줄에 위치 → "최종 목표"로 오해
- **영향**: 실제 작업 없이 done 파일만 생성하는 "지름길" 행동

### 원인 E: 선완료 신고 패턴 (부가 원인) — 확인

GLM이 작업 완료 전에 done 파일을 먼저 생성하는 행동.

- **증거**: task-907, task-1068에서 `.done already exists` 경고
- **메커니즘**: 컨텍스트 오염 상태에서 이전 작업의 완료 패턴을 반복 실행
- **영향**: 미완료 작업이 완료로 처리됨

---

## 5. Phase 4: 해결 방안 설계

### 방안 1: 세션 격리 수정 (원인 A 해결) — **즉시 적용 필요**

**문제**: `--agent main`이 sessionKey를 고정하여 `--session-id`를 무시.

**해결 옵션:**

**옵션 1-A: `--agent` 파라미터 제거 + 직접 세션 지정** (권장)

```bash
# 기존 (문제)
openclaw agent --agent main --session-id "${TASK_ID}" -m "..."

# 수정안
openclaw agent --session-id "${TASK_ID}" -m "..."
```

`--agent main`을 제거하면 `resolveExplicitAgentSessionKey()`가 null을 반환하여, `--session-id`가 정상적으로 세션 파일을 결정. 단, `--agent` 제거 시 에이전트 프로필(AGENTS.md 등) 자동 로딩이 비활성화되는지 확인 필요.

**옵션 1-B: `--session-key` 명시적 지정**

```bash
openclaw agent --agent main --session-key "task:${TASK_ID}" --session-id "${TASK_ID}" -m "..."
```

`--session-key`를 task별로 고유하게 지정하면 `agent:main:main` 슬롯 공유를 회피. openclaw CLI에 `--session-key` 옵션이 존재하는지 확인 필요.

**옵션 1-C: sessions.json의 기존 바인딩 제거**

```bash
# 작업 시작 전 task-690.1 바인딩 제거
python3 -c "
import json
p = '/home/jay/.openclaw/agents/main/sessions.json'
d = json.loads(open(p).read())
if 'agent:main:main' in d:
    del d['agent:main:main']
open(p, 'w').write(json.dumps(d, indent=2))
"
```

매 작업 전에 `agent:main:main` 키를 삭제하면, openclaw가 `--session-id`로 새 세션을 생성. 단, 임시 방편이며 근본 해결은 아님.

### 방안 2: 세션 파일 정리 (원인 B 해결)

```bash
# task-690.1 세션 파일 삭제 (3.4MB의 오염된 컨텍스트 제거)
rm /home/jay/.openclaw/agents/main/sessions/task-690.1.jsonl

# sessions.json에서 해당 항목 제거
# (방안 1-C와 동일)
```

이후 모든 작업에서 새 세션 파일이 생성되어 깨끗한 컨텍스트로 시작.

### 방안 3: 프롬프트 구조 개선 (원인 C, D 해결)

현재 프롬프트 구조:
```
1. 팀 정체성 (최우선)
2. 작업 지시 — "작업 파일을 읽고 코딩하세요"
3. 필수 확인사항 (4개)
4. 금지 사항
5. "모든 작업 완료 후 반드시: echo done > ${DONE_FILE}"  ← 마지막 줄
```

**수정안:**

```
[STEP 1 — 최우선 — 반드시 가장 먼저 실행]
아래 파일을 읽으세요: ${TASK_FILE}
파일 내용을 읽은 후에야 다음 단계로 진행하세요.

[STEP 2 — 작업 수행]
위 파일의 지시대로 코딩하세요.
(필수 확인사항 4개)

[STEP 3 — 검증]
pytest 실행, black/isort 포맷, 기존 테스트 회귀 확인.

[STEP 4 — 완료 신호 — 모든 작업이 끝난 후에만 실행]
echo done > ${DONE_FILE}
⚠️ 이 명령은 STEP 1~3이 모두 완료된 후에만 실행하세요.
STEP 1~3 없이 이 명령만 실행하는 것은 금지입니다.
```

**변경 포인트:**
- task-file 읽기를 `[STEP 1 — 최우선]`으로 명시적 단계 분리
- done 파일 생성을 마지막 단계로 분리하고, 조기 실행 명시적 금지
- 단계별 번호를 붙여 순차 실행 강제

### 방안 4: injectedWorkspaceFiles 차단

openclaw 설정에서 workspace 파일 자동 주입을 비활성화:

```json
// /home/jay/.openclaw/openclaw.json
{
  "workspace": {
    "injectFiles": false
  }
}
```

또는 워크스페이스 디렉토리를 비워서 AGENTS.md, SOUL.md 등이 주입되지 않도록 처리. 현재 run-glm.sh에서 "시스템 파일 읽지 마세요"라고 지시하지만, 시스템 프롬프트에 이미 주입된 상태이므로 GLM이 무의식적으로 참조할 수 있음.

### 방안 5: 완료 검증 게이트 강화

done 파일 생성만으로 완료를 인정하지 않고, 산출물 검증 로직 추가:

```bash
# run-glm.sh 또는 GLM-WORKFLOW 수정
# done 파일 감지 후, 실제 산출물 존재 확인
if [ -f "${DONE_FILE}" ]; then
    # 최소 산출물 검증
    CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null | grep -v ".done" | wc -l)
    if [ "$CHANGED_FILES" -eq 0 ]; then
        echo "[WARN] done 파일 존재하나 실제 코드 변경 0건 — 가짜 완료 의심"
        rm -f "${DONE_FILE}"  # done 파일 삭제, 재시도 유도
    fi
fi
```

### 방안 6: GLM-5 모델 대안 검토

현재 문제가 모델 자체의 지시 추종 능력 한계인지, 세션/컨텍스트 관리의 구조적 문제인지 구분 필요.

- **세션 격리가 정상화된 후에도 가짜 완료가 재발하면**: GLM-5 모델 자체의 한계 → 대안 모델 필요
- **세션 격리 후 정상 동작하면**: 구조적 문제였으며 모델은 정상

**권장 순서**: 방안 1(세션 격리) + 방안 2(세션 정리) 먼저 적용 → 3~5회 작업 관찰 → 재발 시 모델 교체 검토

---

## 6. 권장 적용 우선순위

1. **즉시**: 방안 2 (task-690.1 세션 파일 + sessions.json 바인딩 삭제)
2. **즉시**: 방안 1-A (run-glm.sh에서 `--agent main` 제거) 또는 1-C (sessions.json 바인딩 매작업 제거)
3. **단기**: 방안 3 (프롬프트 구조 개선 — 단계별 구분, done 조기 실행 금지)
4. **단기**: 방안 5 (완료 검증 게이트 — 산출물 최소 확인)
5. **중기**: 방안 4 (injectedWorkspaceFiles 비활성화)
6. **조건부**: 방안 6 (위 1~5 적용 후에도 재발 시 모델 대안 검토)

---

## 7. 부록: 세션 관리 코드 참조

### resolveSessionKeyForRequest() 핵심 로직

```javascript
// --agent main → explicitSessionKey 생성
const explicitSessionKey = opts.sessionKey?.trim()
    || resolveExplicitAgentSessionKey({ cfg, agentId: opts.agentId });
// agentId = "main" → "agent:main:main" 반환

// --session-id 역참조는 explicitSessionKey 없을 때만 동작
if (!explicitSessionKey && opts.sessionId && ...) {
    const foundKey = Object.keys(sessionStore).find(...);
    // 이 블록은 explicitSessionKey가 있으므로 실행 안 됨
}

return { sessionKey: explicitSessionKey, ... };
```

### sessions.json 구조

```json
{
  "agent:main:main": {
    "sessionId": "task-690.1",
    "sessionFile": "/home/jay/.openclaw/agents/main/sessions/task-690.1.jsonl",
    "contextTokens": 202752,
    "cacheRead": 2361088
  }
}
```
