Telegram Source Layout and Contract Boundaries
The first Telegram listener decision is the boundary between source code and org runtime state.
Thesis
The first production decision in the Telegram listener was not how to poll Telegram. It was where the code belongs, where runtime data belongs, and how to keep those two surfaces from pretending to be the same thing.
Telegram creates a naming trap. There is Telegram implementation code, and
there is an org's Telegram runtime state. Both naturally want to be called
channels/telegram. If those are allowed to blur, the codebase becomes harder
to package, harder to test, and harder to explain to an operator inspecting an
org folder. The source-layout work solved that boundary before adding any network
loop.
The Two Telegram Roots
The Python implementation root is:
src/pkgs/core/channels/telegram/
The org runtime root is:
<org-root>/.matic/channels/telegram/
Those paths look similar because they describe the same product concept, but
they have different jobs. The src/pkgs/... path is importable source code. It
contains modules such as client.py, server.py, state.py, lease.py,
paths.py, and cli.py. The .matic/... path is durable state. It contains
operator-facing files and folders such as spec.md, outbox/, inbox/,
sent/, failed/, and listener/.
This split follows the current Python source convention from ADR 0022:
implementation packages live under src/pkgs/; org runtime data stays in the
org filesystem. The older repo-root source-layout exception is no longer the
active strategy.
Why Layout Is Behavior
Source layout can look like housekeeping, but in a filesystem-first system it is part of the behavior contract. Matic is built around readable files. That means a user should be able to open an org folder and understand what happened without also needing to know Python import rules.
The Telegram listener needs both kinds of files:
- Python modules that implement sending, listening, leases, state, and intake.
- Runtime records that show what the listener did inside a specific org.
If implementation code were mixed into the runtime data path, an operator could not tell whether a file is part of the product or part of one org's history. If runtime files were treated as Python source, tests and packaging would inherit accidental behavior from a local org checkout. The source-layout work made that impossible by keeping the roots explicit.
The Contract Before The Loop
The source-layout work also defined the filesystem contract that later runtime features build on:
<org-root>/
.matic/
channels/
telegram/
spec.md
outbox/
inbox/
sent/
failed/
listener/
state.yaml
history.md
lease.yaml
That contract matters before a listener polls a single update. state.yaml
describes the listener lifecycle. lease.yaml prevents duplicate starts.
history.md records lifecycle events. inbox/ is reserved for inbound records.
The outbound folders keep the existing send behavior intact.
The implementation centralizes these paths in constants and exposes them
through TelegramFilesystemPaths. Tests assert that source paths and runtime
paths resolve to the expected places. That gives later work a stable surface:
the foreground-shell work can add a foreground shell without redefining the folder
layout, and the intake work can add inbox records without changing the listener
contract.
Compatibility Without Drift
The Telegram sender already existed before the listener sprint. The source-layout work preserved that behavior while moving the Telegram-specific implementation into the active source package. Existing channel commands still send outbound messages through the Telegram sender, and the generic channel service still reports the channel folders.
The important point is that the listener did not become a special case outside the channel model. Telegram got its own package because Telegram has Telegram-specific protocol code. It did not get a separate runtime data model. The org still sees a channel folder with inbox, outbox, sent, failed, and listener state.
Why This Was The Right First Slice
It is tempting to start a listener by writing the loop. That would have been the wrong first move. A loop without a durable contract only proves that a process can run. It does not prove that the work can be inspected, stopped, restarted, packaged, or reviewed.
The source-layout work established the pieces that make the later loop meaningful:
- a source package under
src/pkgs/core/channels/telegram/, - a runtime data surface under
.matic/channels/telegram/, - explicit path constants,
- tests for the filesystem contract,
- and a CLI command shape for
channels telegram listenandclose.
That is not cosmetic structure. It is the foundation that lets the listener become operational software instead of a script with side effects.
What Comes Next
The lifecycle work builds on this boundary by turning the planned listener state into an actual lifecycle contract. The next question is not "can we call Telegram?" It is "can an operator start and stop the listener, inspect the files, and trust that duplicate starts are blocked?"