Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remorses/kimaki/llms.txt
Use this file to discover all available pages before exploring further.
Session Management
Kimaki manages OpenCode AI sessions through Discord threads. Each thread maps to one OpenCode session that persists across messages.
Session Lifecycle
Creation
Sessions are created automatically when you send the first message in a thread:
// session-handler.ts: handleOpencodeSession
1. Check if thread has existing session
2. If no session:
- Create new OpenCode session
- Set session title (first 80 chars of prompt)
- Store thread → session mapping in SQLite
3. If session exists:
- Reuse existing session
- Continue conversation
Session creation API call:
const response = await client.session.create({
title: prompt.slice(0, 80),
directory: sdkDirectory
})
const sessionId = response.data.id
await setThreadSession(threadId, sessionId)
Session Reuse
Replying to a thread reuses the same session:
const sessionId = await getThreadSession(threadId)
if (sessionId) {
// Verify session still exists on server
const session = await client.session.get({
sessionID: sessionId,
directory: sdkDirectory
})
// Continue with existing session
}
This provides conversation continuity - the AI remembers previous context.
Abort and Interruption
When a new message arrives during an active response:
// 1. Signal abort via AbortController
const controller = abortControllers.get(sessionId)
controller.abort(new SessionAbortError({ reason: 'new-request' }))
// 2. Call server abort API
await client.session.abort({
sessionID: sessionId,
directory: sdkDirectory
})
// 3. Wait for cleanup (200ms debounce)
await new Promise(resolve => setTimeout(resolve, 200))
// 4. Process new message
Abort reasons:
new-request: User sent new message
model-change: User changed model mid-request
finished: Session completed normally
Session Termination
Sessions terminate when:
- Idle event received: OpenCode signals completion
- Manual abort:
/abort command
- Error: Unrecoverable error during processing
- Server restart: OpenCode server process restarts
Terminated sessions remain in OpenCode’s storage and can be resumed with /resume.
Message Queue
Kimaki queues messages when the AI is busy:
export const messageQueue = new Map<
string, // threadId
QueuedMessage[]
>()
type QueuedMessage = {
prompt: string
userId: string
username: string
queuedAt: number
images?: DiscordFileAttachment[]
command?: { name: string; arguments: string }
}
Queue Behavior
// Check if request is active
const controller = abortControllers.get(sessionId)
const hasActiveRequest = controller && !controller.signal.aborted
if (hasActiveRequest) {
// Queue the message
addToQueue({ threadId, message })
// Abort current request immediately
controller.abort(new SessionAbortError({ reason: 'new-request' }))
} else {
// Send immediately
handleOpencodeSession({ prompt, thread, ... })
}
Queue Draining
After a response completes:
// Check for queued messages
const queue = messageQueue.get(threadId)
if (queue && queue.length > 0) {
const nextMessage = queue.shift()
// Process next message
handleOpencodeSession({
prompt: nextMessage.prompt,
thread,
projectDirectory,
channelId,
username: nextMessage.username,
userId: nextMessage.userId,
images: nextMessage.images
})
}
Use /queue <message> to explicitly queue a follow-up message.
Event Stream Processing
Kimaki subscribes to OpenCode’s Server-Sent Events (SSE) stream:
const events = await client.event.subscribe(
{ directory: sdkDirectory },
{ signal: abortController.signal }
)
for await (const event of events.stream) {
switch (event.type) {
case 'message.updated':
// Buffer and display message parts
break
case 'step-finish':
// Flush buffered parts, stop typing
break
case 'permission.request':
// Show permission buttons
break
case 'question.request':
// Show question dropdowns
break
case 'idle':
// Session completed, drain queue
break
}
}
Part Buffering
Message parts are buffered and flushed strategically:
const partBuffer = new Map<
string, // messageID
Map<string, Part> // partID → Part
>()
// Buffer parts as they stream in
function storePart(part: Part) {
const messageParts = partBuffer.get(part.messageID) || new Map()
messageParts.set(part.id, part)
partBuffer.set(part.messageID, messageParts)
}
// Flush when appropriate:
- Text part completes (time.end is set)
- Tool part starts running
- Step finishes
- Permission/question request
Typing Indicator
Kimaki shows typing indicator during AI responses:
function startTyping() {
// Initial typing
await thread.sendTyping()
// Refresh every 8 seconds (Discord expires at ~10s)
typingInterval = setInterval(() => {
thread.sendTyping()
}, 8000)
}
function stopTyping() {
clearInterval(typingInterval)
clearTimeout(typingRestartTimeout)
}
// Stop during permission/question prompts
// Restart 300ms after prompt is answered
Session Preferences
Model Selection
Model priority order:
- Explicit override:
/model command or --model flag
- Session preference: Saved from previous
/model command
- Channel default: Set via
/model in channel
- OpenCode config:
opencode.json model field
- Recent TUI model: From
~/.local/state/opencode/model.json
- Provider default: First connected provider’s default model
const modelInfo = await getCurrentModelInfo({
sessionId,
channelId,
appId,
agentPreference,
getClient
})
if (modelInfo.type === 'none') {
// No provider connected
await thread.send('No AI provider connected. Use `/connect` command.')
return
}
// Use modelInfo.providerID / modelInfo.modelID
Agent Selection
Agent priority order:
- Explicit override:
--agent flag
- Session preference: Saved from previous
/agent command
- Channel default: Set via
/agent in channel
- OpenCode default: Primary agent from config
const { agentPreference, agents } =
await resolveValidatedAgentPreference({
agent: overrideAgent,
sessionId,
channelId,
getClient
})
if (agentPreference) {
// Use specific agent
} else {
// Use OpenCode's default agent selection
}
Preferences Snapshot
Preferences are snapshotted early to avoid race conditions:
// Before event subscription
const earlyAgentPreference = await resolveValidatedAgentPreference(...)
const earlyModelParam = await getCurrentModelInfo(...)
const earlyThinkingValue = await validateThinkingVariant(...)
// Use these snapshots for the entire request
// User changes during request don't affect current response
Permission Handling
When OpenCode requests permission:
// Store pending permission
pendingPermissions.set(threadId, new Map([
[permission.id, {
permission,
messageId,
directory,
permissionDirectory,
contextHash,
dedupeKey
}]
]))
// Show buttons
await showPermissionButtons({
thread,
permission,
directory,
permissionDirectory
})
// Stop typing while waiting
stopTyping()
// User clicks button → permission.reply()
// Resume typing 300ms later
Permission Deduplication
Multiple identical permission requests are merged:
function buildPermissionDedupeKey({
permission,
directory
}) {
const sorted = [...permission.patterns].sort()
return `${directory}::${permission.permission}::${sorted.join('|')}`
}
// If dedupeKey already exists, batch requests:
context.requestIds.push(permission.id)
// All are answered together when user clicks button
Auto-Reject on New Message
Pending permissions are auto-rejected when a new message arrives:
for (const [permId, pendingPerm] of threadPermissions) {
// Remove buttons from Discord message
await msg.edit({ components: [] })
// Reject permission via API
await client.permission.reply({
requestID: permId,
directory: pendingPerm.permissionDirectory,
reply: 'reject'
})
// Cleanup context
cleanupPermissionContext(pendingPerm.contextHash)
}
Question Handling
OpenCode can ask questions via the question tool:
// Plugin tool registration
export async function kimaki_question(input: {
question: string
options: string[]
allow_multiple: boolean
}) {
// Show dropdown menu in Discord
await showAskUserQuestionDropdowns({
thread,
question: input.question,
options: input.options,
allowMultiple: input.allow_multiple
})
// Wait for user selection (blocks plugin tool)
const answer = await waitForQuestionAnswer()
return { answer }
}
If user sends a message instead of answering:
// Auto-answer with the message content
const answered = await cancelPendingQuestion(threadId, userMessage)
if (answered) {
// Plugin tool unblocks with user message as answer
}
Session Commands
Resume Previous Session
// /resume command
const sessions = await client.session.list({ directory })
const options = sessions.data.map(s => ({
label: s.title,
value: s.id
}))
// User selects session
await setThreadSession(threadId, selectedSessionId)
Fork Session
// /fork command
const messages = await client.session.messages({ sessionID })
// User selects message to fork from
const newSession = await client.session.fork({
sessionID,
messageID: selectedMessageId
})
await setThreadSession(threadId, newSession.data.id)
Share Session
// /share command
const shareUrl = await client.session.share({ sessionID })
await interaction.reply({ content: shareUrl.data.url })
Subtask Sessions
When OpenCode spawns subtasks (e.g., task tool):
const subtaskSessions = new Map<
string, // childSessionId
{ label: string; assistantMessageId?: string }
>()
if (part.tool === 'task') {
const childSessionId = part.state.metadata?.sessionId
const agent = part.state.input?.subagent_type || 'task'
// Track spawned tasks per agent type
agentSpawnCounts[agent] = (agentSpawnCounts[agent] || 0) + 1
const label = `${agent}-${agentSpawnCounts[agent]}`
subtaskSessions.set(childSessionId, { label })
// Show in Discord
await thread.send(`┣ task **${description}** _${agent}_`)
}
Subtask messages are tracked separately but not shown in Discord by default (to avoid spam).
Verbosity Modes
Control how much output is shown:
type Verbosity =
| 'text-only' // Only text parts
| 'text-and-essential-tools' // Text + edits, bash, custom MCP tools
| 'tools-and-text' // Everything (default)
const verbosity = await getChannelVerbosity(channelId)
function shouldShowPart(part: Part): boolean {
if (verbosity === 'text-only') {
return part.type === 'text'
}
if (verbosity === 'text-and-essential-tools') {
return part.type === 'text' || isEssentialToolPart(part)
}
return true // tools-and-text shows everything
}
Non-essential tools hidden in text-and-essential-tools mode:
read, list, glob, grep
todoread, question, kimaki_action_buttons
webfetch
Session Persistence
Session data persists in:
- OpenCode storage: Session messages, state, metadata
- SQLite database: Thread → session mapping, preferences
- Discord threads: Visual conversation history
Deleting a Discord thread does NOT delete the OpenCode session. You can:
- Resume the session in a new thread with
/resume
- Access it via OpenCode TUI
- Export it with
/share