Sessions and logs
A skillet keeps two distinct stores per conversation:
- A SQLite session store is the canonical state the LLM sees. It holds the running list of items (user messages, assistant outputs, tool calls) that get re-sent to the model on the next turn.
- A JSONL replay log is an append-only file the UI reads back to redraw a conversation. CLI replays and the web chat both consume the same file.
Both are scoped by userId + sessionName. The CLI exposes both as flags (--user-email, --session-name); when omitted they default to the seeded user john@example.com and a freshly generated session_<ISO timestamp>_<random>. The email is the userId — one identity shared with the web client — so it appears verbatim in the user_id column and, sanitized, as the per-user log directory.
Where these files live
Runtime paths are resolved by SkilletPaths, which splits them into three roles: state (the databases, session logs, and REPL history), cache (the disposable response cache), and config (your own crews). The location depends on how you run:
| How you run | Base for all three roles |
|---|---|
| From a git checkout (development) | <repo root>/.skilled-agent/ → state/, cache/, config/ |
| Installed via npm | XDG dirs: ~/.local/state/skilled-crew/, ~/.cache/skilled-crew/, ~/.config/skilled-crew/ |
SKILLET_HOME set | $SKILLET_HOME/state, $SKILLET_HOME/cache, $SKILLET_HOME/config |
The paths below use the development layout (.skilled-agent/state/…, relative to the repo root). There is no longer an outputs/ directory — that was the pre-SkilletPaths layout.
SQLite session store
File: .skilled-agent/state/.agent_sessions.sqlite. Schema:
session_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
session_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
item_json TEXT NOT NULL
)Items are stored as opaque JSON blobs from the OpenAI Agents SDK. You can poke at it with sqlite3 if needed:
sqlite3 .skilled-agent/state/.agent_sessions.sqlite \
"SELECT count(*) FROM session_items WHERE user_id = 'john@example.com';"The store is wrapped by OpenAIResponsesCompactionSession so the runtime never re-sends more than the compaction threshold (default 40 non-user items). Older content is collapsed into a summary turn.
JSONL replay log
Each (userId, sessionName) pair gets a single append-only file:
.skilled-agent/state/.agent_session_logs/{userId}/{sessionName}.jsonlNames are sanitized to [a-zA-Z0-9_-]+ before being used as path segments, so the default user john@example.com becomes the directory john_example_com.
A turn is one user message followed by zero or more step entries, closed by one final result. Three entry shapes — kind: 'user_message', kind: 'step', kind: 'final_result' — each timestamped:
{"kind":"user_message","userInput":"what's on my list?","timestamp":"2026-05-27T20:11:02.482Z"}
{"kind":"step","stepResult":{"type":"agent_start","agentName":"todo_list_agent"},"timestamp":"..."}
{"kind":"step","stepResult":{"type":"agent_tool_start","agentName":"list-tasks","toolName":"run_command_line","toolArgumentsStr":"..."},"timestamp":"..."}
{"kind":"step","stepResult":{"type":"agent_tool_end","agentName":"list-tasks","toolName":"run_command_line","result":"1. [ ] buy milk"},"timestamp":"..."}
{"kind":"step","stepResult":{"type":"text","text":"1. [ ] buy milk"},"timestamp":"..."}
{"kind":"final_result","finalResult":{"text":"1. [ ] buy milk"},"timestamp":"..."}The step event types come straight from AgentRunnerStepResult.
Reading a replay
When you start chat or run with an existing --session-name, the prior turns are replayed automatically before the first prompt — there is no --replay flag. To follow a log directly, tail the JSONL file:
# Tail live as a chat (or a job worker) runs
tail -f .skilled-agent/state/.agent_session_logs/john_example_com/<session-name>.jsonl | jq
# Stream every session log under the logs root, oldest-first, then follow new appends
npx tsx ./src/cli.ts log streamlog stream discovers every .jsonl under the session-logs root (.skilled-agent/state/.agent_session_logs in development; override with --logs-dir); it is the easiest way to watch running job workers. SessionLogReader and SessionLogTailer are also exported from the library — see API › SessionLogReader.
Clearing state
The session store and the log are independent. Both can be wiped:
# Drop one session's LLM context
sqlite3 .skilled-agent/state/.agent_sessions.sqlite \
"DELETE FROM session_items WHERE user_id='john@example.com' AND session_name='<session-name>';"
# Drop one session's UI log
rm .skilled-agent/state/.agent_session_logs/john_example_com/<session-name>.jsonlThere’s no single “clear everything” command in the CLI today — drop the files or rows you want gone.
Picking userId and sessionName
For local development, the defaults are fine. For multi-user web hosting (the _skillet_webclient package wires this), the userId comes from the authenticated session. The sessionName is what differentiates conversations within one user; a new sessionName starts an empty context window.