You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The curated-memory compaction arc (epic #426) needs a single, authoritative
classifier deciding which MemoryItems compaction may never drop. Today no
such predicate exists in the memory package: the only load-bearing logic is forge_loop.eventlog.models.is_load_bearing, which operates on EventKind, not
on curated MemoryItems. Without a mirror predicate, the upcoming compaction
step would have to inline ad-hoc kind == .../tag checks, scattering the
"what's sacred" rule across call sites and risking silent erasure of decisions.
Concrete example: a maestro resumes forge-loop after context loss. Its store
holds a SEMANTIC decision, a rejected-path-tagged item, a PROCEDURAL
validated skill, and dozens of EPISODIC per-issue failed/merged episodes.
Compaction should bound the episodic noise but keep all three load-bearing
items. There is currently no is_load_bearing_memory(item) to consult, so the
compaction arc (#426) has nothing to call.
Acceptance criteria
A pure function is_load_bearing_memory(item: MemoryItem) -> bool exists
in src/forge_loop/memory/models.py, with no I/O and no store access.
Returns True for MemoryKind.SEMANTIC items (strategic decisions).
Returns True for MemoryKind.PROCEDURAL items (validated skills).
Returns True for any item whose tags contain REJECTED_PATH_TAG,
regardless of kind (a rejected path is sacred even if filed differently).
Returns False for MemoryKind.EPISODIC items that are NOT
rejected-path-tagged (the prunable per-issue failed/merged episodes).
The function has a docstring naming it the single source of truth for the
compaction prune guard, mirroring eventlog.models.is_load_bearing.
is_load_bearing_memory is added to memory/__init__.py's __all__
export list (alphabetical position) and importable as from forge_loop.memory import is_load_bearing_memory.
All existing tests still pass; ruff/type checks clean on touched files.
Test matrix
Unit tests (new file tests/test_memory_load_bearing.py, mirroring tests/test_eventlog_load_bearing.py):
Parametrized table asserting a deliberate verdict for every MemoryKind: SEMANTIC -> True, PROCEDURAL -> True, EPISODIC -> False. Use a dict[MemoryKind, bool] keyed by every enum member and assert set(EXPECTED) == set(MemoryKind) so a future MemoryKind fails CI rather
than silently inheriting a default.
A SEMANTIC decision item returns True.
A PROCEDURAL skill item returns True.
An EPISODIC item tagged ("failed",) returns False.
An EPISODIC item tagged ("merged",) returns False.
Adversarial / sad-path tests:
An EPISODIC item that ALSO carries REJECTED_PATH_TAG returns True
(tag wins over prunable kind — the sacred-path rule must not be defeated by
kind). This is the key adversarial case.
An item with empty tags=() is classified purely by kind.
A rejected-path-tagged item alongside an unrelated tag
((REJECTED_PATH_TAG, "axis:foo")) still returns True.
Integration: assert the public import path from forge_loop.memory import is_load_bearing_memory resolves (guards the __all__ wiring).
No e2e tests required — this is a pure leaf predicate with no runtime wiring
yet (the compaction arc that consumes it lands in a sibling issue under #426).
Do NOT add any I/O, DB access, or MemoryStore calls.
Do NOT change MemoryItem, MemoryKind, or REJECTED_PATH_TAG definitions.
Do NOT touch eventlog.models.is_load_bearing or its tests.
Do NOT introduce a "fail-safe unknown kind" branch — MemoryKind is a closed StrEnum with three members; enumerate them explicitly (a CI guard test
catches any future addition). Keep net size ~40 LOC incl. tests.
Do NOT add config flags, CLI commands, or events.
File pointers
src/forge_loop/memory/models.py — add is_load_bearing_memory(item) here,
near MemoryKind/REJECTED_PATH_TAG (lines ~13–21). This is the mirror of src/forge_loop/eventlog/models.py:167 (is_load_bearing).
src/forge_loop/memory/__init__.py — import the new symbol from memory.models (the existing import block, lines 7–17) and add it to __all__ (lines 40–54).
tests/test_memory_load_bearing.py — NEW test file; model its structure on tests/test_eventlog_load_bearing.py (the EXPECTED_VERDICTS table + edge
classes pattern).
Part of epic: "Bound curated memory with a load-bearing-preserving compaction arc"
Add a pure predicate in the memory package (mirroring eventlog/models.is_load_bearing) that classifies a MemoryItem as load-bearing vs prunable. Load-bearing = MemoryKind.SEMANTIC (decisions), any item carrying REJECTED_PATH_TAG, and MemoryKind.PROCEDURAL (validated skills). Prunable = MemoryKind.EPISODIC items (the per-issue 'failed'/'merged' episodes). No I/O, no store access — just the classifier the compaction arc will consume. Primary acceptance: a semantic decision, a rejected-path-tagged item, and a procedural skill each return True; an episodic 'failed' item returns False. ~40 net LOC incl. tests.
Customer story
A maestro resuming forge-loop after context loss needs its load-bearing decisions and rejected paths kept; this predicate is the single point that decides what compaction may never drop.
Problem
The curated-memory compaction arc (epic #426) needs a single, authoritative
classifier deciding which
MemoryItems compaction may never drop. Today nosuch predicate exists in the
memorypackage: the only load-bearing logic isforge_loop.eventlog.models.is_load_bearing, which operates onEventKind, noton curated
MemoryItems. Without a mirror predicate, the upcoming compactionstep would have to inline ad-hoc
kind == .../tag checks, scattering the"what's sacred" rule across call sites and risking silent erasure of decisions.
Concrete example: a maestro resumes forge-loop after context loss. Its store
holds a
SEMANTICdecision, arejected-path-tagged item, aPROCEDURALvalidated skill, and dozens of
EPISODICper-issuefailed/mergedepisodes.Compaction should bound the episodic noise but keep all three load-bearing
items. There is currently no
is_load_bearing_memory(item)to consult, so thecompaction arc (#426) has nothing to call.
Acceptance criteria
is_load_bearing_memory(item: MemoryItem) -> boolexistsin
src/forge_loop/memory/models.py, with no I/O and no store access.TrueforMemoryKind.SEMANTICitems (strategic decisions).TrueforMemoryKind.PROCEDURALitems (validated skills).Truefor any item whosetagscontainREJECTED_PATH_TAG,regardless of
kind(a rejected path is sacred even if filed differently).FalseforMemoryKind.EPISODICitems that are NOTrejected-path-tagged (the prunable per-issue
failed/mergedepisodes).compaction prune guard, mirroring
eventlog.models.is_load_bearing.is_load_bearing_memoryis added tomemory/__init__.py's__all__export list (alphabetical position) and importable as
from forge_loop.memory import is_load_bearing_memory.ruff/type checks clean on touched files.Test matrix
Unit tests (new file
tests/test_memory_load_bearing.py, mirroringtests/test_eventlog_load_bearing.py):MemoryKind:SEMANTIC -> True,PROCEDURAL -> True,EPISODIC -> False. Use adict[MemoryKind, bool]keyed by every enum member and assertset(EXPECTED) == set(MemoryKind)so a futureMemoryKindfails CI ratherthan silently inheriting a default.
SEMANTICdecision item returnsTrue.PROCEDURALskill item returnsTrue.EPISODICitem tagged("failed",)returnsFalse.EPISODICitem tagged("merged",)returnsFalse.Adversarial / sad-path tests:
EPISODICitem that ALSO carriesREJECTED_PATH_TAGreturnsTrue(tag wins over prunable kind — the sacred-path rule must not be defeated by
kind). This is the key adversarial case.
tags=()is classified purely bykind.(
(REJECTED_PATH_TAG, "axis:foo")) still returnsTrue.Integration: assert the public import path
from forge_loop.memory import is_load_bearing_memoryresolves (guards the__all__wiring).No e2e tests required — this is a pure leaf predicate with no runtime wiring
yet (the compaction arc that consumes it lands in a sibling issue under #426).
Out of scope
this issue delivers only the classifier (those live elsewhere under Bound curated memory with a load-bearing-preserving compaction arc #426).
MemoryStorecalls.MemoryItem,MemoryKind, orREJECTED_PATH_TAGdefinitions.eventlog.models.is_load_bearingor its tests.MemoryKindis a closedStrEnumwith three members; enumerate them explicitly (a CI guard testcatches any future addition). Keep net size ~40 LOC incl. tests.
File pointers
src/forge_loop/memory/models.py— addis_load_bearing_memory(item)here,near
MemoryKind/REJECTED_PATH_TAG(lines ~13–21). This is the mirror ofsrc/forge_loop/eventlog/models.py:167(is_load_bearing).src/forge_loop/memory/__init__.py— import the new symbol frommemory.models(the existing import block, lines 7–17) and add it to__all__(lines 40–54).tests/test_memory_load_bearing.py— NEW test file; model its structure ontests/test_eventlog_load_bearing.py(theEXPECTED_VERDICTStable + edgeclasses pattern).
Original report
Parent: #426
Part of epic: "Bound curated memory with a load-bearing-preserving compaction arc"
Add a pure predicate in the memory package (mirroring eventlog/models.is_load_bearing) that classifies a MemoryItem as load-bearing vs prunable. Load-bearing = MemoryKind.SEMANTIC (decisions), any item carrying REJECTED_PATH_TAG, and MemoryKind.PROCEDURAL (validated skills). Prunable = MemoryKind.EPISODIC items (the per-issue 'failed'/'merged' episodes). No I/O, no store access — just the classifier the compaction arc will consume. Primary acceptance: a semantic decision, a rejected-path-tagged item, and a procedural skill each return True; an episodic 'failed' item returns False. ~40 net LOC incl. tests.
Customer story
A maestro resuming forge-loop after context loss needs its load-bearing decisions and rejected paths kept; this predicate is the single point that decides what compaction may never drop.