Attachments and spt fetch
Sending a file with a message does not push bytes to the receiver. The file is snapshot into this node’s own store, registered for serving, and the message carries its URL. The receiver decides whether to pull it, and when.
Attaching a file to a message
$ printf 'the report you asked for' | spt send doyle --attachment ./report.md
ATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)
SENT:doyle
--attachment repeats; each file becomes its own registry entry with its own
URL. The message envelope grows an attachments attribute carrying, per file,
its served name, its URL and its size in bytes — so a receiving agent can decide
whether to fetch before it fetches. A core that has never heard of the
attribute delivers the message unchanged.
An attachment is a snapshot, and that is the one place the serving registry does not resolve at request time:
spt serve add | spt send --attachment | |
|---|---|---|
| What is served | the file at its path | a copy taken at send time |
| Editing the source | the reader sees the edit | the reader still sees what was sent |
| Deleting the source | the URL answers 404 | the URL keeps serving |
| Lifetime | until removed | a TTL, 30 days by default |
A message’s attachment is as immutable as the message: an edit after the send must not change what the receiver pulls, and deleting the original must not turn a delivered link into a dead one.
Two sends of the same filename get two stable, distinct URLs under the
registry’s ordinary disambiguation rule (report.md, then report~1.md), so no
link ever changes meaning.
A missing or unreadable path refuses the send by name — nothing is delivered and nothing is spooled — rather than delivering a message whose link is dead on arrival:
$ printf 'here' | spt send doyle --attachment ./nope.md
ATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)
Lifetime
--ttl sets this send’s lifetime and requires a unit — s, m, h or
d. A bare number is refused rather than guessed, because 30 meaning seconds
when the sender meant days is a deleted attachment.
$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h
The brain’s pulse reaps expired registry entries and logs how many it removed. For an attachment, it attempts to unlink the core-owned snapshot before saving the registry without that entry. These are not one atomic transaction: if saving fails after unlinking, the next pulse can finish removing the expired entry even though its snapshot is already gone. Saving first could instead leave untracked snapshot bytes that later pulses cannot find.
Only attachment snapshots under $SPT_HOME/serve/snapshots/ are candidates for
unlinking. Expiry removes a live file or directory reference from the registry
but never deletes the user’s source. The
writer-coordination contract keeps the
guard across the reaper’s fresh load, snapshot cleanup, and explicit save;
a pass with nothing expired does not save the registry.
An expired entry stops serving the moment it expires, whether or not the reaper has run yet: between expiry and the next tick the bytes may still be on disk, and serving them would make the TTL a suggestion.
Pulling one: spt fetch
$ spt fetch hfenduleam/f/report.md
report.md
$ spt fetch http://localhost:5474/hfenduleam/f/report.md ~/inbox/report.md
/home/reavus/inbox/report.md
It accepts a full node-prefixed URL or the bare <node>/f/<name> shorthand —
the shorthand is how a link is said between agents, and the host and port belong
to whichever node is doing the fetching. The written path is printed on stdout
so a caller can pipe it.
Without a destination the file lands in the current directory under its served
name. An existing destination is not overwritten without --force. The body
streams to a temporary file and is renamed into place, so an interrupted or
refused fetch never leaves a truncated file where a later reader would trust it.
The three outcomes are distinct, and the distinction is the point:
| Exit | Meaning |
|---|---|
0 | The file was written; its path is on stdout. |
3 | The owner refused it — an access decision. Retrying is wrong. |
1 | Anything else: unreachable owner, not found, a deadline, a local write error. |
Every fetch — local or remote — is served through the local daemon’s web surface, the same path a browser takes, so a fetch of your own node’s file exercises the code a remote fetch will use.
The FILE_ACCESS_HELPER signal
An agent does not have to notice an attachment. When a delivered message carries one, the now-signal hands the agent the exact command to run, taken verbatim from the envelope:
<FILE_ACCESS_HELPER>
spt fetch http://localhost:5474/kitsubito/f/report.md
</FILE_ACCESS_HELPER>
The same category answers the other way a file arrives: a user quoting a
filepath while controlling the receiving agent through remote spt rc.
Core binds the existing session-authenticated input report to the broker’s live
remote controller when the report arrives. A later controller change cannot
reattribute it. Registration happens on that controller’s machine, not by looking
for a similarly named file on the receiving machine.
The file is a live reference, not an attachment snapshot: edits are visible and deletion returns not found. The reference lasts up to 24 hours, addressed only to the receiving endpoint. The fetch command appears on a subsequent signal poll once the owner answers; input processing never waits for that answer.
- Absolute or
~-rooted only. The owner resolves~against its own home. A relative path has no anchor on another node. At most five paths per report; directories receive the same lifetime and audience. - No quoted paths means no broker IPC and no line. An absent hosted session, local or viewer-only seat, or no controller binds nothing silently. A missing file on the bound owner’s machine is also silent, with no fallback origin.
- CLI broker-connect and unanswered-receipt failures are silent unless
SPT_PUMP_TRACEenables diagnostics. Named declines apply only to failures with a remote controller, such as an invalid session, uncertain delivery-byte evidence, or owner refusal. The broker’s 10-second owner-reply timeout remains named. - A repeated payload in the same harness session does not register again, repeat the helper notice, or extend an existing reference’s deadline.
- Core excludes bytes it knows it physically wrote for peer delivery: either an exact match or a match after trimming ASCII whitespace from both byte sequences’ ends. It does not parse message shapes or normalize interior text. This is receipt-seat attribution, not proof of who typed every byte: external automation and native harness re-submission cannot be distinguished beyond the current controller seat. No adapter token or integration change is required.
Every entry the helper registers is enumerable in spt serve list with its
origin, so an automatic exposure is exactly as visible as a deliberate one.
Two-node field acceptance remains pending, separate from doc/impl/unit evidence. It must include a remote controller remaining seated while core physically delivers a path-bearing peer message, whose input report must cause no serve, and measure the added hook cost. Neither obligation is claimed as executed here.
Reading a message back
Every message carries a short-ID, and an attachment link is part of what that id
renders — spt msg show <id> prints the message with its attachment URLs, and
/<node>/m/<id> is the same view in a browser. See
Messaging for the id itself.