Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C993_passing-brightgreen" alt="2,993 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C008_passing-brightgreen" alt="3,008 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C993_passing-brightgreen" alt="2,993 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C008_passing-brightgreen" alt="3,008 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-2%2C993_passing-brightgreen" alt="2,993 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C008_passing-brightgreen" alt="3,008 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,3 +770,21 @@ B4(000과의 정합): 000 §work-phase 맵의 wp6 설명을 이 문서 기준(sr
| rollout_summaries 256 상한 영향 | 파일 수·날짜 분포와 `max_raw_memories_for_consolidation` 대조 | 동일 |

행 번호 정정: `hook.ts:110-145`, `hook.ts:499-531`, `format.ts:52-67`.


## P 재검증 (wp6 사이클, 2026-09-10)

기준 트리 = origin/dev(#128 머지 직후). 계획 이후 `recall/src/hook.ts`는 wp4(#127)가 `listCwdSessions` 호출부와 "Recent work — project" 문구를 바꿨고(cwd-context.ts repo_key 연합), `format.ts`·`cxc-ops/src/doctor.ts`·`hook-trust.ts`는 계획 시점과 동일하다. §4.1의 SessionStart 분기는 #127의 cwd-context 호출을 그대로 감싸고, `dedicatedToolsEnabled`는 A 감사 반영대로 `paths.ts codexHome()`을 쓴다. §4.4의 훅 결과 헬퍼는 `assertLegalHookResult({stdout, stderr, code})`(자식 프로세스 실행). §10 관측 항목은 C에서 실측해 receipt에 붙인다. B는 행이 아니라 심볼로 patch한다. 브랜치 `codex/memory-l1-wp6-native`.



## A 감사 반영 (wp6 round 1, 2026-09-10)

리뷰어(grok-4.6) GO-WITH-FIXES(blocker 3, Medium 1, Low 1). 구현 제약으로 접는다.

1. resume 안내문은 기존 테스트가 요구하는 "recall is available" 문구를 유지한다(resume 전용 문구는 그 뒤에 덧붙이는 형태). `hook.test.ts:98-102`의 startup/resume/clear 루프는 그대로 통과해야 한다.
2. `handleSessionStart`의 `dedicatedTools` 기본값은 이 머신 config(`[memories] dedicated_tools = true`)를 읽으므로, 기존 SessionStart 테스트(`hook.test.ts:74, :94` `/cxc chat search/`)는 `{ dedicatedTools: false }`를 명시하거나 테스트 홈의 config.toml 픽스처로 분기를 고정한다. §5의 "false/생략 → cxc chat search" 기대에서 "생략"을 뺀다. 분기 활성화 테스트는 두 값을 모두 명시.
3. §4.2 활성화 행의 프롬프트를 `그때 그 작업 hook.ts MEMORY-WRITE-GATE`(현재 `detectRecallIntent` 참)로 바꾼다. `그때` 단독은 트리거가 아니다.
4. Medium: §4.4 자식 프로세스 테스트는 `hook-e2e.test.mjs:10-16,124`의 `emptyCodexHome()` 패턴대로 `CODEX_HOME`·`CODEX_SQLITE_HOME`을 빈 임시 홈으로 고정한다(러너의 `CODEXCLAW_HOME`만으로는 라이브 `~/.codex`를 읽는다).
5. Low: doctor WARN 강등이 바꾸는 기존 단언은 `hook-trust.test.ts:256`, `:543`(drifted FAIL→WARN, :240 제목)이고 `:527`(untrusted FAIL)은 유지.

39 changes: 39 additions & 0 deletions devlog/_plan/260910_memory-followup-roadmap/076_wp6_receipt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 076 — wp6 receipt: 네이티브 통합 보강

날짜: 2026-09-10 (KST). 세션 01a0880e-149e-72d0-86db-53bd99bcac0e. 브랜치 `codex/memory-l1-wp6-native`(origin/dev 9384a4f5 위), 구현 커밋 `7f21ed65`. 구현은 opus-5 실행 서브에이전트, 검증·통합·관측은 메인.

## 결론

웹 조사(001 §3)가 권한 네 항목이 기존 훅과 인덱스 위에서 들어갔다. SessionStart는 source별로 브리핑 형태를 나누고 끝에 회수 안내 한 줄을 붙이며(`dedicated_tools = true`면 `memories.search`, 아니면 cxc 명령), UserPromptSubmit은 recall 의도가 잡힐 때 프롬프트에서 뽑은 표적 용어를 제안하고(검색 미실행), memory search 출력은 `[age: Nd]`·`[newer: <relpath>]` 라벨을 달며(랭킹 불변), 훅 출력 계약(exit 0·stderr ANSI 없음·stdout JSON 또는 빈 문자열)이 자식 프로세스 테스트로 고정됐다. doctor는 trusted_hash drift만 있으면 WARN, 항목 없음은 FAIL이다.

## 무엇이 바뀌었나

- `recall/src/hook.ts`: `handleSessionStart(…, opts?)` source 분기(startup/clear는 기존 문구, resume은 재개 문장 추가, compact는 기존 COMPACTED_BUDGET 경로) + 160자 캡 회수 안내; `dedicatedToolsEnabled()`(`codexHome()/config.toml` `[memories]` 테이블, fail-open false); `extractRecallTargets`(버전·파일명·오류 코드·CamelCase·따옴표, 최대 4) + "Suggested recall terms:"; 트리거 어휘 5개 추가(`그때` 단독 제외); `assertLegalHookResult` export.
- `recall/src/format.ts`: `[age: Nd]`(nowMs 주입 가능), 다른 파일의 더 최신 히트가 같은 특징 토큰을 가지면 `[newer: …]`.
- `cxc-ops/src/doctor.ts`·`hook-trust.ts`: drift만 WARN("trusted_hash drift (reinstall updates it; hooks still run)"), untrusted 포함 시 FAIL 유지, `file_sha256` 근거.
- 테스트 +15(recall 191, cxc-ops 199), NEW `format-freshness.test.ts`, 자식 프로세스 계약 테스트(빈 임시 CODEX_HOME/CODEX_SQLITE_HOME/CODEXCLAW_HOME). dist 4파일. 배지 2993→3007.

계획과의 차이: compact 브리핑은 두 줄 명령 대신 단일 회수 안내로 통일; resume 문구는 "recall is available" 뒤에 덧붙임(감사 제약 1); `fileSha256`은 `HookEntry`에 위치.

## 증거

- receipt `.codexclaw/evidence/01a0880e-149e-72d0-86db-53bd99bcac0e/test-receipt.json`: `/tmp/mfu-260910/check-wp6.sh` exit 0 @ 7f21ed65 — dist CLI로 SessionStart startup/resume/compact 3형태(350/429/336바이트, exit 0, ANSI 없음, 회수 안내 포함), config 픽스처 `dedicated_tools = true`에서 `memories.search` 안내, UserPromptSubmit 표적 제안(`hook.ts`·`2.49.0`), 실인덱스 `2.49.0 SLSA` 출력에 age 4·newer 3, 스위트 390 + 38 pass.
- 이 머신의 doctor는 FAIL이다: #119 bg-wake·#116 fallback 훅이 `hooks.state`에 아직 없어(untrusted) 설계대로 FAIL, drift 1건은 같은 줄에 표시. 재설치 후 `cxc hooks retrust`가 처방이다.
- 감사: grok-4.6 리뷰어 GO-WITH-FIXES(3 High + Medium + Low) → 구현 제약으로 접어 C 테스트가 확인.

## §10 관측 항목 실측 (2026-09-10, 메인)

| 항목 | 실측 |
|---|---|
| compaction 후 SessionStart 재점화 | 09-09 15:00 이후 rollout 526개 중 `compacted` 11건, 그 직후 6항목 안에 `[cxc-recall]`/compact 문맥 주입 7건(64%). 나머지 4건은 compaction 뒤 사용자 턴이 없었거나 SessionStart가 재발화되지 않은 것으로 보이며 원인 분리는 미완(unknown) |
| `memories.search` 호출 빈도 | 같은 창에서 최상위 function_call로 `memor*` 이름 0건. Desktop이 `exec` 코드모드 안에서 호출하면 집계에 안 잡히므로 하한값이다(이 세션 자체가 `tools.memories__read`를 exec 안에서 썼다) |
| rollout_summaries 상한 | 256개 = `max_raw_memories_for_consolidation` 상한 그대로(notes/02) |
| `msgs_fts` 바이트 | `msgs_fts_data` 319,176 블록, 1,252,722,036 바이트(약 1.25GB, 인덱스 12.1GB의 10%) |
| 훅 `--no-refresh`와 신규 워크트리 주입 | wp4로 워크트리가 같은 origin에 묶이므로 신규 슬롯에서도 메인 체크아웃 세션이 Recent work에 들어간다(074 골든). refresh 지연 자체는 별도 측정 안 함 |

## 개선되지 않은 것 (LOOP-PESSIMIST-01)

- 표적 회수 제안은 안내문일 뿐 검색을 대신 실행하지 않는다. 에이전트가 제안을 따르는 비율은 이 사이클이 측정하지 못한다.
- 네이티브 요약 포화(92.5%)와 rollout_summaries 256 상한은 codexclaw 코드로 못 줄인다(000 wp7 결정 항목 그대로).
- 방향이 틀렸다는 신호: 회수 안내 한 줄이 들어간 뒤에도 recall 도구 호출이 늘지 않으면 안내 위치(브리핑 끝)가 아니라 모델의 도구 선택 정책이 병목이다.

22 changes: 18 additions & 4 deletions plugins/codexclaw/components/cxc-ops/dist/doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,27 +461,41 @@ export function runHookTrustCheck(pluginRoot , options = {
// Distinguish "never trusted" from "drifted" so the repair line is the one
// the operator actually needs (issue #33).
const neverTrusted = failed.filter((result) => result.actual === null);
// Drift and absence are different facts. A recorded hash that no longer
// matches means the manifest moved on while the hooks kept running — the
// host decides trust from its own record, so codexclaw reporting this as a
// failure would paint every post-update machine red for a condition that
// blocks nothing (PLAN-BYPASS-NAMED-01). A MISSING entry is different: the
// hooks were never approved, and that stays a failure.
const driftedOnly = failed.length > 0 && neverTrusted.length === 0;
const repair =
failed.length === 0
? undefined
: neverTrusted.length === failed.length
? `${failed.length} hook(s) have no trust entry in ${join(codexHome, "config.toml")}; only Codex itself writes those on hook approval. Approve this plugin's hooks in Codex, or record them explicitly with: cxc hooks retrust --key ${pluginKey} --codex-home ${codexHome} --bootstrap-ok`
: `cxc hooks retrust --key ${pluginKey} --codex-home ${codexHome}`;
const failureDetail = failed
.map(
(result) =>
`${result.status} ${result.key} expected=${result.hash} actual=${result.actual ?? "(none)"} file_sha256=${result.fileSha256.slice(0, 16)}`,
)
.join("; ");
return {
name: "hook-trust",
// An EMPTY result set is not a pass. `diagnoseHookTrust` skips a handler it
// cannot hash (invalid matcher, empty command, async), so "0 failed" can also
// mean "0 examined" — a green check over hooks nobody verified.
severity: results.length === 0 ? "WARN" : failed.length === 0 ? "PASS" : "FAIL",
severity:
results.length === 0 ? "WARN" : failed.length === 0 ? "PASS" : driftedOnly ? "WARN" : "FAIL",
repair,
evidence:
results.length === 0
? `no hook handler could be hashed for ${pluginKey}; nothing was verified`
: failed.length === 0
? `${results.length} hook hash(es) trusted for ${pluginKey}`
: failed
.map((result) => `${result.status} ${result.key} expected=${result.hash} actual=${result.actual ?? "(none)"}`)
.join("; "),
: driftedOnly
? `trusted_hash drift (reinstall updates it; hooks still run): ${failureDetail}`
: failureDetail,
};
} catch (error) {
return { name: "hook-trust", severity: "FAIL", evidence: error instanceof Error ? error.message : String(error) };
Expand Down
11 changes: 10 additions & 1 deletion plugins/codexclaw/components/cxc-ops/dist/hook-trust.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ const MATCHER_DROPPED_EVENTS = new Set (["UserPromptSubmit", "Stop












Expand Down Expand Up @@ -166,7 +172,9 @@ export function listHookEntries(pluginRoot , pluginKey )
if (typeof hookRef !== "string") throw new Error("plugin manifest hook references must be strings");
const relativePath = normalizeHookPath(hookRef);
assertSafeHeaderValue(relativePath, "hook path");
const document = JSON.parse(readFileSync(containedPluginFile(pluginRoot, relativePath), "utf8"))
const raw = readFileSync(containedPluginFile(pluginRoot, relativePath));
const fileSha256 = createHash("sha256").update(raw).digest("hex");
const document = JSON.parse(raw.toString("utf8"))

;
for (const [rawEventName, rawGroups] of Object.entries(document.hooks ?? {})) {
Expand Down Expand Up @@ -197,6 +205,7 @@ export function listHookEntries(pluginRoot , pluginKey )
entries.push({
key: `${pluginKey}:${relativePath}:${EVENT_LABELS[eventName]}:${groupIdx}:${handlerIdx}`,
hash: identityHash(eventName, group.matcher , handler),
fileSha256,
});
}
}
Expand Down
22 changes: 18 additions & 4 deletions plugins/codexclaw/components/cxc-ops/src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,27 +461,41 @@ export function runHookTrustCheck(pluginRoot: string, options: DoctorOptions = {
// Distinguish "never trusted" from "drifted" so the repair line is the one
// the operator actually needs (issue #33).
const neverTrusted = failed.filter((result) => result.actual === null);
// Drift and absence are different facts. A recorded hash that no longer
// matches means the manifest moved on while the hooks kept running — the
// host decides trust from its own record, so codexclaw reporting this as a
// failure would paint every post-update machine red for a condition that
// blocks nothing (PLAN-BYPASS-NAMED-01). A MISSING entry is different: the
// hooks were never approved, and that stays a failure.
const driftedOnly = failed.length > 0 && neverTrusted.length === 0;
const repair =
failed.length === 0
? undefined
: neverTrusted.length === failed.length
? `${failed.length} hook(s) have no trust entry in ${join(codexHome, "config.toml")}; only Codex itself writes those on hook approval. Approve this plugin's hooks in Codex, or record them explicitly with: cxc hooks retrust --key ${pluginKey} --codex-home ${codexHome} --bootstrap-ok`
: `cxc hooks retrust --key ${pluginKey} --codex-home ${codexHome}`;
const failureDetail = failed
.map(
(result) =>
`${result.status} ${result.key} expected=${result.hash} actual=${result.actual ?? "(none)"} file_sha256=${result.fileSha256.slice(0, 16)}`,
)
.join("; ");
return {
name: "hook-trust",
// An EMPTY result set is not a pass. `diagnoseHookTrust` skips a handler it
// cannot hash (invalid matcher, empty command, async), so "0 failed" can also
// mean "0 examined" — a green check over hooks nobody verified.
severity: results.length === 0 ? "WARN" : failed.length === 0 ? "PASS" : "FAIL",
severity:
results.length === 0 ? "WARN" : failed.length === 0 ? "PASS" : driftedOnly ? "WARN" : "FAIL",
repair,
evidence:
results.length === 0
? `no hook handler could be hashed for ${pluginKey}; nothing was verified`
: failed.length === 0
? `${results.length} hook hash(es) trusted for ${pluginKey}`
: failed
.map((result) => `${result.status} ${result.key} expected=${result.hash} actual=${result.actual ?? "(none)"}`)
.join("; "),
: driftedOnly
? `trusted_hash drift (reinstall updates it; hooks still run): ${failureDetail}`
: failureDetail,
};
} catch (error) {
return { name: "hook-trust", severity: "FAIL", evidence: error instanceof Error ? error.message : String(error) };
Expand Down
11 changes: 10 additions & 1 deletion plugins/codexclaw/components/cxc-ops/src/hook-trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export interface HookHandler {
export interface HookEntry {
key: string;
hash: string;
/**
* sha256 of the hook JSON file's bytes. EVIDENCE ONLY — trust is decided by
* `hash` (the canonical event+matcher+handler identity), and this digest is
* expected to differ from a recorded trusted_hash even on a healthy install.
*/
fileSha256: string;
}

export interface HookTrustResult extends HookEntry {
Expand Down Expand Up @@ -166,7 +172,9 @@ export function listHookEntries(pluginRoot: string, pluginKey: string): HookEntr
if (typeof hookRef !== "string") throw new Error("plugin manifest hook references must be strings");
const relativePath = normalizeHookPath(hookRef);
assertSafeHeaderValue(relativePath, "hook path");
const document = JSON.parse(readFileSync(containedPluginFile(pluginRoot, relativePath), "utf8")) as {
const raw = readFileSync(containedPluginFile(pluginRoot, relativePath));
const fileSha256 = createHash("sha256").update(raw).digest("hex");
const document = JSON.parse(raw.toString("utf8")) as {
hooks?: Record<string, unknown>;
};
for (const [rawEventName, rawGroups] of Object.entries(document.hooks ?? {})) {
Expand Down Expand Up @@ -197,6 +205,7 @@ export function listHookEntries(pluginRoot: string, pluginKey: string): HookEntr
entries.push({
key: `${pluginKey}:${relativePath}:${EVENT_LABELS[eventName]}:${groupIdx}:${handlerIdx}`,
hash: identityHash(eventName, group.matcher as string | undefined, handler),
fileSha256,
});
}
}
Expand Down
Loading
Loading