@deepseek-ai/dsh-session 0.1.1-rc.2 → 0.1.2-alpha.3
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.i18n.yaml +2 -2
- package/README.md +98 -57
- package/README.zh.md +104 -63
- package/lib/index.js +89 -193
- package/lib/invariant.js +2 -2
- package/lib/types/chunk-rows.d.ts +24 -11
- package/lib/types/chunk-rows.js +36 -15
- package/lib/types/index.d.ts +3 -5
- package/lib/types/index.js +7 -7
- package/lib/types/invariant.js +1 -2
- package/lib/types/known-event-types.d.ts +5 -2
- package/lib/types/known-event-types.js +8 -2
- package/lib/types/repair.js +4 -3
- package/lib/types/seq-ranges.d.ts +17 -0
- package/lib/types/seq-ranges.js +73 -0
- package/lib/types/types.d.ts +19 -28
- package/lib/types/types.js +3 -2
- package/package.json +17 -14
- package/lib/types/json.d.ts +0 -36
- package/lib/types/json.js +0 -174
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/core/session/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 0d691b31c4918152ecf1092002646296c5d9984a
|
|
6
|
+
README.zh.md: 2118def74c059f6637f12a9aeba20ff92a5d2363
|
package/README.md
CHANGED
|
@@ -1,104 +1,130 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The event-sourced session log and in-memory store for users and maintainers building, inspecting, or extending the durable record behind every agent interaction."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-session
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-session` provides the append-only session log that records an agent's whole interaction history — the single source of truth every model-visible fact flows through. The LLM message history is *derived* from the log (`deriveMessages()`), never stored separately, so replay is re-derivation from the same events and compaction can shadow older surface entries without deleting history. The package also provides the in-memory store (`ctx.sessions`), the typed `SessionEvent` vocabulary that plugins extend by declaration merging, and the surface layer that orders message-producing events. Persistence is deliberately a separate concern: backends subscribe to `session/event` and flush on `session/flush`. Choose it as the foundation of any agent session; it runs no model calls itself.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
8
22
|
|
|
9
|
-
|
|
23
|
+
-----
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
Mount `dsh-session` wherever a session must exist. It creates and holds event-sourced `Session` instances in memory; durable storage is layered on by a persistence plugin that subscribes to the `session/event` feed.
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
- `ctx.sessions.flush(session)` dispatches the awaited parallel durability checkpoint through the session's captured scope. Every listener starts and the call waits for all to settle before reporting failure; unpublished, detached, and stale objects reject.
|
|
17
|
-
- `ctx.sessions.fork(source, boundary?, childSessionId?): Session` — Resolve a live session object or id, select a seed through the inclusive `boundary` event seq (default: current last event), require that prefix to end outside an open turn, and create a live child session with lineage metadata.
|
|
18
|
-
- `ctx.sessions.get(id: SessionId): Session | undefined`
|
|
19
|
-
- `ctx.sessions.list(): Session[]`
|
|
30
|
+
### Create and inspect sessions
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
`ctx.sessions.create()` builds a live session bound to the calling fiber; `get(id)` and `list()` find sessions, and `fork()` creates a child session from a stable prefix of a live one.
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
```text
|
|
35
|
+
const session = ctx.sessions.create(sessionId, { meta: { cwd: '/workspace' } })
|
|
36
|
+
ctx.sessions.get(sessionId) // the live session
|
|
37
|
+
ctx.sessions.list() // every live session, in creation order
|
|
38
|
+
```
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
- `enter(session)` performs the collision check, publishes without announcing, and returns an entry-bound idempotent detach. Concurrent same-id preparations are allowed, but only one entry succeeds; a stale detach cannot remove its replacement.
|
|
27
|
-
- `announce(session)` emits the single creation edge and rejects repeat or reentrant announcements. Detach during that dispatch is deferred and later emits the paired disposal edge; an unannounced entry emits neither lifecycle edge.
|
|
40
|
+
### Append and derive
|
|
28
41
|
|
|
29
|
-
`
|
|
42
|
+
`session.append(type, data, opts?)` commits one typed event — it snapshots and freezes the payload, validates it as lossless JSON, and notifies observers. `session.deriveMessages()` projects the log into the `Message[]` the model sees, incrementally and cached:
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
```text
|
|
45
|
+
session.append('user/message', { role: 'user', content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
|
|
46
|
+
{ surfaceOp: 'append' })
|
|
47
|
+
session.deriveMessages() // the derived model history
|
|
48
|
+
```
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
Surface events (`user/message`, `assistant/message`, `tool/result`) must declare how they join the ordered surface; raw chunks, boundaries, and other log-only events never produce a message.
|
|
34
51
|
|
|
35
|
-
###
|
|
52
|
+
### Fork a session
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
`ctx.sessions.fork(source, boundary?, childSessionId?)` selects source events through an inclusive `boundary` seq (default: the current last event), requires the prefix to end outside an open turn, and creates a live child session with lineage metadata. A tool-time delegation that must branch mid-turn clips to a completed prefix instead.
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
- `session.deriveMessages()` incrementally projects each new surface entry once and returns a fresh array over the complete identified, frozen messages stored by those entries. Assistant messages preserve the provider and model that produced them plus adapter-private replay state in their model source. A surface rewrite rebuilds the projection; there is no raw-log fallback.
|
|
41
|
-
- `session.deriveEventMessage(event)` is the canonical per-event projection used by reconstruction and request checks.
|
|
42
|
-
- `session.surface` exposes the readonly `SessionSurface` view owned by the session's single incremental surface manager; `replaceGeneration` changes on every committed rewrite.
|
|
43
|
-
- `session.events` is a cached frozen snapshot invalidated by append; accepted events remain deeply frozen.
|
|
44
|
-
- `session.seq`, `session.id` — current sequence and readonly typed identity.
|
|
45
|
-
- `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`/`delegationDepth`). Construction validates the durable record and requires its id to match `session.id`.
|
|
56
|
+
### Flush durable state
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
`ctx.sessions.flush(session)` dispatches the awaited durability checkpoint: every persistence listener flushes and the call settles after all of them. A producer that needs an immediate durability barrier awaits it instead of assuming the write-behind drained.
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
-----
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
<a id="understand-the-implementation"></a>
|
|
63
|
+
## Understand the implementation
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
<details>
|
|
66
|
+
<summary>Implementation internals — click to expand</summary>
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
This section explains how the package realizes the behavior above; the observable contract is covered in [Use this package](#use-this-package).
|
|
56
69
|
|
|
57
|
-
###
|
|
70
|
+
### Design concept
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
The package is built on event sourcing: a `Session` is an append-only log of typed `SessionEvent`s, and everything else — model history, transcripts, telemetry, titles, persistence — derives from that stream. The surface is a derived projection: an incremental manager validates append candidates, advances the ordered view from committed events, and tracks a `replaceGeneration` that bumps on every committed rewrite. Model-visible means logged: anything that reaches a model request must be reconstructable from the log. The shared [row codec](src/chunk-rows.ts) losslessly converts event sequences to compact rows and back, preserves unrecognized events verbatim, and rejects malformed rows. Persistence backends decide whether to pack writes; bounded history transports can use the same rows while retaining the complete logical interval and exact decoding for consumers that need token boundaries.
|
|
60
73
|
|
|
61
|
-
### Request
|
|
74
|
+
### Request headers
|
|
62
75
|
|
|
63
|
-
`request/header`
|
|
76
|
+
`request/header` stores a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, `change`, or `series`. An explicit message-series start or a surface replacement writes a `series` snapshot when the envelope is unchanged; a simultaneous change uses `startsSeries: true`. Same-series steps, retries, and ordinary later turns inherit the latest snapshot. `adapterDefaults` distinguishes values resolved by the adapter from explicit settings, and `foldRequestHeader()` selects the latest snapshot. This self-contained record supports partial-window rendering and exact reconstruction at the cost of growth per message series; the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md) owns the detail.
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
### Source map
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
| File | Role |
|
|
81
|
+
|---|---|
|
|
82
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: `SessionStore` service, store lifecycle, `fork`, `flush` |
|
|
83
|
+
| [`src/types.ts`](src/types.ts) | `SessionEventMap`, `SessionEvent`, `UserMessage`, `SessionHeader`, `TurnEndReasonMap` |
|
|
84
|
+
| [`src/surface.ts`](src/surface.ts) | Ordered surface projection, replacement validation, `deriveEventMessage` |
|
|
85
|
+
| [`src/request-header.ts`](src/request-header.ts) | `request/header` folding and reconstruction |
|
|
86
|
+
| [`dsh-util-values`](../../util/values/README.md) | Shared lossless JSON validation and detached snapshots |
|
|
87
|
+
| [`src/chunk-rows.ts`](src/chunk-rows.ts) | Shared compact-row storage codec for persistence backends |
|
|
88
|
+
| [`src/repair.ts`](src/repair.ts) | Cold repair of crash-orphaned logs |
|
|
89
|
+
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: seq, turn/step enclosure, tool call/result pairing |
|
|
68
90
|
|
|
69
|
-
###
|
|
91
|
+
### Append validation
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
Every append uses the shared iterative `snapshotJsonValue()` pass, which reads, validates, and copies each nested value once, so a stateful getter cannot supply one value to validation and another to storage. Non-lossless-JSON payloads (BigInt, cycles, sparse arrays, `-0`, exotic prototypes) are rejected at the append site, before any backend flush. Surface events additionally validate marker shape, cited source-event seqs, and complete shadowed-node coverage for replacements.
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
### Derived history
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
`deriveMessages()` caches each surface node's projection once and returns a fresh array per call over shared, deep-frozen messages; each of the three surface event types (`user/message`, `assistant/message`, `tool/result`) projects its own message kind — user content verbatim, the assembled assistant message with its provider and model, or a user-role tool result. A surface rewrite rebuilds the projection — there is no raw-log fallback, so the surface is the single source of derived history.
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
### The request header
|
|
78
100
|
|
|
79
|
-
|
|
101
|
+
The loop logs a full canonical `request/header` snapshot (call config, adapter defaults, rendered system prompt, assembled tool schemas) at each loop-instance boundary and on change; `foldRequestHeader(events)` reconstructs it by selecting the latest snapshot, making every conversation request a pure function of the log. Route metadata (`request/context`) is separate logged state appended only when the provider, model, or capacity differs.
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
- `surfaceOp?: SurfaceOp` — how this event entered the surface. Absent for non-surface events (boundaries, chunks, usage, errors).
|
|
83
|
-
- `ignorable?: true` — marks an event a reader may safely skip when it does not recognize the type; absent means required, so an unknown-type event refuses session reconstruction ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)).
|
|
103
|
+
</details>
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
-----
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
<a id="further-exploration"></a>
|
|
108
|
+
## Further Exploration
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
The package-level contract is enough for most consumers; read these when you need the surrounding domain.
|
|
90
111
|
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
112
|
+
- [Session subsystem](../../../docs/subsystems/session.md) — the full event vocabulary, surface types, and generated service API.
|
|
113
|
+
- [Persistence subsystem](../../../docs/subsystems/persistence.md) — how backends make this log durable.
|
|
114
|
+
- [Core subsystem](../../../docs/subsystems/core.md) — the loop that writes and derives from sessions.
|
|
115
|
+
- [Generated persistence catalog](../../../docs/persistence-catalog.md) — every session event with its payload and declaration site.
|
|
116
|
+
- [Core group map](../README.md) — how the core packages compose.
|
|
94
117
|
|
|
118
|
+
-----
|
|
119
|
+
|
|
120
|
+
<a id="model-experience"></a>
|
|
95
121
|
## Model Experience
|
|
96
122
|
|
|
97
123
|
### Derived message history
|
|
98
124
|
|
|
99
125
|
#### What the model sees
|
|
100
126
|
|
|
101
|
-
The model receives the complete messages from `user/message`, `assistant/message`, and `tool/result` surface entries verbatim
|
|
127
|
+
The model receives the complete messages from `user/message`, `assistant/message`, and `tool/result` surface entries verbatim — identities, roles, sources, and content blocks are the same values established at creation, and projections never mint identities. Direct prompts and injected context remain separate `user/message` events whose sources preserve their provenance. Chunks, boundaries, usage, and other log-only events add no message.
|
|
102
128
|
|
|
103
129
|
#### Token effect
|
|
104
130
|
|
|
@@ -138,7 +164,22 @@ Logging causes no invalidation, and exact reconstruction preserves request-prefi
|
|
|
138
164
|
|
|
139
165
|
## Known Limitations and Deferred Work
|
|
140
166
|
|
|
141
|
-
|
|
167
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
These limits define when the session store needs special care. They are current package constraints, not a task backlog.
|
|
171
|
+
|
|
142
172
|
- **`fork()` cuts only at stable boundaries of live sessions** — the selected prefix must end outside an open turn and the source must be in the store; forking a persisted-but-unloaded session is excluded from the [fork API](../../../.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md).
|
|
143
|
-
- **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no broad compatibility implied: `Session` accepts only current seed shapes,
|
|
173
|
+
- **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no broad compatibility implied: `Session` accepts only current seed shapes, a backend refuses any other version, and unknown event types refuse reconstruction unless marked `ignorable` in the envelope ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)).
|
|
144
174
|
- **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them.
|
|
175
|
+
- **No session tree beyond fork** — a pi-style entry tree over branched sessions is deferred unless a consumer needs more than boundary-based forking.
|
|
176
|
+
|
|
177
|
+
<a id="dev-note"></a>
|
|
178
|
+
### Dev Note
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
182
|
+
|
|
183
|
+
None.
|
|
184
|
+
|
|
185
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,104 +1,130 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "面向用户与维护者的事件溯源会话日志与内存存储说明,用于构建、检查或扩展每个 agent 交互背后的持久记录。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-session
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-session` 提供仅追加的会话日志,记录 agent(智能体)的完整交互历史——每个模型可见事实都流经的单一真源。LLM(大语言模型)消息历史由日志*派生*(`deriveMessages()`),从不另行存储,因此回放就是对同一批事件重新派生,压缩(compaction)也可以遮蔽较旧的表层条目而不删除历史。该包还提供内存存储(`ctx.sessions`)、插件通过声明合并扩展的类型化 `SessionEvent` 词汇,以及为产生消息的事件排序的 surface 层。持久化刻意是独立关注点:后端订阅 `session/event` 并在 `session/flush` 时刷新。作为任何 agent 会话的基础时请选择本包;它本身不运行模型调用。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
8
22
|
|
|
9
|
-
|
|
23
|
+
-----
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
在必须存在会话的任何地方挂载 `dsh-session`。它在内存中创建并持有事件溯源的 `Session` 实例;持久存储由订阅 `session/event` 流的持久化插件叠加。
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
- `ctx.sessions.flush(session)` 通过会话捕获的作用域分发一个需等待完成的并行持久性检查点。每个监听器都会启动;调用会等待全部结算后才报告失败。未发布、已脱离和陈旧的对象会被拒绝。
|
|
17
|
-
- `ctx.sessions.fork(source, boundary?, childSessionId?): Session`:解析实时会话对象或 id,选取截至 `boundary` 事件序号(含该事件)的种子(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,再创建带谱系元数据的实时子会话。
|
|
18
|
-
- `ctx.sessions.get(id: SessionId): Session | undefined`
|
|
19
|
-
- `ctx.sessions.list(): Session[]`
|
|
30
|
+
### 创建与检查会话
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
`ctx.sessions.create()` 构建绑定到调用方 fiber 的实时会话;`get(id)` 与 `list()` 查找会话,`fork()` 从实时会话的稳定前缀创建子会话。
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
```text
|
|
35
|
+
const session = ctx.sessions.create(sessionId, { meta: { cwd: '/workspace' } })
|
|
36
|
+
ctx.sessions.get(sessionId) // the live session
|
|
37
|
+
ctx.sessions.list() // every live session, in creation order
|
|
38
|
+
```
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
- `enter(session)` 执行冲突检查,在不通知的情况下发布,并返回一个绑定到该条目的幂等脱离函数。允许并发准备相同 id,但只有一个条目能够成功进入;陈旧的脱离函数无法移除其替代项。
|
|
27
|
-
- `announce(session)` 发出唯一一次创建边,并拒绝重复或重入通知。该次分发期间请求的脱离操作会延后,之后再发出成对的释放边;未通知的条目不会发出任何生命周期边。
|
|
40
|
+
### 追加与派生
|
|
28
41
|
|
|
29
|
-
`
|
|
42
|
+
`session.append(type, data, opts?)` 提交一个类型化事件——它先快照并冻结载荷、校验其为无损 JSON,再通知观察者。`session.deriveMessages()` 把日志投影为模型看到的 `Message[]`,采用增量且有缓存的方式:
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
```text
|
|
45
|
+
session.append('user/message', { role: 'user', content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
|
|
46
|
+
{ surfaceOp: 'append' })
|
|
47
|
+
session.deriveMessages() // the derived model history
|
|
48
|
+
```
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
表层事件(`user/message`、`assistant/message`、`tool/result`)必须声明如何进入有序 surface;原始分片、边界与其他仅日志事件从不产生消息。
|
|
34
51
|
|
|
35
|
-
###
|
|
52
|
+
### 派生会话的 fork
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
`ctx.sessions.fork(source, boundary?, childSessionId?)` 选取截至 `boundary` 事件序号(含该事件)的源事件(默认:当前最后一个事件),要求所选前缀结束时没有开放轮次,再创建带谱系元数据的实时子会话。必须在轮次中途分支的工具时委派会裁剪到已完成前缀。
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
- `session.deriveMessages()` 对每个新的 surface 条目只做一次增量投影,并返回一个新数组,其中包含这些条目存储的完整、带标识且冻结的消息。assistant 消息的模型来源会保留生成该消息的提供方和模型,以及适配器私有回放状态。surface 重写会重建投影;不存在原始日志回退。
|
|
41
|
-
- `session.deriveEventMessage(event)` 是重建和请求检查使用的规范逐事件投影。
|
|
42
|
-
- `session.surface` 暴露只读 `SessionSurface` 视图,由会话唯一的增量 surface 管理器所有;每次提交重写,`replaceGeneration` 都会变化。
|
|
43
|
-
- `session.events` 是按追加失效的缓存冻结快照;已接受事件保持深度冻结。
|
|
44
|
-
- `session.seq`、`session.id`:当前序号和只读类型化身份。
|
|
45
|
-
- `session.header: SessionHeader`:脱离、深冻结的创建元数据(`version`、`id`、`createdAt`,以及可选的 `cwd`/`parentSession`/`seedLength`/`delegationDepth`)。构造时会校验持久记录,并要求其中的 id 与 `session.id` 一致。
|
|
56
|
+
### 刷新持久状态
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
`ctx.sessions.flush(session)` 分发需等待完成的持久性检查点:每个持久化监听器都会刷新,调用在所有监听器结算后完成。需要立即持久性屏障的生产方应等待它,而不是假定写后刷新已完成。
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
-----
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
<a id="understand-the-implementation"></a>
|
|
63
|
+
## 理解实现
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
<details>
|
|
66
|
+
<summary>实现细节——点击展开</summary>
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
本节解释该包如何实现上述行为;可观察约定已在[使用本包](#use-this-package)中完整说明。
|
|
56
69
|
|
|
57
|
-
###
|
|
70
|
+
### 设计理念
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
该包建立在事件溯源之上:`Session` 是类型化 `SessionEvent` 的仅追加日志,其他一切——模型历史、transcript、遥测、标题、持久化——都从这条流派生。surface 是派生投影:一个增量管理器校验追加候选、根据已提交事件推进有序视图,并跟踪每次已提交重写都会递增的 `replaceGeneration`。模型可见即已记录:任何到达模型请求的内容都必须能从日志重建。共享的[行编解码器](src/chunk-rows.ts)在事件序列与紧凑行之间无损转换,逐字保留无法识别的事件,并拒绝形态错误的行。持久化后端决定是否打包写入;有界历史传输可以使用同一种行,同时保留完整逻辑区间,并为需要 token 边界的消费方提供精确解码。
|
|
60
73
|
|
|
61
|
-
###
|
|
74
|
+
### 请求 header
|
|
62
75
|
|
|
63
|
-
`request/header`
|
|
76
|
+
`request/header` 存储非历史请求 envelope 的完整规范快照,原因为 `initial`、`resume`、`change` 或 `series`。显式消息序列起点或表层替换会在 envelope 不变时写入 `series` 快照;同时发生变化时使用 `startsSeries: true`。同一序列内的步骤、重试与普通后续轮次继承最新快照。`adapterDefaults` 区分由适配器解析的值与显式设置,`foldRequestHeader()` 选择最新快照。这种自包含记录以每个消息序列增加存储为代价,支持局部窗口渲染与精确重建;细节由[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)负责。
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
### 源码地图
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
| 文件 | 职责 |
|
|
81
|
+
|---|---|
|
|
82
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:`SessionStore` 服务、存储生命周期、`fork`、`flush` |
|
|
83
|
+
| [`src/types.ts`](src/types.ts) | `SessionEventMap`、`SessionEvent`、`UserMessage`、`SessionHeader`、`TurnEndReasonMap` |
|
|
84
|
+
| [`src/surface.ts`](src/surface.ts) | 有序 surface 投影、替换校验、`deriveEventMessage` |
|
|
85
|
+
| [`src/request-header.ts`](src/request-header.ts) | `request/header` 折叠与重建 |
|
|
86
|
+
| [`dsh-util-values`](../../util/values/README.zh.md) | 共享无损 JSON 校验与分离式快照 |
|
|
87
|
+
| [`src/chunk-rows.ts`](src/chunk-rows.ts) | 供持久化后端使用的共享紧凑行存储编解码器 |
|
|
88
|
+
| [`src/repair.ts`](src/repair.ts) | 崩溃遗留日志的冷修复 |
|
|
89
|
+
| [`src/invariant.ts`](src/invariant.ts) | 不变式配套:序号、轮次/步骤闭合、工具调用/结果配对 |
|
|
68
90
|
|
|
69
|
-
###
|
|
91
|
+
### 追加校验
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
每次追加都会使用共享的迭代式 `snapshotJsonValue()` 流程,对每个嵌套值只读取、校验并复制一次,因此有状态的 getter 无法给校验提供一个值、给存储提供另一个值。非无损 JSON 载荷(BigInt、循环、稀疏数组、`-0`、特殊原型)会在追加位置被拒绝,先于任何后端刷新。表层事件还会校验标记形态、被引用的源事件 seq,以及替换的完整遮蔽节点覆盖。
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
### 派生历史
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
`deriveMessages()` 把每个 surface 节点的投影缓存一次,每次调用都返回共享、深度冻结消息之上的新数组;三种 surface 事件类型(`user/message`、`assistant/message`、`tool/result`)各自投影自己的消息种类——user 内容原样、带提供方与模型的组装 assistant 消息,或 user 角色的工具结果。surface 重写会重建投影——不存在原始日志回退,因此 surface 是派生历史的唯一来源。
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
### 请求头
|
|
78
100
|
|
|
79
|
-
|
|
101
|
+
循环在每个循环实例边界及变更时记录完整规范 `request/header` 快照(调用配置、适配器默认值、渲染后的系统提示词、组装后的工具 schema);`foldRequestHeader(events)` 通过选择最新快照来重建它,使每个对话请求都成为日志的纯函数。路由元数据(`request/context`)是独立的已记录状态,仅在提供方、模型或容量变化时追加。
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
- `surfaceOp?: SurfaceOp`:事件进入 surface 的方式。非 surface 事件(边界、分片、用量、错误)不含该字段。
|
|
83
|
-
- `ignorable?: true`:标记读取器在不认识事件类型时可以安全跳过该事件;缺失表示必需,不认识的事件类型会使会话重建被拒绝([机制](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md))。
|
|
103
|
+
</details>
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
-----
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
<a id="further-exploration"></a>
|
|
108
|
+
## 进一步探索
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
包级约定对大多数消费方已经足够;需要周边领域时再阅读以下页面。
|
|
90
111
|
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
112
|
+
- [会话子系统](../../../docs/subsystems/session.zh.md)——完整事件词汇、surface 类型与生成的服务 API。
|
|
113
|
+
- [持久化子系统](../../../docs/subsystems/persistence.zh.md)——后端如何让该日志持久化。
|
|
114
|
+
- [Core 子系统](../../../docs/subsystems/core.zh.md)——写入并派生会话的循环。
|
|
115
|
+
- [生成持久化目录](../../../docs/persistence-catalog.zh.md)——每个会话事件及其载荷与声明位置。
|
|
116
|
+
- [core 分组地图](../README.zh.md)——core 各包如何组合。
|
|
94
117
|
|
|
118
|
+
-----
|
|
119
|
+
|
|
120
|
+
<a id="model-experience"></a>
|
|
95
121
|
## 模型体验
|
|
96
122
|
|
|
97
123
|
### 派生消息历史
|
|
98
124
|
|
|
99
|
-
####
|
|
125
|
+
#### 模型看到什么
|
|
100
126
|
|
|
101
|
-
模型会原样接收 `user/message`、`assistant/message`
|
|
127
|
+
模型会原样接收 `user/message`、`assistant/message` 与 `tool/result` surface 条目中的完整消息——标识、角色、来源与内容块都与创建时确定的值相同,投影从不生成标识。直接提示词与注入上下文仍是彼此独立的 `user/message` 事件,各事件的来源会保留其出处。分片、边界、用量与其他仅日志事件不会添加消息。
|
|
102
128
|
|
|
103
129
|
#### Token 影响
|
|
104
130
|
|
|
@@ -110,7 +136,7 @@
|
|
|
110
136
|
|
|
111
137
|
### 崩溃修复结果
|
|
112
138
|
|
|
113
|
-
####
|
|
139
|
+
#### 模型看到什么
|
|
114
140
|
|
|
115
141
|
如果恢复发现 assistant 工具请求没有持久 `tool/call`,其合成 `TOOL_NOT_STARTED` 结果内容为 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。如果持久 `tool/call` 没有结果,其 `TOOL_OUTCOME_UNKNOWN` 结果内容为 `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.`。
|
|
116
142
|
|
|
@@ -124,21 +150,36 @@
|
|
|
124
150
|
|
|
125
151
|
### 已记录的请求头
|
|
126
152
|
|
|
127
|
-
####
|
|
153
|
+
#### 模型看到什么
|
|
128
154
|
|
|
129
|
-
会话会重建循环实际发送的系统提示词、工具 schema
|
|
155
|
+
会话会重建循环实际发送的系统提示词、工具 schema、调用配置与会话前缀。请求头事件不会向消息历史加入第二份副本;前缀在 `deriveMessages()` 外部前置。
|
|
130
156
|
|
|
131
157
|
#### Token 影响
|
|
132
158
|
|
|
133
|
-
日志记录不产生重复 token
|
|
159
|
+
日志记录不产生重复 token。重建的前缀、系统文本与 schema 仍会产生正常的逐请求开销。
|
|
134
160
|
|
|
135
161
|
#### KV Cache 影响
|
|
136
162
|
|
|
137
163
|
记录日志不会导致失效,精确重建会保持请求前缀一致。后续请求头若更改前缀、提示词或 schema,可能从第一处差异开始使复用失效。
|
|
138
164
|
|
|
139
|
-
##
|
|
165
|
+
## 已知限制与延期工作
|
|
166
|
+
|
|
167
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
这些限制说明会话存储何时需要特别留意。它们是当前包约束,不是任务积压。
|
|
140
171
|
|
|
141
|
-
- **会话分支/树结构**(pi 风格条目树):除非需要超越基于边界的 `fork()` 能力,否则暂缓。
|
|
142
172
|
- **`fork()` 仅在实时会话的稳定边界处切分**:所选前缀结束时不得有开放轮次,且源会话必须位于存储中;[fork API](../../../.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.zh.md) 不支持对已持久化但未加载的会话进行 fork。
|
|
143
|
-
- **`SESSION_FORMAT_VERSION` 固定为 `0`**:预发布阶段不承诺广泛兼容性;`Session` 只接受当前 seed
|
|
173
|
+
- **`SESSION_FORMAT_VERSION` 固定为 `0`**:预发布阶段不承诺广泛兼容性;`Session` 只接受当前 seed 形状,后端拒绝任何其他版本,不认识的事件类型也会拒绝重建,除非信封带 `ignorable` 标记([机制](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md))。
|
|
144
174
|
- **`TurnEndReasonMap` 不含 ACP(Agent Client Protocol)命名的 `refusal`/`max_turn_requests` 变体**:受生产方约束;只有当适配器或循环首次产生这些变体时才加入。
|
|
175
|
+
- **fork 之外没有会话树**:基于分支会话的 pi 风格条目树被推迟,除非消费方需要超越基于边界的 forking 的能力。
|
|
176
|
+
|
|
177
|
+
<a id="dev-note"></a>
|
|
178
|
+
### 开发备注
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
182
|
+
|
|
183
|
+
无。
|
|
184
|
+
|
|
185
|
+
</details>
|