chore: Merge branch 'main'

This commit is contained in:
Nikhil Sonti
2026-03-13 07:48:40 -07:00
23 changed files with 87 additions and 158 deletions

View File

@@ -62,7 +62,7 @@ const primarySettingsSections: NavSection[] = [
items: [
{ name: 'BrowserOS AI', to: '/settings/ai', icon: Bot },
{
name: 'Chat & Hub Provider',
name: 'Chat & Council Provider',
to: '/settings/chat',
icon: MessageSquare,
},

View File

@@ -109,25 +109,6 @@ export const scheduledJobRuns = async () => {
throw new Error(`Job not found: ${jobId}`)
}
const backgroundWindow = await chrome.windows.create({
url: 'chrome://newtab',
focused: false,
state: 'minimized',
type: 'normal',
})
// FIXME: Race condition - the controller-ext extension sends a window_created
// WebSocket message to register window ownership, but our HTTP request may arrive
// at the server before that registration completes. This delay is a temporary fix.
// Proper solution: ControllerBridge should wait/poll for window ownership registration.
await new Promise((resolve) => setTimeout(resolve, 1000))
const backgroundTab = backgroundWindow?.tabs?.[0]
if (!backgroundWindow || !backgroundTab) {
throw new Error('Failed to create background window')
}
const jobRun = await createJobRun(jobId, 'running')
const abortController = new AbortController()
runAbortControllers.set(jobRun.id, abortController)
@@ -135,8 +116,6 @@ export const scheduledJobRuns = async () => {
try {
const response = await getChatServerResponse({
message: job.query,
activeTab: backgroundTab,
windowId: backgroundWindow.id,
signal: abortController.signal,
})
@@ -163,13 +142,6 @@ export const scheduledJobRuns = async () => {
})
} finally {
runAbortControllers.delete(jobRun.id)
if (backgroundWindow.id) {
try {
await chrome.windows.remove(backgroundWindow.id)
} catch {
// Window may already be closed
}
}
await updateJobLastRunAt(jobId)
}
}

View File

@@ -72,7 +72,7 @@ export class AiSdkAgent {
const allBrowserTools = buildBrowserToolSet(
config.registry,
config.browser,
config.resolvedConfig.sessionExecutionDir,
config.resolvedConfig.workingDir,
)
const browserTools = config.resolvedConfig.chatMode
? Object.fromEntries(
@@ -98,7 +98,7 @@ export class AiSdkAgent {
// Add filesystem tools (Pi coding agent) — skip in chat mode (read-only)
const filesystemTools = config.resolvedConfig.chatMode
? {}
: buildFilesystemToolSet(config.resolvedConfig.sessionExecutionDir)
: buildFilesystemToolSet(config.resolvedConfig.workingDir)
const memoryTools = config.resolvedConfig.chatMode
? {}
: buildMemoryToolSet()
@@ -118,9 +118,7 @@ export class AiSdkAgent {
}
// Build system prompt with optional section exclusions
// Tool definitions are already injected by the AI SDK via tool schemas,
// so skip the redundant tool-reference section.
const excludeSections: string[] = ['tool-reference']
const excludeSections: string[] = []
if (config.resolvedConfig.isScheduledTask) {
excludeSections.push('tab-grouping')
}
@@ -143,7 +141,7 @@ export class AiSdkAgent {
exclude: excludeSections,
isScheduledTask: config.resolvedConfig.isScheduledTask,
scheduledTaskWindowId: config.browserContext?.windowId,
workspaceDir: config.resolvedConfig.sessionExecutionDir,
workspaceDir: config.resolvedConfig.workingDir,
soulContent,
isSoulBootstrap: isBootstrap,
chatMode: config.resolvedConfig.chatMode,

View File

@@ -128,82 +128,6 @@ function getErrorRecovery(): string {
---`
}
// -----------------------------------------------------------------------------
// section: cdp-tool-reference
// Skipped by ToolLoopAgent — the AI SDK already injects tool schemas into the
// LLM call. Kept for MCP prompt serving where clients lack tool definitions.
// -----------------------------------------------------------------------------
function getCdpToolReference(): string {
return `# Tool Reference
## Page Management
- \`get_active_page\` - Get the currently active (focused) page
- \`list_pages\` - Get all open pages with IDs, titles, tab IDs, and URLs
- \`new_page(url, hidden?, background?, windowId?)\` - Open a new page. Use hidden for background processing, background to avoid activating.
- \`close_page(page)\` - Close a page by its page ID
- \`navigate_page(page, action, url?)\` - Navigate: action is "url", "back", "forward", or "reload"
- \`wait_for(page, text?, selector?, timeout?)\` - Wait for text or CSS selector to appear
## Content Capture
- \`take_snapshot(page)\` - Get interactive elements with IDs (e.g. [47]). **Always take before interacting.**
- \`take_enhanced_snapshot(page)\` - Detailed accessibility tree with structural context
- \`get_page_content(page, selector?, viewportOnly?, includeLinks?, includeImages?)\` - Extract page as clean markdown with headers, links, lists, tables. **Prefer for data extraction.**
- \`take_screenshot(page, format?, quality?, fullPage?)\` - Capture page image
- \`evaluate_script(page, expression)\` - Run JavaScript in page context
## Input & Interaction
- \`click(page, element)\` - Click element by ID from snapshot
- \`click_at(page, x, y)\` - Click at specific coordinates
- \`hover(page, element)\` - Hover over element
- \`focus(page, element)\` - Focus an element (scrolls into view first)
- \`clear(page, element)\` - Clear text from input or textarea
- \`fill(page, element, text, clear?)\` - Type into input/textarea (clears first by default)
- \`check(page, element)\` - Check a checkbox or radio button (no-op if already checked)
- \`uncheck(page, element)\` - Uncheck a checkbox (no-op if already unchecked)
- \`upload_file(page, element, files)\` - Set file(s) on a file input (absolute paths)
- \`select_option(page, element, value)\` - Select dropdown option by value or text
- \`press_key(page, key)\` - Press key or combo (e.g., "Enter", "Control+A", "ArrowDown")
- \`drag(page, sourceElement, targetElement?, targetX?, targetY?)\` - Drag element to another element or coordinates
- \`scroll(page, direction?, amount?, element?)\` - Scroll page or element (up/down/left/right)
- \`handle_dialog(page, accept, promptText?)\` - Handle browser dialogs (alert, confirm, prompt)
## Page Actions
- \`save_pdf(page, path, cwd?)\` - Save page as PDF to disk
- \`download_file(page, element, path, cwd?)\` - Click element to trigger download, save to directory
## Window Management
- \`list_windows\` - Get all browser windows
- \`create_window(hidden?)\` - Create a new browser window
- \`close_window(windowId)\` - Close a browser window
- \`activate_window(windowId)\` - Activate (focus) a browser window
## Tab Groups
- \`list_tab_groups\` - Get all tab groups with IDs, titles, colors, and page IDs
- \`group_tabs(pageIds, title?, groupId?)\` - Create group or add pages to existing group (groupId is a string)
- \`update_tab_group(groupId, title?, color?, collapsed?)\` - Update group properties
- \`ungroup_tabs(pageIds)\` - Remove pages from their groups
- \`close_tab_group(groupId)\` - Close a tab group and all its tabs
**Colors**: grey, blue, red, yellow, green, pink, purple, cyan, orange
## Bookmarks
- \`get_bookmarks\` - Get all bookmarks
- \`create_bookmark(title, url?, parentId?)\` - Create bookmark or folder (omit url for folder)
- \`update_bookmark(id, title?, url?)\` - Edit bookmark
- \`remove_bookmark(id)\` - Delete bookmark or folder (recursive)
- \`move_bookmark(id, parentId?, index?)\` - Move bookmark or folder
- \`search_bookmarks(query)\` - Search bookmarks by title or URL
## History
- \`search_history(query, maxResults?)\` - Search browser history
- \`get_recent_history(maxResults?)\` - Get recent history items
- \`delete_history_url(url)\` - Delete a specific URL from history
- \`delete_history_range(startTime, endTime)\` - Delete history within a time range (epoch ms)
---`
}
// -----------------------------------------------------------------------------
// section: external-integrations
// -----------------------------------------------------------------------------
@@ -428,11 +352,15 @@ function getPageContext(
'\n\n**CRITICAL RULES:**\n1. **Do NOT call `get_active_page` or `list_pages` to find your starting page.** Use the **page ID from the Browser Context** directly.'
if (options?.isScheduledTask) {
const windowLine = options.scheduledTaskWindowId
? `When creating new pages with \`new_page\`, always pass \`windowId: ${options.scheduledTaskWindowId}\`.`
: 'When creating new pages with `new_page`, pass the `windowId` from the Browser Context.'
prompt += `\n2. ${windowLine}`
prompt += '\n3. Complete the task end-to-end and report results.'
const windowRef = options.scheduledTaskWindowId
? `\`windowId: ${options.scheduledTaskWindowId}\``
: 'the `windowId` from the Browser Context'
prompt += `\n2. **Always pass ${windowRef}** when calling \`new_page\` or \`new_hidden_page\`. Never omit the \`windowId\` parameter.`
prompt +=
'\n3. **Do NOT close your dedicated hidden window** (via `close_window`). It is managed by the system and will be cleaned up automatically.'
prompt +=
'\n4. **Do NOT create new windows** (via `create_window` or `create_hidden_window`). Use your existing hidden window for all pages.'
prompt += '\n5. Complete the task end-to-end and report results.'
}
prompt += '\n</page_context>'
@@ -481,7 +409,6 @@ const promptSections: Record<string, PromptSectionFn> = {
'observe-act-verify': getObserveActVerify,
'handle-obstacles': getHandleObstacles,
'error-recovery': getErrorRecovery,
'tool-reference': getCdpToolReference,
'external-integrations': getExternalIntegrations,
style: getStyle,
nudges: getNudges,
@@ -495,8 +422,6 @@ const promptSections: Record<string, PromptSectionFn> = {
'security-reminder': getSecurityReminder,
}
export const PROMPT_SECTION_KEYS = Object.keys(promptSections)
interface BuildSystemPromptOptions {
userSystemPrompt?: string
exclude?: string[]
@@ -523,7 +448,3 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
return `<AGENT_PROMPT>\n${sections.join('\n\n')}\n</AGENT_PROMPT>`
}
export function getSystemPrompt(): string {
return buildSystemPrompt()
}

View File

@@ -38,12 +38,12 @@ function contentToModelOutput(
export function buildBrowserToolSet(
registry: ToolRegistry,
browser: Browser,
executionDir: string,
workingDir: string,
): ToolSet {
const toolSet: ToolSet = {}
const ctx: ToolContext = {
browser,
directories: { executionDir },
directories: { workingDir },
}
for (const def of registry.all()) {

View File

@@ -32,7 +32,7 @@ export interface ResolvedAgentConfig {
sessionToken?: string
contextWindowSize?: number
userSystemPrompt?: string
sessionExecutionDir: string
workingDir: string
/** Whether the model supports image inputs (vision). Defaults to true. */
supportsImages?: boolean
/** Eval mode - enables window management tools. Defaults to false. */

View File

@@ -1,4 +1,3 @@
import { PATHS } from '@browseros/shared/constants/paths'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { SessionStore } from '../../agent/session-store'
@@ -17,21 +16,18 @@ import { ConversationIdParamSchema } from '../utils/validation'
interface ChatRouteDeps {
browser: Browser
registry: ToolRegistry
executionDir?: string
browserosId?: string
rateLimiter?: RateLimiter
}
export function createChatRoutes(deps: ChatRouteDeps) {
const { browserosId, rateLimiter } = deps
const executionDir = deps.executionDir || PATHS.DEFAULT_EXECUTION_DIR
const sessionStore = new SessionStore()
const klavisClient = new KlavisClient()
const service = new ChatService({
sessionStore,
klavisClient,
executionDir,
browser: deps.browser,
registry: deps.registry,
browserosId,

View File

@@ -130,7 +130,6 @@ export async function createHttpServer(config: HttpServerConfig) {
createChatRoutes({
browser,
registry,
executionDir,
browserosId,
rateLimiter,
}),

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { mkdir } from 'node:fs/promises'
import { mkdir, utimes } from 'node:fs/promises'
import path from 'node:path'
import { createAgentUIStreamResponse, type UIMessage } from 'ai'
import { AiSdkAgent } from '../../agent/ai-sdk-agent'
@@ -12,6 +12,7 @@ import { formatUserMessage } from '../../agent/format-message'
import type { SessionStore } from '../../agent/session-store'
import type { ResolvedAgentConfig } from '../../agent/types'
import type { Browser } from '../../browser/browser'
import { getSessionsDir } from '../../lib/browseros-dir'
import type { KlavisClient } from '../../lib/clients/klavis/klavis-client'
import { resolveLLMConfig } from '../../lib/clients/llm/config'
import { logger } from '../../lib/logger'
@@ -21,7 +22,6 @@ import type { BrowserContext, ChatRequest } from '../types'
export interface ChatServiceDeps {
sessionStore: SessionStore
klavisClient: KlavisClient
executionDir: string
browser: Browser
registry: ToolRegistry
browserosId?: string
@@ -38,7 +38,7 @@ export class ChatService {
const llmConfig = await resolveLLMConfig(request, this.deps.browserosId)
const sessionExecutionDir = await this.resolveSessionDir(request)
const workingDir = await this.resolveSessionDir(request)
const agentConfig: ResolvedAgentConfig = {
conversationId: request.conversationId,
@@ -54,7 +54,7 @@ export class ChatService {
sessionToken: llmConfig.sessionToken,
contextWindowSize: request.contextWindowSize,
userSystemPrompt: request.userSystemPrompt,
sessionExecutionDir,
workingDir,
supportsImages: request.supportsImages,
chatMode: request.mode === 'chat',
isScheduledTask: request.isScheduledTask,
@@ -261,8 +261,12 @@ export class ChatService {
private async resolveSessionDir(request: ChatRequest): Promise<string> {
const dir = request.userWorkingDir
? request.userWorkingDir
: path.join(this.deps.executionDir, 'sessions', request.conversationId)
: path.join(getSessionsDir(), request.conversationId)
await mkdir(dir, { recursive: true })
if (!request.userWorkingDir) {
const now = new Date()
await utimes(dir, now, now).catch(() => {})
}
return dir
}
}

View File

@@ -42,7 +42,7 @@ export function createMcpServer(deps: McpServiceDeps): McpServer {
registerTools(server, deps.registry, {
browser: deps.browser,
directories: {
executionDir: deps.executionDir,
workingDir: deps.executionDir,
resourcesDir: deps.resourcesDir,
},
})

View File

@@ -1,7 +1,8 @@
import { mkdir } from 'node:fs/promises'
import { mkdir, readdir, rm, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { PATHS } from '@browseros/shared/constants/paths'
import { logger } from './logger'
export function getBrowserosDir(): string {
return join(homedir(), PATHS.BROWSEROS_DIR_NAME)
@@ -11,6 +12,10 @@ export function getMemoryDir(): string {
return join(getBrowserosDir(), PATHS.MEMORY_DIR_NAME)
}
export function getSessionsDir(): string {
return join(getBrowserosDir(), PATHS.SESSIONS_DIR_NAME)
}
export function getSoulPath(): string {
return join(getBrowserosDir(), PATHS.SOUL_FILE_NAME)
}
@@ -26,4 +31,35 @@ export function getSkillsDir(): string {
export async function ensureBrowserosDir(): Promise<void> {
await mkdir(getMemoryDir(), { recursive: true })
await mkdir(getSkillsDir(), { recursive: true })
await mkdir(getSessionsDir(), { recursive: true })
}
export async function cleanOldSessions(): Promise<void> {
const sessionsDir = getSessionsDir()
let entries: string[]
try {
entries = await readdir(sessionsDir)
} catch {
return
}
const cutoff = Date.now() - PATHS.SESSION_RETENTION_DAYS * 24 * 60 * 60 * 1000
let removed = 0
for (const entry of entries) {
const entryPath = join(sessionsDir, entry)
try {
const info = await stat(entryPath)
if (info.isDirectory() && info.mtimeMs < cutoff) {
await rm(entryPath, { recursive: true })
removed++
}
} catch {
// skip entries that were already removed or inaccessible
}
}
if (removed > 0) {
logger.info(`Cleaned ${removed} stale session directories`)
}
}

View File

@@ -18,7 +18,7 @@ import { ControllerBackend } from './browser/backends/controller'
import { Browser } from './browser/browser'
import type { ServerConfig } from './config'
import { INLINED_ENV } from './env'
import { ensureBrowserosDir } from './lib/browseros-dir'
import { cleanOldSessions, ensureBrowserosDir } from './lib/browseros-dir'
import { initializeDb } from './lib/db'
import { identity } from './lib/identity'
import { logger } from './lib/logger'
@@ -132,6 +132,7 @@ export class Application {
private async initCoreServices(): Promise<void> {
this.configureLogDirectory()
await ensureBrowserosDir()
await cleanOldSessions()
await seedSoulTemplate()
await seedDefaultSkills()

View File

@@ -29,7 +29,7 @@ Confirm with the user before searching:
| Step | Tool | Detail |
|------|------|--------|
| Create output directory | `evaluate_script` | Create `~/Downloads/compare-<product-slug>/` with a `raw/` subfolder |
| Create output directory | `evaluate_script` | Create `compare-<product-slug>/` in your working directory with a `raw/` subfolder |
| Open hidden window | `create_hidden_window` | Dedicated workspace — keeps the user's browsing undisturbed |
| Open parallel tabs | `new_hidden_page` | Open up to **10 tabs** concurrently, one per retailer/search |

View File

@@ -19,7 +19,7 @@ Activate when the user asks to research a topic, compare information across sour
### Phase 1 — Clarify & Plan
1. **Clarify the research question.** If the query is vague, ask the user for specifics: scope, depth, preferred sources, and where to save output (default: `~/Downloads/research-<topic-slug>/`).
1. **Clarify the research question.** If the query is vague, ask the user for specifics: scope, depth, preferred sources, and where to save output (default: `research-<topic-slug>/` in your working directory).
2. **Plan search queries.** Break the topic into 35 search angles. Example for "best standing desks":
- `best standing desks 2025 reviews`
- `standing desk comparison reddit`

View File

@@ -22,7 +22,7 @@ Activate when the user asks to extract, scrape, pull, or collect structured data
1. **Clarify the request.** Before extracting, confirm with the user:
- **Source(s):** Single page, list of URLs, or search-then-extract?
- **Output format:** CSV, JSON, or Markdown table? Default to CSV if not specified.
- **Output location:** Where to save files. Default: `~/Downloads/extract-<topic-slug>/`.
- **Output location:** Where to save files. Default: `extract-<topic-slug>/` in your working directory.
- **What data to extract:** Column names, specific fields, or "everything in the table."
2. **Create the output directory.** Use `evaluate_script` to create the target folder:
```

View File

@@ -29,9 +29,9 @@ Activate when the user:
- **Price range** — same range, cheaper, or open budget? If unclear, default to ±30% of the reference product's price.
- **Key criteria** — what matters most? (e.g., price, quality, brand, specific features)
- **Any exclusions** — brands or stores to skip
3. **Create output directory.** Use `evaluate_script` to create:
3. **Create output directory.** Use `evaluate_script` to create in your working directory:
```
~/Downloads/alternatives-<product-slug>/
alternatives-<product-slug>/
├── raw/ ← per-source research data
├── findings.md ← running notes and rankings
└── report.html ← final HTML report

View File

@@ -1,6 +1,6 @@
---
name: read-later
description: Bookmark the current page to a "Read Later" folder and save a PDF copy for offline reading. Use when the user wants to save a page for later, bookmark it for reading, or keep an offline copy.
description: Bookmark the current page to a "📚 Read Later" folder and save a PDF copy for offline reading. Use when the user wants to save a page for later, bookmark it for reading, or keep an offline copy.
metadata:
display-name: Read Later
enabled: "true"
@@ -23,7 +23,7 @@ Activate when the user asks to save a page for later, read it later, bookmark so
| Check for folder | `get_bookmarks` | Look for an existing folder named "📚 Read Later" in the bookmark bar |
| Create folder (if needed) | `create_bookmark` | If the folder doesn't exist, create "📚 Read Later" in the bookmark bar |
| Add bookmark | `create_bookmark` | Save the current page URL and title into the "📚 Read Later" folder |
| Save PDF | `save_pdf` | Download the full page as a PDF to the user's default downloads directory |
| Save PDF | `save_pdf` | Download the full page as a PDF to the working directory |
| Notify user | — | Tell the user the page has been saved with the bookmark location and PDF file path |
## Notification Format

View File

@@ -21,7 +21,7 @@ Activate when the user asks to save a page as PDF, download a page for offline r
- Dismiss any popups or overlays that would appear in the PDF
- Scroll to load any lazy-loaded content if the page uses infinite scroll
3. **Save as PDF** using `save_pdf` with a descriptive filename:
3. **Save as PDF** using `save_pdf` with a descriptive filename in the working directory:
- Pattern: `{domain}-{title-slug}-{date}.pdf`
- Example: `nytimes-climate-report-2025-03-11.pdf`
- Let the user specify a custom path if they prefer

View File

@@ -18,7 +18,7 @@ export type ToolHandler = (
) => Promise<void>
export interface ToolDirectories {
executionDir: string
workingDir: string
resourcesDir?: string
}
@@ -27,12 +27,12 @@ export type ToolContext = {
directories: ToolDirectories
}
export function resolveExecutionPath(
export function resolveWorkingPath(
ctx: ToolContext,
targetPath: string,
cwd?: string,
): string {
return resolve(cwd ?? ctx.directories.executionDir, targetPath)
return resolve(cwd ?? ctx.directories.workingDir, targetPath)
}
export function defineTool<

View File

@@ -1,7 +1,7 @@
import { mkdir, mkdtemp, rename, rm } from 'node:fs/promises'
import { join } from 'node:path'
import { z } from 'zod'
import { defineTool, resolveExecutionPath } from './framework'
import { defineTool, resolveWorkingPath } from './framework'
const pageParam = z.number().describe('Page ID (from list_pages)')
const elementParam = z
@@ -27,7 +27,7 @@ export const save_pdf = defineTool({
path: z.string(),
}),
handler: async (args, ctx, response) => {
const resolvedPath = resolveExecutionPath(ctx, args.path, args.cwd)
const resolvedPath = resolveWorkingPath(ctx, args.path, args.cwd)
const { data } = await ctx.browser.printToPDF(args.page)
await Bun.write(resolvedPath, Buffer.from(data, 'base64'))
response.text(`Saved PDF to ${resolvedPath}`)
@@ -77,7 +77,7 @@ export const save_screenshot = defineTool({
fullPage: z.boolean(),
}),
handler: async (args, ctx, response) => {
const resolvedPath = resolveExecutionPath(ctx, args.path, args.cwd)
const resolvedPath = resolveWorkingPath(ctx, args.path, args.cwd)
const { data } = await ctx.browser.screenshot(args.page, {
format: args.format,
quality: args.quality,
@@ -120,10 +120,10 @@ export const download_file = defineTool({
destinationPath: z.string(),
}),
handler: async (args, ctx, response) => {
const resolvedDir = resolveExecutionPath(ctx, args.path, args.cwd)
await mkdir(ctx.directories.executionDir, { recursive: true })
const resolvedDir = resolveWorkingPath(ctx, args.path, args.cwd)
await mkdir(ctx.directories.workingDir, { recursive: true })
const tempDir = await mkdtemp(
join(ctx.directories.executionDir, 'browseros-dl-'),
join(ctx.directories.workingDir, 'browseros-dl-'),
)
try {

View File

@@ -86,7 +86,7 @@ export async function withBrowser(
args,
{
browser,
directories: { executionDir: process.cwd() },
directories: { workingDir: process.cwd() },
},
signal,
)

View File

@@ -311,7 +311,7 @@ function agentConfig(
conversationId: 'test-conversation',
provider: LLM_PROVIDERS.OPENROUTER,
model: 'moonshotai/kimi-k2.5',
sessionExecutionDir: '/tmp/browseros-tests',
workingDir: '/tmp/browseros-tests',
...overrides,
}
}

View File

@@ -10,10 +10,12 @@ export const PATHS = {
DEFAULT_EXECUTION_DIR: process.cwd(),
BROWSEROS_DIR_NAME: '.browseros',
MEMORY_DIR_NAME: 'memory',
SESSIONS_DIR_NAME: 'sessions',
TOOL_OUTPUT_DIR_NAME: 'tool-output',
SOUL_FILE_NAME: 'SOUL.md',
CORE_MEMORY_FILE_NAME: 'CORE.md',
SKILLS_DIR_NAME: 'skills',
SOUL_MAX_LINES: 150,
MEMORY_RETENTION_DAYS: 30,
SESSION_RETENTION_DAYS: 30,
} as const