Files
passepartout/org/channel-discord.org
Amr Gharbeia 8fd56dece3 v0.8.2: cleanup + prose + structure + decomposition + budget + errors
Phase 1 — dedup + hardening (~9 items):
- Remove duplicate *skill-registry* defvar from core-skills
- Merge *backend-registry* into *probabilistic-backends*, delete backend-register
- Remove inject-stimulus alias, standardize on stimulus-inject
- Add pre-eval sandbox (skill-source-scan) blocks restricted symbols before eval
- Remove dead plist-get function; remove duplicate json-alist-to-plist export
- Fix read-framed-message whitespace DoS (4096-iteration max)
- Add *read-eval* nil to dispatcher-approvals-process read-from-string (RCE)
- Add test-op to ASDF; update .asd version 0.4.3→0.7.2

Phase 2 — prose + contracts + reorder:
- Split ROADMAP: 2623→1089 lines (TODO only), CHANGELOG: 260→1528 lines (full DONE history, 14 versions reverse chron)
- Add Contracts + Overview to 6 channel files + embedding-native + programming-standards + symbolic-scope
- Reorder 28 .org files: Contract → Test Suite → Implementation (TDD order)
- Add 7-phase inline prose to think() in core-reason
- Expand USER_MANUAL: 183→461 lines (10 new sections)

Phase 3 — decomposition + export organization:
- Decompose think() into think-assemble-prompt, think-call-llm, think-parse-response orchestrator
- Organize 188 exports into 16 grouped sections by module

Phase 4 — budget enforcement + error protocol:
- Per-session budget enforcement (SESSION_BUDGET_USD env var, budget-exhausted-p, guard in think-call-llm)
- Error condition hierarchy (6 conditions: pipeline-error, llm-error, gate-error, budget-error, protocol-error)
- Restarts in loop-process: skip-signal, use-fallback, abort-pipeline
2026-05-13 09:17:48 -04:00

4.3 KiB

Channel Discord (channel-discord.org)

Channel Discord

Extracted from gateway-messaging in v0.5.0. Isolated platform — Discord-specific poll and send logic.

Overview

The Discord channel provides bidirectional communication via the Discord REST API and Gateway WebSocket. Messages received from Discord channels are injected into the cognitive pipeline as :user-input signals with :source :discord. Outbound messages route through the actuator registry when the pipeline targets :discord.

The channel uses two functions: discord-poll (inbound sensor, REST polling) and discord-send (outbound actuator, REST POST). Both retrieve the bot token from the credentials vault (vault-get-secret :discord). HITL commands are intercepted before injection so approval flows work identically across all channels.

Contract

  1. (discord-get-token): returns the Discord bot token from the vault (via vault-get-secret :discord), or nil if not configured.
  2. (discord-poll): polls configured channels via GET /channels/{id}/messages, injects each non-bot message as a :user-input stimulus with :source :discord. Handles JSON parse failures and API errors gracefully. HITL commands are intercepted before injection.
  3. (discord-send action context): sends a message via POST /channels/{id}/messages. Extracts :channel-id and :text from the action plist. Uses bot token authentication. Logs send failures without crashing the pipeline.

Implementation

(in-package :passepartout)
(defun discord-get-token ()
  (vault-get-secret :discord))

(defun discord-send (action context)
  "Sends a message via Discord REST API."
  (declare (ignore context))
  (let* ((payload (getf action :payload))
         (meta (getf action :meta))
         (channel-id (or (getf meta :channel-id) (getf payload :chat-id)))
         (text (or (getf payload :text) (getf action :text)))
         (token (discord-get-token)))
    (when (and token channel-id text)
      (handler-case
          (dex:post (format nil "https://discord.com/api/v10/channels/~a/messages" channel-id)
                    :headers '(("Authorization" . ,(format nil "Bot ~a" token))
                               ("Content-Type" . "application/json"))
                    :content (cl-json:encode-json-to-string
                              `((content . ,text))))
        (error (c) (log-message "DISCORD ERROR: ~a" c))))))

(defun discord-poll ()
  "Polls Discord via HTTP GET /channels/{id}/messages. In production,
a WebSocket connection to the Gateway is preferred for real-time events."
  (let* ((token (discord-get-token)))
    (when token
      (handler-case
          (dolist (channel '("channel-id-here"))  ;; configured channel IDs
            (let* ((last-id (getf (gethash "discord" *gateway-configs*) :last-update-id 0))
                   (url (format nil "https://discord.com/api/v10/channels/~a/messages?after=~a"
                               channel last-id))
                   (response (dex:get url :headers
                                       `(("Authorization" . ,(format nil "Bot ~a" token))))))
              (let ((messages (ignore-errors
                               (cdr (assoc :message
                                      (cl-json:decode-json-from-string response))))))
                (dolist (msg (and (listp messages) messages))
                  (let* ((id (cdr (assoc :id msg)))
                         (content (cdr (assoc :content msg)))
                         (author (cdr (assoc :author msg)))
                         (author-id (cdr (assoc :id author)))
                         (is-bot (cdr (assoc :bot author))))
                    (when (and id content (not is-bot))
                      (setf (getf (gethash "discord" *gateway-configs*) :last-update-id) id)
                      (unless (ignore-errors (hitl-handle-message content :discord))
                        (stimulus-inject
                         (list :type :EVENT
                               :meta (list :source :discord :chat-id channel)
                               :payload (list :sensor :user-input :text content))))))))))
        (error (c) (log-message "DISCORD POLL ERROR: ~a" c))))))

#+end_src