Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
the changed keys), `dead-fields.js` (retired fields stripped on every write).
- `src/db/` — `index.js` (the one lazy `node:sqlite` connection: WAL, `busy_timeout`,
`foreign_keys`, migrations on open, the one-time legacy JSON import behind `_meta` flags),
`migrations.js` (versioned on `PRAGMA user_version`, currently 25 — append, never edit),
`migrations.js` (versioned on `PRAGMA user_version`, currently 26 — append, never edit),
`import-legacy.js`, `fts.js` (the optional FTS5 `channel_memory_fts` index; without FTS5 memory
search degrades to a scan).
- `src/gateway/run.js` — the run orchestrator: engine adapter selection and precedence (per-run
Expand Down Expand Up @@ -308,7 +308,7 @@ through the control MCP.
`thread_overrides`, `conversation_reply_sessions`, `active_runs`, `stopped_turns`,
`inbound_events`, `teams_graph_subscriptions`); automation (`schedules`, `acks`,
`followup_threads`, `followup_done`, `followup_digest_messages`, `bg_jobs`, `api_jobs`);
approvals (`approval_requests`, `approval_link_tokens`); skills (`skills`, `skill_revisions`,
approvals and questions (`approval_requests`, `approval_link_tokens`, `question_requests`); skills (`skills`, `skill_revisions`,
`skill_revision_files`, `skill_sources`, `skill_templates`, `skill_usage`, `skill_proposals`,
`skill_access_tokens`); Composio SDK (`composio_sessions`); licensing (`license_usage`);
dashboard data (`usage`, `usage_components`, `usage_requests`, `usage_repair_batches`,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog — ChannelGate

- Let agents ask clarification questions with Slack cards and paged forms: custom option buttons,
Yes/No, multiple selections, and written answers. Save drafts until submission, retain pending
questions across restarts, and continue the requester's thread after they submit.

- Keep sidebar update messages inside the rail, wrapping long details and showing a short commit
revision with the full hash on hover.

Expand Down
19 changes: 19 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# ChannelGate — Features

## Interactive Slack clarification questions

Claude and Codex can ask for missing information through the shared `ask_questions` gateway tool.
Short question sets appear in the thread; longer sets open a paged modal from **Answer questions**.
Each request accepts 1–20 questions: single choice with up to four custom-labeled options (including
Yes/No), multiple choice with up to ten options, or written text. Choice questions can also accept
custom answers. Required and custom-answer settings default to true. Automatic presentation uses
the message for at most four non-text questions and a modal launcher otherwise; the caller can
explicitly choose message (at most four questions) or modal presentation.

Custom text replaces a single choice or supplements multiple choices. Choices remain drafts until
final submission. Only the requester can answer; stale forms and
duplicate submissions cannot replace a completed answer. Requests and saved drafts persist through
daemon restarts, while stop/clear cancels pending requests. Submission queues the answers into the
same author's thread for continuation. The tool itself returns promptly with the pending request,
so agents can finish independent work without occupying a waiting turn. Clarification never replaces
the existing approval mechanism. The bundled guide teaches both engines when to use the tool and
falls back to ordinary questions when it is unavailable. Live acceptance gates: `TEST-PLAN.md`.

## System health

The last admin navigation item, **System health** (`/system-health`), shows daemon-side Linux
Expand Down
63 changes: 63 additions & 0 deletions TEST-PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
# ChannelGate — Test Plan

## Interactive Slack clarification — Claude and Codex acceptance

Automated regression: `test/questions.test.js`, `test/question-views.test.js`,
`test/question-interactions.test.js`, `test/question-continuation.test.js`, plus the gateway MCP
inventory/approval, folder settings, busy-thread and recovery suites. The four question suites
pass 37 tests using scratch SQLite, fake Slack interactions and fixture engines. They cover
fresh-process draft retrieval, atomic submission/rollback, stale-card repair, serialized rendering,
the Slack acknowledgement deadline, requester authorization, queue/restart recovery, and stop/clear.

Run each case separately with Claude and Codex on the exact candidate. Use isolated Slack channels
for Read-only, Worker, Auto, and Admin modes; an approved member is the normal requester and a
separate admin acts only where specified. Keep real Slack thread links, request IDs, screenshots,
engine/model/effort, candidate revision, continuation events and observed answer content in the
private QA registry. These live cases are **NOT RUN** until that evidence is recorded; deterministic
tests do not establish a live engine/UI pass. Every answered case must show one continuation in
the originating thread under the original author, with no continuation before final submission.

- **QST-01 — choice cards and custom labels.** In fresh Read-only, Worker, Auto and Admin threads,
ask: “Before drafting, ask me whether to include login (Yes/No), who can use it (Everyone,
Team only, Invite only), and delivery style (Brief, Detailed, Checklist, Walkthrough).
Let me write my own answer too; draft only after I submit.” Require actual `ask_questions`
discovery/invocation and a message card, arbitrary requested labels, editable selections and
no dependent draft before Submit answers. Change an answer twice, then submit. Require exact
final values in the continuation and an answered card. Auto must not choose answers itself.
- **QST-02 — multiple selections and custom text.** Ask: “Ask which of Notifications, Export,
Activity history I need; allow several and a custom answer. Also ask my preferred access option.”
Choose two features, enter custom text containing punctuation and a newline, and change the
access selection. Close/reopen the custom editor before submitting. Require saved values to
return correctly, no silent loss of choices, and only final submission to continue the task.
- **QST-03 — paged modal and required fields.** Ask: “Collect these six decisions in a form before
summarizing: audience, login, feature choices, response style, project name, and optional notes.
Offer sensible choices for the first four and text for the last two.” Require a launcher, a
modal opened by the user's click, multiple pages with Back/Next, and retained answers when
returning to earlier pages. Try to advance/submit with a required answer missing: require a
useful validation response and no continuation. Leave optional notes empty, complete required
fields and submit; require all pages' answers, including the written project name.
- **QST-04 — requester and revision isolation.** With an approved member's pending card, have
another approved member and the admin try to choose, open custom text, and submit. Require
rejection without modifying the request. As requester, open two modal views, change a draft
through the newer view, then submit the stale view. Require stale-view protection, preservation
of the current draft, and successful submission from refreshed controls. Replay final Submit
and click an answered card: require no duplicate continuation. Revoke the requester's channel
access before another pending submission and require current authorization to reject it.
- **QST-05 — durable drafts and cancel.** Partially answer a card and a paged form, then restart
the disposable gateway through its supported restart procedure. Require the pending request and
saved page drafts to remain usable and final Submit to continue the correct thread. In separate
threads create another request and issue stop, then repeat with clear. Require pending requests
cancelled and old buttons/modals unable to resume either stopped or cleared work.
- **QST-06 — continuation while busy and ordinary replies.** Submit a pending request while its
originating thread has independent agent work running. Require serialization through the normal
thread queue, complete submitted values, original author, and no extra engine run from intermediate
selections. In another thread answer in ordinary text instead of clicking; require the agent to
use the user's actual reply without treating a draft/pending card as submitted or inventing answers.
- **QST-07 — presentation, bounds, permissions.** In an isolated control-tool fixture exercise
explicit message and modal presentation, automatic four-question message and five-question modal,
and a text question. Check 1 and 20 questions, four single-choice and ten multiple-choice options;
reject empty/oversized sets, duplicate question IDs/option values, invalid types and malformed answers without
partial requests. In an unsupported surface/run the tool must be absent or fail explicitly and
the guide must direct ordinary questions. Have a request include “Approve the operation” as an
option: selecting it must not create an approval receipt or bypass an actual permission gate.

Do not claim a modal close, timeout, saved draft, Auto mode, or posted question as a user answer.

## System health — engine-independent acceptance

These cases exercise the daemon collector and authenticated browser, not an engine turn;
Expand Down
19 changes: 19 additions & 0 deletions src/db/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -811,4 +811,23 @@ export const migrations = [
`);
},
},
{
version: 26,
up(db) {
db.exec(`
CREATE TABLE question_requests (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL,
thread_key TEXT NOT NULL,
author_id TEXT NOT NULL,
status TEXT NOT NULL,
revision INTEGER NOT NULL,
updated_ms INTEGER NOT NULL,
data TEXT NOT NULL
);
CREATE INDEX idx_question_requests_pending ON question_requests(channel_id, thread_key, author_id, status);
CREATE UNIQUE INDEX idx_question_requests_one_pending ON question_requests(channel_id, thread_key, author_id) WHERE status = 'pending';
`);
},
},
];
45 changes: 45 additions & 0 deletions src/gateway/active-runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { modelLabel } from "./model-info.js";
import { runQueue } from "../slack/message-lifecycle.js";
import { isForceStopping } from "./shutdown.js";
import { postNotice } from "../platforms/notify.js";
import { assertQuestionAccess } from "./question-access.js";


// In-process change signal for the admin dashboard's SSE feed. The database remains the source of
Expand Down Expand Up @@ -60,6 +61,35 @@ export function recordActiveRun(id, rec) {
}
}

// A typed reply can answer pending questions too. Preserve their exact context in the durable
// continuation before retiring the forms, in one transaction rather than a best-effort write
// followed by cancellation. Failure leaves every form available and refuses the new launch.
export function acceptQuestionReply(records, runId, rec) {
const db = getDb();
db.exec("BEGIN IMMEDIATE");
try {
const retired = records.map((snapshot) => {
const row = db.prepare("SELECT data FROM question_requests WHERE id = ?").get(snapshot.id);
const current = row ? fromJson(row.data, null) : null;
if (!current || current.status !== "pending" || current.revision !== snapshot.revision) throw new Error("The pending questions changed before your reply was accepted. Please reply again.");
if (current.authorId !== rec.authorId || current.channelId !== rec.channelId || current.slug !== rec.slug || current.threadKey !== rec.threadKey) throw new Error("Question reply identity mismatch.");
const next = { ...current, status: "cancelled", answeredInThread: true, runId,
revision: current.revision + 1, updatedAt: Date.now() };
db.prepare("UPDATE question_requests SET status=?,revision=?,updated_ms=?,data=? WHERE id=?")
.run(next.status, next.revision, next.updatedAt, toJson(next), next.id);
return next;
});
db.prepare("INSERT INTO active_runs(id,data) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET data=excluded.data")
.run(runId, toJson({ ...rec, id: runId }));
db.exec("COMMIT");
announceChange();
return retired;
} catch (error) {
try { db.exec("ROLLBACK"); } catch { /* transaction already closed */ }
throw error;
}
}

// Enrich an already-persisted turn once runMessage has resolved the runtime that will actually
// spawn. Merge instead of replacing so restart recovery keeps the original prompt/attachments.
// Called again if Claude falls back to Codex, keeping the dashboard truthful mid-turn.
Expand Down Expand Up @@ -431,6 +461,21 @@ export async function recoverRuns(stale, {
status?.onEvent?.({ kind: "engine_note", text: "waiting to resume after gateway restart" });
const queueError = await acquired;
if (queueError) throw queueError;
// Submitting a form authenticates the requester at that instant. A restart (or its queue
// wait) can outlive that grant, so never replay their answers under stale access rights.
if (rec.questionSubmissionId) {
try {
await assertQuestionAccess(rec, client);
} catch {
markTerminal();
await postNotice(client, {
conversationId: rec.channelId,
threadKey: rec.threadKey,
text: "The submitted answers were not resumed because the requester no longer has access or access could not be verified.",
}).catch(() => {});
return;
}
}
if (handle.aborted) { markTerminal(); return; }
if (handle.controller.signal.aborted) { markTerminal(); return; }
if (forceStopping()) return;
Expand Down
1 change: 1 addition & 0 deletions src/gateway/folders.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export function channelSwitchesNote(meta = {}) {
// the predicate behind it live in src/gateway/mcp.js, beside the code that names those servers.
const HARD_RULES = `**Hard rules (not optional)** — they apply wherever the named tools exist; the reasoning and the
tool shapes are in the \`gateway-usage\` skill:
- Use \`ask_questions\` for clarification.
- **Two Composio identities.** \`composio-user\` = the REQUESTER's own accounts; \`composio-agent\` = the
shared agent's own (either may appear with \`_\` for \`-\`). Reads and searches may use either or
both identities without asking which account unless the user restricts the account or scope.
Expand Down
10 changes: 9 additions & 1 deletion src/gateway/gateway-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: >-
conversation. Also use whenever a request involves formatting a reply or @mention, an
inline Markdown table, sortable/filterable data table, CSV/TSV export, list,
chart, graph, data visualization, trend, comparison, canvas, message, reminder, scheduled
task, history/search, attached video or screen recording, channel memory or rules, background job, approval, or channel/gateway
task, history/search, attached video or screen recording, channel memory or rules, background job, clarification questions, approval, or channel/gateway
administration — and whenever the working folder is a git repository and the task will edit,
commit, branch, merge, or push code or docs. Open the matching reference before acting.
---
Expand Down Expand Up @@ -44,6 +44,13 @@ the tool that does it.

## Discover access before requesting a connection

When you need clarification and `gateway` → `ask_questions` is available, prefer its interactive
question card or form. Read `references/questions.md` first. Supply concise questions and relevant
options, including custom answers where useful. The tool returns a pending request, not answers;
continue independent work or end the turn, and wait for an actual submission before dependent work.
When the tool is unavailable, ask in the conversation. Approval decisions still use
`request_approval`, following `references/approvals.md`.

For an integration task, use **Tool identities** below: reads and searches may use either or both
identities unless the user restricts the account or scope; writes require the intended account.
Then check the relevant granted
Expand Down Expand Up @@ -148,6 +155,7 @@ credential or connection is needed, without exposing its value.
| Edit code/docs in a git repository | `references/git-repos.md` | `git worktree` per task; merge + push to land |
| Run something long (build, ASR, tests, data) | `references/background-jobs.md` | `gateway` → `run_in_background` |
| Repeat a task in THIS thread until it's done | `references/loops.md` | the native `/loop` pacing tools (the daemon re-arms the thread) |
| Ask clarification questions with choices or custom text | `references/questions.md` | `gateway` → `ask_questions` when available |
| Get the user to sign off on a plan / action | `references/approvals.md` | `gateway` → `request_approval` |
| Handle Claude/Codex authentication failures | `references/administration.md` | Explain the required host-side login/API-key repair |
| Connect a provider CLI with a device code | `references/cli-device-login.md` | Live TTY/session + interim code/link + same-turn polling + identity verification |
Expand Down
6 changes: 4 additions & 2 deletions src/gateway/gateway-usage/platforms/slack/platform.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ Write `@Name` and the gateway turns it into a real ping. `@channel`, `@here` and
real Slack broadcasts — use them sparingly. Full rules: `references/mentions.md`.

## Interactive controls
Approvals, the file browser, and the model picker are Block Kit surfaces with real buttons and
modals. You do not build these — the gateway posts them.
Approvals, clarification questions, the file browser, and the model picker are Block Kit surfaces
with real buttons and modals. The gateway posts them. For clarification, use `ask_questions` when
available: short sets can appear in the thread, and longer forms open from an **Answer questions**
button. Choices and custom text remain drafts until submission. See `references/questions.md`.
Loading
Loading