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 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.28'
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.28",
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
  ],