# ADR-066 Parallel CLI Session Coordination with Conflict Zone Locking

## Status

Accepted, 2026-06-27.

## Status History

```yaml
status_history:
  - date: 2026-06-27
    status: Accepted
    changed_by: hkl
    reason: Multiple concurrent Claude Code sessions (PWA, OC, INFRA, DB) share infrastructure (Docker, Traefik, DB grants, hub session format, SW cache). Without explicit coordination, simultaneous edits cause race conditions and silent overwrites. ADR captures session types, conflict zones, locking protocol, stale-lock detection, and safe-zone rules to prevent outages and data corruption.
    changed_via: adr-kit (360lm)
```

## Context

The 360lm project and OpenClaw services run parallel Claude Code sessions, each focused on a distinct domain:

- **PWA session**: `/var/www/360lm/` development (HTML/CSS/JS, PWA-specific DB schemas, tests)
- **OC session**: `/opt/openclaw/` config and setup (WhatsApp/Telegram bridges, contact import, service topology)
- **INFRA session**: VPS infrastructure only (Traefik, Docker networks, systemd, cron jobs)
- **DB session**: Pure database schema and migrations (PostgREST grants, shared cross-PWA tables)

Each session focuses on one domain to reduce cognitive load. However, they share critical infrastructure:

1. **Traefik routing** — Docker labels in `docker-compose.yml` files
2. **PostgREST grants and config** — `04_grants.sql`, per-PWA RPC access control
3. **Docker shared networks** — `proxy` network, cross-service connectivity
4. **Hub session format** — PWAs depend on consistent `hub.pwa_registry` schema
5. **Service Worker cache keys** — all PWAs respect `CACHE_VER` string
6. **Systemd user environment** — affects all Node.js services in the same user session
7. **Cron jobs** — user and system-level scheduled tasks

**The problem:** Without explicit coordination, concurrent edits to shared infrastructure cause:
- Silent overwrites (last writer wins in files like `MEMORY.md`)
- Migration conflicts (two sessions alter the same table DDL)
- Routing failures (Traefik labels edited simultaneously, changes lost)
- Grant races (PostgREST reloads mid-edit, inconsistent permissions)
- Deadlocks (if two sessions try to acquire locks in different orders)

**Existing attempts:** Session state files (`session_state_pwa.md`, `session_state_oc.md`) track what each session is working on, but do not prevent concurrent writes to shared infrastructure.

**Affected systems**: All PWAs, OpenClaw, Traefik, PostgREST, Docker daemon, hub session registry, SW cache version strings, systemd user environment, cron scheduler.

## Decision

Implement **conflict zone locking** — a lightweight, non-distributed coordination protocol where each session:

1. **Registers** itself in a live session board
2. **Acquires locks** on named conflict zones (alphabetically ordered to prevent deadlock) before touching shared infrastructure
3. **Releases locks** immediately after completing the operation
4. **Detects stale locks** (>30 min, no activity) and asks for user confirmation before claiming
5. **Cleans up** at session end

### Session Types

Use these exact session names:

| Session | Domain | When to Use |
|---------|--------|------------|
| `PWA` | `/var/www/360lm/` | PWA development, single-PWA DB schema work, tests |
| `OC` | `/opt/openclaw/` | OpenClaw services, WhatsApp bridge, contact management |
| `INFRA` | VPS infrastructure | Docker networks, Traefik, systemd, cron (no app-specific domain) |
| `DB` | Database schema | PostgREST grants, cross-PWA migrations, shared table DDL |

**Identity inference rule:** If the first task clearly targets a domain, register accordingly. If ambiguous, ask the user before registering. Never guess.

### Session Registration Board

Live state file: `/root/.claude/projects/-var-www-360lm/memory/parallel_sessions.md`

#### Active Sessions Table

```
| Session | Domain | Started | Working On | Last Activity |
|---------|--------|---------|------------|---------------|
| PWA | Tour Playground (/tour-pg/) | 2026-06-22 | [current task] | [timestamp] |
| OC | OpenClaw | 2026-05-14 | [current task] | [timestamp] |
```

Each session adds one row. At session start:

1. Read `parallel_sessions.md` (always fresh, never cached)
2. Note other active sessions and their zone locks
3. Add your row with: session name, domain, current timestamp, brief task description, same timestamp
4. Report findings to user: "No other session active" or "I see [Session] is active on [task], holding locks: [list]"

#### Conflict Zone Locks Table

```
| Zone | Resources Covered | Locked By | Since | Status |
|------|-------------------|-----------|-------|--------|
| CRON_JOBS | `/etc/cron.d/`, `crontab` entries | — | — | 🟢 FREE |
| DBT | `design_build_tracker.md` shared tracker | — | — | 🟢 FREE |
| DB_EXPENSE_SHARED | `expense.employees`, `expense.verify_pin`, `expense.departments` | — | — | 🟢 FREE |
| DB_GRANTS | `04_grants.sql`, any `GRANT`/`REVOKE` SQL, PostgREST reload | — | — | 🟢 FREE |
| DB_MIGRATIONS | DDL on `public` schema or any multi-PWA schema | — | — | 🟢 FREE |
| DOCKER_SHARED | `proxy` network, shared Docker containers | — | — | 🟢 FREE |
| MEMORY_MD | `/root/.claude/projects/-var-www-360lm/memory/MEMORY.md` index | — | — | 🟢 FREE |
| NGINX_SHARED | Nginx configs shared across multiple services | — | — | 🟢 FREE |
| SYSTEMD_ENV | `systemctl --user set-environment` / `unset-environment` | — | — | 🟢 FREE |
| TRAEFIK | Traefik labels, `docker-compose.yml` with Traefik sections | — | — | 🟢 FREE |
```

#### Lock Acquisition Protocol

**Before every operation that touches a conflict zone:**

1. Read `parallel_sessions.md` (fresh read — never cached)
2. Identify all zones the operation touches (see zone reference below)
3. **Sort zone names alphabetically** — this is your lock acquisition order (prevents deadlock)
4. For each zone in alphabetical order:
   - **FREE (shows `—`):** Write `[YourSession] | [YYYY-MM-DD HH:MM] | 🔴 LOCKED` into that row, move to next zone
   - **LOCKED by another session:** STOP. Release any zones you just acquired (reverse alphabetical order). Report to user: *"⚠️ Cannot proceed — [Zone] is locked by [Session] since [time]. Ask the [Session] to finish and release, then tell me to retry."*
   - **LOCKED by this session already:** Continue — no re-acquire needed

Proceed only after all locks are cleanly held.

#### Lock Release Protocol

**After completing a conflict zone operation:**

1. Release locks in **reverse alphabetical order** — update each row: `— | — | 🟢 FREE`
2. Update your session row's "Last Activity" timestamp
3. Append to Lock History (newest at top): `- [timestamp] | [Session] | Released [ZONE] after [description]`

### Conflict Zone Reference

Ten named zones cover all shared infrastructure:

| Zone | What Triggers Lock Acquisition | Examples |
|------|-------------------------------|----------|
| **CRON_JOBS** | Any read/write of `/etc/cron.d/` files, `crontab -e`, `crontab -l`, or any cron entry | Adding a nightly backup job, installing a task scheduler entry |
| **DBT** | Reading or writing `design_build_tracker.md` or other shared memory trackers (last-writer-wins race risk) | Updating dbt after PWA phase completion |
| **DB_EXPENSE_SHARED** | `ALTER TABLE` or `GRANT`/`REVOKE` on `expense.employees`, `expense.verify_pin`, `expense.departments` (used by ALL PWA logins) | Adding a new department column, changing verify_pin function |
| **DB_GRANTS** | Any SQL with `GRANT`/`REVOKE`, editing `04_grants.sql`, or `pg_reload_conf()` for PostgREST | Granting new RPC access, revoking web_anon on a table |
| **DB_MIGRATIONS** | `CREATE`/`ALTER`/`DROP TABLE` on `public` schema or any schema shared across multiple PWAs | Adding a new `hub.pwa_registry` column, creating a view |
| **DOCKER_SHARED** | Creating or modifying shared Docker networks; `docker network` commands; `docker compose up/down` for containers on `proxy` network | Creating a new overlay network, starting proxy-dependent containers |
| **MEMORY_MD** | Updating `/root/.claude/projects/-var-www-360lm/memory/MEMORY.md` index file (brief lock — claim, write, release immediately) | Adding a new memory file pointer to the index |
| **NGINX_SHARED** | Editing nginx configs shared across multiple services (outside `/opt/openclaw/nginx.conf`) | Modifying a reverse-proxy rule affecting multiple PWAs |
| **SYSTEMD_ENV** | `systemctl --user set-environment` or `unset-environment` (affects all Node services in the same user env) | Setting `NODE_ENV=production`, unsetting a debug flag |
| **TRAEFIK** | Editing any `docker-compose.yml` with Traefik labels; editing `traefik.yml` or `.toml`; `docker compose up` for Traefik-connected containers | Adding a new service route, changing TLS cert path, updating middleware |

### Multi-Zone Operations

A single high-level operation often touches multiple zones. Example: editing a `docker-compose.yml` with Traefik labels hits both **DOCKER_SHARED** and **TRAEFIK**. When this occurs:

1. Identify all applicable zones
2. Acquire them in **alphabetical order** (D before T)
3. Release in **reverse alphabetical order** (T before D)

Example lock sequence for docker-compose with Traefik:
```
1. Acquire DOCKER_SHARED
2. Acquire TRAEFIK
3. [Edit docker-compose.yml]
4. Release TRAEFIK (reverse order)
5. Release DOCKER_SHARED
```

### Stale Lock Detection

At session start and before any lock acquisition, check lock timestamps:

- Lock > 30 minutes old AND session's "Last Activity" also > 30 minutes ago → **potentially stale**
- **Action:** Ask user: *"⚠️ [Zone] has been locked by [Session] since [time] — [X] minutes with no activity. Is that session still running? If it crashed, I can clear the stale lock — shall I?"*
- **Do NOT auto-clear** — user may still be working in the other session
- **User override:** If user says "clear it" / "that session is done":
  - Clear the lock, set row to FREE
  - Append to Lock History: `- [timestamp] | [YourSession] | Cleared stale [Zone] lock — user approved`
  - Proceed

### Session End Cleanup

When the user signals the session is ending:

1. Release **ALL locks** this session holds → set each row to FREE
2. Remove your row from Active Sessions table (or mark status as `ENDED`)
3. Append to Lock History: `- [timestamp] | [Session] | Session ended — all locks released`

### Safe Zones (No Lock Required)

These operations do NOT require lock acquisition:

| Safe For | Path / Resource | Reason |
|----------|----------------|--------|
| OC session only | `/opt/openclaw/` (all files) | OC-exclusive domain, no cross-session risk |
| OC session only | `/opt/openclaw/docker-compose.yml`, `/opt/openclaw/nginx.conf` | OC-only containers, OC-only reverse proxy |
| OC session only | `openclaw-gateway.service` systemd unit | OC-only service |
| PWA session only | `/var/www/360lm/{single_pwa}/` HTML/CSS/JS files | Single PWA, no cross-PWA impact |
| PWA session only | Single PWA's exclusive DB schema | No other PWA reads that schema |
| Both sessions | Individual `.md` files in memory folder (except `MEMORY.md`) | Separate files, no collision (each session has its own state file) |

### Bootstrap Warning

Sessions that started **before 2026-05-08** (creation of these rules) do not know this protocol. Until such a session is restarted or the user confirms it has ended:

- Warn: *"The [Session] session may predate these coordination rules. Treat all zones as potentially in-use until the user informs that session or it restarts."*
- Do NOT automatically lock or claim zones from old sessions

## Implementation Notes

### Lock History Maintenance

The Lock History table in `parallel_sessions.md` is a permanent record (newest entries at top). Trim to the **last 10 entries** at every session end. Format:

```
- [YYYY-MM-DD HH:MM] | [Session] | [description of action]
```

Example:
```
- 2026-06-23 09:15 | PWA | Session ended — all locks released
- 2026-06-23 09:14 | PWA | Released DB_MIGRATIONS after tour-pg v23 persistence
- 2026-06-23 09:10 | PWA | Released DOCKER_SHARED after proxy restart
```

### Session State Files

Each session type has its own state file (separate from `parallel_sessions.md`):

| Session | State File | Purpose |
|---------|-----------|---------|
| PWA | `session_state_pwa.md` | PWA-specific task, progress, pending items |
| OC | `session_state_oc.md` | OC-specific task, progress, pending items |
| INFRA | `session_state_infra.md` | Create when INFRA session starts |
| DB | `session_state_db.md` | Create when DB session starts |

**Rule:** Read only your own session state file at start; never read the other session's file. Update only your file at session end.

### Coordination Examples

**Scenario 1: PWA session wants to edit a PWA DB schema, OC session is idle**

```
1. PWA reads parallel_sessions.md
2. OC session present but Last Activity 2 hours ago
3. PWA checks: Is OC still active? Ask user: "OC session is 2+ hours idle — clear it?"
4. User confirms OC is done
5. PWA clears stale OC locks (if any)
6. PWA acquires DB_MIGRATIONS lock
7. PWA alters the schema
8. PWA releases DB_MIGRATIONS lock
9. PWA updates "Last Activity" timestamp
```

**Scenario 2: PWA and INFRA sessions both want to modify Traefik**

```
1. PWA reads parallel_sessions.md, sees INFRA is active
2. PWA tries to acquire TRAEFIK lock
3. TRAEFIK is FREE, PWA acquires it
4. PWA makes edit, releases TRAEFIK lock
5. INFRA session arrives, reads parallel_sessions.md
6. INFRA tries to acquire TRAEFIK lock
7. TRAEFIK is FREE (PWA released it), INFRA acquires it
8. INFRA makes different edit, releases TRAEFIK lock
```

**Scenario 3: Both PWA and DB sessions touch shared expense schema**

```
1. PWA wants to add a column to expense.employees (needs DB_EXPENSE_SHARED)
2. DB session is active, holding DB_GRANTS lock (from recent grant changes)
3. PWA acquires DB_EXPENSE_SHARED lock (different zone, no deadlock)
4. PWA alters expense.employees
5. PWA releases DB_EXPENSE_SHARED lock
6. Later, DB session acquires DB_EXPENSE_SHARED to change verify_pin grants
```

## Alternatives Considered

### 1. No Coordination (Status Quo Pre-2026-05-08)

Each session touches shared infra independently. Risk: silent overwrites, migration conflicts, Traefik routing failures, grant races, deadlocks.

**Rejected because:** Caused at least one documented outage (Traefik config loss during simultaneous edits) and DBT merge conflicts.

### 2. Distributed Locking (etcd, Redis, DB Advisory Locks)

Use external distributed lock service (e.g., PostgreSQL advisory locks, Redis SET NX, etcd). Each session acquires locks before writing.

**Rejected because:**
- Adds external dependency (Redis or etcd), increases operational cost
- PostgreSQL advisory locks require persistent session (not suitable for one-off CLI tasks)
- Complexity disproportionate to problem (only 2–4 sessions in practice)
- Lighter-weight protocol (this ADR) is sufficient for human-paced work

### 3. Git Branches for Infrastructure Code

Store Docker Compose, Traefik config, and grants in git. Each session works on a branch; code review before merge.

**Rejected because:**
- Infrastructure changes are often immediate (e.g., adding a grant, restarting Traefik). Code review delay not acceptable for operational fixes.
- Git conflicts are harder to resolve than simple file-based locking
- Doesn't prevent in-flight race conditions during review

### 4. Strict Session Scheduling (One Session at a Time)

Sessions are mutually exclusive — only one runs at a time.

**Rejected because:**
- Artificial bottleneck. PWA and OC sessions rarely touch the same infrastructure; forcing serialization is wasteful.
- User experience suffers (long waits for session start)
- Violates principle of least constraint

## Consequences

### Positive

1. **Race condition prevention** — Alphabetical lock ordering prevents deadlocks. Only one session writes to each shared zone at a time.
2. **Operational transparency** — Lock history is readable; users can see who locked what and for how long.
3. **Human-centric** — No external dependencies. Stale-lock detection respects user judgment (ask before clearing).
4. **Low operational overhead** — Protocol is lightweight; most operations proceed with no locks held (safe zones). Only ~10 zones are shared.
5. **Failure isolation** — If one session crashes with locks held, stale-lock detection catches it within 30 min.

### Negative

1. **Manual discipline required** — Developers must remember to acquire/release locks. Forgetting to release blocks other sessions. Mitigation: session end cleanup rules are strict.
2. **Read-after-write race** — If session releases a lock immediately after writing, the other session may read stale data before the change persists (e.g., before Docker/Traefik restart). Mitigation: protocol includes "complete the operation to operational state" rule (e.g., `docker compose up` for Traefik changes, not just label edit).
3. **Not distributed** — Works only with one machine. If infrastructure scales to multiple VPS instances, this protocol will need revision. Mitigation: current architecture is single VPS; ADR can be superseded if multi-host architecture is adopted.

### Risks

1. **Lock starvation** — If one session repeatedly acquires a zone and another never gets it, the second session starves. Mitigation: human-paced work makes this unlikely; if it occurs, user is aware and can negotiate.
2. **Forgotten lock release** — If a session crashes while holding locks, other sessions see stale locks. Mitigation: stale-lock detection (>30 min, no activity) with user override.
3. **Inconsistent zone assignment** — Two developers disagree on whether an operation needs a lock. Mitigation: this ADR is canonical; any ambiguity is resolved by adding the zone to the reference table.

## Related Decisions

- **ADR-015** — Dev/Prod Two Stacks: explains why dev and prod run on the same VPS, motivating this coordination protocol
- **ADR-009** — Each PWA Owns Its DB Schema: motivates DBT, DB_EXPENSE_SHARED, and DB_MIGRATIONS zones
- **ADR-017** — Traefik Docker Labels: motivates TRAEFIK and DOCKER_SHARED zones
- **ADR-029** — Dual-Write Access Control: motivates DB_GRANTS zone (PostgREST reload coordination)

## References

- Live session board: `/root/.claude/projects/-var-www-360lm/memory/parallel_sessions.md`
- Protocol feedback: `/root/.claude/projects/-var-www-360lm/memory/feedback_parallel_sessions.md`
- Session state: `/root/.claude/projects/-var-www-360lm/memory/session_state_pwa.md`, `session_state_oc.md`
- Project memory index: `/root/.claude/projects/-var-www-360lm/memory/MEMORY.md`
