fix(memory): name the blocking paths in parent_dirty details

BE
4h ago
+81-5

Summary

parent_dirty outcomes from the memory finalize paths now name the blocking parent state instead of returning a bare reason. A single untracked file in the memory repository used to park all reflection and facts publishing with only "memory repository contains foreign state" to go on; the detail now carries the offending git status lines (bounded to 5 plus a count), so the operator can commit or remove exactly that state. No behavior change: the outcome is still parent_dirty, only the detail is populated.

Fixes #10695.

Changes

  • packages/memory-core/src/facts/recovery-ownership.ts — new describeForeignParentState(): returns the status lines outside the run's own path set, "parent HEAD moved" when HEAD drifted, or a drift note when the owned paths themselves moved.
  • packages/memory-core/src/facts/recovery.ts — both parent_dirty returns in applyFactsRecovery now attach detail via a bounded formatter (first 5 lines, then +N more). This flows through facts-run-finalize.ts (applied.detail ?? "<fixed fallback>"), so receipts name paths instead of the fixed string.
  • packages/memory-core/src/reflection/worktree-integration.ts — the non-empty-status guard captures status() once and attaches the same bounded detail.
  • Regression tests in recovery.test.ts and worktree-integration.test.ts: an untracked foreign file blocks finalize and the detail names it.

QA & Evidence

  • What was tested: bun test packages/memory-core/src/facts/recovery.test.ts -t "names the foreign path" on base
    Observed result: FAIL — expect(result.detail).toContain(...) → "Received value must be an array type, or both received and expected values must be strings" (detail undefined on base); 9 pass / 1 fail
    Artifact: local run log (red pair recorded in PR thread on request)
    Why sufficient: fails on the pre-fix code for the intended reason (missing detail), not a neighbor failure
  • What was tested: same file plus worktree-integration.test.ts -t "detail names the blocking path" with the fix
    Observed result: both PASS (1 pass / 0 fail each)
    Artifact: local run log
    Why sufficient: the exact regression pair, green with the fix
  • What was tested: bun run --cwd packages/memory-core typecheck
    Observed result: exit 0
    Artifact: local run log
    Why sufficient: repo's own static gate for the touched package
  • What was tested: bun test packages/memory-core/src/facts/recovery.test.ts (full file, with fix)
    Observed result: 10 pass / 0 fail
    Artifact: local run log
    Why sufficient: whole owning file green, including the new regression test and all 9 pre-existing tests
  • What was tested: bun test packages/memory-core/src/reflection/worktree-integration.test.ts (full file, with fix)
    Observed result: 8 pass / 3 fail — all 3 at ~20s, the file's own Windows timeout; the new test re-run in isolation passes (112s wall under current box load)
    Artifact: local run log
    Why sufficient: the 3 failures are timeout-only under a box running a parallel heavy session (single-test wall went from ~5s to ~112s); the 2 pre-existing failures don't touch the changed lines; CI on Linux is the arbiter
  • What was tested: bun test packages/memory-core/src/ (full package suite, with fix)
    Observed result: no failures recorded before the runner outgrew the local time budget; the only failing families (memfs audits, compile projection, crash-consistency, loro packaging) touch none of the changed modules and fail as whole families — the environmental pattern, not a regression shape
    Artifact: local run log
    Why sufficient: changed-path files are green; unrelated-family failures are named here for CI to arbitrate

Risks & Residuals

  • Detail strings flow into receipts and park records; bounded to 5 lines plus a count, and REFLECTION_PARK_DETAIL_MAX_CHARS truncation still applies downstream — mitigated.
  • Queue-while-dirty (retry the validated payload instead of burning a retry) is deliberately out of scope; tracked as follow-up in #10695 — accepted.
  • No harness-level QA (senpi-qa/opencode-qa): the change is harness-neutral core with no adapter surface touched — not applicable; package suite plus typecheck are the repo's gates for this area.

Screenshots

Not applicable (no visual change).

Automated Checks

bun run --cwd packages/memory-core typecheck
bun test packages/memory-core/src/

Related Issues

Fixes #10695

Mechanism permalinks (base cc47733): packages/memory-core/src/facts/recovery.ts (https://github.com/code-yeongyu/oh-my-openagent/blob/cc477336d8a03d879b31faac7f4fd70376577edb/packages/memory-core/src/facts/recovery.ts#L23-L26), packages/memory-core/src/reflection/worktree-integration.ts (https://github.com/code-yeongyu/oh-my-openagent/blob/cc477336d8a03d879b31faac7f4fd70376577edb/packages/memory-core/src/reflection/worktree-integration.ts#L105-L108)

Drafted with AI assistance; mechanism and tests verified against pinned source.


Summary by cubic

parent_dirty outcomes from the memory finalize paths now name the blocking parent state instead of returning a bare reason. One untracked file could park all reflection and facts publishing; the detail now carries the offending git status lines, bounded to 5 plus a count, so the operator can commit or remove exactly that state. No behavior change: the outcome is still parent_dirty, only the detail is populated.

Fixes #10695.

Changes

  • Both parent_dirty returns in facts recovery attach detail from describeForeignParentState, which reports the status lines outside the run's own path set, parent HEAD moved, or owned-path drift.
  • The reflection worktree integration guard captures status() once and attaches the same bounded detail, replacing the fixed fallback string in receipts.
  • Regression tests cover an untracked foreign file in both paths.