select bot slash commands by the harness advertised in %contacts - #6252
Conversation
…ots-advertise-their-slash-command-manifests
Icon names now arrive from bot-published manifests, so resolve them against real icon exports rather than `name in icons` — under the native CJS interop that check also matches __esModule and inherited Object.prototype keys, which the renderer would then try to mount, letting a bot crash the composer for a whole conversation. Both registries carry the per-command glyphs develop's static list uses, so advertised popups keep today's appearance instead of degrading to a uniform generic icon. A client-side contract test asserts every fixture icon actually resolves, so a typo'd name fails CI instead of silently falling back.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dab143c910
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The OpenClaw plugin is installed with plain `npm install` in its containerized shared E2E, so the `@tloncorp/shared` devDependency added for these tests 404'd on an unpublished workspace package and the gateway never came up. The parser lives in shared; the fixtures are readable by path from anywhere, so the assertions move there and the plugin keeps no workspace dep.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db7631e700
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A poke that failed transiently after a successful self-contact read left a healthy long-lived bot unadvertised until an unrelated SSE reconnect or a restart. Both publishers now retry the write in place — 3 attempts, 2s/8s backoff between them, never after the last — then fall through to the existing non-fatal handling. Reads are never retried and an unchanged compare still performs no pokes and no sleeps.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 713886681e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A shutdown or config-reload restart during the 2s/8s backoff left the retired monitor alive for up to ten seconds, retrying against its stale SSE client. The sleeper now takes the monitor's abortSignal — clearing the pending timer and rejecting on abort, following the authentication backoff's pattern — and the rejection lands in the existing non-fatal skip. Hermes needs no change: asyncio.sleep is already task-cancellation aware and CancelledError escapes its except-Exception handlers.
Publishing full command manifests through %contacts made identity infrastructure carry a mutable, multi-KB, deploy-coupled application database: every ship that ever meets a bot stores that bot's whole profile, and every %self edit republishes it to every subscriber. The first real payload proved the point — 46 audited Hermes core commands overran the client's 32-entry abuse cap, and the cap had to grow to fit our own first write. Bots now publish only who they are, under `bot-info`: harness plus the plugin/adapter version, with the host runtime's version as an optional diagnostic rider that a missing value must never invalidate. The command lists move into the app as static per-harness lists, and a CI drift contract binds each list to a token-only fixture generated from that runtime's own registry — sorted-sequence equality, so an addition, a removal, or a duplicate turns red on the PR that makes it. The contract runs in bot-checks, which is what a runtime-only PR actually triggers. Ordering stays editorial: carried by `priority`, asserted through the production ranking function, never by array position.
mock's patch.dict restores sys.modules by clear+update, so a module first
imported inside a patched block gets evicted on restore. CI's Python 3.10
does not preload importlib.metadata the way 3.12 does: after the first
version-fallback test, every later patch("importlib.metadata.version")
patched a different module object than the one resolve_harness_version
re-imports, silently unmocking the fallback — two tests red on CI, green
locally. Importing it at test-module top puts it in every snapshot.
…nto patrick/tlon-6300-bots-advertise-their-slash-command-manifests
branch-desk.ts caps its assemble-desk.sh call at 300s to contain a hung fetch, but that cap also covered `peru sync`, which clones ~900MB of upstream history on a cold cache — roughly two minutes at best. Whenever GitHub ran slower than that the job died with SIGTERM before a single scenario loaded, which is how both Shared E2E jobs have been failing. Sync up front in its own step, answering to the job timeout, and set SKIP_SYNC so the capped call does only its two rsyncs. Cache the peru clone cache to make the sync itself cheap. Only .peru/cache is restored: .peru/lastimports records what peru already materialized, and on a fresh checkout with no desk-deps/ that could turn the sync into a no-op and leave assemble-desk.sh failing outright. Verified against a simulated fresh checkout — desk-deps/ removed, the lastimports files removed, .peru/cache kept: sync repopulates desk-deps in 0.3s and assemble with SKIP_SYNC=true produces a 629-file desk.
Every existing test of the publish path uses fakes, so nothing confirmed that a real %contacts agent accepts the poke, that the namespaced key survives sane-contact, or that it reaches the peer profile the client actually reads. The new scenario boots both runtimes against fake ships and asserts the claim on the bot's own /v1/self and on the owner's per-ship contact, byte-identical, with the harness and the runtime's real package version. The owner pokes %meet first. %contacts holds no record for an unmet peer, so the per-ship scry 404s without it — the same constraint the client solves the same way in its bot-info backfill. Scope is deliberately publication and replication only. Compare-then-poke idempotence stays unit-tested in both runtimes: asserting it here meant restarting the bot container mid-suite, which the OpenClaw gateway does not survive (gateway_stop with no recovery), and that took 19 of the following 24 scenarios down with it.
…ots-advertise-their-slash-command-manifests
OpenClaw's reply carries a Harness Version row naming its host runtime; the Hermes reply had no equivalent, so the two harnesses answered with different shapes despite the README describing one field-per-line summary. Render the row directly beneath Harness, sourced from the adapter's existing cached resolver. An absent or blank value reads `unknown` rather than dropping the row, matching OpenClaw, so the replies stay line-for-line comparable. The chat-reply assertions bind to a sentinel the resolver alone can supply: a regex for "some nonempty string" stays green when the wiring is removed and every bot silently reports `unknown`.
…-their-slash-command-manifests' into patrick/tlon-6320-hermes-report-harness-version-in-tlon-version
…t-harness-version-in-tlon-version report harness version in hermes /tlon version
Summary
A Hermes-backed bot currently opens the slash-command popup with OpenClaw suggestions, so people are offered commands that bot cannot run. The client hardcodes OpenClaw because bots provide no runtime identity; this adds a versioned
bot-infoclaim in%contactsso the claimed harness (the bot runtime: the TypeScript OpenClaw plugin or the Python Hermes agent) selects the matching app-owned command list. It separately fixes OpenClaw registry drift—/tlonand/migrateare real plugin commands missing from its list—and gives Hermes a reachable list audited against the adapter and pinned host core.Fixes TLON-6300
Changes
Identity publishing and client selection
v,harness, its plugin or adapterversion, and an optional hostharnessVersionas JSON in a%textbot-infofield on its self-contact. OpenClaw and Hermes compare against the current field at boot and reconnect, poke only on a difference, retry transient write failures, and treat publishing failure as non-fatal. A failed self-contact read never causes a blind write.bot-infothrough the contact API and a new nullablecontacts.bot_infocolumn. V1 contact sources can set or clear it; lossy v0/allrows preserve a stored claim, and qualifying bot DMs without a usable claim trigger a bounded/v1/contact/{ship}backfill.parseBotInfoaccepts only version 1 with non-empty string fields, caps the raw JSON at 512 UTF-8 bytes and each field at 64 Unicode code points, and ignores unknown fields for forward compatibility. Missing, malformed, mis-cased, or unknown harnesses select the OpenClaw fallback.Command lists and drift protection
/tlonand/migratecommands. Its ten plugin commands now come from one registration table that also produces the committed token fixture; the three host-core suggestions remain audit-pinned app constants./tlon-versionlegacy alias handled but deliberately excludes that alias from suggestions.bot-checks, including on bot-only pull requests.assemble-desk.shis called under a 300-second cap meant to contain a hung fetch, but that cap also coveredperu sync, which cold-clones roughly 900MB of upstream history; when it overran, the job was killed before any scenario loaded. The peru clone cache is now cached, and the capped call does only its rsyncs.The wire schema, validation limits, version-source rules, two-phase command-removal rule, and explicit clear-to-null procedure are documented in
docs/bot-info.md. No Hoon or desk changes are required because v1 contacts already carry open key-value fields through their normal sync paths.How did I test?
The shared, API, app, OpenClaw, and Hermes adapter test suites pass, along with repo-wide TypeScript checks and Prettier on the changed files. Focused coverage exercises claim parsing and publishing, retry and compare-before-poke behavior, contact-source provenance and cold-start backfill, runtime selection and fallback, command registry/fixture parity, popup ordering, and icon resolution. The drift contract was also mutation-checked with added, removed, and duplicated tokens in both runtime fixtures.
A shared E2E scenario,
bot-info-publishes-and-replicates, exercises the publish path against real ships in Docker for both runtimes: it asserts the claim on the bot's own/v1/selfand on the owner's per-ship contact, byte-identical, carrying the harness and that runtime's real package version. This is the part no unit test can reach — that a live%contactsagent accepts the poke, that the namespaced key survivessane-contact, and that it reaches the profile the client actually reads. Both drivers pass, publishing{"v":1,"harness":"hermes","version":"0.15.0","harnessVersion":"0.17.0 (2026.6.19)"}and{"v":1,"harness":"openclaw","version":"0.19.0","harnessVersion":"2026.5.28"}respectively. Disabling a publisher fails that scenario and only that scenario.Its scope is publication and replication. Compare-then-poke idempotence remains unit-tested in both runtimes rather than asserted there, because asserting it end to end required restarting the bot container mid-suite, which the OpenClaw gateway does not survive.
Risks and impact
The identity claim is self-published, but it can only select between static lists shipped by the app; no wire-provided command, label, or icon is rendered. Invalid claims and failed reads fall back to OpenClaw, and publish failures do not stop either bot runtime.
The database change is an additive nullable column. The visible OpenClaw list gains
/tlonand/migrate; sending/migrateinvokes the existing diary-to-notes migration flow. Hermes selection is DM-only in this scope, and home-group conversations keep the fallback list.Command lists ship with the app, so a bot-side command change reaches users on an app release rather than a bot deploy, and a third-party bot cannot advertise commands of its own — an unrecognized harness gets the fallback list.
The hosted-bot deploy trigger is intentionally wider for Hermes top-level runtime modules. Command removals require a two-phase rollout: keep the runtime handler until an app release no longer suggests it.
Rollback plan
Revert this pull request. The base app does not read
bot-info, so claims already published in contact profiles are inert after an app rollback. If the identity key should also be removed from bot profiles, send the documented%selfpoke withbot-info: null; stopping a publisher alone does not delete a merged contact key.Screenshots / videos
Not included; this changes which commands populate the existing popup, not its layout.
Stacked on #6243 (base branch
po/hermes-bot-profile-authors); retarget todeveloponce that merges.