zames_pro 2.82.52 → 2.82.54

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
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.82.54] - 2026-10-23
11
+
12
+ ### Fixed
13
+
14
+ - **telemetry:** measure a rate-limit block once, at the lift
15
+ - **hooks:** load the config before firing the Stop hook
16
+ - **history:** match fragment types case-insensitively in every parser
17
+
18
+ ## [2.82.53] - 2026-10-23
19
+
20
+ ### Fixed
21
+
22
+ - **glm:** keep a message without a timestamp in dialogue order
23
+ - **qwen:** keep a message without a timestamp in dialogue order
24
+ - **rewind:** resolve a full checkpoint stamp against the whole index
25
+
10
26
  ## [2.82.52] - 2026-10-23
11
27
 
12
28
  ### Fixed
@@ -49,7 +49,11 @@ export function parseHistoryMessages(messages) {
49
49
  const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
50
50
  let text = '';
51
51
  for (const fr of (raw && raw.fragments) || []) {
52
- if (!fr || fr.type !== want)
52
+ // The fragment TYPE is matched case-insensitively, like the role: a
53
+ // payload spelling it `response`/`request` (a proxy build, a JSON
54
+ // round-trip through a normalizer) skipped EVERY fragment and the
55
+ // restored dialogue came back empty although it carried the answer.
56
+ if (!fr || String(fr.type ?? '').toUpperCase() !== want)
53
57
  continue;
54
58
  if (typeof fr.content === 'string')
55
59
  text += fr.content;
@@ -44,10 +44,30 @@ export function rotateRecords(records, max) {
44
44
  * Resolve a `/rewind` selector against records sorted NEWEST FIRST.
45
45
  * `1` = most recent; a non-numeric selector is matched as a stamp prefix.
46
46
  */
47
- export function pickRecord(records, selector) {
47
+ /** True for a selector that is exactly a full epoch-ms stamp (13 digits). */
48
+ export function isFullStampSelector(selector) {
49
+ return /^\d{13}$/.test(String(selector ?? '').trim());
50
+ }
51
+ const isFullStamp = isFullStampSelector;
52
+ export function pickRecord(records, selector,
53
+ /** The FULL record set, when `records` is only a display window. */
54
+ allRecords) {
48
55
  const sel = String(selector ?? '').trim();
49
56
  if (!sel)
50
57
  return null;
58
+ // A FULL epoch-ms stamp names exactly ONE checkpoint, wherever it sits in
59
+ // the index. The command resolves against the newest-10 display window while
60
+ // restore() searches the WHOLE index, so an older checkpoint picked by its
61
+ // full stamp used to report "/rewind failed" — and, worse, a stamp that
62
+ // happened to share its first digits with a NEWER one was resolved to that
63
+ // WRONG record. A full stamp is therefore matched against the full set
64
+ // FIRST, and only then treated as an index.
65
+ if (isFullStamp(sel)) {
66
+ const pool = allRecords && allRecords.length ? allRecords : records;
67
+ const exact = pool.find((r) => String(r.stamp) === sel);
68
+ if (exact)
69
+ return exact;
70
+ }
51
71
  if (/^\d+$/.test(sel)) {
52
72
  const n = Number(sel);
53
73
  // A small number is an index (1 = newest).
@@ -60,7 +80,10 @@ export function pickRecord(records, selector) {
60
80
  if (sel.length < 2)
61
81
  return null;
62
82
  }
63
- return records.find((r) => String(r.stamp).startsWith(sel)) || null;
83
+ const pool = allRecords && allRecords.length ? allRecords : records;
84
+ return (records.find((r) => String(r.stamp).startsWith(sel)) ||
85
+ pool.find((r) => String(r.stamp).startsWith(sel)) ||
86
+ null);
64
87
  }
65
88
  // ---------- tar / fs plumbing ----------
66
89
  function runTar(args, cwd, timeout = 60_000) {
package/dist/glm-chats.js CHANGED
@@ -176,7 +176,19 @@ export function parseGlmHistory(json) {
176
176
  };
177
177
  const sorted = arr
178
178
  .filter((x) => !!x && typeof x === 'object')
179
- .sort((a, b) => stampOf(a) - stampOf(b));
179
+ // Order by INSERTION INDEX first and by stamp only when BOTH messages
180
+ // carry one. `stampOf` answers 0 for a missing stamp, so a half-stamped
181
+ // payload pushed every undated turn BEFORE all dated ones and the dialogue
182
+ // came back as "the model answered before it was asked".
183
+ .map((m, i) => ({ m, i, t: stampOf(m) }))
184
+ .sort((a, b) => {
185
+ const da = a.t !== 0;
186
+ const db = b.t !== 0;
187
+ if (da && db && a.t !== b.t)
188
+ return a.t - b.t;
189
+ return a.i - b.i;
190
+ })
191
+ .map((x) => x.m);
180
192
  const out = [];
181
193
  for (const m of sorted) {
182
194
  // The role comes from `role` (confirmed by the live fixture). Only the
@@ -201,7 +213,11 @@ export function parseGlmHistory(json) {
201
213
  else if (Array.isArray(m.fragments)) {
202
214
  const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
203
215
  for (const fr of m.fragments) {
204
- if (fr && fr.type === want && typeof fr.content === 'string') {
216
+ // Case-insensitive, like the DeepSeek parser: a payload spelling the
217
+ // fragment type `response`/`request` used to contribute nothing.
218
+ if (fr &&
219
+ String(fr.type ?? '').toUpperCase() === want &&
220
+ typeof fr.content === 'string') {
205
221
  text += fr.content;
206
222
  }
207
223
  }
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import { handleConfigCommand as runConfigCommand } from './config-command.js';
28
28
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
29
29
  import { Transcript } from './transcript.js';
30
30
  import { UndoStore } from './undo.js';
31
- import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
31
+ import { CheckpointStore, pickRecord as pickCheckpoint, isFullStampSelector, } from './checkpoint.js';
32
32
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
33
33
  import { performCompact } from './compact.js';
34
34
  import { clampGoalCondition } from './goal.js';
@@ -3913,8 +3913,16 @@ async function main() {
3913
3913
  if (editor)
3914
3914
  editor.lock(t('msg.input_locked'));
3915
3915
  try {
3916
+ // The DISPLAY window is the newest 10, but a FULL stamp must resolve
3917
+ // against the whole index (restore() searches all of it): an older
3918
+ // checkpoint picked by its stamp used to report "/rewind failed", and a
3919
+ // stamp sharing its first digits with a newer one could resolve to
3920
+ // THAT wrong record.
3916
3921
  const list = await checkpoints.list(10);
3917
- const target = pickCheckpoint(list, selector);
3922
+ const all = isFullStampSelector(selector)
3923
+ ? await checkpoints.list(Number.MAX_SAFE_INTEGER)
3924
+ : list;
3925
+ const target = pickCheckpoint(list, selector, all);
3918
3926
  if (!target) {
3919
3927
  console.error(theme.error(t('rewind.failed', { v: selector })));
3920
3928
  continue;
@@ -208,7 +208,28 @@ export function parseQwenHistory(json) {
208
208
  // The map is UNORDERED; `timestamp` (seconds) restores the dialogue order.
209
209
  const sorted = arr
210
210
  .filter((x) => !!x && typeof x === 'object')
211
- .sort((a, b) => (Number(a.timestamp) || 0) - (Number(b.timestamp) || 0));
211
+ // A message WITHOUT a usable timestamp must not be forced to 0: that put
212
+ // it BEFORE every dated turn, so a partially stamped payload reordered the
213
+ // dialogue (the operator's question surfaced after the model's answer).
214
+ // Order by index first — the map is in insertion order — and only then by
215
+ // stamp; a missing stamp inherits its neighbour's position instead of
216
+ // jumping to the front. A stable sort keeps the original order on a tie.
217
+ .map((m, i) => ({ m, i }))
218
+ .sort((a, b) => {
219
+ const ta = Number(a.m.timestamp);
220
+ const tb = Number(b.m.timestamp);
221
+ const va = Number.isFinite(ta) ? ta : NaN;
222
+ const vb = Number.isFinite(tb) ? tb : NaN;
223
+ if (Number.isFinite(va) && Number.isFinite(vb)) {
224
+ if (va !== vb)
225
+ return va - vb;
226
+ return a.i - b.i;
227
+ }
228
+ // Only ONE side is dated: keep the insertion order (a half-stamped
229
+ // payload has no trustworthy clock to sort by).
230
+ return a.i - b.i;
231
+ })
232
+ .map((x) => x.m);
212
233
  const out = [];
213
234
  for (const m of sorted) {
214
235
  // Role detection is DEFENSIVE: the field has been `role` (string,
@@ -245,7 +266,11 @@ export function parseQwenHistory(json) {
245
266
  else if (Array.isArray(m.fragments)) {
246
267
  const want = role === 'assistant' ? 'RESPONSE' : 'REQUEST';
247
268
  for (const fr of m.fragments) {
248
- if (fr && fr.type === want && typeof fr.content === 'string') {
269
+ // Case-insensitive, like the DeepSeek parser: a payload spelling the
270
+ // fragment type `response`/`request` used to contribute nothing.
271
+ if (fr &&
272
+ String(fr.type ?? '').toUpperCase() === want &&
273
+ typeof fr.content === 'string') {
249
274
  text += fr.content;
250
275
  }
251
276
  }
package/dist/run-task.js CHANGED
@@ -573,8 +573,11 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
573
573
  // Stop hook (N32): the whole task (including drained queue messages) is
574
574
  // done. A cleanup/notification hook runs here. Best-effort.
575
575
  try {
576
- const { runLifecycleHooks } = await import('./hooks.js');
577
- const out = await runLifecycleHooks(undefined, 'Stop', workdir, {
576
+ // LOAD the config: `undefined` here made every Stop hook a no-op — the
577
+ // operator's `.zames/hooks.json` entry never ran, while the PreToolUse
578
+ // hooks from the SAME file worked (they are loaded separately).
579
+ const { runLifecycleHooks, loadHooks } = await import('./hooks.js');
580
+ const out = await runLifecycleHooks(loadHooks(workdir), 'Stop', workdir, {
578
581
  failure: failure ?? '',
579
582
  });
580
583
  if (out)
@@ -10,7 +10,7 @@
10
10
  // survives across tasks and a provider switch within a session (a new browser
11
11
  // object is created, but the operator's decision about the interval does not
12
12
  // silently reset mid-session — it is re-derived from this session's events).
13
- import { DEFAULT_THROTTLE, initialThrottleState, applySendEvent, nextWaitMs, withBlockDuration, } from './send-throttle.js';
13
+ import { DEFAULT_THROTTLE, initialThrottleState, applySendEvent, nextWaitMs, } from './send-throttle.js';
14
14
  import { appendSendEvent, } from './send-telemetry.js';
15
15
  /**
16
16
  * The runtime a provider keeps: the learned interval, the last send and the
@@ -72,7 +72,7 @@ export class SendRuntime {
72
72
  onOutcome(outcome, extra = {}) {
73
73
  if (!this.pending)
74
74
  return;
75
- let e = { ...this.pending, outcome };
75
+ const e = { ...this.pending, outcome };
76
76
  if (extra.errorText)
77
77
  e.errorText = String(extra.errorText).slice(0, 300);
78
78
  if (typeof extra.retryAfterMs === 'number') {
@@ -88,7 +88,14 @@ export class SendRuntime {
88
88
  this.state = { ...this.state, blocked: true, blockedAt: this.now() };
89
89
  }
90
90
  if (outcome === 'rate_limit') {
91
- e = withBlockDuration(e, this.state.blockedAt, this.now());
91
+ // The block START is only known AFTER the fold above (the adaptive path
92
+ // sets it there). The event itself carries NO duration: a rate-limit
93
+ // send is not the END of the block, so any value measured here is a
94
+ // lower bound that would then be averaged together with the real
95
+ // duration written by onBlockLifted — the same block counted TWICE.
96
+ if (!this.state.blockedAt) {
97
+ this.state = { ...this.state, blocked: true, blockedAt: this.now() };
98
+ }
92
99
  }
93
100
  if (this.opts.telemetry) {
94
101
  appendSendEvent(this.opts.logsDir, e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.52",
3
+ "version": "2.82.54",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {