agentus 0.11.2 → 0.11.3
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/installation.md +1 -1
- package/docs/installation.zh-CN.md +1 -1
- package/package.json +1 -1
- package/packages/server/src/acp/session-manager.ts +35 -19
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-
|
|
60
|
+
`agentus/v0.22.0/merge-20261023-0607`, 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-
|
|
54
|
+
`agentus/v0.22.0/merge-20261023-0607`,基座 Hermes `v0.22.0`。
|
|
55
55
|
|
|
56
56
|
你启动的第一个 slot 会使用**你自己的 agent 已经在用的**那个 Hermes home(`~/.hermes`),除非后端那一行
|
|
57
57
|
指定了另一个 —— 见下面的「HERMES_HOME」。
|
package/docs/installation.md
CHANGED
|
@@ -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-
|
|
63
|
+
**Current integration build: `agentus/v0.22.0/merge-20261023-0607`** — 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-
|
|
55
|
+
**当前集成构建:`agentus/v0.22.0/merge-20261023-0607`** —— 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.
|
|
3
|
+
"version": "0.11.3",
|
|
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": {
|
|
@@ -320,6 +320,8 @@ export class SessionManager {
|
|
|
320
320
|
#callLive: () => string | null = () => null;
|
|
321
321
|
/** Serialises call-state transitions (see syncCallState). */
|
|
322
322
|
#callSync: Promise<void> = Promise.resolve();
|
|
323
|
+
/** Per-session call generation (see #callGen). */
|
|
324
|
+
#callGens = new Map<string, number>();
|
|
323
325
|
setCallLiveSource(read: () => string | null): void {
|
|
324
326
|
this.#callLive = read;
|
|
325
327
|
}
|
|
@@ -470,6 +472,7 @@ export class SessionManager {
|
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
async #beginCall(id: string): Promise<void> {
|
|
475
|
+
this.#bumpCallGen(id);
|
|
473
476
|
const prev = this.#store.getVoiceMode(id);
|
|
474
477
|
if (prev.inCall) return; // idempotent: a second presence beat for the same call changes nothing
|
|
475
478
|
if (!this.#store.getSession(id)) return; // a call with a session this store has never seen
|
|
@@ -498,6 +501,7 @@ export class SessionManager {
|
|
|
498
501
|
}
|
|
499
502
|
|
|
500
503
|
async #endCall(id: string): Promise<void> {
|
|
504
|
+
this.#bumpCallGen(id);
|
|
501
505
|
const v = this.#store.getVoiceMode(id);
|
|
502
506
|
// Nothing was actually said (no prompt ever rode this call): owe nothing. An exit block here
|
|
503
507
|
// would tell the agent "the conversation above was spoken", which is false — and it is cheap to
|
|
@@ -532,13 +536,27 @@ export class SessionManager {
|
|
|
532
536
|
// leave it alone (the new call will restore it on ITS hang-up).
|
|
533
537
|
if (this.#store.getVoiceMode(id).inCall) return false;
|
|
534
538
|
// `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).
|
|
539
|
+
// agent actually advertises before declaring the level restored (review round 2, A-m1). The
|
|
540
|
+
// knob must still be the SAME one (a model/mode switch changes the list) and an answer without
|
|
541
|
+
// `currentValue` proves nothing — in both cases keep the snapshot rather than silently staying
|
|
542
|
+
// on the call level (review r3 A-③).
|
|
536
543
|
const now = thinkingOptionOf(s.info.configOptions);
|
|
537
|
-
if (now
|
|
544
|
+
if (!now || now.id !== choice.id || !now.current) return false;
|
|
545
|
+
if (now.current !== v.thoughtBefore) return false;
|
|
538
546
|
this.#store.upsertVoiceMode(id, { thoughtBefore: null, thoughtTouched: false });
|
|
539
547
|
return true;
|
|
540
548
|
}
|
|
541
549
|
|
|
550
|
+
/** Monotonic per-session call generation: every begin/end is a new one. Used to make sure a
|
|
551
|
+
* settle() only touches the call it was created in (review r3 A-②). */
|
|
552
|
+
#callGen(id: string): number {
|
|
553
|
+
return this.#callGens.get(id) ?? 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#bumpCallGen(id: string): void {
|
|
557
|
+
this.#callGens.set(id, (this.#callGens.get(id) ?? 0) + 1);
|
|
558
|
+
}
|
|
559
|
+
|
|
542
560
|
/** Run a restore on the call-state chain instead of behind its back. */
|
|
543
561
|
#queueRestore(id: string): void {
|
|
544
562
|
this.#callSync = this.#callSync
|
|
@@ -549,9 +567,9 @@ export class SessionManager {
|
|
|
549
567
|
/** Attach this prompt's voice block, and settle what it settles. Synchronous on purpose: it runs
|
|
550
568
|
* inside the send path, and a slow store write must not delay the operator's words.
|
|
551
569
|
*
|
|
552
|
-
* Returns a settle() to call once the turn
|
|
553
|
-
*
|
|
554
|
-
#applyVoiceMode(s: LiveSession, blocks: unknown[], opts: { operator: boolean }): (
|
|
570
|
+
* Returns a settle() to call once the turn has been dispatched: before that point the boundary is
|
|
571
|
+
* still owed, so a failed send does not burn it (review m1). */
|
|
572
|
+
#applyVoiceMode(s: LiveSession, blocks: unknown[], opts: { operator: boolean }): () => void {
|
|
555
573
|
// A background-completion wake is not a spoken turn: letting it consume the boundary injected
|
|
556
574
|
// "the call is over" into a synthetic turn nobody reads, so the operator's next real message
|
|
557
575
|
// never learned it, and each wake inflated the turn count (review M1).
|
|
@@ -581,17 +599,16 @@ export class SessionManager {
|
|
|
581
599
|
// said or it owes no exit boundary (review round 2, A-M1). A prompt that never reaches the agent
|
|
582
600
|
// rolls the count back.
|
|
583
601
|
if (counted) this.#store.upsertVoiceMode(s.info.id, { turns: v.turns + 1 });
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (seenBoundary) {
|
|
602
|
+
// No rollback on a thrown prompt, deliberately: "the frame went out and the answer never came
|
|
603
|
+
// back" cannot be told apart from "the frame never went out", and the two mistakes are not
|
|
604
|
+
// symmetric — over-counting costs one extra sentence in the exit block, under-counting means the
|
|
605
|
+
// exit boundary is never owed at all and the agent keeps answering text in spoken-word style
|
|
606
|
+
// (review r3 A-①).
|
|
607
|
+
// The generation guards against a same-shaped boundary that arrived AFTER this turn started:
|
|
608
|
+
// consume the enter boundary, hang up and dial again → the new call's enter must survive.
|
|
609
|
+
const gen = this.#callGen(s.info.id);
|
|
610
|
+
return () => {
|
|
611
|
+
if (seenBoundary && this.#callGen(s.info.id) === gen) {
|
|
595
612
|
const cur = this.#store.getVoiceMode(s.info.id);
|
|
596
613
|
if (cur.boundary === seenBoundary) this.#store.upsertVoiceMode(s.info.id, { boundary: null });
|
|
597
614
|
}
|
|
@@ -1367,9 +1384,8 @@ export class SessionManager {
|
|
|
1367
1384
|
prompt: blocks as any,
|
|
1368
1385
|
});
|
|
1369
1386
|
const terminalState = res?.stopReason === "cancelled" ? "interrupted" : "ended";
|
|
1370
|
-
// The agent has the block (a cancelled turn still received it): NOW the boundary is spent
|
|
1371
|
-
|
|
1372
|
-
settleVoice(true);
|
|
1387
|
+
// The agent has the block (a cancelled turn still received it): NOW the boundary is spent.
|
|
1388
|
+
settleVoice();
|
|
1373
1389
|
this.#finalizePlan(sessionId, terminalState);
|
|
1374
1390
|
this.#emit({ t: "turn-end", sessionId, stopReason: res?.stopReason, durationMs: Date.now() - turnStartedAt });
|
|
1375
1391
|
} catch (err) {
|