@gavinc-cn/touchstone-dsh 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +22 -1
- package/README.zh.md +12 -1
- package/agents.py +0 -0
- package/auth.py +0 -0
- package/board.py +1119 -93
- package/builtin_assets.py +0 -0
- package/builtin_prompts/free_style/flow.md +0 -0
- package/builtin_prompts/free_style/load_test.md +0 -0
- package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
- package/builtin_prompts/free_style/templates/case_template.md +0 -0
- package/builtin_prompts/free_style/templates/index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
- package/builtin_prompts/free_style/templates/status_template.md +0 -0
- package/chat.py +590 -17
- package/db.py +63 -12
- package/dsh-plugin/cordis.patch.yml +7 -3
- package/dsh-plugin/install.sh +8 -5
- package/dsh-plugin/lib/agent-driver.js +895 -126
- package/dsh-plugin/lib/client.bundle.js +94 -9
- package/dsh-plugin/lib/index.js +399 -20
- package/dsh-plugin/scripts/build.mjs +0 -0
- package/dsh-plugin/scripts/prepack-check.mjs +0 -0
- package/dsh-plugin/scripts/select-bundle.mjs +0 -0
- package/dsh-plugin/src/client.js +94 -9
- package/dshdriver.py +74 -5
- package/dshevents.py +100 -6
- package/export_cases.py +0 -0
- package/extensions/dsh-autocommit/asset.json +0 -0
- package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
- package/extensions/dsh-autocommit/pkg/package.json +0 -0
- package/feishu.py +1226 -42
- package/feishu_conv.py +920 -0
- package/lib.py +0 -0
- package/lifecycle.py +0 -0
- package/loadcase.py +0 -0
- package/loadgen.py +0 -0
- package/localbus.py +0 -0
- package/migrate_p7b.py +0 -0
- package/package.json +8 -3
- package/platcompat.py +0 -0
- package/prompts.py +0 -0
- package/rag.py +0 -0
- package/requirements.txt +2 -1
- package/runner.py +11 -0
- package/server.py +264 -30
- package/sessparse.py +244 -44
- package/touchstone.py +8 -4
- package/waitq.py +23 -0
- package/webui/dist-plugin/assets/index-JLwyZZxC.js +484 -0
- package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +1 -0
- package/webui/dist-plugin/index.html +2 -2
- package/worktree.py +100 -0
- package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
- package/webui/dist-plugin/assets/index-eHpxBfP6.js +0 -473
package/sessparse.py
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
tool_result {call_id, name, text, is_error, truncated}
|
|
20
20
|
(dsh 事件流没有 usage 汇总事件与「本轮失败」独立通道,故不产出 usage / error 条目;
|
|
21
21
|
响应里的 totals 字段保留为 0,维持 API 形状稳定。)
|
|
22
|
+
|
|
23
|
+
上下文占用(会话窗底栏 ctx 圈,2026-10-10 修「恒为 0」):dsh 会话日志里有两个
|
|
24
|
+
可用槽位——`request/context` 事件带**上下文窗口**(适配器声明,见 _parse_dsh)、
|
|
25
|
+
`assistant/message` 带本次调用的 **TokenUsage**;`load()` 在有窗口时附带
|
|
26
|
+
`ctx={"used","max"}`(分子口径=prompt 侧 token 数,见 _dsh_prompt_tokens)。
|
|
22
27
|
"""
|
|
23
28
|
|
|
24
29
|
import base64
|
|
@@ -26,6 +31,7 @@ import glob
|
|
|
26
31
|
import json
|
|
27
32
|
import os
|
|
28
33
|
import re
|
|
34
|
+
import threading
|
|
29
35
|
|
|
30
36
|
import zstandard
|
|
31
37
|
|
|
@@ -70,13 +76,50 @@ def _obj(raw):
|
|
|
70
76
|
return d if isinstance(d, dict) else None
|
|
71
77
|
|
|
72
78
|
|
|
79
|
+
def _nonneg_int(v):
|
|
80
|
+
"""JSON 值 → 非负整数,其他(bool / 字符串 / 负数 / None)一律 None。
|
|
81
|
+
|
|
82
|
+
token 计数与窗口大小都必须是干净的整数:`True` 在 Python 里是 int 子类,
|
|
83
|
+
直接参与算术会算出垃圾值。
|
|
84
|
+
"""
|
|
85
|
+
return v if isinstance(v, int) and not isinstance(v, bool) and v >= 0 else None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _dsh_prompt_tokens(usage):
|
|
89
|
+
"""dsh `TokenUsage` → 本次请求的 **prompt 侧 token 数**(上下文占用分子)。
|
|
90
|
+
|
|
91
|
+
口径照抄 dsh `dsh-token-meter` 的 `pressureFrom`:
|
|
92
|
+
`inputTokens + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0)`=「这一次请求
|
|
93
|
+
送进模型的上下文有多大」。**不能**用 totalTokens:它含本次回复的输出 token,
|
|
94
|
+
环会随回复长度越画越大(且与上下文窗口不成比例)。inputTokens 缺失(老格式)
|
|
95
|
+
退回 `totalTokens - outputTokens`;再取不到返回 None(调用方保留上一槽位值)。
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(usage, dict):
|
|
98
|
+
return None
|
|
99
|
+
inp = _nonneg_int(usage.get("inputTokens"))
|
|
100
|
+
if inp is not None:
|
|
101
|
+
return inp + (_nonneg_int(usage.get("cacheReadTokens")) or 0) \
|
|
102
|
+
+ (_nonneg_int(usage.get("cacheWriteTokens")) or 0)
|
|
103
|
+
total = _nonneg_int(usage.get("totalTokens"))
|
|
104
|
+
out = _nonneg_int(usage.get("outputTokens"))
|
|
105
|
+
if total is not None and out is not None and total >= out:
|
|
106
|
+
return total - out
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
73
110
|
class _Entries:
|
|
74
111
|
"""entry 收集器:自动分配 seq 并累计 totals。"""
|
|
75
112
|
|
|
76
113
|
def __init__(self):
|
|
77
114
|
self.entries = []
|
|
78
|
-
# token/耗时汇总(API 契约字段):dsh 事件流无 usage
|
|
115
|
+
# token/耗时汇总(API 契约字段):dsh 事件流无 usage 汇总事件,恒为 0
|
|
79
116
|
self.totals = {"input": 0, "output": 0, "cache_read": 0, "duration_ms": 0}
|
|
117
|
+
# 上下文占用两槽位(会话窗底栏 ctx 圈,2026-10-10 修「恒为 0」):两个**独立
|
|
118
|
+
# last-wins** 槽位,口径与 dsh token-meter 的 `contextPressure` 投影一致:
|
|
119
|
+
# used = 最新一次 usage 的 prompt 侧 token 数(pressureTokens)
|
|
120
|
+
# max = 最新 `request/context` 事件的 contextWindow(适配器声明的路由窗口)
|
|
121
|
+
# None = 该槽位尚未出现过(老会话 / 适配器未声明窗口 / 还没有一次模型调用)。
|
|
122
|
+
self.ctx = {"used": None, "max": None}
|
|
80
123
|
# dsh 回退锚点(仅 _parse_dsh 填充):[(mid, ese),...] —— 真实用户提问的
|
|
81
124
|
# 合成 mid 与**会话事件 seq**;dsh 没有原地 undo,回退=按该 seq fork 新会话
|
|
82
125
|
self.dsh_anchors = []
|
|
@@ -88,10 +131,30 @@ class _Entries:
|
|
|
88
131
|
|
|
89
132
|
# ---------------------------------------------------------------- dsh 解析
|
|
90
133
|
|
|
91
|
-
# dsh 会话文件为多帧 zstd 拼接(每帧一批 JSONL
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
134
|
+
# dsh 会话文件为多帧 zstd 拼接(每帧一批 JSONL 事件行),逐帧 `decompressobj` 解压
|
|
135
|
+
# (项目唯一第三方依赖,见 requirements.txt)。
|
|
136
|
+
#
|
|
137
|
+
# **解压器必须按线程隔离**(2026-10-09 定因的 SIGSEGV 实障):zstandard 的
|
|
138
|
+
# `ZstdDecompressor` **实例并发调用 `decompressobj()` 不是线程安全的**——原先这里
|
|
139
|
+
# 是模块级共享实例,而站点是多线程的(HTTP 请求线程池 + board 30s 调度线程 +
|
|
140
|
+
# 会话窗读口),并发解压直接 **SIGSEGV 打崩整个后端**(面板 502,薄壳不自愈)。
|
|
141
|
+
# 实测对照(8 线程 × 25s 反复解压真实会话文件):共享实例 **core dumped**;
|
|
142
|
+
# 每线程独立实例同负载 765 次解压**零崩溃**。崩溃现场见
|
|
143
|
+
# `doc_ai/bug_report/20261009_0600_插件后端两次SIGSEGV…`(faulthandler 栈落在
|
|
144
|
+
# `server.py:_api_get_board` 的 `session_title` 调用行,扩展模块清单只有
|
|
145
|
+
# `zstandard.backend_c`)。
|
|
146
|
+
_DEC_TLS = threading.local()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dec():
|
|
150
|
+
"""取**本线程**的 zstd 解压器(无则新建并缓存,随线程回收)。
|
|
151
|
+
|
|
152
|
+
不要改回模块级共享实例——那正是 SIGSEGV 的根因(见上方注释)。
|
|
153
|
+
"""
|
|
154
|
+
d = getattr(_DEC_TLS, "d", None)
|
|
155
|
+
if d is None:
|
|
156
|
+
d = _DEC_TLS.d = zstandard.ZstdDecompressor()
|
|
157
|
+
return d
|
|
95
158
|
|
|
96
159
|
|
|
97
160
|
def _dsh_decompressed(path):
|
|
@@ -115,7 +178,7 @@ def _dsh_decompressed(path):
|
|
|
115
178
|
chunks = []
|
|
116
179
|
rest = buf
|
|
117
180
|
while rest:
|
|
118
|
-
obj =
|
|
181
|
+
obj = _dec().decompressobj()
|
|
119
182
|
try:
|
|
120
183
|
chunks.append(obj.decompress(rest))
|
|
121
184
|
except zstandard.ZstdError:
|
|
@@ -139,7 +202,8 @@ def _dsh_session_file(sid):
|
|
|
139
202
|
"""按会话 id 跨工作区 bucket 定位 zstd 会话文件(返回路径或 None)。
|
|
140
203
|
|
|
141
204
|
id 形态两代并存(本机实测 2026-10-02):`session-<uuid>`(dsh web/headless 新建)
|
|
142
|
-
与裸 `<uuid
|
|
205
|
+
与裸 `<uuid>`(**绝大多数是子代理会话**,少数是老主会话,如 64655aea-…;
|
|
206
|
+
2026-10-07 实测 82 个裸 uuid 目录里 73 个 origin=subagent、9 个主会话)。
|
|
143
207
|
文件名也带格式版本:`session.v4.jsonl.zstd`(62 个)/ `session.v3.jsonl.zstd`(1 个)
|
|
144
208
|
/ 无版本的 `session.jsonl.zstd`(9 个存量)——**只认无版本名会让 v3/v4 会话全部
|
|
145
209
|
found=false**(真机踩中),故按 `session*.jsonl.zstd` 通配并优先取版本号最大者。
|
|
@@ -160,6 +224,93 @@ def _dsh_format_version(path):
|
|
|
160
224
|
return int(m.group(1)) if m else 0
|
|
161
225
|
|
|
162
226
|
|
|
227
|
+
# dsh 会话**头行**缓存:{path: (stamp, header)}。头行承载 origin/delegationDepth/cwd,
|
|
228
|
+
# 是判「子代理会话」的唯一可靠依据(目录名不是判据,见 is_subagent)。
|
|
229
|
+
_DSH_HEADER_CACHE = {}
|
|
230
|
+
_DSH_HEADER_FRAMES = 4 # 头行最多向前找几帧(真机 304 个会话全在第 1 帧第 1 行)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _dsh_header(path):
|
|
234
|
+
"""读 dsh 会话**头行**(`{"type":"session","id":…,"origin":…,"delegationDepth":…}`),
|
|
235
|
+
读不到/无头行返回 {}(老样本与异常一律降级为空)。
|
|
236
|
+
|
|
237
|
+
只解前 `_DSH_HEADER_FRAMES` 帧即停(头行就是第 1 帧第 1 行,实测 278 字节)——
|
|
238
|
+
与 `_dsh_decompressed` 的全量解压相比,判一个会话是不是子代理不该付整段解压的
|
|
239
|
+
代价(枚举 300+ 会话时差一个量级)。按 (mtime_ns, size) 缓存:会话只追加,
|
|
240
|
+
stamp 变即失效重读。
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
st = os.stat(path)
|
|
244
|
+
stamp = (st.st_mtime_ns, st.st_size)
|
|
245
|
+
except OSError:
|
|
246
|
+
return {}
|
|
247
|
+
hit = _DSH_HEADER_CACHE.get(path)
|
|
248
|
+
if hit and hit[0] == stamp:
|
|
249
|
+
return hit[1]
|
|
250
|
+
header = {}
|
|
251
|
+
try:
|
|
252
|
+
with open(path, "rb") as f:
|
|
253
|
+
rest = f.read()
|
|
254
|
+
except OSError:
|
|
255
|
+
return {}
|
|
256
|
+
for _ in range(_DSH_HEADER_FRAMES):
|
|
257
|
+
if not rest:
|
|
258
|
+
break
|
|
259
|
+
obj = _dec().decompressobj()
|
|
260
|
+
try:
|
|
261
|
+
text = obj.decompress(rest).decode("utf-8", "replace")
|
|
262
|
+
except zstandard.ZstdError:
|
|
263
|
+
break # 坏帧:头行读不到就按「未知」(=主会话)
|
|
264
|
+
rest = obj.unused_data # 余量即下一帧(逐帧解压,同 _dsh_decompressed)
|
|
265
|
+
for line in text.split("\n"):
|
|
266
|
+
line = line.strip()
|
|
267
|
+
if not line:
|
|
268
|
+
continue
|
|
269
|
+
d = _obj(line)
|
|
270
|
+
if d is not None and d.get("type") == "session":
|
|
271
|
+
header = d
|
|
272
|
+
break
|
|
273
|
+
if header:
|
|
274
|
+
break
|
|
275
|
+
_DSH_HEADER_CACHE[path] = (stamp, header)
|
|
276
|
+
return header
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _header_subagent(header):
|
|
280
|
+
"""头行 → 是否子代理会话:`origin=subagent` 为主判据,`delegationDepth>0` 兜底。
|
|
281
|
+
|
|
282
|
+
2026-10-07 全机实测 304 个会话目录:73 个 `origin=subagent`(且 delegationDepth
|
|
283
|
+
全为 1)、231 个主会话(origin 字段缺失、delegationDepth 全为 0),两集合严格互斥。
|
|
284
|
+
两个判据都留,是防 dsh 后续只写其一。
|
|
285
|
+
"""
|
|
286
|
+
if str(header.get("origin") or "") == "subagent":
|
|
287
|
+
return True
|
|
288
|
+
try:
|
|
289
|
+
return int(header.get("delegationDepth") or 0) > 0
|
|
290
|
+
except (TypeError, ValueError):
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def is_subagent(family, sid):
|
|
295
|
+
"""该会话是否 dsh **子代理会话**(主会话派生的调研/审计子代理,非用户会话)。
|
|
296
|
+
|
|
297
|
+
子代理会话与主会话同 bucket、同格式,只能靠头行区分(见 `_header_subagent`;
|
|
298
|
+
**目录名不是判据**——裸 uuid 目录 82 个里 73 个是子代理,另 9 个是老主会话)。
|
|
299
|
+
子代理是主会话的实现细节:不建看板卡、不进「绑定已有会话」下拉、不按「外部会话
|
|
300
|
+
在跑」占项目运行位(看板 sync 投影 / server 绑定下拉 / board 占用三处共用本读口)。
|
|
301
|
+
|
|
302
|
+
族不在白名单 / 存储不存在 / 头行缺失一律 False(未知按主会话处理——占用判定
|
|
303
|
+
宁多等不可误放行,与 `dshevents` 的「断连=未知」同向)。
|
|
304
|
+
"""
|
|
305
|
+
if family not in FAMILIES:
|
|
306
|
+
return False
|
|
307
|
+
try:
|
|
308
|
+
path = _dsh_session_file(sid)
|
|
309
|
+
except Exception:
|
|
310
|
+
return False
|
|
311
|
+
return _header_subagent(_dsh_header(path)) if path else False
|
|
312
|
+
|
|
313
|
+
|
|
163
314
|
def dsh_bucket(cwd):
|
|
164
315
|
"""工作区路径 → dsh 会话 bucket 目录。
|
|
165
316
|
|
|
@@ -193,8 +344,9 @@ def dsh_latest_session(cwd):
|
|
|
193
344
|
return best
|
|
194
345
|
|
|
195
346
|
|
|
196
|
-
# path -> (stamp, title, first_prompt)
|
|
197
|
-
#
|
|
347
|
+
# path -> (stamp, title, first_prompt, titles):当前标题、首问与全部标题事件同源
|
|
348
|
+
# 同缓存——三者都要全量解压会话文件,一次扫描一起取(30s 同步节拍里同一文件只解压
|
|
349
|
+
# 一次)
|
|
198
350
|
_DSH_TITLE_CACHE = {}
|
|
199
351
|
|
|
200
352
|
|
|
@@ -209,15 +361,31 @@ def _line_at(text, idx):
|
|
|
209
361
|
return text[start:] if end < 0 else text[start:end]
|
|
210
362
|
|
|
211
363
|
|
|
212
|
-
def
|
|
213
|
-
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
364
|
+
def _dsh_scan_titles(text):
|
|
365
|
+
"""全部 `session/title` 事件的标题,按文件顺序(无事件返回 [])。
|
|
366
|
+
|
|
367
|
+
**末枚即 DSH 当前标题**(dsh 对同一会话会写多枚:先 fallback 截断句、再由 LLM
|
|
368
|
+
标题覆盖、用户显式改名再追加;GUI 侧栏显示 last-wins 的那一枚)。平台侧一律
|
|
369
|
+
取同一口径(会话窗 / 任务标题 / 看板 sync 卡标题);`list_sessions` 另把整串
|
|
370
|
+
下发给看板,用于判定「卡面标题是否仍是平台自动写入的形态」。
|
|
371
|
+
|
|
372
|
+
逐次 find + **整行 JSON 校验**(同 `_dsh_scan_prompt`):非事件行里出现同名
|
|
373
|
+
字符串(坏行 / 别处文本)一律跳过;空标题事件跳过(dsh 不会写空标题,真出现
|
|
374
|
+
时按「不表态」处理,避免把当前标题抹成空串)。
|
|
375
|
+
"""
|
|
376
|
+
out = []
|
|
377
|
+
pos = 0
|
|
378
|
+
while True:
|
|
379
|
+
idx = text.find('"session/title"', pos)
|
|
380
|
+
if idx < 0:
|
|
381
|
+
return out
|
|
382
|
+
pos = idx + 1
|
|
383
|
+
d = _obj(_line_at(text, idx))
|
|
384
|
+
if d is None or d.get("type") != "session/title":
|
|
385
|
+
continue
|
|
386
|
+
title = str((d.get("data") or {}).get("title") or "")
|
|
387
|
+
if title:
|
|
388
|
+
out.append(title)
|
|
221
389
|
|
|
222
390
|
|
|
223
391
|
def _dsh_scan_prompt(text):
|
|
@@ -225,7 +393,7 @@ def _dsh_scan_prompt(text):
|
|
|
225
393
|
|
|
226
394
|
判定与会话窗口的 user entry 同口径:`user/message` 且 `source.kind == "user"`
|
|
227
395
|
(系统注入的 plugin / agent-instructions 消息被过滤);文本块按换行拼接、
|
|
228
|
-
|
|
396
|
+
**原样保留内部换行**——调用方(看板同步卡)直接作为卡描述(首问全文)。
|
|
229
397
|
只有图片没有文本的提问返回 `[图片]`(沿用会话窗的图片占位口径)。
|
|
230
398
|
|
|
231
399
|
「第一条」按**文件顺序**取(会话窗的展示顺序;真机 20 个会话核对:文件序首条
|
|
@@ -254,31 +422,33 @@ def _dsh_scan_prompt(text):
|
|
|
254
422
|
|
|
255
423
|
|
|
256
424
|
def _dsh_meta(path):
|
|
257
|
-
"""一次解压取出 (
|
|
258
|
-
|
|
259
|
-
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
425
|
+
"""一次解压取出 (当前标题, 主会话首问原文, 全部标题事件),按文件 stamp 缓存。
|
|
426
|
+
|
|
427
|
+
- 当前标题 = `titles` 的**末枚**(`session/title` 事件 last-wins)= dsh GUI 侧栏
|
|
428
|
+
显示的标题(口径说明见 `_dsh_scan_titles`);
|
|
429
|
+
- 首问 = 见 `_dsh_scan_prompt`,是看板 sync 卡描述(首问全文)的数据源;
|
|
430
|
+
- 全部标题事件(tuple)= 该会话历史上出现过的每个标题,看板 sync 卡据此判定
|
|
431
|
+
「卡面标题是否仍是平台自动写入的形态」(建卡时只有兜底标题、LLM 标题后到的
|
|
432
|
+
窗口里,旧的兜底标题只存在于事件历史中)。
|
|
264
433
|
"""
|
|
265
434
|
try:
|
|
266
435
|
st = os.stat(path)
|
|
267
436
|
stamp = (st.st_mtime_ns, st.st_size)
|
|
268
437
|
except OSError:
|
|
269
|
-
return "", ""
|
|
438
|
+
return "", "", ()
|
|
270
439
|
hit = _DSH_TITLE_CACHE.get(path)
|
|
271
440
|
if hit and hit[0] == stamp:
|
|
272
|
-
return hit[1], hit[2]
|
|
441
|
+
return hit[1], hit[2], hit[3]
|
|
273
442
|
text = _dsh_decompressed(path)
|
|
274
|
-
|
|
443
|
+
titles = tuple(_dsh_scan_titles(text))
|
|
444
|
+
title = titles[-1] if titles else ""
|
|
275
445
|
prompt = _dsh_scan_prompt(text)
|
|
276
|
-
_DSH_TITLE_CACHE[path] = (stamp, title, prompt)
|
|
277
|
-
return title, prompt
|
|
446
|
+
_DSH_TITLE_CACHE[path] = (stamp, title, prompt, titles)
|
|
447
|
+
return title, prompt, titles
|
|
278
448
|
|
|
279
449
|
|
|
280
450
|
def dsh_title(sid):
|
|
281
|
-
"""读取 dsh
|
|
451
|
+
"""读取 dsh 会话当前标题(最后一枚 session/title 事件),无/不可读返回 ''。"""
|
|
282
452
|
path = _dsh_session_file(sid)
|
|
283
453
|
return _dsh_meta(path)[0] if path else ""
|
|
284
454
|
|
|
@@ -286,7 +456,7 @@ def dsh_title(sid):
|
|
|
286
456
|
def dsh_first_prompt(sid):
|
|
287
457
|
"""读取主会话第一条真实用户提问的原文(可多行),无/不可读返回 ''。
|
|
288
458
|
|
|
289
|
-
看板 sync
|
|
459
|
+
看板 sync 卡的描述数据源(见 board.sync_sessions):会话还没收到提问时
|
|
290
460
|
返回 '',调用方按会话标题 / sid 短码兜底,首问落盘后由同步节拍补齐。
|
|
291
461
|
"""
|
|
292
462
|
path = _dsh_session_file(sid)
|
|
@@ -344,6 +514,18 @@ def _parse_dsh(path):
|
|
|
344
514
|
col.add("think", tm, text=b.get("text", ""))
|
|
345
515
|
elif b.get("type") == "text":
|
|
346
516
|
col.add("assistant", tm, text=b.get("text", ""))
|
|
517
|
+
# 上下文占用分子(会话窗 ctx 圈):每次模型调用结算的 prompt 侧 token 数,
|
|
518
|
+
# last-wins(无 usage 的中断消息不参与,槽位保留上一次可用的值)
|
|
519
|
+
used = _dsh_prompt_tokens(data.get("usage"))
|
|
520
|
+
if used is not None:
|
|
521
|
+
col.ctx["used"] = used
|
|
522
|
+
elif t == "request/context":
|
|
523
|
+
# 上下文占用分母(会话窗 ctx 圈):宿主为该路由解析出的上下文窗口
|
|
524
|
+
# (适配器声明才有;中途 /model 换模型会再落一条,故 last-wins)。
|
|
525
|
+
# 0/缺失一律忽略——窗口必须为正才有意义(宿主 schema 也是 positive)。
|
|
526
|
+
win = _nonneg_int(data.get("contextWindow"))
|
|
527
|
+
if win:
|
|
528
|
+
col.ctx["max"] = win
|
|
347
529
|
elif t == "tool/call":
|
|
348
530
|
cid = data.get("callId", "")
|
|
349
531
|
tool_names[cid] = data.get("name", "")
|
|
@@ -460,6 +642,8 @@ def load(family, sid, agent, after=0):
|
|
|
460
642
|
或 {"found": False, "reason": "missing" | "unsupported"}。
|
|
461
643
|
entries 为 seq >= after 的增量;total 为全量条数(变小时客户端应重置重拉)。
|
|
462
644
|
dsh 为单线会话(固定 main,无子 agent)。
|
|
645
|
+
另在**分母(上下文窗口)已知**时附带 `ctx`(会话窗底栏占用圈的数据源,
|
|
646
|
+
见 _dsh_prompt_tokens / `request/context`);窗口未知时**不带**该键。
|
|
463
647
|
"""
|
|
464
648
|
if family not in FAMILIES:
|
|
465
649
|
return {"found": False, "reason": "unsupported"}
|
|
@@ -476,14 +660,21 @@ def load(family, sid, agent, after=0):
|
|
|
476
660
|
cache = _CACHE.get(key)
|
|
477
661
|
if cache is None or cache["stamp"] != stamp:
|
|
478
662
|
col = _parse_dsh(zpath)
|
|
479
|
-
cache = {"stamp": stamp, "entries": col.entries, "totals": col.totals
|
|
663
|
+
cache = {"stamp": stamp, "entries": col.entries, "totals": col.totals,
|
|
664
|
+
"ctx": col.ctx}
|
|
480
665
|
_CACHE[key] = cache
|
|
481
666
|
except (OSError, ValueError):
|
|
482
667
|
return {"found": False, "reason": "missing"}
|
|
483
668
|
entries = cache["entries"]
|
|
484
669
|
after = max(0, int(after or 0))
|
|
485
|
-
|
|
486
|
-
|
|
670
|
+
out = {"found": True, "agents": agents, "agent": agent,
|
|
671
|
+
"entries": entries[after:], "total": len(entries), "totals": cache["totals"]}
|
|
672
|
+
# 上下文占用(会话窗底栏 ctx 圈,2026-10-10):只在**分母已知**时下发——前端按
|
|
673
|
+
# `+ctx.max > 0` 决定画不画进度弧,窗口未知时给了也只能画成 0%(本次修的旧形态)。
|
|
674
|
+
# 分子缺省 0:窗口已知但还没有一次模型调用=空上下文,0% 是真实读数。
|
|
675
|
+
if cache["ctx"]["max"]:
|
|
676
|
+
out["ctx"] = {"used": cache["ctx"]["used"] or 0, "max": cache["ctx"]["max"]}
|
|
677
|
+
return out
|
|
487
678
|
|
|
488
679
|
|
|
489
680
|
def resolve_media(family, sid, agent, media_id):
|
|
@@ -564,7 +755,8 @@ def _dsh_archived_ids():
|
|
|
564
755
|
def session_title(family, sid):
|
|
565
756
|
"""统一会话标题读取(异常静默返回 '',标题只是展示增强,不打断主流程)。
|
|
566
757
|
|
|
567
|
-
dsh
|
|
758
|
+
dsh 标题取会话**当前**标题(末枚 `session/title` 事件,与 dsh GUI 侧栏一致);
|
|
759
|
+
族不在白名单返回 ''。
|
|
568
760
|
"""
|
|
569
761
|
if family not in FAMILIES:
|
|
570
762
|
return ""
|
|
@@ -580,6 +772,10 @@ def _list_dsh(cwd):
|
|
|
580
772
|
|
|
581
773
|
目录名两代并存:`session-<uuid>` 与裸 `<uuid>`;文件名带格式版本
|
|
582
774
|
(session.v4.jsonl.zstd 等),故目录匹配与文件匹配都用通配(见 _dsh_session_file)。
|
|
775
|
+
**子代理会话不进列表**(2026-10-07 实障修复):它由主会话派生,被当成独立会话
|
|
776
|
+
会各自建卡(实测 72 张、70 张永留「待审核」)并在跑时占项目运行位,判据取会话
|
|
777
|
+
头行的 origin/delegationDepth(见 `is_subagent`)。裸 uuid 目录**不代表**是子代理:
|
|
778
|
+
实测 82 个里 73 个是子代理、9 个是老主会话。
|
|
583
779
|
归档只影响归档标记:会话目录仍在 bucket 里,列表照常枚举到它(平台据此把
|
|
584
780
|
归档会话建成落在「已完成」的 sync 卡 / 在绑定下拉里标 [已归档])。
|
|
585
781
|
"""
|
|
@@ -594,20 +790,24 @@ def _list_dsh(cwd):
|
|
|
594
790
|
except OSError:
|
|
595
791
|
continue
|
|
596
792
|
sid = os.path.basename(sdir)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
793
|
+
if _header_subagent(_dsh_header(zfile)):
|
|
794
|
+
continue # 子代理会话:先判头行再解全量(省一次整段解压)
|
|
795
|
+
# 一次解压同时取当前标题、首问与全部标题事件(同 stamp 缓存):看板 sync 卡
|
|
796
|
+
# 建卡/标题同步都要后两者,逐会话各调一次读口会重复解压同一个文件(见 _dsh_meta)
|
|
797
|
+
title, first_prompt, titles = _dsh_meta(zfile)
|
|
798
|
+
out.append({"sid": sid, "title": title, "titles": list(titles),
|
|
799
|
+
"first_prompt": first_prompt,
|
|
601
800
|
"mtime": mtime, "archived": sid in archived})
|
|
602
801
|
return out
|
|
603
802
|
|
|
604
803
|
|
|
605
804
|
def list_sessions(family, cwd):
|
|
606
|
-
"""按工作区枚举 dsh 会话 [{sid, title, first_prompt, mtime秒, archived}],
|
|
805
|
+
"""按工作区枚举 dsh 会话 [{sid, title, titles, first_prompt, mtime秒, archived}],
|
|
607
806
|
mtime 降序、上限 LIST_SESSIONS_LIMIT。
|
|
608
807
|
|
|
609
|
-
`title` =
|
|
610
|
-
|
|
808
|
+
`title` = **当前**会话标题(末枚 session/title 事件,与 dsh GUI 一致)、
|
|
809
|
+
`titles` = 全部标题事件、`first_prompt` = 主会话第一条真实用户提问原文
|
|
810
|
+
(看板 sync 卡取它做描述、取 title 做卡标题)。
|
|
611
811
|
|
|
612
812
|
供看板「绑定已有会话」下拉与「自动同步」共用;`archived` 读宿主归档集
|
|
613
813
|
(`<dsh home>/storages/workspace.json` 的 `global.archivedSessionIds`,读不到
|
package/touchstone.py
CHANGED
|
@@ -6,7 +6,7 @@ Windows 原生运行的首选入口(bash/nohup/kill 不可用),Linux 上
|
|
|
6
6
|
.run/server.log)。用法:
|
|
7
7
|
|
|
8
8
|
python3 touchstone.py start|stop|restart|status|build|test|test-full|test-ui
|
|
9
|
-
# Windows: python touchstone.py 或 touchstone.cmd
|
|
9
|
+
# Windows: python touchstone.py 或 touchstone.cmd(后者自动解析 python → python3)
|
|
10
10
|
|
|
11
11
|
行为对齐 touchstone.sh:
|
|
12
12
|
- start / restart 先按需构建前端(npm install 按需:依赖指纹未变化则跳过,
|
|
@@ -117,8 +117,11 @@ def precheck_deps():
|
|
|
117
117
|
import requests # noqa: F401
|
|
118
118
|
import zstandard # noqa: F401
|
|
119
119
|
except ImportError as e:
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
# 用当前解释器自己的路径提示(不写死 python3/python): Windows 官方安装器只有
|
|
121
|
+
# `python.exe`、Linux 惯例是 `python3`, 报用户机器上真正在跑的那个最不容易误导。
|
|
122
|
+
py = sys.executable or "python"
|
|
123
|
+
print_err(f"错误: 当前解释器缺少运行依赖({e}),请先执行:")
|
|
124
|
+
print_err(f" {py} -m pip install -r requirements.txt")
|
|
122
125
|
sys.exit(1)
|
|
123
126
|
|
|
124
127
|
|
|
@@ -448,7 +451,8 @@ def cmd_test_full():
|
|
|
448
451
|
ran = 0
|
|
449
452
|
for name in ("e2e_board_continue.py", "e2e_board_stream.py", "e2e_chat_queue.py",
|
|
450
453
|
"e2e_unit_state.py", "e2e_stress_case.py", "e2e_board_archive.py",
|
|
451
|
-
"e2e_board_worktree.py"
|
|
454
|
+
"e2e_board_worktree.py", "e2e_ext_stale_recovery.py",
|
|
455
|
+
"e2e_subagent_no_occupancy.py"):
|
|
452
456
|
script = os.path.join(ROOT, "tests", name)
|
|
453
457
|
if not os.path.isfile(script):
|
|
454
458
|
print(f"(跳过: 未随附 tests/{name})")
|
package/waitq.py
CHANGED
|
@@ -727,6 +727,9 @@ def selfcheck(probe=None):
|
|
|
727
727
|
的扫描集内**——③ 只取 `kind=KIND_CARD` 的活跃行,ext 行永不被 ③ 覆盖
|
|
728
728
|
(v3 终审修复:原文「③ 只对『卡行已删』形态告警」不成立;③ 的实际报告面
|
|
729
729
|
是两类 c: 行形态——卡不在 doing(列已非开发容器)+ 卡行已删(行无主))。
|
|
730
|
+
反向形态「占位在、行不在」(doing+queue 卡无活跃 c: 行)**不在本函数面**:
|
|
731
|
+
它要读 board_cards 的排队占位语义,归 `board._heal_queue_placeholders`
|
|
732
|
+
的 30s 节拍(报告 + 补建一体,2026-10-10 实障卡 936)。
|
|
730
733
|
probe:行判活探针(web busy 三态;`selfcheck_units` 透传,直调缺省 None)。
|
|
731
734
|
"""
|
|
732
735
|
problems = []
|
|
@@ -1216,6 +1219,26 @@ def msg_get(msg_id):
|
|
|
1216
1219
|
(str(msg_id),)).fetchone()
|
|
1217
1220
|
|
|
1218
1221
|
|
|
1222
|
+
def msg_meta(msg_id):
|
|
1223
|
+
"""消息等待项 meta(取该 target 的**最后一行**,含终态行):飞书回流钩子在
|
|
1224
|
+
终态收口之后读「提交时定格」的载荷(feishu 命名空间)。无行返回 {},坏 JSON 亦 {}。
|
|
1225
|
+
|
|
1226
|
+
只读口:与 msg_get 相邻;口径上不看 state(收口已把行落终态,活跃口径读不到),
|
|
1227
|
+
也不去 join chat_msgs——载荷只在等待项行上(chat_msgs 无 meta 列)。
|
|
1228
|
+
"""
|
|
1229
|
+
with db.connect() as conn:
|
|
1230
|
+
row = conn.execute(
|
|
1231
|
+
"SELECT meta FROM wait_items WHERE kind=? AND target_id=?"
|
|
1232
|
+
" ORDER BY id DESC LIMIT 1", (KIND_MSG, str(msg_id))).fetchone()
|
|
1233
|
+
if row is None:
|
|
1234
|
+
return {}
|
|
1235
|
+
try:
|
|
1236
|
+
meta = json.loads(row["meta"] or "{}")
|
|
1237
|
+
except ValueError:
|
|
1238
|
+
return {}
|
|
1239
|
+
return meta if isinstance(meta, dict) else {}
|
|
1240
|
+
|
|
1241
|
+
|
|
1219
1242
|
def msg_rows(sid=None, card_id=None, states=None):
|
|
1220
1243
|
"""消息行查询(chat 读侧专用):sid/card_id/states 可选过滤,created_at 升序
|
|
1221
1244
|
(idx_chat_msgs_sid 覆盖 sid 过滤);states 传元组。"""
|