Skip to content

Human channels ​

VeloxSaarthi talks to its human operator through a channel — the place where the factory posts story updates, asks clarification questions, requests plan approvals, and accepts operator commands. The channel is pluggable: you pick one in config, and the rest of the system is unaware of which messenger is behind it.

Selecting a channel ​

yaml
adapters:
  human_channel: "telegram" # "telegram" | "teams" | "web"

Each channel reads its own config block (telegram:, teams:) — see Configure. The daemon instantiates exactly one channel at startup.

The common interface ​

Every channel implements a single contract, HumanChannelPort (src/core/ports/human-channel.ts). Nothing in the orchestrator, state machine, or notifier knows about Telegram or Teams — they only call this port:

MethodPurpose
ensureStoryThread(story)Create/return the per-story thread, return a ThreadRef
postUpdate(thread, update)Post a stage/lifecycle status message
postQuestion(thread, q)Ask a clarification question (optionally with options)
postApprovalRequest(thread, req)Post a non-blocking plan-approval request
requestApproval(thread, req)Blocking approval (sensitive-tool permission path)
editUpdate?(thread, ref, update)Edit a previously posted message in place
postMedia?(thread, media)Attach a photo/video/document
registerApprovalWaiter?(cid)Re-attach a blocking waiter after a restart

Inbound events (operator replies, approvals, slash commands) are delivered back to the daemon through a small mutable deps object per channel (telegramDeps, teamsDeps), which the daemon late-binds with DB-backed callbacks at startup.

Capability differences between channels ​

The port is identical, but the underlying messengers are not. Where a messenger cannot do something the way Telegram does, the adapter degrades gracefully — this table is the source of truth for what to expect on each channel.

CapabilityTelegramMicrosoft TeamsWeb (local)
Per-story thread✅ forum topic✅ channel root message✅ dashboard panel
Status updates✅✅✅
In-place message edit✅⚠️ best-effort (Graph PATCH often 403 → posts a follow-up reply)✅
Clarification question✅✅✅
Option picker✅ tap an inline button⚠️ type the option number/text (Adaptive Card buttons are visual only)✅ click
Plan approval✅ Approve/Reject buttons⚠️ type "approve" / "reject"✅ click
Approval artifacts (spec/plan)✅ uploaded as files⚠️ linked as text (no ad-hoc upload without SharePoint)✅
Media (photo/video/doc)✅❌ not supported (Graph limitation)✅
Slash commands✅ /respin /requeue /cancel /history /restart /vetomerge⚠️ same, typed as text (no autocomplete registration)n/a
Inbound transportlong-poll (getUpdates)interval poll (default 5 s)in-process
Decision latency~instantup to poll_interval_ms (default 5 s)instant
Authorizationnumeric user-id allowlistAAD object-id allowlistlocal-only

Legend: ✅ full parity · ⚠️ available but works differently · ❌ unavailable.

Why Teams differs ​

These are inherent to the Microsoft Graph API, not gaps in the adapter:

  • No long-poll. Graph has no getUpdates equivalent, so the Teams adapter polls each story thread on an interval. A webhook (Bot Framework) build would remove the latency but requires a public HTTPS endpoint — a deployment cost.
  • No inline-button callbacks. Without a Bot Framework endpoint, button taps produce no event. Approvals and option answers are therefore made by typing in the thread; an Adaptive Card is shown for affordance only.
  • Edits are best-effort. Patching a message you don't own typically returns 403 in app context, so the adapter falls back to posting a follow-up reply.

Adding a new channel (e.g. Slack) ​

The seam is small. To add a channel:

  1. Implement HumanChannelPort in src/adapters/<channel>/channel.ts (outbound posting + an inbound poller/handler that calls the deps callbacks).
  2. Export a <channel>Deps holder (mirror src/adapters/telegram/index.ts).
  3. Register a factory in src/bootstrap/adapter-registry.ts keyed by the config string.
  4. Add the channel's config block to the zod schema in src/config/schema.ts.
  5. Wire the daemon deps for the channel (see src/daemon/*-commands.ts).
  6. Add a column to the table above documenting any capability differences.

Because the orchestrator only ever sees HumanChannelPort, no other code changes when a new channel is added — selection is a single config string.

Internal Veloxcore tool — not a public product.