Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions docs/ai/design/2026-08-07-feature-agent-print-mode.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
phase: design
title: Claude Print-Mode Agent Design
description: Minimal durable print-agent identity, execution, locking, and CLI integration
description: Minimal durable durable-agent identity, execution, locking, and CLI integration
---

# Claude Print-Mode Agent Design

## Architecture Overview

Print agents are an additive control path beside the existing process adapters. Existing `AgentManager`, terminal discovery, tmux start, interactive send/wait, groups, channels, and TUI continue to operate on live `AgentInfo` objects. A small print-agent service in `agent-manager` owns durable records and Claude print execution; CLI command orchestration combines the two target kinds only for start, list, detail, and direct send.
Durable agents are an additive control path beside the existing process adapters. Existing `AgentManager`, terminal discovery, tmux start, interactive send/wait, groups, channels, and TUI continue to operate on live `AgentInfo` objects. A small durable-agent service in `agent-manager` owns durable records and Claude print execution; CLI command orchestration combines the two target kinds only for start, list, detail, and direct send.

```mermaid
flowchart LR
Expand All @@ -26,8 +26,8 @@ flowchart LR

### Design boundaries

- `AgentInfo` remains the live-process type with a required PID. Print agents do not fabricate one.
- `PrintAgent` is a separate durable type.
- `AgentInfo` remains the live-process type with a required PID. Durable agents do not fabricate one.
- `DurableAgent` is a separate durable type.
- `AgentManager.listAgents()` remains live-only so existing TUI, channels, groups, kill, open, rename, and terminal flows do not accidentally acquire print semantics.
- CLI list/detail/direct-send use a small combined resolver. Other commands remain unchanged.
- Only Claude is implemented. The runner is injectable for tests but no generic multi-provider framework is introduced.
Expand All @@ -36,36 +36,36 @@ flowchart LR

### Store file

Default path: `~/.ai-devkit/print-agents.json`.
Default path: `~/.ai-devkit/durable-agents.json`.

```ts
interface PrintAgentStoreFile {
interface DurableAgentRepositoryFile {
version: 1;
agents: PrintAgent[];
agents: DurableAgent[];
}

type PrintAgentState = 'ready' | 'running' | 'degraded';
type PrintSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
type PrintRunStatus = 'succeeded' | 'failed' | 'interrupted';
type DurableAgentState = 'ready' | 'running' | 'degraded';
type DurableSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
type DurableRunStatus = 'succeeded' | 'failed' | 'interrupted';

interface PrintAgent {
interface DurableAgent {
id: string; // immutable AI DevKit UUID
name: string; // unique among print agents
name: string; // unique among durable agents
provider: 'claude';
mode: 'print';
mode: 'durable';
cwd: string; // canonical real path
providerSessionId: string; // immutable caller-assigned Claude UUID
state: PrintAgentState;
sessionHealth: PrintSessionHealth;
state: DurableAgentState;
sessionHealth: DurableSessionHealth;
createdAt: string;
updatedAt: string;
lastActiveAt: string | null;
lastResult: PrintLastResult | null;
activeRun: PrintActiveRun | null;
lastResult: DurableLastResult | null;
activeRun: DurableActiveRun | null;
}

interface PrintLastResult {
status: PrintRunStatus;
interface DurableLastResult {
status: DurableRunStatus;
completedAt: string;
exitCode: number | null;
summary: string; // sanitized and bounded
Expand All @@ -76,7 +76,7 @@ interface ProcessIdentity {
startedAt: string; // OS-observed process start identity
}

interface PrintActiveRun {
interface DurableActiveRun {
token: string; // random ownership token
owner: ProcessIdentity;
provider: ProcessIdentity | null;
Expand All @@ -88,8 +88,8 @@ No prompts, transcripts, event history, tool inputs, full provider output, queue

### Lock files

- Store mutation lock: sibling directory `print-agents.json.lock`.
- Per-agent execution lock: `~/.ai-devkit/print-agent-locks/<agent-id>.lock/owner.json`.
- Store mutation lock: sibling directory `durable-agents.json.lock`.
- Per-agent execution lock: `~/.ai-devkit/durable-agent-locks/<agent-id>.lock/owner.json`.
- Directory creation with `mkdir` is the cross-process atomic primitive.
- Lock owner metadata uses the same token and process identities as `activeRun`.

Expand All @@ -100,22 +100,22 @@ The per-agent lock is authoritative for exclusion. Persisted `activeRun` makes s
### Store

```ts
interface PrintAgentStoreOptions {
interface DurableAgentRepositoryOptions {
filePath?: string;
lockTimeoutMs?: number;
now?: () => Date;
processInspector?: ProcessInspector;
}

class PrintAgentStore {
create(input: CreatePrintAgentInput): Promise<PrintAgent>;
list(): Promise<PrintAgent[]>;
getById(id: string): Promise<PrintAgent | null>;
resolve(ref: string): Promise<PrintAgent | PrintAgent[] | null>;
acquireRun(id: string): Promise<{ agent: PrintAgent; token: string }>;
class DurableAgentRepository {
create(input: CreateDurableAgentInput): Promise<DurableAgent>;
list(): Promise<DurableAgent[]>;
getById(id: string): Promise<DurableAgent | null>;
resolve(ref: string): Promise<DurableAgent | DurableAgent[] | null>;
acquireRun(id: string): Promise<{ agent: DurableAgent; token: string }>;
recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
completeRun(id: string, token: string, result: PrintRunCompletion): Promise<PrintAgent>;
failRun(id: string, token: string, result: PrintRunFailure): Promise<PrintAgent>;
completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent>;
failRun(id: string, token: string, result: DurableRunFailure): Promise<DurableAgent>;
reconcile(id?: string): Promise<void>;
}
```
Expand All @@ -139,7 +139,7 @@ Validation runs only `claude --version` and `claude --help`. It requires help te

```ts
interface ClaudePrintRunRequest {
agent: PrintAgent;
agent: DurableAgent;
prompt: string;
executable?: string;
firstRun: boolean;
Expand Down Expand Up @@ -201,12 +201,12 @@ Full result text may be returned to the invoking terminal/JSON response, but onl
```ts
type DirectAgentTarget =
| { kind: 'interactive'; agent: AgentInfo }
| { kind: 'print'; agent: PrintAgent };
| { kind: 'print'; agent: DurableAgent };
```

Resolution order:

1. Exact print-agent stable ID.
1. Exact durable-agent stable ID.
2. Gather exact case-insensitive name matches across print and live agents.
3. If exactly one, use it; if multiple, report ambiguity with mode/type.
4. Apply existing live-agent partial matching only when no print name matches.
Expand All @@ -218,16 +218,16 @@ Direct `agent send --id` uses this resolver. Group sends remain live-only.

- Print sends are always synchronous.
- `--wait` is accepted as a no-op semantic confirmation, preserving scripts that add it.
- `--timeout` is rejected for print agents with a clear error. Enforcing it would require process cancellation semantics that are explicitly outside the MVP; silently ignoring it would be unsafe. Interactive timeout behavior is unchanged.
- `--timeout` is rejected for durable agents with a clear error. Enforcing it would require process cancellation semantics that are explicitly outside the MVP; silently ignoring it would be unsafe. Interactive timeout behavior is unchanged.
- `--json` emits a print-specific result object without echoing the prompt.
- Interactive send behavior and JSON shape remain unchanged.

## Component Breakdown

### `agent-manager`

- `print/PrintAgent.ts`: durable types and typed errors.
- `print/PrintAgentStore.ts`: atomic JSON persistence, name/ID resolution, locking, ownership, reconciliation, and path safety.
- `print/DurableAgent.ts`: durable types and typed errors.
- `print/DurableAgentRepository.ts`: atomic JSON persistence, name/ID resolution, locking, ownership, reconciliation, and path safety.
- `print/ProcessInspector.ts`: exact PID/start-time liveness checks, injectable in tests.
- `print/ClaudeCliProbe.ts`: non-billable local capability validation.
- `print/ClaudePrintRunner.ts`: safe process launch, stdin delivery, bounded stream parsing, session verification.
Expand Down
67 changes: 67 additions & 0 deletions docs/ai/design/2026-08-18-feature-durable-agents-sqlite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
phase: design
title: Durable Agents SQLite Design
description: SQLite schema and transactional ownership design
---

# Durable Agents SQLite Design

## Architecture Overview

```mermaid
flowchart LR
CLI[CLI / runner] --> Service[ClaudePrintAgentService]
Service --> Repository[DurableAgentRepository adapter]
Store --> DB[(agents.db)]
Inspector[LocalProcessInspector] --> Store
```

`DurableAgentRepository` remains the public adapter and owns row mapping, validation, and transactional state changes. `DatabaseConnection` owns SQLite configuration and schema migration. Process inspection and cwd canonicalization remain outside transactions; transactions reread state and apply conditional mutations.

## Data Model

`durable_agents` is one flattened row per durable agent:

- Identity: `id` primary key; case-insensitive unique `name`; unconstrained `provider`; `mode` defaulting to `durable`; canonical `cwd`; unique `provider_session_id`.
- Lifecycle: constrained `state`, constrained `session_health`, created/updated timestamps, nullable last-active timestamp.
- Latest result: nullable constrained status, completion timestamp, exit code, and summary.
- Active run: unique token plus owner/provider PID and start-time identity, and run start timestamp.
- Integrity: running state requires every active field; non-running requires all active fields to be null.
- Indexes: state lookup and updated-desc/name-case-insensitive listing.

## API Design

- Existing `DurableAgentRepository` methods and `RepositoryLike` structural consumers stay unchanged.
- Options add `dbPath`; tests inject explicit database paths.
- `lockTimeoutMs`, `incompleteLockGraceMs`, and `mutationLockStaleMs` remain type-compatible but have no runtime effect and are deprecated.
- Domain errors continue to represent conflicts, busy ownership, lost tokens, invalid input, and storage failures.

## Data Flows

### Ownership

- Acquire canonicalizes cwd and inspects candidate processes outside the transaction, then uses `BEGIN IMMEDIATE`, rereads the row, and conditionally updates exactly one eligible row with an atomic token and owner identity.
- Record-provider and complete use `UPDATE ... WHERE id = ? AND active_run_token = ?`; zero changes means ownership was lost.
- Reconciliation queries running rows, inspects processes, then CAS-updates using the observed token and start-time identity. A live owner or provider keeps the run busy.

### Readonly

Readonly construction requires an existing migrated database and skips directory creation, schema initialization, and write pragmas. Readonly `list()` maps rows only and never reconciles.

## Design Decisions

- A separate table isolates durable rows from the registry's dead-process pruning.
- Flattening matches the existing latest-result contract and avoids premature run-history scope.
- SQLite uniqueness and transactions replace lock directories and temp-file replacement.
- Application-layer provider validation avoids migrations when new providers arrive.
- Durable agents persist directly in SQLite; `durable-agents.json` was never released and requires no compatibility path.

Rejected alternatives are merging into `agents`, storing a whole JSON document in one row, adding `durable_runs`, introducing a repository abstraction, and retaining filesystem lock machinery.

## Non-Functional Requirements

- Transactions remain short; filesystem checks and process inspection occur outside them.
- WAL plus a 5-second busy timeout handle contention; acquisition contention maps to `DurableAgentBusyError`.
- Symlink-safe cwd binding prevents path substitution.
- Schema checks reject inconsistent active-run rows and invalid lifecycle/result values.
- State transitions are atomic and recover cleanly on reopen.
16 changes: 8 additions & 8 deletions docs/ai/implementation/2026-08-07-feature-agent-print-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ description: Implementation record, decisions, validation, and deviations

### Task 1.1

- Added `packages/agent-manager/src/print/PrintAgent.ts` with the durable record, state, session health, last-result, active-run, and process-identity contracts.
- Added classified print-agent/store/Claude errors that do not carry prompt content.
- Added `packages/agent-manager/src/durable/DurableAgent.ts` with the durable record, state, session health, last-result, active-run, and process-identity contracts.
- Added classified durable-agent/repository/Claude errors that do not carry prompt content.
- Exported the public contracts from `@ai-devkit/agent-manager`.

## TDD Evidence

- Red: `npx vitest run src/__tests__/print/PrintAgent.test.ts` failed because `PrintAgentBusyError` was absent.
- Red: `npx vitest run src/__tests__/print/DurableAgent.test.ts` failed because `DurableAgentBusyError` was absent.
- Green/refactor: the same focused test passed (1/1), followed by `npm run typecheck` exit 0.
- Task 1.2 red: three focused store tests failed because `PrintAgentStore` was absent.
- Task 1.2 red: three focused store tests failed because `DurableAgentRepository` was absent.
- Task 1.2 green/refactor: all three store tests passed and `npm run typecheck` exited 0.
- Task 1.3 red: two run-ownership tests failed because acquisition/completion methods were absent.
- Task 1.3 green/refactor: all five store tests passed and `npm run typecheck` exited 0.
Expand All @@ -38,7 +38,7 @@ description: Implementation record, decisions, validation, and deviations

### Task 1.2

- Added a separate versioned `~/.ai-devkit/print-agents.json` store.
- Added a separate versioned `~/.ai-devkit/durable-agents.json` repository.
- Added canonical cwd validation, distinct UUID generation, exact ID/name resolution, duplicate-name rejection, atomic exclusive temp-file replacement, owner-only mode, bounded mutation locking, and symlink rejection.

### Task 1.3
Expand All @@ -58,15 +58,15 @@ description: Implementation record, decisions, validation, and deviations

- Added create/send orchestration with fail-fast ownership and no retries.
- Added the narrow CLI integrations for print start, merged list/detail, and synchronous direct send while leaving interactive defaults and excluded commands unchanged.
- Added a user-facing list mode boundary: live process agents render as `interactive`, while internal print-mode records render as `durable` in both table and JSON output. Internal storage and harness contracts remain `mode: 'print'`.
- Added a user-facing list mode boundary: live process agents render as `interactive`, while internal print-mode records render as `durable` in both table and JSON output. Internal storage and harness contracts remain `mode: 'durable'`.
- Added deterministic unit/integration fixtures that never invoke a real model.
- Added crash recovery for old mutation and incomplete run locks and exact cwd/session binding checks.
- Documented inherited Claude permissions, hooks, MCP/tool side effects, and explicit print-mode timeout rejection.

## Design Alignment

- `AgentInfo` remains unchanged and process-specific.
- Print-agent identity is a separate durable type.
- Durable-agent identity is a separate durable type.
- No channel, task, receipt, daemon, queue, cancellation, deletion, transcript, or non-Claude provider behavior was added.

## Deviations and Follow-ups
Expand All @@ -75,7 +75,7 @@ description: Implementation record, decisions, validation, and deviations

## Formal Security Review

- Scope: new print-agent domain/store/probe/runner/service, direct CLI integrations, fixtures, and documentation. Trust boundaries are CLI caller → local state → ephemeral Claude process → untrusted stream/output; the local OS account is the authorization boundary.
- Scope: new durable-agent domain/store/probe/runner/service, direct CLI integrations, fixtures, and documentation. Trust boundaries are CLI caller → local state → ephemeral Claude process → untrusted stream/output; the local OS account is the authorization boundary.
- Remediated `SEC-PRINT-001` (medium, data exposure): provider stderr could contain an echoed prompt or tool secret. The runner now drains stderr but never reflects or persists it; a regression test uses a secret-bearing failure.
- Remediated `SEC-PRINT-002` (medium, availability/business logic): a crash could strand the global mutation lock. Old empty mutation locks are atomically quarantined and removed after a bounded age; live short operations remain protected.
- Remediated `SEC-PRINT-003` (medium, workflow correctness): print `--timeout` was accepted but unenforced. It is now explicitly rejected, because adding termination/cancellation is outside scope.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The implementation matches the design without schema or migration changes. The o
- Initial focused red: 7 failures, including redundant BEGIN/INSERT/COMMIT plus empty prune BEGIN/COMMIT on an unchanged refresh.
- Timestamp regression red: the changed-field test failed when `updated_at` used wall-clock time instead of the injected clock.
- Restored green: `AgentRegistry.test.ts` and `AgentManager.test.ts` passed 67/67, including atomic rollback coverage.
- Full agent-manager suite passed 509/509 with OS process visibility enabled for the existing print-agent integration.
- Full agent-manager suite passed 509/509 with OS process visibility enabled for the existing durable-agent integration.
- Full CLI suite passed 959/959 after the required workspace build.
- Full six-project build and lint completed successfully; lint reported six unrelated pre-existing warnings and zero errors.

Expand Down
Loading
Loading