Background Assignments
A background assignment hands a piece of work to your Pia server and lets it run there: work your Pia server runs for you, grounded in records you choose. The answer comes back as a chat. It survives the app closing, it is not tied to a chat turn, and the client’s job is to say exactly what leaves the device, collect the result, and put it back inside the encrypted plane.
The server half is Pia Mesh’s task plane: an assignment is executed by an operator running a skill, and the client never addresses an operator directly — only a skill name the server says this user may run.
Where the Surface Appears
Section titled “Where the Surface Appears”On load, the client probes GET /api/assignments/skills. Anything other than a populated list — no server URL configured, no access token, 401/403/404, or an empty array — resolves to a hidden surface. The entry points are then absent, not disabled: a server that does not offer assignments produces a client with no trace of them.
| Entry point | Shown when |
|---|---|
| Assignments in the navigation sidebar | the surface is available and the window is in Assistant mode |
| The rocket button in the assistant composer’s action row — Hand this to your Pia server as a background assignment | the surface is available |
The probe result carries the skills this user’s groups have been granted, each with its chat mode and its declaredInputTypes. Both entry points open the same consent dialog; the composer one prefills the prompt with whatever is typed there and leaves the composer’s text alone, since a draft belongs to whichever destination the user affirms.
Choosing What to Send
Section titled “Choosing What to Send”The dialog is titled Send to your Pia server, and everything below is on one screen — the affirmation happens where the manifest is, not a page later.
- Skill — a picker only when more than one skill is granted; otherwise the single one, with Mode: showing the chat mode the skill declares (
Research,Assistant). The mode is the server’s, not a choice: a caller cannot steer an assignment into a different one. - What should it do? — the prompt, placeholder Describe the work in your own words, capped at 4 000 characters.
- Records to send — the picker, listing only record types the selected skill declared.
Three states replace the list rather than showing an empty one:
| State | What the dialog says |
|---|---|
| The skill declares no input types | This skill works from your prompt alone. No records are sent, but your prompt is — in plain text. |
| The skill declares types, and you have no records of them | You have no records this skill can use. You can still send your prompt on its own, in plain text. |
| The local read failed | Your records could not be read, so none can be listed or sent. |
The last one matters: “you have no records” is a claim about your data, so a read that failed says so instead of answering it.
A running total sits under the list — {selected} of 20 records · {chars} of 32,000 characters — and the primary action Send unencrypted is enabled only when a skill is selected, the prompt is non-empty and within its cap, the selection is within both caps, the record load has finished, the affirmation checkbox I understand these records leave end-to-end encryption is ticked, and nothing has been sent yet.
What Can Be Sent
Section titled “What Can Be Sent”The vocabulary is user-authored content only. Providers, settings, certificates, plugins and plugin preferences are not members of it, and no skill has a reason to read them.
| In the dialog | Envelope entityType |
Local source | What is sent as the record’s text |
|---|---|---|---|
| Conversation | assistantChat |
Assistant chats | Every message as role: content, oldest first |
| Session | session |
Optimize history | The original text and the result, both halves |
| Memory | memory |
Memory vault | The label and the entry |
| Todo | todo |
Todos | The title and its notes |
| Template | template |
Custom templates | The description and the prompt |
Built-in templates are excluded: they are client-side constants that never sync and are not the user’s own writing, so they are not theirs to consent to sending. The picker offers up to 50 recent records of each declared type — the envelope takes at most 20 anyway — and a type a newer server declares that this build cannot read contributes nothing rather than failing the dialog.
The client refuses against the same constants the server enforces, so an over-size selection never becomes a 400 the user cannot act on.
| Cap | Value |
|---|---|
| Records in one assignment | 20 |
| Characters in one record | 8 000 |
| Characters across all records | 32 000 |
| Prompt | 4 000 |
An over-cap record is listed as unsendable — Over the 8,000-character limit for one record, so it cannot be sent. — and never truncated: a user who affirms sending a record and sends a fifth of it was not asked the question they answered. Ticking one record too many, or one that would push the total past 32 000 characters, is refused with a stated reason rather than a silent trim.
What the Consent Screen States
Section titled “What the Consent Screen States”Three claims, verbatim, because they are the ones the affirmation is about:
This leaves end-to-end encryption. The records below are sent to your Pia server in plain text and stored there unencrypted while the work runs.
The plain text is deleted at most 72 hours after the run finishes, whether or not this device ever collects the result. What lives longer is the run’s metadata — status, step count and token spend — which holds none of your content.
While it is there, anyone with access to your Pia server can read it.
After Send unencrypted, one line reports what happened:
| Outcome | Message |
|---|---|
| Started | The assignment started. Its answer arrives as a chat when the run finishes. |
| No receipt, or one that does not match the selection | Nothing was sent: this selection was not affirmed. |
| Over a cap | Nothing was sent: the selection is over a size limit. |
| The server refused it, or could not be reached | Nothing was sent: your Pia server refused the assignment. |
| The dialog could not be completed | Nothing was sent: the assignment could not be started. |
The Job List
Section titled “The Job List”Background assignments lists this user’s runs: the server’s own page (GET /api/assignments, 50 rows) joined to this device’s local journal. The server knows what state a run is in; only this device knows what was asked and which chat holds the answer.
| Column | Notes |
|---|---|
| Skill | The skill’s display name from the skills probe, falling back to its wire name |
| Status | Queued, Running, Completed, Failed, Cancelled — the server’s own vocabulary — plus Unknown for a value this build does not recognise |
| Steps | Steps: {n}, shown once a run has taken one. Progress is a step count, never a percentage: a one-step skill is honestly just running, and a fraction invented from a step count becomes a lie the moment a skill’s pass count changes |
| Elapsed | under a minute, {n} min, {h} h {m} min, measured from the row’s creation to its completion or to now |
Rows come from the server, so a run started on another device still appears — without its prompt and without a chat to open, marked Started on another device, so its prompt and its answer are not on this one. Its artifact went to that device’s chat store.
- Open chat appears once the run has been collected on this device.
- Cancel appears while a run is Queued or Running. It reports Your Pia server was asked to stop this run., or Your Pia server did not stop this run — it may already have finished. when the server had nothing to cancel, or This run could not be cancelled. on an error. A cancelled run still lands terminal, so its result is still stored and still acknowledged — cancelling stops the work, it does not abandon the plaintext.
- Refresh is manual, and the view also polls every 10 seconds while it is shown. Navigating away stops the poll; a list nobody is looking at does not keep one alive.
- A read that fails never empties the list. It shows Your Pia server could not be reached, so this list may be out of date. and leaves the rows alone, because an unanswered read must not be rendered as “nothing has run”.
- With no runs at all: Nothing has run yet. Start an assignment and its answer arrives here, and as a chat, when the run finishes.
How the Answer Comes Back
Section titled “How the Answer Comes Back”A background pass runs from app start and every 20 seconds after it. It reads a cached local file first, so a tick with nothing outstanding costs nothing and never touches the network — this is not a poll of a server the user may not even have. The startup pass is the point of the whole thing: closing the app mid-run would otherwise lose the artifact silently, since the server drops the plaintext whether or not anyone comes back for it.
For each finished run, the order is fixed and is not a matter of taste:
- Read the assignment, including its artifact.
- Write it locally as an ordinary assistant chat.
- Only then
POST /api/assignments/{id}/collect, which drops the server’s plaintext copy.
Collect is irreversible, so it goes last: acknowledging first and then failing the local write would destroy the result with nothing left to fetch. If the write throws, the pending entry survives and the next pass tries again. The chat’s id is minted before the run starts, so a re-pull overwrites its own chat instead of creating a second one.
The chat carries the prompt as the user message and the artifact as the assistant message, titled from the prompt’s first line (60 characters). A run that ended without an artifact — failed, or cancelled — still produces the chat, saying so with the run’s error code or status, so a result is never simply missing.
Once a run is stored and acknowledged, the client raises it once: a persistent Flow card — Background assignment, Finished — tap to review or Didn’t finish — tap to review — plus an in-app notification when the assistant window is in the foreground, or a Windows toast when it is not (Your assignment finished and its answer is in a chat / Your assignment didn’t finish — the chat says what came back). Each offers Open chat.
If the server stops answering for a run — the row was deleted, or the plaintext window passed — the client gives up on it 7 days after it started. Nothing is lost by that: past the plaintext window the input and the artifact are both gone, so there was never anything left to collect.
Timings and Retention
Section titled “Timings and Retention”| What | Where | Value |
|---|---|---|
| Job-list refresh | Client, only while the view is shown | 10 s |
| Collection pass | Client, from startup; idle when nothing is outstanding | 20 s |
| Give up on a run the server no longer answers for | Client | 7 days after it started |
| The server drops input, artifact and error text | Server, Operators:PlaintextRetentionHours |
72 h after the run finishes (default) |
| A collected run stays in the local journal | Client | 30 days |
| The server row and its events are deleted | Server, Operators:RetentionDays |
30 days (default) |
The client’s give-up bound sits deliberately past the server’s plaintext window, so a laptop closed for a long weekend still collects its result, and well short of the row’s own retention, by which time there is nothing left to collect.
What This Device Keeps on Disk
Section titled “What This Device Keeps on Disk”| File | Holds |
|---|---|
%LOCALAPPDATA%\Pia\ConsentAudit\assignments.jsonl |
One append-only JSON line per affirmation: record id, timestamp, skill, mode, item count, total characters, and per item its entity type, id and character count. Metadata only — no titles, no content |
%APPDATA%\Pia\pending-assignments.json |
One entry per run started here: assignment id, chat id, skill, prompt, start time, and the collection time once it has one |
The pending file is kept after collection rather than deleted, because it is the only thing that can still say which chat holds a given run’s answer: the prompt travels inside the input the server drops, and the list projection never carried it.
Limits
Section titled “Limits”- Nothing headless can start a run. The send path takes a consent receipt as a required argument, and only the consent store — writing the record and awaiting the disk — can mint one. A receipt is session-scoped, so one that outlived the process it was granted in is evidence of nothing. The block is also a dependency direction rather than a runtime check: a test pins that no background entry point takes the orchestrator at all.
- A consent decision is never remembered, and a receipt authorises exactly the selection it was granted for: skill and record set are re-checked before anything is read.
- A run started on another device cannot be collected here. Its artifact went to that device’s chat store, and its prompt was never on this one.
- A finished assignment cannot be re-run or edited from the list. A new run is a new consent decision.
- After collect, the artifact is only in the local chat. The server’s copy is gone, and
plaintextDroppedAton the row records that it went.