|
|
@@ -179,6 +179,28 @@ interface RequestContext {
|
|
|
|
|
|
A proper discriminated union over `type` (not independent `type`/`data` unions), so `switch (event.type)` narrows `event.data` without casts. `seq` is the monotonic position in the log (`seq = log.length`); `time` is epoch ms.
|
|
|
|
|
|
+```ts type-equiv
|
|
|
+/** Sequence number of one existing event in a Session log. */
|
|
|
+type SessionSeq = BrandedNumber<'SessionSeq'>
|
|
|
+```
|
|
|
+
|
|
|
+```ts type-equiv
|
|
|
+/** A Session log gap, prefix length, or read offset, which may equal the event count. */
|
|
|
+type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
|
|
|
+```
|
|
|
+
|
|
|
+```ts type-equiv
|
|
|
+/** Inclusive Session event watermark, or `-1` before any event exists. */
|
|
|
+type SessionSeqCursor = SessionSeq | -1
|
|
|
+```
|
|
|
+
|
|
|
+```ts type-equiv
|
|
|
+/** One existing Session event position, or explicit absence. */
|
|
|
+type OptionalSessionSeq = SessionSeq | null
|
|
|
+```
|
|
|
+
|
|
|
+`SessionSeq(value)` and `SessionLogOffset(value)` admit only non-negative safe integers and reject negative zero. They add compile-time brands without changing the serialized number; arithmetic returns an ordinary `number` that callers must admit again through the constructor for its intended role.
|
|
|
+
|
|
|
```ts type-equiv
|
|
|
/**
|
|
|
* One immutable entry in the session log.
|
|
|
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|
|
[K in SessionEventType]: {
|
|
|
type: K
|
|
|
/** Monotonic sequence number within the session. */
|
|
|
- seq: number
|
|
|
+ seq: SessionSeq
|
|
|
/** Unix epoch milliseconds. */
|
|
|
time: number
|
|
|
data: SessionEventMap[K]
|
|
|
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|
|
* provider stream; when the field is absent, the event does not record which
|
|
|
* earlier events produced the message.
|
|
|
*/
|
|
|
- sourceEventSeqs?: number[]
|
|
|
+ sourceEventSeqs?: SessionSeq[]
|
|
|
/** How this event entered the surface; absent for non-surface events. */
|
|
|
surfaceOp?: SurfaceOp
|
|
|
} : object)
|
|
|
@@ -268,7 +290,7 @@ type SurfaceEventType =
|
|
|
*/
|
|
|
type SurfaceOp =
|
|
|
| 'append'
|
|
|
- | { op: 'replace'; start: number; end: number }
|
|
|
+ | { op: 'replace'; start: SessionSeq; end: SessionSeq }
|
|
|
```
|
|
|
|
|
|
`'append'` is the normal tail-append path. `replace` shadows surface entries from `start` through `end` inclusive (both must be valid surface seqs; `start === end` replaces a single entry) and inserts the new event in their place.
|
|
|
@@ -288,7 +310,7 @@ interface SurfaceIntent {
|
|
|
* absent, the event does not record which earlier events produced the message.
|
|
|
* Other surface events require a non-empty set when this field is present.
|
|
|
*/
|
|
|
- sourceEventSeqs?: number[]
|
|
|
+ sourceEventSeqs?: SessionSeq[]
|
|
|
}
|
|
|
```
|
|
|
|
|
|
@@ -306,7 +328,7 @@ Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the f
|
|
|
/** Readonly live projection of the message-producing session events. */
|
|
|
interface SessionSurface {
|
|
|
/** Current surface event sequences in model-visible order. */
|
|
|
- readonly nodes: readonly number[]
|
|
|
+ readonly nodes: readonly SessionSeq[]
|
|
|
/** Monotonic count of committed positional replacements. */
|
|
|
readonly replaceGeneration: number
|
|
|
}
|
|
|
@@ -320,13 +342,13 @@ interface SessionSurface {
|
|
|
/** One replacement operation observed while folding a session surface. */
|
|
|
interface SurfaceFoldReplacement {
|
|
|
/** Seq of the event that replaced the prior surface range. */
|
|
|
- seq: number
|
|
|
+ seq: SessionSeq
|
|
|
/** Declared inclusive start seq of the replaced surface range. */
|
|
|
- start: number
|
|
|
+ start: SessionSeq
|
|
|
/** Declared inclusive end seq of the replaced surface range. */
|
|
|
- end: number
|
|
|
+ end: SessionSeq
|
|
|
/** Actual surface entries removed by the operation, in surface order. */
|
|
|
- shadowedSeqs: number[]
|
|
|
+ shadowedSeqs: SessionSeq[]
|
|
|
}
|
|
|
```
|
|
|
|
|
|
@@ -334,7 +356,7 @@ interface SurfaceFoldReplacement {
|
|
|
/** Complete result of replaying the surface operations in a session log. */
|
|
|
interface SurfaceFoldResult {
|
|
|
/** Current surface event sequences in model-visible order. */
|
|
|
- nodes: number[]
|
|
|
+ nodes: SessionSeq[]
|
|
|
/** Replacement operations in event order. */
|
|
|
replacements: SurfaceFoldReplacement[]
|
|
|
}
|
|
|
@@ -358,13 +380,15 @@ declare class Session {
|
|
|
get surface(): SessionSurface;
|
|
|
/**
|
|
|
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
|
|
|
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
|
|
|
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
|
|
|
* `Session` is created without a store-owned header, a minimal header is
|
|
|
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
|
|
|
* `session.header` is always present. Kept out of the event log — it is a
|
|
|
* storage concern, not replayable conversation state.
|
|
|
*/
|
|
|
readonly header: SessionHeader;
|
|
|
+ /** Number of leading events inherited from this Session's fork parent. */
|
|
|
+ readonly inheritedEventCount: SessionLogOffset;
|
|
|
/** The session identity, derived from its durable header's single copy. */
|
|
|
get id(): SessionId;
|
|
|
/**
|
|
|
@@ -373,9 +397,9 @@ declare class Session {
|
|
|
* construction — replay, fork, or resume — and were never published on the
|
|
|
* `session/event` firehose (constructor seeds do not emit), so consumers
|
|
|
* that replay the log as a publication substitute (telemetry adoption)
|
|
|
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
|
|
|
- * boundary: a resumed session's constructor seed is its full stored log,
|
|
|
- * while its header keeps the original fork value — this field is the
|
|
|
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
|
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
|
+ * log, while the inherited count keeps the original fork value — this field is the
|
|
|
* in-process construction fact.
|
|
|
*
|
|
|
* Not persisted itself: a seeded session projects it into the log as the
|
|
|
@@ -389,16 +413,22 @@ declare class Session {
|
|
|
* store attaches and therefore does not publish either. Otherwise this seq
|
|
|
* holds an ordinary published write.
|
|
|
*/
|
|
|
- readonly firstLiveSeq: number;
|
|
|
+ readonly firstLiveSeq: SessionLogOffset;
|
|
|
/**
|
|
|
* Create a detached session by validating and snapshotting borrowed seed
|
|
|
* events and storage metadata.
|
|
|
* @param id - session identity.
|
|
|
* @param seed - optional borrowed replay or fork events.
|
|
|
* @param header - optional borrowed storage metadata.
|
|
|
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
|
|
|
* @returns a detached session.
|
|
|
*/
|
|
|
- static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
|
|
|
+ static create(
|
|
|
+ id: SessionId,
|
|
|
+ seed?: readonly SessionEvent[],
|
|
|
+ header?: SessionHeader,
|
|
|
+ inheritedEventCount?: SessionLogOffset,
|
|
|
+ ): Session;
|
|
|
/**
|
|
|
* Restore a detached session by taking ownership of fresh persistence values.
|
|
|
* The storage format, event envelopes, sequence continuity, surface transitions,
|
|
|
@@ -406,15 +436,21 @@ declare class Session {
|
|
|
* @param id - restored session identity.
|
|
|
* @param seed - fresh detached events whose ownership is transferred.
|
|
|
* @param header - fresh detached metadata whose ownership is transferred.
|
|
|
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
|
* @returns a restored detached session.
|
|
|
*/
|
|
|
- static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
|
|
|
+ static fromRestore(
|
|
|
+ id: SessionId,
|
|
|
+ seed: readonly SessionEvent[],
|
|
|
+ header: SessionHeader,
|
|
|
+ inheritedEventCount: SessionLogOffset,
|
|
|
+ ): Session;
|
|
|
/**
|
|
|
* Return the immutable event stored at one exact sequence number.
|
|
|
* @param seq - event sequence number.
|
|
|
* @returns the accepted event, or undefined when the log does not contain it.
|
|
|
*/
|
|
|
- eventAt(seq: number): SessionEvent | undefined;
|
|
|
+ eventAt(seq: SessionSeq): SessionEvent | undefined;
|
|
|
/**
|
|
|
* Materialize an immutable snapshot of a half-open event sequence range.
|
|
|
* A full current snapshot is reused until the next append; every previously
|
|
|
@@ -423,9 +459,23 @@ declare class Session {
|
|
|
* @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
|
|
|
* @returns a frozen array of the selected deeply frozen events.
|
|
|
*/
|
|
|
- snapshotEvents(fromSeq: number = 0, toSeqExclusive: number = this.log.length): readonly SessionEvent[];
|
|
|
+ snapshotEvents(
|
|
|
+ fromSeq: SessionLogOffset = SessionLogOffset(0),
|
|
|
+ toSeqExclusive: SessionLogOffset = this.seq,
|
|
|
+ ): readonly SessionEvent[];
|
|
|
+ /**
|
|
|
+ * Return this Session's events after its fork-inherited prefix.
|
|
|
+ * @returns a fresh array containing child-owned events in log order.
|
|
|
+ */
|
|
|
+ ownEvents(): readonly SessionEvent[];
|
|
|
+ /**
|
|
|
+ * Whether one existing event position is outside the fork-inherited prefix.
|
|
|
+ * @param seq - event position in this Session.
|
|
|
+ * @returns true when the event belongs to this Session rather than its parent.
|
|
|
+ */
|
|
|
+ isOwnSeq(seq: SessionSeq): boolean;
|
|
|
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
|
|
|
- get seq(): number;
|
|
|
+ get seq(): SessionLogOffset;
|
|
|
/**
|
|
|
* Append one typed event to the log and synchronously notify observers via
|
|
|
* the store-owned, module-private publication hooks. The hot path never blocks
|
|
|
@@ -525,7 +575,7 @@ Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and
|
|
|
|
|
|
`ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API:
|
|
|
|
|
|
-- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `boundary` seq (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events plus child metadata (`parentSession`, `seedLength`, and inherited `cwd`).
|
|
|
+- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `SessionSeq` boundary (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events, `parentSession`, `isSeeded: true`, the exact `inheritedEventCount`, and inherited `cwd`.
|
|
|
|
|
|
An explicit `boundary` lets callers fork from any stable between-turn position, including a previous `turn/end` or a later standalone log-only event, even if the source has newer events or an open current turn. The API rejects a prefix that ends inside an open turn instead of clipping silently. Broader execution-relation sanity stays in the existing `dsh-invariants` plugin and persistence repair path rather than being duplicated in `fork()`. `dsh-subagent-fork-in-process` keeps its completed-prefix clipping because tool-time delegation usually starts while the parent turn is open; ordinary session branching should make the requested boundary explicit.
|
|
|
|
|
|
@@ -574,7 +624,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
|
|
|
|
|
|
## The end-seed boundary: `session/end-seed`
|
|
|
|
|
|
-A seeded session — resume, fork, or replay — appends this log-only event immediately after its constructor seed, as its first live write. Events before it have smaller seq values and came from the seed. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
|
|
|
+A Session constructed with an explicit seed — restore, fork, or replay — appends this log-only event immediately after that constructor seed, as its first live write. Events before it have smaller seq values and came through construction. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. It does not define fork ownership; `isSeeded` plus `inheritedEventCount` do. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
|
|
|
|
|
|
An explicitly supplied empty seed writes `session/end-seed` at seq 0, which distinguishes an empty resumed session from a fresh one. A seed already ending in `session/end-seed` is not re-marked, so reopening an untouched session does not grow its log per pickup. Locate the LAST `session/end-seed` in stored history rather than assuming one exists at `firstLiveSeq`: after a pickup with no work, the event has a smaller seq than the next lifecycle's `firstLiveSeq`.
|
|
|
|
|
|
@@ -630,7 +680,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
|
|
|
* @param signal - optional caller cancellation for persistence reads.
|
|
|
* @returns the current attached state or persisted header and event prefix.
|
|
|
*/
|
|
|
-inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }>
|
|
|
+inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
|
|
|
|
|
|
/**
|
|
|
* Read all visible Session rows without resuming an Agent.
|
|
|
@@ -750,7 +800,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
|
|
|
@Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
|
|
|
```
|
|
|
|
|
|
-Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
|
|
|
+Types: [SessionId](core.md) · [SessionInspection](persistence.md) · [SessionSearchRequest](session-query.md)
|
|
|
|
|
|
Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
|
|
|
|
|
|
@@ -883,7 +933,7 @@ list(): Session[]
|
|
|
* `SessionStore`'s id policy.
|
|
|
* @returns The created live child session.
|
|
|
*/
|
|
|
-fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
|
|
|
+fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
|
|
|
```
|
|
|
|
|
|
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
|