Turn NemoHermes into an attention bridge for inbound conversations. This recipe builds a source-backed work wiki and surfaces the decisions, replies, and follow-ups that require the user's attention.
From Message Overload to the Work That Needs You
Work arrives across threads and providers. NemoHermes rebuilds the context needed to understand what changed and where the user must step in.
With this recipe, you can ask NemoHermes questions such as:
What changed on this project since yesterday?
Which conversations need a decision, response, or follow-up from me?
What are the most important items on my todo list today?
What should I know about this person or project, and how does it connect to my work?
NemoHermes turns the same inbound stream into one continuous workflow:
direct messages · group chats · channels · email
│
▼
living, source-backed work wiki
│ │
▼ ▼
“What changed?” “Where do I need to act?”
│ │
└────────────┬────────────┘
▼
ranked attention recommendations
▲
│
user pins · ignores · corrections
Answers show what changed, where the user is needed, why an item matters, and which sources support it. A message can update the wiki without becoming a todo; an urgent broadcast can rank below work the user has chosen.
Memory That Tracks Work, Not Just Preferences
Hermes built-in memory stores compact preferences, environment facts, and lessons across sessions.
This recipe tracks the work around the person in evolving, linked pages for people, projects, goals, patterns, concepts, and current attention. Its scheduled writer currently maintains people and attention pages from message evidence and user corrections. It also keeps the evidence behind non-obvious claims and the user's corrections to message-derived obligations.
Hermes built-in memory: “What should the agent know about me?”
This recipe's operational memory: “What changed around my work, what is the evidence, and where does my attention matter now?”
General personalization stays compact. Operational memory can grow with the work while remaining linked, source-backed, schema-checked, and repairable. The recipe complements Hermes memory and optional external memory providers.
Table of Contents
- From Message Overload to the Work That Needs You
- Memory That Tracks Work, Not Just Preferences
- How It Works
- Quick Start
- Connect Messaging Providers
- Offline Walkthrough (No Deployment)
- Configuration
- Scheduled Operation
- Data Lifecycle and Privacy
- Troubleshooting and FAQ
- Limitations and Support
- For Contributors
How It Works
Hermes built-in memory remains available for general identity, preferences, and learned facts. The recipe adds a workflow around three connected layers:
- Inbound signals bring new information from configured messaging providers into a source-neutral format.
- The work wiki organizes durable context about people, projects, goals, patterns, concepts, and current attention, with provenance and maintenance rules. Its scheduled writer currently maintains people and attention pages, and user-confirmed identity links connect the same person across providers.
- The attention layer uses that context to identify work that requires the user's response, decision, or follow-up; recommend priorities; and preserve the user's corrections over time.
configured IM providers ──► normalized inbound signals
│
┌──────────────────┴─────────────────┐
▼ ▼
self-maintaining work wiki attention recommendations
people · projects · goals · patterns ranked obligations · todos
│ ▲
└──► sourced retrieval │
│
user corrections ──► learned preferences
Hermes built-in memory ──► general profile and cross-session context
The model does not write SQL. It returns a versioned JSON envelope;
apply_decisions.py validates that envelope and applies it transactionally.
The offline walkthrough substitutes recorded envelopes only at the two points
where inference would otherwise be required. Everything downstream is the same
code used by scheduled runs.
Core concepts
| Term | Meaning |
|---|---|
| Profile home | One Hermes profile directory, named by HERMES_HOME |
| Hermes built-in memory | Bounded USER.md and MEMORY.md notes for general personalization and learned facts |
| Work wiki | Schema-checked pages for people, projects, goals, patterns, concepts, and attention |
| Provenance | Source, evidence date, and trust attached to non-obvious wiki claims |
| Attention layer | Ranked recommendations for inbound work that may require the user's response, decision, or follow-up |
| Obligation ledger | Message judgments, corrections, and audit history at $HERMES_HOME/workspace/ledger/state.db |
| Obligation | A source message that still needs a response or action |
| Intent gate | Proof in memory that the user chose the work; required for high |
| Wake gate | The last non-empty selector line that tells Hermes not to call the model |
| Envelope | Versioned JSON containing model decisions for the transactional writer |
Quick Start
This is the path that ends with a running, scheduled assistant. Steps 1 through 5 are what you run, in order, to get there. Step 6 is optional — connect a real Slack or Outlook source instead of leaving the store empty. Step 7 runs nothing new; it explains how to look at what steps 1-5 already started.
The scheduled jobs run inside a Linux NemoHermes sandbox. Your own computer is outside that sandbox and can use any platform supported by NemoHermes.
This guide uses two command locations:
| Location | What it means | CLI available there |
|---|---|---|
| Your machine, outside the sandbox | The terminal with the repository checkout that you use to manage NemoHermes | nemohermes, openshell |
| NemoHermes sandbox | The isolated Linux environment where Hermes and the scheduled jobs run | hermes |
A plain "gateway" in this guide always means Hermes's own hermes gateway
process — the agent-serving process for a profile, started explicitly in
step 5. That is a different system from the OpenShell gateway, the
daemon that manages this sandbox's provider credentials and refresh-token
rotation (see docs/set-up-graph.md and
docs/set-up-slack.md); this guide always spells that
one out by name.
The sandbox satisfies the recipe's Linux runtime requirement. The current provider setup helpers still require Linux or WSL on your machine; this is a helper-script limitation, not a recipe runtime requirement.
scripts/install.sh always runs inside the sandbox. Your machine is not
expected to have hermes on PATH.
1. Install prerequisites
This recipe assumes an existing NemoClaw-managed Hermes sandbox; it does not
cover NemoClaw onboarding itself. If NemoClaw is not installed, follow the
upstream NemoClaw setup guide and select
Hermes, an inference provider, and a model during onboarding. That installer
also installs openshell as part of its own setup — Quick Start uses both
nemohermes and openshell on your machine below, and neither needs a
separate install.
Then, on your machine (not inside the sandbox), clone this repository and enter it:
git clone https://github.com/NVIDIA/nemoclaw-community.git
cd nemoclaw-community
The remaining Quick Start steps run from this checkout.
2. Confirm and inspect a Hermes sandbox from your machine
nemohermes my-hermes status
openshell sandbox provider list my-hermes
Replace my-hermes in this section with your registered sandbox name.
Before starting scheduled intake, inspect every provider already attached to the sandbox:
openshell provider get "<provider-name>"
Do not continue if an unrelated provider exposes MS_GRAPH_ACCESS_TOKEN.
The current Graph collector can see the injected environment value but cannot
identify which attached provider supplied it. Use a dedicated sandbox or
detach the conflicting provider before installing or starting the scheduled
runtime.
3. Upload the recipe from your machine
Run this from the nemoclaw-community checkout from step 1.
nemohermes sandbox upload my-hermes \
examples/recipes/nvidia/memory-driven-chief-of-staff \
/sandbox
nemohermes my-hermes connect
The upload is required: the sandbox cannot see the checkout on your machine.
After connect opens a shell, the remaining commands in this subsection run
inside the sandbox.
4. Install the profile and jobs inside the sandbox
cd /sandbox/memory-driven-chief-of-staff
export PROFILE_NAME="memory-driven-chief-of-staff"
bash scripts/install.sh
The installer creates the target profile and copies only model.default,
model.provider, and model.base_url. It never copies a credential. On a first
run without a target-profile credential, it
stops before registering jobs.
In a NemoClaw-managed sandbox, set the non-secret OpenShell rewrite sentinel, then rerun the installer:
hermes -p "$PROFILE_NAME" config set model.api_key \
"sk-OPENSHELL-PROXY-REWRITE"
bash scripts/install.sh
The sentinel is not an upstream API key. Hermes requires an sk--prefixed
value before it sends a request, and OpenShell removes this marker and
injects the managed inference credential at the egress boundary. Use the
exact sentinel above rather than another placeholder — do not copy a real
inference key into the recipe profile on the supported NemoClaw path.
Not on NemoClaw, and your endpoint genuinely needs no key? This opt-out is not a substitute for the rewrite sentinel above — use it only for a genuinely keyless, non-NemoClaw endpoint.
bash ALLOW_NO_API_KEY=1 bash scripts/install.sh
5. Start and verify the scheduled runtime inside the sandbox
NemoClaw's own supervisor launches and continuously respawns hermes gateway
run for the sandbox's default profile automatically, logging to
/tmp/gateway.log under a 0600 file it owns. This recipe installs as a
separate, named profile (memory-driven-chief-of-staff), which that
supervision does not cover — there is no NemoClaw-managed lifecycle for a
second named-profile gateway today, so this step starts it manually. Hermes
records runtime state for the named profile, and its profile-scoped stop
command validates that runtime identity before it signals the process. The
separate log path below avoids colliding with the managed gateway's log. Check
first whether the named-profile gateway is already running:
hermes -p "$PROFILE_NAME" cron status
If it reports the gateway is not running, start it in the background,
logging to its own path — reusing /tmp/gateway.log would collide with
NemoClaw's managed one — and make the new log owner-only. Unlike NemoClaw's
own gateway, nothing restarts this one automatically. If it dies because of a
crash, an out-of-memory event, or a sandbox restart, cron stops firing until
you rerun this command:
umask 077
nohup hermes gateway run --profile "$PROFILE_NAME" \
> /tmp/mdcos-gateway.log 2>&1 &
disown
hermes -p "$PROFILE_NAME" cron status
Piece by piece: umask 077 makes the log file the next line creates
owner-only (0600), matching NemoClaw's own gateway log. nohup … &
backgrounds the process and makes it immune to hangup, so it keeps running
after you disconnect from the sandbox shell. The redirect targets
/tmp/mdcos-gateway.log — named differently from NemoClaw's own
/tmp/gateway.log specifically so the two logs don't collide, since NemoClaw
already writes its managed profile's gateway output there. disown then
drops the job from the shell's job table too, on top of what nohup already
protects against. The final cron status re-runs the same check from above,
now that the gateway should actually be up.
cron status should now report the gateway running and the next scheduled
job. If it still reports not running, check /tmp/mdcos-gateway.log for a
startup error.
The seven registered jobs drive distinct Hermes skills — intake runs
inbound-judging, review runs obligation-review, and there are separate
memory-writing, memory-repair, memory-consolidation, and
preference-update skills; retention runs no skill at all. See
Scheduled Operation for the full schedule and
job-to-skill table.
The work wiki those skills write lives inside the sandbox at
$HERMES_HOME/workspace/memory/ — concretely
/sandbox/.hermes/profiles/$PROFILE_NAME/workspace/memory/ for this recipe's
default profile name. The obligation ledger the intake and review jobs work
from lives alongside it, at $HERMES_HOME/workspace/ledger/state.db.
cron statusshows the gateway running but no scheduled job? The jobs were never registered — for example if step 4 stopped at the credential check before reaching its3/3 Registering scheduled jobsphase. Do not runregister-jobs.shdirectly to fix that: it checks only the platform and that the profile exists, not that a credential is set, so it would schedule all seven jobs against a profile that fails authentication on every run. Verify the credential first, then rerun the installer, which registers jobs as its own last step once the credential check passes:
bash hermes -p "$PROFILE_NAME" config get model.api_key bash scripts/install.shOnce you know the credential is already set,
bash scripts/register-jobs.shon its own is safe to resync job definitions — it is idempotent, so rerunning it updates existing jobs in place rather than duplicating them.
To remove every registered job at once instead of one at a time, see Persistence and reboot behavior.
To stop it later, use Hermes's profile-scoped lifecycle command:
hermes gateway stop --profile "$PROFILE_NAME"
Hermes scopes the stop to the selected profile and checks its recorded runtime identity instead of trusting a shell PID file. Deleting the profile already stops this gateway and its related backends as part of deletion — an explicit stop first is not required, but it is harmless and can make teardown easier to verify.
Running this outside a NemoClaw sandbox, on a host with systemd? Any environment without a running systemd — WSL included — still needs the foreground command above. Only where systemd is actually running does
gateway installregister it as a persistent service instead. See Persistence and reboot behavior for the trade-off againstgateway run.
bash hermes gateway install --profile "$PROFILE_NAME" hermes gateway start --profile "$PROFILE_NAME"
At this point the schedule works over any rows already in the store. Slack and Outlook are independent and optional; each unconfigured collector exits successfully and reports that state.
6. Connect a messaging provider (optional)
Step 5 leaves you inside the sandbox shell. Exit it (or open a separate
terminal on your machine) before continuing — openshell and nemohermes
are host-side commands and are not available inside the sandbox.
Nothing so far requires a provider — the profile, jobs, and gateway all work with an empty store. But an empty store is exactly what you will see if you chat with it next: no messages ingested, nothing to rank. Check what is already attached first, from your machine:
openshell sandbox provider list my-hermes
If nothing there exposes SLACK_USER_TOKEN or MS_GRAPH_ACCESS_TOKEN,
connect one now — see Connect Messaging Providers
below for the full Slack and Outlook setup. Both are optional and
independent; connect one, both, or skip this step and use the
offline fixtures instead to see the
recipe's behavior without connecting anything.
7. A note on the web dashboard (reference — nothing to run)
Nothing in this step is required to finish Quick Start; it explains what is already reachable from steps above and how to look at it.
From your machine, not inside the sandbox, this prints the URL for the Hermes dashboard NemoClaw already forwards for the sandbox by default — already reachable, with no forwarding step of your own:
nemohermes my-hermes dashboard-url --quiet
That dashboard is a machine-level surface: a profile switcher lets it
manage any profile on the sandbox, including this recipe's
memory-driven-chief-of-staff. --isolated (used when NemoClaw launches
this dashboard) changes only how the hermes dashboard command routes on
launch — whether it reuses an already-running shared server or starts its
own — not what a running dashboard's pages can reach. Selecting this
recipe's profile, through the switcher or a
?profile=memory-driven-chief-of-staff URL, routes Config, API Keys,
Skills, MCP, Models, and Chat to it — including starting a brand-new chat
session as it, from the browser. Sessions is likewise scoped to whichever
profile is currently selected, rather than showing every profile at once.
Cron and Profiles are the two pages that are not scoped by the switcher:
- Cron defaults to showing every registered job across every profile, with its full prompt text, schedule, delivery target, and Pause/Resume/Edit/Trigger-now/Delete.
- Profiles always lists every profile globally as a management card
(model, skill count, gateway state, description), with Rename and
Delete — deleting from here removes the workspace, store, and memory
the same as
hermes profile deletedoes. See Persistence and reboot behavior.
So this one forwarded URL already gives you full access to this recipe's profile once you select it — chat, config, sessions, plus the always-visible cron jobs and destructive profile management — with no extra step. Hermes uses session auth for this dashboard, so the printed URL is a plain link, not a credential, but treat it as sensitive regardless: protect access to it through the sandbox's normal session controls, and do not expose it publicly.
If you don't have that URL handy, it is safe to rerun the command above at any time.
Prefer the terminal to the browser? This is the verified way to talk to this recipe directly, from inside the sandbox — no dashboard, no forwarding, nothing above needed:
hermes -p memory-driven-chief-of-staff chat
Things to try
Once real data is flowing, ask it the questions from the top of this document:
What changed on this project since yesterday?
Which conversations need a decision, response, or follow-up from me?
What are the most important items on my todo list today?
What should I know about this person or project, and how does it connect to my work?
Then push on the ranking itself — this is where the intent gate shows up:
- "Why is this ranked above that?"
- "What's urgent right now that I haven't actually chosen to work on?"
Ask it to ground a claim in evidence rather than answer from general recall:
- "What should I know about [a real colleague]? How does it connect to my current work?"
- "What did [someone] say this week?"
And after you correct something (priority ... low or ignore ... via
profile/scripts/correct.py), ask again without re-explaining the
correction — it should already be reflected.
Connect Messaging Providers
Slack and Microsoft Outlook are independent, optional inputs. Configure either one or both after installing the recipe and setting any intake exclusions. Each setup script requires encrypted sandbox storage and attaches a read-only OpenShell provider to the NemoHermes sandbox.
Both setup scripts require SANDBOX_STORAGE_PATH: the host-side path
where this sandbox's storage actually lives, not a path inside the sandbox
and not $HOME. Once a connector is attached, real message subjects,
senders, and bodies land in the store, and that requires the underlying
volume to be encrypted — a check require-encrypted-storage.sh cannot make
from inside the sandbox, since HERMES_HOME there is an overlay filesystem
with no block device behind it. There is no default: where the sandbox's
storage actually lives depends on the driver. For the Docker driver, discover
it with docker info --format '{{.DockerRootDir}}' (commonly
/var/lib/docker on a stock install, but not guaranteed — always verify
rather than assume). See docs/encrypted-storage.md
for the VM and Kubernetes drivers and how to verify the volume is encrypted.
Profile configuration
The installer has already created the target profile from
profile/distribution.yaml. Its model block should have this shape:
model:
default: "<provider/model>"
provider: "<provider>"
base_url: "<https-endpoint>"
api_key: sk-OPENSHELL-PROXY-REWRITE
The api_key value is the non-secret routing marker configured during
installation. OpenShell replaces it at egress; it is not a credential to rotate
or hide.
Provider setup now continues on your machine, outside the sandbox. The current setup helpers require a Linux shell; use WSL on Windows.
Slack
Slack setup is optional. Complete it only after placing the sandbox storage on an encrypted volume; owner-only permissions are access control, not encryption. See docs/encrypted-storage.md first.
Run the setup script from the recipe checkout on your machine, not inside
the sandbox. Your machine has openshell; the sandbox has hermes.
export SANDBOX_STORAGE_PATH="<path-containing-sandbox-storage>"
export OPENSHELL_SANDBOX_NAME="my-hermes"
bash scripts/setup-slack.sh
The script imports docs/slack_app_manifest.json, requests user scopes, checks the provider type and read-only policy, configures token rotation, and attaches the provider to the named sandbox. The required scopes are:
user_scopes:
- im:read
- im:history
- mpim:read
- mpim:history
- channels:read
- channels:history
- users:read
Static user tokens, bot tokens, and app tokens are refused. Attachments are not downloaded. Full setup and workspace-admin recovery steps are in docs/set-up-slack.md.
Verify the live collector from your machine through the supported sandbox exec path. Replace both placeholders.
nemohermes my-hermes exec \
--workdir /sandbox/memory-driven-chief-of-staff \
-- env HERMES_HOME=/sandbox/.hermes/profiles/memory-driven-chief-of-staff \
python3 profile/scripts/ingest_slack.py --recheck
Collector exit codes are stable diagnostics:
| Exit | Meaning |
|---|---|
0 |
Fetch succeeded, or Slack is intentionally unconfigured |
1 |
Other collector error |
2 |
Missing/wrong credential type or unreachable API |
3 |
Slack rate limit |
4 |
Required Slack scope missing |
Microsoft Outlook
Outlook intake uses the Microsoft Graph inbox delta API. Register a Microsoft
Entra application with public-client flows enabled and delegated Mail.Read,
User.Read, and offline_access permissions. Do not grant application-level
mail permissions, which would authorize access beyond the signed-in mailbox.
Run the device-code setup from the recipe checkout on your machine:
export SANDBOX_STORAGE_PATH="<path-containing-sandbox-storage>"
export OPENSHELL_SANDBOX_NAME="my-hermes"
export GRAPH_CLIENT_ID="<entra-application-client-id>"
export GRAPH_TENANT_ID="<entra-directory-tenant-id>"
bash scripts/setup-graph.sh
If the script reports that another attached provider already exposes
MS_GRAPH_ACCESS_TOKEN, treat that as a hard stop. Detach the conflicting
provider or use a dedicated sandbox, then rerun setup. Do not run the Graph
collector while the credential source is ambiguous.
On the supported path, the OpenShell gateway stores and refreshes the
delegated credential, and the sandbox receives only an OpenShell placeholder. Confirm
that the attached provider has type memory-driven-cos-graph-user and that its
exported profile declares access: read-only with enforcement: enforce before
running the collector. The initial synchronization covers seven days by default
and resumes across intake ticks when more pages remain. Configure a different
GRAPH_BACKFILL_DAYS in the profile environment file before the first
synchronization. See docs/set-up-graph.md for the
application registration, provider inspection, backfill, revocation, and
recovery steps.
Link identities across providers
Slack identifies a person by user ID, while email uses an address. The recipe
never assumes that matching display names belong to the same person. The memory
job reports likely matches as identity_candidates; only the user can confirm
or reject them.
Run the identity command from your machine through sandbox exec:
nemohermes my-hermes exec \
--workdir /sandbox/memory-driven-chief-of-staff \
-- env HERMES_HOME=/sandbox/.hermes/profiles/memory-driven-chief-of-staff \
python3 profile/scripts/link_identity.py same \
slack:U01DANA email:dana@example.com
Use different instead of same to reject a match. Confirmed relationships
compose across providers. Rejected candidates are not proposed again. If saved
answers conflict, the recipe reports identity_conflicts and changes nothing.
Existing page names remain stable after identities are linked so that index
entries and source-backed links do not move.
Collector failures
When a collector fails, the intake batch records only its name, exit code, and error class. It does not place collector output in the model prompt or the scheduler log because that output can contain message text or authentication material. Run the collector directly to inspect its full error.
Verify the collector from your machine:
nemohermes my-hermes exec \
--workdir /sandbox/memory-driven-chief-of-staff \
-- env HERMES_HOME=/sandbox/.hermes/profiles/memory-driven-chief-of-staff \
python3 profile/scripts/ingest_graph.py --recheck
Outlook collector exit codes are:
| Exit | Meaning |
|---|---|
0 |
Fetch succeeded, can resume, or Outlook is unconfigured |
1 |
Other collector error |
2 |
Missing, invalid, or refused credential |
3 |
Microsoft Graph rate limit |
4 |
Token is not a delegated mailbox token with the required scopes |
Offline Walkthrough (No Deployment)
This does not deploy the recipe or produce a running assistant. It runs the decision logic — ranking, the intent gate, correction durability, memory self-checks — entirely on your own machine against recorded fixtures, with no NemoClaw, no sandbox, no credentials, and no network access. Use it to verify the recipe's behavior before investing in a real deployment, or skip straight to Quick Start above if you already have a sandbox ready.
It needs only Python 3.10+ on macOS, Linux, or WSL.
1. Clone and enter the recipe
git clone https://github.com/NVIDIA/nemoclaw-community.git
cd nemoclaw-community/examples/recipes/nvidia/memory-driven-chief-of-staff
2. Create isolated local state and run the walkthrough
export RECIPE_TMP_HOME="$(mktemp -d)"
export HERMES_HOME="$RECIPE_TMP_HOME"
python3 profile/scripts/walkthrough.py --fixtures fixtures
The command prints seven stages and exits with status 0. The important
outcomes are:
- eight messages are ingested and two are skipped;
- six obligations remain open;
- exactly three memory-gated obligations enter
high; - an urgent deadline unrelated to the user's chosen work stays in
medium; - a user pin and ignore survive a later recorded review;
- the memory checker is shown succeeding and failing on a deliberate defect.
3. Prove fixture ingestion is idempotent
Use a new profile home because the walkthrough has already populated the first one.
export RECIPE_TMP_HOME_2="$(mktemp -d)"
export HERMES_HOME="$RECIPE_TMP_HOME_2"
python3 profile/scripts/load_fixtures.py --fixtures fixtures
python3 profile/scripts/load_fixtures.py --fixtures fixtures
The first loader run reports "added": 8; the second reports "added": 0.
4. Inspect or correct state
Replace the placeholder with a source identifier printed by the walkthrough.
python3 profile/scripts/memory_check.py
python3 profile/scripts/correct.py priority msg-priorities-match low
python3 profile/scripts/correct.py ignore msg-cc-only
python3 profile/scripts/correct.py unignore msg-cc-only
Repeated corrections are no-ops. Corrections against completed or incompatible
rows exit with status 3 and explain the required state transition.
Configuration
Environment variables
| Name | Location | Required | Default / valid range | Meaning |
|---|---|---|---|---|
HERMES_HOME |
Offline or sandbox | Yes for stateful scripts | No default | Existing Hermes profile home; state is written below workspace/ |
PROFILE_NAME |
Sandbox installer | No | memory-driven-chief-of-staff |
Target profile for installation and cron registration |
ALLOW_NO_API_KEY |
Sandbox installer | No | 0; set to 1 only for a genuinely keyless non-NemoClaw endpoint |
Explicit credential-check bypass; do not use for a managed NemoClaw route |
INTAKE_SLICE |
Scheduled runtime | No | 25; integer 1..200 |
Maximum pending rows given to an intake turn |
REVIEW_BATCH |
Scheduled runtime | No | 15; integer 1..200 |
Maximum open rows given to a review turn |
INTAKE_SLACK_BUDGET |
Scheduled runtime | No | 10; integer 1..200 |
Maximum Slack history calls per intake tick |
MEMORY_WINDOW_DAYS |
Scheduled runtime | No | 30; integer 1..3650 |
Evidence window used by the memory-writing selector |
RETENTION_DAYS |
Scheduled runtime | No | 30; integer 1..3650 |
Age at which stored message bodies are cleared |
SLACK_USER_TOKEN |
OpenShell provider | Injected | Rotating user token | Collector credential; do not set manually on the supported path |
MS_GRAPH_ACCESS_TOKEN |
OpenShell provider | Injected | Rotating delegated token | Outlook collector credential; the collector cannot currently attest which attached provider supplied it |
GRAPH_BACKFILL_DAYS |
Scheduled runtime | No | 7; integer 1..3650 |
Initial Outlook mailbox synchronization window |
GRAPH_CLIENT_ID |
Outlook setup on your machine | Yes | No default | Microsoft Entra application client ID |
GRAPH_TENANT_ID |
Outlook setup on your machine | No | common |
Microsoft Entra directory tenant ID |
GRAPH_PROVIDER_NAME |
Outlook setup on your machine | No | memory-driven-cos-graph |
Name used when creating the recipe provider; it does not resolve a conflicting attached provider that exposes the same credential key |
SANDBOX_STORAGE_PATH |
Provider setup on your machine | Yes | No default | Path whose encryption status protects sandbox storage |
OPENSHELL_SANDBOX_NAME |
Provider setup on your machine | No | Falls back to SANDBOX_NAME, then hermes |
Sandbox receiving the provider |
SANDBOX_NAME |
Provider setup on your machine | No | hermes |
Compatibility fallback for the sandbox name |
SLACK_PROVIDER_NAME |
Slack setup on your machine | No | memory-driven-cos-slack-user |
OpenShell provider name |
STORE_ENCRYPTION_ACKNOWLEDGED |
Provider setup on your machine | No | 0; unattended confirmation is 1 |
Acknowledges an encryption result the script cannot prove |
FORCE_REAUTH |
Provider setup on your machine | No | 0; replacement is 1 |
Replaces an attached rotating provider credential |
Scheduled environment variables must reach the target profile. Persist a value through the profile environment file returned by Hermes rather than relying on a temporary shell export. See docs/data-lifecycle.md for retention and memory settings, and docs/set-up-graph.md for Outlook backfill.
Public Slack channels
Direct messages and group DMs are discovered automatically. Public channels
are read only when explicitly listed at
$HERMES_HOME/workspace/slack_channels.json.
{
"channels": ["C0TEAM0001", "C0PROJECT2"]
}
Ingest exclusions
Create $HERMES_HOME/workspace/exclusions.json to prevent matching rows from
ever reaching the store. Matching is exact and case-insensitive; glob patterns
are not supported. Invalid or unknown fields fail closed.
{
"senders": ["recruiter@agency.example", "U01RECRUIT"],
"domains": ["agency.example"],
"channels": ["C0SALARY01", "D0PRIVATE1"]
}
Scheduled Operation
scripts/register-jobs.sh is idempotent: it edits jobs with matching names
instead of creating duplicates.
| Job | Schedule | Pre-step | Skill |
|---|---|---|---|
| intake | every 30 minutes | select_intake.py |
inbound-judging |
| review | every 6 hours | select_review.py |
obligation-review |
| memory writing | daily 01:00 | select_memory.py |
memory-writing |
| retention | daily 02:00 | retention.py |
— |
| memory repair | daily 03:00 | — | memory-repair |
| memory consolidation | daily 04:00 | — | memory-consolidation |
| preference update | daily 04:30 | — | preference-update |
Intake, review, and memory writing run their selector before an agent turn. If no work is available, the selector's final non-empty line is the wake gate and Hermes skips inference. Retention never wakes the agent. Memory writing runs before repair and consolidation so every new page is checked and compacted in the same nightly sequence.
Persistence and reboot behavior
Jobs live in $HERMES_HOME/cron/jobs.json. The distribution does not own the
cron path, so profile updates leave job definitions and run history unchanged.
Jobs survive a reboot. Do they resume automatically?
Only if the gateway was installed as a service with gateway install. A
gateway started with gateway run is a foreground process and must be started
again after a reboot.
What happens to recurring runs missed while the gateway was down? One of them runs, not the entire backlog. Hermes advances the schedule and runs once over current state. A machine that was off for two days does not wake to ninety-six intake runs; it wakes to one and then resumes its half-hourly schedule.
List or remove registered jobs inside the sandbox.
hermes -p memory-driven-chief-of-staff cron list
JOB_ID="<copy-an-id-from-the-list>"
hermes -p memory-driven-chief-of-staff cron remove "$JOB_ID"
There is no single cron subcommand that removes every job at once — cron
list prints a table for a person, not machine-readable output. To remove all
of them, read each job's id from the profile's own job store instead, the same
file register-jobs.sh itself reads to stay idempotent, and remove them in a
loop:
PROFILE_HOME="$(hermes profile show memory-driven-chief-of-staff \
| sed -n 's/^Path:[[:space:]]*//p')"
python3 -c '
import json, sys
with open(sys.argv[1], encoding="utf-8") as handle:
data = json.load(handle)
for job in data.get("jobs", []):
print(job["id"])
' "$PROFILE_HOME/cron/jobs.json" | while read -r JOB_ID; do
hermes -p memory-driven-chief-of-staff cron remove "$JOB_ID"
done
Deleting the profile also deletes its workspace, store, and memory. Hermes already stops the profile's gateway and related backends as part of deletion, so a separate stop is not required — but if you started one by hand (step 5 of Quick Start), stopping it explicitly first is harmless and makes teardown easier to verify:
hermes gateway stop --profile memory-driven-chief-of-staff
hermes profile delete memory-driven-chief-of-staff
Data Lifecycle and Privacy
Network boundary: the offline walkthrough makes no network requests.
Scheduled jobs call the configured inference endpoint only when work is found.
The shipped provider profiles declare read-only access to slack.com and
graph.microsoft.com, and the shipped collectors contain no source-system
write operations.
Runtime provenance limitation: the effective network boundary is the policy of the provider actually attached to the sandbox, not the YAML file in this checkout.
ingest_graph.pycurrently trusts whichever attached provider suppliesMS_GRAPH_ACCESS_TOKEN; it does not verify the provider name, type, or policy at runtime. A different provider exposing the same key can therefore bypass the setup-time refusal. Inspect the attached providers on your machine and do not run Graph intake unless the effective provider is the recipe's read-only, enforced profile.
Source-system behavior: the shipped collectors read Slack and the signed-in Outlook mailbox but do not post, reply, edit, delete, move, or mark messages as read. This describes the collector code; it is not a claim that an unrelated read-write provider attached to the same sandbox would refuse writes from other code.
- The offline fixtures are entirely synthetic and make no network request.
- Recipient lists are reduced to
direct,mentioned, orbroadcastand are never stored. - Message bodies are cleared after 30 days by default; metadata, obligation state, and audit history remain.
- Exclusions are enforced at the shared insert boundary, before a row reaches the store.
- Export includes the complete store, memory, and learned policy in Markdown and JSON.
- Slack content deleted at the source is not detected immediately because a bounded history read cannot distinguish deletion from an older page. It ages out through retention.
- Outlook messages removed from the inbox are reconciled through Microsoft Graph. A confirmed deletion is tombstoned and its body is cleared immediately; the metadata, obligation, and audit history remain. A move to another folder is not treated as a deletion.
- Microsoft Graph files in
fixtures/remain synthetic and exercise the same normalization path without contacting a mailbox.
The store and memory directories are created with owner-only permissions. That does not protect a lost disk, disk image, or unencrypted backup. Before connecting Slack or Outlook, follow docs/encrypted-storage.md.
Troubleshooting and FAQ
HERMES_HOME is unset, missing, or points at the checkout
Stateful scripts require an existing profile-like directory and refuse a file, a missing path, or an occupied non-profile directory. For an offline run, make a fresh directory and export it first.
export HERMES_HOME="$(mktemp -d)"
The walkthrough refuses a second run
The walkthrough demonstrates a clean first run and rejects state that contains old corrections. Create a new temporary profile home.
export HERMES_HOME="$(mktemp -d)"
python3 profile/scripts/walkthrough.py --fixtures fixtures
Collector tests try to reach the real network or report HTTP 503
The Slack and Outlook tests use local HTTP stand-ins at 127.0.0.1. If your
shell sets a proxy without a localhost bypass, the proxy can intercept those
requests. The documented test command extends both NO_PROXY and no_proxy;
for an individual test, apply the same bypass first.
export NO_PROXY="${NO_PROXY:+$NO_PROXY,}127.0.0.1,localhost"
export no_proxy="${no_proxy:+$no_proxy,}127.0.0.1,localhost"
python3 profile/scripts/tests/test_ingest_slack.py
python3 profile/scripts/tests/test_ingest_graph.py
The installer says the platform is unsupported
The offline path runs on macOS, Linux, and WSL. Scheduled skills require the
Linux environment inside a NemoHermes sandbox. If scripts/install.sh reports
another platform, run it inside the sandbox rather than on your machine.
The installer reports no model or API credential
The new profile receives three model routing settings but never a copied
secret. Set model.default if the source profile had none. On NemoClaw, set the
OpenShell rewrite sentinel as the target profile's model.api_key, then rerun
the installer.
MODEL_ID="<provider/model>"
hermes -p memory-driven-chief-of-staff \
config set model.default "$MODEL_ID"
hermes -p memory-driven-chief-of-staff \
config set model.api_key "sk-OPENSHELL-PROXY-REWRITE"
bash scripts/install.sh
The sentinel is a non-secret routing marker. Do not use
ALLOW_NO_API_KEY=1 for a NemoClaw-managed inference route.
How do I check whether my API key actually works, and replace it if not?
The real key does not live in this recipe's profile — the sentinel above is not a credential. Check and rotate it at the NemoClaw/OpenShell layer, from your machine, not inside the sandbox:
nemohermes my-hermes inference get
This reports the configured provider and model, but sends no request, so it
cannot tell you whether the key is valid. The global nemohermes status
(no sandbox name) cannot either — that reachability check counts HTTP
401/403 as "reachable." The named-sandbox form does validate it, because
it sends one real inference request through the stored credential:
nemohermes my-hermes status
It reports unauthorized when that request is rejected with 401/403.
For a final, recipe-level confirmation, also try a real chat turn inside the
sandbox:
hermes -p memory-driven-chief-of-staff chat
A real reply means the key works; an authentication error means it does not.
If it is wrong, replace it from your machine — do not edit the recipe
profile's model.api_key, which only ever holds the non-secret rewrite
sentinel. Export the replacement under the environment variable name your
configured provider expects, then rerun onboarding for the existing sandbox:
printf 'New inference API key: ' >&2
IFS= read -r -s PROVIDER_API_KEY_VAR_NAME
printf '\n' >&2
export PROVIDER_API_KEY_VAR_NAME
nemohermes onboard --name my-hermes \
--non-interactive --yes --yes-i-accept-third-party-software
unset PROVIDER_API_KEY_VAR_NAME
PROVIDER_API_KEY_VAR_NAME above is a placeholder — substitute the actual
variable name your provider expects (this differs by provider; NemoClaw's
own credential rotation guide lists them). This updates the registered
OpenShell provider and normally reuses the existing sandbox; it does not
touch this recipe's installed profile or its scheduled jobs. Confirm the
replacement the same way — nemohermes my-hermes status or a real request,
not inference get or the global status.
Jobs are registered but do not run
Registration does not start the scheduler. Check the gateway and cron status.
hermes gateway status --profile memory-driven-chief-of-staff
hermes -p memory-driven-chief-of-staff cron status
Install and start the service, or use the foreground command shown in Quick Start.
A collector reports unconfigured
That is a supported state. The scheduler continues over existing store rows.
Enable the source by running scripts/setup-slack.sh or
scripts/setup-graph.sh on your machine, then run its collector with
--recheck through sandbox exec.
Provider setup says openshell is missing
The setup command is running inside the sandbox or on a machine without OpenShell. Return to the checkout on your machine and confirm the CLI and sandbox name.
command -v openshell
openshell sandbox list
Slack is rate-limited or missing a scope
The collector exits 3 for a rate limit and 4 for a missing scope. Reduce
the named public channels or call budget for rate limits. For missing scopes,
have the workspace administrator grant the manifest scopes, reinstall the app,
and replace the rotating credential as described in
docs/set-up-slack.md.
Outlook synchronization is incomplete, rate-limited, or rejected
"complete": false means the bounded initial synchronization will resume on
the next intake tick. Exit 3 means Microsoft Graph rate-limited the current
round. Exit 4 means the token is not a delegated mailbox token with the
required Mail.Read and User.Read scopes. Follow the recovery steps in
docs/set-up-graph.md.
Graph setup refuses an attached provider with the same credential key
Treat the refusal as a provider collision. From your machine, inspect and detach the conflicting provider or use a dedicated sandbox before starting Graph intake.
openshell sandbox provider list my-hermes
openshell provider get "<provider-name>"
Does the recipe modify Outlook or Slack?
The shipped collectors do not. They normalize source records into a local
SQLite store and contain no post, reply, move, delete, or mark-as-read calls.
The shipped provider profiles also declare read-only access. However, verify
the provider actually attached to the sandbox: the current Graph collector
cannot attest which provider supplied MS_GRAPH_ACCESS_TOKEN, so a foreign
read-write provider would make the platform-level write-refusal claim false.
Limitations and Support
- Scheduled jobs require the Linux environment inside a NemoHermes sandbox. The current provider setup helpers require Linux or WSL on your machine.
- Recorded judgment turns test the workflow, not model quality.
- Live connectors currently cover Slack and a Microsoft Outlook inbox through Microsoft Graph. Other messaging providers need their own collector and OpenShell provider policy.
- Graph credential provenance is not yet enforced at collector runtime. An
unrelated attached provider exposing
MS_GRAPH_ACCESS_TOKENcan be used even aftersetup-graph.shrefuses it. Use a dedicated sandbox or remove the collision before enabling Graph intake. - Slack deletion is not observed immediately; retention clears old bodies.
- The scheduled writer maintains people and attention pages. Other page types have schemas but no automatic writer.
- Memory compaction is model-guided.
memory_check.pydetects invariant and ceiling violations but does not invent a safe consolidation. - Append-only reranking events are not compacted.
- This is a reference recipe for one user's work stream on a machine they control, not a hosted service or a production-readiness claim.
This NVIDIA-authored recipe was proposed and reviewed in NemoClaw Community #122. Support is best effort under the repository support policy. Security reports should follow SECURITY.md.
For Contributors
The following sections document the implementation, contracts, and evidence used to review or extend this recipe.
Project Structure
Paths below are relative to this recipe directory.
memory-driven-chief-of-staff/
├── README.md # Start here: setup, operation, and API contract
├── profile/
│ ├── distribution.yaml # Version, Hermes requirement, owned paths
│ ├── SOUL.md # Chief-of-staff persona and response boundary
│ ├── schema.md # Memory page types, provenance, decay, ceilings
│ ├── seed/ # Initial index and attention pages for a new memory
│ ├── scripts/
│ │ ├── schema.sql # Current v5 SQLite store schema
│ │ ├── schema-v1.sql # Frozen schemas used by migration tests
│ │ ├── schema-v2.sql
│ │ ├── schema-v3.sql
│ │ ├── schema-v4.sql
│ │ ├── _db.py # Profile-home, connection, and transaction boundary
│ │ ├── identity.py # Cross-provider identity relation resolver
│ │ ├── link_identity.py # User command for identity confirmations
│ │ ├── normalize.py # Source payloads to source-neutral item rows
│ │ ├── ingest_graph.py # Optional read-only Outlook mailbox collector
│ │ ├── ingest_slack.py # Optional read-only Slack collector
│ │ ├── select_intake.py # Intake batch selector and wake gate
│ │ ├── select_review.py # Review batch selector and wake gate
│ │ ├── select_memory.py # Evidence selector for scheduled wiki writing
│ │ ├── apply_decisions.py # Validates and commits agent envelopes
│ │ ├── ranking.py # Deterministic cap, reservation, and cascade
│ │ ├── correct.py # User pin, ignore, and unignore writer
│ │ ├── walkthrough.py # Offline end-to-end entry point
│ │ ├── retention.py # Scheduled message-body clearing
│ │ ├── exclusions.py # Sender, domain, and channel filtering
│ │ ├── export_store.py # Complete Markdown and JSON export
│ │ ├── reset.py # Store, memory, and policy reset
│ │ ├── migrate.py # Forward-only store migration
│ │ ├── memory_check.py # Deterministic memory invariant checker
│ │ └── tests/ # 14 direct-execution unittest modules
│ └── skills/
│ ├── inbound-judging/ # New-message judgment instructions
│ ├── obligation-review/ # Scheduled re-judgment instructions
│ ├── memory-writing/ # Evidence-grounded people and attention pages
│ ├── memory-repair/ # Safe invariant repair instructions
│ ├── memory-consolidation/ # Bounded compaction instructions
│ └── preference-update/ # Repeated-correction learning instructions
├── fixtures/
│ ├── README.md # Fixture schema, controls, and provenance
│ ├── graph_messages.json # Five synthetic Graph-shaped messages
│ ├── slack_messages.json # Three synthetic Slack-shaped messages
│ ├── envelopes/intake.json # Recorded intake model turn
│ └── memory/ # Synthetic seed memory for the walkthrough
├── providers/
│ ├── graph-user.yaml # Read-only delegated Graph mailbox policy
│ └── slack-user.yaml # Read-only Slack endpoint and credential policy
├── scripts/
│ ├── install.sh # Sandbox: install profile and register jobs
│ ├── register-jobs.sh # Sandbox: idempotent Hermes cron registration
│ ├── require-encrypted-storage.sh # Shared connector storage prerequisite
│ ├── require-linux.sh # Shared scheduled-runtime platform check
│ ├── setup-graph.sh # Outside sandbox: authorize and attach Outlook access
│ ├── setup-slack.sh # Outside sandbox: authorize and attach a Slack token
│ └── validate-provider-profile.sh # Outside sandbox: validate provider policy
└── docs/
├── data-lifecycle.md # Retention, exclusions, export, reset, migration
├── encrypted-storage.md # Required storage-encryption checks
├── set-up-graph.md # Full Outlook mailbox authorization walkthrough
├── set-up-slack.md # Full Slack authorization walkthrough
└── slack_app_manifest.json # User-scoped Slack app manifest
Dependencies
| Dependency source | Path | Purpose |
|---|---|---|
| Python package manifest | None | All Python modules use the standard library |
| Recipe manifest | profile/distribution.yaml |
Pins recipe version and Hermes 0.19.0+ |
| SQLite schema | profile/scripts/schema.sql |
Defines application state schema v5 |
| Outlook provider policy | providers/graph-user.yaml |
Declares the recipe's intended read-only delegated graph.microsoft.com boundary |
| Slack provider policy | providers/slack-user.yaml |
Declares the recipe's intended read-only slack.com boundary |
API and Module Reference
Decision envelope
apply_decisions.py reads one JSON document from standard input. The supported
decisions are CREATE, KEEP_OPEN, MARK_DONE, and SKIP. A create or keep
decision requires a rank, title, and intent-gate verdict.
{
"version": 1,
"pass": "intake",
"decisions": [
{
"source_id": "msg-priorities-match",
"decision": "CREATE",
"rank": 1,
"intent_gated": true,
"title": "Review the migration plan",
"context": "Matches a current priority",
"urgency_reason": "Requested before the planning review",
"kind": "response",
"est_effort": "minutes"
},
{
"source_id": "msg-automated-noise",
"decision": "SKIP"
}
],
"cursor": {
"source": "email",
"scope": "inbox",
"value": "synthetic-cursor"
}
}
Run the writer against a saved envelope from the recipe root.
python3 profile/scripts/apply_decisions.py < /path/to/envelope.json
Valid optional values are:
kind:
- response
- action
- null
est_effort:
- minutes
- hours
- day
- multi_day
- null
Module contracts
| Module | Reads | Writes / returns |
|---|---|---|
normalize.py |
Graph- or Slack-shaped source objects | Source-neutral item dictionaries; recipient lists become one addressing value |
_db.py |
HERMES_HOME, schema.sql |
Validated profile path, SQLite connection, transactions, automatic migration |
ingest_graph.py |
Delegated Graph token and inbox delta | New Outlook message rows, resumable cursor state, and source-removal tombstones |
ingest_slack.py |
Rotating Slack token and selected conversations | New Slack rows and per-conversation watermarks |
select_intake.py |
Collectors and pending rows | JSON batch or a final wake-gate line |
select_review.py |
Open obligations | Oldest-review-first JSON batch or a final wake-gate line |
select_memory.py |
Message evidence, open obligations, and user corrections | Bounded memory-writing evidence or a final wake-gate line |
apply_decisions.py |
Versioned JSON envelope on standard input | Items, obligations, cursor, and append-only events in one transaction |
correct.py |
One user command | Pin/ignore state plus actor='user' audit events |
identity.py |
Stored pairwise identity answers | Resolved identity groups, candidates, and conflicts |
link_identity.py |
User confirmation or rejection | Durable relationship between two provider identities |
ranking.py |
Open obligations and gate verdicts | Deterministic bounded tiers and positions |
preferences.py |
User correction events | Bounded preference policy after the fixed threshold is met |
memory_check.py |
Memory Markdown pages | Diagnostics and exit status; no model call |
retention.py |
Store and RETENTION_DAYS |
Clears expired bodies; keeps metadata, obligations, and history |
export_store.py |
Store, memory, and policy | Complete Markdown and JSON export directory |
reset.py |
Profile workspace | Removes store, memory, policy, and collection state after confirmation |
migrate.py |
Existing store | Forward-only schema migration or compatibility check |
Store and migration commands
python3 profile/scripts/retention.py --dry-run
python3 profile/scripts/retention.py
python3 profile/scripts/export_store.py --to /path/to/export
python3 profile/scripts/migrate.py --check
python3 profile/scripts/migrate.py
python3 profile/scripts/reset.py --dry-run
python3 profile/scripts/reset.py --yes
reset.py --yes is destructive. Stop or pause the schedule first, export if
needed, detach and revoke external credentials separately, and verify the
profile named by HERMES_HOME before running it.
Verification
This is an integration-level reference implementation. Its evidence includes an offline end-to-end walkthrough, deterministic tests, scheduled Linux validation, plus live Slack and Outlook collector and credential-rotation validation.
Offline acceptance walkthrough
export RECIPE_VERIFY_HOME="$(mktemp -d)"
export HERMES_HOME="$RECIPE_VERIFY_HOME"
python3 profile/scripts/walkthrough.py --fixtures fixtures
Full test suite
Run from the recipe root. Each test file is executed directly because some tests verify direct-execution behavior.
cd profile/scripts
export NO_PROXY="${NO_PROXY:+$NO_PROXY,}127.0.0.1,localhost"
export no_proxy="${no_proxy:+$no_proxy,}127.0.0.1,localhost"
fail=0
for t in tests/*.py; do python3 "$t" || fail=1; done
echo "failed=$fail"
cd ../..
test "$fail" -eq 0
Expected result: every file ends with OK, the fourteen files report 640 tests
in total, and the final line is failed=0. Do not shorten the loop with an
early break; running every module is part of the documented check.
The suite covers schema migration, memory invariants, concurrency and crash recovery, deterministic ranking, preference thresholds, normalization, transactional decisions, correction state transitions, the walkthrough, intake, review, and memory-writing selector wake gates, scheduler contracts, lifecycle controls, and Slack and Outlook collection/rotation behavior.
Recipe Metadata
name: memory-driven-chief-of-staff
version: "0.1.0"
kind: nvidia-recipe
status: reference-example
target_runtime: NemoHermes
tech_stack:
- "Python 3.10+"
- SQLite
- Bash
- "Hermes >=0.19.0"
- NemoClaw
- OpenShell
entry_point: profile/scripts/walkthrough.py
install_entry_point: scripts/install.sh
configuration_manifest: profile/distribution.yaml
state_root: "$HERMES_HOME/workspace"
source_connectors:
available:
- Slack
- "Microsoft Outlook (via Microsoft Graph)"
evidence_level: integration
license: Apache-2.0