# ADR-0028: ACP-Native Session Identity Model

## Status

Proposed

## Context

Issue [#2584](https://github.com/OneStepAt4time/aegis/issues/2584)
starts M1 of the Phase 3.5 ACP backend migration. ADR-0027 approved the ACP
migration as a yellow-green program and explicitly requires production code to
keep JSON-RPC ids, pending approval ids, and session ids distinct.

The current `SessionInfo` model in `src/session.ts` and `src/api-contracts.ts`
is tmux-shaped. It exposes a stable Aegis `id`, but it also treats
`windowId`, `windowName`, `jsonlPath`, byte offsets, monitor offsets, terminal
UI state, and Claude hook metadata as core session fields. `StateStore`
persists serialized `SessionInfo` blobs, so the durable record is coupled to
tmux windows, JSONL transcript discovery, and terminal parsing.

ACP changes the runtime boundary. Aegis will supervise child processes,
exchange JSON-RPC over stdio, receive ACP agent session ids, store normalized
events, persist control actions, replay chat snapshots, and coordinate live
operators. Reusing tmux fields or upstream protocol identifiers as public
resource ids would blur ownership, break resume/restart behavior, and create
authorization ambiguity in multi-tenant deployments.

## Decision

Aegis will use an ACP-native identity model with explicit namespaces. The
public Aegis session id remains the stable resource id. Runtime, protocol,
event, action, approval, transcript, and authorization identifiers are separate
and owned by the layer that creates them.

### Identifier boundaries

| Identifier | Suggested field | Owner | Scope and rules |
| --- | --- | --- | --- |
| Aegis public session id | `aegisSessionId` or existing API `id` | `SessionService` | UUID generated by Aegis. Primary REST, MCP, dashboard, SSE, audit, and store lookup key. Never replaced by an ACP id. |
| ACP agent session id | `acpAgentSessionId` | ACP agent/backend mapping | Opaque id returned or accepted by the ACP agent. Nullable until the agent creates or resumes. Stored for backend calls and diagnostics, not used as the Aegis primary key. |
| Claude session id | `claudeSessionId` | Claude Code / Agent SDK | Optional diagnostic or resume aid when the agent exposes it. Not required for all ACP sessions and not a route id. |
| Backend instance/run id | `backendRunId` | `AcpBackend` supervision | UUID generated for each child process/runtime attempt. Changes on spawn, restart, or recovery. PID is diagnostic metadata under this run, not identity. |
| Conversation id | `conversationId` | `SessionService` | Stable logical conversation chain for resume/subagent relationships. May outlive one runtime attempt. |
| Transcript id | `transcriptId` | transcript/chat cache layer | Stable sink for replay and compatibility transcript output. Offsets are cursors within this transcript, not session ids. |
| Checkpoint id | `checkpointId` | checkpoint/cache layer | Optional replay anchor for compacted history or cached chat state. Unique within a conversation or transcript. |
| Event id | `eventId` | `AcpEventStore` | Opaque event identifier for references, audit, and idempotent fanout. |
| Event sequence | `eventSeq` | `AcpEventStore` | Monotonically increasing integer per `aegisSessionId`. Primary replay cursor and ordering key. |
| Action id | `actionId` | `AcpActionQueue` | Durable idempotency key for prompts, approvals, pause, resume, cancel, driver transfer, and intervention actions. |
| Approval/request id | `approvalId`, `questionId`, or `controlRequestId` | `SessionService` / action queue | Aegis-owned ids for pending human or client decisions. Distinct from `actionId` when one action creates a pending request. |
| JSON-RPC request id | `jsonRpcRequestId` | `AcpJsonRpcClient` | Transport-scoped correlation for one JSON-RPC connection. It may be string or number, may restart at reconnect, and must not be persisted as a resource id or idempotency key. |
| Authorization scope | `tenantId`, `ownerKeyId` | auth layer | Lookup and audit predicates. They are not aliases for sessions and must not be encoded into public ids. |
| Relationship/correlation ids | `parentSessionId`, `rootSessionId`, `correlationId`, `resumeFromSessionId` | `SessionService` | Optional Aegis ids for subagents, root chains, request tracing, and resume lineage. They never replace `aegisSessionId`. |

ADR-0028 refines earlier epic shorthand that used `eventId` for replay
ordering. ACP implementations should model both `eventId` for opaque references
and `eventSeq` for ordered replay cursors.

### Ownership rules

1. `SessionService` owns `aegisSessionId`, lifecycle state, tenant/owner
   predicates, parent/root relationships, and the mapping to ACP/runtime ids.
2. `AcpBackend` owns child process attempts, JSON-RPC ids, protocol timeout
   handling, and ACP agent session attachment.
3. `AcpEventStore` owns `eventId` and `eventSeq`; fanout and replay use
   `eventSeq` as the cursor.
4. `AcpActionQueue` owns `actionId` and idempotency. Approval and question ids
   are Aegis control-plane ids, not JSON-RPC ids.
5. `tenantId` and `ownerKeyId` are mandatory authorization scopes on reads,
   writes, audit export, and cleanup. They are not identity substitutes.

## Lifecycle and Mapping

### Create

On create, `SessionService` generates `aegisSessionId`, `conversationId`, and
`transcriptId`, persists the session with `tenantId` and `ownerKeyId`, and
records the initial state before any ACP child is trusted. `AcpBackend` then
creates a `backendRunId` for the child process attempt. The ACP agent session id
is attached only after the agent successfully creates or resumes its session.
Initial prompts enter `AcpActionQueue` with an `actionId`; any JSON-RPC id used
to deliver the prompt remains transport-local.

For normal resume, `aegisSessionId` and `conversationId` are preserved.
Subagent or forked sessions receive their own public session id and conversation
id by default, linked through `parentSessionId`, `rootSessionId`, and
`correlationId`. Shared transcript or conversation semantics must be explicit
rather than inferred from a parent relationship.

### Attach or resume

Clients attach by public `id` only. Store lookups must include
`aegisSessionId`, `tenantId`, and the applicable ownership or role predicate.
Resume may use `acpAgentSessionId`, `claudeSessionId`, `conversationId`, or a
checkpoint, depending on agent capability. If the agent returns a new
`acpAgentSessionId`, Aegis updates the mapping while preserving the public
`aegisSessionId`, transcript cursors, event sequence, audit chain, and
authorization scope.

### Restart or recover

Every runtime restart creates a new `backendRunId` and a new JSON-RPC id space.
Pending JSON-RPC calls from the old run fail closed. Durable actions remain in
`AcpActionQueue` and are retried, cancelled, or marked failed by action state,
not by JSON-RPC id. `eventSeq` continues monotonically for the Aegis session.
Chat caches and subscribers recover from the last durable event sequence.

### Pause or intervene

Pause, resume, intervention, driver transfer, approval, and cancellation are
durable control actions. They use `actionId` plus a bounded action type and may
create pending `approvalId`, `questionId`, or `controlRequestId` records. They
do not create a new public session id. Audit records must identify the actor,
tenant, owner scope, action id, and affected Aegis session id.

### Close and cleanup

Close records a terminal session state and final event before releasing Redis
presence, driver locks, wakeups, and in-memory fanout state. Runtime metadata
such as `backendRunId`, PID, JSON-RPC ids, and ACP transport errors may be
retained only as bounded diagnostics. Durable session, event, transcript, chat
snapshot, action, and audit records remain until the configured retention or
tenant-scoped delete workflow removes them.

## Persistence Implications

Follow-up M1/M2 stores should expose typed records instead of serializing the
tmux-shaped `SessionInfo` blob as the ACP source of truth. The following names
are implementation guidance, not a full schema freeze.

| Component | Required identity fields |
| --- | --- |
| `AcpSessionStore` | `session_id`/`aegis_session_id`, `tenant_id`, `owner_key_id`, `acp_agent_session_id`, `claude_session_id`, `conversation_id`, `transcript_id`, `parent_session_id`, `root_session_id`, `correlation_id`, `current_backend_run_id`, `status`, `created_at`, `updated_at`, `closed_at`, and bounded backend metadata. |
| `AcpEventStore` | `session_id`, `event_seq`, `event_id`, `backend_run_id`, `event_type`, `occurred_at`, `ingested_at`, redacted `payload`, and optional `payload_ref`. Enforce uniqueness of `(session_id, event_seq)` and tenant-scoped reads through the session row. |
| `AcpActionQueue` | `action_id`, `session_id`, `tenant_id`, `owner_key_id`, `action_type`, client `idempotency_key`, `status`, `created_at`, `available_at`, `attempt_count`, optional `approval_id`/`control_request_id`, and redacted bounded metadata. Never use `jsonRpcRequestId` as the idempotency key. |
| Redis coordination | Keys should use the public Aegis session id, for example `aegis:acp:presence:<sessionId>`, `aegis:acp:driver-lock:<sessionId>`, and `aegis:acp:events:<sessionId>`. Values may include `backendRunId` and subscriber ids. Redis is volatile and never the only copy of history, actions, or authorization state. |
| Chat cache snapshots | Store `snapshot_id`, `session_id`, `transcript_id`, `from_event_seq`, `to_event_seq`, and render metadata. Snapshot ids are cache ids, not route ids. |
| Audit logs | Store `tenant_id`, `owner_key_id`, actor identity, `session_id`, `action_id` or `event_id`, decision/result, timestamps, and redacted bounded metadata. |

Legacy fields such as `windowId`, `windowName`, `jsonlPath`, `byteOffset`, and
`monitorOffset` may remain in compatibility projections while tmux is still
production. ACP stores should model them as nullable compatibility fields or
backend-specific metadata, not required identity columns.

## API Compatibility and Migration

- Public REST, MCP, SSE, WebSocket, dashboard, SDK, and audit surfaces keep
  using `SessionInfo.id` as the public resource id during the transition.
- Clients must not route, authorize, deduplicate, or resume sessions by
  `acpAgentSessionId`, `claudeSessionId`, `backendRunId`, `jsonRpcRequestId`,
  `approvalId`, or `actionId` unless the endpoint explicitly names that
  resource type.
- During M1/M2, tmux-only fields can remain in existing contracts as
  compatibility fields. ACP-backed projections should make them nullable,
  empty, or move them under backend-specific diagnostics until the M3 breaking
  contract cleanup removes them.
- `jsonlPath`, `byteOffset`, and `monitorOffset` are compatibility transcript
  concepts. ACP replay should prefer `transcriptId`, `eventSeq`, and snapshot
  ranges.
- Error messages and diagnostics may display truncated ACP/runtime identifiers,
  but public route examples and client documentation must always use the Aegis
  public session id.

## Security and Privacy

- Do not derive identifiers from prompt bodies, approval payloads, provider
  environment variables, secrets, tool arguments, file contents, or model
  responses.
- Do not log or persist secrets, provider tokens, raw environment values,
  prompt bodies, or approval payloads as identity metadata.
- Redact and bound all diagnostic metadata before storage, logs, REST responses,
  SSE/WebSocket fanout, Redis values, and audit export.
- Include `tenantId` and `ownerKeyId` or role predicates in lookup queries for
  sessions, events, actions, snapshots, pause/intervention records, and audit
  exports.
- Treat upstream ACP and Claude ids as potentially sensitive diagnostics. They
  are opaque and should be truncated in logs unless full values are required in
  a protected debug path.
- JSON-RPC ids are correlation values only. They must not grant authorization,
  deduplicate client actions, or identify durable resources.

## Testing and Acceptance Guidance

Dependent M1/M2 issues should add contract tests that prove the identity
boundaries above:

1. Creating a session generates a UUID public `id` and does not require an ACP
   agent id before persistence.
2. Store reads and writes include tenant and owner/role predicates; cross-tenant
   lookups fail even when an ACP id is known.
3. Resume can update `acpAgentSessionId` without changing public `id`,
   `conversationId`, `transcriptId`, or event sequence.
4. Restart creates a new `backendRunId`; old JSON-RPC ids cannot complete new
   durable actions.
5. `AcpEventStore` returns events in `eventSeq` order and resumes replay from a
   cursor without gaps or duplicates.
6. `AcpActionQueue` deduplicates by `actionId` or explicit client
   `idempotency_key`, never by JSON-RPC id.
7. Approval/request ids are distinct from action ids, session ids, and JSON-RPC
   ids in tests and fixtures.
8. Chat snapshots identify their source by `session_id`, `transcript_id`, and
   event sequence range.
9. Redis fanout loss is recoverable from Postgres event replay.
10. API compatibility tests keep `SessionInfo.id` stable and prevent ACP ids
    from appearing as public route ids.

ACP-021 should introduce the typed identity helpers and state machine skeleton.
ACP-022 through ACP-024 should enforce these fields and constraints in the
Postgres stores. ACP-025 through ACP-028 should consume the same ids for chat
snapshots, pause/intervention persistence, and Redis coordination rather than
defining new identity namespaces.

## Alternatives Considered

### Use the ACP agent session id as the public id

Rejected. The ACP agent owns that namespace, and its semantics can change
across agent versions, resume behavior, or backend restarts. It would also make
tenant-scoped audit and compatibility migration depend on upstream behavior.

### Keep the tmux-shaped `SessionInfo` as the ACP durable model

Rejected. It preserves window, pane, JSONL, and terminal-parser coupling in the
new backend. Compatibility projections are acceptable during migration, but the
ACP source of truth needs typed identity, event, action, and runtime records.

### Use JSON-RPC ids for actions and approvals

Rejected. JSON-RPC ids are scoped to one transport connection. They reset or
become ambiguous after reconnects and are unsuitable for durable idempotency,
approval queues, audit, or replay.

### Encode tenant or owner information in public ids

Rejected. Tenant and owner scopes are authorization predicates, not identity
aliases. Encoding them in ids would leak metadata and weaken query-level
isolation discipline.

## Consequences

- M1/M2 implementations can proceed with stable identifier boundaries for
  `SessionService`, `AcpSessionStore`, `AcpEventStore`, and `AcpActionQueue`.
- Public clients keep using the existing Aegis session id while internal ACP
  mappings evolve behind compatibility projections.
- Restart, replay, and Redis recovery behavior have durable cursors independent
  of ACP transport details.
- The model adds more ids than the tmux runtime, but each id has one owner and
  one scope, reducing accidental coupling and security ambiguity.

## Related

- [ADR-0027: ACP Feasibility Spike Verdict](0027-acp-feasibility-spike-verdict.md)
- [ADR-0023: Positioning - Claude Code Control Plane, MIT, BYO LLM, `ag` CLI](0023-positioning-claude-code-control-plane.md)
- [Phase 3.5 ACP backend migration epic](../../.claude/epics/phase-3-5-acp-backend-migration/epic.md)
- Issue [#2584](https://github.com/OneStepAt4time/aegis/issues/2584)
