Skip to content

select bot slash commands by the harness advertised in %contacts - #6252

Open
patosullivan wants to merge 15 commits into
po/hermes-bot-profile-authorsfrom
patrick/tlon-6300-bots-advertise-their-slash-command-manifests
Open

select bot slash commands by the harness advertised in %contacts#6252
patosullivan wants to merge 15 commits into
po/hermes-bot-profile-authorsfrom
patrick/tlon-6300-bots-advertise-their-slash-command-manifests

Conversation

@patosullivan

@patosullivan patosullivan commented Aug 11, 2026

Copy link
Copy Markdown
Member

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-info claim in %contacts so 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—/tlon and /migrate are 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

  • Each runtime publishes v, harness, its plugin or adapter version, and an optional host harnessVersion as JSON in a %text bot-info field 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.
  • The client carries bot-info through the contact API and a new nullable contacts.bot_info column. V1 contact sources can set or clear it; lossy v0 /all rows preserve a stored claim, and qualifying bot DMs without a usable claim trigger a bounded /v1/contact/{ship} backfill.
  • parseBotInfo accepts 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.
  • Bot DMs resolve the counterpart ship and select that contact's list. The home-group bot channel has no counterpart ship signal, so it continues to use the fallback list; identifying its bot member is tracked separately in TLON-6301.

Command lists and drift protection

  • OpenClaw's app list adds the missing /tlon and /migrate commands. 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.
  • The dormant Hermes constant does not include any adapter control commands. The selectable Hermes list contains the adapter's ten advertised commands plus six host-core commands audited against the pinned Hermes agent. Its new eleven-row adapter registry also keeps the /tlon-version legacy alias handled but deliberately excludes that alias from suggestions.
  • A shared contract compares each runtime fixture with the corresponding app-owned runtime list after sorting. Exact sequence equality catches additions, removals, and duplicates, while popup order remains an app concern expressed through priorities. The contract also runs in bot-checks, including on bot-only pull requests.
  • Popup icon lookup now accepts only real own exports from the icon namespace, excludes module metadata, and falls back to the generic command glyph. Tests assert that every static entry resolves and that both harnesses retain their curated order.
  • The Hermes deployment filter now covers every top-level runtime Python module while excluding tests, so the identity publisher and command registry participate in hosted bot restarts.
  • The shared bot E2E suite gains a scenario asserting the claim reaches both the bot's own profile and the owner's peer contact against real ships.
  • Both bot CI workflows vendor desk dependencies in their own step before the E2E run. assemble-desk.sh is called under a 300-second cap meant to contain a hung fetch, but that cap also covered peru 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/self and 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 %contacts agent accepts the poke, that the namespaced key survives sane-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

  • Safe to rollback without consulting PR author? Yes
  • Affects important code area:
    • Onboarding
    • State / providers
    • Message sync
    • Channel display
    • Notifications
    • Other: contacts sync/schema and bot runtime publishing

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 /tlon and /migrate; sending /migrate invokes 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 %self poke with bot-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 to develop once that merges.

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.
@linear-code

linear-code Bot commented Aug 11, 2026

Copy link
Copy Markdown

TLON-6300

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/shared/src/db/migrations/0000_hesitant_ultimo.sql Outdated
Comment thread packages/openclaw/src/bot-info.ts
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/shared/src/store/useBotSlashCommandManifest.ts Outdated
Comment thread packages/hermes-tlon-adapter/adapter.py Outdated
Comment thread packages/shared/src/store/contactActions.ts Outdated
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/openclaw/src/bot-command-manifest.ts Outdated
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.
@patosullivan patosullivan changed the title bots advertise slash-command manifests in their %contacts profiles select bot slash commands by the harness advertised in %contacts Aug 11, 2026
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.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant