Thanks to @HarveyZed for /session (#9, the groundwork for /sessions),
@heshuaichen-charlie for the file transfer (#7) and the pnpm build-policy fix (#3), and
@vidgewong for proxy support (#5).
Added
-
/sessionscontinues a session this conversation did not start: the one/newleft behind, or the one opened on a laptop in the web UI and now wanted on a phone. A conversation's session id is derived — chat, workspace, epoch — and that derivation is what makes a restarted process find the same conversation again; a pick is an override on it, and everything else follows from what a chat can safely be shown and safely be given. Nothing is typed: a session id is a machine identifier, so every switch is a press on a row this channel produced, which is also what keeps authorization in one place. The list is that place — a conversation is offered its own past sessions and sessions no conversation owns, never another chat's, whose title alone summarizes what was said there. The workspace decides what is on offer, because a session carries the directory it runs in and continuing one from elsewhere would move the sandbox without anyone saying so. Two kinds never appear at all: work an agent delegated to itself, which is a transcript answering to nobody rather than a conversation, and anything the operator archived, since archiving is the host's own “hide this from every surface” and a chat still offering it would be the one place that decision did not land. What each row shows is what a person recognizes a conversation by — the last thing a HUMAN said in it, since the same stream also carries system prompts and skill catalogs, and titles are folded from the first prompt, which makes half of them “Hello” — and the list reads newest-activity first, which is the timestamp each row prints. And a pick is undone by picking: the conversation's own derived session is always a row, so going back is the same gesture as going away, while/cdand/newend a pick on their own — both of them ARE a change of session. A picked session that cannot be resumed is never started over in place: a pick names a conversation that already exists, so an empty session created under its id would be written over the very thing someone asked to carry on with; the chat is told, and the pick is retired so the next message lands on its own session. -
Files sent to the chat land in the workspace, so the model reads them instead of guessing. The transport already put a
<file name="app.log"/>marker in the message text, which showed the model a name and no bytes — the worst of both, because a model handed a file name answers as though it had read the file. Every file a message carries (documents, images, voice notes, video; a sticker is not a file) is now streamed into.dsh-lark/inbox/<timestamp>-<message hash>/under the conversation's own workspace, and a note riding the same message names the absolute path. The sender's file name is sanitized to one safe component, per-message directories scope collisions to a single message, and a name that survives sanitizing down to nothing lands asfile. Nothing is dropped in silence: a file over the per-file ceiling, over the per-message total, refused by configuration, or lost to a failed download leaves a note saying so, because a model that received a file and believes it did not is worse off than one that knows what it is missing. Images pass through this and the attachment path both, and are downloaded exactly once. The first landing in a workspace adds one line suggesting.dsh-lark/for.gitignore; the channel never edits that file itself, and never deletes what it landed. -
send_filehands the model's own artifacts back to the person who asked — a report, a diff, a generated image, exported data — from inside the workspace and only ever to the chat the agent already belongs to. In a direct message it goes straight out: the reader is the person already driving the agent, so an approval would only breed the fatigue that ends in blind approving. In a group every send raises an approval card showing where the file sits inside the workspace, the workspace's own name and the size, and the bytes are read BEFORE the room is asked so what the card certifies is the artifact that leaves. Both halves of that location are cut from the canonical path the containment check cleared, so a symlink still cannot present itself as a file the room believes it is approving, while the operator's home directory and login name stay out of the group. Reading first is what bounds the card's cost, and the count of undecided cards is bounded too: three per chat, after which a send is refused on the spot rather than pinning a fourth buffer for the half-hour a card may stand. There is no setting that turns that group card off, and none that raises the three — either would be an official back door onto the prompt-injection chain (ADR 0002). A path that leaves the workspace is refused afterrealpath, not before it, so a symlink cannot walk out through a name that looks local; the refusal reaches the model in English with the reason, and an escape attempt reaches the operator console. -
/get <path>sends one workspace file to the chat, with no agent and no card: a human who typed the path has stated his intent, and making him approve his own command is theatre. It answers in a chat that has no session yet, resolves relative paths against the conversation's workspace, and walks the exact same containment check the model's tool does. A file that cannot be read is reported by its place inside the workspace, with the absolute prefix taken out of the filesystem's own message too — a read failure is the branch an injected instruction can provoke on purpose, and in a group the reply is read by the whole room. The same scrub covers whatsend_filetells the model, which no more gets a path it did not type itself than any other refusal here hands it one. -
Four configuration fields, both switches on by default:
receiveFiles(true) andmaxReceiveFileBytes(20 MiB) for what arrives,sendFiles(true) andmaxSendFileBytes(20 MiB) for what leaves. Inbound content never enters the model's context — it lands on disk and the model gets a path — and the outbound risk is handled structurally by the gate above, so a gate that only exists when a deployment goes looking for it would be a gate designed not to be used. WheresendFilesis off, or the host registry cannot take a per-agent tool,send_fileis absent and the standing prompt says so, so the model writes its findings into the reply instead of offering an attachment forever;/getstays available either way. -
The approval card names the escalation it is granting, and can grant more than one call. A host approval request carries the tool and the reason but not the sandbox mode being asked for — that rides the call's own
sandbox_permissions, which this channel already snapshots, so the card can say "this call asks to be raised to danger-full-access" instead of showing a command and hoping. Where a call did ask, a third button settles the approval and then switches the conversation to that preset, in that order: the preset also sets the approval policy tonever, andneverREFUSES what still needs approval, so switching first could reject the very decision being made. The button says all of that where it is pressed. -
/permissionanswers with a picker, and a switch runs from the agent's own idle phase. The host command always took a preset name; a bare/permissiononly printed one, so switching meant knowing the name and typing it. Rows now carry what each preset means — the host's own name and description, plus this channel's own words for the unconfined one, which stops the ASKING as well as the confining — and a press runs the host's own command. Reading is a read: the preset comes from the publishedpermissionsprojection, because running/permissionto find out appends to the session log, and a card that "just looks" must not be a second writer. Writing waits its turn: every switch is queued per conversation and claims the agent's idle phase throughrunMaintenance, so it can never land beside a running turn or inside the publication of one — the two ways this channel has already torn a session log. A card settles the moment it is pressed and repaints when the switch lands, so what it says is what happened. -
One rule decides who may change a preset, wherever it was asked from. The picker, the typed
/permission <name>and the approval card's third button now pass the same check, and it is asymmetric on purpose: taking the sandbox off is a grant and needs whoever may approve one, while putting it back on is something anyone who may drive the conversation can do. The typed form used to walk straight into the host command, so the card could refuse a switch that typing performed — and the card, gating both directions the same way, refused an ordinary member the switch that makes their own conversation safer. -
/statusgained the preset row, so a session that stopped asking is something you can see rather than something you notice by the silence. The approval card's third button appears only where the switch it promises can actually happen: the preset has to exist in this deployment, the command has to be composed, and the host has to lend an idle phase to run it from. -
The websocket transport respects the proxy environment, so a deployment behind a corporate proxy connects instead of hanging on a handshake nothing routes.
respectProxyEnvis on, which meansHTTPS_PROXY/HTTP_PROXY/NO_PROXYare read the way every other tool on that host reads them.
Fixed
- A profile installs even where a dependency wants to run a build script. pnpm holds unapproved build scripts back, and a bot that never finished installing is a bot that never starts: the generated profile now approves what it actually needs and pins
strictDepBuilds: false, so a future dependency's script is skipped with a warning rather than failing the install of a running deployment. - Settled approval cards best-effort resolve a decider name from the chat roster when the platform callback carries only an open id. Missing roster permission, lookup failures, and departed members still settle the approval and safely show the open id, while unauthorized clicks never trigger a roster lookup.
- A control card's buttons keep working. The transport drops a repeated card action for twelve hours, keyed by the message, the operator and the payload — right for a redelivered event, wrong for a person pressing refresh a second time, picking a model they picked before, or switching a preset back. Every rendering of the status, model and permission cards now stamps its payloads, so a later press is a different event, and a click is answered immediately with the work done behind it: the platform stops showing a callback's result after a few seconds, which is why a switch that worked looked like a dead button.
- A card click resolves the conversation the way a message does — by key, then the session it derives to — instead of scanning bindings for a matching chat. A chat that had run
/newor/cdwas still holding its older sessions in that table, so a click found the first, released one and reported that nothing was running beside a conversation that plainly was. A conversation with no live agent is now opened for the switch rather than told to send a message first: a preset belongs to the durable session, which outlives the process. - Two people talking in one chat no longer answer for each other under a finer session scope. The transport merges a chat's burst into a single message with the latest sender and thread attached, which is right when a chat owns the session and wrong for
chat-threadandchat-sender; the merge window is now closed for those scopes. - A reply is aimed where the turn actually takes the message:
agent/inbox/claimed, which lands before the turn's first step. The session log's own order isturn/start,step/start,user/message, so aiming at that last record arrived after the turn had already begun producing. An artifactsend_filehands over early in a turn now lands under the ask for the same reason, because both the renderer's aim and the file's travel through one place. - Who may loosen a conversation's permissions is decided by what the preset DOES, not by what it is called. A deployment defines its own preset table, so
unrestricted-prod: { sandbox: danger-full-access, approval: never }walked past a check that only knew the namedanger-full-access; the sandbox mode and the approval policy now decide, and a preset whose bundle cannot be read needs an approver rather than being assumed harmless. - A conversation's queued switches are cancelled BEFORE its agent is released, and the release waits for them. Releasing is what makes an agent idle, so a switch waiting for exactly that could slip through the window and write to a session being disposed. Cancellation also reaches into the wait itself rather than queueing behind it, and disposal now waits for the work that surrounds a switch — opening an agent, answering the chat, repainting a card — not only for the command.