Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
122c51a
fix(messaging): diagnose Hermes WhatsApp session split
danielpolimac Aug 5, 2026
3eeb72f
Merge branch 'main' into issue-8184-hermes-whatsapp-status
senthilr-nv Aug 5, 2026
c576b85
merge: sync main into WhatsApp diagnostics PR
danielpolimac Aug 5, 2026
33d9d47
Merge branch 'main' into issue-8184-hermes-whatsapp-status
senthilr-nv Aug 5, 2026
2d55cc0
fix(messaging): use Hermes dashboard profile path
apurvvkumaria Aug 5, 2026
edfd75a
Merge branch 'main' into issue-8184-hermes-whatsapp-status
senthilr-nv Aug 5, 2026
b07bbc6
docs(messaging): clarify Hermes WhatsApp state
apurvvkumaria Aug 5, 2026
123ffe7
Merge branch 'main' into issue-8184-hermes-whatsapp-status
senthilr-nv Aug 5, 2026
fc1af07
Merge branch 'main' into issue-8184-hermes-whatsapp-status
senthilr-nv Aug 5, 2026
f918e0b
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
91674b1
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
10eeeee
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
d812ca9
test(messaging): cover duplicate Hermes sessions (#8184)
cjagwani Aug 5, 2026
1de30e6
test(messaging): cover Hermes cleanup fallback
apurvvkumaria Aug 5, 2026
987d04d
docs(messaging): clarify Hermes session redirect
apurvvkumaria Aug 5, 2026
d10e083
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
bb95a5b
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
99fc773
merge(main): refresh PR #8242
cjagwani Aug 5, 2026
7dfbf40
Merge branch 'main' into issue-8184-hermes-whatsapp-status
apurvvkumaria Aug 6, 2026
4ea2dd0
fix(messaging): scope WhatsApp profile cleanup to Hermes
apurvvkumaria Aug 6, 2026
17ca062
test(messaging): keep cleanup path assertions linear
apurvvkumaria Aug 6, 2026
678a470
Merge branch 'main' into issue-8184-hermes-whatsapp-status
apurvvkumaria Aug 6, 2026
c296f46
docs(messaging): document Shields-safe WhatsApp repair
apurvvkumaria Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/manage-sandboxes/set-up-whatsapp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,35 @@ hermes whatsapp
NemoClaw validates the gateway URL before pairing and renders the WhatsApp QR code in a compact terminal form so it fits in smaller terminal windows.
If pairing exits with a gateway close such as `1008`, rerun the login command once, then check `$$nemoclaw <sandbox> channels status --channel whatsapp` to diagnose the gateway and session path separately from QR rendering.
</AgentOnly>
<AgentOnly variant="hermes">
Hermes can pair from the Dashboard profile while the gateway reads the default `platforms/whatsapp/session` path.
After scanning a dashboard QR code, run `$$nemoclaw <sandbox> channels status --channel whatsapp`.
If the report says `dashboard-home has WhatsApp credentials, but the Hermes gateway session path is empty`, point the gateway at the dashboard session with the supported config command:

```bash
$$nemoclaw <sandbox> shields down --reason "repair Hermes WhatsApp session path"
$$nemoclaw <sandbox> config set --key platforms.whatsapp.extra.session_path --value /sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session --restart --config-accept-new-path
$$nemoclaw <sandbox> shields up
```

Keep Shields down until `config set` finishes, then restore the restrictive posture with `shields up`.
The `--config-accept-new-path` flag lets the command write the `platforms.whatsapp.extra.session_path` configuration key when the generated config does not already contain it.
Dashboard pairing writes session credentials under `profiles/dashboard-home`; this bounded compatibility repair redirects the gateway to that session path while it still reads the default path otherwise.
The repair stays documented until Hermes dashboard pairing state and gateway startup converge on one shared session path; NemoClaw can redirect the configured path but does not rewrite Hermes pairing state.
Before removing the override, run `$$nemoclaw <sandbox> channels status --channel whatsapp` after dashboard pairing and gateway startup and confirm the report no longer shows a dashboard-home session with an empty gateway session path.
Remove the override only after Hermes uses one shared WhatsApp session path for dashboard pairing and gateway startup.
Do not edit `/sandbox/.hermes/.env` or `/sandbox/.hermes/config.yaml` by hand.
Manual edits can cause the Hermes config-integrity guard to reject restart with `hash-mismatch`.
Supervisor quarantine happens after repeated relaunch failures or other integrity failures.
</AgentOnly>

## Protect Paired Session State

<AgentOnly variant="openclaw">
The sandbox stores mutable session credentials in durable `whatsapp` state.
</AgentOnly>
<AgentOnly variant="hermes">
The sandbox stores mutable session credentials in durable `platforms/whatsapp` state.
The sandbox stores mutable session credentials in durable `platforms/whatsapp` state for the gateway and `profiles/dashboard-home/platforms/whatsapp` state for Dashboard pairing.
</AgentOnly>
The credentials survive rebuilds without re-pairing.
This is the runtime tradeoff of enabling WhatsApp without a host bridge: a paired sandbox can use that WhatsApp account until you unpair it or clear the durable state.
Expand All @@ -50,6 +71,8 @@ Pair only one sandbox per WhatsApp account at a time.
For non-interactive onboarding, set `WHATSAPP_ALLOWED_IDS` to a nonempty comma-separated sender list to select WhatsApp for either agent.
<AgentOnly variant="hermes">
Hermes also uses these values as its WhatsApp sender allowlist.
Use digits only, such as `15551234567`.
Do not include a leading `+`.
</AgentOnly>

Continue with [Enable Channels During Onboarding](enable-channels-during-onboarding) or [Add Channels After Onboarding](add-channels-after-onboarding).
14 changes: 9 additions & 5 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2285,10 +2285,11 @@ When the bridge provider is attached to a live sandbox, NemoClaw detaches it bef
If the matching built-in policy preset is applied, NemoClaw also removes that preset so the upstream API is no longer allow-listed after the channel is gone.
NemoClaw also strips the channel from `session.policyPresets` so a subsequent `onboard --resume` does not re-apply the preset on the next rebuild.

For QR-paired channels (today: WhatsApp), NemoClaw destructively clears the in-sandbox session directory before the rebuild so the `state_dirs` backup does not restore the auth blob and let the channel reconnect:
For QR-paired channels (today: WhatsApp), NemoClaw destructively clears the in-sandbox session directories before the rebuild so the `state_dirs` backup does not restore the auth blob and let the channel reconnect:

- OpenClaw: `/sandbox/.openclaw/<channel>/` (for example `/sandbox/.openclaw/whatsapp/`).
- Hermes: `/sandbox/.hermes/platforms/<channel>/` (for example `/sandbox/.hermes/platforms/whatsapp/`).
For WhatsApp, NemoClaw also clears the current Dashboard profile at `/sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session/` and the legacy migration source at `/sandbox/.hermes/dashboard-home/platforms/whatsapp/session/`.

The cleanup tries `openshell sandbox exec` first and falls back to SSH if the exec wrapper does not return the success sentinel. If both transports fail (the sandbox is stopped, the gateway is down, or SSH cannot reach it) the command refuses to proceed to the rebuild and asks you to start the sandbox and re-run, so a half-removed state cannot leave stale Baileys auth files behind for the next rebuild to restore.

Expand Down Expand Up @@ -2344,13 +2345,15 @@ $$nemoclaw my-assistant channels start telegram

Run messaging channel status checks.
Without `--channel`, the command prints a compact summary for every configured channel, including registration, policy coverage, and non-secret rendered config comparisons.
For channel and agent combinations that support a live health probe (WhatsApp or Telegram on OpenClaw), the summary adds a `Runtime health: not checked in summary view` pointer instead of running the probe, so it never reads as healthy without an explicit check.
For channel and agent combinations that support a detailed probe, the summary adds a `Runtime health: not checked in summary view` pointer instead of running the probe, so it never reads as healthy without an explicit check.
With `--channel`, it prints the detailed status for that channel.

For an OpenClaw WhatsApp sandbox, `--channel whatsapp` also probes the sandbox to separately report pairing/session state, the Noise WebSocket connection, inbound event delivery, and policy coverage.
For an OpenClaw WhatsApp sandbox, `--channel whatsapp` probes the sandbox to separately report pairing/session state, the Noise WebSocket connection, inbound event delivery, and policy coverage.
A paired channel with no observed inbound delivery exits non-zero with verdict `idle` so an unhealthy bridge cannot pass as healthy.
The detailed WhatsApp probe stays focused on QR/session runtime diagnostics and does not include rendered-config comparison lines.
A Hermes WhatsApp sandbox uses the basic registration, policy, and config report because NemoClaw does not treat the Hermes session file as a live-health signal.
For a Hermes WhatsApp sandbox, `--channel whatsapp` probes only whether the default gateway session path and the `profiles/dashboard-home` session path contain `creds.json`.
If the dashboard path has credentials and the gateway path is empty, the report prints the supported `config set` command for the `platforms.whatsapp.extra.session_path` configuration key, including the new-configuration-key opt-in when generated configs do not already contain that key.
NemoClaw does not treat a Hermes session file as live inbound-health evidence.

For Telegram, `--channel telegram` probes the sandbox to report the gateway process, Bot API reachability, and inbound delivery alongside the config comparison.
It classifies the state as `healthy`, `idle`, `token_rejected`, `unreachable`, `not_started`, `policy_gap`, `config_gap`, `unknown`, or `probe_failed`.
Expand All @@ -2373,10 +2376,11 @@ $$nemoclaw my-assistant channels status --channel telegram
| Flag | Description |
|------|-------------|
| `--channel <channel>` | Channel to inspect in detail |
| `--json` | Emit the status report as JSON (for an available detailed WhatsApp or Telegram probe on OpenClaw, exit non-zero when the verdict is not `healthy` or `unknown`) |
| `--json` | Emit the status report as JSON (for an available detailed WhatsApp or Telegram probe, exit non-zero when the verdict is not `healthy` or `unknown`) |

Each live probe is bounded by an in-sandbox `openshell sandbox exec` with a hard timeout.
The WhatsApp probe returns strict OpenClaw status JSON to the host, where NemoClaw allowlists pairing, liveness, connection-state, and timestamp fields before rendering the report and discards phone-number and free-text error fields.
The Hermes WhatsApp probe returns only fixed session-path booleans to the host.
The Telegram probe returns only matched gateway log lines to the host, where NemoClaw reduces them to fixed classifications without rendering the raw lines, message bodies, or tokens.

</AgentOnly>
Expand Down
59 changes: 54 additions & 5 deletions src/lib/actions/sandbox/channel-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ function waStatusJson(wa: Record<string, unknown>): string {
});
}

function hermesSessionProbeOutput(options: {
gatewaySessionCreds: boolean;
dashboardSessionCreds: boolean;
}): string {
return [
"NEMOCLAW_HERMES_WHATSAPP_SESSION_V1",
`GATEWAY_SESSION=${options.gatewaySessionCreds ? "present" : "missing"}`,
`DASHBOARD_SESSION=${options.dashboardSessionCreds ? "present" : "missing"}`,
].join("\n");
}

describe("showSandboxChannelStatus (whatsapp)", () => {
it("returns idle verdict and exit code 1 when paired but no inbound observed", async () => {
const stdout = waStatusJson({
Expand Down Expand Up @@ -214,22 +225,60 @@ describe("showSandboxChannelStatus (whatsapp)", () => {
expect(threw?.message).toBe("process.exit(1)");
});

it("falls back to basic status for Hermes without running the OpenClaw probe", async () => {
it("reports a Hermes dashboard-home session that the gateway path cannot read", async () => {
const exec = vi.fn((_sandbox: string, _command: string, _timeoutMs?: number) => ({
status: 0,
stdout: "",
stdout: hermesSessionProbeOutput({
gatewaySessionCreds: false,
dashboardSessionCreds: true,
}),
stderr: "",
}));
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`process.exit(${code})`);
}) as never);
const { deps } = makeDeps({
exec,
agentName: "hermes",
sandbox: entry(["whatsapp"], [], {}, "hermes"),
});
const result = await showSandboxChannelStatus("alpha", { deps, channel: "whatsapp" });
let threw: Error | null = null;
try {
await showSandboxChannelStatus("alpha", { deps, channel: "whatsapp" });
} catch (err) {
threw = err as Error;
} finally {
exitSpy.mockRestore();
}
const commands = exec.mock.calls.map((call) => String(call[1] ?? "")).join("\n");
expect(threw?.message).toBe("process.exit(1)");
expect(commands).not.toContain("openclaw channels status");
expect(commands).not.toContain("platforms/whatsapp/session/creds.json");
expect(result && "verdict" in result && result.verdict).toBe("info");
expect(commands).toContain("/sandbox/.hermes/platforms/whatsapp/session/creds.json");
expect(commands).toContain(
"/sandbox/.hermes/profiles/dashboard-home/platforms/whatsapp/session/creds.json",
);
});

it("keeps Hermes gateway session presence as an unknown live-health verdict", async () => {
const { deps } = makeDeps({
exec: () => ({
status: 0,
stdout: hermesSessionProbeOutput({
gatewaySessionCreds: true,
dashboardSessionCreds: false,
}),
stderr: "",
}),
agentName: "hermes",
sandbox: entry(["whatsapp"], [], {}, "hermes"),
});
const result = await showSandboxChannelStatus("alpha", { deps, channel: "whatsapp" });
expect(result && "report" in result && result.report.verdict).toBe("unknown");
const session =
result && "report" in result
? result.report.signals.find((signal) => signal.label === "Session location")
: undefined;
expect(session?.severity).toBe("ok");
});

it("skips the deep probe and reports paused state when WhatsApp is in disabledChannels", async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/actions/sandbox/channel-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ function buildBasicChannelReport(
: "not checked in summary view",
hint: options.channelPaused
? undefined
: `run \`${CLI_NAME} ${sandboxName} channels status --channel ${channelName}\` to probe live health`,
: `run \`${CLI_NAME} ${sandboxName} channels status --channel ${channelName}\` for detailed status`,
});
} else if (options.includeDeepDiagnostics ?? true) {
signals.push({
Expand Down
Loading
Loading