@chanlerdev/scorel 0.0.1
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 +110 -0
- package/dist/index.js +6675 -0
- package/dist/index.js.map +7 -0
- package/docs/CHANGELOG.md +12 -0
- package/docs/README.md +116 -0
- package/docs/ROADMAP.md +669 -0
- package/docs/SHIP.md +242 -0
- package/docs/spec/channels.md +156 -0
- package/docs/spec/client.md +326 -0
- package/docs/spec/daemon.md +408 -0
- package/docs/spec/events.md +423 -0
- package/docs/spec/extensions.md +255 -0
- package/docs/spec/relay.md +391 -0
- package/docs/spec/runtime.md +251 -0
- package/docs/spec/session.md +380 -0
- package/docs/spec/ship/S0001-docs-baseline.md +41 -0
- package/docs/spec/ship/S0002-package-skeleton.md +56 -0
- package/docs/spec/ship/S0003-protocol-contracts.md +49 -0
- package/docs/spec/ship/S0004-session-core.md +50 -0
- package/docs/spec/ship/S0005-runtime-loop.md +48 -0
- package/docs/spec/ship/S0006-embedded-daemon-client.md +51 -0
- package/docs/spec/ship/S0007-cli-alpha.md +49 -0
- package/docs/spec/ship/S0008-coding-tools.md +107 -0
- package/docs/spec/ship/S0009-code-discovery-tools.md +82 -0
- package/docs/spec/ship/S0010-todo-tool-and-cli.md +81 -0
- package/docs/spec/ship/S0011-coding-agent-alpha-smoke.md +110 -0
- package/docs/spec/ship/S0012-coding-tools-maturity.md +143 -0
- package/docs/spec/ship/S0013-local-daemon-protocol.md +57 -0
- package/docs/spec/ship/S0014-local-daemon-lifecycle.md +64 -0
- package/docs/spec/ship/S0015-local-attach-and-broadcast.md +58 -0
- package/docs/spec/ship/S0016-local-daemon-resync-smoke.md +60 -0
- package/docs/spec/ship/S0017-grep-files-output-mode.md +49 -0
- package/docs/spec/ship/S0018-daemon-entrypoint-smoke.md +48 -0
- package/docs/spec/ship/S0019-remote-transport-contract.md +59 -0
- package/docs/spec/ship/S0020-remote-websocket-server.md +56 -0
- package/docs/spec/ship/S0021-remote-websocket-client-transport.md +55 -0
- package/docs/spec/ship/S0022-remote-daemon-cli-lifecycle.md +60 -0
- package/docs/spec/ship/S0023-remote-control-e2e-validation.md +66 -0
- package/docs/spec/ship/S0024-remote-attach-interactive-stream.md +49 -0
- package/docs/spec/ship/S0025-remote-attach-session-event-view.md +57 -0
- package/docs/spec/ship/S0026-attach-project-cache-and-dual-seq-reconnect.md +87 -0
- package/docs/spec/ship/S0027-session-diagnostics-log.md +77 -0
- package/docs/spec/ship/S0028-client-attach-diagnostics-log.md +70 -0
- package/docs/spec/ship/S0029-project-index-for-session-lookup.md +119 -0
- package/docs/spec/ship/S0030-webui-product-intent.md +73 -0
- package/docs/spec/ship/S0031-daemon-projectslug-rule.md +72 -0
- package/docs/spec/ship/S0032-daemon-protocol-completion.md +123 -0
- package/docs/spec/ship/S0033-webui-skeleton-routing.md +92 -0
- package/docs/spec/ship/S0034-webui-device-settings.md +121 -0
- package/docs/spec/ship/S0035-webui-device-handshake.md +83 -0
- package/docs/spec/ship/S0036-webui-project-session-sync.md +70 -0
- package/docs/spec/ship/S0037-webui-chatbox-v1.md +97 -0
- package/docs/spec/ship/S0038-webui-cancel-multiclient.md +65 -0
- package/docs/spec/ship/S0039-webui-e2e-newchat.md +74 -0
- package/docs/spec/ship/S0040-webui-codex-visual-tokens.md +227 -0
- package/docs/spec/ship/S0041-webui-markdown-and-tool-block.md +248 -0
- package/docs/spec/ship/S0042-webui-streaming-ux-autoscroll.md +130 -0
- package/docs/spec/ship/S0043-startup-ergonomics.md +278 -0
- package/docs/spec/ship/S0044-webui-chatbox-rebuild.md +556 -0
- package/docs/spec/ship/S0045-webui-card-sidebar-and-session-fixes.md +469 -0
- package/docs/spec/ship/S0046-webui-empty-composer-and-lazy-session.md +428 -0
- package/docs/spec/ship/S0047-webui-project-hover-newchat-and-dynamic-greeting.md +176 -0
- package/docs/spec/ship/S0048-device-level-host-project-registry.md +253 -0
- package/docs/spec/ship/S0049-webui-add-project-directory-browser.md +217 -0
- package/docs/spec/ship/S0050-instruction-snapshot-and-agents-assembly.md +338 -0
- package/docs/spec/ship/S0051-harness-item-and-system-reminder.md +190 -0
- package/docs/spec/ship/S0052-follow-up-queue-and-dual-loop.md +195 -0
- package/docs/spec/ship/S0053-skill-index-and-skill-tool.md +252 -0
- package/docs/spec/ship/S0054-webui-running-message-behavior.md +72 -0
- package/docs/spec/ship/S0055-webui-composer-acceptance-and-queue-strip.md +68 -0
- package/docs/spec/ship/S0056-relay-and-hosted-webui-contract.md +106 -0
- package/docs/spec/ship/S0057-relay-service-protocol-skeleton.md +161 -0
- package/docs/spec/ship/S0058-host-outbound-relay-and-pair-command.md +138 -0
- package/docs/spec/ship/S0059-relay-transport-and-hosted-webui-connector.md +140 -0
- package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.md +132 -0
- package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.verification.md +90 -0
- package/docs/spec/ship/S0061-hosted-defaults-and-cli-command-surface.md +208 -0
- package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +166 -0
- package/docs/spec/tools.md +173 -0
- package/package.json +51 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Session — 会话资产与树状存储
|
|
2
|
+
|
|
3
|
+
> 上游:`architecture.md`、`spec/events.md`
|
|
4
|
+
> 主题:把对话、工具调用、文件修改全部收敛到一条 append-only JSONL 上,所有"时间旅行"都是同一个 `replay` / `buildContext` 函数的不同输入。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 设计目标
|
|
9
|
+
|
|
10
|
+
Scorel 不把会话当做"可丢的上下文",而当做**资产**。资产的核心要求有三条:
|
|
11
|
+
|
|
12
|
+
1. **不丢失**:JSONL 只追加、不修改,任何历史都可被重建
|
|
13
|
+
2. **可重放**:Rewind、Fork、Compact 等能力都通过同一个机制从 JSONL 推导出目标状态
|
|
14
|
+
3. **可隔离**:会话中的"自定义记录"(rewind 标记、channel 元数据)只存在于应用层,LLM 永远看不到(由 `convertToLlm` 在边界上过滤)
|
|
15
|
+
|
|
16
|
+
这三条要求共同决定了架构形状:**单一日志 + 树状结构 + 两层消息**。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. JSONL 格式(v1)
|
|
21
|
+
|
|
22
|
+
### 2.1 文件结构
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
~/.scorel/sessions/{sessionId}.jsonl
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- 第 0 行:SessionHeader
|
|
29
|
+
- 第 1+ 行:PersistentEvent(每行一个 JSON)
|
|
30
|
+
|
|
31
|
+
### 2.2 SessionHeader
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
interface SessionHeader {
|
|
35
|
+
version: 1;
|
|
36
|
+
sessionId: SessionId;
|
|
37
|
+
deviceId: DeviceId; // daemon 宿主机,session 的物理归属
|
|
38
|
+
createdAt: number;
|
|
39
|
+
clonedFrom?: { // 如果是从另一个 session clone 来的
|
|
40
|
+
sessionId: SessionId;
|
|
41
|
+
deviceId: DeviceId;
|
|
42
|
+
eventId: EventId; // clone 自哪个事件
|
|
43
|
+
};
|
|
44
|
+
meta: SessionMeta;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SessionMeta {
|
|
48
|
+
projectId: ProjectId; // owning Host 中稳定的 Project 身份
|
|
49
|
+
name?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
model: string;
|
|
52
|
+
thinkingLevel: "none" | "low" | "medium" | "high";
|
|
53
|
+
[key: string]: unknown; // 可扩展
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`sessionId` 是随机稳定身份,不承担用户可读命名。产品 UI / CLI 应优先展示 `title`、时间、project、short index;short index 只允许作为本机选择辅助,不作为跨 daemon 协议 ID。测试和调试路径可以显式传入 `--session <id>`,但默认新建 session 应由 daemon 生成随机 ID。
|
|
58
|
+
|
|
59
|
+
### 2.3 示例文件
|
|
60
|
+
|
|
61
|
+
```jsonl
|
|
62
|
+
{"version":1,"sessionId":"ses_abc","deviceId":"vps-tokyo-01","createdAt":1716000000000,"meta":{"projectId":"prj_abc","model":"claude-sonnet-4-20250514","thinkingLevel":"medium"}}
|
|
63
|
+
{"type":"message","id":"e01","parentId":null,"seq":1,"sessionId":"ses_abc","clientId":"gui-mac-chanler","ts":1716000001000,"message":{"role":"user","content":"解释 monads"}}
|
|
64
|
+
{"type":"message","id":"e02","parentId":"e01","seq":5,"sessionId":"ses_abc","clientId":"daemon","ts":1716000005000,"message":{"role":"assistant","content":[{"type":"text","text":"Monad 是..."}],"model":"claude-sonnet-4-20250514","stopReason":"end_turn","usage":{"inputTokens":150,"outputTokens":420}}}
|
|
65
|
+
{"type":"message","id":"e03","parentId":"e01","seq":10,"sessionId":"ses_abc","clientId":"tg-bot-001","ts":1716000010000,"message":{"role":"user","content":"用更简单的话解释"}}
|
|
66
|
+
{"type":"rewind","id":"e04","parentId":"e03","seq":15,"sessionId":"ses_abc","clientId":"gui-mac-chanler","ts":1716000015000,"targetEventId":"e01"}
|
|
67
|
+
{"type":"message","id":"e05","parentId":"e01","seq":16,"sessionId":"ses_abc","clientId":"gui-mac-chanler","ts":1716000016000,"message":{"role":"user","content":"解释 functor 吧"}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
注意:
|
|
71
|
+
- SessionHeader 有 `deviceId`(session 归属机器)
|
|
72
|
+
- 每个 event 有 `clientId`(谁发起的操作)
|
|
73
|
+
- assistant 消息的 `clientId` 是 `"daemon"`(runtime 自己产生的)
|
|
74
|
+
- Telegram bot 注入的消息 `clientId` 是 `"tg-bot-001"`
|
|
75
|
+
|
|
76
|
+
### 2.4 树可视化
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
e01 (user: "解释 monads") ← parentId: null
|
|
80
|
+
├── e02 (assistant: "Monad 是...") ← parentId: e01
|
|
81
|
+
│ └── e03 (user: "用更简单的话解释") ← parentId: e02(这条后来被 rewind 了)
|
|
82
|
+
│ └── e04 (rewind → e01) ← 审计记录,死端
|
|
83
|
+
└── e05 (user: "解释 functor 吧") ← parentId: e01(rewind 后新消息)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
注意:
|
|
87
|
+
- `e04`(rewind)记录在树上但不影响 context building
|
|
88
|
+
- `e05` 的 parentId 是 `e01`(rewind 的 target),不是 `e04`
|
|
89
|
+
- `seq` 不连续(中间是 transient events 占的序号)
|
|
90
|
+
|
|
91
|
+
### 2.5 Attach Client Cache
|
|
92
|
+
|
|
93
|
+
Daemon-owned JSONL remains the authoritative session store. Attach clients may keep a local project-scoped persistent cache to speed up terminal recovery, but that cache is not a second writer.
|
|
94
|
+
|
|
95
|
+
Cache scope is part of the identity:
|
|
96
|
+
|
|
97
|
+
- local attach cache is scoped under a local project locator
|
|
98
|
+
- remote attach cache is scoped under a remote `deviceId + projectId`
|
|
99
|
+
- same `sessionId` under different scopes must not share cache files
|
|
100
|
+
|
|
101
|
+
The remote endpoint URL is a connection locator, not stable identity. If a daemon reports the same `deviceId + projectId` after the URL changes, attach should reuse the same cache. A daemon may also provide a `deviceDisplayName` for UI labels, but display names are not identity.
|
|
102
|
+
|
|
103
|
+
The cache may advance `persistentLastSeq` only after a persistent event has been durably written to the local cache. It must not advance persistent anchors from transient events. If metadata no longer matches the requested attach target, the client must ignore or isolate the cache and perform daemon reconciliation.
|
|
104
|
+
|
|
105
|
+
If the cache stores transient stream state, it must be explicitly separate from persistent events. A cached transient may advance `streamLastSeq`, but it is provisional UI state only and must be discarded once the matching persistent assistant event is observed.
|
|
106
|
+
|
|
107
|
+
### 2.6 Session Diagnostics Log
|
|
108
|
+
|
|
109
|
+
Each daemon-owned session may also have a sibling plain-text diagnostics log:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
~/.scorel/sessions/{sessionId}.jsonl
|
|
113
|
+
~/.scorel/sessions/{sessionId}.log
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The `.log` file is append-only operational evidence, not replay state. It is written by the daemon that owns the session JSONL and stays on that machine. Remote attach must not copy daemon diagnostics into local attach cache.
|
|
117
|
+
|
|
118
|
+
Each log line is human-readable and grep-friendly:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
ts=1716000000000 level=info event=send_message_started sessionId=ses_abc clientId=client_cli
|
|
122
|
+
ts=1716000000100 level=error event=runtime_error sessionId=ses_abc message="stream closed before response.completed"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Diagnostics can record lifecycle, reconnect, runtime, provider result summaries, and errors. They must not record API keys, bearer tokens, full prompts, full tool results, or raw provider payloads by default.
|
|
126
|
+
|
|
127
|
+
Attach clients have their own client-side diagnostics under the attach cache tree, not under daemon session storage:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
~/.scorel/attach-cache/{scopeKey}/{sessionId}.json
|
|
131
|
+
~/.scorel/attach-cache/{scopeKey}/{sessionId}.log
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
That attach `.log` is local client evidence for connection, cache, resync, rendering, and outbound send behavior. It is intentionally separate from the daemon-owned session `.log`; remote attach should not mix remote daemon runtime/provider diagnostics into the local attach cache.
|
|
135
|
+
|
|
136
|
+
### 2.7 Project Registry
|
|
137
|
+
|
|
138
|
+
The owning Host maintains a Project Registry:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
~/.scorel/projects.json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This file is the owning Host's Project Registry. It records the Device's registered workspaces for CLI, WebUI, GUI, and future HTTP clients. It is not replay state and is not the authority for session contents.
|
|
145
|
+
|
|
146
|
+
The Registry points to existing paths:
|
|
147
|
+
|
|
148
|
+
- local session JSONL and daemon diagnostics under `sessions/`
|
|
149
|
+
- attach cache and attach diagnostics under `attach-cache/`
|
|
150
|
+
|
|
151
|
+
No session, cache, or log file is moved because of the Registry. `projectId` is the stable Project identity; the owning Host maps it to canonical `workDir`. Remote clients scope their cache with both `deviceId` and `projectId`. Client code must not derive identity from paths, display names, or transport URLs.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 3. SessionTree 与 Context 构建
|
|
156
|
+
|
|
157
|
+
### 3.1 树接口
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface SessionTree {
|
|
161
|
+
get(id: EventId): TreeNode | undefined;
|
|
162
|
+
has(id: EventId): boolean;
|
|
163
|
+
append(event: PersistentEvent): void;
|
|
164
|
+
|
|
165
|
+
readonly rootId: EventId | null;
|
|
166
|
+
getLeaves(): EventId[];
|
|
167
|
+
getChildren(id: EventId): EventId[];
|
|
168
|
+
getPath(id: EventId): EventId[]; // root → node
|
|
169
|
+
getBranchPoints(): EventId[]; // 有多个 children 的节点
|
|
170
|
+
|
|
171
|
+
readonly size: number;
|
|
172
|
+
[Symbol.iterator](): Iterator<PersistentEvent>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface TreeNode {
|
|
176
|
+
event: PersistentEvent;
|
|
177
|
+
children: EventId[];
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 3.2 Context 构建算法
|
|
182
|
+
|
|
183
|
+
buildContext 使用 EventTypeHandler 的通用遍历模式(详见 `spec/events.md §6`)。核心不 hardcode 任何事件类型的特殊逻辑——全靠各事件 handler 的 `convertToLlm` 声明行为。
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
function buildContext(tree: SessionTree, leafId: EventId): ScorelMessage[] {
|
|
187
|
+
const path = tree.getPath(leafId); // root → leaf
|
|
188
|
+
const messages: ScorelMessage[] = [];
|
|
189
|
+
|
|
190
|
+
// 从 leaf 往 root 走(reverse)
|
|
191
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
192
|
+
const event = tree.get(path[i])!.event;
|
|
193
|
+
const handler = getHandler(event.type);
|
|
194
|
+
const result = handler.convertToLlm(event, ctx);
|
|
195
|
+
|
|
196
|
+
switch (result.action) {
|
|
197
|
+
case "include":
|
|
198
|
+
messages.unshift(result.message);
|
|
199
|
+
break;
|
|
200
|
+
case "merge_prev":
|
|
201
|
+
// 合入 messages 中最后一条 tool_result 的 content 末尾(<system-reminder> 包裹)
|
|
202
|
+
mergeIntoPrevToolResult(messages, result.content);
|
|
203
|
+
break;
|
|
204
|
+
case "skip":
|
|
205
|
+
break;
|
|
206
|
+
case "barrier":
|
|
207
|
+
// compact: 注入 summary,停止向上遍历
|
|
208
|
+
messages.unshift(result.summary);
|
|
209
|
+
return messages;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return messages;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
各事件类型的 LlmAction:
|
|
218
|
+
- `message`(普通)→ `include`
|
|
219
|
+
- `message`(meta.source = "steer"/"followUp")→ `merge_prev`(前面有 tool_result 时)或 `include`(没有时)
|
|
220
|
+
- `compact` → `barrier`(注入 summary,停止遍历)
|
|
221
|
+
- `rewind` / `branch` / `channel_inject` / `session_info` / `custom` → `skip`
|
|
222
|
+
- `custom_message` → `include`
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 4. Rewind:append marker,不删历史
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
async function rewindTo(targetEventId: EventId, expectedLeafId: EventId) {
|
|
231
|
+
// 1. 乐观锁检查
|
|
232
|
+
if (sessionLane.activeLeafId !== expectedLeafId) {
|
|
233
|
+
throw new ConflictError("leaf_changed");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 2. append rewind event —— 历史永不丢失
|
|
237
|
+
const rewindEvent = await sessionLane.append({
|
|
238
|
+
type: "rewind",
|
|
239
|
+
targetEventId,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 3. 更新 active leaf
|
|
243
|
+
sessionLane.setActiveLeaf(targetEventId);
|
|
244
|
+
|
|
245
|
+
// 4. 文件系统回滚不属于 session replay;用户仍通过 Git / 编辑工具处理工作区状态。
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**约束**:rewind 目标必须是"turn 边界"——user 消息之后,或一组 toolResult 完成之后。UI 只暴露这些点,避免 rewind 到"assistant 已发消息但工具还没跑"的脏状态。
|
|
250
|
+
|
|
251
|
+
**Rewind 不跨 Compact**:Compact event 是硬边界。`rewind(targetId)` 时检查 path:target 在最近 compact 之后 → 允许;target 在 compact 之前 → 拒绝(返回 error: "cannot_rewind_past_compact")。Compact 之前的事件保留在 JSONL 中供审计查阅,但不可回退到、UI 不展示。
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 5. Fork:clone 后独立
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
async function cloneSession(
|
|
259
|
+
fromEventId: EventId,
|
|
260
|
+
meta?: Partial<SessionMeta>
|
|
261
|
+
): Promise<SessionId> {
|
|
262
|
+
const newId = generateSessionId();
|
|
263
|
+
const path = tree.getPath(fromEventId); // root → target
|
|
264
|
+
const events = path.map(id => tree.get(id)!.event);
|
|
265
|
+
|
|
266
|
+
// 创建新 session,复制 events 到 fromEventId 为止
|
|
267
|
+
await sessionStore.createWithEvents(newId, {
|
|
268
|
+
...currentHeader,
|
|
269
|
+
sessionId: newId,
|
|
270
|
+
clonedFrom: { sessionId: currentSessionId, deviceId, eventId: fromEventId },
|
|
271
|
+
meta: { ...currentMeta, ...meta },
|
|
272
|
+
}, events);
|
|
273
|
+
|
|
274
|
+
return newId;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Clone 不引入任何新机制,只是在已有 JSONL 上切一刀、复制一份。Clone 后完全独立。
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. 压缩:`transformContext` 管线
|
|
283
|
+
|
|
284
|
+
压缩全部实现为 `transformContext` hook,每轮推理前执行。初期两层:
|
|
285
|
+
|
|
286
|
+
**Layer 1 · micro compact**
|
|
287
|
+
- 每轮都跑
|
|
288
|
+
- 把 >3 轮前的 `ToolResultMessage.content` 替换为占位符 `"[tool result omitted]"`
|
|
289
|
+
- 工具历史对 LLM 的下一步决策价值很低,但 UI 层仍能从原始 JSONL 还原展示
|
|
290
|
+
|
|
291
|
+
**Layer 2 · auto compact**
|
|
292
|
+
- 当 token 超过阈值(默认 `contextWindow * 0.7`)触发
|
|
293
|
+
- 前 70% 消息交给一次独立 LLM 调用生成摘要,后 30% 保留原样
|
|
294
|
+
- 摘要作为 CompactEvent 写入 JSONL
|
|
295
|
+
- **原始消息仍在 JSONL 里**;下次 buildContext 依据 CompactEvent 决定注入摘要并停止向上
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const compactionPipeline: TransformContextHook = async (messages, signal) => {
|
|
299
|
+
messages = replaceOldToolResults(messages, { olderThan: 3 }); // Layer 1
|
|
300
|
+
|
|
301
|
+
const tokens = estimateTokens(messages);
|
|
302
|
+
if (tokens > agent.state.model.contextWindow * 0.7) { // Layer 2
|
|
303
|
+
const { summary, keepFrom } = await summarize(
|
|
304
|
+
messages.slice(0, Math.floor(messages.length * 0.7)),
|
|
305
|
+
agent.state.model,
|
|
306
|
+
signal,
|
|
307
|
+
);
|
|
308
|
+
await sessionLane.append({
|
|
309
|
+
type: 'compact',
|
|
310
|
+
summary,
|
|
311
|
+
compactedThrough: messages[keepFrom - 1].id,
|
|
312
|
+
tokensBefore: tokens,
|
|
313
|
+
tokensAfter: estimateTokens([createSummaryMessage(summary), ...messages.slice(keepFrom)]),
|
|
314
|
+
});
|
|
315
|
+
messages = [createSummaryMessage(summary), ...messages.slice(keepFrom)];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return messages;
|
|
319
|
+
};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**用户手动触发**(如 `/compact` 斜杠命令)直接跑一次 Layer 2,不需要另一条独立逻辑。
|
|
323
|
+
|
|
324
|
+
### 6.1 Auto Compact 安全约束
|
|
325
|
+
|
|
326
|
+
**自动 compact 绝不压缩当前 turn**。当前 turn(user message + assistant + 所有 tool_result)必须完整保留,否则 LLM 看不到自己的 tool_use/tool_result pair。
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// transformContext 中 auto compact 的安全边界
|
|
330
|
+
const currentTurnStart = findCurrentTurnStartIndex(messages);
|
|
331
|
+
const compactCandidates = messages.slice(0, currentTurnStart); // 只压缩旧的
|
|
332
|
+
const preservedTail = messages.slice(currentTurnStart); // 当前 turn 完整保留
|
|
333
|
+
|
|
334
|
+
if (estimateTokens(compactCandidates) > threshold) {
|
|
335
|
+
const summary = await summarize(compactCandidates);
|
|
336
|
+
// persist CompactEvent
|
|
337
|
+
return [summaryMessage, ...preservedTail];
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
手动 compact 同理:只能 compact 到当前 activeLeaf 的最近一条 user message 之前。
|
|
342
|
+
|
|
343
|
+
### 6.2 树模型中的 Compact
|
|
344
|
+
- CompactEvent 只影响**它的后代**的 context 构建
|
|
345
|
+
- 在 compact 点之前分叉的其他分支不受影响
|
|
346
|
+
- 旧事件仍在 JSONL 中,可供历史浏览
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 7. 两层消息在本模块的落点
|
|
351
|
+
|
|
352
|
+
`convertToLlm` 边界现在由 EventTypeHandler 的 `convertToLlm` 方法实现(详见 `spec/events.md §6`)。对 Session 模块来说:
|
|
353
|
+
|
|
354
|
+
- 各事件类型通过 handler 声明自己的 LlmAction(include / merge_prev / skip / barrier)
|
|
355
|
+
- buildContext 通用遍历时调用每个 event 的 handler,不 hardcode 任何类型
|
|
356
|
+
- `rewind` / `branch` / `channel_inject` / `session_info` / `custom` → `skip`(不进入 LLM)
|
|
357
|
+
- `compact` → `barrier`(注入 summary,停止向上)
|
|
358
|
+
- `message`(meta.source = "steer")→ `merge_prev`(合入前一条 tool_result 的 `<system-reminder>`)
|
|
359
|
+
|
|
360
|
+
换言之,应用层能玩的花样很多,LLM 始终只看到 handler 声明要暴露的内容。
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 8. 初期范围与延后项
|
|
365
|
+
|
|
366
|
+
**近期落地**
|
|
367
|
+
- v1 JSONL 格式 + SessionHeader
|
|
368
|
+
- SessionTree + buildContext
|
|
369
|
+
|
|
370
|
+
**延后**
|
|
371
|
+
- Rewind(乐观锁 + 不跨 compact)
|
|
372
|
+
- 压缩 Layer 1 + Layer 2
|
|
373
|
+
- Fork / Clone(跨 session 复制)
|
|
374
|
+
- 后续 schema migration(等真实 v2 出现再设计)
|
|
375
|
+
- 压缩摘要的 prompt 调优与策略自适应
|
|
376
|
+
- 跨 session 的资产检索(依赖后期 Memory 模块)
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
*本文档描述 Scorel 资产化存储的全部设计:单日志、树状结构、纯函数 buildContext、应用层与 LLM 层分离。*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# S0001: Docs Baseline
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Create the first implementation-ready documentation baseline for rebuilding Scorel from scratch.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
- Keep `docs/architecture.md` aligned with the final package boundary: `protocol / core / daemon / client / apps`.
|
|
10
|
+
- Keep abstract module specs in top-level `docs/spec/*.md`.
|
|
11
|
+
- Ensure the first baseline commit records the formal documentation baseline and excludes old implementation code.
|
|
12
|
+
- Exclude old implementation code from the baseline.
|
|
13
|
+
|
|
14
|
+
## Not In Scope
|
|
15
|
+
|
|
16
|
+
- No runtime implementation.
|
|
17
|
+
- No package scaffolding.
|
|
18
|
+
- No release automation.
|
|
19
|
+
- No `IMPLEMENTATION_PROMPT.md` cleanup.
|
|
20
|
+
|
|
21
|
+
## Acceptance Criteria
|
|
22
|
+
|
|
23
|
+
- [x] `docs/architecture.md` describes final package structure and data flow.
|
|
24
|
+
- [x] Top-level `docs/spec/*.md` covers events, session, runtime, daemon, client, tools, extensions, channels.
|
|
25
|
+
- [x] Outdated single-package final architecture is not presented as the current target.
|
|
26
|
+
- [x] First docs baseline commit records formal docs and root project metadata only.
|
|
27
|
+
|
|
28
|
+
## Test Requirements
|
|
29
|
+
|
|
30
|
+
- Documentation review only.
|
|
31
|
+
- Run `rg "单包|不拆多包|import type from \"@scorel/core\"" docs/architecture.md docs/spec/*.md` and confirm no current-target contradiction remains.
|
|
32
|
+
|
|
33
|
+
## Affected Paths
|
|
34
|
+
|
|
35
|
+
- `docs/architecture.md`
|
|
36
|
+
- `docs/spec/*.md`
|
|
37
|
+
- `docs/spec/ship/*.md`
|
|
38
|
+
|
|
39
|
+
## Risks
|
|
40
|
+
|
|
41
|
+
- If temporary prompt docs are included in the first commit, they may freeze immature planning as source of truth.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# S0002: Package Skeleton
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Create the final workspace skeleton so every later M1 implementation has the correct package boundary from day one.
|
|
6
|
+
|
|
7
|
+
## Deliverable
|
|
8
|
+
|
|
9
|
+
- Create `packages/protocol`.
|
|
10
|
+
- Create `packages/core`.
|
|
11
|
+
- Create `packages/daemon`.
|
|
12
|
+
- Create `packages/client`.
|
|
13
|
+
- Create `apps/cli`.
|
|
14
|
+
- Create `apps/daemon`.
|
|
15
|
+
- Add minimal package manifests, tsconfigs, source entrypoints, and smoke tests.
|
|
16
|
+
- Keep dependency direction aligned with ADR-004.
|
|
17
|
+
|
|
18
|
+
## Success Criteria
|
|
19
|
+
|
|
20
|
+
- Every package has a minimal public export.
|
|
21
|
+
- Every package has an import smoke test.
|
|
22
|
+
- `@scorel/protocol` has no internal package dependency.
|
|
23
|
+
- `@scorel/core` depends on `@scorel/protocol`, not on daemon/client/apps.
|
|
24
|
+
- `@scorel/daemon` may depend on protocol/core, not on client/apps.
|
|
25
|
+
- `@scorel/client` depends on protocol only.
|
|
26
|
+
- `apps/cli` and `apps/daemon` are entrypoint shells only.
|
|
27
|
+
|
|
28
|
+
## Boundaries
|
|
29
|
+
|
|
30
|
+
- No protocol event definitions beyond minimal placeholders.
|
|
31
|
+
- No runtime loop.
|
|
32
|
+
- No daemon behavior.
|
|
33
|
+
- No CLI chat behavior.
|
|
34
|
+
- No WebUI / GUI.
|
|
35
|
+
|
|
36
|
+
## Verification
|
|
37
|
+
|
|
38
|
+
- `pnpm -r typecheck`
|
|
39
|
+
- `pnpm -r test`
|
|
40
|
+
- Dependency-boundary smoke test or explicit package manifest inspection.
|
|
41
|
+
|
|
42
|
+
## Affected Paths
|
|
43
|
+
|
|
44
|
+
- `packages/protocol/`
|
|
45
|
+
- `packages/core/`
|
|
46
|
+
- `packages/daemon/`
|
|
47
|
+
- `packages/client/`
|
|
48
|
+
- `apps/cli/`
|
|
49
|
+
- `apps/daemon/`
|
|
50
|
+
- `pnpm-workspace.yaml`
|
|
51
|
+
- root `package.json`
|
|
52
|
+
|
|
53
|
+
## Risks
|
|
54
|
+
|
|
55
|
+
- Adding behavior in this spec will blur the foundation. Keep it skeleton-only.
|
|
56
|
+
- If package exports are too clever now, later specs will inherit accidental API shape. Use minimal exports.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# S0003: Protocol Contracts
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Define the minimum cross-package contracts required by M1 so core, daemon, client, and CLI share one protocol language.
|
|
6
|
+
|
|
7
|
+
## Deliverable
|
|
8
|
+
|
|
9
|
+
- ID and version primitives: `SessionId`, `EventId`, `ClientId`, `Seq`, `protocolVersion`.
|
|
10
|
+
- Message primitives: roles, content blocks, usage, stop reason.
|
|
11
|
+
- Persistent event types needed by M1: session header, user message, assistant message, tool result if required by runtime loop.
|
|
12
|
+
- Transient event types needed by M1: turn start/end, message start/end, text delta, error.
|
|
13
|
+
- Client/daemon request-response envelopes for session create/load/list, send message, status, and event subscription.
|
|
14
|
+
- `DaemonTransport` interface shared by client and embedded transport.
|
|
15
|
+
|
|
16
|
+
## Success Criteria
|
|
17
|
+
|
|
18
|
+
- M1 packages import protocol types only from `@scorel/protocol`.
|
|
19
|
+
- Protocol is browser-safe and has no Node API dependency.
|
|
20
|
+
- Request/response correlation is typed and testable.
|
|
21
|
+
- Error responses use stable error codes rather than ad hoc thrown strings.
|
|
22
|
+
- The protocol is sufficient for S0004-S0007 without introducing duplicate local types.
|
|
23
|
+
|
|
24
|
+
## Boundaries
|
|
25
|
+
|
|
26
|
+
- No auth protocol.
|
|
27
|
+
- No WebSocket-specific transport behavior.
|
|
28
|
+
- No remote reconnect algorithm beyond the presence of `lastSeq` / `Seq` primitives.
|
|
29
|
+
- No full rewind, compact, checkpoint, channel, or permission protocol.
|
|
30
|
+
- No provider-specific LLM message formats.
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
- `pnpm --filter @scorel/protocol typecheck`
|
|
35
|
+
- `pnpm --filter @scorel/protocol test`
|
|
36
|
+
- Type tests cover event unions, request/response pairing, and exhaustive event handling.
|
|
37
|
+
- A browser-safety test or lint check ensures protocol imports no Node built-ins.
|
|
38
|
+
|
|
39
|
+
## Affected Paths
|
|
40
|
+
|
|
41
|
+
- `packages/protocol/`
|
|
42
|
+
- `packages/client/`
|
|
43
|
+
- `packages/daemon/`
|
|
44
|
+
- `packages/core/`
|
|
45
|
+
|
|
46
|
+
## Risks
|
|
47
|
+
|
|
48
|
+
- Over-modeling future features will slow M1 and create false stability. Keep contracts to the CLI Alpha path.
|
|
49
|
+
- Under-modeling events will force later packages to invent local types. Treat duplicate protocol-like types as a failure.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# S0004: Session Core
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Make conversation history a recoverable asset by implementing append-only JSONL v1 storage, replay, tree construction, and context building.
|
|
6
|
+
|
|
7
|
+
## Deliverable
|
|
8
|
+
|
|
9
|
+
- Session create/load APIs.
|
|
10
|
+
- Append-only JSONL writer and reader.
|
|
11
|
+
- `SessionHeader` handling.
|
|
12
|
+
- Persistent event append with stable `seq`, `parentId`, and timestamps.
|
|
13
|
+
- `SessionTree` replay from JSONL.
|
|
14
|
+
- `buildContext(tree, leafId)` for M1 message events.
|
|
15
|
+
- Resume support for the latest active leaf.
|
|
16
|
+
|
|
17
|
+
## Success Criteria
|
|
18
|
+
|
|
19
|
+
- A session can be created, appended to, closed, loaded again, and replayed to the same tree.
|
|
20
|
+
- Session storage APIs name the directory `sessionsDir`; it is the directory that directly contains `{sessionId}.jsonl`, not the product root or user home.
|
|
21
|
+
- Context building produces the expected LLM message sequence for a linear M1 conversation.
|
|
22
|
+
- Invalid or partial JSONL fails predictably with typed errors.
|
|
23
|
+
- Session code lives in `@scorel/core` and depends only on `@scorel/protocol` plus allowed runtime dependencies.
|
|
24
|
+
|
|
25
|
+
## Boundaries
|
|
26
|
+
|
|
27
|
+
- No file checkpoint.
|
|
28
|
+
- No compact implementation.
|
|
29
|
+
- No rewind or branch UX.
|
|
30
|
+
- No clone.
|
|
31
|
+
- No schema migration; this is the first clean JSONL version.
|
|
32
|
+
- No daemon ownership rules here. Daemon will be the only writer in S0006.
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
- `pnpm --filter @scorel/core test -- session`
|
|
37
|
+
- Unit tests cover create, append, reload, replay, leaf path, and context building.
|
|
38
|
+
- Error tests cover missing header, invalid JSON line, duplicate event id, invalid parent, and non-monotonic seq.
|
|
39
|
+
- `pnpm -r typecheck`
|
|
40
|
+
|
|
41
|
+
## Affected Paths
|
|
42
|
+
|
|
43
|
+
- `packages/core/src/session/`
|
|
44
|
+
- `packages/core/src/events/`
|
|
45
|
+
- `packages/protocol/src/`
|
|
46
|
+
|
|
47
|
+
## Risks
|
|
48
|
+
|
|
49
|
+
- If session APIs expose mutable internals, daemon/client boundaries will be harder to enforce later.
|
|
50
|
+
- If buildContext starts handling future event types now, M1 becomes harder to test. Use explicit M1 handlers only.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# S0005: Runtime Loop
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement a minimal runtime loop that can turn a prepared context into a stream of assistant runtime events without owning session state.
|
|
6
|
+
|
|
7
|
+
## Deliverable
|
|
8
|
+
|
|
9
|
+
- `ScorelRuntime.executeTurn(context, systemPrompt, options)`.
|
|
10
|
+
- Raw runtime event stream for turn start/end, text delta, message end, and error.
|
|
11
|
+
- Provider adapter seam that supports a deterministic fake provider in tests.
|
|
12
|
+
- Minimal tool loop support only if required to satisfy M1's "LLM + tool loop" criterion.
|
|
13
|
+
- Runtime cancel signal handling that stops generation without corrupting persisted session state.
|
|
14
|
+
|
|
15
|
+
## Success Criteria
|
|
16
|
+
|
|
17
|
+
- Runtime accepts context as input and does not read or write JSONL.
|
|
18
|
+
- Runtime emits ordered raw events that RuntimeBridge can later convert to protocol events.
|
|
19
|
+
- Fake provider tests can simulate streaming, completion, provider error, and cancellation.
|
|
20
|
+
- No app, daemon, or client code is needed to test runtime behavior.
|
|
21
|
+
|
|
22
|
+
## Boundaries
|
|
23
|
+
|
|
24
|
+
- No daemon persistence.
|
|
25
|
+
- No RuntimeBridge implementation.
|
|
26
|
+
- No MCP loading.
|
|
27
|
+
- No permission policy.
|
|
28
|
+
- No parallel tool execution.
|
|
29
|
+
- No steer/followUp queues.
|
|
30
|
+
- No provider-specific behavior in tests that would require API keys.
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
- `pnpm --filter @scorel/core test -- runtime`
|
|
35
|
+
- Unit tests assert raw event ordering for success, error, and cancel.
|
|
36
|
+
- Fake provider/fake tool tests cover the minimum M1 tool loop.
|
|
37
|
+
- `pnpm -r typecheck`
|
|
38
|
+
|
|
39
|
+
## Affected Paths
|
|
40
|
+
|
|
41
|
+
- `packages/core/src/runtime/`
|
|
42
|
+
- `packages/core/src/tools/`
|
|
43
|
+
- `packages/protocol/src/`
|
|
44
|
+
|
|
45
|
+
## Risks
|
|
46
|
+
|
|
47
|
+
- Pulling persistence into runtime would violate the core architecture. Runtime must stay a pure execution engine.
|
|
48
|
+
- Depending on a real provider in CI would make tests flaky and block contributors without credentials.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# S0006: Embedded Daemon + Client
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Close the internal M1 control loop by connecting DaemonClient to an embedded daemon that owns session writes, runtime execution, and event broadcast.
|
|
6
|
+
|
|
7
|
+
## Deliverable
|
|
8
|
+
|
|
9
|
+
- Embedded daemon lifecycle.
|
|
10
|
+
- `RuntimeBridge` that converts raw runtime events into protocol events.
|
|
11
|
+
- `SessionLane` that serializes writes for one session.
|
|
12
|
+
- `EventBroadcaster` for connected clients.
|
|
13
|
+
- `DaemonClient` request/response handling and local UI state projection.
|
|
14
|
+
- In-process embedded transport for CLI Alpha.
|
|
15
|
+
|
|
16
|
+
## Success Criteria
|
|
17
|
+
|
|
18
|
+
- `DaemonClient.sendMessage()` causes daemon to append the user event, execute runtime, persist assistant output, and broadcast events.
|
|
19
|
+
- Daemon is the only writer to session storage in this path.
|
|
20
|
+
- Embedded daemon accepts `sessionsDir`, the directory that directly stores session JSONL files. The product default is fixed at `~/.scorel/sessions`; tests and debugging may pass an explicit directory.
|
|
21
|
+
- Client receives transient streaming events and final persistent events.
|
|
22
|
+
- Multiple embedded clients can subscribe to the same session in-process for tests.
|
|
23
|
+
- CLI-facing code never imports runtime/session directly.
|
|
24
|
+
|
|
25
|
+
## Boundaries
|
|
26
|
+
|
|
27
|
+
- No Unix socket or WebSocket transport.
|
|
28
|
+
- No auth.
|
|
29
|
+
- No process-level daemon service.
|
|
30
|
+
- No remote reconnect beyond in-memory `lastSeq` basics needed by the client reducer.
|
|
31
|
+
- No channel manager.
|
|
32
|
+
|
|
33
|
+
## Verification
|
|
34
|
+
|
|
35
|
+
- `pnpm --filter @scorel/daemon test`
|
|
36
|
+
- `pnpm --filter @scorel/client test`
|
|
37
|
+
- Integration test: client sends a message, daemon writes JSONL, runtime emits stream, client receives ordered events.
|
|
38
|
+
- Boundary test confirms `@scorel/client` does not depend on `@scorel/core` or `@scorel/daemon`.
|
|
39
|
+
- `pnpm -r typecheck`
|
|
40
|
+
|
|
41
|
+
## Affected Paths
|
|
42
|
+
|
|
43
|
+
- `packages/daemon/`
|
|
44
|
+
- `packages/client/`
|
|
45
|
+
- `packages/core/`
|
|
46
|
+
- `packages/protocol/`
|
|
47
|
+
|
|
48
|
+
## Risks
|
|
49
|
+
|
|
50
|
+
- If embedded transport leaks daemon internals into `@scorel/client`, WebUI/browser support will be compromised.
|
|
51
|
+
- If SessionLane is skipped for M1, later multi-client behavior will need a painful rewrite.
|