feat(channels): add Max Messenger channel#1109
Open
HumanGoClaude wants to merge 20 commits intonextlevelbuilder:devfrom
Open
feat(channels): add Max Messenger channel#1109HumanGoClaude wants to merge 20 commits intonextlevelbuilder:devfrom
HumanGoClaude wants to merge 20 commits intonextlevelbuilder:devfrom
Conversation
added 20 commits
May 6, 2026 09:19
Skeleton implementation of channels.Channel interface for Max Messenger (https://max.ru). Lays groundwork for full integration to be filled in by subsequent commits. Day 1 deliverables: - internal/channels/max/ package with factory + Channel struct - TypeMax constant in internal/channels/channel.go - Factory registration in cmd/gateway.go - 7 unit tests covering factory parsing and Channel interface Day 2-5 will add: - Max API HTTP client (GET /me, GET /updates, POST /messages, etc.) - Inbound polling loop and message translator - Outbound dispatcher with chunking and Markdown formatting - Streaming preview via PUT /messages - Webhook mode (POST /subscriptions, HTTPS-only) - request_contact HMAC verification - Reactions, callback buttons, media upload - Documentation in docs/05-channels-messaging.md Refs: https://dev.max.ru/docs-api
Adds production implementation of Max Messenger inbound: - types.go: API types matching dev.max.ru/docs-api - client.go: HTTP client with auth, retries, JSON enc/dec - inbound.go: long-poll loop, update dispatch, translator - max.go: real Start/Stop lifecycle with bot probe + handler pool Send is still a stub (Day 3). Media downloads stubbed (Day 4). Webhook mode (POST /subscriptions) is Day 4. Refs: https://dev.max.ru/docs-api
Critical findings from live PoC test against platform-api.max.ru:
1. Inner message content is returned under JSON key 'message',
not 'body' as suggested in some docs sections. Real payload
structure: {message: {sender, recipient, message: {mid, seq, text}}}.
2. Recipient.user_id and chat_id are BOTH populated for direct
messages (user_id = bot's id, chat_id = dialog thread id).
Authoritative discriminator is chat_type ('dialog' vs 'chat').
3. DM chat_id is the dialog thread ID, not the sender's user_id —
stable per-conversation identifier suitable for goclaw session keys.
Patches:
- types.go: Message.Body json tag 'body' -> 'message'
- types.go: Recipient.IsDialog() now uses ChatType
- inbound.go: DM chatID = recipient.chat_id
Verified against live bot: text 'проверка фикса', 'второй тест',
and image attachment all parse correctly.
Refs: https://dev.max.ru/docs-api
Adds 39 unit tests covering Day 2 production code: client_test.go (18 tests): - GetMe success/auth-error/network-error - GetUpdates with/without marker, type filtering, rate-limit retry, context cancellation - SendMessage DM/group/validation - EditMessage, PostAction, AnswerCallback - SubscribeWebhook, UnsubscribeWebhook inbound_test.go (21 tests): - DM text translator with real PoC fixture - DM with image attachment - Group with @mention via markup — passes mention gate - Group without mention — filtered when require_mention=true - Group without mention — passes when require_mention=false - Self-loop guard, empty messages, malformed messages - detectMention: markup-based, textual fallback, case-insensitive - stripBotMention table-driven - Recipient.IsDialog discriminator - chatIDFromUpdate fallbacks - buildMetadata: basic, edited flag, linked reply - Marker get/set with copy-safety Test data includes real captures from PoC live test plus synthetic fixtures for group/callback scenarios. Refs: https://dev.max.ru/docs-api
Implements channels.Channel.Send for the Max channel. outbound.go (new, 134 LOC): - Send: parses ChatID from bus.OutboundMessage, chunks content, sends each chunk via POST /messages with format=markdown - parseChatID: validates bus string -> int64 conversion - lastMessageIDFor: lookup last sent message_id per chat (used by streaming preview in Day 4) format.go (new, 107 LOC): - chunkText: paragraph -> line -> sentence -> word -> codepoint-safe hard cut - safeUTF8Cut: prevents splitting multi-byte UTF-8 sequences types.go: - SendMessageResponse extended with top-level chat_id, recipient_id, message_id (matches real API response observed in PoC) client.go: - SendMessage / EditMessage now return *SendMessageResponse instead of *Message — needed to expose message_id for streaming edits max.go: - Channel struct gains placeholders sync.Map and sentCount counter - Send() wired to outbound.send Tests: 20 new (11 outbound + 9 format), 3 existing client tests adjusted for new return type. Total 66 unit tests, all green. Day 4 will add: media upload/download, streaming preview (PUT /messages), webhook mode, request_contact HMAC verification. Refs: https://dev.max.ru/docs-api
Day 4 of Sprint 9 — adds production features short of streaming
(StreamingChannel deferred to Day 4.5 due to its complexity).
New files:
auth.go, auth_test.go (60 + 90 LOC, 8 tests)
- VerifyContactHash for request_contact button auth
- HMAC-SHA256(bot_token, vcf_info) per Max docs
- Constant-time comparison; distinct errors for bad-format vs bad-hash
webhook.go, webhook_test.go (144 + 222 LOC, 12 tests)
- WebhookChannel impl: WebhookHandler() returns (path, http.Handler)
- HTTPS-only enforcement, body size cap (1 MiB), proper status codes
- Always 200 OK once parsed (Max retries non-2xx — avoid retry storms)
media_download.go, media_test.go (285 + 413 LOC, 15 tests)
- HTTP fetch with retry, size cap (25 MiB), Content-Length pre-check
- Per-attachment failures logged and skipped — partial-success preferred
- Sanitized filenames; type-prefixed temp files (goclaw_max_image_*.jpg)
media_upload.go (343 LOC, tests in media_test.go)
- Two-step Max upload: POST /uploads -> multipart push -> attach token
- Handles image (photo_ids/photos), video, audio, file
- Failed uploads logged but don't fail send — text still ships
reactions.go, reactions_test.go (204 + 238 LOC, 10 tests)
- ReactionChannel impl mapping goclaw status -> Max actions API
- Per-chat refresher goroutines (4s interval keeps typing visible)
- Graceful Stop via stopAllReactionRefreshers()
Updated:
max.go — Channel struct gains runCtxMu/pollRunCtx/reactionRefreshers
Start saves pollRunCtx; Stop drains reaction refreshers
BlockReplyEnabled() method (BlockReplyChannel impl)
inbound.go — downloadInboundMedia stub removed (real impl in
media_download.go)
outbound.go — Send wires uploadAndAttachMedia; media attached only to
first chunk; sendOneChunk helper for media-only case
Tests: 45 new (8 auth + 12 webhook + 15 media + 10 reactions),
111 total all green.
Day 4.5 will add StreamingChannel implementation.
Refs: https://dev.max.ru/docs-api
Day 4.5 of Sprint 9 — adds StreamingChannel implementation. Architectural decision: Опция 2 (answer-only streaming). Reasoning lanes (Опция 3) deferred to a future PR. Slack and Max share this minimal pattern; Telegram extends it with per-lane draft transport. stream.go (312 LOC): - maxStream struct implementing channels.ChannelStream: * Update: throttle (800ms) + dedup + accumulator pattern * Stop: final flush, idempotent, no message deletion * MessageID: returns 0 (Max uses string mids, handed off via type assertion) - StreamingChannel methods on *Channel: * StreamEnabled: cfg.DMStream/GroupStream with default true/false * CreateStream: POST '💭 Печатаю...' eagerly, returns stream w/ mid * FinalizeStream: type-asserts maxStream, stores mid in c.placeholders * ReasoningStreamEnabled: false (Опция 2) Lifecycle: CreateStream → POST /messages '💭 Печатаю...' → returns stream w/ messageID Update(text) → PUT /messages (throttled, plain text) Stop → final PUT if pending text remains FinalizeStream → c.placeholders.Store(chatID, messageID) Send (next call) → consumePlaceholder → PUT /messages with markdown format Configuration (factory.go): - dm_stream: default true (modern UX expected for new channel) - group_stream: default false (Max doesn't yet support bots in groups) outbound.go updates: - Send detects pre-stored placeholder via consumePlaceholder - First chunk routes to editPlaceholder (PUT) instead of SendMessage (POST) - Subsequent chunks always POST as before - Media-on-first-chunk path: deletes placeholder, sends fresh (Max API doesn't support attachments on EditMessage) - Edited placeholder mid is NOT re-stored — would cause next Send to overwrite the user-visible answer instead of producing a new message Decisions documented: - Throttle 800ms: balance UX vs Max 30 rps limit (1.25 rps even at full utilization, headroom for parallel runs) - Plain text during stream, markdown final: avoids partial-token rendering glitches mid-stream (e.g. unclosed **bold) - Edit errors logged at debug, not propagated: stream is best-effort UX; the next Update will retry with fresher text - Eager placeholder (in CreateStream): user sees '💭 Печатаю...' immediately on receiving their message — better UX than lazy-on-first-chunk Tests: 22 new (throttle, dedup, lifecycle, accumulation, FinalizeStream handoff, full E2E with Send placeholder edit). Total 133 unit tests, all green. Refs: https://dev.max.ru/docs-api
docs/05-channels-messaging.md: - New '## 12. Max Messenger' section, structured in Slack/WhatsApp style - Key Behaviors covering both transports (polling + webhook), markdown forwarding, streaming, reactions, media, request_contact verification, group support status, lifecycle - API Findings table documenting three live-API discrepancies vs docs (body→message, recipient discriminator, DM chat_id semantics) - Configuration shapes for channel_instances.config and credentials JSONB - Streaming lifecycle Mermaid diagram showing CreateStream → Update → Stop → FinalizeStream → Send placeholder edit handoff Cross-channel sections (Channel-Isolated Workspaces, Local Key Propagation, Per-User Isolation, Pairing System) renumbered 12-15 → 13-16 to keep the channel-specific group (5-12) contiguous. internal/channels/max/README.md (new): - File map describing each .go file's purpose - Architecture decisions: minimal upstream fork (5 LOC), custom HTTP client rationale, answer-only streaming choice, placeholder ownership invariant, chat_type discriminator - Live test commands via cmd/max-smoke - Guide for adding new endpoints - Known limitations (groups, voice, callbacks, request_contact outbound) Refs: https://dev.max.ru/docs-api
Addresses 5 of 7 valid issues from a four-role expert review (Architect,
Product Owner, Analyst, Team Lead Go). Two issues (concurrent runs race,
media-upload-failure UX) were rejected after maintainer reverification.
Code fixes:
webhook.go — independent context for dispatch
Webhook handler used to dispatch on c.pollRunCtx, sharing the polling
goroutine's lifecycle. A webhook arriving mid-Stop saw a just-cancelled
context and dropped the message AFTER we'd already 200-OK'd Max →
silent message loss during rolling restarts. Fix: per-delivery
context.Background with 5-minute timeout, dispatched on a fresh
goroutine. Renamed runContext → pollContext (still used by reaction
refreshers, which correctly stop on channel Stop).
client.go, media_download.go — DownloadFile helper
media_download.go reached into c.client.httpClient directly, breaking
encapsulation. New Client.DownloadFile uses the same transport stack;
future middleware (otelhttp, retry) will apply uniformly to both API
calls and asset downloads.
stream.go — orphan placeholder cleanup
When a stream's CreateStream succeeds but no Update fires (agent crash
before first chunk), the '💭 Печатаю...' placeholder lived in chat
forever. Fix: FinalizeStream now deletes the placeholder if lastSent
is empty.
outbound.go, stream.go — panic safety
defer recover in send() restores the consumed placeholder before
re-raising — preserves runtime visibility while preventing UI orphans
if a slog handler or transport panics. defer recover in flushLocked()
swallows (stream is best-effort UX; the next Update retries).
media_upload.go — single transient retry
RequestUploadURL and UploadFile now retry once on transient errors
(context.DeadlineExceeded, net.OpError, 5xx, EOF, connection reset).
Permanent errors (4xx, local file errors) are not retried. Trade-off
documented: a transient retry may produce one orphan upload, which
Max storage TTLs out within 24-48h.
Test additions:
stream_test.go — TestStreaming_ConcurrentRuns_DoNotInterfere
Documents the known limitation that c.placeholders is keyed only on
chatID, so the second FinalizeStream in a chat overwrites the first.
Practically unreachable in DM (debounce + per-session run limits);
will need a per-RunContext key once Max enables group bots.
stream_test.go — TestFinalizeStream_DeletesOrphanPlaceholder
Regression check for the orphan cleanup fix.
webhook_test.go — TestServeWebhook_DispatchSurvivesStop
Verifies ServeHTTP returns promptly (dispatch is async). Full
Start/Stop lifecycle aspect of the fix is exercised in
integration_test.go.
integration_test.go (NEW)
Full pipeline: Channel.Start → mock backend serves /me + /updates →
bus delivery → Channel.Send → Channel.Stop. The safety net we lacked.
Doc updates:
internal/channels/max/README.md
- 'Concurrent runs in same chat' under Known limitations
- New 'Webhook security' section with required and recommended
operator controls (hard-to-guess URL, TLS, dm_policy: allowlist,
ingress allowlist, rate limiting)
docs/05-channels-messaging.md
- 'Webhook security' subsection appended to Max Messenger section,
summarizing the operator guidance.
Tooling:
apply-day5b.sh runs go test with -race. The Max channel has multiple
goroutines (polling, reaction refreshers, webhook dispatch); race
detector coverage is non-negotiable for production.
Rejected after maintainer reverification:
Concurrent runs placeholder race: documented as known limitation.
Goclaw's debounce + per-session run limits make this practically
unreachable in DM. Adding a per-chat mutex would block legitimate
parallel work without solving the actual problem (which is the
chatID-only handoff key, not concurrency itself).
Media upload failure UX (Variant A: append '(не удалось прикрепить
файл)' to text): rejected. Hardcoded Russian, modifies agent output
without agent's knowledge, may corrupt conversation history. Better
long-term fix is to surface failure metadata on a follow-up agent
turn — out of scope for this PR.
Tests: 4 new (orphan delete, concurrent runs limitation, webhook async
verify, full integration). Total 137 unit + 1 integration test, all
green with -race detector enabled.
Refs: https://dev.max.ru/docs-api
isValidChannelType() in internal/http/channel_instances.go had a hardcoded switch over known channel-type string literals. Although the factory was registered in cmd/gateway.go:470 and channels.TypeMax defined in internal/channels/channel.go:77, POST /api/channel-instances returned HTTP 400 'invalid channel_type' when admin UI tried to create a Max channel. Added 'max' to the validator's allow-list, matching the existing literal-based convention in the function.
Live PoC validation in local goclaw stack revealed that dm_policy and
group_policy settings were silently ignored: the inbound handler called
BaseChannel.HandleMessage directly, skipping the CheckDMPolicy /
CheckGroupPolicy flow that all other channels (whatsapp, telegram,
discord, slack, feishu, zalo) explicitly wire in their handlers.
Anyone could DM the bot regardless of dm_policy.
Day 1 left a misleading comment 'Hand off to BaseChannel — this
enforces allowlist + publishes to bus' which was incorrect:
HandleMessage only filters allowlist as a fallback and never consults
the pairing service. The README also claimed the channel 'rejects
unauthorized senders before invoking the agent' — this was false
until this patch.
Changes:
- policy.go: checkDMPolicy / checkGroupPolicy / sendPairingReply,
modeled on whatsapp/policy.go. PolicyNeedsPairing triggers a
pairing-code reply via Max API; PolicyDeny silently drops.
Debounce reuses BaseChannel.CanSendPairingNotif so repeated
messages from one unpaired sender don't burn fresh codes.
- inbound.go: parses chatID -> int64 and calls checkDMPolicy /
checkGroupPolicy before HandleMessage. Returns early on deny or
pairing-needed so the agent loop is never invoked for
unauthorized senders.
- policy_test.go: 12 tests covering open / disabled / allowlist /
pairing for DM, plus group disabled / open / allowlist. Tests
use the existing mockMaxBackend pattern from outbound_test.go,
so SendMessage calls hit a real httptest.Server. Verifies that
paired senders proceed, unpaired senders get a pairing reply,
allowlisted senders bypass pairing, and repeat sends from one
unpaired user are debounced.
- README.md: 'Production policy' section now accurately describes
what the channel enforces.
Group policy is implemented for forward compatibility — Max does not
yet expose adding bots to groups via the public API.
Run gofmt -w on 5 files to bring them in line with the project's formatting check. No semantic changes — whitespace and line-break alignment only. Files: - internal/channels/max/format_test.go - internal/channels/max/outbound_test.go - internal/channels/max/stream.go - internal/channels/max/stream_test.go - internal/channels/max/types.go
Bumps the messaging-channels count from 7 to 8 and adds Max to the three places it's listed: - the feature bullet - the Lite vs Standard comparison table - the documentation-links table
Adds Max to the channel-manager comment list and registers the new max/ subpackage in the file-tree breakdown. Mirrors the existing whatsapp/ entry.
Max implements all four optional channel interfaces (StreamingChannel, WebhookChannel, ReactionChannel, BlockReplyChannel). Updates the Extended Interfaces table in docs/05-channels-messaging.md so the list of implementers is complete and matches the code.
Adds Max Messenger to the Unreleased / New Features section. Entry documents transports (polling + webhook), streaming, reactions, media, access control, and current validation status (tested locally end-to-end; webhook mode not yet live-validated).
Replace the Russian "💭 Печатаю..." streaming placeholder with "💭 Thinking..." to match the English-default convention used by other channels (Telegram, Discord, Slack). The Russian text was a local artefact from PoC deployment for a Russian-speaking user base; in upstream goclaw, English is the appropriate default. Operators who want a localized placeholder per deployment can be addressed in a follow-up that introduces a configurable string (out of scope for this PR).
Backend factory was registered in cmd/gateway.go:470, but the React
admin dashboard had hardcoded enums in 5 files that omitted 'max',
making the channel invisible in the create-channel dropdown and
contacts/permissions/bindings UI.
Files:
- constants/channels.ts: CHANNEL_TYPES (used by setup wizard and
create form)
- channel-schemas.ts: credentialsSchema.max (bot_token) and
configSchema.max mirroring internal/channels/max/types.go
(mode, polling_timeout, dm_policy, group_policy, dm_stream,
group_stream, history_limit, media_max_mb, allow_from,
block_reply)
- channels-status-utils.ts: channelTypeLabels.max for status
badges
- contacts-page.tsx: CHANNEL_TYPES filter and PERM_CHANNELS
permissions list
- bindings-section.tsx: <SelectItem value=max> in the channel
binding selector
group_policy defaults to 'disabled' since Max does not yet expose a
platform API for adding bots to groups (see
internal/channels/max/README.md known limitations).
channels-section.tsx CHANNEL_META was intentionally NOT modified:
that section drives legacy config.json-based gateway-level env-var
secret overrides (GOCLAW_TELEGRAM_TOKEN etc.); Max stores its
bot_token in the channel_instances credentials column like all other
modern channels.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Max (https://max.ru) as a first-class channel
under
internal/channels/max/, following the same factory-registrationpattern as Telegram, Discord, Slack, Feishu, WhatsApp, Zalo OA, Zalo
Personal, Facebook, and Pancake. The new package supplies long-poll
and webhook transports, in-place streaming edits, allowlist / pairing
access control, and inbound / outbound media. Admin UI integration is
included so operators can create Max channels through the dashboard.
The backend wires through three small touch-points (one line each in
cmd/gateway.go,internal/channels/channel.go, andinternal/http/channel_instances.go). The admin dashboard receives Maxthrough five entries in the existing schema-driven channel forms — no
new components, just data added to the schemas already used by Discord,
Facebook, and Pancake.
The diff totals 48 files / +8,076 / −21 — large because the new
backend package is test-heavy: 3,188 LOC of production Go with
3,559 LOC of unit/integration tests + 173 LOC of JSON fixtures
captured from real API responses (a 1.12× test-to-production ratio).
The remaining ~1,140 LOC are documentation. The UI portion is 20 LOC of
TypeScript schema entries — no React components — so no UI test layer
was added (same pattern as Discord / Facebook / Pancake schemas).
Type
main)Target Branch
dev(perCONTRIBUTING.md).Checklist
go build ./...passesgo build -tags sqliteonly ./...passesgo vet ./...passesgo test -race ./...pnpm lint+pnpm buildpass$1, $2(no new SQL — uses theexisting
channel_instancestable only)(only the pairing-reply text and
"💭 Thinking..."streaming placeholder are new; both are English defaults
identical in approach to Telegram, Discord, Slack — none of
the existing channels localize these strings via
i18n.Catalog)internal/upgrade/version.go(if new migration)(no migration — uses existing
channel_instancestable withchannel_type='max')Test Plan
Automated
internal/channels/max/package,covering: HTTP client, inbound translator, outbound dispatcher,
chunker, streaming state machine, webhook signature verification,
HMAC for
request_contact, media download/upload, reactions,policy translator (DM + group), and a lifecycle
integration_test.goexercising start → poll → policy → dispatch → stream → finalize.
go test -race -count=1 -timeout=120s ./internal/channels/max/...(~20 s).go test -count=1 -short ./...is green — no regressionin any other package.
go build ./...andgo build -tags sqliteonly ./...both pass.
go vet ./...clean.gofmtclean on every filetouched by this PR.
webjob).pnpm install --frozen-lockfileresolves cleanly,pnpm lintreportszero errors,
tsc -b && vite buildtransforms 6,077 modules withouterrors.
Manual
End-to-end validation against a real Max bot in a local goclaw stack:
Max),
POST /api/channel-instanceswithchannel_type=max.Channel reaches healthy state after probing
GET /me.dm_policy=open) → agent loop→ streaming placeholder edits with throttled markdown finalisation
→ final reply delivered with citations from
web_fetchtool.dm_policy=pairingfrom an unpaired user → pairing-codereply sent via Max API; agent loop is not triggered (verified
the security gate works).
does not yet permit adding bots to chats. Group policy logic is
unit-tested.
exercised against the production Max API yet. Polling is the
recommended primary mode and the only one validated end-to-end.
Why this design
Max (https://max.ru) is a messaging platform popular
in Russia (>140M MAU), with a documented Bot API at
platform-api.max.ru. There is no widely-adopted Go SDK for it, so thechannel uses a small
net/httpclient directly. The API surface in useis deliberately narrow (~10 endpoints), which keeps the client simple
enough to maintain in-tree without a third-party dependency — same
reasoning by which goclaw uses stdlib + a thin client for several
existing channels.
The package mirrors existing channels' conventions:
BaseChannelfor shared allowlist/pairing/health logicinstanceLoader.RegisterFactoryMessageBus; consumes outbound fromMessageBusPairingStoreandPendingMessageStoresharedservices
StreamingChannel,WebhookChannel,ReactionChannel,BlockReplyChannel— the same optional interfaces other channelsexpose
Notable choices specific to Max:
config.mode. Splitting them would duplicate the inbound translatorand the policy wiring; existing channels with multiple transport
modes (WhatsApp bridge vs. native, Slack bot vs. user) keep them
in one package.
markdown on final
Send(). Max's markdown parser breaks on partialsyntax mid-stream.
request_contactHMAC verified withhmac.Equalforconstant-time comparison; distinct error types for malformed hex
vs. tampered payload.
channel forms from
configSchemaandcredentialsSchema. AddingMax meant a few schema entries and channel-type list additions
across 5 existing files (~20 LOC total) — no new TSX components,
mirroring how Discord, Facebook, and Pancake are integrated.
What's in this PR
.gofiles ininternal/channels/max/(+README.md, 7testdata/*.json)cmd/gateway.go,internal/channels/channel.go,internal/http/channel_instances.goTypeMaxconstant, validator entry)ui/web/src/(constants/channels.ts,pages/channels/channel-schemas.ts,pages/channels/channels-status-utils.ts,pages/config/sections/bindings-section.tsx,pages/contacts/contacts-page.tsx)docs/05-channels-messaging.md,README.md,CLAUDE.md,CHANGELOG.md.gitignore(+3 lines for local smoke binary)The diff in
cmd/gateway.goshows the 1 new factory-registrationline plus a few lines of
goimportsre-grouping that the formatterapplied automatically when the file was opened. Happy to revert the
re-grouping if you prefer the patch strictly minimal.
Backwards compatibility
feat(channels/max)Conventional-Commits scope follows theper-channel scoping convention.
Follow-ups (separate PRs, after this one merges)
docs(channels): add Max setup guideinnextlevelbuilder/goclaw-docs(modeled onchannels/telegram.md)is available, exercise webhook mode against the live API and
tighten the "known limitations" wording
docs/05-channels-messaging.md § 4Reviewer suggested order
If you'd like to slice the review:
internal/channels/channel.go(1 line) — confirm constant placementcmd/gateway.go(1 substantive line — theRegisterFactorycall) — confirm wiring matches the pattern
internal/channels/max/factory.go— config + creds shapeinternal/channels/max/max.go— lifecycle and embeddedBaseChannelinternal/channels/max/inbound.go+policy.go— request flow,policy enforcement
internal/channels/max/outbound.go+stream.go+format.go— response flow + chunking
internal/channels/max/webhook.go+auth.go— alt transportand HMAC
*_test.go, especiallyintegration_test.goui/web/src/pages/channels/channel-schemas.ts—credentialsSchema.maxand
configSchema.maxmirrorfactory.go'sInstanceConfigdocs/05-channels-messaging.mdMax section + extendedinterfaces table
Happy to squash the commit history or drop the webhook mode if you'd
prefer a narrower scope.
Origin
The 20 commits split into 15 code (
feat/fix/test/chore)and 5 docs. Branch is linear, no merge commits. Branch is based
on the public
v3.11.2tag — every commit is reachable from it. Thework was developed on a separate fork
(HumanGoClaude/goclaw_max_channel)
to allow iterative testing before upstreaming; this branch
(
feat/max-channel) is the version targetingdev.