Skip to content

[feat·M2] Append-only rationale refresh anchored to a position cycle #403

Description

@atomchung

Status

Owner-approved active implementation front, 2026-07-31. May proceed in parallel with #610.

#610 remains the frozen M1 judgment-quality dogfood lane. This issue is a separate M2 memory vertical slice: continuing-position self-report, not TradeEvaluation answer quality. Development may proceed on isolated branches while #610 runs against its frozen SHA. Do not merge a consider consumer into the #610 candidate mid-walk.

Milestone

M2 — Recall and Compare My Reasons.

This is a narrow owner-approved exception to #475's earlier sequencing gate. It does not unlock broad M2: #636 DecisionIntent, #490 execution linkage, #609 prior-evaluation recall, profile distillation and rule learning remain separately gated.

User outcome

A user can state or update why they currently hold a recorded position without running a review:

「我現在繼續持有 X,是因為 Y。」

FOMO Kernel records the exact words locally, returns a short receipt, and the next relevant review starts from that statement rather than asking from zero.

The same canonical writer is used when the statement arrives through:

  1. direct chat / standalone CLI entry;
  2. a review rationale-refresh question;
  3. the existing light-capture route.

A review is a reader and selective collector, not the aggregate root of the rationale.

Current behavior / evidence

Verified on main@9d56a2d:

Domain decision

1. One object: PositionRationale

This issue owns why the user says they currently continue to hold one active position cycle.

It does not own:

Changed wording is evidence, not automatically drift, dishonesty or rationalization.

2. One canonical stream

Add a dedicated canonical append-only stream:

<root>/position_rationales.jsonl

with one schema and one module owning validation, append, fold and bounded query. theses.jsonl remains a rebuildable thesis projection / agent interpretation source and is not the canonical home of direct user wording.

Register the stream with data-status, export and reset. Corruption, conflicting duplicate identity and partial writes fail closed. Append uses the existing root write lock plus flush/fsync discipline; no second lock hierarchy.

3. Event contract

The first schema carries at least:

  • schema_version;
  • engine-assigned event_id, content-addressed from the stable payload and excluding recorded_at;
  • subject: active cycle_id, ticker, market and currency facts resolved by the engine;
  • change: initial | changed | no_change;
  • exact user_statement, byte-for-byte for initial / changed;
  • continues_event_id for no_change, so the prior substantive wording remains the effective statement without being copied or rewritten;
  • stated_at for when the statement applies and canonical recorded_at for append time;
  • voice: user_verbatim;
  • capture_source: direct | review | light_capture plus the producing interaction/session id when one exists;
  • the recorded-book state_version used to resolve the active subject;
  • mapping_status: mapped | unmapped and an optional condition reference when a later slice legally creates one.

A first statement is initial, never falsely labelled changed. Exact retry is a no-op; different wording or a later stated_at creates a new event on the same subject.

4. Direct public action

Use one explicit host-neutral facade action:

review.py record-rationale --ticker <TICKER> --statement <TEXT> [--stated-at <DATE>]

The host normally hides the CLI and invokes it from natural language.

Route rules:

  1. Resolve the ticker against the canonical recorded book under the write lock.
  2. Exactly one active cycle → append.
  3. No active position → append nothing. A live contemplated trade belongs to [feat·M2] DecisionIntent memory — automatically record a live trade intent, then resolve it from execution evidence or one follow-up #636; otherwise return a controlled holding-specific error.
  4. Ambiguous active subject or changed book/state version → fail closed and retry; never attach words to a guessed cycle.
  5. No card, Review Plan, commitment, rule choice or second save-confirmation.

5. Review and light-capture collection

A user answer to a rationale-refresh question is independently valid memory. The review invokes the same append service and records the returned event_id; it does not maintain a second session-owned rationale truth.

If the review later aborts, the rationale remains valid: the user explicitly stated why they hold the position, and that fact does not depend on the card finalizing.

The existing light capture UX may remain, but its writer delegates to this service. Any thesis projection it still needs is rebuilt or derived from the canonical event; direct appending to a rebuildable projection is retired.

Bounded reader contract (#450 handshake)

The first query is position-specific and bounded:

  • earliest eligible event;
  • latest eligible event;
  • up to six most recent remaining events;
  • total_count, shown_count, beyond_cap;
  • effective current statement plus latest confirmation/change event;
  • verbatim text, source/voice, stated_at, recorded_at, event id and subject identity.

The reader follows engine-proven cycle relinks/carry-forward aliases. It never joins by ticker alone across an unproven sale/rebuy boundary.

Review integration

Add a bounded rationale_refresh opportunity/question rather than turning every missing rationale into a questionnaire.

Eligibility requires an existing active rationale plus a fact that could make a new answer useful, for example:

  • material new trade in the cycle;
  • due/crossed condition or horizon;
  • meaningful position change after a stale statement;
  • behavior/self-report conflict evidence.

The engine supplies those facts and how long since the last statement. The agent chooses whether this question deserves the one available slot. At most one rationale-refresh question per review; the global route question budget is unchanged.

A no_change answer references the prior substantive event. A changed answer writes new exact wording. The question quotes the prior statement; it never asks as if nothing was recorded.

The first user-visible reader is the next review question/context. Do not add an unconditional card section in this slice.

Ordered implementation

Front A — stable subject continuity (#539 / #536)

Create one shared engine-side carry-forward/relink primitive used by both refresh-lane and review-lane snapshot adoption. A continuously held position preserves its cycle lineage; a confirmed sale and later re-entry opens a new one. Rationale queries consume the same relink chain. #563's later-history start-date upgrade must preserve aliases rather than orphan memory.

This front touches snapshot/ledger identity and has its own PR/review boundary.

Front B — canonical event and bounded query

In parallel with Front A, on disjoint files:

No review.py integration yet.

Integration slice — direct entry + next-review reader

After Front A and Front B stabilize:

  • add record-rationale through review.py;
  • stamp/verify state_version;
  • expose bounded current rationale in Review Plan state;
  • add one rationale_refresh opportunity/question and duplicate-question suppression;
  • prove direct entry changes the next review visibly.

This is the first owner-live stop boundary.

Follow-up slices, not in the first PR

  1. Migrate the light capture and full-review capture paths to the same writer.
  2. Optional condition mapping. Preserve any supplied invalidation wording first; do not claim monitoring until a canonical condition link exists.
  3. Same-ticker consider reader after [QA·dogfood] Judgment-quality dogfood — does a real consider answer now carry a supported insight? #610. It must freeze the exact rationale event used and settle whether that reference participates in evaluation_id; do not slip it in as display-only mutable context.
  4. Behavior/self-report verdict and profile consumption under [design·P1] The distillation layer: bounded profile memory over the append-only ledger (scoping) #446.

Acceptance criteria — first vertical slice

  • An active position accepts one direct rationale without a Review Plan or card.
  • Exact words round-trip byte-for-byte; first / changed / no-change semantics are distinct.
  • Exact retry is a no-op; conflicting duplicates fail closed.
  • No-position, ambiguous-position and stale-state cases append nothing.
  • Direct, review and light routes have one canonical writer; no parallel theses.jsonl truth.
  • The next review reads the event, quotes it when relevant and does not ask the same fact from zero.
  • A continuously held snapshot position retains the memory across adoption; sold/rebought does not inherit it.
  • The bounded reader reports truncation honestly.
  • Status/export/reset cover the stream; corruption and interruption preserve prior canonical bytes.
  • Review/card/TradeEvaluation arithmetic and current [QA·dogfood] Judgment-quality dogfood — does a real consider answer now carry a supported insight? #610 behavior remain unchanged.
  • Public fixtures and evidence are fictional.

Non-goals

  • No generic add-memory command or universal memory graph.
  • No mandatory falsifier for every rationale.
  • No automatic semantic drift/accusation.
  • No price target, recommendation or execution claim.
  • No consider integration in the first vertical slice.
  • No profile label or standing-rule implementation.
  • No migration that upgrades legacy classifications into user-verbatim statements.

Decisions still open

No owner decision blocks the first vertical slice above. The design deliberately resolves command shape, canonical source, first reader, snapshot-continuity prerequisite and first-statement semantics.

Later slices still require separate decisions/evidence:

  1. whether an auto-loaded rationale reference changes TradeEvaluation.evaluation_id or remains a non-identity display context — decide before the consider consumer;
  2. how a direct rationale and a direct condition append become atomically/recoverably linked without creating two canonical truths;
  3. when repeated rationale/behavior mismatch earns a [design·P1] The distillation layer: bounded profile memory over the append-only ledger (scoping) #446 verdict rather than merely a question;
  4. [design·P1] A rule is the user's own standing expectation, not a review output — the loop, and what currently forces it through one door #650's independent standing-rule lifecycle and adherence accumulation.

Dependencies / relations

Privacy / rollback

All real statements remain local under the user's coach root. Public tests/issues/receipts use fictional wording and instruments only.

Rollback before integration removes the new reader/writer without mutating ledger, sessions or TradeEvaluations. After events exist, rollback must leave the canonical stream exportable/readable even if no runtime surface consumes it; never delete user statements as part of code rollback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions