Orchestrata Ultimate › Plans › spec-021-multiplatform-producers
SPEC-021 multi-platform conversation producers — kimi (IRC) + Claude Code
Source: plans/spec-021-multiplatform-producers.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-13 · 89 lines.
Prose pending.
Plan text
SPEC-021 multi-platform conversation producers — kimi (IRC) + Claude Code
User directive 2026-07-13: "Apply the same plugin specs to 1. kimi 2. claude code." The W1a backend (
knowledge_graph/conversation_consumer.py, landed a194d41b) is platform-agnostic by design — message v2 carries aplatformfield, the consumer never whitelists it, andconversation_entity_id/conversation_turn_entity_idtake producer-bare natural ids. Applying SPEC-021 to a new platform therefore means ONE thing: a producer that emits message v2 ontoorchestrata:conversation:queue, plus that platform's native integration surface. The ontology contract (plans/contracts/spec-021-ontology.contract.json) is unchanged.
Shared rules (both producers)
- Message v2 exactly as the contract defines it (
schema:2,project,conversation_id,session_seq,turn_id,turn_seq,role,content,timestamp,platform,tool_call_ids?). Anything else quarantines. - conversation_id scheme generalizes the contract's
opencode-{seq}:{platform_slug}-{session_seq}→claude-{seq},kimi-{seq}. The KG node id becomes{project}:{platform_slug}-{seq}via the existing builder. - turn_seq: producer-side Redis INCR on the contract's counter key
orchestrata:conversation:seq:{project}:{natural_id}(already platform-agnostic — natural_id embeds the platform slug). - session_seq: producer-side Redis INCR on
orchestrata:conversation:session_seq:{project}:{platform_slug}, allocated once per platform session and cached (see per-platform mapping below). - role is only
user|assistant(consumer rejects others). - content truncated to 2000 chars producer-side (consumer re-enforces).
- Fail-open, never block the host platform: a Redis failure logs and drops the turn — a capture hiccup must never break a Claude session or the IRC bus. (The consumer side is the reliable half; the producer is best-effort by design, same as the original plugin.)
- Bounded queue: producers
LPUSHthenLTRIMtoORCHESTRATA_CONVERSATION_QUEUE_MAX(default 10000) so an un-consumed queue (consumer is opt-in viaORCHESTRATA_CONVERSATION_CONSUMER) cannot grow without bound. Drop-oldest is an accepted, documented loss mode. - Kill switch:
ORCHESTRATA_CONVERSATION_CAPTURE=0disables both producers (default ON — producing to a bounded queue is harmless and the consumer stays opt-in). - project slug:
ORCHESTRATA_PROJECT_SLUGenv override, else the repo-directory basename — must match the KGProject.slugthe ingest scripts use for this repo.
1. Claude Code producer (scripts/claude_turn_capture.py)
- Invoked by two ADDITIVE
.claude/settings.jsonhook registrations:UserPromptSubmit→ captures the user turn (promptfield of the hook stdin JSON).Stop→ parsestranscript_path(JSONL), captures assistant messages appended since the last captured uuid.
- Session mapping: Claude's
session_iduuid →session_seqvia a Redis hashorchestrata:conversation:session_map:{project}:claude(HSETNX uuid→INCR'd seq), so every hook invocation in one Claude session lands in the same Conversation node — including across resume/compaction (same uuid, same seq). - turn_id: the transcript message uuid where one exists (deterministic → re-fired hooks MERGE onto the same node, the strongest idempotency available); UUIDv4 for user prompts (UserPromptSubmit carries no uuid).
- Hook exits 0 in <1s in every failure mode; never writes to stdout (stdout on some hook events feeds the model context).
- Assistant
contentis the text blocks of the message only (no tool_use blocks);tool_call_idsleft empty in v1 of this producer (ToolCall linkage is a later enhancement, matching the contract's OPTIONAL edge).
2. kimi/IRC producer (extends scripts/_irc_listen.py + _irc_send.py)
- Conversation unit: one listener connection = one session
(
session_seqINCR'd at connect in_irc_listen.py, written to a Redis key the sender reads:orchestrata:conversation:current:{project}:kimi). - role mapping: PRIVMSG lines in
#agentsFROM the fleet nicks (kimi,codewhale) →assistant; lines WE send via_irc_send.py→user. Other nicks' chatter is not captured (delegation traffic only). - turn_id: UUIDv4 at enqueue (IRC lines have no stable ids); dedupe is not needed because each line is observed exactly once by the process that enqueues it.
- Capture must be non-fatal to the bus: enqueue failures log to the listener's existing log and continue; the watchdog behavior is untouched.
platform:"irc-kimi"for kimi lines,"irc-codewhale"for codewhale,"irc"for our outbound (all under the samekimi-{seq}conversation — the conversation is the delegation channel session, the per-turn platform records the speaker's client).
Verification (both)
End-to-end proof per producer: enqueue real turns → run the LANDED
ConversationQueueConsumer.flush() against real Redis + Neo4j → assert the
Conversation/ConversationTurn nodes and HAS_TURN edges exist with correct
project-qualified ids. No doubles anywhere. IRC tests run against the real
localhost:6667 miniircd with test-id-scoped channels/nicks.