agentus 0.11.1 → 0.11.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 CHANGED
@@ -57,7 +57,7 @@ background process comes back as a turn of its own), an **ACP session that write
57
57
  cockpit-side failure is diagnosable afterwards), and a **DeepSeek web backend** (search +
58
58
  extract on the model key) — come from the fork's integration build instead. That is a five-line install:
59
59
  [§ Which Hermes to run](docs/installation.md#which-hermes-to-run) — currently the tag
60
- `agentus/v0.22.0/merge-20261023-0540`, on Hermes `v0.22.0`.
60
+ `agentus/v0.22.0/merge-20261023-0556`, on Hermes `v0.22.0`.
61
61
 
62
62
  The first slot you start will use the Hermes home **your own agent already uses**
63
63
  (`~/.hermes`) unless the backend row names another one — see "HERMES_HOME" below.
package/README.zh-CN.md CHANGED
@@ -51,7 +51,7 @@ web bundle、配套 APK —— 然后直接退出,不启动任何东西)。
51
51
  以及 **DeepSeek 的 web 后端**(搜索 + 正文抓取,共用模型 key)—— 来自 fork 的集成构建(tag)。
52
52
  装法就五行:
53
53
  [§ 该跑哪份 Hermes](docs/installation.zh-CN.md#该跑哪份-hermes) —— 当前是 tag
54
- `agentus/v0.22.0/merge-20261023-0540`,基座 Hermes `v0.22.0`。
54
+ `agentus/v0.22.0/merge-20261023-0556`,基座 Hermes `v0.22.0`。
55
55
 
56
56
  你启动的第一个 slot 会使用**你自己的 agent 已经在用的**那个 Hermes home(`~/.hermes`),除非后端那一行
57
57
  指定了另一个 —— 见下面的「HERMES_HOME」。
@@ -60,7 +60,7 @@ ref and gets rebuilt, while a tag never moves — so pin the tag. The `agentus/`
60
60
  ours apart from upstream's own tags (`v0.22.0`, `v0.22.0+canary.*`), and the timestamp in the
61
61
  last segment sorts, so "newest" is a `sort -V`.
62
62
 
63
- **Current integration build: `agentus/v0.22.0/merge-20261023-0540`** — the newest tag on the fork,
63
+ **Current integration build: `agentus/v0.22.0/merge-20261023-0556`** — the newest tag on the fork,
64
64
  built on Hermes `v0.22.0`. This line is for orientation only, not the source of truth: the
65
65
  `TAG=$(…)` step below resolves the newest one on every run, so read it there if this sentence
66
66
  ever looks stale.
@@ -52,7 +52,7 @@ agentus/<hermes 内部版本>/merge-<YYYYMMDD-HHMM>
52
52
  tag。`agentus/` 这个前缀也把它和上游自己的 tag(`v0.22.0`、`v0.22.0+canary.*`)分开;最后一段是时间戳,
53
53
  可排序,所以「最新」就是一次 `sort -V`。
54
54
 
55
- **当前集成构建:`agentus/v0.22.0/merge-20261023-0540`** —— fork 上最新的那个 tag,基座 Hermes
55
+ **当前集成构建:`agentus/v0.22.0/merge-20261023-0556`** —— fork 上最新的那个 tag,基座 Hermes
56
56
  `v0.22.0`。这行只为一眼看个大概,不是权威:下面的 `TAG=$(…)` 每次都会自己解析出最新,觉得这行过期就
57
57
  以它为准。
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentus",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Multi-session web cockpit for ACP-speaking coding agents (Qoder CLI / Hermes). UI has no intelligence — the agent lives in spawned CLI subprocesses.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -482,7 +482,10 @@ export class SessionManager {
482
482
  this.#store.upsertVoiceMode(id, {
483
483
  inCall: true, boundary: "enter", turns: 0,
484
484
  thoughtBefore: before,
485
- thoughtTouched: prev.thoughtBefore ? prev.thoughtTouched : false,
485
+ // A new call starts untouched even if the PREVIOUS call ended with the operator's own pick
486
+ // still recorded: that pick was already honoured (by not restoring it), and inheriting the
487
+ // flag here would skip the lowering for this whole call (review round 2, A-m2).
488
+ thoughtTouched: false,
486
489
  });
487
490
  // A call is full-duplex: waiting 20 s for a first token reads as a dead line to the operator.
488
491
  // Lower the dial for the call and put the operator's own choice back on hang-up (see #restoreThought).
@@ -525,10 +528,24 @@ export class SessionManager {
525
528
  } catch {
526
529
  return false;
527
530
  }
531
+ // A new call may have started while this round-trip was in flight: it needs the snapshot, so
532
+ // leave it alone (the new call will restore it on ITS hang-up).
533
+ if (this.#store.getVoiceMode(id).inCall) return false;
534
+ // `setConfig` reports a rejected value by emitting, not by throwing: verify against what the
535
+ // agent actually advertises before declaring the level restored (review round 2, A-m1).
536
+ const now = thinkingOptionOf(s.info.configOptions);
537
+ if (now && now.current && now.current !== v.thoughtBefore) return false;
528
538
  this.#store.upsertVoiceMode(id, { thoughtBefore: null, thoughtTouched: false });
529
539
  return true;
530
540
  }
531
541
 
542
+ /** Run a restore on the call-state chain instead of behind its back. */
543
+ #queueRestore(id: string): void {
544
+ this.#callSync = this.#callSync
545
+ .then(() => this.#restoreThought(id))
546
+ .then(() => undefined, () => undefined);
547
+ }
548
+
532
549
  /** Attach this prompt's voice block, and settle what it settles. Synchronous on purpose: it runs
533
550
  * inside the send path, and a slow store write must not delay the operator's words.
534
551
  *
@@ -547,19 +564,37 @@ export class SessionManager {
547
564
  if (v.inCall && live !== s.info.id) {
548
565
  const owed = v.turns > 0;
549
566
  v = this.#store.upsertVoiceMode(s.info.id, { inCall: false, boundary: owed ? "exit" : null, ...(owed ? {} : { turns: 0 }) });
550
- void this.#restoreThought(s.info.id);
567
+ // On the SAME chain as every other transition: a fire-and-forget restore could finish after a
568
+ // new call had already pressed the level down, clear the snapshot, and strand the session on
569
+ // the call level (review round 2, A-M2).
570
+ this.#queueRestore(s.info.id);
551
571
  } else if (!v.inCall && live === s.info.id) {
552
572
  v = this.#store.upsertVoiceMode(s.info.id, { inCall: true, boundary: v.boundary ?? "enter", turns: 0 });
553
573
  }
554
574
  const block = voiceModeBlock(s.info.id, { inCall: v.inCall, boundary: v.boundary, turns: v.turns });
555
575
  if (block) blocks.push(block);
556
- const hadBoundary = Boolean(v.boundary);
576
+ const seenBoundary = v.boundary;
557
577
  const counted = v.inCall;
558
- if (!hadBoundary && !counted) return () => { /* steady state, nothing to settle */ };
578
+ if (!seenBoundary && !counted) return () => { /* steady state, nothing to settle */ };
579
+ // The turn is counted NOW, not when the prompt returns: hanging up while the agent is still
580
+ // answering is the ordinary way a call ends, and `#endCall` must already see that something was
581
+ // said or it owes no exit boundary (review round 2, A-M1). A prompt that never reaches the agent
582
+ // rolls the count back.
583
+ if (counted) this.#store.upsertVoiceMode(s.info.id, { turns: v.turns + 1 });
559
584
  return (delivered: boolean) => {
560
- if (!delivered) return;
561
- if (hadBoundary) this.#store.upsertVoiceMode(s.info.id, { boundary: null });
562
- if (counted) this.#store.upsertVoiceMode(s.info.id, { turns: this.#store.getVoiceMode(s.info.id).turns + 1 });
585
+ if (!delivered) {
586
+ if (counted) {
587
+ const now = this.#store.getVoiceMode(s.info.id);
588
+ if (now.inCall && now.turns > 0) this.#store.upsertVoiceMode(s.info.id, { turns: now.turns - 1 });
589
+ }
590
+ return;
591
+ }
592
+ // Clear only the boundary this turn actually carried: a hang-up during the turn may have
593
+ // replaced it with an exit boundary the operator still needs to hear (review r2 A-M1).
594
+ if (seenBoundary) {
595
+ const cur = this.#store.getVoiceMode(s.info.id);
596
+ if (cur.boundary === seenBoundary) this.#store.upsertVoiceMode(s.info.id, { boundary: null });
597
+ }
563
598
  };
564
599
  }
565
600