@ganglion/xacpx 0.19.2 → 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
@@ -5763,8 +5839,7 @@ var init_agent_templates = __esm(() => {
5763
5839
  driver: "grok-build"
5764
5840
  },
5765
5841
  hermes: {
5766
- driver: "hermes",
5767
- command: "hermes acp"
5842
+ driver: "hermes"
5768
5843
  },
5769
5844
  iflow: {
5770
5845
  driver: "iflow"
@@ -12755,11 +12830,11 @@ var require_dist = __commonJS((exports, module) => {
12755
12830
  // src/version.ts
12756
12831
  import fs from "node:fs";
12757
12832
  import path2 from "node:path";
12758
- import { fileURLToPath } from "node:url";
12833
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
12759
12834
  function readVersion(moduleUrl = import.meta.url) {
12760
12835
  let dir;
12761
12836
  try {
12762
- dir = path2.dirname(fileURLToPath(moduleUrl));
12837
+ dir = path2.dirname(fileURLToPath2(moduleUrl));
12763
12838
  } catch {
12764
12839
  return "unknown";
12765
12840
  }
@@ -13832,10 +13907,10 @@ import { readFileSync as readFileSync3 } from "node:fs";
13832
13907
  import { copyFile, mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
13833
13908
  import { homedir as homedir4 } from "node:os";
13834
13909
  import { dirname as dirname4, join as join8 } from "node:path";
13835
- import { fileURLToPath as fileURLToPath2 } from "node:url";
13910
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
13836
13911
  function resolveCoreRoot() {
13837
13912
  try {
13838
- let dir = dirname4(fileURLToPath2(import.meta.url));
13913
+ let dir = dirname4(fileURLToPath3(import.meta.url));
13839
13914
  for (let depth = 0;depth < 12; depth++) {
13840
13915
  try {
13841
13916
  const pkg = JSON.parse(readFileSync3(join8(dir, "package.json"), "utf-8"));
@@ -31351,7 +31426,8 @@ class SessionService {
31351
31426
  const effectiveReplyMode = channelId === "relay" ? "stream" : undefined;
31352
31427
  const currentAgentCommand = resolveConfiguredAgentCommand(agentConfig, this.config.transport);
31353
31428
  const configuredAgentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
31354
- 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;
31355
31431
  return {
31356
31432
  alias: session3.alias,
31357
31433
  agent: session3.agent,
@@ -31519,6 +31595,7 @@ class SessionService {
31519
31595
  var init_session_service = __esm(() => {
31520
31596
  init_resolve_agent_command();
31521
31597
  init_adapter_catalog();
31598
+ init_hermes_shim();
31522
31599
  init_i18n();
31523
31600
  init_channel_scope();
31524
31601
  });
@@ -32232,7 +32309,7 @@ var init_command_timeouts = __esm(() => {
32232
32309
 
32233
32310
  // src/transport/acpx-bridge/acpx-bridge-client.ts
32234
32311
  import { spawn as spawn10 } from "node:child_process";
32235
- import { fileURLToPath as fileURLToPath4 } from "node:url";
32312
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
32236
32313
  import { createInterface } from "node:readline";
32237
32314
  function bridgeRequestTimeoutMs(method, sessionInitTimeoutMs = DEFAULT_SESSION_INIT_TIMEOUT_MS) {
32238
32315
  switch (method) {
@@ -32440,7 +32517,7 @@ function buildBridgeSpawnSpec(options) {
32440
32517
  };
32441
32518
  }
32442
32519
  async function spawnAcpxBridgeClient(options = {}) {
32443
- 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));
32444
32521
  const spawnSpec = buildBridgeSpawnSpec({
32445
32522
  execPath: process.execPath,
32446
32523
  bridgeEntryPath
@@ -37193,7 +37270,7 @@ function buildControlMetadata(senderId, isOwner) {
37193
37270
  ...isOwner === undefined ? {} : { isOwner }
37194
37271
  };
37195
37272
  }
37196
- 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;
37197
37274
  var init_turn_support = __esm(() => {
37198
37275
  TURN_IDLE_TIMEOUT_REASON = Symbol("turn-idle-timeout");
37199
37276
  });
@@ -37392,14 +37469,17 @@ class TurnQueue {
37392
37469
  deps;
37393
37470
  setTimer;
37394
37471
  clearTimer;
37472
+ cancelDrainTimeoutMs;
37395
37473
  constructor(deps) {
37396
37474
  this.deps = deps;
37397
37475
  this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
37398
37476
  this.clearTimer = deps.clearTimer ?? ((id) => clearTimeout(id));
37477
+ this.cancelDrainTimeoutMs = deps.cancelDrainTimeoutMs ?? CANCEL_DRAIN_TIMEOUT_MS;
37399
37478
  }
37400
37479
  inFlight = new Map;
37401
37480
  queues = new Map;
37402
37481
  draining = new Set;
37482
+ removing = new Set;
37403
37483
  queueLength(chatKey, sessionAlias) {
37404
37484
  return this.queues.get(turnKey(chatKey, sessionAlias))?.length ?? 0;
37405
37485
  }
@@ -37420,9 +37500,13 @@ class TurnQueue {
37420
37500
  const key = turnKey(params.chatKey, params.sessionAlias);
37421
37501
  if (!params.drained) {
37422
37502
  const existing = this.inFlight.get(key);
37423
- 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;
37424
37504
  if (busy) {
37425
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
+ }
37426
37510
  const id = randomUUID3();
37427
37511
  const item = {
37428
37512
  id,
@@ -37433,7 +37517,6 @@ class TurnQueue {
37433
37517
  ...params.accountId !== undefined ? { accountId: params.accountId } : {},
37434
37518
  ...params.media !== undefined ? { media: params.media } : {}
37435
37519
  };
37436
- const q = this.queues.get(key) ?? [];
37437
37520
  q.push(item);
37438
37521
  this.queues.set(key, q);
37439
37522
  this.emitQueueUpdated(params.chatKey, params.sessionAlias);
@@ -37554,6 +37637,28 @@ class TurnQueue {
37554
37637
  entry.controller.abort();
37555
37638
  return true;
37556
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
+ }
37557
37662
  cancelQueuedItem(chatKey, sessionAlias, itemId) {
37558
37663
  const key = turnKey(chatKey, sessionAlias);
37559
37664
  const q = this.queues.get(key);
@@ -37595,6 +37700,7 @@ class ControlService {
37595
37700
  runTurn: (req, signal, onActivity) => this.runner.run(req, signal, onActivity),
37596
37701
  ...this.deps.turnIdleTimeoutMs ? { turnIdleTimeoutMs: this.deps.turnIdleTimeoutMs } : {},
37597
37702
  ...this.deps.onTurnIdleTimeout ? { onIdleTimeout: this.deps.onTurnIdleTimeout } : {},
37703
+ ...this.deps.cancelDrainTimeoutMs !== undefined ? { cancelDrainTimeoutMs: this.deps.cancelDrainTimeoutMs } : {},
37598
37704
  emitQueueUpdated: (chatKey, sessionAlias, items) => this.deps.events.emit({ type: "queue-updated", chatKey, sessionAlias, items }),
37599
37705
  detectSessionsChanged: async (detection) => {
37600
37706
  try {
@@ -37912,19 +38018,35 @@ class ControlService {
37912
38018
  }
37913
38019
  async removeSession(chatKey, alias) {
37914
38020
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
37915
- const result = await this.deps.removeSessionWithTransport(internalAlias);
37916
- this.deps.events.emit({ type: "sessions-changed" });
37917
- 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
+ }
37918
38032
  }
37919
38033
  async archiveSession(chatKey, alias) {
37920
38034
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
37921
- await this.deps.archiveSessionWithTransport(internalAlias);
37922
- const session3 = await this.deps.sessions.getSession(internalAlias).catch(() => {
37923
- return;
37924
- });
37925
- if (session3)
37926
- this.deps.sessionWarmth?.markCold(session3);
37927
- 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
+ }
37928
38050
  }
37929
38051
  async unarchiveSession(chatKey, alias) {
37930
38052
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
@@ -38555,7 +38677,7 @@ __export(exports_main, {
38555
38677
  import { randomUUID as randomUUID5 } from "node:crypto";
38556
38678
  import { homedir as homedir17 } from "node:os";
38557
38679
  import { dirname as dirname16, join as join26 } from "node:path";
38558
- import { fileURLToPath as fileURLToPath5 } from "node:url";
38680
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
38559
38681
  function startProgressHeartbeat(orchestration3, config4, logger, channel) {
38560
38682
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
38561
38683
  if (thresholdSeconds <= 0) {
@@ -39311,9 +39433,9 @@ function resolveRuntimePaths() {
39311
39433
  }
39312
39434
  function resolveBridgeEntryPath() {
39313
39435
  if (import.meta.url.includes("/dist/")) {
39314
- return fileURLToPath5(new URL("./bridge/bridge-main.js", import.meta.url));
39436
+ return fileURLToPath6(new URL("./bridge/bridge-main.js", import.meta.url));
39315
39437
  }
39316
- return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
39438
+ return fileURLToPath6(new URL("./bridge/bridge-main.ts", import.meta.url));
39317
39439
  }
39318
39440
  function resolveAppLogPath(configPath) {
39319
39441
  const rootDir = dirname16(configPath);
@@ -39568,7 +39690,7 @@ var init_config_check = __esm(async () => {
39568
39690
 
39569
39691
  // src/doctor/checks/daemon-check.ts
39570
39692
  import { readdir as readdir7, readFile as readFile17, rm as rm12 } from "node:fs/promises";
39571
- import { fileURLToPath as fileURLToPath6 } from "node:url";
39693
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
39572
39694
  import { homedir as homedir18 } from "node:os";
39573
39695
  import { join as join27 } from "node:path";
39574
39696
  async function checkDaemon(options = {}) {
@@ -39724,7 +39846,7 @@ async function defaultRemoveConsumerLock(path16) {
39724
39846
  await rm12(path16, { force: true });
39725
39847
  }
39726
39848
  function resolveCliEntryPath() {
39727
- return process.argv[1] ?? fileURLToPath6(import.meta.url);
39849
+ return process.argv[1] ?? fileURLToPath7(import.meta.url);
39728
39850
  }
39729
39851
  function formatError5(error2) {
39730
39852
  return error2 instanceof Error ? error2.message : String(error2);
@@ -41080,7 +41202,7 @@ init_core_home();
41080
41202
  import { randomUUID as randomUUID6 } from "node:crypto";
41081
41203
  import { homedir as homedir23 } from "node:os";
41082
41204
  import { dirname as dirname18, join as join30, sep as sep3 } from "node:path";
41083
- import { fileURLToPath as fileURLToPath7 } from "node:url";
41205
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
41084
41206
 
41085
41207
  // src/runtime/migrate-core-home.ts
41086
41208
  init_core_home();
@@ -55122,7 +55244,7 @@ init_plugin_home();
55122
55244
  import { spawn as spawn4 } from "node:child_process";
55123
55245
  import { readFile as readFile9 } from "node:fs/promises";
55124
55246
  import { dirname as dirname8, join as join13 } from "node:path";
55125
- import { fileURLToPath as fileURLToPath3 } from "node:url";
55247
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
55126
55248
 
55127
55249
  // src/plugins/package-manager.ts
55128
55250
  init_plugin_home();
@@ -55467,7 +55589,7 @@ async function runInherit(command, args) {
55467
55589
  }
55468
55590
  async function readPackageName() {
55469
55591
  try {
55470
- const here = dirname8(fileURLToPath3(import.meta.url));
55592
+ const here = dirname8(fileURLToPath4(import.meta.url));
55471
55593
  for (const candidate of [join13(here, "..", "package.json"), join13(here, "..", "..", "package.json")]) {
55472
55594
  try {
55473
55595
  const parsed = JSON.parse(await readFile9(candidate, "utf8"));
@@ -58126,7 +58248,7 @@ function resolveCliEntryPath2() {
58126
58248
  if (process.argv[1]) {
58127
58249
  return process.argv[1];
58128
58250
  }
58129
- return fileURLToPath7(import.meta.url);
58251
+ return fileURLToPath8(import.meta.url);
58130
58252
  }
58131
58253
  function parseDoctorArgs(args) {
58132
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.2",
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",