Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 addspt send --attachment
What is servedthe file at its patha copy taken at send time
Editing the sourcethe reader sees the editthe reader still sees what was sent
Deleting the sourcethe URL answers 404the URL keeps serving
Lifetimeuntil removeda 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 units, 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:

ExitMeaning
0The file was written; its path is on stdout.
3The owner refused it — an access decision. Retrying is wrong.
1Anything 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_TRACE enables 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.