| `onGenerationAccepted` | One signal per acceptance event. | (a) Explicit "Use this" button. (b) Tab/run promotion to the active artifact. (c) Idle timeout after preview opens with no edit. | (a) only. (b) and (c) inflate the confidence ladder by treating non-rejection as approval. | Is there an explicit-accept affordance today, or should we add one before wiring this handler? |
| `onArtifactEditedAndSaved` | One signal per saved diff. | Manual-edit panel save, srcdoc bridge save, finalize-design save. | All three, host coalesces by `(artifactId, session)` window before calling. | Coalesce window. 30s feels conservative; longer windows risk dropping intent shifts within a single edit session. |
| `onExplicitTagApplied` | One signal per tag. | A "tag this" / "I like the spacing here" affordance that does not exist today. | Defer until the affordance exists; do not synthesize tags from chat NLP. | Is the surface area for this UI in scope, or should the handler stay dormant? |
| `onThumbsRated` | One signal per click. | Critique theater rating, comment-mode reaction. | Critique theater first, comment-mode if/when it ships ratings. Host debounces double-click. | Whether thumbs are unary or pairwise (see §4 below). |
| `onGenerationAbandoned` | One signal per abandon. | (a) Explicit discard. (b) Inactivity timeout. (c) New generation started with previous unaccepted. | (a) + (c). (b) is too noisy across multitasking. | Whether (c) requires the previous run to be the active tab, or just any pending one. |
| `onRevertAfterEdit` | One signal per revert. | History panel revert, `git revert` on artifact dir, `Cmd-Z` rapid-undo. | History-panel revert only. The other two are noisy and context-free. | Whether to surface a "revert as feedback" affordance distinct from undo. |
### Cross-cutting
- **Debounce ownership.** Lean: host coalesces and calls the handler once per
with a different invalidation contract (deterministic ranking, decay,
reversal, and the §4 raw-events / content-addressed-derivations contract).
| Option | What it does | Cost | Benefit |
|---|---|---|---|
| A1. Replace | Creative memory subsumes `## Personal memory`. Single block, single pipeline. | Loses the existing block's identity / tone / "things-already-told-you" content, which chat NLP captures and event-based extraction does not. | One layer to reason about. |
| A2. Append | Creative memory body is concatenated into the same `## Personal memory` block. Same precedence rules. | Two extractors writing into one block: no audit trail, no per-source diagnostics, harder to disable one source without the other. | Smallest composer change. |
| A3. Sibling block, shared precedence | New `## Creative memory` block placed adjacent to `## Personal memory`. Each has explicit scope; both follow the same "preferences not rules; brand wins; skill wins" precedence. | One more block in the system prompt; small token cost. | Audit trail preserved (which extractor produced what); §4 contract advantages preserved; either source can be disabled independently. |
Lean: **A3.** The two memories serve different purposes (identity facts vs
stylistic preferences) and have different invalidation models. Mashing them
together loses the §4 raw-events contract advantages (re-derivability,
with shared precedence wording lets the model treat them similarly without
collapsing them.
This is the one explicit decision §2 surfaces that the live composer makes
unavoidable; treating it as implicit is the failure mode this section is
meant to remove.
### Decision B: insertion point
Given A3, the question is where the new `## Creative memory` block sits in
the live order above.
| Option | Where | Effect | Lean |
|---|---|---|---|
| B1. Inside the soft-preferences cluster, after `## Personal memory` | Position 3.5, before custom instructions | Both preference layers are adjacent; custom instructions and the brand contract follow. Coarse-to-fine progression preserved. | **Lean.** Matches the live composer's existing grouping (soft → instructions → brand → craft → skill). |
| B2. After the brand contract, before craft | Position 9.5 / 10 | Brand wins by virtue of preceding the block; memory becomes a brand-aware nudge. | Reasonable if real-world traces show creative memory drifting against the brand. Treat as a fallback. |
| B3. After craft, before the skill | Position 12.5 | Memory becomes a skill-prefix nudge. Weakest influence; easy for opinionated skills to ignore. | Not preferred — buries the signal too late in the stack. |
| B4. As a separate user-role turn | Outside system | Treats memory as a runtime hint, not a system constraint. | Worth A/B testing once integration ships, not the default. |
Lean: **B1**. Placement keeps the two preference layers together; the
existing precedence narrative (brand wins on tokens, skill wins on workflow)
already covers both blocks once they share the precedence wording.
### Decision C: precedence wording
If A3 + B1, the new block needs a header narrative that mirrors `## Personal
memory` so the model treats them with the same hierarchy. Proposed wording:
> ## Creative memory (preferences from generation feedback)
>
> The following stylistic preferences have been derived from this user's
> past acceptances, edits, tags, and rejections in generation runs. Treat
> them as preferences and context, NOT hard rules: when they collide with
> the active design system tokens, the brand wins; when they collide with
> the active skill's workflow, the skill wins. Project-level preferences
> take precedence over global ones whenever both address the same pattern.
This duplicates the `brand wins / skill wins` clause from `## Personal
memory` deliberately — the model treats each block on its own; a single
shared rule split across two blocks would be lost.
| Pause without forgetting | Settings → Memory → Pause | `od memory pause [--until <date>]` | Useful when the user knows they're working in an atypical mode (client work, exploration) without wanting to lose the existing model. |
### Surfaces to consider
- **Diagnostic feed in chat.** The engine emits typed diagnostics on every
| A. Engine package owns raw events | Engine-owned storage alongside `preferences.json` | Engine boundary expands to include event log. Re-derivation runs inside the package. |
| B. Daemon owns raw events | Daemon-managed storage | Engine becomes a pure derivation function over an event slice handed in by the daemon. |
| C. Hybrid — daemon writes, engine reads | Daemon writes through its storage contract; engine reads through a typed accessor | Decouples write path (event capture is a host concern) from derivation (engine concern). |
Lean: **C**. Event capture is intrinsically a host concern — the daemon is
where the pipeline events fire from, where debounce/coalesce happens, and where
sensitivity/redaction policy applies (a chat transcript can carry PII; raw
events should respect redaction before they hit disk). Derivation is
intrinsically a memory-package concern. Splitting the responsibilities along
that axis matches the existing daemon ↔ package boundary in the repo.
### Where does the derivation function version live?
The content-addressed key is `hash(raw_event_set, derivation_fn_version)`. The
version tag has to live somewhere both the writer and reader agree on.
Lean: as an exported constant in `@open-design/creative-memory-system`,
imported by the daemon when computing the cache key. Bumping the version is a
package release. This matches how the package already exports
`schema_version: "1.0"` in `preferences.json`.
### Read-time vs background derivation
Two modes for when re-derivation runs:
| Mode | Trigger | Latency | Cost |
|---|---|---|---|
| Lazy on read | First call to `retrieveForInjection` after raw events change | Adds derivation cost to first call | Small if events are batched, can spike if a session's events are large |
| Background worker | Daemon job after each accepted batch of events | Read is always fast | Constant background cost, even when the user is idle |
Lean: **lazy on read** for MVP, with the structured option to add a background
worker behind a feature flag later. Lazy-on-read with a write-back cache is
the documented path; background re-derivation is a follow-up once derivations
grow heavy enough to dominate p99 latency.
### Pairwise vs unary signal capture
The engine accepts unary signals today (accept / reject / edit / tag). The
contract favors pairwise where the UX can support it.
Pairwise candidate surfaces:
- **Critique theater.** Naturally pairwise — the user is comparing variants.
Lowest-friction place to introduce a `comparison` event type.
- **Tab promotion.** When a user switches the active artifact between two
open generations, the implicit comparison signal is "I prefer this one
right now." High noise, but also high volume.
- **Live-artifact refresh.** When a refresh produces a new variant of a
saved artifact, accepting the new one over the old is pairwise.
Lean: ship pairwise for critique theater first, leave the other two as
follow-ups. Adding pairwise means a new event shape (`{ chose: artifactA, over: artifactB, dimensions?: [...] }`)
and a new handler (`onPairwiseComparison`); the engine's existing handlers
stay valid for unary cases. The weighting policy between pairwise and unary
aggregates lives in `derivation_fn_version` and is auditable per derived
record, per the §11 contract.
### Cache eviction
Content-addressed cache → eviction is by capacity, not by age. Decision: