dsh-fast-mode 1.5.28 → 1.5.29
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/CHANGELOG.md +5 -0
- package/bench/turns.py +210 -0
- package/index.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.29(1.5.28 的补丁:把 `bench/turns.py` 真的放进包里)
|
|
4
|
+
|
|
5
|
+
- `package.json` 的 `files` 是一份白名单,1.5.28 新增的 `bench/turns.py` 没进去:`npm pack dsh-fast-mode@1.5.28` 里没有这个脚本,而 `CHANGELOG` 和 `bench/README.md` 都在教读者运行它。1.5.29 只改白名单 + 版本号,**代码与 1.5.28 逐字相同**。
|
|
6
|
+
- 复核方式:`npm pack dsh-fast-mode@1.5.29 --silent` 解包后应同时看到 `package/bench/turns.py` 与 `package/index.js` 里的 `VERSION = '1.5.29'`。
|
|
7
|
+
|
|
3
8
|
## 1.5.28(稳定性:压缩后同一轮里不再重复发同一条提醒)
|
|
4
9
|
|
|
5
10
|
- **先量后改**:新增 `bench/turns.py`,把留存会话按 `turn/start` 切成轮,统计「够长却没收到提醒」「同一轮收到两次以上」「多轮会话里后续轮是否重新上膛」。72 小时窗口 68 个会话 / 1446 轮:647 个合格轮**646 个收到**提醒(99.8 %)、后续轮重新上膛 599/600;但**同一轮收到两次的有 114 轮**,其中 **105 轮(92 %)两次之间夹着一次上下文压缩**。
|
package/bench/turns.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Does the late-discipline reminder re-arm per turn, and only once per turn?
|
|
3
|
+
|
|
4
|
+
The reminder is a once-per-turn lever: `disciplineSent` latches it so a long turn
|
|
5
|
+
is told to stop narrating exactly once, and `noteTurn` clears the latch so the
|
|
6
|
+
next request in the same session is told again. Both halves are invisible in the
|
|
7
|
+
wall-clock numbers — a turn that never got the reminder looks like a turn that
|
|
8
|
+
ignored it, and a turn that got it twice looks like a model that keeps forgetting.
|
|
9
|
+
|
|
10
|
+
This reads retained transcripts, segments them on `turn/start`, and reports:
|
|
11
|
+
|
|
12
|
+
- turns per session, steps per turn;
|
|
13
|
+
- turns long enough to qualify (>= 4 steps) that got **no** reminder;
|
|
14
|
+
- turns that got **two or more** reminders (the latch leaking);
|
|
15
|
+
- re-arm rate: in multi-turn sessions, the share of later qualifying turns that
|
|
16
|
+
were reminded again.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python3 bench/turns.py # suite sessions
|
|
20
|
+
python3 bench/turns.py --limit 100 # first 100 transcripts
|
|
21
|
+
python3 bench/turns.py --min-steps 4 --json
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import collections
|
|
28
|
+
import glob
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import subprocess
|
|
32
|
+
import time
|
|
33
|
+
|
|
34
|
+
SESS = os.path.expanduser('~/.dsh/sessions')
|
|
35
|
+
NEEDLE = 'this turn is long enough that prose'
|
|
36
|
+
# A delivered directive is a message part; the model quoting it back inside its
|
|
37
|
+
# own prose is an echo, so assistant-authored text never counts as delivery.
|
|
38
|
+
PLUGIN_ROLES = {'user/message', 'agent/inbox/spliced', 'system/message'}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transcripts(pattern, limit, exclude_arm=('off',)):
|
|
42
|
+
"""Session transcripts whose run directory (the slug) is not one of `exclude_arm`.
|
|
43
|
+
|
|
44
|
+
The slug mirrors the run directory, so `...-suite-D2G2-ledger-p1-def15--`
|
|
45
|
+
identifies the arm without re-reading the YAML.
|
|
46
|
+
"""
|
|
47
|
+
out = []
|
|
48
|
+
for d in sorted(glob.glob(os.path.join(SESS, pattern))):
|
|
49
|
+
name = os.path.basename(d)
|
|
50
|
+
if any(name.endswith(f'-{arm}--') for arm in exclude_arm):
|
|
51
|
+
continue
|
|
52
|
+
out.extend(glob.glob(os.path.join(d, 'session-*', 'session.v4.jsonl.zstd')))
|
|
53
|
+
return out[:limit] if limit else out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def text_of(data):
|
|
57
|
+
msg = data.get('message') or data
|
|
58
|
+
parts = msg.get('content') if isinstance(msg, dict) else None
|
|
59
|
+
if not isinstance(parts, list):
|
|
60
|
+
return ''
|
|
61
|
+
return ' '.join(p.get('text') or '' for p in parts
|
|
62
|
+
if isinstance(p, dict) and p.get('type') == 'text')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def scan(path):
|
|
66
|
+
"""Return (started_ms, turns) for one transcript, or None if unreadable."""
|
|
67
|
+
p = subprocess.run(['zstd', '-dc', path], capture_output=True)
|
|
68
|
+
if p.returncode != 0:
|
|
69
|
+
return None
|
|
70
|
+
started = 0
|
|
71
|
+
turns = []
|
|
72
|
+
cur = {'steps': 0, 'reminders': 0, 'reminderSeqs': [], 'compactions': [],
|
|
73
|
+
'reminderSteps': []}
|
|
74
|
+
for line in p.stdout.decode('utf-8', 'replace').splitlines():
|
|
75
|
+
if ('turn/start' not in line and 'step/start' not in line
|
|
76
|
+
and 'Auto schedule' not in line and 'compaction/start' not in line):
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
o = json.loads(line)
|
|
80
|
+
except Exception: # noqa: BLE001 - a torn line is not fatal
|
|
81
|
+
continue
|
|
82
|
+
kind = o.get('type')
|
|
83
|
+
data = o.get('data') or {}
|
|
84
|
+
if not started and isinstance(o.get('time'), int):
|
|
85
|
+
started = o['time']
|
|
86
|
+
if kind == 'turn/start':
|
|
87
|
+
if cur['steps'] or cur['reminders']:
|
|
88
|
+
turns.append(cur)
|
|
89
|
+
cur = {'steps': 0, 'reminders': 0, 'reminderSeqs': [], 'compactions': [],
|
|
90
|
+
'reminderSteps': []}
|
|
91
|
+
elif kind == 'step/start':
|
|
92
|
+
cur['steps'] += 1
|
|
93
|
+
elif kind == 'compaction/start':
|
|
94
|
+
cur['compactions'].append(o.get('seq'))
|
|
95
|
+
elif kind in PLUGIN_ROLES and NEEDLE in text_of(data):
|
|
96
|
+
cur['reminders'] += 1
|
|
97
|
+
cur['reminderSeqs'].append(o.get('seq'))
|
|
98
|
+
cur['reminderSteps'].append(cur['steps'])
|
|
99
|
+
if cur['steps'] or cur['reminders']:
|
|
100
|
+
turns.append(cur)
|
|
101
|
+
return started, turns
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
ap = argparse.ArgumentParser()
|
|
106
|
+
ap.add_argument('--limit', type=int, default=0, help='transcripts; 0 = all')
|
|
107
|
+
ap.add_argument('--dir', default='--*dsh-runs-suite-*')
|
|
108
|
+
ap.add_argument('--min-steps', type=int, default=4)
|
|
109
|
+
ap.add_argument('--json', action='store_true')
|
|
110
|
+
ap.add_argument('--exclude-arm', action='append', default=[],
|
|
111
|
+
help='skip sessions whose run directory ends in this arm (default: off)')
|
|
112
|
+
ap.add_argument('--explain-doubles', action='store_true',
|
|
113
|
+
help='list every turn that got two reminders, with seqs and compactions')
|
|
114
|
+
ap.add_argument('--since-hours', type=float, default=0,
|
|
115
|
+
help='only sessions started within this many hours')
|
|
116
|
+
a = ap.parse_args()
|
|
117
|
+
|
|
118
|
+
sessions = turns_total = multi = 0
|
|
119
|
+
qualifying = reminded = 0
|
|
120
|
+
misses = doubled = doubled_compacted = 0
|
|
121
|
+
later_qualifying = later_reminded = 0
|
|
122
|
+
per_session_turns = collections.Counter()
|
|
123
|
+
entry_steps = collections.Counter()
|
|
124
|
+
offenders = []
|
|
125
|
+
cutoff = (time.time() - a.since_hours * 3600) * 1000 if a.since_hours else 0
|
|
126
|
+
skipped_old = 0
|
|
127
|
+
for f in transcripts(a.dir, a.limit, tuple(a.exclude_arm or ['off'])):
|
|
128
|
+
r = scan(f)
|
|
129
|
+
if not r:
|
|
130
|
+
continue
|
|
131
|
+
started, turns = r
|
|
132
|
+
if cutoff and started and started < cutoff:
|
|
133
|
+
skipped_old += 1
|
|
134
|
+
continue
|
|
135
|
+
if not turns:
|
|
136
|
+
continue
|
|
137
|
+
sessions += 1
|
|
138
|
+
turns_total += len(turns)
|
|
139
|
+
per_session_turns[len(turns)] += 1
|
|
140
|
+
if len(turns) > 1:
|
|
141
|
+
multi += 1
|
|
142
|
+
for i, t in enumerate(turns):
|
|
143
|
+
if t['steps'] >= a.min_steps:
|
|
144
|
+
qualifying += 1
|
|
145
|
+
if t['reminders']:
|
|
146
|
+
reminded += 1
|
|
147
|
+
else:
|
|
148
|
+
misses += 1
|
|
149
|
+
if i > 0:
|
|
150
|
+
later_qualifying += 1
|
|
151
|
+
later_reminded += 1 if t['reminders'] else 0
|
|
152
|
+
if t['reminders']:
|
|
153
|
+
entry_steps[t['reminderSteps'][0]] += 1
|
|
154
|
+
if t['reminders'] > 1:
|
|
155
|
+
doubled += 1
|
|
156
|
+
seqs = t['reminderSeqs']
|
|
157
|
+
if any(c is not None and seqs and seqs[0] < c < seqs[-1] for c in t['compactions']):
|
|
158
|
+
doubled_compacted += 1
|
|
159
|
+
if a.explain_doubles:
|
|
160
|
+
offenders.append({
|
|
161
|
+
'session': os.path.basename(os.path.dirname(os.path.dirname(f))),
|
|
162
|
+
'turn': i + 1,
|
|
163
|
+
'steps': t['steps'],
|
|
164
|
+
'reminderSeqs': seqs,
|
|
165
|
+
'reminderSteps': t['reminderSteps'],
|
|
166
|
+
'compactions': t['compactions'],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
out = {
|
|
170
|
+
'sessions': sessions,
|
|
171
|
+
'turns': turns_total,
|
|
172
|
+
'multi_turn_sessions': multi,
|
|
173
|
+
'turns_per_session': dict(sorted(per_session_turns.items())),
|
|
174
|
+
'min_steps': a.min_steps,
|
|
175
|
+
'qualifying_turns': qualifying,
|
|
176
|
+
'reminded': reminded,
|
|
177
|
+
'missed': misses,
|
|
178
|
+
'doubled': doubled,
|
|
179
|
+
'doubled_with_compaction_between': doubled_compacted,
|
|
180
|
+
'entry_steps': dict(sorted(entry_steps.items())),
|
|
181
|
+
'later_qualifying_turns': later_qualifying,
|
|
182
|
+
'later_reminded': later_reminded,
|
|
183
|
+
'skipped_old': skipped_old,
|
|
184
|
+
'since_hours': a.since_hours,
|
|
185
|
+
}
|
|
186
|
+
if a.json:
|
|
187
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
188
|
+
return
|
|
189
|
+
print(f"sessions {sessions} turns {turns_total} multi-turn {multi} "
|
|
190
|
+
f"({100 * multi / max(sessions, 1):.0f} %)")
|
|
191
|
+
print(f"turns per session: {dict(sorted(per_session_turns.items()))}")
|
|
192
|
+
print(f"qualifying turns (>= {a.min_steps} steps) {qualifying} "
|
|
193
|
+
f"reminded {reminded} ({100 * reminded / max(qualifying, 1):.0f} %) "
|
|
194
|
+
f"missed {misses} doubled {doubled} "
|
|
195
|
+
f"(with a compaction between: {doubled_compacted})")
|
|
196
|
+
if entry_steps:
|
|
197
|
+
print(f"first reminder landed at step: {dict(sorted(entry_steps.items()))}")
|
|
198
|
+
if a.explain_doubles and offenders:
|
|
199
|
+
print('doubled turns:')
|
|
200
|
+
for o in offenders[:20]:
|
|
201
|
+
print(f" {o['session']} turn {o['turn']} steps {o['steps']} "
|
|
202
|
+
f"reminders at steps {o['reminderSteps']} seqs {o['reminderSeqs']} "
|
|
203
|
+
f"compactions {o['compactions']}")
|
|
204
|
+
print(f"later turns in multi-turn sessions: qualifying {later_qualifying} "
|
|
205
|
+
f"reminded {later_reminded} "
|
|
206
|
+
f"({100 * later_reminded / max(later_qualifying, 1):.0f} %)")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == '__main__':
|
|
210
|
+
main()
|
package/index.js
CHANGED
|
@@ -97,7 +97,7 @@ const TAG = '[fast-mode]'
|
|
|
97
97
|
* Shipped identity of this build. Traced into the debug log so a stale install is
|
|
98
98
|
* visible at a glance, and pinned by a test against `package.json`.
|
|
99
99
|
*/
|
|
100
|
-
export const VERSION = '1.5.
|
|
100
|
+
export const VERSION = '1.5.29'
|
|
101
101
|
const GLOBAL_AGENT = '__global__'
|
|
102
102
|
|
|
103
103
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fast-mode",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.29",
|
|
4
4
|
"description": "DeepSeek Harness fast mode: same thinking effort, fewer round trips. An adaptive parallel tool fan-out, output discipline that never trims the answer, and a cancellable stall guard. Never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"bench/reminders.py",
|
|
45
45
|
"bench/patchshare.py",
|
|
46
46
|
"bench/ablate.py",
|
|
47
|
+
"bench/turns.py",
|
|
47
48
|
"docs/",
|
|
48
49
|
"LICENSE"
|
|
49
50
|
],
|