ADR-058: Machine-Specific Session Logs with Multi-Device Sync
Status
Accepted - January 6, 2026
Context
Problem Statement
Session logs (SESSION-LOG-YYYY-MM-DD.md) capture daily Claude Code session activity. Current issues:
- Git Pollution: Session logs in git repos sync back to GitHub, mixing personal machine activity with shared code
- Multi-Machine Conflict: Contributors using multiple machines get merge conflicts on session logs
- No Machine Identity: Cannot distinguish which machine generated which session log
- Lost Context: Session logs are machine-specific context that should persist per-machine
- Backup Gap: No cloud backup of machine-specific session history
Current State
coditect-rollout-master/
└── docs/
└── session-logs/ # ❌ IN GIT - syncs to GitHub
├── SESSION-LOG-2026-01-04.md
├── SESSION-LOG-2026-01-05.md
└── SESSION-LOG-2026-01-06.md # Which machine?
Requirements
- Session logs must be machine-specific with unique hardware identification
- Session logs must be stored outside git-tracked directories
- Each machine maintains its own session log history
- Support cloud backup for disaster recovery
- Support cross-machine querying (view logs from other machines)
- No merge conflicts when syncing coditect-core
Decision
Implement a machine-identified session log system with:
- Hardware-based Machine UUID generated from hardware characteristics
- Local storage outside git in
~/.coditect-data/session-logs/ - Cloud sync to GCS bucket with machine-namespaced paths
- Conflict-free replication using machine UUID as partition key
Architecture
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MACHINE-SPECIFIC SESSION LOG ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ MACHINE A (MacBook Pro - hal-mbp-2024) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ~/.coditect-data/ │ │
│ │ ├── machine-id.json # Hardware UUID + metadata │ │
│ │ │ { │ │
│ │ │ "machine_uuid": "a1b2c3d4-...", │ │
│ │ │ "hardware_hash": "sha256:abc123...", │ │
│ │ │ "hostname": "hal-mbp-2024", │ │
│ │ │ "created": "2026-01-06T10:30:00Z" │ │
│ │ │ } │ │
│ │ │ │ │
│ │ └── session-logs/ │ │
│ │ ├── SESSION-LOG-2026-01-04.md │ │
│ │ ├── SESSION-LOG-2026-01-05.md │ │
│ │ └── SESSION-LOG-2026-01-06.md │ │
│ │ │ │
│ │ session-log-sync.py ──▶ GCS Upload │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ MACHINE B (iMac - hal-imac-studio) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ~/.coditect-data/ │ │
│ │ ├── machine-id.json # Different UUID │ │
│ │ │ { │ │
│ │ │ "machine_uuid": "e5f6g7h8-...", │ │
│ │ │ "hardware_hash": "sha256:def456...", │ │
│ │ │ "hostname": "hal-imac-studio", │ │
│ │ │ "created": "2026-01-01T08:00:00Z" │ │
│ │ │ } │ │
│ │ │ │ │
│ │ └── session-logs/ │ │
│ │ ├── SESSION-LOG-2026-01-03.md │ │
│ │ └── SESSION-LOG-2026-01-06.md # Same date, different machine │ │
│ │ │ │
│ │ session-log-sync.py ──▶ GCS Upload │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ CLOUD STORAGE (GCS) │
│ gs://coditect-session-logs/ │
│ ├── tenant-abc123/ # Tenant isolation │
│ │ ├── machines/ │
│ │ │ ├── a1b2c3d4-.../ # Machine A UUID │
│ │ │ │ ├── machine-id.json │
│ │ │ │ ├── SESSION-LOG-2026-01-04.md │
│ │ │ │ ├── SESSION-LOG-2026-01-05.md │
│ │ │ │ └── SESSION-LOG-2026-01-06.md │
│ │ │ │ │
│ │ │ └── e5f6g7h8-.../ # Machine B UUID │
│ │ │ ├── machine-id.json │
│ │ │ ├── SESSION-LOG-2026-01-03.md │
│ │ │ └── SESSION-LOG-2026-01-06.md │
│ │ │ │
│ │ └── index/ # Cross-machine index │
│ │ └── session-index.json # Aggregated metadata │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Machine UUID Generation
The machine UUID is generated from hardware characteristics to ensure:
- Persistence: Same UUID across reboots and reinstalls
- Uniqueness: No collision between machines
- Privacy: Hash-based, doesn't expose raw serial numbers
import hashlib
import uuid
import subprocess
import platform
def generate_machine_uuid() -> dict:
"""Generate a persistent, unique machine identifier."""
# Collect hardware identifiers (macOS)
hardware_data = []
# 1. Hardware UUID (from IOKit)
try:
result = subprocess.run(
['ioreg', '-rd1', '-c', 'IOPlatformExpertDevice'],
capture_output=True, text=True
)
for line in result.stdout.split('\n'):
if 'IOPlatformUUID' in line:
hw_uuid = line.split('"')[-2]
hardware_data.append(f"hw_uuid:{hw_uuid}")
break
except Exception:
pass
# 2. Serial number (hashed for privacy)
try:
result = subprocess.run(
['ioreg', '-l', '-c', 'IOPlatformExpertDevice'],
capture_output=True, text=True
)
for line in result.stdout.split('\n'):
if 'IOPlatformSerialNumber' in line:
serial = line.split('"')[-2]
# Hash the serial for privacy
serial_hash = hashlib.sha256(serial.encode()).hexdigest()[:16]
hardware_data.append(f"serial_hash:{serial_hash}")
break
except Exception:
pass
# 3. MAC address of primary interface (hashed)
try:
result = subprocess.run(['ifconfig', 'en0'], capture_output=True, text=True)
for line in result.stdout.split('\n'):
if 'ether' in line:
mac = line.split()[1]
mac_hash = hashlib.sha256(mac.encode()).hexdigest()[:16]
hardware_data.append(f"mac_hash:{mac_hash}")
break
except Exception:
pass
# 4. CPU identifier
hardware_data.append(f"cpu:{platform.processor()}")
# 5. Machine model
try:
result = subprocess.run(
['sysctl', '-n', 'hw.model'],
capture_output=True, text=True
)
hardware_data.append(f"model:{result.stdout.strip()}")
except Exception:
pass
# Create composite hash
composite = '|'.join(sorted(hardware_data))
hardware_hash = hashlib.sha256(composite.encode()).hexdigest()
# Generate UUID from hash (deterministic)
machine_uuid = str(uuid.UUID(hardware_hash[:32]))
return {
'machine_uuid': machine_uuid,
'hardware_hash': f"sha256:{hardware_hash}",
'hostname': platform.node(),
'platform': platform.system(),
'platform_version': platform.mac_ver()[0] if platform.system() == 'Darwin' else platform.release(),
'model': hardware_data[-1].split(':')[1] if hardware_data else 'unknown',
'created': datetime.now(timezone.utc).isoformat()
}
Synchronization Strategy
Conflict-Free by Design
Key Insight: By using machine UUID as the partition key, we eliminate conflicts entirely.
┌─────────────────────────────────────────────────────────────────┐
│ CONFLICT-FREE REPLICATION (CRF) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Traditional Approach (CONFLICTS): │
│ ───────────────────────────────── │
│ Machine A writes: SESSION-LOG-2026-01-06.md │
│ Machine B writes: SESSION-LOG-2026-01-06.md ← CONFLICT! │
│ │
│ Our Approach (NO CONFLICTS): │
│ ──────────────────────────── │
│ Machine A writes: machines/uuid-A/SESSION-LOG-2026-01-06.md │
│ Machine B writes: machines/uuid-B/SESSION-LOG-2026-01-06.md │
│ │
│ Same date, different paths → No conflict possible! │
│ │
└─────────────────────────────────────────────────────────────────┘
Sync Modes
| Mode | Description | When Used |
|---|---|---|
| Push | Local → Cloud | After session end, daily backup |
| Pull Index | Cloud index → Local | Start of session, show available |
| Pull Logs | Specific logs → Local cache | On-demand viewing |
| Full Sync | Bidirectional reconciliation | Weekly or manual |
Directory Structure
~/.coditect-data/
├── machine-id.json # Machine identification (generated once)
├── session-logs/ # LOCAL session logs (this machine only)
│ ├── SESSION-LOG-2026-01-04.md
│ ├── SESSION-LOG-2026-01-05.md
│ └── SESSION-LOG-2026-01-06.md
│
├── session-logs-cache/ # CACHED logs from other machines (read-only)
│ ├── e5f6g7h8-.../ # Other machine's logs
│ │ └── SESSION-LOG-2026-01-03.md
│ └── index.json # Cross-machine index
│
└── context-storage/ # ADR-118 Four-Tier databases
├── org.db # Tier 2: IRREPLACEABLE
├── sessions.db # Tier 3: REGENERABLE
└── platform.db # Tier 1: REGENERABLE
Consequences
Positive
- No Git Conflicts: Session logs never sync to GitHub, eliminating merge conflicts
- Machine Traceability: Every log linked to specific hardware via UUID
- Cloud Backup: All session history preserved in GCS
- Cross-Machine Visibility: Can view logs from any machine (on-demand)
- Privacy: Hardware identifiers hashed, not exposed
- Offline First: Works without internet, syncs when available
Negative
- Complexity: More moving parts than simple git-tracked files
- Storage Cost: GCS storage costs (~$0.02/GB/month)
- Setup Required: Initial setup generates machine ID
Risks
| Risk | Mitigation |
|---|---|
| Machine ID collision | UUID from SHA-256 hash has negligible collision probability |
| GCS access failure | Local logs always authoritative; sync is best-effort |
| Hardware change | Regenerate machine ID; link old UUID in metadata |
| Lost machine ID | Can regenerate (deterministic); cloud has backup |
Implementation
Required Changes
- ADR-057 Update: Add session-log setup to initial setup script
- New Script:
session-log-sync.pyfor cloud sync - Hook: Post-session hook to trigger sync
- Migration: One-time migration of existing logs
Commands
# Initialize machine ID (first time)
python3 scripts/session-log-init.py
# Sync to cloud
python3 scripts/session-log-sync.py --push
# View available logs (all machines)
python3 scripts/session-log-sync.py --index
# Fetch specific log from another machine
python3 scripts/session-log-sync.py --pull --machine e5f6g7h8 --date 2026-01-03
# Migrate from git-tracked location
python3 scripts/session-log-migrate.py
Related Documents
- ADR-053: Cloud Context Sync Architecture
- ADR-057: CODITECT Core Initial Setup
- ADR-118: Four-Tier Database Architecture
- MEMORY-MANAGEMENT-GUIDE.md
Author: AI Assistant Reviewed By: Hal Casteel Approved: January 6, 2026