@xiaobuyu/nodesign 0.0.1 → 0.0.2
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 +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
|
|
3
|
-
*
|
|
4
|
-
* 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
|
|
5
|
-
* streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
|
|
6
|
-
* 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
|
|
7
|
-
* ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
|
|
8
|
-
*
|
|
9
|
-
* 新范式(快照 + 尾随,2026-07-27 重构):
|
|
10
|
-
* - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
|
|
11
|
-
* display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
|
|
12
|
-
* 逻辑同构)
|
|
13
|
-
* - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
|
|
14
|
-
* → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
|
|
15
|
-
* 不再影响正确性
|
|
16
|
-
* - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
|
|
17
|
-
* 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
|
|
18
|
-
*
|
|
19
|
-
* 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
|
|
20
|
-
* run.delta.text / thinking → 同 role 连续累加一条消息
|
|
21
|
-
* run.tool_use.started → push tool 消息 status='running'
|
|
22
|
-
* run.delta.tool_use → 补 toolInput
|
|
23
|
-
* run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
|
|
24
|
-
* run.todo.updated → todos 覆盖
|
|
25
|
-
* run.context_usage → contextUsage 覆盖
|
|
26
|
-
* run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
|
|
27
|
-
* run.query.end → 清
|
|
28
|
-
*
|
|
29
|
-
* turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
|
|
30
|
-
* 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
|
|
31
|
-
* 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
|
|
32
|
-
* 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
const MAX_TOOL_TEXT = 16_000;
|
|
36
|
-
// 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
|
|
37
|
-
// 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
|
|
38
|
-
const ENDED_GRACE_MS = 3_000;
|
|
39
|
-
|
|
40
|
-
/** @type {Map<string, object>} sessionId → live turn state */
|
|
41
|
-
const liveTurns = new Map();
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* sessionId → 最近一次 run.context_usage 事件(2026-07-30)
|
|
45
|
-
*
|
|
46
|
-
* 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
|
|
47
|
-
* 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
|
|
48
|
-
*
|
|
49
|
-
* query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
|
|
50
|
-
* 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
|
|
51
|
-
* 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
|
|
52
|
-
* (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
|
|
53
|
-
*
|
|
54
|
-
* 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
|
|
55
|
-
*/
|
|
56
|
-
const lastUsage = new Map();
|
|
57
|
-
const LAST_USAGE_CAP = 200;
|
|
58
|
-
|
|
59
|
-
export function getLastContextUsage(sessionId) {
|
|
60
|
-
return lastUsage.get(sessionId) || null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
|
|
65
|
-
*
|
|
66
|
-
* `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
|
|
67
|
-
* message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
|
|
68
|
-
* handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
|
|
69
|
-
* 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
|
|
70
|
-
* 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
|
|
71
|
-
* (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
|
|
72
|
-
*/
|
|
73
|
-
function rememberUsage(sid, evt) {
|
|
74
|
-
if (typeof evt.totalTokens !== 'number') return;
|
|
75
|
-
if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
|
|
76
|
-
// 先进先出丢一条:进程活得久了不至于攒着所有历史 session
|
|
77
|
-
const oldest = lastUsage.keys().next().value;
|
|
78
|
-
if (oldest) lastUsage.delete(oldest);
|
|
79
|
-
}
|
|
80
|
-
lastUsage.set(sid, evt);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function truncate(s) {
|
|
84
|
-
if (typeof s !== 'string') return s;
|
|
85
|
-
if (s.length <= MAX_TOOL_TEXT) return s;
|
|
86
|
-
return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
|
|
91
|
-
*/
|
|
92
|
-
export function attachLiveTurnTracker(bus) {
|
|
93
|
-
if (bus.__liveTurnTracked) return;
|
|
94
|
-
bus.__liveTurnTracked = true;
|
|
95
|
-
bus.subscribe('*', fold);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
|
|
100
|
-
* 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
|
|
101
|
-
*
|
|
102
|
-
* running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
|
|
103
|
-
* 不把自己切回 streaming 态**。
|
|
104
|
-
*/
|
|
105
|
-
export function getLiveTurnSnapshot(sessionId) {
|
|
106
|
-
const st = liveTurns.get(sessionId);
|
|
107
|
-
if (!st) return null;
|
|
108
|
-
if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
109
|
-
liveTurns.delete(sessionId);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
runId: st.runId,
|
|
114
|
-
seq: st.seq,
|
|
115
|
-
startedAt: st.startedAt,
|
|
116
|
-
running: !st.endedAt,
|
|
117
|
-
messages: st.messages,
|
|
118
|
-
todos: st.todos,
|
|
119
|
-
contextUsage: st.contextUsage,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function fold(evt) {
|
|
124
|
-
const sid = evt.sessionId;
|
|
125
|
-
if (!sid) return;
|
|
126
|
-
|
|
127
|
-
// 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
|
|
128
|
-
// 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
|
|
129
|
-
if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
|
|
130
|
-
|
|
131
|
-
if (evt.type === 'run.start') {
|
|
132
|
-
liveTurns.set(sid, {
|
|
133
|
-
runId: evt.runId || null,
|
|
134
|
-
startedAt: evt.ts || new Date().toISOString(),
|
|
135
|
-
seq: evt.seq || 0,
|
|
136
|
-
messages: [],
|
|
137
|
-
todos: [],
|
|
138
|
-
contextUsage: null,
|
|
139
|
-
endedAt: null,
|
|
140
|
-
_msgCounter: 0,
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const st = liveTurns.get(sid);
|
|
146
|
-
if (!st) return;
|
|
147
|
-
|
|
148
|
-
// 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
|
|
149
|
-
// (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
|
|
150
|
-
// 起把这轮已经放过的事件再放一遍。
|
|
151
|
-
if (st.endedAt) {
|
|
152
|
-
if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
153
|
-
liveTurns.delete(sid);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
|
|
161
|
-
const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
|
|
162
|
-
|
|
163
|
-
switch (evt.type) {
|
|
164
|
-
case 'run.delta.text':
|
|
165
|
-
if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
|
|
166
|
-
break;
|
|
167
|
-
case 'run.delta.thinking':
|
|
168
|
-
if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
|
|
169
|
-
break;
|
|
170
|
-
case 'run.tool_use.started':
|
|
171
|
-
if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
|
|
172
|
-
st.messages.push({
|
|
173
|
-
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
174
|
-
toolInput: undefined, status: 'running', runId: st.runId,
|
|
175
|
-
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
case 'run.delta.tool_use': {
|
|
180
|
-
if (!runMatches) break;
|
|
181
|
-
const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
182
|
-
if (existing) existing.toolInput = evt.input;
|
|
183
|
-
else if (evt.blockId) {
|
|
184
|
-
st.messages.push({
|
|
185
|
-
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
186
|
-
toolInput: evt.input, status: 'running', runId: st.runId,
|
|
187
|
-
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case 'run.delta.tool_result': {
|
|
193
|
-
if (!runMatches) break;
|
|
194
|
-
const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
195
|
-
if (tool) {
|
|
196
|
-
tool.status = evt.ok ? 'success' : 'error';
|
|
197
|
-
if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
|
|
198
|
-
if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
|
|
199
|
-
// images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
|
|
200
|
-
}
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
case 'run.tool_use_summary': {
|
|
204
|
-
if (!runMatches || !evt.summary) break;
|
|
205
|
-
const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
|
|
206
|
-
const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
|
|
207
|
-
if (hit) hit.groupSummary = evt.summary;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
case 'run.todo.updated':
|
|
211
|
-
if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
|
|
212
|
-
break;
|
|
213
|
-
case 'run.context_usage':
|
|
214
|
-
if (runMatches) st.contextUsage = evt;
|
|
215
|
-
break;
|
|
216
|
-
case 'run.done':
|
|
217
|
-
case 'run.error':
|
|
218
|
-
case 'run.cancelled':
|
|
219
|
-
// 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
|
|
220
|
-
if (runMatches) {
|
|
221
|
-
st.endedAt = Date.now();
|
|
222
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
case 'run.query.end':
|
|
226
|
-
liveTurns.delete(sid);
|
|
227
|
-
return;
|
|
228
|
-
default:
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 所有本 session 事件都推进快照游标(包括未折叠类型)——
|
|
233
|
-
// 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
|
|
234
|
-
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function appendText(st, role, text, parentToolUseId = null) {
|
|
238
|
-
if (!text) return;
|
|
239
|
-
const last = st.messages[st.messages.length - 1];
|
|
240
|
-
// 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
|
|
241
|
-
if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
|
|
242
|
-
last.content = (last.content || '') + text;
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
st.messages.push({
|
|
246
|
-
id: `${st.runId || 'live'}:m${st._msgCounter++}`,
|
|
247
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
248
|
-
role,
|
|
249
|
-
content: text,
|
|
250
|
-
runId: st.runId,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
|
|
5
|
+
* streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
|
|
6
|
+
* 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
|
|
7
|
+
* ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
|
|
8
|
+
*
|
|
9
|
+
* 新范式(快照 + 尾随,2026-07-27 重构):
|
|
10
|
+
* - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
|
|
11
|
+
* display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
|
|
12
|
+
* 逻辑同构)
|
|
13
|
+
* - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
|
|
14
|
+
* → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
|
|
15
|
+
* 不再影响正确性
|
|
16
|
+
* - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
|
|
17
|
+
* 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
|
|
18
|
+
*
|
|
19
|
+
* 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
|
|
20
|
+
* run.delta.text / thinking → 同 role 连续累加一条消息
|
|
21
|
+
* run.tool_use.started → push tool 消息 status='running'
|
|
22
|
+
* run.delta.tool_use → 补 toolInput
|
|
23
|
+
* run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
|
|
24
|
+
* run.todo.updated → todos 覆盖
|
|
25
|
+
* run.context_usage → contextUsage 覆盖
|
|
26
|
+
* run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
|
|
27
|
+
* run.query.end → 清
|
|
28
|
+
*
|
|
29
|
+
* turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
|
|
30
|
+
* 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
|
|
31
|
+
* 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
|
|
32
|
+
* 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const MAX_TOOL_TEXT = 16_000;
|
|
36
|
+
// 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
|
|
37
|
+
// 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
|
|
38
|
+
const ENDED_GRACE_MS = 3_000;
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, object>} sessionId → live turn state */
|
|
41
|
+
const liveTurns = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* sessionId → 最近一次 run.context_usage 事件(2026-07-30)
|
|
45
|
+
*
|
|
46
|
+
* 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
|
|
47
|
+
* 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
|
|
48
|
+
*
|
|
49
|
+
* query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
|
|
50
|
+
* 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
|
|
51
|
+
* 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
|
|
52
|
+
* (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
|
|
53
|
+
*
|
|
54
|
+
* 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
|
|
55
|
+
*/
|
|
56
|
+
const lastUsage = new Map();
|
|
57
|
+
const LAST_USAGE_CAP = 200;
|
|
58
|
+
|
|
59
|
+
export function getLastContextUsage(sessionId) {
|
|
60
|
+
return lastUsage.get(sessionId) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
|
|
65
|
+
*
|
|
66
|
+
* `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
|
|
67
|
+
* message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
|
|
68
|
+
* handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
|
|
69
|
+
* 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
|
|
70
|
+
* 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
|
|
71
|
+
* (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
|
|
72
|
+
*/
|
|
73
|
+
function rememberUsage(sid, evt) {
|
|
74
|
+
if (typeof evt.totalTokens !== 'number') return;
|
|
75
|
+
if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
|
|
76
|
+
// 先进先出丢一条:进程活得久了不至于攒着所有历史 session
|
|
77
|
+
const oldest = lastUsage.keys().next().value;
|
|
78
|
+
if (oldest) lastUsage.delete(oldest);
|
|
79
|
+
}
|
|
80
|
+
lastUsage.set(sid, evt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function truncate(s) {
|
|
84
|
+
if (typeof s !== 'string') return s;
|
|
85
|
+
if (s.length <= MAX_TOOL_TEXT) return s;
|
|
86
|
+
return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
|
|
91
|
+
*/
|
|
92
|
+
export function attachLiveTurnTracker(bus) {
|
|
93
|
+
if (bus.__liveTurnTracked) return;
|
|
94
|
+
bus.__liveTurnTracked = true;
|
|
95
|
+
bus.subscribe('*', fold);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
|
|
100
|
+
* 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
|
|
101
|
+
*
|
|
102
|
+
* running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
|
|
103
|
+
* 不把自己切回 streaming 态**。
|
|
104
|
+
*/
|
|
105
|
+
export function getLiveTurnSnapshot(sessionId) {
|
|
106
|
+
const st = liveTurns.get(sessionId);
|
|
107
|
+
if (!st) return null;
|
|
108
|
+
if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
109
|
+
liveTurns.delete(sessionId);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
runId: st.runId,
|
|
114
|
+
seq: st.seq,
|
|
115
|
+
startedAt: st.startedAt,
|
|
116
|
+
running: !st.endedAt,
|
|
117
|
+
messages: st.messages,
|
|
118
|
+
todos: st.todos,
|
|
119
|
+
contextUsage: st.contextUsage,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fold(evt) {
|
|
124
|
+
const sid = evt.sessionId;
|
|
125
|
+
if (!sid) return;
|
|
126
|
+
|
|
127
|
+
// 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
|
|
128
|
+
// 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
|
|
129
|
+
if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
|
|
130
|
+
|
|
131
|
+
if (evt.type === 'run.start') {
|
|
132
|
+
liveTurns.set(sid, {
|
|
133
|
+
runId: evt.runId || null,
|
|
134
|
+
startedAt: evt.ts || new Date().toISOString(),
|
|
135
|
+
seq: evt.seq || 0,
|
|
136
|
+
messages: [],
|
|
137
|
+
todos: [],
|
|
138
|
+
contextUsage: null,
|
|
139
|
+
endedAt: null,
|
|
140
|
+
_msgCounter: 0,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const st = liveTurns.get(sid);
|
|
146
|
+
if (!st) return;
|
|
147
|
+
|
|
148
|
+
// 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
|
|
149
|
+
// (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
|
|
150
|
+
// 起把这轮已经放过的事件再放一遍。
|
|
151
|
+
if (st.endedAt) {
|
|
152
|
+
if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
|
|
153
|
+
liveTurns.delete(sid);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
|
|
161
|
+
const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
|
|
162
|
+
|
|
163
|
+
switch (evt.type) {
|
|
164
|
+
case 'run.delta.text':
|
|
165
|
+
if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
|
|
166
|
+
break;
|
|
167
|
+
case 'run.delta.thinking':
|
|
168
|
+
if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
|
|
169
|
+
break;
|
|
170
|
+
case 'run.tool_use.started':
|
|
171
|
+
if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
|
|
172
|
+
st.messages.push({
|
|
173
|
+
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
174
|
+
toolInput: undefined, status: 'running', runId: st.runId,
|
|
175
|
+
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'run.delta.tool_use': {
|
|
180
|
+
if (!runMatches) break;
|
|
181
|
+
const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
182
|
+
if (existing) existing.toolInput = evt.input;
|
|
183
|
+
else if (evt.blockId) {
|
|
184
|
+
st.messages.push({
|
|
185
|
+
id: evt.blockId, role: 'tool', toolName: evt.name,
|
|
186
|
+
toolInput: evt.input, status: 'running', runId: st.runId,
|
|
187
|
+
...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'run.delta.tool_result': {
|
|
193
|
+
if (!runMatches) break;
|
|
194
|
+
const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
|
|
195
|
+
if (tool) {
|
|
196
|
+
tool.status = evt.ok ? 'success' : 'error';
|
|
197
|
+
if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
|
|
198
|
+
if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
|
|
199
|
+
// images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'run.tool_use_summary': {
|
|
204
|
+
if (!runMatches || !evt.summary) break;
|
|
205
|
+
const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
|
|
206
|
+
const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
|
|
207
|
+
if (hit) hit.groupSummary = evt.summary;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case 'run.todo.updated':
|
|
211
|
+
if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
|
|
212
|
+
break;
|
|
213
|
+
case 'run.context_usage':
|
|
214
|
+
if (runMatches) st.contextUsage = evt;
|
|
215
|
+
break;
|
|
216
|
+
case 'run.done':
|
|
217
|
+
case 'run.error':
|
|
218
|
+
case 'run.cancelled':
|
|
219
|
+
// 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
|
|
220
|
+
if (runMatches) {
|
|
221
|
+
st.endedAt = Date.now();
|
|
222
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
case 'run.query.end':
|
|
226
|
+
liveTurns.delete(sid);
|
|
227
|
+
return;
|
|
228
|
+
default:
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 所有本 session 事件都推进快照游标(包括未折叠类型)——
|
|
233
|
+
// 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
|
|
234
|
+
if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function appendText(st, role, text, parentToolUseId = null) {
|
|
238
|
+
if (!text) return;
|
|
239
|
+
const last = st.messages[st.messages.length - 1];
|
|
240
|
+
// 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
|
|
241
|
+
if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
|
|
242
|
+
last.content = (last.content || '') + text;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
st.messages.push({
|
|
246
|
+
id: `${st.runId || 'live'}:m${st._msgCounter++}`,
|
|
247
|
+
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
248
|
+
role,
|
|
249
|
+
content: text,
|
|
250
|
+
runId: st.runId,
|
|
251
|
+
});
|
|
252
|
+
}
|