8 Commits

Author SHA1 Message Date
7911cc6320 feat(presence): F-5b presence-sync — mirror sm.Machine into Fabric
Adds an internal/presence package that ticks every 30s (configurable
via presence_interval_seconds), reads each bound agent's sm.Machine
state through host.ReadAgentState, maps to Fabric's 6-status enum,
and PUTs diffs to /api/agents/:userId/presence on every guild the
agent belongs to.

Semantic mapping (the part flagged "needed" in the prior README):
  idle    → idle
  working → on_call
  busy    → busy
  offline → offline
exhausted/unknown reserved for backend-side fallbacks; we don't push.

Tick is mutex-guarded (avoids the upsert race the openclaw incident
called out in agent-presence.service.ts) and diff-gated so writes are
sparse. Token-cache invalidation on PUT failure handles guild JWT
rotation.

fabric.Client gains SetAgentPresence helper. README marks F-5b .
2026-06-01 08:40:17 +01:00
76a3bfbedb feat(inbound): emit media in notification instead of appending footer
Channel plugins now have a structured way to propagate attachments —
Plexum's host turns each MediaItem into a canonical.ImageBlock content
on the user message. This replaces the F-6 workaround that crammed
local file paths into a markdown footer at the end of the message
text.

internal/inbound/inbound.go:
- Notifier signature gains `media []MediaItem`; MediaItem mirrors
  the host's channel.MediaRef (Path/MediaType/Name)
- dispatch downloads attachments (unchanged) then forwards results
  as MediaItem to Notify — no more footer appending
- dispatch now also receives guildEndpoint so it can resolve Fabric's
  RELATIVE attachment URLs (`/api/files/<id>`) against the guild base.
  Previously the downloader received the relative path verbatim and
  failed every fetch silently.

cmd/plexum-fabric-channel-plugin/main.go:
- notifier closure pushes media[] in the EmitNotification payload

Live verified: alice uploads blue 32x32 PNG → Fabric guild → plugin
downloads to /tmp/plexum-fabric/<msg>/blue32.png → emits inbound with
media → host routes to kimi agent → Kimi: "Blue".

(MiniMax M2.7 is text-only per openclaw model definitions, so the
same flow against MiniMax returns "I don't see an image" — that's a
model capability limit, not a plugin issue.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 21:03:24 +01:00
4674c5a2b7 fix(socketio): remove broken client-initiated ping (engine.io v4)
The socket.io client was sending its own EIO ping frames every
pingInterval (default 25s). That's wrong for engine.io v4: in v4 the
SERVER initiates pings and the CLIENT must respond with pong inside
pingTimeout, else the server closes the connection. Client-initiated
pings get misinterpreted by Fabric's NestJS socket.io backend, which
quietly closes the connection — producing the warn-flap every ~25s:

  inbound: socket ended; reconnecting
    err="read: failed to get reader: received close frame:
         status = StatusNoStatusRcvd and reason = \"\""

Fix:
- delete pingLoop() entirely
- delete the pingPeriod/pingTimeout struct fields + their assignments
  in recvOpen (server enforces both anyway; client doesn't need them)
- keep the eioPing case in handlePacket (already correct — responds
  with pong)
- drop the now-unused "time" import

End-to-end verified on live Fabric:
- Restarted Plexum at 20:17:35; watched for 90+ seconds
- ZERO "socket ended" events (vs. ~3-4 per 90s before the fix)
- Channel inbound still delivers: alice posted seq=20 → gem agent
  (gemini CLI) replied seq=21 "pong"

The plugin no longer flaps. Reconnect backoff machinery (1s→60s)
stays in place as a safety net for genuine network drops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:19:58 +01:00
6def33161b feat: Phase F-6 — attachments + final docs (F-8 = no-op)
F-6 attachments:
- internal/attachments/ (~145 LOC + 8 tests): per-message Downloader
  that fetches Fabric attachment URLs into $TMPDIR/plexum-fabric/<msg-id>/
  with the agent's guild token, sanitizing filenames + message ids
  against path-traversal
- AppendFooter renders downloaded paths as a markdown footer
  ("Attachments:\n  - /tmp/... (mime, N bytes)\n")
- Agents access via the exec MCP tool (cat / file / etc.)

internal/inbound.Supervisor:
- new Attachments *attachments.Downloader field (nil → skip with warn)
- inbound.dispatch: when message has attachments, blocking-download +
  AppendFooter before emitting notification (so agent's first turn
  sees the paths)

cmd/plexum-fabric-channel-plugin:
- sup.Attachments = attachments.New("") wired at init

F-8 coalesce: no-op. openclaw plugin's coalesce buffered the
text→thinking→tool→text segments openclaw emits across multiple
deliver() calls per turn. Plexum's loop.Run returns ONE final assistant
text per turn (via extractFinalText), so coalescing isn't a concern.
The channel outbound posts a single message naturally.

F-5b presence-sync: deferred. The openclaw plugin pushes HarborForge
on-call status to Fabric's per-recipient presence so the backend can
busy-discard 'announce' deliveries. Plexum's state machine has
different semantics (idle/working/busy/offline) and is per-Plexum-agent
not per-user; mapping requires more design.

README updated with the phase status table + 16-tool list.

Tests: 8 new in internal/attachments (35 total in this repo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:42:23 +01:00
ed3676ebc8 feat: Phase F-5+F-7 — command-sync + sub-discussion + channel-create tools
Bundles the remaining "agent does things in Fabric" surface, skipping
presence-sync (Plexum's state machine isn't 1:1 with HF status semantics)
and attachments (Plexum has no media pipeline yet) — both deferred.

internal/subdisc/ (~140 LOC + 5 tests): persistent KV at
<profile>/state/plugin-kv/fabric-sub-discussions.json
- Entry: SubChannelID, HostAgentID, HostUserID, GuestUserIDs,
  HostGuide, GuestGuide, CallbackGuildNodeID, CallbackChannelID, CreatedAt
- Open / Lookup / LookupForHost (host-enforcement) / Add / Remove / All
- atomic tmp+rename persistence; corrupt file = start empty

internal/tools/tools.go — 7 new tools:
- create-chat-channel       (xType=general)
- create-work-channel       (xType=work)
- create-report-channel     (xType=report)
- create-discussion-channel (xType=discuss)
- discussion-complete       (post summary + close channel)
- create-sub-discussion     (create + invite + KV store + greeting)
- close-sub-discussion      (host-only; posts callback to parent + closes)

internal/fabric/client.go: CreateChannel / CloseChannel /
JoinChannel / LeaveChannel / SetChannelPurpose / Canvas CRUD /
SyncCommands (added in earlier F-4 commit; reused here)

cmd/plexum-fabric-channel-plugin/main.go:
- subdisc.Store opened from DefaultPath at init; passed into tools.Deps
- HostConfig adds commands_sync_key + sync_commands (defaults
  ["new","stop"]) — F-5 command-sync to every (agent,guild) pair at
  init when key is set; silently skips when omitted
- syncCommandsToGuilds: best-effort PUT per guild, logs per-guild
  outcome

scripts/install.sh: manifest tools[] expanded to 16 entries
(9 from F-4 + 7 new). create-channel variants share a schema shape.

Live verified:
  $ plexum plugin-call create-chat-channel \
      '{"agent_id":"fabrictester","guild_node_id":"test-guild2",
        "name":"plexum-f7-smoke","is_public":true}'
    → "created general channel plexum-f7-smoke (id=6315e636-...)"
  $ plexum plugin-call fabric-channel-list ...
    → 3 channels listed including the new one

F-6 (attachments) + F-5b (presence-sync) + F-8 (coalesce) deliberately
deferred — see DEV-NOTES.

Tests: 5 new in internal/subdisc (27 total in this repo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:40:09 +01:00
d6bea46d00 feat: Phase F-3 + F-4 — exp backoff + agent tool surface (batch 1)
F-3 refinements:
- internal/inbound: replace fixed 3s reconnect wait with exponential
  backoff (1s → 60s, ×2, reset when prior session lasted >30s); proxy
  for "healthy" vs "flapping" and avoids hot reconnect loops when the
  server is sick

F-4 agent tool surface (port of openclaw plugin's tools.ts):
- internal/tools/tools.go (~370 LOC): Registry binds Deps {Client,
  Tokens, Identities} and exposes 8 agent-facing tools:
    fabric-send-message     post a normal message to any channel
    fabric-send-sys-msg     post a kind=sys message (bypasses turn engine)
    fabric-channel-list     list channels visible in a guild
    fabric-guild-list       list guilds the agent is in
    fabric-message-history  paginate channel messages by seq
    fabric-channel-set-purpose  PATCH the channel's purpose
    fabric-channel          fetch metadata + members for one channel
    fabric-canvas           get/share/update/remove channel canvas
- internal/tools/contracts.go: static ToolContract list — kept in sync
  with install.sh's manifest emitter
- Every agent-scoped tool requires agent_id in input args (Plexum SDK
  doesn't propagate calling agent id through CallTool today)
- guild_node_id defaults to agent's first guild for fabric-send-message

internal/fabric/client.go: new REST methods needed by tools —
PostSystemMessage, CreateChannel, CloseChannel, JoinChannel,
LeaveChannel, SetChannelPurpose, GetCanvas, ShareCanvas, UpdateCanvas,
RemoveCanvas, SyncCommands.

cmd/plexum-fabric-channel-plugin/main.go:
- Manifest declares the tool surface via tools.New(...).Contracts()
- CallTool dispatches "send" to handleSend (outbound for channel
  manager), everything else to tools.Registry.Handler(name)

scripts/install.sh:
- Manifest tools[] now lists all 9 tools with schemas — matches what
  internal/tools/contracts.go advertises

Live verified against running Fabric stack:
  $ plexum plugin-call fabric-guild-list '{"agent_id":"fabrictester"}'
    → "guilds for agent fabrictester (1): test-guild2 @ http://localhost:7003"
  $ plexum plugin-call fabric-channel-list '{...,"guild_node_id":"test-guild2"}'
    → 2 channels listed
  $ plexum plugin-call fabric-message-history '{...,"limit":5}'
    → 5 messages with timestamps + authors

F-5+ deferred:
- create-{chat,work,report,discussion}-channel (batch 2)
- sub-discussion family (state store + 3 tools)
- presence-sync + command-sync
- attachments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:35:39 +01:00
0efcdfd342 feat: Phase F-2 — socket.io inbound + wakeup gate + token refresh
End-to-end Fabric inbound→Plexum→Fabric outbound now works against a
live Fabric stack:

  alice posts in bt2-clean (Fabric REST)
    → guild emits message.created over socket.io
    → plugin's wakeup gate decides dispatch
    → notifications/plexum/channel/inbound to host
    → Plexum agent runs (echo provider)
    → outbound `send` tool posts via Fabric REST
    → fabrictester reply visible in channel

internal/socketio/ (~280 LOC + 2 tests):
- Minimal Engine.IO v4 + Socket.IO v5 client over websocket
- WebSocket-only transport (skip polling upgrade dance)
- AuthFunc callback re-evaluated on every (re)connect — fixes the
  stale-JWT-on-reconnect bug openclaw plugin documented for the JS
  client's single-shot auth, which the available Go socket.io
  library (zishang520) doesn't address either
- PING/PONG per server-supplied interval
- Caller-driven reconnect: Connect returns on close, supervisor
  re-dials with fresh token

internal/tokens/ (~95 LOC + 9 tests):
- Per-agent session cache with 8min TTL (matches openclaw's
  TOKEN_TTL_MS); guild tokens are ~15min so 8min keeps a margin
- Invalidate forces re-login (used by inbound when CONNECT auth fires)
- GuildToken helper picks the per-guild JWT from the cached session;
  if the guild is missing from the cache, invalidate + retry once

internal/inbound/ (~290 LOC):
- Supervisor: one socket.io conn per (agent, guild); reconnect with
  fresh token on drop; ChannelSyncInterval (60s) polling + push
  channel.joined/channel.left handlers
- Wakeup gate: dm channels deliver any non-self message; other
  x_types require wakeup=true (record-only for non-wake non-dm
  deferred — Plexum has no history-injection equivalent in v1)
- Self-author filter on selfUserId from cached session
- Per-(agent,msgId) dedup bounded to 5000 entries
- Per-channel serial queue with 5s idle drain so concurrent inbounds
  on the same channel run one-at-a-time (matches openclaw plugin)
- Emits notifications/plexum/channel/inbound with session_id =
  "s_fab_<fabric_channel_id>" for stable per-channel session continuity

cmd/plexum-fabric-channel-plugin:
- Wires inbound supervisor at Init; runs in a background goroutine
  for the plugin's lifetime
- Replaces F-1's sessions map with tokens.Cache (same warm-sessions
  behavior, now backed by TTL)
- hostLogHandler: bridges slog records from inbound supervisor to
  HostAPI.Log notifications

F-2 deferred to F-3+:
- record-only history injection (Plexum v1 has no equivalent)
- tools.ts port (15 MCP tools — channel/canvas/sub-discussion family)
- presence-sync, command-sync, attachments, coalesce parity

Tests: 22 (5 identity + 6 config + 9 tokens + 2 socketio).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:29:01 +01:00
f8d43ae70e feat: Phase F-1 — Plexum-fabric-channel-plugin foundation
Ports the foundation of Fabric.OpenclawPlugin to a native Plexum
channel plugin (Go). F-2+ phases (socket.io inbound, wakeup gate,
tools, presence, etc.) follow.

Layout:
  internal/identity/      — fabric-identity.json registry (agent → API key)
  internal/fabric/        — REST client (Center auth + Guild messaging)
  internal/config/        — channels/<name>.json fabric extension parser
  cmd/plexum-fabric-register/      — agent registration CLI
  cmd/plexum-fabric-channel-plugin/— Plexum SDK plugin entry
  scripts/install.sh      — build + install + manifest generator

Plugin behavior (F-1):
- Reads <profile>/channels/*.json, filters plugin=plexum-fabric-channel,
  builds (plexum-channel-name → fabric channel-id) index
- Validates each bound agent's API key against Center at init
  (warmSessions); logs warning but doesn't refuse init on bad keys
- `send` MCP tool: POST plain text to the bound Fabric channel as the
  agent user; selects guild endpoint+token from cached session
- Manifest channels[] is generated by install.sh from current
  channels/*.json — re-run with --reset-manifest after adding bindings
- Plugin-private config at
  <profile>/plugins/plexum-fabric-channel/config.json
  (center_api_base, default http://localhost:7001/api)

Live smoke verified:
- plexum-fabric-register against running Fabric Center (port 7001):
  validated fak_..., wrote identity file with user_id + email captured

Tests: identity (5) + config (6) = 11 unit tests.

F-2 will hook socket.io for inbound + wakeup gating + token refresh.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:13:34 +01:00