@x-otto/session 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @x-otto/session
2
+
3
+ Agent session pure domain primitives: branching entry tree, compaction storage (append forever), context building, paginated listing, SQLite relational persistence with write leases.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @x-otto/session
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { InMemorySession, createSessionStore } from '@x-otto/session'
15
+
16
+ // In-memory session (entry tree branching)
17
+ const session = new InMemorySession({ model: 'claude-sonnet-4-5' })
18
+ session.append({ role: 'user', content: 'Hello' })
19
+ const fork = session.branch(session.leafId) // fork at current leaf
20
+ session.recordCompaction({ summary: 'User greeted', replacement: [{ role: 'assistant', content: 'Hi' }] })
21
+ const ctx = session.buildContext() // entry tree → message array
22
+
23
+ // Persistent store
24
+ const store = createSessionStore({ kind: 'memory' })
25
+ await store.save('session-1', session.snapshot())
26
+ const loaded = await store.load('session-1')
27
+ ```
28
+
29
+ ## Core Architecture
30
+
31
+ ### Entry Tree (InMemorySession)
32
+
33
+ Message history as an entry tree:
34
+
35
+ - `SessionEntry<T>` discriminated union: `message` / `compaction` / `clear`
36
+ - `walkBranch()` — traverse along `parentId` chain to `leafId`
37
+ - `recordCompaction(summary, replacement)` — adds a compaction entry with incremental replacement stripping of the prior boundary (RFC-160 D2)
38
+ - `branch(entryId)` — fork by resetting `leafId`; fail-fast if target boundary is stripped
39
+ - `buildContext()` — walks from the most recent boundary (compaction/clear), consuming only its `replacement`
40
+ - `capStoredHistory(max)` — pure in-memory limit (DB retains all data, append-forever)
41
+ - `capStoredHistoryByBytes(maxBytes)` — RFC-178 byte-budget hard gate
42
+
43
+ ### Data Safety Invariants (RFC-159, post-2026-07-13 data loss)
44
+
45
+ | Invariant | Guard |
46
+ |-----------|-------|
47
+ | save path NEVER deletes `session_entries` rows | No delete code path; `deleteAllEntries` removed entirely |
48
+ | `next_seq` monotonic, never rolls back | Runtime assert: `seq > MAX(seq)` in append transaction |
49
+ | No lease = read-only, no silent overwrites | Save gate throws `SessionWriteLeaseDeniedError` |
50
+ | Stripped state NEVER persisted/checkpointed/forked | `appendEntry` first-line reject + runtime 4-exit assertions + `branch()` fail-fast |
51
+ | Blob absence = fail-fast on assembly | `decodeReplacementFromBlobs` throws on missing blob |
52
+
53
+ ### Relational Persistence (RFC-037 + RFC-159 + RFC-160)
54
+
55
+ `SqliteSessionRepository` (`SESSION_MIGRATIONS` v1-v5) + `RelationalSessionPersistence` — the production persistence path:
56
+
57
+ **Write**: incremental append via `entry_id` cursor (fallback to full idempotent append with `ON CONFLICT(session_id, entry_id) DO NOTHING`). Compaction replacement encoded to content-addressed blobs (sha256 → `message_blobs` dedup) before append.
58
+
59
+ **Read**: tail window (load last N entries when DB exceeds `maxLoadEntries`), inactive replacement stripping (only active boundary retains replacement; new-format rows mark with zero blob reads), dual-format assembly (old inline / new hashes, R6 = assembly-equivalent).
60
+
61
+ **Hydrate**: `loadEntriesByIds` point query + `hydrateStrippedReplacements` (dual-format). `ReplacementHydrationCapable` capability interface for runtime 4-exit detection (checkpoint, forkSession, forkFromCheckpoint, restore).
62
+
63
+ ### Write Lease (`session-write-lease.ts`)
64
+
65
+ Per-session single-writer: `O_EXCL` lockfile + token + unref heartbeat + stale takeover (60s). Process exit hook releases synchronously (`/exit -r` zero-wait handoff).
66
+
67
+ ### Thin Persistence Subclasses
68
+
69
+ InMemory / File / Remote adapters implementing `SessionPersistence<T>`, all thin wrappers over `@x-otto/persistence` backends. File version `@deprecated` (blob legacy). None participate in tail window / stripping / lease.
70
+
71
+ ## Key Files
72
+
73
+ ```
74
+ src/
75
+ types.ts # SessionEntry, SessionContext, SessionPersistence, etc.
76
+ in-memory-session.ts # Entry tree + compaction + branch fail-fast
77
+ session-repository.ts # SessionRepository interface
78
+ sqlite-session-repository.ts # SQLite + MIGRATIONS v1-v5 (message_blobs)
79
+ relational-session-persistence.ts # Incremental append + lease gate + tail window + hydration
80
+ relational-bridge.ts # Snapshot↔rows bridge + dual-format assembly (R6)
81
+ replacement-blob-codec.ts # Content-addressed codec (sha256, fail-fast on absent)
82
+ replacement-hydrator.ts # Stripped state rehydration + capability interface
83
+ session-write-lease.ts # O_EXCL lockfile + token + heartbeat + stale takeover
84
+ store.ts # InMemorySessionStore
85
+ memory-persistence.ts # InMemory session persistence adapter
86
+ file-persistence.ts # File session persistence adapter (deprecated)
87
+ remote-persistence.ts # Remote HTTP session persistence adapter
88
+ message-bytes.ts # RFC-178 estimatedContentBytes + capStoredHistoryByBytes
89
+ constants.ts # Default limits
90
+ index.ts # Barrel exports
91
+ scripts/
92
+ migrate-replacement-blobs.mjs # One-time inline→hashes migration (dry-run / apply / verify)
93
+ replacement-residency-probe.mjs # Memory residency dual-probe (fixture + production)
94
+ ```
95
+
96
+ ## Dependencies
97
+
98
+ - Internal: `@x-otto/persistence`, `@x-otto/env`
99
+ - External: `better-sqlite3`
100
+ - Consumers: `@x-otto/runtime` (PersistenceSync, SessionManager, TimeTravelController), `@x-otto/coding` (session-pool-factory)
101
+
102
+ ## Testing
103
+
104
+ ```bash
105
+ pnpm --filter @x-otto/session typecheck
106
+ pnpm --filter @x-otto/session build
107
+ pnpm vitest run packages/session/tests/
108
+ ```
109
+
110
+ 14 test files including:
111
+ - `rfc159-incident-replay-e2e`: 2026-07-13 data loss incident replay (real disk DB, dual-process, zero-loss)
112
+ - `session-write-lease`: mutual exclusion / stale takeover / fencing
113
+ - `replacement-residency` + `replacement-blob-codec` + `rfc160-integration-e2e`: strip/hydrate/dual-format/migration equivalence
114
+ - `rfc178-content-bytes`: byte budget incremental counter + hard gate
115
+
116
+ ## Related RFCs
117
+
118
+ - [RFC-037 Relational + append-only](../../docs/rfc/RFC-037-backend-database-relational-append.md)
119
+ - [RFC-159 Append-forever + write lease](../../docs/rfc/RFC-159-session-persistence-append-forever.md) (2026-07-13 postmortem)
120
+ - [RFC-160 Replacement residency](../../docs/rfc/RFC-160-compaction-replacement-residency.md) (blob dedup + residency data)
121
+ - [RFC-178 Content byte budget](../../docs/rfc/RFC-178-content-bytes-cap.md)