mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* feat(app): pin projects to top of the sidebar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): dedicated Pinned sidebar section with UX fixes Replaces the invisible float-in-place pinning with a Codex-style Pinned section at the top of the sidebar that hoists pinned chats and projects. - Pin-aware keyboard shortcut numbering (badges match visual order) - Pinned chats show their project name as subtitle (context after hoisting) - Pinned entries ordered most-recently-pinned first - Collapsible Pinned section (persisted) - Double-click guard on workspace pin toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): move Pinned section above the Workspaces header Pinned now sits at the top of the scroll, above the "Workspaces" section header (which moves into the scroll below it), matching the market-standard sidebar layout. Chevron sits beside the "Pinned" label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): reveal Pinned collapse chevron on hover only Extracts PinnedSectionHeader with its own hover state; the chevron hides until hover on web and stays visible on touch/compact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): pin the active chat via keyboard shortcut and inline pin button Adds a ⌘⇧P / Ctrl+Shift+P shortcut that pins the selected chat, mirroring the worktree.archive action across actions/dispatcher/route-shortcut, gated by `selected && supportsWorkspacePinning`. Also surfaces an inline pin button on row hover (always-on for touch) alongside the kebab in the shared trailing overlay, swapping Pin/PinOff by pinned state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): unify project and workspace pin handlers Collapses the two near-identical pin.set handlers into a shared handlePinSetRequest that owns control flow, error handling and the workspace-update fan-out; each caller only supplies its registry apply step and typed response payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): drop empty pinned project shells and empty Workspaces header Pinning every chat of a project hoisted them into Pinned but left an empty project header (plus its new-workspace ghost row) below, duplicating the project. Skip projects emptied by pinning while keeping genuinely empty projects. Also hide the "Workspaces" section header when no unpinned projects remain, so it no longer floats above nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(protocol): correct workspacePinning COMPAT version to v0.1.105 Feature ships in 0.1.105 per the changelog, not the placeholder v0.1.103. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hide redundant chat pin control inside pinned projects A pinned project already hoists all its chats to the top, so the per-chat pin was a no-op that just toggled a dead icon. Suppress the inline pin button, the kebab pin item, and the pin keyboard shortcut on chats whose project is pinned; they still work for chats in unpinned projects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hide project pin control on empty projects Project pin state is derived from workspace descriptors, so a project with no workspaces would set pinnedAt on the server but emit no descriptor update — the pin silently never surfaced and the user got no feedback. Offer the pin only once the project has a chat, matching how chat pins are suppressed inside already-pinned projects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): drop collapsed Pinned rows from keyboard shortcut numbering When the Pinned section is collapsed its chats and projects are hidden, but the shortcut model kept counting them: visible unpinned rows showed off-by-N badge numbers and pressing a number key jumped to a hidden pinned chat. Exclude the pinned rows from numbering when Pinned is collapsed, matching what renders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hydrate pinned sidebar rows * fix(app): keep filtered workspace controls reachable * feat(app): scope sidebar pinning to chats, drop project pinning Per PR review: pinning whole projects overlapped with project reordering and mixed projects into the pinned list. Keep chat pinning only. - Remove the project.pin.set RPC, its server handler, the projectPinnedAt descriptor field, and the client setProjectPinned method. - Drop the project pin control, isProjectPinned reads, and the suppressPin plumbing (its only source — a pinned project — is gone). - Fold pin-aware shortcut ordering into SidebarModelProvider (the live shortcut owner) and delete the orphaned use-sidebar-shortcut-model hook, so pinned chats and their shortcut numbers share one projection. - Prune the now-unused project pin i18n keys across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): inline the pin handler into the workspace path With project pinning gone, handlePinSetRequest had a single caller. Fold it back into handleWorkspacePinSetRequest and drop the project/workspace generalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert: keep main's use-sidebar-shortcut-model untouched This hook (and its test) already live on main. The scope-down had deleted them, but that's out of scope for this PR — the pin-aware shortcut ordering lives in SidebarModelProvider now, so main's hook is left exactly as it was. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(protocol): align workspacePinning COMPAT version in websocket-server Match the v0.1.105 version already corrected in messages.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): reshape pinned sidebar updates * chore(app): remove stale pin button styles * refactor(app): simplify pinned workspace rows --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
541 lines
41 KiB
Markdown
541 lines
41 KiB
Markdown
# Data Model
|
|
|
|
Paseo uses **file-based JSON persistence** instead of a traditional database. All data is validated at runtime with Zod schemas. Most stores write atomically (write to temp file, then rename); a few still use plain `writeFile` — see each section. There is no schema-versioning/migration framework — schemas rely on optional fields with defaults for forward compatibility, with a small amount of inline normalization in `persisted-config.ts` for legacy provider/speech entries.
|
|
|
|
All server-side stores live under `$PASEO_HOME` (defaults to `~/.paseo`).
|
|
|
|
## Store Surface Rules
|
|
|
|
Store APIs own persistence atomicity and should not make services coordinate raw reads and writes. A good store method maps cleanly to one SQL statement or one SQL transaction, even when the current implementation is JSON files. If a caller needs a queue, lock, read-merge-write loop, or uniqueness race workaround, that behavior belongs behind the store surface.
|
|
|
|
---
|
|
|
|
## Directory layout
|
|
|
|
```
|
|
$PASEO_HOME/
|
|
├── config.json # Daemon configuration
|
|
├── server-id # Stable daemon identifier (plain text, "srv_<base64url>")
|
|
├── daemon-keypair.json # E2EE keypair for relay (mode 0600)
|
|
├── paseo.pid # Daemon PID lock file
|
|
├── daemon.log # Default log file (path configurable)
|
|
├── agents/
|
|
│ └── {sanitized-cwd}/
|
|
│ └── {agentId}.json # One file per agent
|
|
├── schedules/
|
|
│ └── {scheduleId}.json # One file per schedule
|
|
├── chat/
|
|
│ └── rooms.json # All rooms + messages
|
|
├── loops/
|
|
│ └── loops.json # All loop records
|
|
├── projects/
|
|
│ ├── projects.json # Project registry
|
|
│ └── workspaces.json # Workspace registry
|
|
├── runtime/
|
|
│ └── managed-processes/
|
|
│ └── {recordId}.json # Helper processes owned by Paseo; reconciled on daemon bootstrap
|
|
└── push-tokens.json # Expo push notification tokens
|
|
```
|
|
|
|
The `agents/{sanitized-cwd}/` directory name is derived from the agent's `cwd` by stripping the filesystem root and replacing path separators with `-` (Windows drive letters become a `C-` style prefix). Persistent server stores write atomically by writing a temp file in the target directory and then renaming it into place.
|
|
|
|
---
|
|
|
|
## 1. Agent Record
|
|
|
|
**Path:** `$PASEO_HOME/agents/{project-dir}/{agentId}.json`
|
|
|
|
Each agent is stored as a separate JSON file, grouped by project directory.
|
|
|
|
| Field | Type | Description |
|
|
| -------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `id` | `string` | UUID, primary key |
|
|
| `provider` | `string` | Agent provider (`"claude"`, `"codex"`, `"opencode"`, etc.) |
|
|
| `cwd` | `string` | Working directory the agent operates in |
|
|
| `workspaceId` | `string?` | Owning workspace id — the single source of ownership. Every agent is stamped with one at create time; legacy cwd-only records are backfilled once by `migrations/backfill-workspace-id.migration.ts` (the only place a cwd→id mapping exists). Runtime code never infers ownership or status from cwd: status is computed per `workspaceId`, and same-cwd siblings are independent. |
|
|
| `createdAt` | `string` (ISO 8601) | Creation timestamp |
|
|
| `updatedAt` | `string` (ISO 8601) | Last update timestamp |
|
|
| `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp |
|
|
| `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp |
|
|
| `title` | `string?` | User-visible title |
|
|
| `labels` | `Record<string, string>` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for `create_agent` subagent relationships — see [agent-lifecycle.md](./agent-lifecycle.md) |
|
|
| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` |
|
|
| `lastModeId` | `string?` | Last active mode ID |
|
|
| `config` | `SerializableConfig?` | Agent session configuration (see below) |
|
|
| `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) |
|
|
| `features` | `AgentFeature[]?` | Provider-reported features (toggles/selects) |
|
|
| `persistence` | `PersistenceHandle?` | Handle for resuming sessions |
|
|
| `lastError` | `string?` (nullable) | Last error message, if any |
|
|
| `requiresAttention` | `boolean?` | Whether the agent needs user attention |
|
|
| `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed |
|
|
| `attentionTimestamp` | `string?` (ISO 8601) | When attention was flagged |
|
|
| `internal` | `boolean?` | Whether this is a system-internal agent (loop workers, etc.) |
|
|
| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp |
|
|
|
|
### Nested: SerializableConfig
|
|
|
|
| Field | Type | Description |
|
|
| ------------------ | -------------------------- | ---------------------------- |
|
|
| `title` | `string?` | Configured title |
|
|
| `modeId` | `string?` | Configured mode |
|
|
| `model` | `string?` | Configured model |
|
|
| `thinkingOptionId` | `string?` | Thinking/reasoning level |
|
|
| `featureValues` | `Record<string, unknown>?` | Feature preference overrides |
|
|
| `extra` | `Record<string, any>?` | Provider-specific config |
|
|
| `systemPrompt` | `string?` | Custom system prompt |
|
|
| `mcpServers` | `Record<string, any>?` | MCP server configurations |
|
|
|
|
### Nested: RuntimeInfo
|
|
|
|
| Field | Type | Description |
|
|
| ------------------ | -------------------------- | ------------------------------ |
|
|
| `provider` | `string` | Active provider |
|
|
| `sessionId` | `string?` | Active session ID |
|
|
| `model` | `string?` | Active model |
|
|
| `thinkingOptionId` | `string?` | Active thinking option |
|
|
| `modeId` | `string?` | Active mode |
|
|
| `extra` | `Record<string, unknown>?` | Provider-specific runtime data |
|
|
|
|
### Nested: PersistenceHandle
|
|
|
|
| Field | Type | Description |
|
|
| -------------- | ---------------------- | --------------------------------------------------------------------- |
|
|
| `provider` | `string` | Provider that owns the session |
|
|
| `sessionId` | `string` | Session ID for resumption |
|
|
| `nativeHandle` | `any?` | Provider-specific handle (Codex thread ID, Claude resume token, etc.) |
|
|
| `metadata` | `Record<string, any>?` | Extra metadata |
|
|
|
|
### Nested: AgentFeature (discriminated union on `type`)
|
|
|
|
**Toggle:**
|
|
|
|
| Field | Type |
|
|
| ------------- | ---------- |
|
|
| `type` | `"toggle"` |
|
|
| `id` | `string` |
|
|
| `label` | `string` |
|
|
| `description` | `string?` |
|
|
| `tooltip` | `string?` |
|
|
| `icon` | `string?` |
|
|
| `value` | `boolean` |
|
|
|
|
**Select:**
|
|
|
|
| Field | Type |
|
|
| ------------- | --------------------- |
|
|
| `type` | `"select"` |
|
|
| `id` | `string` |
|
|
| `label` | `string` |
|
|
| `description` | `string?` |
|
|
| `tooltip` | `string?` |
|
|
| `icon` | `string?` |
|
|
| `value` | `string \| null` |
|
|
| `options` | `AgentSelectOption[]` |
|
|
|
|
---
|
|
|
|
## Runtime-only Terminal Sessions
|
|
|
|
Terminals are live daemon state, not persisted JSON records. A terminal carries a `workspaceId` while it is running; workspace-scoped terminal lists include only terminals with the matching `workspaceId`. Legacy live terminals without an owner remain visible to unscoped terminal reads but contribute to no workspace status.
|
|
|
|
Terminal activity contributes to the workspace status bucket **per `workspaceId`**: a working terminal drives `running` onto the workspace it carries only. Same-`cwd` siblings are untouched; terminal visibility is likewise `workspaceId`-scoped.
|
|
|
|
---
|
|
|
|
## 2. Daemon Configuration
|
|
|
|
**Path:** `$PASEO_HOME/config.json`
|
|
|
|
Single file, validated with `PersistedConfigSchema`.
|
|
|
|
```
|
|
{
|
|
version: 1,
|
|
daemon: {
|
|
listen: "127.0.0.1:6767",
|
|
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
|
|
trustedProxies: true | string[], // defaults to ["loopback"]; Express proxy names/CIDRs
|
|
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
|
appendSystemPrompt: string, // appended to supported provider system/developer prompts
|
|
cors: { allowedOrigins: string[] },
|
|
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
|
|
auth: { password: string } // bcrypt hash, optional
|
|
},
|
|
app: {
|
|
baseUrl: string
|
|
},
|
|
worktrees?: {
|
|
root?: string // optional root for new worktrees; defaults to $PASEO_HOME/worktrees
|
|
},
|
|
providers: {
|
|
openai: {
|
|
apiKey?: string,
|
|
baseUrl?: string,
|
|
stt?: { apiKey?: string, baseUrl?: string },
|
|
tts?: { apiKey?: string, baseUrl?: string }
|
|
},
|
|
local: { modelsDir: string }
|
|
},
|
|
agents: {
|
|
// ProviderOverrideSchema; legacy entries with `command: { mode, ... }` are migrated to the
|
|
// current shape on load via `migrateProviderSettings`. Custom provider IDs must declare
|
|
// `extends` (one of the built-ins or `"acp"`) and `label`. See `provider-launch-config.ts`.
|
|
providers: Record<providerId, ProviderOverride>,
|
|
metadataGeneration: {
|
|
providers: [{ provider, model?, thinkingOptionId? }]
|
|
}
|
|
},
|
|
features: {
|
|
dictation: { enabled, stt: { provider, model, language, confidenceThreshold } },
|
|
voiceMode: { enabled, llm, stt: { provider, model, language }, turnDetection, tts: { provider, model, voice, speakerId, speed } }
|
|
},
|
|
log: {
|
|
level, format,
|
|
console: { level, format },
|
|
file: { level, path, rotate: { maxSize, maxFiles } }
|
|
}
|
|
}
|
|
```
|
|
|
|
All fields are optional with sensible defaults.
|
|
|
|
`agents.metadataGeneration.providers` controls the preferred structured-generation fallback order for daemon-side metadata tasks such as commit messages, PR text, branch names, and generated agent titles. Entries are tried first in the configured order, then Paseo falls through to dynamically discovered defaults and finally the current selection when available.
|
|
|
|
Local speech model ids are intentionally narrow: STT uses `parakeet-tdt-0.6b-v2-int8`, TTS uses `kokoro-en-v0_19`, and turn detection uses the bundled Silero VAD model.
|
|
|
|
Set these to select OpenAI instead of local speech:
|
|
|
|
| Env var | Applies to |
|
|
| ------------------------------ | ------------------------------- |
|
|
| `PASEO_VOICE_STT_PROVIDER` | Voice mode STT provider |
|
|
| `PASEO_DICTATION_STT_PROVIDER` | Composer dictation STT provider |
|
|
| `PASEO_VOICE_TTS_PROVIDER` | Voice mode TTS provider |
|
|
|
|
OpenAI speech can be configured under `providers.openai`. STT and TTS resolve independently, so they can point at different endpoints:
|
|
|
|
```json
|
|
{
|
|
"providers": {
|
|
"openai": {
|
|
"stt": {
|
|
"apiKey": "sk-...",
|
|
"baseUrl": "https://stt.example.com/v1"
|
|
},
|
|
"tts": {
|
|
"apiKey": "sk-...",
|
|
"baseUrl": "https://api.openai.com/v1"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`providers.openai.stt` is used for both composer dictation and voice mode speech-to-text; `providers.openai.tts` is used for voice mode text-to-speech. The equivalent env vars are `OPENAI_STT_API_KEY`/`OPENAI_STT_BASE_URL` and `OPENAI_TTS_API_KEY`/`OPENAI_TTS_BASE_URL`. Each feature falls back to `providers.openai.apiKey`/`providers.openai.baseUrl`, then `OPENAI_API_KEY`/`OPENAI_BASE_URL`, when its own fields are unset. These settings apply only to Paseo OpenAI speech features, not to Codex or other OpenAI-backed tools.
|
|
|
|
Paseo uses these paths under the configured OpenAI base URL:
|
|
|
|
- dictation STT: `/v1/audio/transcriptions`
|
|
- voice mode STT: `/v1/audio/transcriptions`
|
|
- voice mode TTS: `/v1/audio/speech`
|
|
|
|
---
|
|
|
|
## 3. Schedule
|
|
|
|
**Path:** `$PASEO_HOME/schedules/{id}.json`
|
|
|
|
One file per schedule. ID is 8 hex characters.
|
|
|
|
| Field | Type | Description |
|
|
| ----------- | ------------------------------------- | -------------------------------- |
|
|
| `id` | `string` | 8-char hex ID |
|
|
| `name` | `string?` | Human-readable name |
|
|
| `prompt` | `string` | The prompt to send |
|
|
| `cadence` | `ScheduleCadence` | Timing (see below) |
|
|
| `target` | `ScheduleTarget` | What to run (see below) |
|
|
| `status` | `"active" \| "paused" \| "completed"` | Current state |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
| `updatedAt` | `string` (ISO 8601) | |
|
|
| `nextRunAt` | `string?` (ISO 8601) | Next scheduled execution |
|
|
| `lastRunAt` | `string?` (ISO 8601) | Last execution time |
|
|
| `pausedAt` | `string?` (ISO 8601) | When paused |
|
|
| `expiresAt` | `string?` (ISO 8601) | Auto-expire time |
|
|
| `maxRuns` | `number?` | Max executions before completing |
|
|
| `runs` | `ScheduleRun[]` | Execution history |
|
|
|
|
### Nested: ScheduleCadence (discriminated union on `type`)
|
|
|
|
- `{ type: "every", everyMs: number }` — interval in milliseconds
|
|
- `{ type: "cron", expression: string, timezone?: string }` — cron expression; absent `timezone` means UTC, present `timezone` is an IANA time zone used for local wall-clock recurrence
|
|
|
|
### Nested: ScheduleTarget (discriminated union on `type`)
|
|
|
|
- `{ type: "agent", agentId: string }` — send to existing agent
|
|
- `{ type: "new-agent", config: { provider, cwd, modeId?, model?, thinkingOptionId?, title?, approvalPolicy?, sandboxMode?, networkAccess?, webSearch?, extra?, systemPrompt?, mcpServers? } }` — create a new agent
|
|
|
|
### Nested: ScheduleRun
|
|
|
|
| Field | Type | Description |
|
|
| -------------- | -------------------------------------- | ----------------------- |
|
|
| `id` | `string` | Run ID |
|
|
| `scheduledFor` | `string` (ISO 8601) | Intended execution time |
|
|
| `startedAt` | `string` (ISO 8601) | |
|
|
| `endedAt` | `string?` (ISO 8601) | |
|
|
| `status` | `"running" \| "succeeded" \| "failed"` | |
|
|
| `agentId` | `string?` (UUID) | Agent used for this run |
|
|
| `output` | `string?` | Agent output text |
|
|
| `error` | `string?` | Error message if failed |
|
|
|
|
---
|
|
|
|
## 4. Chat
|
|
|
|
**Path:** `$PASEO_HOME/chat/rooms.json`
|
|
|
|
Single file containing all rooms and messages.
|
|
|
|
```json
|
|
{
|
|
"rooms": [ ... ],
|
|
"messages": [ ... ]
|
|
}
|
|
```
|
|
|
|
### ChatRoom
|
|
|
|
| Field | Type | Description |
|
|
| ----------- | ------------------- | ----------------------------------- |
|
|
| `id` | `string` (UUID) | |
|
|
| `name` | `string` | Unique room name (case-insensitive) |
|
|
| `purpose` | `string?` | Room description |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
| `updatedAt` | `string` (ISO 8601) | Updated on each new message |
|
|
|
|
### ChatMessage
|
|
|
|
| Field | Type | Description |
|
|
| ------------------ | ------------------- | ----------------------------------- |
|
|
| `id` | `string` (UUID) | |
|
|
| `roomId` | `string` | FK to ChatRoom.id |
|
|
| `authorAgentId` | `string` | Agent ID of the author |
|
|
| `body` | `string` | Message text (supports `@mentions`) |
|
|
| `replyToMessageId` | `string?` | FK to another ChatMessage.id |
|
|
| `mentionAgentIds` | `string[]` | Extracted `@mention` agent IDs |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
|
|
---
|
|
|
|
## 5. Loop
|
|
|
|
**Path:** `$PASEO_HOME/loops/loops.json`
|
|
|
|
Single file containing an array of all loop records. Writes are direct (not atomic) and serialized through an in-memory queue. On daemon startup any record with `status: "running"` is recovered as `"stopped"` with an interruption log entry.
|
|
|
|
| Field | Type | Description |
|
|
| ----------------------- | --------------------------------------------------- | ------------------------------------------ |
|
|
| `id` | `string` | 8-char UUID prefix |
|
|
| `name` | `string?` | Human-readable name |
|
|
| `prompt` | `string` | Worker prompt |
|
|
| `cwd` | `string` | Working directory |
|
|
| `provider` | `string` | Default provider |
|
|
| `model` | `string?` | Default model |
|
|
| `modeId` | `string?` | Default mode ID |
|
|
| `workerProvider` | `string?` | Override provider for workers |
|
|
| `workerModel` | `string?` | Override model for workers |
|
|
| `verifierProvider` | `string?` | Override provider for verifiers |
|
|
| `verifierModel` | `string?` | Override model for verifiers |
|
|
| `verifierModeId` | `string?` | Override mode ID for verifiers |
|
|
| `verifyPrompt` | `string?` | LLM verification prompt |
|
|
| `verifyChecks` | `string[]` | Shell commands to run as checks |
|
|
| `archive` | `boolean` | Whether to archive worker agents after use |
|
|
| `sleepMs` | `number` | Delay between iterations (ms) |
|
|
| `maxIterations` | `number?` | Cap on iterations |
|
|
| `maxTimeMs` | `number?` | Total time budget (ms) |
|
|
| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
| `updatedAt` | `string` (ISO 8601) | |
|
|
| `startedAt` | `string` (ISO 8601) | |
|
|
| `completedAt` | `string?` (ISO 8601) | |
|
|
| `stopRequestedAt` | `string?` (ISO 8601) | |
|
|
| `iterations` | `LoopIteration[]` | |
|
|
| `logs` | `LoopLogEntry[]` | |
|
|
| `nextLogSeq` | `number` | Monotonic log sequence counter |
|
|
| `activeIteration` | `number?` | Currently executing iteration index |
|
|
| `activeWorkerAgentId` | `string?` | Currently running worker agent |
|
|
| `activeVerifierAgentId` | `string?` | Currently running verifier agent |
|
|
|
|
### Nested: LoopIteration
|
|
|
|
| Field | Type | Description |
|
|
| ------------------- | --------------------------------------------------- | ------------------------ |
|
|
| `index` | `number` | 1-based iteration index |
|
|
| `workerAgentId` | `string?` | Agent ID of the worker |
|
|
| `workerStartedAt` | `string` (ISO 8601) | |
|
|
| `workerCompletedAt` | `string?` (ISO 8601) | |
|
|
| `verifierAgentId` | `string?` | Agent ID of the verifier |
|
|
| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | |
|
|
| `workerOutcome` | `"completed" \| "failed" \| "canceled"?` | |
|
|
| `failureReason` | `string?` | |
|
|
| `verifyChecks` | `LoopVerifyCheckResult[]` | Shell check results |
|
|
| `verifyPrompt` | `LoopVerifyPromptResult?` | LLM verification result |
|
|
|
|
### Nested: LoopLogEntry
|
|
|
|
| Field | Type |
|
|
| ----------- | ---------------------------------------------------- |
|
|
| `seq` | `number` (monotonic) |
|
|
| `timestamp` | `string` (ISO 8601) |
|
|
| `iteration` | `number?` |
|
|
| `source` | `"loop" \| "worker" \| "verifier" \| "verify-check"` |
|
|
| `level` | `"info" \| "error"` |
|
|
| `text` | `string` |
|
|
|
|
### Nested: LoopVerifyCheckResult
|
|
|
|
| Field | Type |
|
|
| ------------- | ------------------- |
|
|
| `command` | `string` |
|
|
| `exitCode` | `number` |
|
|
| `passed` | `boolean` |
|
|
| `stdout` | `string` |
|
|
| `stderr` | `string` |
|
|
| `startedAt` | `string` (ISO 8601) |
|
|
| `completedAt` | `string` (ISO 8601) |
|
|
|
|
### Nested: LoopVerifyPromptResult
|
|
|
|
| Field | Type |
|
|
| ----------------- | ------------------- |
|
|
| `passed` | `boolean` |
|
|
| `reason` | `string` |
|
|
| `verifierAgentId` | `string?` |
|
|
| `startedAt` | `string` (ISO 8601) |
|
|
| `completedAt` | `string` (ISO 8601) |
|
|
|
|
---
|
|
|
|
## 6. Project Registry
|
|
|
|
**Path:** `$PASEO_HOME/projects/projects.json`
|
|
|
|
Array of project records.
|
|
|
|
| Field | Type | Description |
|
|
| ------------- | --------------------------- | -------------------------------------------------------------------------------- |
|
|
| `projectId` | `string` | Primary key |
|
|
| `rootPath` | `string` | Filesystem root of the project |
|
|
| `kind` | `"git" \| "non_git"` | |
|
|
| `displayName` | `string` | |
|
|
| `customName` | `string \| null` | User-set override layered over `displayName`. Null means "use the derived name". |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
| `updatedAt` | `string` (ISO 8601) | |
|
|
| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete timestamp; required nullable |
|
|
|
|
Active git projects are unique by normalized `rootPath`. Startup reconciliation repairs older bad
|
|
states by moving workspaces from duplicate path-keyed projects onto the canonical project,
|
|
preferring remote-keyed project IDs such as `remote:github.com/owner/repo`, then archiving the
|
|
emptied duplicate.
|
|
|
|
---
|
|
|
|
## 7. Workspace Registry
|
|
|
|
**Path:** `$PASEO_HOME/projects/workspaces.json`
|
|
|
|
Array of workspace records. A workspace is a specific working directory within a project.
|
|
|
|
| Field | Type | Description |
|
|
| ------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `workspaceId` | `string` | Opaque stable identifier (`wks_<hex>`), generated independently of the directory. MUST NOT be treated as a path; compare by exact equality. Use the `cwd` field for directory access. |
|
|
| `projectId` | `string` | FK to Project.projectId |
|
|
| `cwd` | `string` | Filesystem path |
|
|
| `kind` | `"local_checkout" \| "worktree" \| "directory"` | |
|
|
| `displayName` | `string` | The human name (the generated/derived title). Decoupled from `branch` by construction. |
|
|
| `title` | `string \| null` | User-set name override layered over `displayName`. Null means "use `displayName`". |
|
|
| `branch` | `string \| null` | The worktree's git branch. Separate from `displayName`/`title`; only worktree workspaces set it. A branch rename writes this and never the name. |
|
|
| `createdAt` | `string` (ISO 8601) | |
|
|
| `updatedAt` | `string` (ISO 8601) | |
|
|
| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete; required nullable |
|
|
| `pinnedAt` | `string \| null` (ISO 8601) | Pinned-to-top-of-sidebar timestamp; null means "not pinned" |
|
|
|
|
> **Opaque-ID invariant:** `workspaceId` is opaque identity, never a filesystem path. Filesystem and git operations take `cwd`/`workspaceDirectory` only — never the id. Path-derived grouping keys (e.g. `deriveWorkspaceDirectoryKey`, used at bootstrap to group agents into a workspace) are directory keys, not workspace identity, and must not be persisted or compared as ids.
|
|
|
|
`projectId` is still a real FK: workspace records should have a matching project record. Read-only
|
|
history surfaces tolerate transient orphaned workspaces by omitting those rows so one bad FK cannot
|
|
blank the whole History screen, but mutation paths should repair or remove the orphaned state rather
|
|
than treating it as valid.
|
|
|
|
---
|
|
|
|
## 8. Push Token Store
|
|
|
|
**Path:** `$PASEO_HOME/push-tokens.json`
|
|
|
|
```json
|
|
{
|
|
"tokens": ["ExponentPushToken[...]", ...]
|
|
}
|
|
```
|
|
|
|
Simple set of Expo push notification tokens. Loaded with permissive parsing (filters non-string entries). Persisted with atomic temp-file rename.
|
|
|
|
---
|
|
|
|
## 9. Daemon meta files
|
|
|
|
These small files are not validated as full Zod schemas but are persisted under `$PASEO_HOME` for daemon identity and runtime coordination.
|
|
|
|
| Path | Format | Notes |
|
|
| --------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
| `server-id` | Plain text, e.g. `srv_<base64url>` | Stable per-`$PASEO_HOME` daemon ID. Overridable via `PASEO_SERVER_ID` env. |
|
|
| `daemon-keypair.json` | `{ v: 2, publicKeyB64, secretKeyB64 }` (libsodium box keypair) | E2EE relay identity. Written with mode `0600`. Regenerated if file is unreadable. |
|
|
| `paseo.pid` | JSON `{ pid, startedAt, ... }` | PID lock; prevents two daemons sharing one `$PASEO_HOME`. |
|
|
| `daemon.log` | Pino log output | Default location; path/rotation configurable via `log.file` in `config.json`. |
|
|
|
|
---
|
|
|
|
## Client-side stores (App)
|
|
|
|
These live in React Native `AsyncStorage` or browser `IndexedDB`, not on the daemon filesystem.
|
|
|
|
### Keying convention: directory-backed vs workspace-owned
|
|
|
|
Right-sidebar client state splits on whether it is determined by the directory or owned by the workspace (two workspaces can share one `cwd`). The split is enforced by the cache key, so changing a key changes the sharing semantics — see [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned) for the full table.
|
|
|
|
- **Directory-backed** (shared by same-`cwd` workspaces): keyed by `(serverId, cwd)`. Git status/diff, GitHub PR status, PR timeline, file preview content. These are TanStack Query caches, not persisted stores.
|
|
- **Workspace-owned** (independent per workspace): keyed by `workspaceId`, with `cwd` used only as a fallback when no `workspaceId` is present. Review draft comments (`@paseo:review-draft-store`), diff-mode overrides (in-memory), workspace composer attachments, and file-explorer nav/expand state. The `workspaceId` part of these keys is **opaque** — never parse it back into a path.
|
|
|
|
### Draft Store
|
|
|
|
**AsyncStorage key:** `paseo-drafts` (version 2)
|
|
|
|
```typescript
|
|
{
|
|
drafts: Record<draftKey, {
|
|
input: { text: string, images: AttachmentMetadata[] },
|
|
lifecycle: "active" | "abandoned" | "sent",
|
|
updatedAt: number, // epoch ms
|
|
version: number // optimistic concurrency
|
|
}>,
|
|
createModalDraft: DraftRecord | null
|
|
}
|
|
```
|
|
|
|
### Attachment Store (Web)
|
|
|
|
**IndexedDB database:** `paseo-attachment-bytes`, object store: `attachments`
|
|
|
|
Stores binary attachment blobs keyed by attachment ID.
|
|
|
|
### AttachmentMetadata
|
|
|
|
| Field | Type | Description |
|
|
| ------------- | --------- | ------------------------------ |
|
|
| `id` | `string` | Unique attachment ID |
|
|
| `mimeType` | `string` | MIME type |
|
|
| `storageType` | `string` | Storage backend identifier |
|
|
| `storageKey` | `string` | Key within the storage backend |
|
|
| `createdAt` | `number` | Epoch ms |
|
|
| `fileName` | `string?` | Original filename |
|
|
| `byteSize` | `number?` | Size in bytes |
|