From 2927e41445c1bda3557236a389d9c02b641c6d2e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 13 May 2026 20:23:37 +0200 Subject: [PATCH] feat: add four relayfile workspace skills Adds activity-summary, daily-digest, writeback-as-files, and workspace-layout skills that document the relayfile workspace primitives referenced in the "Just Give the Agent Files" blog post. - activity-summary: tells agents to read digests/yesterday.md before crawling provider data for "what did I work on yesterday" questions - daily-digest: authoring contract for /digests/ files, including adapter digest() exports and regeneration rules - writeback-as-files: drop-a-JSON-file writeback contract with schema discovery and dead-letter recovery - workspace-layout: LAYOUT.md + by-* alias indexes instead of find/grep -r Each skill is registered in prpm.json and listed in README.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 4 + prpm.json | 68 +++++++++++++ skills/activity-summary/SKILL.md | 96 +++++++++++++++++++ skills/daily-digest/SKILL.md | 104 ++++++++++++++++++++ skills/workspace-layout/SKILL.md | 116 +++++++++++++++++++++++ skills/writeback-as-files/SKILL.md | 147 +++++++++++++++++++++++++++++ 6 files changed, 535 insertions(+) create mode 100644 skills/activity-summary/SKILL.md create mode 100644 skills/daily-digest/SKILL.md create mode 100644 skills/workspace-layout/SKILL.md create mode 100644 skills/writeback-as-files/SKILL.md diff --git a/README.md b/README.md index da25cb4..32c5743 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Package metadata lives in [prpm.json](prpm.json). The repo currently publishes ` | [using-agent-relay](skills/using-agent-relay/SKILL.md) | 1.2.0 | Coordinate agents in real time with Relaycast messaging, channels, threads, reactions, search, and webhooks. | | [running-headless-orchestrator](skills/running-headless-orchestrator/SKILL.md) | 1.0.4 | Self-bootstrap Agent Relay infrastructure and manage worker agents without human intervention. | | [relay-80-100-workflow](skills/relay-80-100-workflow/SKILL.md) | 1.0.4 | Author workflows that close the 80-to-100 validation gap with repair-aware test, verify, and commit gates. | +| [activity-summary](skills/activity-summary/SKILL.md) | 1.0.0 | Answer "what did I work on yesterday" questions by reading `digests/yesterday.md` first instead of crawling provider directories. | +| [daily-digest](skills/daily-digest/SKILL.md) | 1.0.0 | Authoring contract for `/digests/` files — windows, per-provider sections, adapter `digest()` exports, regeneration rules. | +| [writeback-as-files](skills/writeback-as-files/SKILL.md) | 1.0.0 | File-creation writeback contract — drop a JSON file at the canonical path and relayfile delivers the mutation, with dead-letter recovery. | +| [workspace-layout](skills/workspace-layout/SKILL.md) | 1.0.0 | Navigate a relayfile mount via `LAYOUT.md`, per-provider `.layout.md`, and `by-*` alias indexes instead of `find`/`grep -r`. | ## Slash Commands diff --git a/prpm.json b/prpm.json index f7c0358..f1a1022 100644 --- a/prpm.json +++ b/prpm.json @@ -124,6 +124,74 @@ "skills/relay-80-100-workflow/SKILL.md" ] }, + { + "name": "activity-summary", + "version": "1.0.0", + "description": "Use when an agent is asked \"what did I (or my team) work on yesterday / this week / today\" across provider data in a relayfile mount (Linear, GitHub, Notion, Slack, Confluence, Jira, etc.). Tells the agent to consult the pre-computed `digests/yesterday.md` (and sibling digest files) at the workspace root BEFORE doing manual exploration with `ls`/`grep`/`find`. The digest is deterministic, exhaustive over the window, and costs one file read instead of dozens of provider queries.", + "format": "claude", + "subtype": "skill", + "tags": [ + "relayfile", + "activity-summary", + "digest", + "agent-context", + "cost-optimization" + ], + "files": [ + "skills/activity-summary/SKILL.md" + ] + }, + { + "name": "daily-digest", + "version": "1.0.0", + "description": "Use when authoring or extending the digest set in a relayfile workspace - covers the contract for files under `/digests/` (`yesterday.md`, `today.md`, `this-week.md`, date-stamped daily files), per-provider section format, link conventions back into the canonical mount tree, when digests are regenerated, and how adapter authors expose new provider data to the digest pipeline.", + "format": "claude", + "subtype": "skill", + "tags": [ + "relayfile", + "digest", + "adapter", + "authoring", + "provider-integration" + ], + "files": [ + "skills/daily-digest/SKILL.md" + ] + }, + { + "name": "writeback-as-files", + "version": "1.0.0", + "description": "Use when an agent needs to write back to a provider (create a Linear comment, open a GitHub issue, post a Slack message, edit a Notion page, etc.) through a relayfile mount. Covers the file-creation writeback contract (drop a JSON file at the canonical path → provider mutation), discovering the right path and schema via `.schema.json` siblings, idempotency keys, watching writeback status with `relayfile writeback list` and `relayfile status`, and recovering from dead-lettered writes under `.relay/dead-letter/`.", + "format": "claude", + "subtype": "skill", + "tags": [ + "relayfile", + "writeback", + "provider-mutation", + "agent-action", + "dead-letter" + ], + "files": [ + "skills/writeback-as-files/SKILL.md" + ] + }, + { + "name": "workspace-layout", + "version": "1.0.0", + "description": "Use when an agent is exploring a relayfile mount for the first time or trying to locate a specific resource (Notion page, Linear issue, Slack channel, GitHub PR). Tells the agent to start with `/LAYOUT.md` and `/.layout.md` rather than guessing paths from memory, and to use the `by-title/`, `by-id/`, `by-name/`, `by-edited//`, `by-state/` alias subtrees instead of recursively grepping. Filename convention is `__`.", + "format": "claude", + "subtype": "skill", + "tags": [ + "relayfile", + "workspace", + "navigation", + "indexes", + "agent-context" + ], + "files": [ + "skills/workspace-layout/SKILL.md" + ] + }, { "name": "create-workflow", "version": "1.0.0", diff --git a/skills/activity-summary/SKILL.md b/skills/activity-summary/SKILL.md new file mode 100644 index 0000000..e36785f --- /dev/null +++ b/skills/activity-summary/SKILL.md @@ -0,0 +1,96 @@ +--- +name: activity-summary +description: Use when an agent is asked "what did I (or my team) work on yesterday / this week / today" across provider data in a relayfile mount (Linear, GitHub, Notion, Slack, Confluence, Jira, etc.). Tells the agent to consult the pre-computed `digests/yesterday.md` (and sibling digest files) at the workspace root BEFORE doing manual exploration with `ls`/`grep`/`find`. The digest is deterministic, exhaustive over the window, and costs one file read instead of dozens of provider queries. +--- + +# Activity Summary — Read the Digest First + +## Overview + +A relayfile workspace pre-computes deterministic daily activity digests at `/digests/`. These are produced by relayfile itself from the raw provider data, so they are **complete over the time window** (no API pagination gaps) and **free for the agent to consume** (one file read, no LLM generation step). + +If you've been asked an activity-summary question, **read the digest before doing anything else.** Reaching for `ls`, `grep`, or per-provider exploration first is the most common reason these answers cost 20+ tool calls when they could cost 1. + +## When to use this skill + +Trigger phrases from the user — read the digest first: + +- "what did I work on yesterday / today / this week" +- "what changed across {GitHub, Linear, Notion, Slack, ...} {yesterday, since Friday, etc.}" +- "summarize my activity" +- "give me a standup update" +- "what did $team_member ship recently" + +If the user's question is **not** windowed by time (e.g. "find the Notion page about onboarding"), the digest is not the right entry point — use `by-title/` or `by-id/` indexes instead. See the `workspace-layout` skill. + +## What digests look like + +```bash +$ ls $MOUNT/digests/ +yesterday.md # rolling — generated at 00:00 local for the prior calendar day +today.md # rolling — updated continuously as the day progresses +2026-05-12.md # date-stamped, immutable once the day closes +2026-05-11.md +... +this-week.md # rolling — Mon→now of the current ISO week +last-week.md # immutable, prior ISO week +``` + +The body is plain Markdown with one section per provider: + +```markdown +# Activity for 2026-05-12 + +## linear +- AGE-16 moved to Blocked (waiting on design) — [/linear/issues/AGE-16__87389837-...] +- AGE-9 closed — [/linear/issues/AGE-9__2bb2c00f-...] + +## github +- relayfile-adapters#412 merged to main — [/github/repos/.../pulls/412.json] +- relayfile#287 opened — [/github/repos/.../pulls/287.json] + +## notion +- "Khaliq's To Dos" edited — [/notion/pages/3566800c-.../content.md] + +## slack +- 7 messages in #gtm-prospects mentioning "ACME" +``` + +Each bullet links to the canonical file in the mount, so a follow-up question ("what changed about AGE-16?") is one `cat` away. + +## How to use it + +```bash +# 1. Check the digest covers the date the user asked about. +ls $MOUNT/digests/ +cat $MOUNT/digests/yesterday.md + +# 2. If the user asked about a specific date, prefer the date-stamped file. +cat $MOUNT/digests/2026-05-12.md + +# 3. Confirm coverage before answering. The digest header includes the window +# it spans; if the user's window is wider, read multiple digest files +# rather than re-deriving from raw provider data. +head -5 $MOUNT/digests/this-week.md +``` + +That is usually the entire workflow: read → quote → done. Four tool calls or fewer. + +## When to fall back to exploration + +The digest is the right answer when: + +- The window matches a digest file (yesterday, today, a specific past date, this/last week). +- The user wants **everything** in the window, not a filtered slice. + +Fall back to direct exploration via `by-edited//` index subtrees when: + +- The window is unusual (e.g. "the last 36 hours"). Use `by-edited/` indexes; see `workspace-layout`. +- The user wants a filter the digest doesn't pre-compute (e.g. "only Linear issues assigned to me"). +- The digest file is missing or its header indicates incomplete provider coverage. + +## Why this matters + +In our published benchmarks, the activity-summary question dropped from ~20 turns and $0.30+ to 4 turns and ~$0.07 once the digest existed and the agent read it first. The digest is one file read that replaces ~25 individual provider queries. + +If you find yourself listing more than 2-3 directories to answer an activity question, stop and check `digests/` — you're almost certainly working harder than you need to. diff --git a/skills/daily-digest/SKILL.md b/skills/daily-digest/SKILL.md new file mode 100644 index 0000000..24f8a58 --- /dev/null +++ b/skills/daily-digest/SKILL.md @@ -0,0 +1,104 @@ +--- +name: daily-digest +description: Use when authoring or extending the digest set in a relayfile workspace - covers the contract for files under `/digests/` (`yesterday.md`, `today.md`, `this-week.md`, date-stamped daily files), the per-provider section format, link conventions back into the canonical mount tree, when digests are regenerated, and how adapter authors expose new provider data to the digest pipeline. NOT for agents answering activity questions — those should use the `activity-summary` skill to consume digests, not produce them. +--- + +# Daily Digest — Authoring Contract + +## Overview + +`/digests/` is a relayfile-managed directory containing deterministic, pre-computed Markdown summaries of provider activity over fixed time windows. Digests are the cheap entry point for activity-summary queries (see the `activity-summary` skill). This skill is for **producers** — adapter authors, provider integrators, and anyone extending what shows up in a digest. + +## When to use this skill + +- You are writing a new relayfile adapter (Notion, Linear, …) and need to wire it into the digest pipeline. +- You want a new digest window (e.g. `last-30-days.md`) and need to know the contract. +- A digest is missing a provider's content and you need to know why. +- You need to debug why `yesterday.md` was empty or stale this morning. + +If you're an agent **reading** the digest to answer a user question, switch to the `activity-summary` skill instead. + +## Digest file taxonomy + +| File | Window | Mutability | +|---|---|---| +| `today.md` | 00:00 local → now | Updated on every change-event for the current day | +| `yesterday.md` | full prior calendar day | Generated at 00:00 local, immutable for the rest of the day | +| `YYYY-MM-DD.md` | that calendar day | Immutable once the day closes | +| `this-week.md` | Mon 00:00 → now (ISO week) | Updated on every change-event | +| `last-week.md` | full prior ISO week | Immutable | + +A digest file is **always present** for each window even if the window contains no activity (the body says `_no activity_`). Agents can detect empty windows without retrying. + +## File header + +Every digest starts with a frontmatter-style header that callers can verify before trusting the body: + +```markdown +# Activity for 2026-05-12 + +> window: 2026-05-12T00:00:00-07:00 → 2026-05-13T00:00:00-07:00 +> generated: 2026-05-13T00:01:14Z +> providers: linear, github, notion, slack +> events: 47 +``` + +- `window` is the half-open interval in the workspace's configured timezone. +- `providers` lists every adapter that contributed (or attempted to). A missing provider here means the adapter never ran, not that it had zero activity. +- `events` is the raw change-event count over the window — useful for sanity-checking against the body. + +## Per-provider section format + +Each provider gets exactly one `## ` section, in alphabetical order. Bullets within a section are sorted by event time, ascending. Each bullet must: + +1. Start with the canonical record identifier the provider uses (`AGE-16`, `#412`, page title, etc.). +2. Describe the change in past tense — "moved to Blocked", "merged to main", "edited". +3. End with a Markdown link to the canonical file path in the mount, in square brackets. + +```markdown +## linear +- AGE-16 moved to Blocked (waiting on design) — [/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json] +- AGE-9 closed — [/linear/issues/AGE-9__2bb2c00f-ee93-4c73-a793-df5b725d9a1a.json] +``` + +The link target is what makes the digest interactive — a follow-up "tell me more about AGE-16" is one `cat` away from the digest line. + +## Adapter contract + +For an adapter to contribute to digests it must export a `digest()` function in addition to its sync/writeback handlers: + +```typescript +import type { DigestContext, DigestSection } from '@relayfile/adapter-sdk'; + +export async function digest(ctx: DigestContext): Promise { + const events = await ctx.changeEvents({ + window: ctx.window, // { from: ISO8601, to: ISO8601 } + providers: [ctx.provider], + }); + + if (events.length === 0) return null; + + return { + provider: ctx.provider, + bullets: events.map((e) => ({ + text: renderBullet(e), + canonicalPath: e.resource.path, + })), + }; +} +``` + +Returning `null` is correct for "ran successfully, no activity"; throwing is reserved for "could not produce a digest" and surfaces as a warning in the digest header. + +## Regeneration + +- **Rolling windows** (`today.md`, `this-week.md`) are regenerated on every change event for the current window, coalesced to a max of one rebuild per 30 seconds. +- **Closing windows** (`yesterday.md`, `YYYY-MM-DD.md`, `last-week.md`) are produced once at window close (00:00 local for daily, Monday 00:00 for weekly) and never modified afterward. This is what makes them safe to cache and quote verbatim. +- If you need to force a rebuild (e.g. after fixing an adapter bug), `relayfile digest rebuild --window yesterday` re-derives from the change log. + +## Common mistakes when adding a new digest + +- **Don't inline raw provider payloads.** Bullets are one line. Anything larger belongs in the canonical file the bullet links to. +- **Don't summarize across providers in one section.** Cross-provider correlation is the agent's job; the digest's job is exhaustive per-provider listing. +- **Don't omit the canonical link.** A digest line without a link is dead weight — the agent can't follow up without re-deriving the path. +- **Don't generate non-deterministic content.** Two runs over the same change-event window must produce byte-identical output. LLM-generated prose belongs in the agent, not the digest. diff --git a/skills/workspace-layout/SKILL.md b/skills/workspace-layout/SKILL.md new file mode 100644 index 0000000..c9d45ac --- /dev/null +++ b/skills/workspace-layout/SKILL.md @@ -0,0 +1,116 @@ +--- +name: workspace-layout +description: Use when an agent is exploring a relayfile mount for the first time or trying to locate a specific resource (Notion page, Linear issue, Slack channel, GitHub PR). Tells the agent to start with `/LAYOUT.md` and `/.layout.md` rather than guessing paths from memory, and to use the `by-title/`, `by-id/`, `by-name/`, `by-edited//`, `by-state/` alias subtrees instead of recursively grepping. Filename convention is `__` (ticket number / slug first so listings are scannable). NOT for activity-summary questions, which should use the `activity-summary` skill instead. +--- + +# Workspace Layout — Start With LAYOUT.md + +## Overview + +A relayfile mount is **self-describing**. Every workspace has a `LAYOUT.md` at its root, and every provider has a `.layout.md` at its provider root, that together describe the directory shape, the filename conventions, and the indexes available for fast lookup. Read these first. Do not guess paths from memory — provider layouts can be customized per workspace and the indexes available may differ. + +## When to use this skill + +- You just connected to a relayfile mount and have no prior context about its shape. +- You need to find a specific resource (a Notion page by title, a Linear issue by number, a Slack channel by name). +- You're tempted to run `find` or `grep -r` across the mount — almost always there is an index that does it cheaper. +- You see paths in someone else's code or in a digest and want to understand them. + +If the user is asking an activity-summary question ("what did I work on yesterday"), use the `activity-summary` skill instead. This skill is for resource lookup, not time-windowed queries. + +## Step 1: Read the root layout + +```bash +$ cat $MOUNT/LAYOUT.md +``` + +The root `LAYOUT.md` lists the connected providers, the digests directory, the skills directory, and any cross-provider conventions in effect for this workspace. + +## Step 2: Read the provider layout + +```bash +$ cat $MOUNT/linear/.layout.md +``` + +The per-provider layout covers: + +- Top-level directories under the provider root (e.g. `issues/`, `projects/`, `cycles/`) +- Filename conventions in use (`__.json`, plain UUID, slug-based, …) +- Which `by-*` alias indexes are populated +- Writeback directories and their schemas (see the `writeback-as-files` skill) +- Whether content is paginated and how + +## Step 3: Use alias indexes, not recursive search + +Canonical records are keyed by UUID for stability. Alias indexes live under `by-*/` and point back to the canonical files. Reach for an index that matches your query shape: + +```bash +# Find a Notion page by title +$ ls $MOUNT/notion/pages/by-title/ | grep -i "onboarding" +onboarding-runbook__c24642bb.json +$ jq -r '.id' $MOUNT/notion/pages/by-title/onboarding-runbook__c24642bb.json + +# Find a Linear issue by ticket number (identifier is part of the canonical +# filename, so a direct ls is sufficient — no by-id/ needed) +$ ls $MOUNT/linear/issues/ | grep "^AGE-16" + +# Find what was edited yesterday in Notion +$ ls $MOUNT/notion/pages/by-edited/2026-05-12/ + +# Find all Linear issues currently In Progress +$ ls $MOUNT/linear/issues/by-state/in-progress/ + +# Find a Slack channel by name +$ ls $MOUNT/slack/channels/by-name/ | grep "gtm" +``` + +Indexes are symlinks (or directory listings on filesystems without symlink support) — they don't duplicate the underlying content, so they stay cheap to enumerate even on workspaces with thousands of records. + +## Filename convention + +Canonical files use **`__.`** — identifier first so directory listings are immediately scannable: + +```bash +$ ls $MOUNT/linear/issues/ +AGE-9__2bb2c00f-ee93-4c73-a793-df5b725d9a1a.json +AGE-10__8c313d70-9800-4539-820f-96a481c09ce0.json +AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json +``` + +The `__` (double underscore) separator is reserved — provider data must not produce it in the identifier portion. If you see a filename without `__` it's an alias-index symlink or a metadata file, not a canonical record. + +We borrowed the `__` shape from [Mirage](https://github.com/strukto-ai/mirage) after seeing the pattern in their mount. + +## Common patterns + +### "Where does X live?" + +```bash +cat $MOUNT/LAYOUT.md # provider list and cross-cutting layout +cat $MOUNT//.layout.md # provider-specific shape +ls $MOUNT// # top-level resource directories +ls $MOUNT///by-*/ # available indexes +``` + +Three to four `cat`/`ls` calls and you have the full map. + +### "I have a UUID, what is it?" + +The UUID is in the filename. `ls` and `grep` find it without needing to know which directory: + +```bash +$ find $MOUNT -name "*87389837-62b1-4e1a-a237-59218bab2974*" -type f +$MOUNT/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json +``` + +Use `find` here because UUIDs are globally unique — the result is one file. + +### "I have a slug or title, what is it?" + +Use `by-title/` or `by-name/` rather than `find`. The index is sorted and bounded; `find` walks the full tree. + +## What NOT to do + +- **Don't `grep -r` over the mount** for a title or name. There's an index. Use it. +- **Don't hardcode paths from a different workspace's LAYOUT.md.** Workspaces can customize which adapters and indexes are mounted. +- **Don't ignore `.layout.md`.** If you wrote a Notion-specific path from memory and it doesn't exist, the provider's `.layout.md` will tell you the actual shape in one read. diff --git a/skills/writeback-as-files/SKILL.md b/skills/writeback-as-files/SKILL.md new file mode 100644 index 0000000..5090d63 --- /dev/null +++ b/skills/writeback-as-files/SKILL.md @@ -0,0 +1,147 @@ +--- +name: writeback-as-files +description: Use when an agent needs to write back to a provider (create a Linear comment, open a GitHub issue, post a Slack message, edit a Notion page, etc.) through a relayfile mount. Covers the file-creation writeback contract (drop a JSON file at the canonical path → provider mutation), discovering the right path and schema via `.schema.json` siblings, idempotency keys, watching writeback status with `relayfile writeback list` and `relayfile status`, and recovering from dead-lettered writes under `.relay/dead-letter/`. NOT for read operations or for direct API calls — relayfile mediates the writeback so you can ignore provider auth, retries, and rate limits. +--- + +# Writebacks Are Files + +## Overview + +In a relayfile mount, the agent does not call a provider API to mutate state. It **writes a file**. The mount daemon picks up the change, validates the payload against the canonical schema, signs and delivers the request to the provider, and records the result. Auth, retries, idempotency, dead-lettering, and the audit trail are handled on the other side. + +## When to use this skill + +- The user asks the agent to take an action against a provider (comment, message, create, update, close, react, …). +- You see a writable path under `//…` and want to know what shape the file should be. +- A previous write didn't seem to take effect and you need to find out why. +- You're about to call a provider SDK directly — stop and check if relayfile already exposes a writeback for that mutation. + +## The contract in one sentence + +> Drop a JSON file at the canonical writeback path; the provider receives the corresponding mutation within ~30 seconds. + +## Find the canonical path + +Writeback directories are discoverable in the mount. They sit next to the read-side data and carry a sibling `.schema.json` describing the expected payload. + +```bash +# What can I do under a Linear issue? +$ ls $MOUNT/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974/ +content.md +comments/ # writeback dir — drop JSON files here +comments/.schema.json # schema for individual comment writebacks +state-transitions/ # writeback dir — drop JSON to move issue state +state-transitions/.schema.json +``` + +```bash +# What's the schema for a Linear comment? +$ cat $MOUNT/linear/issues/AGE-16__.../comments/.schema.json +{ + "type": "object", + "required": ["body"], + "properties": { + "body": { "type": "string", "minLength": 1, "maxLength": 65535 }, + "asUserId": { "type": "string" } + } +} +``` + +Schemas are the source of truth. Read them before guessing payload shape. + +## Examples + +### Post a Linear comment + +```bash +cat > $MOUNT/linear/issues/AGE-16__.../comments/wb-$(date +%s).json <<'EOF' +{ + "body": "Picking this up — design clarified the blocker." +} +EOF +``` + +### Open a GitHub issue + +```bash +cat > $MOUNT/github/repos/AgentWorkforce/relay/issues/wb-$(date +%s).json <<'EOF' +{ + "title": "Race condition in writeback retry loop", + "body": "Repro: …\n\nExpected: …\n\nActual: …", + "labels": ["bug", "writeback"] +} +EOF +``` + +### Send a Slack message + +```bash +cat > $MOUNT/slack/channels/C0ADE9B71CN__gtm-prospects/messages/wb-$(date +%s).json <<'EOF' +{ + "text": "ACME signed — moving them to the activation channel." +} +EOF +``` + +### Update a Notion page body + +```bash +# Notion content.md is a *write-through* file — overwriting it queues an update. +echo "# Onboarding\n\nUpdated …" > $MOUNT/notion/pages//content.md +``` + +## Filename conventions + +- **Use a unique suffix** (timestamp + short random) so retries don't collide. The mount daemon also derives an idempotency key from the file path, so two writes to the same path inside the dedup window are coalesced. +- **`wb-.json`** is the conventional prefix for agent-authored writebacks. It makes them easy to spot in dead-letter forensics. +- Do **not** name files `.tmp` or use rsync-style `.partial` — the daemon picks up files atomically on rename close; partial-suffix files are ignored. + +## Watching status + +```bash +# Pending writebacks (queued but not delivered yet) +relayfile writeback list --state pending + +# Failed writebacks (dead-lettered after exhausting retries) +relayfile writeback list --state dead + +# Quick health check +relayfile status +# workspace rw_xxxxxxxx (my-agent) mode: poll lag: 4s +# linear ready 214 files last event 2s ago +# pending writebacks: 0 dead-lettered: 0 +``` + +`dead-lettered: 0` is the field to watch. If it goes non-zero, your writes are not landing. + +## Dead-letter recovery + +Failed writebacks land in `/.relay/dead-letter/` with the original payload plus a `.error.json` sibling explaining the failure: + +```bash +$ ls $MOUNT/.relay/dead-letter/ +wb-1715608327.json +wb-1715608327.error.json + +$ cat $MOUNT/.relay/dead-letter/wb-1715608327.error.json +{ + "code": "schema_violation", + "message": "body: must be at least 1 character", + "attempts": 1, + "lastAttemptAt": "2026-05-13T14:32:07Z" +} +``` + +Typical causes: + +- `schema_violation` — your payload didn't match `.schema.json`. Fix and re-drop. +- `provider_4xx` — provider rejected (auth scope, missing parent, etc.). The error body contains the provider's response. +- `provider_5xx_exhausted` — provider repeatedly failed after backoff. Usually transient; re-drop with a fresh filename. + +To replay: read the original payload, fix what's wrong, write to a fresh path with a new suffix. + +## What NOT to do + +- **Don't call the provider SDK directly** from within the agent if a writeback path exists. You lose the retry, idempotency, dead-letter, and audit story. +- **Don't write to read-only paths.** The mount enforces read-only at the OS level on canonical record files (e.g. `*.json` payloads). If your write returns `EACCES`, find the writeback subdirectory instead. +- **Don't poll for completion in a tight loop.** Subscribe to the change stream for `writeback.succeeded` and `writeback.failed` events.