Skip to content

docs(adr-0010): add amendments for OQ2, OQ3, OQ4 resolutions#331

Merged
ojongerius merged 7 commits intomainfrom
worktree-oq-resolutions-236
May 6, 2026
Merged

docs(adr-0010): add amendments for OQ2, OQ3, OQ4 resolutions#331
ojongerius merged 7 commits intomainfrom
worktree-oq-resolutions-236

Conversation

@ojongerius
Copy link
Copy Markdown
Contributor

@ojongerius ojongerius commented May 6, 2026

Summary

Add three amendments to ADR-0010 resolving the open design questions that gate Section 3 (thin-emitter refactor). No code changes, design resolutions only.

OQ2 — Existing chain migration policy:

  • Decision: Abandon v1 in-process chains; daemon starts fresh at seq=1.
  • Rationale: Pre-1.0 adoption stage (solo dev, lab usage), no production audit dependencies blocking a clean break.
  • Trade-off: Auditors must preserve v1 SQLite DBs and public keys offline if they require long-term verification. V1 receipts remain cryptographically verifiable with those artifacts; v2 tooling and daemon will not verify or import v1 chains.
  • Spec/code impact: v2 daemon/emitters use new default DB/key paths to prevent accidental resume of v1 chains. No migration logic in daemon or SDK store; documentation includes deprecation notice.

OQ3 — SDK cutover sequencing:

  • Decision: Single-shot PR/release: all three SDKs (Go, TS, Py), mcp-proxy, and OpenClaw ship together. No phased rollout.
  • Rationale: Phased cutover creates a mixed-state window (v1 and v2 emitters writing separate chains), violating the single-unified-chain property. Single-shot on day one ensures all emitters speak the same schema.
  • Consequence: One large multi-module PR (~1500+ lines), but one cohesive unit. Eliminates audit coverage gap from mixed-state window.

OQ4 — session_id allocation rule:

  • Decision: Each emitter provides a stable session_id. If the host provides one (e.g., Claude Code session ID), forward it; otherwise generate UUID v4 at startup. Remains constant across daemon reconnects; retired on emitter exit.
  • Rationale: Grouping by instance lifetime (not per-run) keeps a logical agent session in one receipt group. Persistent across reconnect avoids session fragmentation. Forward-compatible with channels that have upstream session IDs.
  • Cardinality: Few long-lived sessions per deployment (~1–10 unique IDs per day).
  • Indexing: Add non-unique index on session_id for efficient filtering (Phase 2).
  • Spec/code impact: Normative spec line + SDK author guideline for uniform initialization. Daemon already captures session_id in receipts; no new daemon logic.

All three resolutions are prerequisites for Section 3 emitter refactor (issue #236). Resolving them here prevents mid-PR re-litigation of the design choices.

Related

Add three amendments to ADR-0010 resolving open design questions:

OQ2 — Existing chain migration: Abandon v1 chains on upgrade. No in-place
migration or import-chain script. Cost: auditors must preserve old SQLite DBs
offline for long-term pre-Phase-2 audit. Rationale: pre-1.0 adoption stage,
no production audit dependencies blocking a clean break, and migration logic
compounds the Section 3 refactor burden.

OQ3 — SDK cutover sequencing: Single-shot PR/release (all SDKs + OpenClaw +
mcp-proxy at once). No phased rollout. Rationale: phased introduction creates
a mixed-state window (v1 and v2 emitters writing separate chains), violating
the daemon's single-unified-chain property. Single-shot on day one ensures all
emitters speak the same schema.

OQ4 — session_id rule: UUID generated at emitter startup, reused across all
tool calls within process lifetime, persistent across daemon reconnects but
retired on emitter exit. Uniform rule across all three SDKs. Cardinality:
few long-lived sessions per deployment (~1–10 unique IDs/day). Indexing:
add non-unique index on Issuer.SessionID for filtering by session.

All three are design decisions only; no spec changes, no code changes.
The amendments establish normative rules for the Section 3 emitter refactor
and SDK major version bump.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR amends ADR-0010 with resolutions for three previously-open design questions (OQ2/OQ3/OQ4) that gate the upcoming Section 3 “thin-emitter refactor”, clarifying migration policy, cutover sequencing, and session_id allocation.

Changes:

  • Add OQ2 decision to abandon v1 in-process chains and start a fresh daemon-managed chain at seq=1 (no migration/import tooling).
  • Add OQ3 decision to do a single-shot cutover/release across all SDKs and integrations to avoid mixed-state multi-chain audit gaps.
  • Add OQ4 decision defining session_id generation and lifetime, plus notes on expected cardinality and planned indexing.

Comment on lines +146 to +165
**Decision:** Each emitter process MUST generate a unique `session_id` (UUID v4 or v5) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects and process-local (lifetime of the emitter process). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary.

**Rationale:**
- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-process lifetime naturally clusters tool calls.
- **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter process).
- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` at construction time; never generate a new one per emit().

**Cardinality and indexing:**
- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage.
- **Index:** Add a non-unique index on `Issuer.SessionID` in the receipts table to support queries like `SELECT * FROM receipts WHERE issuer_session_id = ?`.

**Normative spec line:**
> "Each emitter process MUST generate a unique `session_id` (UUID) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully."

**SDK author guideline:**
> "Initialize `session_id` once per emitter/SDK instance at construction time using a UUID (v4 or v5). Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required."

**Spec/code changes:**
- All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic.
- Daemon: no new logic (session_id is already captured in `receipt.Issuer.SessionID`). Add the `session_id` index on the next SQLite schema version.
Comment on lines +153 to +156
**Cardinality and indexing:**
- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage.
- **Index:** Add a non-unique index on `Issuer.SessionID` in the receipts table to support queries like `SELECT * FROM receipts WHERE issuer_session_id = ?`.

- Line 53 (Schema split): Clarify that session_id is generated by emitter at
  startup and persists for emitter-process lifetime (not per-agent-run), with
  detailed allocation rule in OQ4 amendment.
- Line 155 (OQ4 Cardinality and indexing): Clarify that schema extraction and
  indexing of session_id are deferred to Phase 2 Section 3, not blocking the
  design decision. For now, session_id is only in the receipt JSON.
- Line 166 (OQ4 Spec/code changes): Update to note that CLI filtering is
  deferred pending the schema extraction.

Resolves Copilot comments on inconsistent session_id scoping and unclear
indexing strategy.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


**Consequences:**
- Auditors must preserve v1 SQLite databases offline if they require long-term audit of pre-Phase-2 events. This is a one-time notice; v2 chains persist continuously under daemon supervision.
- v1 receipt verification becomes impossible post-upgrade (no daemon will hold v1 public keys or databases).
Comment on lines +114 to +115
**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script.

Comment on lines +144 to +164
### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects

**Decision:** Each emitter process MUST generate a unique `session_id` (UUID v4 or v5) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects and process-local (lifetime of the emitter process). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary.

**Rationale:**
- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-process lifetime naturally clusters tool calls.
- **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter process).
- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` at construction time; never generate a new one per emit().

**Cardinality and indexing:**
- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage.
- **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution.

**Normative spec line:**
> "Each emitter process MUST generate a unique `session_id` (UUID) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully."

**SDK author guideline:**
> "Initialize `session_id` once per emitter/SDK instance at construction time using a UUID (v4 or v5). Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required."

**Spec/code changes:**
- All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic.
OQ2 (existing chain migration):
- Clarify that v2 daemon/emitters use new default DB/key paths to prevent
  accidental resume of v1 chains. Operators managing v1/v2 coexistence must
  keep DBs in separate directories.
- Fix consequences: v1 receipts remain cryptographically verifiable offline
  with preserved v1 DB + public key. v2 tooling will not verify/import v1
  chains, not that verification is cryptographically impossible.

OQ4 (session_id allocation):
- Update normative spec and SDK guideline to accommodate channels with upstream
  session identifiers (e.g., claude_code_hook forwards Claude Code's session_id
  per ADR-0013). Flexible rule: forward host-provided session_id if available,
  otherwise generate UUID v4 at startup.
- Specify UUID v4 explicitly (v5 is deterministic and requires namespace).

Resolves Copilot comments on OQ2 DB path operational rule, OQ2 v1 verification
wording, and OQ4 conflict with ADR-0013.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment on lines +112 to +114
### 2026-05-06: OQ2 — Existing chain migration policy — abandon old chains

**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. The daemon and emitters use new default DB/key paths (separate from v1) to ensure accidental resume does not happen; operators who want to preserve v1 chains for verification must keep v1 and v2 DBs in separate directories.
Comment on lines +158 to +161
> "Each emitter process MUST provide a stable `session_id` (UUID) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully."

**SDK author guideline:**
> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required."
Comment on lines +118 to +120
**Consequences:**
- Auditors must preserve v1 SQLite databases and matching public keys offline if they require long-term audit of pre-Phase-2 events. V1 receipts remain cryptographically verifiable with those artifacts; v2 tooling and daemon will not verify or import v1 chains.
- v1 chains are not resumed on v2 daemon startup (separate DB/key paths prevent accidental coexistence).
Comment on lines +151 to +164
- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` at construction time; never generate a new one per emit().

**Cardinality and indexing:**
- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage.
- **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution.

**Normative spec line:**
> "Each emitter process MUST provide a stable `session_id` (UUID) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully."

**SDK author guideline:**
> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required."

**Spec/code changes:**
- All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic.
- Line 5 (Status): Update amendment date to include 2026-05-06 for OQ2–OQ4
- Line 146 (OQ4 Decision): Change UUID version to v4 only (was inconsistently v4
  or v5), and update decision to reflect forward-or-generate rule (not just
  generate). Also change scope from process-local to instance-local for clarity.
- PR description: Clarify that v1 receipts remain cryptographically verifiable
  offline with preserved artifacts; v2 tooling simply won't verify them (not
  that verification becomes impossible). Align with amendment wording.
- Lines 149–154 (OQ4 Rationale/Cardinality): Change process-scope language to
  instance-scope; clarify that multiple SDK instances in same process should
  coordinate on a single session_id; update cardinality description to note
  typical one-instance-per-run usage.

Resolves Copilot issues on UUID inconsistency, scope ambiguity, status line
date, and v1 verification contradiction.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

- `v` (schema version)
- `ts_emit` (RFC 3339, advisory)
- `session_id` (UUID, scopes one agent run)
- `session_id` (UUID, generated by emitter at startup, persists for emitter-process lifetime; detailed allocation rule in the *Amendments* section, OQ4)
Comment on lines +144 to +147
### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects

**Decision:** Each emitter process MUST provide a stable `session_id` (UUID v4) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup. The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary.

Comment on lines +158 to +166
> "Each emitter process MUST provide a stable `session_id` (UUID) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully."

**SDK author guideline:**
> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required."

**Spec/code changes:**
- All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic.
- Daemon: no new logic (session_id is already captured in `receipt.Issuer.SessionID`). Schema extraction and indexing deferred to Section 3 (Phase 2).
- Agent-receipts verify CLI can filter by session_id (e.g., `--session <UUID>`) once the schema extraction lands.
Resolve Copilot's type consistency issue: session_id was defined as UUID in
Schema split and OQ4 Decision, but OQ4 also allows forwarding host-provided
IDs that may not be UUIDs.

Solution: Define session_id as an opaque string. When emitter-generated,
recommend UUID v4 (future-proof, matches current systems like Claude Code).
When host-provided, forward unchanged (forward-compatible for channels with
custom session identifier formats).

Changes:
- Line 53 (Schema split): opaque string, UUID v4 recommended
- Line 146 (OQ4 Decision): opaque string, forward or generate v4
- Lines 158, 161 (normative spec + SDK guideline): opaque string, UUID v4
  recommended for generated IDs

This maintains type safety for generated IDs while remaining flexible for
host-provided identifiers.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


**Spec/code changes:**
- No migration logic in daemon startup.
- No schema migrations or data import tooling in `sdk/go/store`.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. The daemon and emitters use new default DB/key paths (separate from v1) to ensure accidental resume does not happen; operators who want to preserve v1 chains for verification must keep v1 and v2 DBs in separate directories.

**Rationale:** Agent-Receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3.

### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects

**Decision:** Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary.
Line 125: Change "No schema migrations or data import tooling in
sdk/go/store" to "No v1→v2 chain migration or data import tooling in
sdk/go/store (schema migrations for other purposes are unaffected)".

The original wording sounded like the store avoids all migrations, when the
intent is specifically to avoid v1-to-v2 chain migration logic. Store schema
migrations (like migrateToolName) continue unchanged.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment on lines 3 to 6
## Status

Accepted (2026-05-03), amended 2026-05-05 (see *Amendments*)
Accepted (2026-05-03), amended 2026-05-05 and 2026-05-06 (see *Amendments*)

**Rationale:** Agent-Receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3.

**Consequences:**
- Auditors must preserve v1 SQLite databases and matching public keys offline if they require long-term audit of pre-Phase-2 events. V1 receipts remain cryptographically verifiable with those artifacts; v2 tooling and daemon will not verify or import v1 chains.
**Decision:** Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary.

**Rationale:**
- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-instance lifetime naturally clusters tool calls.
…s, wording)

ADR-0010 fixes:
- Line 116 (OQ2 rationale): Change "Agent-Receipts" to "agent-receipts" for
  consistency with rest of doc.
- Line 146 (OQ4 Decision): Fix grammar "and instance-local" → "and is
  instance-local".
- Line 119 (OQ2 Consequences): Clarify that v2 daemon does not automatically
  resume/import v1 chains (separate DB paths prevent coexistence), not that
  cryptographic verification is impossible. V1 receipts remain verifiable
  offline with preserved artifacts.
- Line 149 (OQ4 Rationale): Rephrase "One session_id per agent-run..." to
  "Generating per tool call would fragment..." for clarity. Make the
  constraint about instance-lifetime obviously correct, not backwards.

Status updates (ADR-0010 is now Accepted, Phase 1 shipped):
- docs/adr/README.md line 26: Change ADR-0010 status from Proposed → Accepted
- docs/threat-model.md line 3: Update "Both ADRs are Proposed" to "ADR-0010
  Accepted, ADR-0015 Proposed" to reflect current status.
- docs/threat-model.md line 130: Update roadmap table to "Accepted; Phase 1
  shipped, Phase 2+ pending" for ADR-0010.

Resolves all 5 Copilot review issues from latest pass.
@ojongerius ojongerius merged commit bacba5d into main May 6, 2026
8 checks passed
@ojongerius ojongerius deleted the worktree-oq-resolutions-236 branch May 6, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants