The chat-adapter-imessage docs use photon.codes — our setup flow
and skill had the wrong domain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- teams app create prints CLIENT_ID/CLIENT_SECRET/TENANT_ID; the existing Configure environment section expects TEAMS_APP_ID/TEAMS_APP_PASSWORD/TEAMS_APP_TENANT_ID, so without the mapping a user pasting verbatim would silently end up with an adapter that can't authenticate
- @microsoft/teams.cli registers bots via the Teams Developer Portal, skipping the Azure subscription requirement that blocks users on locked-down corporate tenants
A long-lived hub session never rotates its continuation, so the on-disk
.jsonl grows without bound — days of history plus base64 image blocks the
agent Read (screenshots from QA lanes, etc.). The SDK reloads the whole
transcript on every --resume, and past a threshold the first turn alone
exceeds the host's 30-min idle ceiling: the container is SIGKILLed before
it can reply, then the next message repeats the cycle forever. Symptom:
a hub that was responsive for days suddenly goes silent on a heavy turn.
Before resuming, the Claude provider now checks the transcript backing the
stored continuation; if it exceeds a size cap (default 12MB) or age cap
(default 14 days, from the first entry's timestamp) it archives a markdown
summary to conversations/ and starts a fresh session. Both caps are
operator-overridable via CLAUDE_TRANSCRIPT_ROTATE_BYTES /
CLAUDE_TRANSCRIPT_ROTATE_AGE_DAYS. The PreCompact archiver is refactored
into a shared archiveTranscriptFile() reused by the rotation path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
signal-cli >= 0.13 emits the account identifier as `number` in JSON
output, not `account`. The skip-if-already-linked path in signal-auth
always returned an empty list, so re-runs of setup unconditionally
tried `signal-cli link`, which fails when the data directory already
exists.
Read `number` first, fall back to `account` for older signal-cli.
Installs rtk (60–90% token savings on dev commands) into agent containers
via host binary mount + Claude Code PreToolUse hook.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filter channel registration target options to the approver's authorized agent groups and re-check target authorization before applying a pending approval. Add regression coverage for scoped admins attempting to connect channels to out-of-scope groups.
Previously, passing --assistant-name <Name> when registering an agent
did a project-wide find-replace of "Andy" → <Name> across every
groups/*/CLAUDE.md file, and overwrote .env's ASSISTANT_NAME.
Two unintended consequences:
- Registering a second agent (e.g. "Homie") clobbered an unrelated
primary agent's CLAUDE.md. Real-world hit when wiring Homie's
Signal group on an install that already had Diddyclaw set up —
groups/diddyclaw/CLAUDE.md ended up with "Homie" references it
shouldn't have had.
- The install-wide .env ASSISTANT_NAME flipped to the most recently-
registered name, becoming the default trigger pattern for any
subsequent group registered without an explicit --assistant-name.
Both were a per-agent operation accidentally exercising project-wide
state. Now only groups/<folder>/CLAUDE.md of the agent being
registered is touched. .env is left alone — it represents the
install-wide default and shouldn't be flipped by per-agent registers.
If the install's primary-default name needs to change, that's an
explicit one-line .env edit, not a side-effect of registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When 2+ pending messages were bundled into <messages>...</messages> at
container/agent-runner/src/formatter.ts:162-167, the Claude Agent SDK
responded with a synthetic stub (model="<synthetic>", stop_reason=
"stop_sequence", content="No response requested.") instead of calling
the real API. The poll loop never yielded a `result` event, so the
inbound message was never marked completed; the container exited; the
next sweep tick respawned it with the same batch; same synthetic; the
transcript file ballooned with each retry until tries=5 → failed.
Single-message turns (which skipped the wrapper) worked normally — the
SDK's heuristic appears to treat the wrapped envelope as a context dump
rather than a real user turn. Each `<message id=... from=...>...</message>`
block is already self-contained, so dropping the outer wrapper lets the
N>1 case work the same way the N=1 case always has.
Fix:
function formatChatMessages(messages: MessageInRow[]): string {
return messages.map(formatSingleChat).join('\n');
}
Updates one existing test that asserted on the envelope, and adds two
regression tests: one negative (no `<messages>` wrapper), one positive
(each inbound row produces a `<message>` block in order).
Confirmed working in a real install: two stuck lanes recovered after
reducing their pending queue to 1 message, and both produced normal
replies from claude after the wipe + this fix were both applied (the
wipe alone wasn't enough — a fresh session given the same batch shape
hit the same synthetic loop).
Refs nanocoai/nanoclaw#2555 for full repro + transcript evidence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SKILL.md recommends `--method qr-browser` and references `--method qr-terminal`, but `setup/whatsapp-auth.ts` on `channels` only accepts `qr` and `pairing-code`. Running the recommended path errors out with `Unknown --method: qr-browser (expected 'qr' or 'pairing-code')`.
Add `.claude/skills/add-whatsapp/scripts/wa-qr-browser.ts` — a small wrapper that spawns the existing `--method qr` step, parses its `WHATSAPP_AUTH_QR` status blocks, and serves the rotating QR as a PNG on a local HTTP server with the default browser auto-opened. Restores the 'QR in browser' UX the skill already promises.
Update SKILL.md to invoke the wrapper for the browser method and to call `--method qr` (not `qr-terminal`) for the terminal method. Also expand the 'pairing code keeps failing' troubleshooting with the 'Couldn't link device — An error happened' server-side rejection seen on fresh dedicated numbers.
No source changes (`setup/`, `src/`) — preserves the 'browser method dropped' decision in `setup/whatsapp-auth.ts`. No new npm deps — uses `qrcode` (already pinned by this skill) and Node's built-in `http`.
Documents the fix from #2510 (closes#2465) in user-facing prose
following the RELEASING.md style guide. Single-bullet release —
no rollup opener since this is a clean one-bump cycle.
The `destinations add` and `destinations remove` custom ops in the admin
CLI INSERT/DELETE rows in the central `agent_destinations` table, but
did not project the change into running sessions' `inbound.db`. The
agent-runner container reads its destination map from the per-session
projection, so until the next container spawn (`container-runner.ts`
hydrates on every wake), the running agent saw a stale map — explaining
the "dropped: unknown destination" symptom after a fresh `ncl
destinations add` even though the central row was clearly committed.
Same handler runs for both the direct-host path and the approval-execution
path because the `cli_command` approval handler in `dispatch.ts` re-enters
`dispatch()` as `caller: 'host'`, so the fix at the handler level covers
both surfaces.
Helper iterates over `getSessionsByAgentGroup(agentGroupId)` (every
active session for the affected agent), guarded by `hasTable('agent_destinations')`
and a lazy dynamic import of `writeDestinations` to keep the agent-to-agent
module optional. Per-session try/catch keeps one bad session from killing
the whole projection; failures are logged at WARN with session id + error.
Regression test invokes the dispatcher with `caller: 'host'` (the same
re-entry the approval handler uses after admin approves), with two active
sessions on the source agent group, and asserts the `destinations` row
lands in every session's inbound.db after `add` and is cleared after `remove`.
Fixes#2465
RELEASING.md frames the per-bump release policy as a goal that is cut
manually, not as automation. The v2.0.63 CHANGELOG rollup line still
asserted the stronger claim ("NanoClaw publishes a GitHub Release on
every package.json version bump"), which contradicts the policy doc.
Soften to match RELEASING.md so the two land consistently on main.
The "For detailed release notes, see the full changelog on the
documentation site" line pointed at a docs portal that does not exist.
CHANGELOG.md is the canonical record, so the header now says only what
is true: all notable changes are documented in this file.
Two revisions in RELEASING.md based on review feedback:
1. Soften the "release per bump" claim. The policy is aspirational and
release publication is manual, so the opening now states the goal
("publish a GitHub Release for every package.json version bump that
lands on main") and acknowledges that there can be lag between a bump
merging and the release being cut. Intent: timeliness, not strict 1:1.
2. Add a "Channels and stability" section that explicitly states NanoClaw
ships a single channel today, distinguishes latest/stable/pinned for
consumers, and reserves space for a future pre-release channel without
inventing structure that does not yet exist. Folds the previous Pinning
section into the new structure as the Pinned bullet.
CHANGELOG.md gets a rollup entry covering v2.0.55..v2.0.63 in the
project voice (bold lead-ins, [BREAKING] prefix with inline workaround,
doc links to setup/lib/install-slug.sh, no PR numbers).
RELEASING.md is new and documents the per-bump release policy starting
with v2.0.63: tag every package.json bump, mirror the CHANGELOG entry
into the GitHub Release body, append Contributors and (when relevant)
New Contributors sections, and use rollup framing when multiple bumps
collapsed into one release.
The gmail/gcal Phase 4 restart blocks and uninstall one-liners
still hardcoded `com.nanoclaw` / `restart nanoclaw`, so on a v2
install they would fail with "no such service" or kick the
wrong unit.
Phase 4 restart now uses the canonical
`source setup/lib/install-slug.sh` + `$(launchd_label)` /
`$(systemd_unit)` pattern with the standalone `Run from your
NanoClaw project root:` lead-in. Uninstall one-liners switch
to the inline-subshell form
`"$(. setup/lib/install-slug.sh && systemd_unit)"`.
(Folds in #2489's v2-alignment changes to the same two files;
the deferral noted in the original PR body is no longer needed
now that #2489 has merged.)
Split the embedded forms ("... — run from your NanoClaw project root:")
into a separate `Run from your NanoClaw project root:` line directly
above the code block, so the lead-in pattern is uniform across all
restart blocks.
Replace inline `# run from your NanoClaw project root` annotations on
`source setup/lib/install-slug.sh` lines with one standalone prose
lead-in per code block. Also drop parenthetical "(run from the project
root...)" mentions where the same convention is already obvious.
- swap remaining inline subshells from `; helper` to `&& helper` so source
failures surface as the source error instead of a downstream 'command not
found' on the helper call
- fix two service-status checks that still grepped for the bare v1 name
(init-first-agent, add-emacs) — same canonical inline form as the rest of
the sweep, scoped to the per-install slug
- collapse add-parallel's verify block to the inline form so it stops
shadowing the canonical pattern
- note 'run from your NanoClaw project root' beside every restart snippet
that sources `setup/lib/install-slug.sh` (inline as a bash comment on
the source line, plus parenthetical lead-ins where the snippet is
prose-form) so the relative-path dependency is loud at the spot it
matters
The `ncl` transport-error message and ~20 skill docs hardcoded v1's
`com.nanoclaw` / `nanoclaw` for launchd labels and systemd units. Under
v2 the names are slug-suffixed per checkout (`com.nanoclaw.<slug>`,
`nanoclaw-<slug>.service`), so those commands no longer match a real
service on the host.
- `src/cli/client.ts` — extract `formatTransportError` into
`src/cli/transport-errors.ts` so it can read `install-slug` and call
`getLaunchdLabel()` / `getSystemdUnit()`.
- `src/cli/transport-errors.test.ts` — regression test for #2484: the
error string must not contain the bare v1 names.
- `.claude/skills/**/*.md` — replace hardcoded restart snippets with
the canonical `source setup/lib/install-slug.sh` + `$(systemd_unit)` /
`$(launchd_label)` pattern (or the inline subshell form where the
snippet is a one-liner).
Closes#2484Closes#2485
Three issues with the DB-edit steps that ship in #2489:
- `'$[#]'` was double-quoted in the surrounding bash string, so bash
arith-expanded `$#` (positional-arg count, 0 in interactive shell)
before sqlite ever saw it — silently overwrote index 0 instead of
appending. Now escaped as `'\$[#]'`.
- `sqlite3` CLI replaced with `pnpm exec tsx scripts/q.ts` — clean
installs have no sqlite3 binary; setup/verify.ts:5 codifies that
NanoClaw avoids depending on it.
- `strftime('%s','now')` replaced with `datetime('now')` — the column
stores ISO strings everywhere else; mixing epoch ints made any
consumer doing `datetime(updated_at)` parse those rows as 1970.
Also: reworded the "approval-gated" wording to distinguish container
vs host-operator-shell invocation, and added the "Why this can't be
container.json" note to add-gcal-tool (gmail had it, gcal didn't).
Two pieces of post-v1 drift in the gmail/gcal skills made the instructions
either dead-code edits or silently broken installs:
1. The TOOL_ALLOWLIST edit step is redundant. claude.ts derives
mcp__<name>__* allow-patterns dynamically from each group's
mcpServers map (claude.ts:294-297), so registering the MCP server in
Phase 3 already authorizes the tools. Removed the edit step, its
pre-check, its troubleshooting attribution, and its uninstall mirror;
replaced with an explanatory note pointing at the dynamic derivation.
2. The "edit groups/<folder>/container.json" step doesn't stick.
materializeContainerJson rewrites that file from the central DB on
every spawn (post-migration 014-container-configs), so hand edits are
silently overwritten on next restart. Rewrote Phase 3 to use
`ncl groups config add-mcp-server` (which persists to DB) for the
MCP-server entry, and a sqlite3 json_insert workaround for the mount
entry — with a note to switch to `ncl groups config add-mount` once
#2395 lands. Removal step rewritten the same way using
`remove-mcp-server` and a sqlite3 json_group_array filter.
Fixes#2488