Rules for trusted NanoClaw groups. Shared memory, session bootstrap, cross-group memory updates. Loaded for trusted and main containers only.
73
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
State written by scripts/register-session.py per jbaruch/coding-policy: stateful-artifacts. Owner skill is tessl__trusted-memory. Reader skills — jbaruch/nanoclaw-admin: tessl__heartbeat and jbaruch/nanoclaw-admin: tessl__check-email — MUST treat any unrecognised shape as "no usable prior state" and let the next owner-skill run rewrite it.
/workspace/group/session-state.jsonMutable JSON object. Per-group, not shared across containers.
{
"schema_version": 1,
"sessions": {
"<NANOCLAW_SESSION_NAME>": {
"started": "<ISO-8601 UTC, e.g. 2026-04-27T15:00:00Z>",
"epoch": <int unix seconds>,
"session_id": "<sqlite session_id from /workspace/store/messages.db, or null>",
"last_seen": "<ISO-8601 UTC>"
}
},
"session_id": "<top-level mirror of the active session_id, back-compat>",
"pending_response": null,
"seen_email_ids": [],
"muted_threads": {}
}| Field | Writers | Readers | Notes |
|---|---|---|---|
schema_version | register-session.py (owner) | All readers gate on this | See Schema versioning below |
sessions.<name>.* | register-session.py (owner) — own session's subtree | All readers may inspect any session | last_seen may be stamped by tessl__heartbeat for maintenance |
session_id (top-level) | register-session.py — both sessions on bootstrap | Legacy readers only | Back-compat; last-writer-wins is accepted |
pending_response | default session writes on inbound start; default clears on send; maintenance heartbeat clears stale entries | All trusted/main sessions | The pending-response-tracking rule governs the protocol |
seen_email_ids | tessl__check-email, tessl__heartbeat, tessl__morning-brief, tessl__nightly (all maintenance) | tessl__check-email for de-dup | Append-only within a window |
muted_threads | default session | default + maintenance | Per-thread mute map |
Back-compat note (legacy migration, ex–reference_session-state-migration.md): the top-level session_id field is the pre-PR jbaruch/nanoclaw#55 shape, when only one session existed per group. It is still written so readers that haven't moved to the per-session subtree continue to work. New readers SHOULD use sessions.<name>.session_id. Old single-session files are accepted on read — register-session.py adds the sessions subtree without dropping the top-level field, so the migration is in-place and idempotent.
Other writers on this file must take fcntl.LOCK_EX on /workspace/group/session-state.json.lock for the duration of their read-modify-write cycle. Current participants: jbaruch/nanoclaw-admin: tessl__heartbeat (writes last_seen, clears stale pending_response) and jbaruch/nanoclaw-admin: tessl__check-email (writes seen_email_ids, pending_response, muted_threads). Without the shared lock, concurrent updates clobber each other.
/tmp/session_bootstrappedPlain-text sentinel. One line: the value of $CLAUDE_SESSION_ID from the run that completed bootstrap.
needs-bootstrap.py compares this file's contents to the current $CLAUDE_SESSION_ID. Mismatch (or missing file) → bootstrap is needed. register-session.py REFUSES to write an empty sentinel because an empty value would match an empty env var on the next run and permanently suppress bootstrap.
session-state.json carries schema_version: 1 at the top level. v1 is the current canonical shape: schema_version + sessions.<name> subtree + back-compat top-level session_id. Files written before this field existed are read-tolerated by register-session.py (the owner skill) and silently upgraded to v1 on the next write — owner-skill migration per jbaruch/coding-policy: stateful-artifacts.
Reader skills (jbaruch/nanoclaw-admin: tessl__heartbeat, jbaruch/nanoclaw-admin: tessl__check-email) MUST treat an unknown future version (schema_version > 1) as "no usable prior state" and let the next register-session.py run perform the upgrade — never migrate from a reader.
/tmp/session_bootstrapped is a single-line plain-text sentinel; it has no envelope shape to version. The only behavioral contract is "non-empty content = bootstrap was completed for this $CLAUDE_SESSION_ID", and that contract is stable.
/workspace/trusted/user_profile.md — ## Addresses blockuser_profile.md is a canonical, special-case profile file with a fixed filename. It does NOT follow the general {type}_{slug}.md typed-memory naming convention in SKILL.md (e.g. user_travel-prefs.md); the travel-tile reader contract below resolves it by that exact name. It still uses type: user frontmatter, and its prose body is agent-read context like any other user file. In addition, it carries one machine-readable block that scripts parse directly — the canonical ## Addresses block. Owner skill is tessl__trusted-memory (this tile); the block is trusted-tile-owned per jbaruch/coding-policy: stateful-artifacts, and every other tile is a read-only consumer.
## Addresses
<!-- canonical, machine-read by travel tile; schema v2 — see trusted-memory state-schema.md -->
- schema_version: 2
- current_home: <current home street address>
- home_airport: <IATA code>
- home_metro: <City, Region>
- new_home_wip: <new-build street address>| Key | Required | Meaning | Mutability |
|---|---|---|---|
schema_version | Yes | Block shape version (currently 2). Bump on any shape change per jbaruch/coding-policy: stateful-artifacts. | Owner-only. |
current_home | Yes | The operator's current residence — the origin every home-anchored drive leg routes from. | Owner-updated. Switch to the new_home_wip value once that home is occupied. |
home_airport | Yes | Home IATA code (e.g. BNA). | Owner-updated. |
home_metro | No | The metro the operator lives in, spelled as TripIt labels a trip destination (<City>, <Region>, e.g. Nashville, TN). Repeat the line to name more than one label — the value carries its own comma, so a separator inside one value would be ambiguous. | Owner-updated. |
new_home_wip | No | New-build street address, not yet occupied. Absent once that home is occupied and its value has moved to current_home. | Owner-updated. Not auto-promoted to current_home — that is an explicit later edit. |
A migration never stamps a block that is missing a required key: publishing a record that claims the current shape while missing it sends readers down their block-is-readable path to find nothing. The check and its diagnostic are in skills/trusted-memory/scripts/migrate-addresses-block.py.
The block separates the address values that the surrounding prose conflates ("home base / new build"). Keep the prose for the agent; the block exists so script reads get an unambiguous single value per key.
schema_version: 2 is the current canonical shape (current_home + home_airport + home_metro + new_home_wip). Only the owner skill (tessl__trusted-memory) bumps it, and only the owner migrates the block — never a reader. Writer and reader ship through separate pipelines (writer here, reader in jbaruch/nanoclaw-travel), so bumps are coordinated per jbaruch/coding-policy: stateful-artifacts.
Every reader gates on schema_version and treats an unaccepted version as "no usable prior state" — jbaruch/nanoclaw-travel@0.2.116 accepts {1, 2} in skills/travel-core/addresses.py. What that no-prior-state path does is the consumer's own call: the drive-origin reader fails closed rather than guess an origin, the booking check reads no home metro and so checks every trip.
So every bump, additive included, follows one order: deploy the dual-accept reader → stamp the block → drop the superseded version from the readers' accepted set once no block can still carry it. A block stamped ahead of its readers reads as unusable everywhere, whatever the new key is.
current_home or changing its line shape. Same order, and the reader has to parse both shapes explicitly for the window.Additive: the block gains the optional home_metro key. jbaruch/nanoclaw-travel#271 — the travel-bookings brief nagged about missing bookings for local placeholder trips (a TripIt trip filed to block time for a Nashville event has no flight and no hotel to book). The destination now rides in travel-db.json, and home_metro is what it is compared against.
home_metro has no default value — it is absent until the operator names a metro, and absent means no trip is treated as local, which is the pre-v2 behaviour. That is what makes the travel-side reader dual-accept without a code change: it reads a v1 block as a v2 block whose home_metro is unset.
Owner-side migration (owner only, never a reader). skills/trusted-memory/scripts/migrate-addresses-block.py performs it — see its top-of-file docstring for what it reads, writes, emits, and refuses. SKILL.md Step 4 invokes it, and Step 1 (Bootstrap) reaches Step 4 every session, so the block migrates when the owner READS it rather than whenever someone next happens to edit the profile. Step 3 reaches it again after any write to user_profile.md.
Two contracts this schema owns rather than the script: no migration adds a home_metro line, because the key has no default; and migrating to a version is legal only once a reader accepting it is deployed, per the order above — jbaruch/nanoclaw-travel@0.2.116 satisfies that for v2. A reader that finds a v1 block never rewrites it: it reads it as v1 and waits for the owner.
| Field | Writer | Readers | Notes |
|---|---|---|---|
current_home | tessl__trusted-memory (owner) | jbaruch/nanoclaw-travel: drive-planner (read-only) | Origin for home-anchored drive legs. |
home_airport | tessl__trusted-memory (owner) | travel-tile consumers (read-only) | IATA code. |
home_metro | tessl__trusted-memory (owner) | jbaruch/nanoclaw-travel: check-travel-bookings (read-only) | Suppresses booking-gap reminders for trips to the operator's own metro. Absent ⇒ every trip is checked. |
new_home_wip | tessl__trusted-memory (owner) | deliberately ignored by drive-planner | Origin switches are an explicit later change, never an auto-pickup. |
Travel-tile reader contract (consumer-side). jbaruch/nanoclaw-travel's skills/drive-engine/home_address.py is the read-only consumer of current_home, over the shared block parse in that plugin's skills/travel-core/addresses.py. The contract this tile guarantees: a - current_home: <address> line under a ## Addresses heading. The reader refuses to guess on a missing or malformed block — it raises an actionable error pointing back at this skill, and drive-planner's sweep fails closed (no blocks created) until the block lands. Parsing details (the match pattern, whitespace tolerance) live in that script and its docstring/tests; owner-side reformatting MUST preserve the - <key>: <value> line shape, which a nanoclaw-travel fixture pins.
.tessl-plugin
rules
skills