@ganglion/xacpx 0.19.1 → 0.19.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.
@@ -0,0 +1,180 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
37
+ var __export = (target, all) => {
38
+ for (var name in all)
39
+ __defProp(target, name, {
40
+ get: all[name],
41
+ enumerable: true,
42
+ configurable: true,
43
+ set: __exportSetter.bind(all, name)
44
+ });
45
+ };
46
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
+ var __promiseAll = (args) => Promise.all(args);
48
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
+
50
+ // src/adapters/hermes-shim.ts
51
+ import { fileURLToPath } from "node:url";
52
+ function isDefaultHermesCommand(command) {
53
+ return command.trim().replaceAll(/\s+/g, " ") === DEFAULT_HERMES_COMMAND;
54
+ }
55
+ function isHermesShimCommand(command) {
56
+ return command.includes("/hermes-acp-shim.") || command.includes("\\hermes-acp-shim.");
57
+ }
58
+ function resolveHermesAcpShimEntry(moduleUrl = import.meta.url) {
59
+ if (moduleUrl.endsWith(".ts")) {
60
+ return fileURLToPath(new URL("./hermes-acp-shim.ts", moduleUrl));
61
+ }
62
+ const idx = moduleUrl.lastIndexOf(DIST_MARKER);
63
+ if (idx !== -1) {
64
+ return fileURLToPath(new URL(`${moduleUrl.slice(0, idx + DIST_MARKER.length)}adapters/hermes-acp-shim.js`));
65
+ }
66
+ return fileURLToPath(new URL("./hermes-acp-shim.js", moduleUrl));
67
+ }
68
+ function quoteAgentCommandToken(token) {
69
+ return `"${token.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
70
+ }
71
+ function hermesAcpShimCommand(execPath = process.execPath, shimEntry = resolveHermesAcpShimEntry()) {
72
+ return [
73
+ quoteAgentCommandToken(execPath),
74
+ quoteAgentCommandToken(shimEntry),
75
+ "hermes",
76
+ "acp"
77
+ ].join(" ");
78
+ }
79
+ function stripResumeCapability(line) {
80
+ let message;
81
+ try {
82
+ message = JSON.parse(line);
83
+ } catch {
84
+ return null;
85
+ }
86
+ if (!message || typeof message !== "object")
87
+ return null;
88
+ const result = message.result;
89
+ if (!result || typeof result !== "object")
90
+ return null;
91
+ const capabilities = result.agentCapabilities;
92
+ if (!capabilities || typeof capabilities !== "object")
93
+ return null;
94
+ const sessionCapabilities = capabilities.sessionCapabilities;
95
+ if (!sessionCapabilities || typeof sessionCapabilities !== "object")
96
+ return null;
97
+ if (!Object.hasOwn(sessionCapabilities, "resume"))
98
+ return null;
99
+ delete sessionCapabilities.resume;
100
+ return JSON.stringify(message);
101
+ }
102
+ function isInitializeResponse(line) {
103
+ let message;
104
+ try {
105
+ message = JSON.parse(line);
106
+ } catch {
107
+ return false;
108
+ }
109
+ if (!message || typeof message !== "object")
110
+ return false;
111
+ const result = message.result;
112
+ if (!result || typeof result !== "object")
113
+ return false;
114
+ const capabilities = result.agentCapabilities;
115
+ return Boolean(capabilities) && typeof capabilities === "object";
116
+ }
117
+ var DEFAULT_HERMES_COMMAND = "hermes acp", DIST_MARKER = "/dist/";
118
+ var init_hermes_shim = () => {};
119
+
120
+ // src/adapters/hermes-acp-shim.ts
121
+ init_hermes_shim();
122
+ import { spawn } from "node:child_process";
123
+ import { constants as osConstants } from "node:os";
124
+ var argv = process.argv.slice(2);
125
+ var command = argv.length > 0 ? argv : ["hermes", "acp"];
126
+ var child = spawn(command[0], command.slice(1), {
127
+ stdio: ["pipe", "pipe", "inherit"],
128
+ shell: process.platform === "win32"
129
+ });
130
+ child.on("error", (error) => {
131
+ process.stderr.write(`[hermes-acp-shim] failed to spawn "${command.join(" ")}": ${error.message}
132
+ `);
133
+ process.exit(1);
134
+ });
135
+ child.on("exit", (code, signal) => {
136
+ process.exitCode = signal ? 128 + (osConstants.signals[signal] ?? 0) : code ?? 0;
137
+ });
138
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
139
+ process.on(signal, () => {
140
+ child.kill(signal);
141
+ });
142
+ }
143
+ process.stdin.pipe(child.stdin);
144
+ var patched = false;
145
+ var pending = Buffer.alloc(0);
146
+ var NEWLINE = 10;
147
+ var interceptStdout = (chunk) => {
148
+ pending = pending.length > 0 ? Buffer.concat([pending, chunk]) : chunk;
149
+ let newlineIndex;
150
+ while (!patched && (newlineIndex = pending.indexOf(NEWLINE)) !== -1) {
151
+ const line = pending.subarray(0, newlineIndex + 1);
152
+ pending = pending.subarray(newlineIndex + 1);
153
+ const text = line.toString("utf8");
154
+ const replaced = stripResumeCapability(text);
155
+ if (replaced !== null) {
156
+ patched = true;
157
+ process.stdout.write(`${replaced}
158
+ `);
159
+ } else {
160
+ process.stdout.write(line);
161
+ if (isInitializeResponse(text))
162
+ patched = true;
163
+ }
164
+ }
165
+ if (patched) {
166
+ if (pending.length > 0) {
167
+ process.stdout.write(pending);
168
+ pending = Buffer.alloc(0);
169
+ }
170
+ child.stdout.off("data", interceptStdout);
171
+ child.stdout.pipe(process.stdout, { end: false });
172
+ }
173
+ };
174
+ child.stdout.on("data", interceptStdout);
175
+ child.stdout.on("end", () => {
176
+ if (pending.length > 0) {
177
+ process.stdout.write(pending);
178
+ pending = Buffer.alloc(0);
179
+ }
180
+ });
@@ -0,0 +1,37 @@
1
+ /** The command the 0.19.2 template baked into configs; treated as "no explicit
2
+ * command" so those configs migrate onto the shim without hand-editing. */
3
+ export declare function isDefaultHermesCommand(command: string): boolean;
4
+ /** A recorded shim command is derived, machine-specific state — the current
5
+ * install's shim (whose dist path may differ after an upgrade) must replace it.
6
+ * The leading separator keeps user wrappers like `my-hermes-acp-shim.sh` custom. */
7
+ export declare function isHermesShimCommand(command: string): boolean;
8
+ /**
9
+ * Path to the shim entry script. Unbundled dev runs (module URL still ends in .ts)
10
+ * resolve the sibling .ts source, which the dev runtime (bun) executes directly —
11
+ * checked FIRST so a checkout path that happens to contain `/dist/` cannot
12
+ * misresolve. Bundled builds (cli.js AND bridge/bridge-main.js both sit under
13
+ * dist/) anchor on the last `/dist/` segment.
14
+ */
15
+ export declare function resolveHermesAcpShimEntry(moduleUrl?: string): string;
16
+ /** Quote one token for acpx's `--agent` parser (double quotes; `\` and `"` escaped —
17
+ * its splitCommandLine treats backslash as an escape inside double quotes). */
18
+ export declare function quoteAgentCommandToken(token: string): string;
19
+ /** Runtime-only agent command for the hermes driver: never persisted to config.
20
+ *
21
+ * Tradeoff: the string embeds `process.execPath` and this install's dist path,
22
+ * and acpx keys its backend session records by EXACT agentCommand equality — so a
23
+ * node upgrade (nvm paths embed the version) or an install relocation re-keys
24
+ * hermes session identity: acpx starts a fresh backend record and the old queue
25
+ * owner ages out via TTL. Accepted because the alternative (a stable bin shim)
26
+ * isn't worth the packaging surface for a workaround slated for removal. */
27
+ export declare function hermesAcpShimCommand(execPath?: string, shimEntry?: string): string;
28
+ /**
29
+ * If `line` is the initialize response advertising `sessionCapabilities.resume`,
30
+ * return the same frame re-serialized without it; otherwise null (caller forwards
31
+ * the original bytes untouched).
32
+ */
33
+ export declare function stripResumeCapability(line: string): string | null;
34
+ /** Is `line` an initialize response (the only frame carrying agentCapabilities)?
35
+ * Lets the shim latch to raw passthrough even when hermes stops advertising
36
+ * `resume` after the upstream fix, instead of parsing every line forever. */
37
+ export declare function isInitializeResponse(line: string): boolean;
package/dist/cli.js CHANGED
@@ -4868,6 +4868,76 @@ var init_local_agent_bin = __esm(() => {
4868
4868
  };
4869
4869
  });
4870
4870
 
4871
+ // src/adapters/hermes-shim.ts
4872
+ import { fileURLToPath } from "node:url";
4873
+ function isDefaultHermesCommand(command) {
4874
+ return command.trim().replaceAll(/\s+/g, " ") === DEFAULT_HERMES_COMMAND;
4875
+ }
4876
+ function isHermesShimCommand(command) {
4877
+ return command.includes("/hermes-acp-shim.") || command.includes("\\hermes-acp-shim.");
4878
+ }
4879
+ function resolveHermesAcpShimEntry(moduleUrl = import.meta.url) {
4880
+ if (moduleUrl.endsWith(".ts")) {
4881
+ return fileURLToPath(new URL("./hermes-acp-shim.ts", moduleUrl));
4882
+ }
4883
+ const idx = moduleUrl.lastIndexOf(DIST_MARKER);
4884
+ if (idx !== -1) {
4885
+ return fileURLToPath(new URL(`${moduleUrl.slice(0, idx + DIST_MARKER.length)}adapters/hermes-acp-shim.js`));
4886
+ }
4887
+ return fileURLToPath(new URL("./hermes-acp-shim.js", moduleUrl));
4888
+ }
4889
+ function quoteAgentCommandToken(token) {
4890
+ return `"${token.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
4891
+ }
4892
+ function hermesAcpShimCommand(execPath = process.execPath, shimEntry = resolveHermesAcpShimEntry()) {
4893
+ return [
4894
+ quoteAgentCommandToken(execPath),
4895
+ quoteAgentCommandToken(shimEntry),
4896
+ "hermes",
4897
+ "acp"
4898
+ ].join(" ");
4899
+ }
4900
+ function stripResumeCapability(line) {
4901
+ let message;
4902
+ try {
4903
+ message = JSON.parse(line);
4904
+ } catch {
4905
+ return null;
4906
+ }
4907
+ if (!message || typeof message !== "object")
4908
+ return null;
4909
+ const result = message.result;
4910
+ if (!result || typeof result !== "object")
4911
+ return null;
4912
+ const capabilities = result.agentCapabilities;
4913
+ if (!capabilities || typeof capabilities !== "object")
4914
+ return null;
4915
+ const sessionCapabilities = capabilities.sessionCapabilities;
4916
+ if (!sessionCapabilities || typeof sessionCapabilities !== "object")
4917
+ return null;
4918
+ if (!Object.hasOwn(sessionCapabilities, "resume"))
4919
+ return null;
4920
+ delete sessionCapabilities.resume;
4921
+ return JSON.stringify(message);
4922
+ }
4923
+ function isInitializeResponse(line) {
4924
+ let message;
4925
+ try {
4926
+ message = JSON.parse(line);
4927
+ } catch {
4928
+ return false;
4929
+ }
4930
+ if (!message || typeof message !== "object")
4931
+ return false;
4932
+ const result = message.result;
4933
+ if (!result || typeof result !== "object")
4934
+ return false;
4935
+ const capabilities = result.agentCapabilities;
4936
+ return Boolean(capabilities) && typeof capabilities === "object";
4937
+ }
4938
+ var DEFAULT_HERMES_COMMAND = "hermes acp", DIST_MARKER = "/dist/";
4939
+ var init_hermes_shim = () => {};
4940
+
4871
4941
  // src/config/resolve-agent-command.ts
4872
4942
  function resolveAgentCommand(driver, command) {
4873
4943
  if (!command) {
@@ -4876,6 +4946,9 @@ function resolveAgentCommand(driver, command) {
4876
4946
  if (driver === "codex" && isLegacyCodexCommand(command)) {
4877
4947
  return;
4878
4948
  }
4949
+ if (driver === "hermes" && isDefaultHermesCommand(command)) {
4950
+ return;
4951
+ }
4879
4952
  return command;
4880
4953
  }
4881
4954
  function resolveRuntimeAgentCommand(driver, command, preferLocal = true, adapterVersions, adapterRegistry) {
@@ -4886,6 +4959,8 @@ function resolveRuntimeAgentCommand(driver, command, preferLocal = true, adapter
4886
4959
  const managed = resolveManagedAdapterCommand(driver, adapterVersions, adapterRegistry);
4887
4960
  if (managed)
4888
4961
  return managed;
4962
+ if (driver === "hermes")
4963
+ return hermesAcpShimCommand();
4889
4964
  return preferLocal ? resolveLocalAgentCommand(driver) : undefined;
4890
4965
  }
4891
4966
  function resolveConfiguredAgentCommand(agent3, transport) {
@@ -4898,6 +4973,7 @@ function isLegacyCodexCommand(command) {
4898
4973
  var init_resolve_agent_command = __esm(() => {
4899
4974
  init_local_agent_bin();
4900
4975
  init_adapter_catalog();
4976
+ init_hermes_shim();
4901
4977
  });
4902
4978
 
4903
4979
  // src/config/load-config.ts
@@ -5762,6 +5838,9 @@ var init_agent_templates = __esm(() => {
5762
5838
  "grok-build": {
5763
5839
  driver: "grok-build"
5764
5840
  },
5841
+ hermes: {
5842
+ driver: "hermes"
5843
+ },
5765
5844
  iflow: {
5766
5845
  driver: "iflow"
5767
5846
  },
@@ -12751,11 +12830,11 @@ var require_dist = __commonJS((exports, module) => {
12751
12830
  // src/version.ts
12752
12831
  import fs from "node:fs";
12753
12832
  import path2 from "node:path";
12754
- import { fileURLToPath } from "node:url";
12833
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
12755
12834
  function readVersion(moduleUrl = import.meta.url) {
12756
12835
  let dir;
12757
12836
  try {
12758
- dir = path2.dirname(fileURLToPath(moduleUrl));
12837
+ dir = path2.dirname(fileURLToPath2(moduleUrl));
12759
12838
  } catch {
12760
12839
  return "unknown";
12761
12840
  }
@@ -13828,10 +13907,10 @@ import { readFileSync as readFileSync3 } from "node:fs";
13828
13907
  import { copyFile, mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
13829
13908
  import { homedir as homedir4 } from "node:os";
13830
13909
  import { dirname as dirname4, join as join8 } from "node:path";
13831
- import { fileURLToPath as fileURLToPath2 } from "node:url";
13910
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
13832
13911
  function resolveCoreRoot() {
13833
13912
  try {
13834
- let dir = dirname4(fileURLToPath2(import.meta.url));
13913
+ let dir = dirname4(fileURLToPath3(import.meta.url));
13835
13914
  for (let depth = 0;depth < 12; depth++) {
13836
13915
  try {
13837
13916
  const pkg = JSON.parse(readFileSync3(join8(dir, "package.json"), "utf-8"));
@@ -31347,7 +31426,8 @@ class SessionService {
31347
31426
  const effectiveReplyMode = channelId === "relay" ? "stream" : undefined;
31348
31427
  const currentAgentCommand = resolveConfiguredAgentCommand(agentConfig, this.config.transport);
31349
31428
  const configuredAgentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
31350
- const recordedAgentCommand = agentConfig.driver === "codex" && session3.transport_agent_command && isLegacyCodexCommand(session3.transport_agent_command) ? undefined : session3.transport_agent_command;
31429
+ const recordedIsDerived = Boolean(session3.transport_agent_command) && (agentConfig.driver === "codex" && isLegacyCodexCommand(session3.transport_agent_command) || agentConfig.driver === "hermes" && (isDefaultHermesCommand(session3.transport_agent_command) || isHermesShimCommand(session3.transport_agent_command)));
31430
+ const recordedAgentCommand = recordedIsDerived ? undefined : session3.transport_agent_command;
31351
31431
  return {
31352
31432
  alias: session3.alias,
31353
31433
  agent: session3.agent,
@@ -31515,6 +31595,7 @@ class SessionService {
31515
31595
  var init_session_service = __esm(() => {
31516
31596
  init_resolve_agent_command();
31517
31597
  init_adapter_catalog();
31598
+ init_hermes_shim();
31518
31599
  init_i18n();
31519
31600
  init_channel_scope();
31520
31601
  });
@@ -32228,7 +32309,7 @@ var init_command_timeouts = __esm(() => {
32228
32309
 
32229
32310
  // src/transport/acpx-bridge/acpx-bridge-client.ts
32230
32311
  import { spawn as spawn10 } from "node:child_process";
32231
- import { fileURLToPath as fileURLToPath4 } from "node:url";
32312
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
32232
32313
  import { createInterface } from "node:readline";
32233
32314
  function bridgeRequestTimeoutMs(method, sessionInitTimeoutMs = DEFAULT_SESSION_INIT_TIMEOUT_MS) {
32234
32315
  switch (method) {
@@ -32436,7 +32517,7 @@ function buildBridgeSpawnSpec(options) {
32436
32517
  };
32437
32518
  }
32438
32519
  async function spawnAcpxBridgeClient(options = {}) {
32439
- const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath4(new URL("../../bridge/bridge-main.ts", import.meta.url));
32520
+ const bridgeEntryPath = options.bridgeEntryPath ?? fileURLToPath5(new URL("../../bridge/bridge-main.ts", import.meta.url));
32440
32521
  const spawnSpec = buildBridgeSpawnSpec({
32441
32522
  execPath: process.execPath,
32442
32523
  bridgeEntryPath
@@ -37189,7 +37270,7 @@ function buildControlMetadata(senderId, isOwner) {
37189
37270
  ...isOwner === undefined ? {} : { isOwner }
37190
37271
  };
37191
37272
  }
37192
- var CANCEL_DRAIN_TIMEOUT_MS = 5000, QUEUE_PREVIEW_MAX = 120, TURN_IDLE_TIMEOUT_REASON;
37273
+ var CANCEL_DRAIN_TIMEOUT_MS = 5000, QUEUE_PREVIEW_MAX = 120, QUEUE_MAX_DEPTH = 20, TURN_IDLE_TIMEOUT_REASON;
37193
37274
  var init_turn_support = __esm(() => {
37194
37275
  TURN_IDLE_TIMEOUT_REASON = Symbol("turn-idle-timeout");
37195
37276
  });
@@ -37388,14 +37469,17 @@ class TurnQueue {
37388
37469
  deps;
37389
37470
  setTimer;
37390
37471
  clearTimer;
37472
+ cancelDrainTimeoutMs;
37391
37473
  constructor(deps) {
37392
37474
  this.deps = deps;
37393
37475
  this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
37394
37476
  this.clearTimer = deps.clearTimer ?? ((id) => clearTimeout(id));
37477
+ this.cancelDrainTimeoutMs = deps.cancelDrainTimeoutMs ?? CANCEL_DRAIN_TIMEOUT_MS;
37395
37478
  }
37396
37479
  inFlight = new Map;
37397
37480
  queues = new Map;
37398
37481
  draining = new Set;
37482
+ removing = new Set;
37399
37483
  queueLength(chatKey, sessionAlias) {
37400
37484
  return this.queues.get(turnKey(chatKey, sessionAlias))?.length ?? 0;
37401
37485
  }
@@ -37416,9 +37500,13 @@ class TurnQueue {
37416
37500
  const key = turnKey(params.chatKey, params.sessionAlias);
37417
37501
  if (!params.drained) {
37418
37502
  const existing = this.inFlight.get(key);
37419
- const busy = this.draining.has(key) || existing !== undefined && !existing.controller.signal.aborted;
37503
+ const busy = this.removing.has(key) || this.draining.has(key) || existing !== undefined && !existing.controller.signal.aborted;
37420
37504
  if (busy) {
37421
37505
  if (params.queueable) {
37506
+ const q = this.queues.get(key) ?? [];
37507
+ if (q.length >= QUEUE_MAX_DEPTH) {
37508
+ return { ok: false, errorMessage: "queue-full" };
37509
+ }
37422
37510
  const id = randomUUID3();
37423
37511
  const item = {
37424
37512
  id,
@@ -37429,7 +37517,6 @@ class TurnQueue {
37429
37517
  ...params.accountId !== undefined ? { accountId: params.accountId } : {},
37430
37518
  ...params.media !== undefined ? { media: params.media } : {}
37431
37519
  };
37432
- const q = this.queues.get(key) ?? [];
37433
37520
  q.push(item);
37434
37521
  this.queues.set(key, q);
37435
37522
  this.emitQueueUpdated(params.chatKey, params.sessionAlias);
@@ -37550,6 +37637,28 @@ class TurnQueue {
37550
37637
  entry.controller.abort();
37551
37638
  return true;
37552
37639
  }
37640
+ async clearSession(chatKey, sessionAlias) {
37641
+ const key = turnKey(chatKey, sessionAlias);
37642
+ if (this.queues.delete(key)) {
37643
+ this.emitQueueUpdated(chatKey, sessionAlias);
37644
+ }
37645
+ const entry = this.inFlight.get(key);
37646
+ if (entry) {
37647
+ entry.controller.abort();
37648
+ await raceWithTimeout(entry.settled, this.cancelDrainTimeoutMs);
37649
+ if (this.queues.delete(key)) {
37650
+ this.emitQueueUpdated(chatKey, sessionAlias);
37651
+ }
37652
+ }
37653
+ const cleared = !this.inFlight.has(key) && !this.draining.has(key);
37654
+ if (cleared) {
37655
+ this.removing.add(key);
37656
+ }
37657
+ return { cleared };
37658
+ }
37659
+ finishClear(chatKey, sessionAlias) {
37660
+ this.removing.delete(turnKey(chatKey, sessionAlias));
37661
+ }
37553
37662
  cancelQueuedItem(chatKey, sessionAlias, itemId) {
37554
37663
  const key = turnKey(chatKey, sessionAlias);
37555
37664
  const q = this.queues.get(key);
@@ -37591,6 +37700,7 @@ class ControlService {
37591
37700
  runTurn: (req, signal, onActivity) => this.runner.run(req, signal, onActivity),
37592
37701
  ...this.deps.turnIdleTimeoutMs ? { turnIdleTimeoutMs: this.deps.turnIdleTimeoutMs } : {},
37593
37702
  ...this.deps.onTurnIdleTimeout ? { onIdleTimeout: this.deps.onTurnIdleTimeout } : {},
37703
+ ...this.deps.cancelDrainTimeoutMs !== undefined ? { cancelDrainTimeoutMs: this.deps.cancelDrainTimeoutMs } : {},
37594
37704
  emitQueueUpdated: (chatKey, sessionAlias, items) => this.deps.events.emit({ type: "queue-updated", chatKey, sessionAlias, items }),
37595
37705
  detectSessionsChanged: async (detection) => {
37596
37706
  try {
@@ -37908,19 +38018,35 @@ class ControlService {
37908
38018
  }
37909
38019
  async removeSession(chatKey, alias) {
37910
38020
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
37911
- const result = await this.deps.removeSessionWithTransport(internalAlias);
37912
- this.deps.events.emit({ type: "sessions-changed" });
37913
- return result;
38021
+ const { cleared } = await this.turnQueue.clearSession(chatKey, alias);
38022
+ if (!cleared) {
38023
+ throw new Error(`session "${alias}" is still finishing a stopped turn; retry in a moment`);
38024
+ }
38025
+ try {
38026
+ const result = await this.deps.removeSessionWithTransport(internalAlias);
38027
+ this.deps.events.emit({ type: "sessions-changed" });
38028
+ return result;
38029
+ } finally {
38030
+ this.turnQueue.finishClear(chatKey, alias);
38031
+ }
37914
38032
  }
37915
38033
  async archiveSession(chatKey, alias) {
37916
38034
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
37917
- await this.deps.archiveSessionWithTransport(internalAlias);
37918
- const session3 = await this.deps.sessions.getSession(internalAlias).catch(() => {
37919
- return;
37920
- });
37921
- if (session3)
37922
- this.deps.sessionWarmth?.markCold(session3);
37923
- this.deps.events.emit({ type: "sessions-changed" });
38035
+ const { cleared } = await this.turnQueue.clearSession(chatKey, alias);
38036
+ if (!cleared) {
38037
+ throw new Error(`session "${alias}" is still finishing a stopped turn; retry in a moment`);
38038
+ }
38039
+ try {
38040
+ await this.deps.archiveSessionWithTransport(internalAlias);
38041
+ const session3 = await this.deps.sessions.getSession(internalAlias).catch(() => {
38042
+ return;
38043
+ });
38044
+ if (session3)
38045
+ this.deps.sessionWarmth?.markCold(session3);
38046
+ this.deps.events.emit({ type: "sessions-changed" });
38047
+ } finally {
38048
+ this.turnQueue.finishClear(chatKey, alias);
38049
+ }
37924
38050
  }
37925
38051
  async unarchiveSession(chatKey, alias) {
37926
38052
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
@@ -38551,7 +38677,7 @@ __export(exports_main, {
38551
38677
  import { randomUUID as randomUUID5 } from "node:crypto";
38552
38678
  import { homedir as homedir17 } from "node:os";
38553
38679
  import { dirname as dirname16, join as join26 } from "node:path";
38554
- import { fileURLToPath as fileURLToPath5 } from "node:url";
38680
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
38555
38681
  function startProgressHeartbeat(orchestration3, config4, logger, channel) {
38556
38682
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
38557
38683
  if (thresholdSeconds <= 0) {
@@ -39307,9 +39433,9 @@ function resolveRuntimePaths() {
39307
39433
  }
39308
39434
  function resolveBridgeEntryPath() {
39309
39435
  if (import.meta.url.includes("/dist/")) {
39310
- return fileURLToPath5(new URL("./bridge/bridge-main.js", import.meta.url));
39436
+ return fileURLToPath6(new URL("./bridge/bridge-main.js", import.meta.url));
39311
39437
  }
39312
- return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
39438
+ return fileURLToPath6(new URL("./bridge/bridge-main.ts", import.meta.url));
39313
39439
  }
39314
39440
  function resolveAppLogPath(configPath) {
39315
39441
  const rootDir = dirname16(configPath);
@@ -39564,7 +39690,7 @@ var init_config_check = __esm(async () => {
39564
39690
 
39565
39691
  // src/doctor/checks/daemon-check.ts
39566
39692
  import { readdir as readdir7, readFile as readFile17, rm as rm12 } from "node:fs/promises";
39567
- import { fileURLToPath as fileURLToPath6 } from "node:url";
39693
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
39568
39694
  import { homedir as homedir18 } from "node:os";
39569
39695
  import { join as join27 } from "node:path";
39570
39696
  async function checkDaemon(options = {}) {
@@ -39720,7 +39846,7 @@ async function defaultRemoveConsumerLock(path16) {
39720
39846
  await rm12(path16, { force: true });
39721
39847
  }
39722
39848
  function resolveCliEntryPath() {
39723
- return process.argv[1] ?? fileURLToPath6(import.meta.url);
39849
+ return process.argv[1] ?? fileURLToPath7(import.meta.url);
39724
39850
  }
39725
39851
  function formatError5(error2) {
39726
39852
  return error2 instanceof Error ? error2.message : String(error2);
@@ -41076,7 +41202,7 @@ init_core_home();
41076
41202
  import { randomUUID as randomUUID6 } from "node:crypto";
41077
41203
  import { homedir as homedir23 } from "node:os";
41078
41204
  import { dirname as dirname18, join as join30, sep as sep3 } from "node:path";
41079
- import { fileURLToPath as fileURLToPath7 } from "node:url";
41205
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
41080
41206
 
41081
41207
  // src/runtime/migrate-core-home.ts
41082
41208
  init_core_home();
@@ -55118,7 +55244,7 @@ init_plugin_home();
55118
55244
  import { spawn as spawn4 } from "node:child_process";
55119
55245
  import { readFile as readFile9 } from "node:fs/promises";
55120
55246
  import { dirname as dirname8, join as join13 } from "node:path";
55121
- import { fileURLToPath as fileURLToPath3 } from "node:url";
55247
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
55122
55248
 
55123
55249
  // src/plugins/package-manager.ts
55124
55250
  init_plugin_home();
@@ -55463,7 +55589,7 @@ async function runInherit(command, args) {
55463
55589
  }
55464
55590
  async function readPackageName() {
55465
55591
  try {
55466
- const here = dirname8(fileURLToPath3(import.meta.url));
55592
+ const here = dirname8(fileURLToPath4(import.meta.url));
55467
55593
  for (const candidate of [join13(here, "..", "package.json"), join13(here, "..", "..", "package.json")]) {
55468
55594
  try {
55469
55595
  const parsed = JSON.parse(await readFile9(candidate, "utf8"));
@@ -58122,7 +58248,7 @@ function resolveCliEntryPath2() {
58122
58248
  if (process.argv[1]) {
58123
58249
  return process.argv[1];
58124
58250
  }
58125
- return fileURLToPath7(import.meta.url);
58251
+ return fileURLToPath8(import.meta.url);
58126
58252
  }
58127
58253
  function parseDoctorArgs(args) {
58128
58254
  const options = {};
@@ -100,6 +100,7 @@ export interface ControlServiceDeps {
100
100
  gitWorktreesRoot?: string;
101
101
  turnIdleTimeoutMs?: () => number;
102
102
  onTurnIdleTimeout?: (detail: TurnIdleTimeoutDetail) => void;
103
+ cancelDrainTimeoutMs?: number;
103
104
  }
104
105
  export interface ControlPromptInput {
105
106
  chatKey: string;
@@ -14,6 +14,7 @@ export interface TurnQueueDeps {
14
14
  onIdleTimeout?: (detail: TurnIdleTimeoutDetail) => void;
15
15
  setTimer?: (fn: () => void, ms: number) => unknown;
16
16
  clearTimer?: (id: unknown) => void;
17
+ cancelDrainTimeoutMs?: number;
17
18
  }
18
19
  export interface SubmitParams {
19
20
  chatKey: string;
@@ -37,16 +38,41 @@ export declare class TurnQueue {
37
38
  private readonly deps;
38
39
  private readonly setTimer;
39
40
  private readonly clearTimer;
41
+ private readonly cancelDrainTimeoutMs;
40
42
  constructor(deps: TurnQueueDeps);
41
43
  private readonly inFlight;
42
44
  private readonly queues;
43
45
  private readonly draining;
46
+ private readonly removing;
44
47
  queueLength(chatKey: string, sessionAlias: string): number;
45
48
  isBusy(chatKey: string, sessionAlias: string): boolean;
46
49
  private emitQueueUpdated;
47
50
  submit(params: SubmitParams): Promise<SubmitResult>;
48
51
  private advanceQueue;
49
52
  cancelTurn(chatKey: string, sessionAlias: string): boolean;
53
+ /** Tear down all turn state for a session that is being removed or archived: drop every
54
+ * queued prompt, abort a running turn, and wait (bounded) for it to unwind. The queue is
55
+ * cleared BEFORE the abort so the aborting turn's finally sees it empty and releases the
56
+ * slot instead of draining a head onto the dead session. Returns `cleared: false` when the
57
+ * session still holds turn state after the bounded wait (a wedged turn that outlived the
58
+ * timeout, or a fresh prompt that slipped in during the unwind) — the caller MUST NOT
59
+ * proceed with removal/archive then, or the surviving turn's events would write history
60
+ * rows for a session that no longer exists.
61
+ *
62
+ * NOT side-effect-free even when it returns `cleared: false`: it has already aborted the
63
+ * in-flight turn and dropped every queued prompt (emitting `queue-updated([])`). The caller
64
+ * should surface a retry, not present the failure as a no-op.
65
+ *
66
+ * On `cleared: true` it arms a teardown guard (the busy gate now rejects new turns for this
67
+ * session) so a scheduled turn cannot cold-start during the caller's transport teardown. The
68
+ * caller MUST call `finishClear` once teardown settles (success or failure) to release it. */
69
+ clearSession(chatKey: string, sessionAlias: string): Promise<{
70
+ cleared: boolean;
71
+ }>;
72
+ /** Release the teardown guard armed by a successful `clearSession`. MUST be called by the
73
+ * caller once transport removal/archive settles (in a finally), whether it succeeded or
74
+ * threw — otherwise the session key stays wedged as busy forever. */
75
+ finishClear(chatKey: string, sessionAlias: string): void;
50
76
  /** Remove a pending queued prompt (by id) before it drains. No-ops (returns
51
77
  * `{ cancelled: false }`) when the queue or the id is absent/already drained — e.g. a race
52
78
  * where the item drained into a running turn just before the cancel arrived. Does NOT touch
@@ -13,6 +13,7 @@ export interface QueuedPrompt {
13
13
  }
14
14
  export declare const CANCEL_DRAIN_TIMEOUT_MS = 5000;
15
15
  export declare const QUEUE_PREVIEW_MAX = 120;
16
+ export declare const QUEUE_MAX_DEPTH = 20;
16
17
  export declare function raceWithTimeout(promise: Promise<void>, ms: number): Promise<void>;
17
18
  export declare const TURN_IDLE_TIMEOUT_REASON: unique symbol;
18
19
  /** Detail handed to the idle-timeout observability hook when the inactivity watchdog reclaims a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",
@@ -42,7 +42,7 @@
42
42
  "clean:dist": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
43
43
  "clean:channel-yuanbao": "node -e \"require('node:fs').rmSync('packages/channel-yuanbao/dist',{recursive:true,force:true})\"",
44
44
  "clean:channel-feishu": "node -e \"require('node:fs').rmSync('packages/channel-feishu/dist',{recursive:true,force:true})\"",
45
- "build": "bun run clean:dist && bun build ./src/cli.ts ./src/bridge/bridge-main.ts ./src/plugin-api.ts --outdir ./dist --target node --external node-pty && bun run build:plugin-api",
45
+ "build": "bun run clean:dist && bun build ./src/cli.ts ./src/bridge/bridge-main.ts ./src/adapters/hermes-acp-shim.ts ./src/plugin-api.ts --outdir ./dist --target node --external node-pty && bun run build:plugin-api",
46
46
  "build:plugin-api": "tsc -p tsconfig.plugin-api.json",
47
47
  "build:channel-yuanbao": "bun run build:plugin-api && bun run clean:channel-yuanbao && bun build ./packages/channel-yuanbao/src/index.ts --outdir ./packages/channel-yuanbao/dist --target node --external xacpx && tsc -p packages/channel-yuanbao/tsconfig.json",
48
48
  "build:channel-feishu": "bun run build:plugin-api && bun run clean:channel-feishu && bun build ./packages/channel-feishu/src/index.ts --outdir ./packages/channel-feishu/dist --target node --external xacpx && tsc -p packages/channel-feishu/tsconfig.json",