---
{
  "n": 38,
  "title": "The journal core, its features, and how they talk",
  "abstract": "What is core plumbing, what is a feature, and the one way both reach resources, events and the chat",
  "refs": [
    "doc:37"
  ],
  "seen": [
    "agent",
    "user"
  ],
  "data": {
    "kept": false,
    "revisions": 1,
    "open_until": 1790095188.157923
  },
  "created": 1790028254.2814488,
  "updated": 1790093388.158331,
  "deleted": 0.0,
  "completed": 0.0,
  "outcome": "",
  "type": "doc"
}
---
The living design that replaces doc 18. It is rewritten in place as decisions land; every earlier revision stays readable in its revision strip.

## Core and features
The core is plumbing: the record, the bus, the engine, the hooks, and the journal client that talks to the viewer. A feature is behaviour on top of it: it listens, decides and acts, and can be switched off without anything else breaking.

Anything that has to happen for the journal to work at all is core, never a feature. Showing what the agent wrote in the chat is core (message 1350). Making a message answerable, tagging, reminding, planning are features.

## The journal is the entry point
Features, the client and plugins reach every resource through the journal, never by making a controller themselves (question 52):

    journal.messages.create("…", brief="…")
    journal.todos.done(12, how="…")

journal.<type> is that type's controller, already bound to the record and the actor. The controllers stay what they are, the one funnel per resource that also backs every journal command, so everything can still reference everything and the commands keep working. What goes is the ceremony: Todos(record, actor=SYSTEM) in feature code.

## A feature describes itself and registers its parts
A feature is two small files and its parts (messages 1338, 1342):

- details.py holds a FeatureDetails class: name, title, abstract, help, lines and behaviours.
- feature.py holds the feature class: details = TagsDetails and one register(journal) that hands over each part.

    class Tags(Feature):
        details = TagsDetails

        def register(self, journal: Journal) -> None:
            journal.events.handler(RunTagCommands())
            journal.client.formatter(StripTags())
            journal.agent.interceptor(NotifyTagNotUsed())

Every part is its own class with its own logic, in handlers.py, formatters.py or interceptors.py beside the feature:
- a Handler handles one typed event,
- a TextFormatter changes text a person reads,
- a ToolInterceptor looks at the agent's tool calls before they run.

Handlers are classes like the rest (question 49); the registries have short names (question 50). @event, @formats and @interceptor still work for features not moved yet.

## Context
Every part receives a context:
- context.record: the environment's record.
- context.settings: the feature's own settings for that environment.
- context.agent: the agent the event concerns, or None; it speaks for itself with say, whisper and type.
- context.on(behaviour) and context.once(kind, key) for switches and once-only work.

Once the entry point exists, context.journal gives a part the same journal.<type> access as everyone else.

## Typed events
Each event is a small frozen dataclass with named fields, in engine/events.py. A handler subscribes through the type of its event parameter, so there is no event string in feature code:

    class RunTagCommands(Handler):
        def handle(self, context: Context, event: AgentMessageCreated) -> None: ...

Names follow <thing>.<past-tense verb>, and the class says the same. agent.said becomes agent.message.created. agent.updated, which dozens of handlers listen to and then ask which hook fired, splits into one event per hook: agent.session.started, agent.prompt.submitted, agent.tool.started, agent.tool.finished, agent.turn.stopped, agent.context.compacting, agent.session.ended, agent.permission.requested.

## The chat
What the agent writes reaches the chat because the core sends it, not because a feature copies it (message 1350):

    agent text arrives (display hook, transcript, Stop hook)
      -> journal.client receives it
      -> dispatches agent.message.sending: listeners may change it or stop it
           Tags runs [!reply:n], [!log:n] … and takes the tag off
           Messages saves it through journal.messages, so it can be read and answered later
      -> journal.client sends it to the viewer over its stream
      -> dispatches agent.message.sent

The client never knows the messages resource. Tags' CopyToChat goes when this lands.

Everything the agent writes is a plain message; only the command tags remain: reply, log, end, todo and fact (question 51). A log turn is a message and a work log entry at once.

## Names and types
- Everything in Python is snake_case (message 1339); classes are CamelCase.
- An attribute is named for what it holds, never as prose (message 1360): says becomes status_labels, shown becomes event_labels, names becomes command_names, handed becomes start_heading.
- Every class attribute carries a type annotation (message 1365).
- A resource type describes itself in a details class with plain title, abstract and help, as features now do (message 1355); the trailing-underscore names go.

## Where it stands
Done (plan 9, 2.41.0 to 2.53.1, committed locally):
- Every feature is details.py and a feature.py that only registers its parts; the old @event, @formats, @interceptor, @command and @handles are gone.
- Parts reach resources through journal.<type> (context.journal.todos...), read declared settings as context.settings.<name>, and say which behaviour they belong to; the journal calls them only when that behaviour is on and due.
- Commands are Command classes; an ActionInterceptor sits in front of a controller action.
- The agent's words reach the chat through engine/chat.said: agent.message.sending, then agent.message.sent.
- Resource types describe themselves with ResourceDetails and list their fields.
- Check 6 fails when a feature drifts from this shape.
- Before plan 9: Tags on parts, command tags only, Designs, readable details.py, plain attribute names, plan mode refused.

Next:
1. Feature renames as agreed in report 26 (to-do 623).
2. Triggers as objects, not dictionaries (to-do 622).
3. Every tool call waits until a required skill is loaded (to-do 613).
4. One typed event per hook (to-do 624).

## Open questions
- Does journal.client stream the agent's words to the chat as they arrive, with the saved rows only for history on a page load, or does the chat keep reading the saved rows?
- Does a plugin get the same journal.<type> entry point, over HTTP, or a narrower one?

## Proposals for the conversion
Small, comprehensible pieces, in the spirit of details.py and registered parts (message 1418). Each is optional; pick the ones the conversion plan should carry.

1. Settings declared like lines. details.py lists them: Setting(name="keep_after_minutes", default=30, title="Keep an open revision after", unit="minutes"). Parts read them as context.settings.keep_after_minutes. The viewer draws every feature's settings from that list, so FeaturePanel loses its per-feature blocks.

2. Cadence on the part, not in its body. A handler says how often it may speak, e.g. every = Cadence(50, "uses"), and the journal only calls it when it's due. The feature.due(...) calls at the top of dozens of handlers go away.

3. Commands as parts. journal.commands.add("work", ParkWork()) replaces @command, and journal.commands.wrap("todo.start", HoldWhilePlanned()) replaces @handles. A command is a class with its arguments as typed fields, like an event.

4. Shared parts for look-alike features. Facts, rules and reminders share Recital today by inheritance; they'd share two parts instead, WhisperOnKeyword and RepeatStanding, each registered with its own type.

5. Resource types described the same way (to-do 593): a details class per type with plain names, and its fields as a list, Field(name="phases", default=list).

6. A size check. A check row fails when a part file grows past about 80 lines or a feature.py holds logic, so the shape stays without anyone watching it.
