193 Commits

Author SHA1 Message Date
gavrielc 729cd8d2a6 feat: add /upload-trace command to upload session trace to Hugging Face
Adds a runner-handled /upload-trace slash command (admin-gated, like /clear)
that uploads the current session's Claude Code transcript to the user's own
private {hf_user}/nanoclaw-traces dataset, browsable in the HF Agent Trace
Viewer. The transcript is already in the format the viewer auto-detects, so
the command just locates the newest one and pushes it via the Hub commit API.

Auth is handled by the OneCLI gateway: curl goes out through the injected
HTTPS_PROXY, which adds the user's HF token — no credential ever touches
agent code. A missing/unassigned token yields a clear setup message.

- container/agent-runner/src/upload-trace.ts: isUploadTraceCommand() + uploadTrace()
- poll-loop.ts: recognize and handle /upload-trace in the runner
- command-gate.ts: admin-gate /upload-trace on the host
- upload-trace.test.ts: unit + integration coverage for the command

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:42:36 +03:00
gavrielc 81d99e1dc9 chore: bump claude-code to 2.1.154 and claude-agent-sdk to 0.3.154
claude-code CLI 2.1.128 -> 2.1.154 (Dockerfile build-arg). agent-runner SDK 0.2.128 -> 0.3.154: the 0.3 major moved @anthropic-ai/sdk and @modelcontextprotocol/sdk from regular deps to peer deps, so add @anthropic-ai/sdk ^0.100.0 as a direct dep and raise @modelcontextprotocol/sdk to ^1.29.0. Regenerate bun.lock. Typecheck + agent-runner tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:35:34 +03:00
gavrielc 9dc9efa3bf Merge branch 'main' into fix/db-malformed-self-restart 2026-05-23 20:06:10 +03:00
gavrielc ecca637fb3 Merge branch 'main' into fix/transcript-rotate-age-zero-disable 2026-05-23 20:04:27 +03:00
gavrielc 4d92b6dd47 Merge pull request #2596 from IamAdamJowett/fix/formatter-test-drop-messages-envelope
test(agent-runner): update formatter test for dropped <messages> envelope
2026-05-23 20:02:59 +03:00
jonnychesthair-crypto c727bb638c fix: load per-group CLAUDE.local.md by adding 'local' to settingSources
The agent-runner runs the Agent SDK with settingSources: ['project', 'user'], which omits 'local'. Per the SDK docs the 'local' source is what loads CLAUDE.local.md (the 'project' source loads CLAUDE.md). So every group's CLAUDE.local.md is silently never read, even though container/CLAUDE.md tells each agent to use it as per-group memory.

Closes #2185.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 02:45:51 +00:00
karta 3df30475ed fix(agent-runner): exit on persistent inbound.db corruption errors
The follow-up poll catches and logs SQLite errors but never recovers
from them. On Docker Desktop macOS, the kernel page cache for the
inbound.db bind mount can latch a torn snapshot mid-host-write (a known
virtiofs / gRPC-FUSE coherency issue), after which every fresh
openInboundDb() in the same process sees the same broken view and
emits 'database disk image is malformed' at the poll rate (2/sec).

Reopening the DB handle inside the container does not recover — only
a fresh container mount does. The fix: after CORRUPTION_STREAK_EXIT
consecutive corruption errors (~5s), log a clear message and
process.exit(75) so host-sweep respawns the container with a fresh
mount. Transient single torn reads are still tolerated.

- Add isCorruptionError() helper covering the three SQLite read-side
  corruption symptoms (disk image malformed, SQLITE_CORRUPT, file is
  not a database).
- Add streak counter scoped to processQuery's pollHandle so it resets
  on any successful or non-corruption error.
- Add unit tests for the matcher.

Refs the cross-mount invariants documented in db/connection.ts:11-18.
2026-05-23 10:10:09 +08:00
Adam df90f9952c test(agent-runner): update formatter test for dropped <messages> envelope
fe2e881b (#2556) removed the <messages> wrapper from formatChatMessages
so the Claude Agent SDK calls the API instead of emitting a synthetic
stub, but poll-loop.test.ts still asserted the wrapper. The test has
failed on every PR built against main since. Assert the current shape:
no envelope, one self-contained <message> block per message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:09:42 +10:00
Adam f00f8637a3 fix(agent-runner): honor zero/negative transcript rotate-age override
CLAUDE_TRANSCRIPT_ROTATE_AGE_DAYS=0 (or negative) is documented to
disable age-based rotation, but transcriptRotateAgeMs() routed it
into the same branch as an unset var and returned the 14-day default.
Sessions intentionally configured to stay long-lived were still
rotated at 14 days, causing unexpected resets and context loss.

Distinguish unset/non-numeric (default 14d) from an explicit
non-positive override (Infinity = disabled; size alone governs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:58:47 +10:00
gavrielc 30f2b6e553 Merge pull request #2553 from IamAdamJowett/feat/whatsapp-formatting-skill
feat(skills): add whatsapp-formatting container skill
2026-05-22 23:06:48 +03:00
gavrielc 77b5ee4897 Merge branch 'main' into fix/agent-runner-drop-messages-envelope 2026-05-22 23:05:40 +03:00
Adam 6686315a10 fix(agent-runner): rotate oversized/old session transcripts before resume
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>
2026-05-22 23:40:47 +10:00
Adam fe2e881b37 fix(agent-runner): drop <messages> envelope so claude-agent-sdk calls API
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>
2026-05-19 23:47:41 +10:00
Adam 7f92f17669 feat(skills): add whatsapp-formatting container skill
Teaches agents WhatsApp's mention syntax (@<phone-digits>, never display
names) and where to find the sender's phone JID in inbound metadata
(content.sender). Without this, agents default to @<displayName>, which
WhatsApp can't tag — it just renders as plain text with no notification.

Two files:

- SKILL.md — frontmatter + description so the Claude Agent SDK can
  discover it via skill metadata for ad-hoc lookups.
- instructions.md — always-on guidance. claude-md-compose.ts inlines
  any skill that ships an instructions.md into every group's CLAUDE.md
  on container spawn, so the rule is in the agent's context for every
  reply (not just when the agent decides to invoke the Skill tool).

Mirrors the existing container/skills/slack-formatting/ layout for the
analogous Slack mrkdwn rules. Pairs with the adapter-side fix on the
`channels` branch that wires `mentions` through to Baileys' contextInfo
— both layers are needed for tags to render end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:21:54 +10:00
Daniel Milliner cbdebe55fc fix(destinations): remove misleading scratchpad clause from internal-tag description
Follow-up to #2467. The trailing "anything outside these tags is also
treated as scratchpad" clause contradicted the rest of the system prompt,
which requires bare text to be wrapped in `<message>` blocks. Removing it
keeps the description focused on what `<internal>` actually does.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:20:43 +03:00
Koshkoshinsk ca52d2c6c1 fix(welcome): stop emitting the greeting twice
The welcome skill told the agent to send the greeting via `send_message`,
but the destinations system prompt also requires the final response to
be wrapped in `<message to="…">` blocks (since 1d4d920). The agent
followed both, sending the greeting once via the MCP tool and once via
the wrapped final output.

- welcome/SKILL.md: drop the mechanism — "send a short, warm greeting"
  lets the system prompt steer how it's delivered.
- destinations.ts: reframe `<message>` blocks and `send_message` as the
  same delivery surface, with the explicit note that each call/block
  lands as its own message — so they compose into a sequence rather than
  reading as additive duplicates of the same content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:12:38 +00:00
Koshkoshinsk 1d4d920629 fix(core-instructions): require message wrapping for single-destination agents
The parenthetical "(single-destination: just write)" was stale after
9db39b2 removed the bare-text routing fallback. Agents following this
hint had their responses silently dropped to scratchpad.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 07:27:07 +00:00
gavrielc 7f4fa65f3c fix(poll-loop): nudge agent when output lacks message wrapping
When the agent outputs bare text without <message to="..."> blocks,
nothing gets delivered — silent failure. Now the poll-loop pushes a
one-shot correction back into the active query telling the agent to
re-send with proper wrapping. Capped at once per user turn to avoid
loops; resets when a new follow-up message arrives.

Also updates destination instructions to require explicit <internal>
wrapping for scratchpad instead of treating bare text as implicit
scratchpad.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:30:23 +03:00
gavrielc 74744599d3 Merge pull request #2413 from nanocoai/fix/compact-instructions-reminder
fix(compact): place destination reminder at end of compaction summary
2026-05-12 00:25:05 +03:00
gavrielc 00ddb3b169 fix(compact): place destination reminder at end of compaction summary
Tell the compactor to include the <message to="name"> wrapping reminder
verbatim at the END of the summary so it's the last thing the agent sees
after compaction. Previously the instruction just asked to "preserve"
routing info, which the compactor could place anywhere or summarize away.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 12:49:28 +03:00
gavrielc a760da7fef revert: remove compaction destination reminder (PR #2327)
The compacted event handler injected a system-tagged reminder into the
live query after SDK auto-compaction, which caused the agent to send
an unintended message. Reverts the four changes from #2327:

- Remove `compacted` variant from ProviderEvent union
- Restore `result` yield for compact_boundary in ClaudeProvider
- Remove compacted event handler and getAllDestinations import in poll-loop
- Remove compaction integration tests and CompactingProvider helper

Closes #2325 differently — the reminder approach is not viable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 12:38:49 +03:00
gavrielc 8ac3cf2912 fix(container): gracefully handle missing on_wake column in pre-migration session DBs
The container opens inbound.db read-only, so it can't ALTER TABLE.
If the host hasn't run migrateMessagesInTable yet (e.g., container
rebuilt before host restart), the on_wake column won't exist and
the query crashes, causing a restart loop.

Detect the column via PRAGMA table_info and conditionally include
the filter clause.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 11:08:02 +03:00
glifocat bda72a4bf4 chore: rename remaining qwibitai/nanoclaw references to nanocoai/nanoclaw
Sweep of outbound strings, doc URLs, comments, and clone instructions
that were missed in the original org rename. One both-match case in
setup/lib/channels-remote.sh (URL detection) accepts either name so
existing forks with a `qwibitai` remote continue to resolve cleanly;
everywhere else is a straight rename.

Historical mentions left intact:
- CHANGELOG.md (v2.0.0 entry, frozen history)
- .claude/skills/add-gmail-tool/SKILL.md (pre-v2 qwibitai skill — historical)
- repo-tokens/badge.svg (auto-regenerated by update-tokens.yml)
2026-05-11 08:40:09 +02:00
johnnyfish f49de0fb01 fix: teach agent to use OneCLI gateway credentials after MCP server install 2026-05-10 19:23:22 +03:00
Yaniv Golan 9e1dbdf48c chore(container): bump claude-code 2.1.116 → 2.1.128
12 patch versions ahead. The 2.1.120 binary baseline introduced a
number of plugin and skill behaviors that have since landed in the
public Claude Code docs: ${CLAUDE_EFFORT} substitution, settled
`arguments` field in skill frontmatter, plugin `channels` field.

No breaking changes for nanoclaw's runtime contract. Verified by
running container/skills/{agent-browser,vercel-cli,slack-formatting}
under the bumped image; all three load and execute as expected.
SDK at ^0.2.116 (caret) remains compatible with claude-code 2.1.128.

Bumping CLAUDE_CODE_VERSION invalidates the pnpm install layer in
container/Dockerfile and triggers a full rebuild of the agent image.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 21:15:43 +03:00
Petki Tamás ad5d4d2664 feat(container-config): add per-group model + effort overrides
Allow individual agent groups to opt into different models or effort levels
without changing host-wide defaults. Useful when one group is high-stakes
(opus, high effort) but most are routine (sonnet/haiku, low effort).

container.json gains two optional fields:
  - model: alias ("sonnet" | "opus" | "haiku") or full model ID
  - effort: "low" | "medium" | "high" | "xhigh" | "max"

Both omitted = SDK default (current behavior). The host plumbs them as
NANOCLAW_MODEL / NANOCLAW_EFFORT env vars at container spawn time; the
agent-runner reads them in providers/index.ts and threads through to the
provider via ProviderOptions. The Claude provider passes them straight to
sdkQuery options.

`effort` is currently typed as `any` because the @anthropic-ai/claude-
agent-sdk type doesn't surface it yet — passing it through still works at
runtime via the SDK's loose option handling. Drop the cast once the SDK
adds an `effort` field to its options type.
2026-05-09 21:04:08 +03:00
gavrielc 0287d71595 docs: move restart guidance into config help descriptions
One-liner in cli.instructions.md pointing to `ncl groups config help`.
Each config operation's description now says whether restart or rebuild
is needed — agent discovers it via progressive disclosure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:41:02 +03:00
gavrielc 6539c0286a docs: explain that CLI config changes require restart
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:39:24 +03:00
gavrielc 1c7623ca41 docs: update for container config DB, on-wake, and CLI scope
- CLAUDE.md: new key files, updated groups verbs, rewritten self-mod
  section, new Container Config and Container Restart sections
- db-central.md: container_configs table (§1.15), migrations 014+015
- db-session.md: messages_in schema with trigger, source_session_id,
  on_wake columns
- schema.ts: comment no longer references disk-based config
- cli.instructions.md: rewritten for scope-aware usage, auto-fill,
  restart/config ops, group-scoped examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:23:44 +03:00
gavrielc be3a8a97c6 feat: race-free on-wake messages and explicit restart CLI
Decouple container restart from config updates — config CLI ops now only
write to the DB; restart is a separate `ncl groups restart` command with
--rebuild and --message flags. Add on_wake column to messages_in so wake
messages are only picked up by a fresh container's first poll, preventing
dying containers from stealing them during the SIGTERM grace window.
killContainer accepts an onExit callback for race-free respawn. Agent-
called restart auto-scopes to the calling session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 19:02:15 +03:00
gavrielc 1efe28ccdc feat(cli): support space-separated multi-word verbs
`ncl groups config get` now works alongside `ncl groups config-get`.
Parser joins all positionals with dashes; dispatcher falls back by
trimming the last segment as a target ID (`ncl groups get abc123`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 12:04:45 +03:00
gavrielc 6caad0757a fix(cli): add list filtering/pagination, fix double-close in container ncl
- genericList now accepts column filters (--flag value) and LIMIT (default 200)
- Remove early inDb.close() in container pollResponse to avoid double-close
- Document filtering and --limit in cli.instructions.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 21:02:23 +03:00
gavrielc ed571d1f66 docs(cli): add write examples, approval flow, and nc→ncl rename
- Add approval flow section explaining the request→notify→result mechanics
- Add write command examples (groups create, roles grant, members add, etc.)
- Rename stale `nc` references to `ncl` in container instructions
- Add CLI reference section to host CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 20:45:18 +03:00
gavrielc 0855369b79 refactor(cli): rename nc to ncl
Rename the CLI binary, socket path, container wrapper, error prefixes,
and all references from `nc` to `ncl`. Add ~/.local/bin symlink during
setup and pnpm script alias.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 15:56:09 +03:00
gavrielc 33cbf59dd8 Merge remote-tracking branch 'origin/main' into nc-cli 2026-05-08 15:35:03 +03:00
gavrielc 85850874ab test: add delivery retry, permission check, and poll-loop error recovery coverage
Delivery:
- Retry exhaustion: adapter fails 3x → markDeliveryFailed
- Retry recovery: transient failure then success clears counter
- Permission check: unauthorized channel destination blocked

Poll-loop (container):
- Provider error: error written to outbound, loop continues
- Stale session: isSessionInvalid → continuation cleared
- /clear command: session wiped, confirmation written

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 15:24:42 +03:00
gavrielc 2f552ce1bb Merge pull request #2321 from johnnyfish/jf/onecli-gateway-skill
feat(skills): add onecli-gateway container skill with auto-composed instructions
2026-05-08 01:09:09 +03:00
gavrielc f3e19872ac refactor: use static gateway skill instead of fetching on spawn
Remove the dynamic `onecli.getGatewaySkill()` fetch from `buildMounts` —
the skill content ships as a static SKILL.md. This avoids adding latency
to every container spawn and dirtying the source tree at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 01:07:09 +03:00
gavrielc 9090c33e7e docs(cli): add agent instructions for nc CLI
Auto-discovered by composeGroupClaudeMd() as module-cli.md fragment,
included in every agent group's composed CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 00:48:57 +03:00
gavrielc 107945f10c fix(agent-to-agent): route A2A replies back to originating session (#2267)
Squash merge of PR #2267 by ddaniels.

When an agent group has more than one active session, A2A replies landed
in the newest session via findSessionByAgentGroup's ORDER BY created_at
DESC. The session that asked the question never saw the answer.

Adds origin-aware return-path routing with three layers:

1. Direct return-path: if the reply has in_reply_to, look up the
   triggering inbound row's source_session_id and route there.
2. Peer-affinity fallback: find the most recent A2A inbound from this
   peer and use its source_session_id.
3. Legacy fallback: newest active session (pre-migration compat).

Container-side: MCP send_message/send_file now thread the current
batch's in_reply_to through to outbound rows via current-batch.ts.

Also flips our A2A bug-documenting test (#2332) from asserting the
broken behavior to asserting the fixed behavior.

Co-Authored-By: Doug Daniels <ddaniels888@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 00:48:10 +03:00
gavrielc 684a98d078 test: add host-side routing and session resolution tests
Host-side (vitest):
- Routed message preserves platformId/channelType/threadId on messages_in
- Fan-out gives each agent correct per-agent routing
- writeSessionRouting populates session_routing from messaging group
- writeSessionRouting writes null routing for agent-shared sessions
- Per-thread session includes thread_id in session_routing
- Agent-shared resolves to same session on repeated calls
- Agent-shared session has null messaging_group_id
- findSessionByAgentGroup returns channel-bound session (documents #2332)
- Skip: agent-shared/channel-bound coexistence (blocked on #2332 fix)

Container-side (bun:test):
- Internal tags stripped between message blocks
- Mixed task + chat batch with correct routing

The agent-shared tests uncovered the exact bug from #2332:
findSessionByAgentGroup doesn't distinguish agent-shared from
channel-bound sessions, so A2A resolution reuses a channel session
when one exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 00:26:41 +03:00
gavrielc 3af6e70c05 test(agent-runner): add dispatch, origin metadata, and thread resolution tests
Add 14 tests covering key routing and dispatch flows that previously had
zero direct coverage:

dispatchResultText:
- bare text produces no outbound (scratchpad only)
- unknown destination dropped, valid destination sent
- multiple <message> blocks each produce correct outbound
- internal tags stripped from scratchpad

originAttr / from= metadata:
- chat/task/webhook/system messages include from= when destination matches
- fallback to raw unknown:channel:platform when no match
- from= omitted when routing is null

resolveDestinationThread:
- null thread_id when no prior inbound from destination
- most recent thread_id wins with multiple inbound messages

Also fix merge issue: restore getAllDestinations import removed by our PR
but still needed by #2327's compaction reminder. Fix stale destinations
test assertion from #2328 ("no special wrapping needed" → "Every response
must be wrapped").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 00:23:03 +03:00
gavrielc 6a56b10ffc Merge pull request #2335 from adjohn/fix/container-pin-pnpm-10
fix(container): pin pnpm to 10.33.0 to match host
2026-05-08 00:11:58 +03:00
gavrielc 350d9631fa Merge pull request #2327 from glifocat/wip/compaction-destination-reminder
fix: inject destination reminder after SDK auto-compaction
2026-05-07 23:57:29 +03:00
gavrielc d92c676327 Merge pull request #2328 from glifocat/wip/destinations-default-to-origin
fix: default reply destination to message origin in multi-destination groups
2026-05-07 23:44:42 +03:00
Adam Johnson 6f0b8f1961 fix(container): pin pnpm to 10.33.0 to match host
Corepack with no version pin pulls latest pnpm (currently 11.0.8), which
silently stops honoring `only-built-dependencies[]=` in `.npmrc` for
global installs. The allowlist file ends up correctly written but
ignored, so:

  - `@anthropic-ai/claude-code`'s postinstall — which downloads the
    platform-native Claude binary — never runs. Agents then crash at
    runtime with "claude native binary not installed... postinstall did
    not run."
  - `agent-browser`'s postinstall, which chmods the linux-arm64 binary,
    is also skipped, so the binary fails with EPERM the first time it's
    invoked.

Pin the container's pnpm to 10.33.0 (the same version host's
package.json already pins via `packageManager`). Keep the two in
lockstep so a host bump triggers a deliberate container bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:37:15 -07:00
johnnyfish 1240a0cf4f feat: fetch gateway skill from OneCLI API with static fallback 2026-05-07 22:16:48 +03:00
gavrielc e3645f799c address review: add thread resolution test, log catch, remove stray comment
- Add integration test for per-destination thread_id resolution: seeds two
  destinations with different thread IDs, verifies each outbound message
  carries the correct thread_id (not a global one from the batch routing).
- Add log line in resolveDestinationThread catch block for debuggability.
- Remove stray "(ensurePreCompactHook is defined after the main function.)"
  comment from group-init.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 20:33:06 +03:00
gavrielc 9db39b291d fix(agent-runner): require explicit destination addressing, fix per-destination threading
The poll loop had a bare-text routing fallback in dispatchResultText: when
the agent produced text without <message to="..."> wrapping, it would auto-
route to the session's originating channel (via a frozen RoutingContext) or
to the single configured destination. This caused three problems:

1. Routing drift: RoutingContext was extracted once from the initial batch
   and never refreshed. When the initial batch was a null-routed cron task
   and a real chat arrived mid-query, replies were silently dropped to
   scratchpad because the frozen routing had all-null fields.

2. Cross-channel thread bleed: sendToDestination applied a single
   routing.threadId to every outbound message regardless of destination.
   In agent-shared sessions (multiple channels sharing one session), one
   channel's thread ID was stamped onto messages to a different channel.

3. Inconsistent formatting: task, webhook, and system messages had no
   origin metadata in their formatted output, so the agent couldn't tell
   which destination they came from — even when the underlying messages_in
   rows carried routing fields.

Changes:

- Remove the bare-text routing fallbacks in dispatchResultText (both the
  routing-based and single-destination shortcuts). All agent output must
  be wrapped in <message to="name">...</message>. Bare text is scratchpad.

- Update buildDestinationsSection() to require explicit wrapping for all
  groups, including single-destination. No more "no special wrapping
  needed" shortcut.

- Resolve thread_id per-destination via resolveDestinationThread(), which
  queries messages_in for the most recent message matching the target
  channel+platform. Falls back to null (top-level channel message) when
  no prior inbound exists for that destination.

- Extract originAttr() helper in formatter.ts and apply it to all message
  types. Tasks now render as <task from="dest" time="...">, webhooks as
  <webhook from="dest" source="..." event="...">, system responses as
  <system_response from="dest" ...>. The agent always sees where a
  message originated.

- Add a PreCompact shell hook (compact-instructions.ts) that outputs
  custom compaction instructions, telling the compactor to preserve
  recent message XML structure and routing metadata in the summary.
  Wired via settings.json in the .claude-shared scaffold, with a
  migration path (ensurePreCompactHook) for existing groups.

Relation to open PRs:

- #2277 (mergeRouting) becomes unnecessary — the routing fallback it
  patches no longer exists. Can be closed.
- #2327 (post-compaction destination reminder) is complementary — it
  handles the post-compaction push, this handles pre-compaction
  instructions. Both can merge independently.
- #2328 (default routing instruction) is complementary — it adds "reply
  to the from= destination" guidance to the multi-destination section.
  Compatible with the unified instruction format here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 19:47:46 +03:00
glifocat 12719be6e1 feat(poll-loop): inject destination reminder after SDK auto-compaction
Closes qwibitai/nanoclaw#2325.

When the Claude Code SDK auto-compacts the conversation context, the
compaction summary tends to drop the agent's learned <message to="…">
wrapping discipline. The destinations table is still populated and the
system prompt still lists them, but the behavioral pattern degrades —
A2A sends and multi-channel routing silently revert to bare-text or
single-channel delivery for the rest of the session, until the next
/clear.

Three small changes wire a reminder back into the live query when this
fires:

- New `compacted` event on ProviderEvent. Distinct from `result` so it
  doesn't mark the turn completed or get dispatched as a chat message
  (which is also why "Context compacted (N tokens compacted)." stops
  appearing as noise in user-facing chats — it was a side-effect of
  reusing the result event path).
- ClaudeProvider yields `compacted` instead of `result` for the SDK's
  compact_boundary system event.
- Poll-loop's event handler reacts by pushing a system-tagged reminder
  back into the active query when there are >1 destinations. Single-
  destination groups skip the push since they have a fallback that
  works without wrapping.

Tests cover both branches (multi-destination → reminder fires;
single-destination → no reminder) using a CompactingProvider that
emits the new event mid-stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:11:25 +02:00