Summary
Issue #128 ("Making PAI Fully Extensible for Forking Without Core Modifications", completed) brought a USER/CUSTOMIZATIONS overlay to skills, agents, and Arbol, so a fork or user can extend those subsystems without editing regenerated SYSTEM files. That overlay never reached the memory subsystem. Adding a custom memory type or a Tier-C proposal target today still requires editing LIFEOS/TOOLS/MemoryTypes.ts, which is a generated file, so the edit is clobbered on the next release and produces a merge conflict on every pull.
This proposes closing that one remaining gap the same way #128 closed it elsewhere: source registry entries from a USER/CUSTOMIZATIONS overlay that is merged in before Object.freeze, preserving the frozen-at-load, review-and-commit determinism exactly as it is today. This is not a request to unfreeze the registry or to add runtime type registration.
The gap, verified against the current release
MemoryTypes.ts describes itself as a deliberately closed registry:
"MemoryTypes — frozen type registry for LifeOS's unified memory subsystem. Extensible by adding a registry entry, no other code changes required. The registry is Object.freezed at module load (ISC-160). A new type means appending a registry entry, reviewing the change, committing, never a runtime mutation."
The determinism there is a feature, and this proposal keeps it. The problem is only the location of the entries. Every type path, and every PROPOSAL_KIND_TO_FILES target, is a hardcoded SYSTEM constant in that generated file. There is no USER-space overlay: MemoryTypes.ts performs no directory read, no customization merge, nothing (confirmed against the current release source). Contrast LoadSkillConfig.ts in the same TOOLS/ directory, which already merges a USER/SKILLCUSTOMIZATIONS overlay at load time, and the optional USER/CUSTOMIZATIONS/TOOLS/ tool loading in the Pulse observability module. Skills and tools can be extended from USER space; memory types cannot.
MutationTier.ts has the same shape: a closed, default-deny allowlist that is "hard-coded in this file (ISC-106), there is no config-file override path." Promoting a new file to a writable tier is therefore also a core-file edit.
Why this is worth doing, and why it is not #845
A fork maintaining custom memory types is exactly the "extend without core modification" case #128 was opened to solve, applied to the one subsystem its solution did not cover. It is not the structural reorganization declined in #845 ("Namespace Prefixing"). Nothing is renamed or moved. The registry mechanism, the freeze, the tier allowlist, and the review-and-commit flow all stay identical. Only the source of entries widens from "SYSTEM constants" to "SYSTEM constants plus a USER overlay merged before the freeze."
Proposed direction (the maintainer owns the final shape)
- At module load,
MemoryTypes.ts reads an optional overlay directory under USER/CUSTOMIZATIONS/ (guarded by existsSync, the same pattern LoadSkillConfig.ts uses), merges those entries into the registry object, then Object.freezes the combined result. Load-time, not runtime. Review-and-commit unchanged. ISC-160 intact.
- Overlay entries are validated on load and rejected if they collide with a core type key, so an overlay can add a type but never re-point
memory, proposal, or any existing type.
ALL_TYPES derives from the keys of the merged object rather than a hardcoded literal, which is the only other change; resolveStoragePath and isKnownType are already generic over the type key.
One safety point worth building in from the start
The registry is a tamper boundary, and it should stay one. A naive overlay that let entries ship their own storage_path_resolver function would turn a data boundary into a code-loading boundary: an overlay file would be arbitrary code executed in every process that imports the registry. The safe shape is a declarative overlay entry (a path template plus tier and write_mode over a fixed schema), interpreted by the core, never a loaded function.
The tier check in MemorySystem.add() should also be closed for completeness. It currently skips getTier when write_mode === "queue", on the reasoning that a queue write lands in a holding-file whose real target is applied later. That is safe for the current closed set of queue types, but if entries can come from an overlay, a queue-mode entry would evade the tier gate entirely. The fix is to validate the declared tier against getTier of the entry's resolved target at load, and to keep MutationTier.getTier unchanged as the default-deny backstop, so an overlay entry can only ever resolve into Tier A/B/C if that path is already allowlisted in MutationTier.ts.
What would help decide
Is a USER-overlay for the memory and tier registries something you would take, framed this way (overlay-before-freeze, declarative entries, determinism unchanged)? If so I am happy to put up a reference implementation against the current release for you to reshape or reimplement privately as you prefer. If the intended answer is "fork the registry," that is a fine answer too and worth stating in the docs next to the existing "append a registry entry" guidance, since that guidance currently reads as if it were conflict-free when for a generated file it is not.
Summary
Issue #128 ("Making PAI Fully Extensible for Forking Without Core Modifications", completed) brought a
USER/CUSTOMIZATIONSoverlay to skills, agents, and Arbol, so a fork or user can extend those subsystems without editing regenerated SYSTEM files. That overlay never reached the memory subsystem. Adding a custom memory type or a Tier-C proposal target today still requires editingLIFEOS/TOOLS/MemoryTypes.ts, which is a generated file, so the edit is clobbered on the next release and produces a merge conflict on every pull.This proposes closing that one remaining gap the same way #128 closed it elsewhere: source registry entries from a
USER/CUSTOMIZATIONSoverlay that is merged in beforeObject.freeze, preserving the frozen-at-load, review-and-commit determinism exactly as it is today. This is not a request to unfreeze the registry or to add runtime type registration.The gap, verified against the current release
MemoryTypes.tsdescribes itself as a deliberately closed registry:The determinism there is a feature, and this proposal keeps it. The problem is only the location of the entries. Every type path, and every
PROPOSAL_KIND_TO_FILEStarget, is a hardcoded SYSTEM constant in that generated file. There is no USER-space overlay:MemoryTypes.tsperforms no directory read, no customization merge, nothing (confirmed against the current release source). ContrastLoadSkillConfig.tsin the sameTOOLS/directory, which already merges aUSER/SKILLCUSTOMIZATIONSoverlay at load time, and the optionalUSER/CUSTOMIZATIONS/TOOLS/tool loading in the Pulse observability module. Skills and tools can be extended from USER space; memory types cannot.MutationTier.tshas the same shape: a closed, default-deny allowlist that is "hard-coded in this file (ISC-106), there is no config-file override path." Promoting a new file to a writable tier is therefore also a core-file edit.Why this is worth doing, and why it is not #845
A fork maintaining custom memory types is exactly the "extend without core modification" case #128 was opened to solve, applied to the one subsystem its solution did not cover. It is not the structural reorganization declined in #845 ("Namespace Prefixing"). Nothing is renamed or moved. The registry mechanism, the freeze, the tier allowlist, and the review-and-commit flow all stay identical. Only the source of entries widens from "SYSTEM constants" to "SYSTEM constants plus a USER overlay merged before the freeze."
Proposed direction (the maintainer owns the final shape)
MemoryTypes.tsreads an optional overlay directory underUSER/CUSTOMIZATIONS/(guarded byexistsSync, the same patternLoadSkillConfig.tsuses), merges those entries into the registry object, thenObject.freezes the combined result. Load-time, not runtime. Review-and-commit unchanged. ISC-160 intact.memory,proposal, or any existing type.ALL_TYPESderives from the keys of the merged object rather than a hardcoded literal, which is the only other change;resolveStoragePathandisKnownTypeare already generic over the type key.One safety point worth building in from the start
The registry is a tamper boundary, and it should stay one. A naive overlay that let entries ship their own
storage_path_resolverfunction would turn a data boundary into a code-loading boundary: an overlay file would be arbitrary code executed in every process that imports the registry. The safe shape is a declarative overlay entry (a path template plustierandwrite_modeover a fixed schema), interpreted by the core, never a loaded function.The tier check in
MemorySystem.add()should also be closed for completeness. It currently skipsgetTierwhenwrite_mode === "queue", on the reasoning that a queue write lands in a holding-file whose real target is applied later. That is safe for the current closed set of queue types, but if entries can come from an overlay, a queue-mode entry would evade the tier gate entirely. The fix is to validate the declared tier againstgetTierof the entry's resolved target at load, and to keepMutationTier.getTierunchanged as the default-deny backstop, so an overlay entry can only ever resolve into Tier A/B/C if that path is already allowlisted inMutationTier.ts.What would help decide
Is a USER-overlay for the memory and tier registries something you would take, framed this way (overlay-before-freeze, declarative entries, determinism unchanged)? If so I am happy to put up a reference implementation against the current release for you to reshape or reimplement privately as you prefer. If the intended answer is "fork the registry," that is a fine answer too and worth stating in the docs next to the existing "append a registry entry" guidance, since that guidance currently reads as if it were conflict-free when for a generated file it is not.