Skip to content

refactor(chat): marshal via the framework serializer, not hand-rolled JSON#5

Open
nickmeinhold wants to merge 2 commits into
geekscape:mainfrom
nickmeinhold:use-framework-serializer
Open

refactor(chat): marshal via the framework serializer, not hand-rolled JSON#5
nickmeinhold wants to merge 2 commits into
geekscape:mainfrom
nickmeinhold:use-framework-serializer

Conversation

@nickmeinhold

Copy link
Copy Markdown
Collaborator

What & why

Andy's note on #4 named Aiko's core principle: developers design at the level of function calls and let the framework insert serialization + transport — they shouldn't hand-write the wire format. chat.py was doing exactly that with json.dumps / json.loads.

This swaps the hand-rolled marshalling for the framework's pluggable serializer (aiko_services.main.utilities.generate / parse) — the same machinery the discovery proxy already uses for remote calls.

Changes

  • generate_payload()generate("message", {username, channel, timestamp, message}). Emits an S-expression today; swappable to JSON/AVRO without touching this code.
  • format_incoming() → decodes via parse(), falling back to legacy JSON then a bare string, so older publishers keep working.

Verified (round-trip + both fallbacks)

  • S-expr (message username: nick … message: 11:hello world)nick: hello world
  • legacy JSON {"username":"deanna","message":"hi"}deanna: hi
  • bare string just textjust text

⚠️ Wire-format change — needs a follow-up

Published messages move from JSON to S-expressions. Reads here are backward-compatible, but external consumers that parse the JSON directly — notably the aiko-bridge — need a matching parse() update. Flagging for sequencing; happy to land the bridge change alongside.

Addresses Andy's #4 comment / the "let the framework do the marshalling" migration.

🤖 Generated with Claude Code

Per Aiko's design principle (and Andy's note on geekscape#4): developers should
express function calls and let the framework insert serialization +
transport, rather than hand-writing the wire format. This swaps
json.dumps/json.loads in chat.py for aiko_services' pluggable serializer.

- generate_payload() now emits an Aiko function call via
  generate("message", {username, channel, timestamp, message}) -- an
  S-expression today, swappable to JSON/AVRO without touching this code.
- format_incoming() decodes via parse(), falling back to legacy JSON and
  then a bare string, so older publishers keep working.

NOTE (wire-format change): published messages move from JSON to
S-expressions. Reads stay backward-compatible, but external consumers that
parse the JSON directly -- notably the aiko-bridge -- need a matching
parse() update as a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nickmeinhold nickmeinhold marked this pull request as ready for review June 8, 2026 00:25
Three findings the adversarial reviewers (Maxwell/Kelvin/Carnot) agreed on:
- Narrow `except Exception` to (ValueError, IndexError, TypeError) so real
  bugs surface instead of silently degrading to the JSON/raw fallback.
- Hoist the magic command string "message" to a module constant
  _MESSAGE_COMMAND, used by both encode and decode paths.
- Require the "message" field in the S-expr branch (mirroring the JSON
  branch) so a malformed (message username: nick) falls through to raw
  instead of rendering an empty "nick: " (Carnot's catch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nickmeinhold

Copy link
Copy Markdown
Collaborator Author

Cage-match + companion bridge PR

Run through adversarial review — Maxwell (Claude), Kelvin (Gemini 2.5 Pro), Carnot (Codex). Verdicts: 2× REQUEST_CHANGES + 1× COMMENT. Consensus findings, all fixed in ed1608d:

  • Narrowed except Exception(ValueError, IndexError, TypeError), so a real parser bug surfaces instead of silently degrading to the JSON/raw fallback.
  • Hoisted the magic command string "message" to a module constant _MESSAGE_COMMAND (used by both encode + decode).
  • Required the message field in the S-expr branch (Carnot's catch) so a malformed (message username: nick) falls through to raw instead of rendering an empty nick: .

Wire-format follow-up — resolved: the consumer update for the aiko-bridge is up as nickmeinhold/aiko-bridge#1. It reads S-expr → legacy JSON → bare string, so it works against both old and new servers (no flag-day). The two land together.

@nickmeinhold

Copy link
Copy Markdown
Collaborator Author

Noting a follow-up from the protocol version bump (dea848a, :0 -> :1) for when this lands:

The aiko-bridge discovers the ChatServer via get_server_service_filter(), which pins the protocol version (_PROTOCOL_SERVER = .../chat_server:{_VERSION}, currently _VERSION = 0). Once the deployed server moves to :1, the bridge needs to track that. And since the bridge genuinely depends on the new functionality (it uses username to mint per-user Matrix ghosts), per your discover-by-version guidance it should arguably require :1 rather than accept either, so it never silently attaches to a :0 server that can't carry sender identity.

Tracking it bridge-side; not a change to this PR. Flagging here since it's the same wire-format thread. (Same direction as this PR, incidentally: the robot half of the bridge just moved off hand-rolled marshaling onto the framework's remote-call proxy, so commands are function calls and the framework handles serialization.)

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.

3 participants