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. If the user later hands the same live path to another endpoint, that endpoint joins the existing entry’s audience and receives the same URL. No duplicate entry is created and the original deadline does not move.

The fetch command normally arrives with the path-bearing prompt after a short bounded wait for registration. If the owner answers later, the command remains available on a subsequent signal poll. The wait does not cancel owner work.

  • 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. Paths containing whitespace must be enclosed in matching double quotes, single quotes, or backticks: "C:\My Pictures\a.png", '/home/x/My Pictures/a.png', or `~/a b/c.md`. Each quoted span is one path, not whitespace-separated fragments; punctuation outside its closing quote is not part of the path. Unquoted paths containing whitespace are outside this contract. An unmatched opening quote ends extraction; complete paths before it are retained.
  • 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.
  • Once per endpoint, session and filepath. Rephrasing a prompt does not repeat its helper. A new endpoint or session can receive the existing URL; neither can extend the live reference’s original 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.

An original prompt and its edge-ASCII-trimmed report share the first path receipt in the same endpoint session, including its controller custody and deadline. Surrounding prose is not a new file handoff.

Each poll emits an identical fetch command at most once, even if distinct attachment or helper records supplied it. All such records are marked seen, so suppressed duplicates do not appear on the following poll.

Every entry the helper registers is enumerable in spt serve list with its origin and complete audience, so an automatic exposure is exactly as visible as a deliberate one.

Two-node field acceptance was granted by Doyle on 2026-09-22 for the W2 candidate: spaced-path fetch, one helper per physical prompt, delivery with the original prompt, two same-entry/URL audience-admission cases preserving the original deadline, and exact plus XML-reencoded peer bodies causing no serve while a remote controller remained seated. The seven-cell observations and verified isolated teardown are preserved in .spt/preserved/318/hertz/w2-two-node/DELIVERY.json; resident daemon records and firewall output remained unchanged on both nodes. This accepts the tested dirty-source candidate, not a later commit SHA or the whole release, and does not turn one node pair’s timing into a WAN or numeric hook-latency guarantee.

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.