Document findings from database investigation: - Session table schema with all fields explained - Session ID format and generation (unique, no duplicates) - Parent-child relationships for forked sessions - Session detection logic used by kugetsu - Permission structure and common issues - SQL queries for debugging session problems - Known issues and solutions (from #81, #36) This document helps future debugging of session-related issues without having to investigate opencode internals directly.
7.3 KiB
OpenCode Session Internals
This document contains findings about how OpenCode manages sessions, based on direct database investigation. Use this when debugging session-related issues in kugetsu.
Database Location
opencode db path
# Returns: ~/.local/share/opencode/opencode.db
Session Table Schema
CREATE TABLE `session` (
`id` text PRIMARY KEY,
`project_id` text NOT NULL,
`parent_id` text, -- Parent session ID (for forked sessions)
`slug` text NOT NULL, -- Auto-generated adjective-animal name
`directory` text NOT NULL, -- Working directory for session
`title` text NOT NULL,
`version` text NOT NULL,
`share_url` text,
`summary_additions` integer,
`summary_deletions` integer,
`summary_files` integer,
`summary_diffs` text,
`revert` text,
`permission` text, -- JSON array of permission rules
`time_created` integer NOT NULL, -- Unix timestamp in milliseconds
`time_updated` integer NOT NULL,
`time_compacting` integer,
`time_archived` integer,
`workspace_id` text
);
Session ID Format
OpenCode session IDs follow the format: ses_<base62_chars>
Example: ses_2b4eb7afbffezJwifgucdLRkt8
The ID appears to be generated using a timestamp-based algorithm with random components. Analysis of 118+ sessions shows:
- No duplicate IDs - Each session gets a unique ID even with concurrent forks
- No sequential patterns - IDs are not sequential even for sessions created milliseconds apart
- Contains timestamp - The first numeric portion appears to encode creation time
Querying Sessions
List all sessions
opencode session list
Query database directly (requires sqlite3 or python)
import sqlite3
conn = sqlite3.connect('/home/shoko/.local/share/opencode/opencode.db')
cursor = conn.cursor()
# Get all sessions
cursor.execute('SELECT id, parent_id, slug, directory FROM session')
# Get forked sessions (sessions with a parent)
cursor.execute('SELECT id, parent_id FROM session WHERE parent_id IS NOT NULL')
# Get sessions by directory
cursor.execute("SELECT id, slug FROM session WHERE directory LIKE '%kugetsu%'")
Session Relationships
Parent-Child Relationships
When you run opencode run --fork --session <parent_id>, OpenCode:
- Creates a NEW session with a unique ID
- Sets the
parent_idfield to reference the parent session - The child session inherits context from parent but has its own workspace
Session Detection in Kugetsu
Kugetsu uses opencode session list to detect newly created sessions. The output format is:
ses_abc123def456
ses_xyz789...
Kugetsu's cmd_start workflow:
- Before fork: List all sessions, store in array
- Fork: Run
opencode run --fork --session <parent> - After fork: List sessions again
- Detect new: Compare before/after arrays, exclude known sessions (base, pm-agent)
# Store before sessions in array
declare -a before_sessions=()
while IFS= read -r sess; do
before_sessions+=("$sess")
done < <(opencode session list 2>/dev/null | grep -oP '^ses_\w+')
# Fork happens here...
# Find sessions not in before array
while IFS= read -r sess; do
# Skip base and pm-agent sessions
[ "$sess" = "$base_session_id"" ] && continue
[ "$sess" = "$pm_agent_session_id" ] && continue
# Check if session existed before
local existed_before=false
for before_sess in "${before_sessions[@]}"; do
if [ "$sess" = "$before_sess" ]; then
existed_before=true
break
fi
done
if [ "$existed_before" = false ]; then
new_session_id="$sess"
break
fi
done < <(opencode session list 2>/dev/null | grep -oP '^ses_\w+')
Session Directories
Each session has a directory field indicating its working directory:
| Directory | Purpose |
|---|---|
/home/shoko |
Base session, PM agent |
/home/shoko/repositories/kugetsu |
Project sessions |
~/.kugetsu/worktrees/<issue-ref> |
Per-issue worktrees |
Permissions
Sessions have a permission field containing a JSON array:
[
{"permission": "question", "pattern": "*", "action": "deny"},
{"permission": "plan_enter", "pattern": "*", "action": "deny"},
{"permission": "plan_exit", "pattern": "*", "action": "deny"},
{"permission": "external_directory", "pattern": "*", "action": "allow"}
]
Common Permission Issues
Issue: permission requested: external_directory (/path/*); auto-rejecting
Cause: The session's permission field may be NULL or missing required rules.
Fix: Update via SQLite:
import sqlite3
conn = sqlite3.connect('/home/shoko/.local/share/opencode/opencode.db')
cursor = conn.cursor()
PERMISSION_JSON = '[{"permission":"question","pattern":"*","action":"deny"},{"permission":"plan_enter","pattern":"*","action":"deny"},{"permission":"plan_exit","pattern":"*","action":"deny"},{"permission":"external_directory","pattern":"*","action":"allow"}]'
cursor.execute("UPDATE session SET permission = ? WHERE id = ?",
(PERMISSION_JSON, session_id))
conn.commit()
Known Issues & Solutions
Session ID Collision (Issue #81)
Problem: Forked sessions showing same ID as PM agent.
Investigation Results:
- OpenCode does NOT generate duplicate IDs (verified with 118+ sessions)
- Database shows unique IDs even for concurrent forks
- Issue is in kugetsu's session detection logic, not opencode
Solution: Use array-based session detection (see above) instead of string/regex matching.
Stale Permission NULL (Issue #36)
Problem: PM agent cannot access directories despite permissions.
Root Cause: Session created with permission = NULL in database.
Detection:
cursor.execute("SELECT id FROM session WHERE permission IS NULL")
Fix: Set permissions via kugetsu:
kugetsu doctor --fix-permissions
Useful Queries
Find sessions by issue reference
# Find sessions for a specific issue worktree
cursor.execute("SELECT id, slug FROM session WHERE directory LIKE '%issue-81%'")
Find orphaned sessions (no parent, old)
import time
old_threshold = time.time() - (30 * 24 * 60 * 60) # 30 days ago
cursor.execute("""SELECT id, slug, directory, time_created
FROM session
WHERE parent_id IS NULL
AND time_created < ?
ORDER BY time_created""", (old_threshold * 1000,))
Count sessions per project
cursor.execute("""SELECT project_id, COUNT(*) as cnt
FROM session
GROUP BY project_id
ORDER BY cnt DESC""")
Debugging Tips
- Check current sessions:
opencode session list - Check database:
opencode db "SELECT id, parent_id, slug FROM session ORDER BY time_created DESC LIMIT 10" - Verify permissions: Check if
permissionfield is NULL or valid JSON - Check directory: Ensure session directory exists and is accessible
- Compare before/after: When debugging detection, log both before and after session lists
External References
- OpenCode Repository: https://github.com/opencode-ai/opencode
- Session Management: Uses SQLite with unique constraint on
idcolumn - Fork Operation: Sets
parent_idto establish relationship