@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 +121 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
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)
|