All user prompts funnel through ThreadSessionRuntime.enqueueIncoming in
cli/src/session-handler/thread-session-runtime.ts. This is the single
centralized injection point for any cross-cutting prompt transformation
(command detection, prefix stripping, etc). The 6 sources that funnel here:
- Discord chat messages →
discord-bot.tsMessageCreate →preprocess*Message→enqueueWithPreprocess /new-sessionslash →commands/session.ts→enqueueIncomingdirectly/queueslash → posts Discord message with» **user:** ...prefix → path #1otto send --thread(existing thread) → posts» **otto-cli:** <prompt>→ path #1otto send --channel(new thread) → raw starter message → botThreadCreatehandler →enqueueIncomingwith preprocess callback- Scheduled tasks (
task-runner.ts) → posts Discord messages like #4/#5
Prefix conventions: » **<username>:** is used for queued reposts and
CLI-injected messages in existing threads. New-thread flows (channel-level
otto send and channel scheduled tasks) post the raw prompt without
prefix and rely on an embed marker (ThreadStartMarker YAML) for metadata.
When adding a prompt-level transformation (like leading /command detection):
- Call the transformer inside
enqueueIncoming()for sources that provide a readyprompt. - ALSO call it inside
enqueueWithPreprocess()after the preprocess callback resolves — otherwise preprocess-based inputs (includingThreadCreateflow and Discord chat messages) skip the transformation. - No double-conversion risk:
enqueueIncomingreturns early toenqueueWithPreprocesswheninput.preprocessis set.
preprocessNewSessionMessage() wraps the user prompt with
Context from thread:\n${starterText}\n\nUser request:\n${prompt} when the
starter message differs from the current message. This breaks any
prefix-based detection (leading /command, etc) because the command is no
longer at the start of the prompt.
Fix pattern: run the detector on the raw prompt BEFORE wrapping and skip the wrapping when detection succeeds.
When adding a transformation that needs to match a user-intent pattern in
prompts that sometimes carry programmatic prefixes (» **otto-cli:** ...,
» **user:** ..., Context from thread: ...), do NOT try to regex-strip
every possible prefix before matching. That creates maintenance burden
(new prefix formats silently break detection) and gets the semantics
wrong when usernames contain regex metacharacters.
Instead:
- Split the prompt by
\nand check each line - Always put programmatic prefixes on their OWN line (separated by
\nfrom the user's content), so the user's text starts at a fresh line - Detection only scans each line's first non-whitespace token
This makes detection oblivious to prefix format — it Just Works for any current or future prefix line.
When writing regexes to match markdown-formatted names like **<name>:**,
use non-greedy [\s\S]+? instead of [^*]+. Discord display names can
(rarely) contain *. Better long-term fix: escape usernames at render
time or pass structured metadata instead of parsing markdown.
git status frequently shows modifications from other agents running in
parallel on the same repo. Never git add -A or git add .. Always
enumerate your files explicitly:
git commit path/to/file1 path/to/file2 -m "message"Before committing, run git status -s and git diff <file> on any file
you don't remember touching. If it's unrelated to your task, leave it out
of the commit.
Discord rate-limits channel/thread renames to ~2 per 10 minutes per thread,
and the limit is undocumented in headers — setName() will silently
block on the 3rd attempt rather than returning 429. See
discord/discord-api-docs#1900 and discordjs/discord.js#6651.
Design rules for any code that calls thread.setName():
- Rename at most once per distinct new value (dedup via a runtime-local field).
- Race
setName()againstAbortSignal.timeout(...)(discord.js doesn't take a signal directly, so wrap inPromise.race). - Fail soft on timeout/429/error — log and continue, never retry.
- Don't let a blocked rename block queue draining, typing, or event handling.
Reference implementation: handleSessionUpdated in
cli/src/session-handler/thread-session-runtime.ts.
client.permission.reply({ requestID, directory, workspace, reply, message })
is the only SDK method to answer a permission.asked event. The body only
accepts reply: "once" | "always" | "reject" plus an optional message.
There is no field to override the directory/path/patterns of the
permission. The directory and workspace query params are just routing
hints to identify which OpenCode server context the reply belongs to —
they do NOT change what the "always" rule covers.
The scope of "always" is determined entirely by PermissionRequest.patterns
set by OpenCode when it emitted permission.asked. If you want a broader
rule (e.g. grant permission for a parent directory instead of a single
file), the user must configure permission rules in OpenCode config / via
per-session permissions option (see parsePermissionRules and the
--permission "tool:pattern:action" CLI flag in
cli/src/session-handler/thread-session-runtime.ts), not via
permission.reply.
There is also a legacy PermissionRespond endpoint
(POST /session/{sessionID}/permissions/{permissionID}) with the same
body shape — no scope override there either.
cli/package.json lists undici: ^8.0.2 as a devDependency (used by
gateway-proxy-reconnect.e2e.test.ts for setGlobalDispatcher). If you
see Cannot find package 'undici' from that test, just run pnpm install
inside cli/. Do NOT assume it's a transitive dep — the comment in
discord-bot.ts:125 saying "undici is a transitive dep from discord.js"
is misleading for the test file which needs the explicit dependency.
getManagedWorktreeDirectory strips the opencode/otto- prefix from the
on-disk folder basename but the git branch name still keeps it (backward
compat: opencode/otto- prefix is also recognized for existing branches).
Two format helpers exist: formatWorktreeName (verbatim, for user-provided
names) and formatAutoWorktreeName (vowel-compressed if >20 chars, for
auto-derived names from thread titles/prompts). Worktrees now live under
<ottoDataDir>/worktrees/<8charProjectHash>/<basename>.