mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 04:03:05 +00:00
chore(merge): integrate main into workspace runtimes
This commit is contained in:
+26
-10
@@ -44,11 +44,26 @@ Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Age
|
||||
- **Subagents** — exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it.
|
||||
- **Detached agents** — stand on their own after an explicit detach transition, do not appear in the former parent's subagent track, and are not archived with it.
|
||||
|
||||
Parent archive detaches a subagent instead of archiving it when either condition holds:
|
||||
|
||||
- The child belongs to another workspace.
|
||||
- The child is currently open in an agent tab.
|
||||
|
||||
All other children archive with the parent. After the workspace layout hydrates, the client marks
|
||||
every managed subagent present in its tabs with `paseo.open-agent-tab.<client-id>=true` through the
|
||||
generic agent metadata update. This includes background and restored tabs; navigation does not own
|
||||
the marker. Closing a tab sets that client's label to `false`. Any `true` client label keeps the child
|
||||
open. Detach clears the parent and every open-tab label. The surviving child therefore becomes a
|
||||
normal root agent immediately, and closing its still-open tab archives it.
|
||||
|
||||
Runtime ownership is resolved from explicit workspace ID and caller context, never from `cwd`. Workspace creation is a separate operation with `local | worktree` isolation; agent creation only selects an existing workspace.
|
||||
|
||||
Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the `paseo.parent-agent-id` label only: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive.
|
||||
Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the parent and open-tab lifecycle labels: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive.
|
||||
|
||||
`notifyOnFinish` defaults to `true` for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts.
|
||||
Permission requests are notification checkpoints, not the end of that subscription. The caller is notified again after a permission response when the child finishes, errors, or requests another permission.
|
||||
The permission notification includes the normalized request plus the child and request IDs, so the caller can inspect it and respond without fetching agent status.
|
||||
A watched child that closes before its finish event also notifies the caller so delegated work cannot disappear silently during archive or workspace teardown.
|
||||
|
||||
## Provider-managed child agents
|
||||
|
||||
@@ -71,7 +86,7 @@ Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/
|
||||
2. Set `archivedAt` and normalize `lastStatus` away from `running`/`initializing`
|
||||
3. Notify subscribers
|
||||
4. Close the runtime (kills the process if still running)
|
||||
5. **Cascade-archive children** — any agent whose `paseo.parent-agent-id` label matches the archived agent gets archived too, recursively
|
||||
5. **Resolve children** — detach cross-workspace and open-tab children; cascade-archive the rest recursively
|
||||
|
||||
Cascade is what keeps subagent fleets from outliving their orchestrator.
|
||||
|
||||
@@ -106,7 +121,7 @@ These are two distinct concepts that used to be conflated:
|
||||
|
||||
Closing a tab on a **root agent** still archives — the tab is the agent's home, so closing it means "I'm done with this agent." A confirm dialog protects against archiving a running agent by accident.
|
||||
|
||||
Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-only**. The agent stays unarchived and stays in its parent's track. The user can re-open the tab from the track at any time. This is implemented in `handleCloseAgentTab` (`packages/app/src/screens/workspace/workspace-screen.tsx`).
|
||||
Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-only**. The app clears the current client's open-tab label before removing the tab. Another client's open tab remains protected. The agent stays unarchived and stays in its parent's track, so a later parent archive cascades to it when no client still has it open. The user can re-open the tab from the track at any time. Single and bulk tab close apply the same policy.
|
||||
|
||||
The asymmetry is intentional: a subagent's persistent relationship lives in the parent's track. Same-workspace subagents are not auto-opened as tabs; the user opens one from that track when needed. A cross-workspace subagent is also auto-opened as a tab in its own workspace so opening that workspace does not appear empty. It remains in the parent's track until it is actually detached.
|
||||
|
||||
@@ -152,7 +167,7 @@ Archived Paseo subagents disappear from the track, by design. To remove one from
|
||||
|
||||
The track header's **Archive finished** action hides finished provider-owned rows in the current app session. Their native sessions and timelines are untouched, and managed Paseo subagents are not archived by this bulk action. If a hidden provider child starts running again, the app brings it back to the track.
|
||||
|
||||
To keep the agent alive but remove it from the parent's track, use **detach**. The daemon clears the parent label, emits the normal agent update, and every client reclassifies the agent from subagent to root/sibling from that updated snapshot.
|
||||
To keep the agent alive but remove it from the parent's track, use **detach**. The daemon clears the relationship lifecycle labels, emits the normal agent update, and every client reclassifies the agent from subagent to root/sibling from that updated snapshot.
|
||||
|
||||
## Why this shape
|
||||
|
||||
@@ -186,11 +201,12 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json
|
||||
|
||||
Each agent is a single JSON file. Fields relevant to this doc:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --------------------------------- | ------------- | ---------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | Stable identifier |
|
||||
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
||||
| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically for agent-scoped creation and removed by detach |
|
||||
| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` |
|
||||
| Field | Type | Meaning |
|
||||
| -------------------------------------------- | ------------- | ---------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | Stable identifier |
|
||||
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
||||
| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically for agent-scoped creation and removed by detach |
|
||||
| `labels["paseo.open-agent-tab.<client-id>"]` | `string?` | `"true"` protects an open tab on that client; detach clears every matching label |
|
||||
| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` |
|
||||
|
||||
See [`docs/data-model.md`](./data-model.md) for the full agent record.
|
||||
|
||||
+17
-14
@@ -69,6 +69,7 @@ not retain non-Git directories.
|
||||
| `server/bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay |
|
||||
| `server/websocket-server.ts` | WebSocket connection management, hello handshake, binary frame routing |
|
||||
| `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations |
|
||||
| `server/directory-sync/` | Daemon-global latest-state sequences for projects, workspaces, and agents |
|
||||
| `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management |
|
||||
| `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` |
|
||||
| `server/agent/tools/` | Transport-neutral catalog for workspaces, agents, permissions, and automation |
|
||||
@@ -78,8 +79,6 @@ not retain non-Git directories.
|
||||
| `server/provider-probe/` | Invisible runtime-bound workspaces for pre-creation provider discovery |
|
||||
| `server/relay-transport.ts` | Outbound relay connection with E2E encryption |
|
||||
| `server/schedule/` | Cron-based scheduled agents |
|
||||
| `server/loop-service.ts` | Looping agent runs that retry until an exit condition |
|
||||
| `server/chat/` | Chat rooms for agent-to-agent and human-to-agent messaging |
|
||||
|
||||
### `packages/protocol` — Wire schemas and shared protocol types
|
||||
|
||||
@@ -114,19 +113,27 @@ Cross-platform React Native app that connects to one or more daemons.
|
||||
|
||||
- Expo Router navigation (`/h/[serverId]/workspace/[workspaceId]`, `/h/[serverId]/agent/[agentId]`, etc.). The `workspaceId` URL segment is an opaque workspace id, not a directly meaningful filesystem path.
|
||||
- `HostRuntimeController` manages saved host connections, reconnection, and per-host runtime state
|
||||
- `runtime/replica-cache` keeps a non-authoritative per-host display replica in AsyncStorage: only the last focused agent, its workspace, and a short timeline tail. It restores before navigation becomes ready, leaves remote hydration flags false, and is atomically replaced by the normal snapshot-plus-delta synchronization path.
|
||||
- `runtime/replica-cache` keeps the complete project, workspace, and active-agent directory plus one short focused timeline tail in AsyncStorage. It restores before navigation becomes ready and leaves remote hydration flags false.
|
||||
- `runtime/directory-sync` owns directory reconciliation. On reconnect it passes the persisted per-entity cursor through `project.list`, `fetch_workspaces`, and `fetch_agents`; the daemon returns each entity's latest projection when its sequence is newer, plus tombstones.
|
||||
- `SessionContext` wraps the daemon client for the active session
|
||||
- Composer UI and submit/draft behavior live in `packages/app/src/composer/`; screens and panels should integrate it from there instead of dropping composer internals into `components/`, `hooks/`, or `screens/workspace/`
|
||||
- Timeline reducers in `timeline/session-stream-reducers.ts` handle compaction, gap detection, sequence-based deduplication
|
||||
- Timeline sync correctness is documented in [docs/timeline-sync.md](timeline-sync.md): live streams are for immediacy, `fetch_agent_timeline_request` is authoritative, and catch-up is paged but complete.
|
||||
- Voice features: dictation (STT) and voice agent (realtime)
|
||||
|
||||
The replica cache exists only to paint stale data immediately while the host connects. It does not
|
||||
own mutations, infer deletions, or replace daemon reconciliation. Pending permission requests are
|
||||
not restored from it. AsyncStorage is not encrypted, so the cached timeline tail may contain source
|
||||
code, prompts, and tool output; encrypted-at-rest storage is a separate product/security decision.
|
||||
Its serialized payload has a 1 MiB byte budget and evicts whole host snapshots in least-recently-
|
||||
written order; a single oversized host is omitted rather than partially restored.
|
||||
The replica cache paints stale data immediately while the host connects. Directory cursors are
|
||||
reconciliation checkpoints; cached entities remain non-authoritative until the daemon answers.
|
||||
Pending permission requests are not restored from it. AsyncStorage is not encrypted, so the cached
|
||||
timeline tail may contain source code, prompts, and tool output; encrypted-at-rest storage is a
|
||||
separate product/security decision. Its serialized payload has a 32 MiB byte budget and evicts whole
|
||||
host snapshots in least-recently-written order; a single oversized host is omitted rather than
|
||||
partially restored. Browser and Electron builds store it in IndexedDB. Native builds use
|
||||
AsyncStorage, and Android reserves 64 MiB for that database.
|
||||
|
||||
The three directory entity types have independent monotonic sequences and share one daemon
|
||||
generation. The daemon retains only the latest projection per entity and bounded tombstones, not an
|
||||
event log. A missing, expired, or previous-generation cursor receives a full snapshot. Projects are
|
||||
independent records; a project with no workspaces does not need a workspace placeholder.
|
||||
|
||||
### `packages/cli` — Command-line client
|
||||
|
||||
@@ -134,13 +141,11 @@ Commander.js CLI with Docker-style commands. Common agent operations are also ex
|
||||
|
||||
- `paseo agent ls/run/import/attach/logs/stop/delete/send/inspect/wait/archive/reload/update/mode`
|
||||
- `paseo daemon start/stop/restart/status/pair/set-password`
|
||||
- `paseo chat ls/create/inspect/post/read/wait/delete`
|
||||
- `paseo terminal ls/create/capture/send-keys/kill`
|
||||
- `paseo script ls/start/stop`
|
||||
- `paseo loop run/ls/inspect/logs/stop`
|
||||
- `paseo schedule create/ls/inspect/update/pause/resume/run-once/logs/delete`
|
||||
- `paseo heartbeat create/update/delete`
|
||||
- `paseo workspace create/ls/archive`
|
||||
- `paseo workspace create/ls/rename/archive`
|
||||
- `paseo permit allow/deny/ls`
|
||||
- `paseo provider ls/models`
|
||||
- hidden legacy `paseo worktree create/ls/archive` compatibility alias
|
||||
@@ -399,9 +404,7 @@ $PASEO_HOME/
|
||||
├── projects/projects.json # Project registry
|
||||
├── projects/workspaces.json # Workspace registry
|
||||
├── projects/icons/ # Custom project icon images
|
||||
├── chat/ # Chat rooms
|
||||
├── schedules/ # Scheduled-agent definitions and runs
|
||||
├── loops/ # Loop runs and logs
|
||||
├── config.json # Daemon config (mutable)
|
||||
├── daemon-keypair.json # Daemon identity for relay/E2EE
|
||||
├── push-tokens.json # Mobile push tokens
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Paseo supports configuring custom agent providers through `config.json` (located at `$PASEO_HOME/config.json`, typically `~/.paseo/config.json`). You can extend built-in providers with different API backends, add ACP-compatible agents, set custom binaries, disable providers, and create multiple profiles for the same underlying provider.
|
||||
|
||||
All provider configuration lives under `agents.providers` in config.json:
|
||||
Provider definitions live under `agents.providers` in config.json:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -17,6 +17,20 @@ All provider configuration lives under `agents.providers` in config.json:
|
||||
|
||||
Provider IDs must be lowercase alphanumeric with hyphens (`/^[a-z][a-z0-9-]*$/`).
|
||||
|
||||
Each provider catalog refresh waits up to 2 minutes. If a provider loads many plugins or a large
|
||||
agent catalog during startup, raise the limit in milliseconds:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"catalogRefreshTimeoutMs": 180000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The limit applies independently to every provider refresh and covers availability plus the entire
|
||||
catalog probe. `PASEO_PROVIDER_REFRESH_TIMEOUT_MS` sets it when the config field is absent.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
@@ -245,6 +259,8 @@ requires_openai_auth = false
|
||||
|
||||
You can create multiple entries that extend the same built-in provider. Each gets its own entry in the provider list with independent credentials, models, and environment.
|
||||
|
||||
"Profile" here means a provider alias, and it is not an **Agent profile** — that is a named bundle of provider, model, mode, thinking option and features, stored under `daemon.agentProfiles`. See [glossary.md](glossary.md) for all four senses of the word.
|
||||
|
||||
Example: two different Anthropic accounts as separate profiles:
|
||||
|
||||
```json
|
||||
|
||||
+23
-136
@@ -58,10 +58,6 @@ $PASEO_HOME/
|
||||
│ └── {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
|
||||
@@ -93,7 +89,7 @@ Each agent is stored as a separate JSON file, grouped by project directory.
|
||||
| `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 agent-scoped creation and removed by detach — see [agent-lifecycle.md](./agent-lifecycle.md) |
|
||||
| `labels` | `Record<string, string>` | Key-value labels (default `{}`). Paseo uses `paseo.parent-agent-id` for parentage and client-scoped `paseo.open-agent-tab.*` labels while managed subagent tabs are open — see [agent-lifecycle.md](./agent-lifecycle.md) |
|
||||
| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"`. `closed` means the record is resumable but has no live provider runtime; archive remains represented separately by `archivedAt`. |
|
||||
| `lastModeId` | `string?` | Last active mode ID |
|
||||
| `config` | `SerializableConfig?` | Agent session configuration (see below) |
|
||||
@@ -104,7 +100,7 @@ Each agent is stored as a separate JSON file, grouped by project directory.
|
||||
| `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.) |
|
||||
| `internal` | `boolean?` | Whether this is a system-internal agent |
|
||||
| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp |
|
||||
|
||||
### Nested: SerializableConfig
|
||||
@@ -193,6 +189,8 @@ Single file, validated with `PersistedConfigSchema`.
|
||||
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
||||
git: { maxProcessesPerSecond: number, maxProcessConcurrency: number },
|
||||
appendSystemPrompt: string, // appended to supported provider system/developer prompts
|
||||
terminalProfiles: TerminalProfile[], // named shell commands; omitted means DEFAULT_TERMINAL_PROFILES
|
||||
agentProfiles: AgentProfile[], // named agent launch bundles; omitted means none
|
||||
cors: { allowedOrigins: string[] },
|
||||
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean }, // new homes materialize enabled: false
|
||||
auth: { password: string } // bcrypt hash, optional
|
||||
@@ -239,6 +237,21 @@ Single file, validated with `PersistedConfigSchema`.
|
||||
|
||||
All fields are optional with sensible defaults.
|
||||
|
||||
### Profile lists
|
||||
|
||||
`terminalProfiles` and `agentProfiles` are both whole-list fields: a config patch replaces the
|
||||
array, never merges entries, so a client sends the complete next list on every add, edit, reorder
|
||||
and remove. List order is the display order.
|
||||
|
||||
Absent and empty mean different things for terminal profiles — omitting the key falls back to
|
||||
`DEFAULT_TERMINAL_PROFILES`, while `[]` means the user removed them all. Agent profiles have no
|
||||
defaults, so both mean none.
|
||||
|
||||
`PersistedConfigSchema` parses strictly, so a daemon that predates a field drops it on write
|
||||
rather than storing something it cannot describe. That is why the client gates the agent profiles
|
||||
UI on `server_info.features.agentProfiles` instead of letting a save appear to succeed against an
|
||||
older daemon.
|
||||
|
||||
### Git process limits
|
||||
|
||||
Git process limits are global to one daemon. The start-rate limit defaults to `64` processes per
|
||||
@@ -362,133 +375,7 @@ One file per schedule. ID is 8 hex characters.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
## 4. Project Registry
|
||||
|
||||
**Path:** `$PASEO_HOME/projects/projects.json`
|
||||
|
||||
@@ -523,7 +410,7 @@ workspace together with its owning project.
|
||||
|
||||
---
|
||||
|
||||
## 7. Workspace Registry
|
||||
## 5. Workspace Registry
|
||||
|
||||
**Path:** `$PASEO_HOME/projects/workspaces.json`
|
||||
|
||||
@@ -564,7 +451,7 @@ than treating it as valid.
|
||||
|
||||
---
|
||||
|
||||
## 8. Push Token Store
|
||||
## 6. Push Token Store
|
||||
|
||||
**Path:** `$PASEO_HOME/push-tokens.json`
|
||||
|
||||
@@ -578,7 +465,7 @@ Simple set of Expo push notification tokens. Loaded with permissive parsing (fil
|
||||
|
||||
---
|
||||
|
||||
## 9. Daemon meta files
|
||||
## 7. 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.
|
||||
|
||||
|
||||
+13
-7
@@ -23,6 +23,10 @@ Desktop dev launches its desktop-managed daemon with `PASEO_NODE_ENV=development
|
||||
so development-only providers such as Mock Load Test are available. Packaged
|
||||
desktop launches always force the daemon to production mode.
|
||||
|
||||
The web and desktop dev launchers pass the current Git branch to Metro as
|
||||
`EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL`. The expanded desktop sidebar shows it in
|
||||
the titlebar row. Production builds leave the variable unset and show no label.
|
||||
|
||||
`npm run dev` is only a shorthand for `npm run dev:server`. Keep `127.0.0.1:6767` for the packaged app and production-style `~/.paseo` state.
|
||||
|
||||
## Nix desktop package
|
||||
@@ -127,9 +131,11 @@ desktop-only environment inherited by terminals opened inside Paseo from couplin
|
||||
a new worktree instance to the parent desktop instance's profile or single-instance
|
||||
lock.
|
||||
|
||||
Electron remains in the desktop dev runner's process group. Closing or stopping the
|
||||
workspace-script terminal must terminate Electron with Metro; detaching Electron
|
||||
leaves an orphan holding the worktree's single-instance lock and broken output pipes.
|
||||
The desktop workspace script `exec`s the dev runner so the terminal owns the runner
|
||||
PID. Terminal shutdown reaches the runner as `SIGHUP`; the runner stops Metro and
|
||||
asks Electron to quit through its normal app lifecycle. Do not add an npm wrapper or
|
||||
detach Electron: either change leaves an orphan holding the worktree's single-instance
|
||||
lock and broken output pipes.
|
||||
|
||||
With desktop dev running, verify the real BrowserWindow, titlebar clearance, fullscreen
|
||||
transition, and 751-pixel settings split with:
|
||||
@@ -302,9 +308,9 @@ For runtime-selected workspaces, lifecycle commands run inside the selected runt
|
||||
resolved from `PATH` on macOS/Linux, and PowerShell with `-NoProfile` on
|
||||
Windows. They inherit the daemon environment plus Paseo's lifecycle variables;
|
||||
login and interactive shell startup files are not loaded, and Bash's `BASH_ENV`
|
||||
hook is unset. Daemon-run loop verify checks and ACP single-string terminal
|
||||
commands use the same non-login Bash behavior on macOS/Linux, but preserve their
|
||||
existing `cmd.exe /c` string semantics on Windows. Service scripts are separate:
|
||||
hook is unset. ACP single-string terminal commands use the same non-login Bash
|
||||
behavior on macOS/Linux, but preserve their existing `cmd.exe /c` string semantics
|
||||
on Windows. Service scripts are separate:
|
||||
they launch in a terminal and receive the service environment described below.
|
||||
|
||||
Because the shell differs per platform, a lifecycle command that must run
|
||||
@@ -456,7 +462,7 @@ install.
|
||||
|
||||
Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing.
|
||||
|
||||
Canonical automation uses `paseo workspace create/ls/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
|
||||
Canonical automation uses `paseo workspace create/ls/rename/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases.
|
||||
|
||||
```bash
|
||||
npm run cli -- ls -a -g # List all agents globally
|
||||
|
||||
@@ -16,4 +16,6 @@ Read aggregate health from the owning observer service. Runtime metrics include
|
||||
|
||||
Git owns Git-ignore evaluation. The observer accepts absolute excluded roots and applies updates without replacing the observation or exposing its watcher topology. This keeps tracked files inside otherwise ignored directories observable and keeps Git policy out of the filesystem module.
|
||||
|
||||
Workspace Git verifies each repository metadata subscription with a one-shot canary inside the Git directory. If the event does not round-trip through the subscription callback, treat the watcher as unavailable and enter degraded polling. Refresh working-tree Git-ignore exclusions from ignore-file events and watcher recovery, never from a healthy-watcher timer.
|
||||
|
||||
The real-filesystem contracts and daemon auto-archive lifecycle run in the normal server test suite. Use the scripts only for manual performance and soak work: `npm run measure:file-observer --workspace=@getpaseo/server` measures burst and sustained-create behavior, and `npm run repro:file-observer-teardown --workspace=@getpaseo/server` runs the teardown soak.
|
||||
|
||||
+4
-2
@@ -26,14 +26,16 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
|
||||
- **Workspace status bucket** — Aggregate activity signal for a workspace row. Same-`cwd` workspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped by `workspaceId`.
|
||||
- **Agent session** — One running instance of an agent inside a workspace (one provider, one model, one cwd, one timeline). The conceptual unit; in the UI this opens as a tab. Moving toward this as the canonical term over "Agent". Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`).
|
||||
- **Session** — Two senses: (a) per-client connection to a daemon, internal; (b) user-facing agent session, see **Agent session**. Code: `Session` (`packages/server/src/server/session.ts`) for (a). Don't confuse with: provider-side agent session log.
|
||||
- **Profile** — Internal name for the persisted shape of a host. Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Never user-facing.
|
||||
- **Profile** — Internal name for the persisted shape of a host. Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Never user-facing. Three other things are called "profile" in this codebase, so always qualify: **Agent profile**, **Terminal profile**, and the provider aliases in [custom-providers.md](custom-providers.md). Bare "profile" is forbidden in UI copy.
|
||||
- **Agent profile** — A named launch bundle: provider, model, mode, thinking option, feature values, and free-text notes. Applying one writes those values into the **Agent controls** and is then forgotten — there is no selected profile, no default profile, and no drift tracking. The notes are written for orchestrating agents and are what the `list_profiles` MCP tool surfaces. Stored host-wide in daemon config as `agentProfiles`. UI: "Agent profiles". Code: `AgentProfile` (`packages/protocol/src/messages.ts:161`). Carries no system prompt, because a per-agent system prompt can only be set at creation. Forbidden: "Favourite", which this replaced.
|
||||
- **Terminal profile** — A named shell command a terminal can launch. Stored host-wide in daemon config as `terminalProfiles`. UI: "Terminal profiles". Code: `TerminalProfile` (`packages/protocol/src/messages.ts:135`), `resolveTerminalProfiles` (`packages/protocol/src/terminal-profiles.ts`). Also selectable as a New Workspace launch target, where the alternative is the chat agent.
|
||||
- **Provider** — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi, Oh My Pi). UI: "Provider". Code: `ProviderSnapshotEntry` (`packages/protocol/src/messages.ts:198`).
|
||||
- **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`).
|
||||
- **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`).
|
||||
- **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`).
|
||||
- **Workspace script** — A named command a workspace can run, declared in `paseo.json`. Two types: `service` and `script` (a command that runs and finishes). UI: "Scripts" for the panel and the menu, both of which list either type. Code: `WorkspaceScriptPayload` (`packages/protocol/src/messages.ts:3126`). Don't confuse with **Service**, which is one of the two types rather than a synonym for the pair.
|
||||
- **Service** — A workspace script of type `service`: long-running, may bind a port, and carries `health` of `healthy | unhealthy | null` once the daemon has observed it. The only type a sidebar workspace row reports, because a running plain script says nothing about whether the workspace is up; the row names the service and turns danger when its health fails, and where several run the failing one takes the row's single slot. UI: "Services" in the sidebar's display preferences (**Show → Services**). Code: `selectWorkspaceServiceSummary` (`packages/app/src/components/sidebar/workspace-meta-row/service-summary.ts`), `WorkspaceScriptHealthSchema` (`packages/protocol/src/messages.ts:3124`). Forbidden: "Service" as a label for the Scripts panel, which lists both types.
|
||||
- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent).
|
||||
- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent).
|
||||
- **Heartbeat** — Ephemeral cron prompt sent back into the same agent/conversation. Agent surfaces expose create, update cron, and delete only. Use for reminders and babysitting where status should return inline.
|
||||
- **Mode** — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code: `modeId` in `AgentSessionConfig` (`packages/protocol/src/messages.ts:257`).
|
||||
- **Attachment** — External or local context bound to an agent prompt: forge issue/change request, review context, uploaded file, text, or image. UI: "Attach issue or PR/MR". Code: `AgentAttachment` (`packages/protocol/src/messages.ts:782`).
|
||||
|
||||
+5
-5
@@ -95,11 +95,11 @@ Hub authentication rejection or close code `4403` permanently revokes the local
|
||||
daemon deletes its credential, stops reconnecting, and retains only the relationship ID, Hub origin,
|
||||
scopes, and a sanitized reason for status reporting.
|
||||
|
||||
`paseo hub disconnect` disables socket reconnect before requesting remote revocation. If the Hub is
|
||||
offline, the daemon persists `disconnecting` and retries revocation across daemon restarts without
|
||||
opening a Hub socket. This also covers an enrollment whose request may have succeeded but whose
|
||||
response was lost. `--force` removes local authority immediately and warns that remote revocation may
|
||||
still be pending.
|
||||
`paseo hub disconnect` disables socket reconnect and execution authority before making one bounded
|
||||
remote revocation request. The daemon then removes the local relationship whether the request
|
||||
succeeds or fails. A failed request returns a warning that server-side revocation may remain pending.
|
||||
`--force` skips the remote request. Legacy persisted `disconnecting` records are removed on startup;
|
||||
the daemon does not retry revocation in the background.
|
||||
|
||||
`paseo hub logout` removes only the active human CLI credential and preserves credentials for other origins. Interactive logout inspects and optionally disconnects a same-origin daemon before deleting the login; a failed requested disconnect preserves the login. JSON and noninteractive logout never prompt or disconnect implicitly.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ The development workflow is shifting from manually editing files to orchestratin
|
||||
|
||||
Freedom and flexibility. Every design decision follows from this:
|
||||
|
||||
- **Multi-provider** — Use any coding agent harness. Pick the right model for each job, switch freely as the landscape shifts. No vendor-lock in.
|
||||
- **Multi-provider** — Use any coding agent harness. Pick the right model for each job, switch freely as the landscape shifts. No vendor lock-in.
|
||||
- **Cross-device** — Desktop, mobile, web, CLI. Start work at your desk, check progress from your phone, script from the terminal.
|
||||
- **Self-hosted** — The daemon runs on your machine. Your code, your keys, your environment. No inference markup, no cloud dependency.
|
||||
- **Respectful** - No telemetry, no forced cloud, no forced accounts
|
||||
|
||||
@@ -85,6 +85,8 @@ Rewind accepts the canonical wire `messageId` and resolves it to the provider id
|
||||
|
||||
Submitted user-message wire items carry the same Paseo ID in `messageId` and `clientMessageId`. Provider adapters attach `clientMessageId` only to the echo for that foreground submission; provider history and externally initiated user rows do not have a Paseo client ID.
|
||||
|
||||
Provider adapters must terminalize every transient timeline row before emitting the turn's terminal event. Codex may omit the completed `contextCompaction` item when a turn ends during compaction, so its adapter closes any pending root compaction before forwarding `turn_completed`, `turn_failed`, or `turn_canceled`. A terminal turn must never leave the client showing an operation as still loading.
|
||||
|
||||
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes. Draft command listing and scratch-session feature listing require an explicit draft model. Do not resolve a default model through catalog discovery. A client-level `listFeatures` implementation may return features from an incomplete, model-less draft and owns which features are valid in that state.
|
||||
|
||||
Provider session import has its own contract. The picker calls `listImportableSessions` and receives rows only: provider handle, cwd, title, prompt previews, and last activity. Import calls `importSession({ providerHandleId, cwd })` for the selected row and must not call listing again. The provider returns the resumed session, storage config, persistence handle, and hydrated timeline for that one native session; `AgentManager.importProviderSession` seeds the daemon timeline and publishes the Paseo agent only after it is ready.
|
||||
@@ -105,6 +107,13 @@ The daemon keeps provider snapshots per bound workspace, with separate legacy cw
|
||||
|
||||
Provider catalog probes receive a discriminated `FetchCatalogOptions`: `{ scope: "global", force }` for global catalog refreshes, or `{ scope: "workspace", cwd, force }` after the workspace runtime has bound its private placement. Providers decide what global means for their runtime; do not infer global by comparing a cwd to the user's home directory.
|
||||
|
||||
`ProviderSnapshotManager` owns one refresh deadline per provider. The deadline starts before the
|
||||
availability check and covers that check plus the complete catalog probe. Providers that make
|
||||
multiple catalog requests must not apply this deadline separately to each request. The manager
|
||||
aborts the shared refresh signal at the deadline. Providers name active catalog operations and
|
||||
finish subprocess, server, or session cleanup before rejecting. Timeout errors list the operations
|
||||
that were still active when the deadline expired.
|
||||
|
||||
Snapshot reads may probe providers only while the requested cwd scope is cold. Once an entry is warm, its `ready`, `error`, or `unavailable` state stays cached until an explicit refresh. Do not add TTL revalidation, focus-triggered refreshes, selector-open refreshes, or config-reload refreshes. Selector-open refetches may read an already-loading or stale React Query, but they must not force provider probing on their own.
|
||||
|
||||
Capable clients receive a compact, content-addressed snapshot. Model rows derive their provider from the containing entry and reference snapshot-level thinking sets. The app persists that compact shape per server and workspace identity, or per cwd for the legacy path, then sends its hash on the next pull; an unchanged response carries no catalog body. Keep the legacy encoding for clients without the capability. The hash covers the complete client-visible compact snapshot, including status and `fetchedAt`, so explicit refreshes invalidate it even when the discovered catalog is otherwise equal.
|
||||
|
||||
@@ -19,23 +19,22 @@ The first carves shipped as **deep modules with a narrow Host seam**, not the `d
|
||||
|
||||
- **#1640 — VoiceSession** (`session/voice/voice-session.ts`, seam `VoiceSessionHost`): the STT/TTS/dictation/turn-detection subsystem. _(Originally landed at `server/voice/`; relocated under `session/` so all session subsystems share one home.)_
|
||||
- **#1644 — CheckoutSession, read side** (`session/checkout/checkout-session.ts`, seam `CheckoutSessionHost`, port `CheckoutDiffSubscriber`): status, branch validate/suggest, diff subscribe/unsubscribe, manual refresh. The workspace-git observer already delegates `emitStatusUpdate`/`scheduleDiffRefresh` to it.
|
||||
- **ScheduleSession** (`session/schedule/schedule-session.ts`, seam `ScheduleSessionHost`): scheduled-agent request handling.
|
||||
|
||||
**Next carve — CheckoutSession mutation side (Slice 4 below).** The 17 checkout _write_ handlers still inline in `session.ts` (`dispatchCheckoutMessage`, ~2010) — branch switch/rename, commit, merge, merge-from-base, pull, push, PR create/merge, github set-auto-merge/get-check-details, PR status, PR timeline, github search, stash save/pop/list — move into the existing `session/checkout/checkout-session.ts` behind `CheckoutSessionHost`. The Slice-3 observer entanglement the table fears is already resolved: #1644 moved the status/diff read side into CheckoutSession, so the workspace observer delegates today and this no longer blocks on the WorkspaceController split.
|
||||
|
||||
### Why this is safe at the dispatch seam (verified)
|
||||
|
||||
`dispatchInboundMessage` builds `a() ?? b() ?? ... ?? dispatchMiscMessage()` and short-circuits on the first non-`undefined` **Promise object** (not its resolved value). Message-type spaces are **disjoint** (no duplicate `case` labels across switches), so at most one dispatcher matches any message — collapsing to delegation cannot change which handler runs. `dispatchTerminalMessage` (2150-2153) already proves this. Two quirks preserved verbatim: schedule/\* is reached via the chat dispatcher's OWN `default` arm (2183), not the top-level `??`; and `start_workspace_script_request` (a workspace type) is special-cased before terminal delegation (2150).
|
||||
`dispatchInboundMessage` builds `a() ?? b() ?? ... ?? dispatchMiscMessage()` and short-circuits on the first non-`undefined` **Promise object** (not its resolved value). Message-type spaces are **disjoint** (no duplicate `case` labels across switches), so at most one dispatcher matches any message — collapsing to delegation cannot change which handler runs. `dispatchTerminalMessage` (2150-2153) already proves this. `start_workspace_script_request` (a workspace type) is special-cased before terminal delegation (2150).
|
||||
|
||||
Rejected alternatives: **feature-module** (free functions + wide context bag) cannot own the live state machines (workspaceUpdatesSubscription, agentUpdatesSubscription, ~25 voice fields) and adds a competing idiom; **mixin-composition** preserves the shared-`this` god object verbatim and requires widening ~325 private fields to protected.
|
||||
|
||||
## Slice ordering (least-coupled first)
|
||||
|
||||
The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verification: `emitCheckoutStatusUpdate` is called from exactly ONE site (session.ts:4915), inside the workspace-owned `syncWorkspaceGitObserver` callback that ALSO fires workspace effects over shared watch-target maps. Extracting checkout first forces splitting the hardest workspace/git seam before workspace is touched. The strictly safer first cuts are **chat-schedule-loop** (only knot: `handleChatPostRequest`; touches no shared observer/git/voice state) and **provider-catalog** (one shared collaborator + injected predicates).
|
||||
The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verification: `emitCheckoutStatusUpdate` is called from exactly ONE site (session.ts:4915), inside the workspace-owned `syncWorkspaceGitObserver` callback that ALSO fires workspace effects over shared watch-target maps. Extracting checkout first forces splitting the hardest workspace/git seam before workspace is touched. The safer first cut is **provider-catalog** (one shared collaborator + injected predicates).
|
||||
|
||||
| # | Slice | Effort | Risk |
|
||||
| --- | ----------------------------------------------------------------------------- | ------ | ------ |
|
||||
| 0 | Test net + disjointness tripwire (no extraction) | M | low |
|
||||
| 1 | ChatScheduleLoopController — **STOP FOR REVIEW after green** | M | low |
|
||||
| 2 | ProviderCatalogController | M | medium |
|
||||
| 3 | Split shared workspace-git observer + agent-subscribe fan-out (no controller) | M | high |
|
||||
| 4 | GitCheckoutController | L | medium |
|
||||
@@ -47,34 +46,6 @@ The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verific
|
||||
|
||||
---
|
||||
|
||||
## Slice 0 — Test net + disjointness tripwire (prerequisite)
|
||||
|
||||
No production code moves. Add `session.dispatch-seam.test.ts`. This is the gate the whole plan rests on, because chat/schedule/loop have **zero** handleMessage coverage today (verified).
|
||||
|
||||
Write RED-then-GREEN against the **current in-place** Session:
|
||||
|
||||
- `chat/post` happy path (asserts `chat/post` response emitted) + fanout-limit error path (asserts the `chat/post` error envelope, NOT a bubbled `rpc_error`).
|
||||
- one `schedule/*` and one `loop/*` round-trip.
|
||||
- a handler that throws **synchronously** emits `rpc_error{code:"handler_error"}` + an `activity_log` error frame.
|
||||
- a handler that **rejects async** emits the SAME pair.
|
||||
- a table-driven assertion that the union of all controllers' owned-type `ReadonlySet`s is pairwise disjoint and covers the dispatched `SessionInboundMessage` union (grows as controllers land).
|
||||
|
||||
**Tests:** `session.dispatch-seam.test.ts`, `session.test.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Slice 1 — ChatScheduleLoopController ← STOP FOR HUMAN REVIEW after this ships green
|
||||
|
||||
**Move:** all 21 handlers (`handleChat*` ×7, `handleSchedule*` ×9, `handleLoop*` ×5), the three rpc-error emitters (`emitChatRpcError`/`emitScheduleRpcError`/`emitLoopRpcError` — **kept separate, not merged**), `toScheduleSummary` → `packages/server/src/server/chat/chat-schedule-loop-controller.ts`. Collapse `dispatchChatScheduleLoopMessage` + `dispatchScheduleMessage` to `return this.chatScheduleLoopController.dispatch(msg)`.
|
||||
|
||||
**SessionContext surface:** `emit`, `sessionLogger`, `clientId` (authorAgentId fallback), `chatService`, `scheduleService`, `loopService`, and a narrow agent-control port `{ listAgents, resolveAgentIdentifier, agentStorage.list }` for `handleChatPostRequest` mention fanout.
|
||||
|
||||
**Owned-type set MUST include all 7 `chat/*` + 5 `loop/*` + 9 `schedule/*` types** — schedule/\* is currently routed via the chat dispatcher's own `default` arm, so it must stay inside this one controller, or schedule requests silently no-op.
|
||||
|
||||
**Behavior note:** least-coupled domain. Move the three rpc-error emitters verbatim (they differ in default code + the `ChatServiceError` branch). **Tests:** `session.dispatch-seam.test.ts`, `loop-service.test.ts`, `session.test.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Slice 2 — ProviderCatalogController
|
||||
|
||||
**Move:** 7 provider handlers + `emitProviderDisabledResponse` + `getProviderSnapshotEntryForRead` → `packages/server/src/server/provider/provider-catalog-controller.ts`. Move the `providers_snapshot_update` PUSH wiring (1235-1254) into the controller's `start()`/`dispose()`. Collapse `dispatchProviderMessage`.
|
||||
|
||||
+28
-16
@@ -464,26 +464,37 @@ No prefix (`v`), no extra text. `Release Notes Sync` matches the `## X.Y.Z` (or
|
||||
## Changelog ownership
|
||||
|
||||
- **The agent running the release writes the changelog entry — beta or stable.** The release context and final wording stay with that agent.
|
||||
- **Commit history is only an index of the changes. Never draft the changelog from commit subjects or diffs alone.** For every PR in the release range, read the full PR description and every issue it links to before deciding what changed, why users care, or how changes should be grouped. Use the implementation only to verify the resulting understanding.
|
||||
- For the first beta or a direct stable release, draft from the previous stable tag to the release source. For later betas, draft from the previous beta tag to the release source. Promotion replaces the beta series with one entry drafted from the previous stable tag to the release source. Review the result against the changelog policy below, show it to the user, and wait for approval before committing it.
|
||||
|
||||
## Changelog voice
|
||||
## Changelog wording
|
||||
|
||||
The changelog is shown on the Paseo homepage. Write it for **end users**, not developers.
|
||||
The changelog is shown on the Paseo homepage. Each bullet is a compact factual record of
|
||||
product behavior that changed.
|
||||
|
||||
- **Frame everything from the user's perspective.** Describe what changed in the app, not what changed in the code. Users care that "workspaces load instantly" — not that a component no longer remounts.
|
||||
- **Never mention component names, internal modules, or implementation details.** No `WorkingIndicator`, no `accumulatedUsage`, no `reconcileAndEmitWorkspaceUpdates`. Also no "virtualized lists", no "remount", no "memoization", no "debounced", no "fuzzy ranking", no "controlled input", no "uncontrolled input" — these are implementation words masquerading as user-facing copy.
|
||||
- **Concrete WRONG → RIGHT examples** (real mistakes from past releases):
|
||||
- **Name the exact change.** Prefer `Added <capability>`, `Removed <behavior>`,
|
||||
`Changed <behavior>`, or `Fixed <failure> when <condition>`.
|
||||
- **Keep the scope exact.** A conditional bug is not a general reliability problem. Do not
|
||||
broaden one failure into claims that Paseo is now faster, smoother, responsive, or reliable.
|
||||
- **Use concrete product and runtime terms.** Git polling, persisted cache, provider catalog,
|
||||
and WebSocket reconnects can identify the affected behavior. Component names, internal
|
||||
modules, code symbols, and implementation techniques cannot: omit `WorkingIndicator`,
|
||||
`reconcileAndEmitWorkspaceUpdates`, remounts, memoization, and controlled inputs.
|
||||
- **State the consequence only when the change itself is unclear.** Keep the condition that
|
||||
makes the consequence true. Do not replace a precise change with a broad benefit claim.
|
||||
- **Do not invent context.** Mention an upgrade, platform, workload, or user action only when
|
||||
the PR or linked issue establishes that scope.
|
||||
|
||||
| Wrong (implementation-facing) | Right (user-facing) |
|
||||
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| Switching layouts no longer remounts the active agent | Splitting a pane no longer loses your scroll position |
|
||||
| Model, mode, and thinking pickers — searchable virtualized lists with fuzzy ranking | Mobile model selector is faster and more straightforward |
|
||||
| Text inputs in mobile sheets no longer flicker while typing fast | Typing in mobile sheets no longer flickers |
|
||||
| Compact web sheets no longer crash when swiped to dismiss | Sheets on mobile web no longer crash when swiped to dismiss |
|
||||
| Reduced re-renders in the agent list | Agent list scrolls smoothly |
|
||||
| Added debouncing to the search input | Search results no longer lag behind typing |
|
||||
| Avoid | Write |
|
||||
| ------------------------------------------------------------ | ----------------------------------------------------- |
|
||||
| Paseo stays responsive with many idle Git workspaces | Removed periodic Git polling for idle workspaces |
|
||||
| Incompatible saved app data no longer crashes after upgrades | Fixed crash when persisted cache was incompatible |
|
||||
| Splitting layouts no longer remounts the active agent | Fixed scroll position resetting when splitting a pane |
|
||||
| Mobile model selector is faster and more straightforward | Added search to the mobile model selector |
|
||||
|
||||
Test: would a non-developer reader recognise what changed when using the app? If they'd need an engineer to translate ("what's a remount?"), the bullet is still implementation-facing — rewrite it as the symptom the user experiences.
|
||||
Test each bullet against the source PR and issue: can a reviewer point to the exact behavior
|
||||
that changed, the failure that was fixed, or the capability that was added? If the bullet only
|
||||
claims a general improvement, rewrite it with the concrete change.
|
||||
|
||||
- **Use the entry's release scope.** Include changes within the matching range in **Changelog scope**.
|
||||
- **Collapse internal iterations within that scope.** Present a feature added and fixed in one range as working. A later beta can describe a fix to behavior delivered in an earlier beta; promotion folds the complete beta series into the final stable behavior.
|
||||
@@ -498,8 +509,7 @@ Every bullet must be scannable at a glance. The changelog is not release documen
|
||||
- **One line per bullet.** If a bullet wraps to three lines in a narrow column, it's too long.
|
||||
- **Split bullets that pack multiple distinct changes.** If a bullet uses "and", "plus", a comma list, or an em-dash to chain several independent improvements, break them into separate bullets — even when they share a theme or author. One bullet = one user-facing change.
|
||||
- **Trim qualifying clauses.** Drop "with a hint shown when…", "matching the CLI's behaviour", "across common install shapes". If the detail doesn't change whether a user cares, cut it.
|
||||
- **Lead with what the user can do, not the mechanism.** The reader cares about the capability, not how it works under the hood. Do not explain LAN vs WAN, TLS handshakes, IPC, the daemon-relay topology, or any internal concept the user has not asked about. "Self-hosted relays can use a different TLS setting for the public endpoint" — not "Self-hosted relays support a separate TLS setting for the public endpoint, so the daemon can reach the relay over the LAN while the phone reaches it over the public secure address." If a feature genuinely needs background to be understood, it belongs in product docs, with a one-line teaser in the changelog.
|
||||
- **Lead with the outcome.** "Windows: agents launch reliably from npm `.cmd` shims…" is better than "Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly…".
|
||||
- **Stop after identifying the change.** Do not explain LAN/WAN topology, TLS handshakes, IPC, or other architecture in a changelog bullet. Put necessary background in product docs.
|
||||
- **Attribution follows the split.** When you split a dense bullet, move each PR/author to the bullet it belongs to. Never duplicate the same PR across multiple bullets.
|
||||
|
||||
## Changelog attribution
|
||||
@@ -567,6 +577,7 @@ Each beta entry records what its testers receive. Promotion produces the single
|
||||
### Beta release
|
||||
|
||||
- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green
|
||||
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
||||
- [ ] Add a new `CHANGELOG.md` entry for this beta (heading `## X.Y.Z-beta.N - YYYY-MM-DD`), review it against the changelog policy, get approval, and commit it before cutting the release
|
||||
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
||||
- [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag
|
||||
@@ -587,6 +598,7 @@ Each beta entry records what its testers receive. Promotion produces the single
|
||||
- [ ] Run the pre-release sanity check (see above) and address any findings
|
||||
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
||||
- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green
|
||||
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
||||
- [ ] Ensure the approved release inputs are committed locally and the git worktree is clean before running any release command
|
||||
- [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command
|
||||
- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). Promotion replaces every `## X.Y.Z-beta.N` entry in the series with one `## X.Y.Z - YYYY-MM-DD` entry covering the full release
|
||||
|
||||
Reference in New Issue
Block a user