@ganglion/xacpx 0.15.4 → 0.15.5

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.
@@ -4193,6 +4193,16 @@ class BridgeRuntime {
4193
4193
  await deleteAcpxSessionFiles({ acpxRecordId });
4194
4194
  return {};
4195
4195
  }
4196
+ async freeWarmProcess(input) {
4197
+ let acpxRecordId;
4198
+ try {
4199
+ ({ acpxRecordId } = await this.readSessionRecord(input));
4200
+ } catch {
4201
+ return {};
4202
+ }
4203
+ await terminateAcpxQueueOwner(acpxRecordId);
4204
+ return {};
4205
+ }
4196
4206
  async shutdown() {
4197
4207
  return {};
4198
4208
  }
@@ -4440,6 +4450,7 @@ var BRIDGE_METHODS = new Set([
4440
4450
  "cancel",
4441
4451
  "removeSession",
4442
4452
  "deleteSession",
4453
+ "freeWarmProcess",
4443
4454
  "getAgentSessionId"
4444
4455
  ]);
4445
4456
  var SESSION_SCOPED_METHODS = new Set([
@@ -4454,6 +4465,7 @@ var SESSION_SCOPED_METHODS = new Set([
4454
4465
  "cancel",
4455
4466
  "removeSession",
4456
4467
  "deleteSession",
4468
+ "freeWarmProcess",
4457
4469
  "getAgentSessionId"
4458
4470
  ]);
4459
4471
 
@@ -4677,6 +4689,13 @@ class BridgeServer {
4677
4689
  cwd: requireString(params, "cwd"),
4678
4690
  name: requireString(params, "name")
4679
4691
  });
4692
+ case "freeWarmProcess":
4693
+ return await this.runtime.freeWarmProcess({
4694
+ agent: requireString(params, "agent"),
4695
+ agentCommand: asOptionalString(params.agentCommand),
4696
+ cwd: requireString(params, "cwd"),
4697
+ name: requireString(params, "name")
4698
+ });
4680
4699
  case "getAgentSessionId":
4681
4700
  return await this.runtime.getAgentSessionId({
4682
4701
  agent: requireString(params, "agent"),
package/dist/cli.js CHANGED
@@ -24832,7 +24832,7 @@ class CommandRouter {
24832
24832
  case "cancel":
24833
24833
  return await handleCancel(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
24834
24834
  case "session.reset":
24835
- return await handleSessionReset(this.createSessionHandlerContext(reply, perfSpan), chatKey);
24835
+ return await handleSessionReset(this.createSessionHandlerContext(metadata?.channel === "control" ? undefined : reply, perfSpan), chatKey);
24836
24836
  case "session.tail":
24837
24837
  return await handleSessionTail(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.lines);
24838
24838
  case "session.rm":
@@ -25046,6 +25046,15 @@ class CommandRouter {
25046
25046
  try {
25047
25047
  await this.transport.cancel(session3);
25048
25048
  } catch {}
25049
+ try {
25050
+ await this.transport.freeWarmProcess?.(session3);
25051
+ } catch (error2) {
25052
+ await this.logger.error("session.free_warm_process_failed", "failed to free warm queue-owner on archive", {
25053
+ alias: internalAlias,
25054
+ transportSession: session3.transportSession,
25055
+ message: error2 instanceof Error ? error2.message : String(error2)
25056
+ });
25057
+ }
25049
25058
  }
25050
25059
  await this.sessions.setArchived(internalAlias, true);
25051
25060
  }
@@ -30095,6 +30104,7 @@ class SessionService {
30095
30104
  agent: session3.agent,
30096
30105
  agentCommand: session3.transport_agent_command ?? resolveRuntimeAgentCommand(agentConfig.driver, agentConfig.command, this.config.transport.preferLocalAgents !== false),
30097
30106
  model: session3.model ?? agentConfig.model,
30107
+ displayName: session3.display_name,
30098
30108
  workspace: session3.workspace,
30099
30109
  transportSession: session3.transport_session,
30100
30110
  source: session3.source,
@@ -30125,6 +30135,22 @@ class SessionService {
30125
30135
  await this.persist();
30126
30136
  });
30127
30137
  }
30138
+ async setDisplayName(alias, name) {
30139
+ await this.mutate(async () => {
30140
+ const session3 = this.state.sessions[alias];
30141
+ if (!session3) {
30142
+ throw new Error(`session "${alias}" does not exist`);
30143
+ }
30144
+ const normalized = name?.trim();
30145
+ if (normalized) {
30146
+ session3.display_name = normalized;
30147
+ } else {
30148
+ delete session3.display_name;
30149
+ }
30150
+ session3.last_used_at = new Date(this.now()).toISOString();
30151
+ await this.persist();
30152
+ });
30153
+ }
30128
30154
  async setCurrentSessionModel(chatKey, modelId) {
30129
30155
  await this.mutate(async () => {
30130
30156
  const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
@@ -31556,6 +31582,9 @@ ${result.text}` : "" };
31556
31582
  async deleteSession(session3) {
31557
31583
  await this.client.request("deleteSession", this.toParams(session3));
31558
31584
  }
31585
+ async freeWarmProcess(session3) {
31586
+ await this.client.request("freeWarmProcess", this.toParams(session3));
31587
+ }
31559
31588
  async getAgentSessionId(session3) {
31560
31589
  const result = await this.client.request("getAgentSessionId", this.toParams(session3));
31561
31590
  return result.agentSessionId;
@@ -32554,7 +32583,7 @@ class AcpxCliTransport {
32554
32583
  const structuredPrompt = await createStructuredPromptFile(text, options?.media);
32555
32584
  const args = this.buildPromptArgs(session3, text, structuredPrompt?.filePath);
32556
32585
  try {
32557
- if (reply || options?.onSegment || options?.onToolEvent || options?.onThought || options?.onPlan) {
32586
+ if (reply || options?.onSegment || options?.onToolEvent || options?.onThought || options?.onPlan || options?.onUsage || options?.onCommands) {
32558
32587
  const effectiveReplyMode = session3.effectiveReplyMode ?? session3.replyMode;
32559
32588
  const formatToolCalls = (effectiveReplyMode ?? "verbose") === "verbose";
32560
32589
  const rawStream = effectiveReplyMode === "stream";
@@ -32562,7 +32591,7 @@ class AcpxCliTransport {
32562
32591
  if ((toolEventMode === "structured" || toolEventMode === "both") && !options?.onToolEvent) {
32563
32592
  toolEventMode = "text";
32564
32593
  }
32565
- const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, formatToolCalls, toolEventMode, replyContext, options?.onSegment, options?.onToolEvent, options?.onThought, options?.onPlan, rawStream);
32594
+ const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, formatToolCalls, toolEventMode, replyContext, options?.onSegment, options?.onToolEvent, options?.onThought, options?.onPlan, options?.onUsage, options?.onCommands, rawStream);
32566
32595
  const baseText = getPromptText(result2);
32567
32596
  if (!reply) {
32568
32597
  return { text: baseText };
@@ -32671,6 +32700,15 @@ ${baseText}` : "" };
32671
32700
  await this.removeSession(session3);
32672
32701
  await deleteAcpxSessionFiles({ acpxRecordId });
32673
32702
  }
32703
+ async freeWarmProcess(session3) {
32704
+ let acpxRecordId;
32705
+ try {
32706
+ ({ acpxRecordId } = await this.readSessionRecord(session3));
32707
+ } catch {
32708
+ return;
32709
+ }
32710
+ await terminateAcpxQueueOwner(acpxRecordId);
32711
+ }
32674
32712
  async hasSession(session3) {
32675
32713
  const result = await this.runCommand(this.command, this.buildArgs(session3, [
32676
32714
  "sessions",
@@ -32758,7 +32796,7 @@ ${baseText}` : "" };
32758
32796
  })
32759
32797
  ]);
32760
32798
  }
32761
- async runStreamingPrompt(command, args, reply, formatToolCalls = false, toolEventMode = "text", replyContext, onSegment, onToolEvent, onThought, onPlan, rawStream = false) {
32799
+ async runStreamingPrompt(command, args, reply, formatToolCalls = false, toolEventMode = "text", replyContext, onSegment, onToolEvent, onThought, onPlan, onUsage, onCommands, rawStream = false) {
32762
32800
  const hooks = this.streamingHooks;
32763
32801
  const doSpawn = hooks.spawnPrompt ?? ((cmd, spawnArgs) => spawn9(cmd, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
32764
32802
  const setIntervalFn = hooks.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
@@ -32780,9 +32818,15 @@ ${baseText}` : "" };
32780
32818
  let thoughtError;
32781
32819
  let planChain = Promise.resolve();
32782
32820
  let planError;
32821
+ let usageChain = Promise.resolve();
32822
+ let usageError;
32823
+ let commandsChain = Promise.resolve();
32824
+ let commandsError;
32783
32825
  const userOnToolEvent = onToolEvent;
32784
32826
  const userOnThought = onThought;
32785
32827
  const userOnPlan = onPlan;
32828
+ const userOnUsage = onUsage;
32829
+ const userOnCommands = onCommands;
32786
32830
  const state = createStreamingPromptState(formatToolCalls, {
32787
32831
  mode: toolEventMode,
32788
32832
  rawStream,
@@ -32806,6 +32850,20 @@ ${baseText}` : "" };
32806
32850
  planError ??= error2;
32807
32851
  });
32808
32852
  }
32853
+ } : {},
32854
+ ...userOnUsage ? {
32855
+ onUsage: (usage) => {
32856
+ usageChain = usageChain.then(() => userOnUsage(usage)).catch((error2) => {
32857
+ usageError ??= error2;
32858
+ });
32859
+ }
32860
+ } : {},
32861
+ ...userOnCommands ? {
32862
+ onCommands: (commands) => {
32863
+ commandsChain = commandsChain.then(() => userOnCommands(commands)).catch((error2) => {
32864
+ commandsError ??= error2;
32865
+ });
32866
+ }
32809
32867
  } : {}
32810
32868
  });
32811
32869
  const sink = reply ? rawStream ? createVerbatimReplySink(reply) : createQuotaGatedReplySink({
@@ -32860,7 +32918,9 @@ ${baseText}` : "" };
32860
32918
  segmentChain,
32861
32919
  toolEventChain,
32862
32920
  thoughtChain,
32863
- planChain
32921
+ planChain,
32922
+ usageChain,
32923
+ commandsChain
32864
32924
  ]).then(() => {
32865
32925
  const deferred = sink?.getPendingError();
32866
32926
  if (deferred) {
@@ -32883,6 +32943,14 @@ ${baseText}` : "" };
32883
32943
  reject(planError);
32884
32944
  return;
32885
32945
  }
32946
+ if (usageError) {
32947
+ reject(usageError);
32948
+ return;
32949
+ }
32950
+ if (commandsError) {
32951
+ reject(commandsError);
32952
+ return;
32953
+ }
32886
32954
  resolve3({
32887
32955
  result: { code: code ?? 1, stdout: stdout2, stderr },
32888
32956
  overflowCount
@@ -33845,6 +33913,12 @@ class ControlService {
33845
33913
  await this.deps.transport.setModel(session3, modelId);
33846
33914
  await this.deps.sessions.setSessionModel(session3.alias, modelId);
33847
33915
  }
33916
+ async setSessionDisplayName(chatKey, alias, displayName) {
33917
+ const session3 = await this.resolveControlSession(chatKey, alias);
33918
+ if (!session3)
33919
+ throw new Error("session not found");
33920
+ await this.deps.sessions.setDisplayName(session3.alias, displayName);
33921
+ }
33848
33922
  async resolveControlSession(chatKey, alias) {
33849
33923
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
33850
33924
  return await this.deps.sessions.getSession(internalAlias);
@@ -33862,7 +33936,8 @@ class ControlService {
33862
33936
  running: this.deps.activeTurns.isActiveAnywhere(session3.alias),
33863
33937
  archived: session3.archived === true,
33864
33938
  ...session3.source === "agent-side" ? { native: true } : {},
33865
- ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {}
33939
+ ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
33940
+ ...session3.displayName ? { displayName: session3.displayName } : {}
33866
33941
  }));
33867
33942
  }
33868
33943
  async listNativeSessions(_chatKey, agent3, workspace3) {
@@ -27,6 +27,8 @@ export interface ControlSessionInfo {
27
27
  * session on a different adapter version (whose advertised model ids may be in an
28
28
  * incompatible format). Omitted when unknown. */
29
29
  agentCommand?: string;
30
+ /** Cosmetic relay-web display label; omitted when unset so the wire stays minimal. */
31
+ displayName?: string;
30
32
  }
31
33
  export interface ControlAgentInfo {
32
34
  name: string;
@@ -46,7 +48,7 @@ export interface ControlWorkspaceInfo {
46
48
  }
47
49
  export interface ControlServiceDeps {
48
50
  agent: Pick<ChatAgent, "chat">;
49
- sessions: Pick<SessionService, "listAllResolvedSessions" | "removeSession" | "useSession" | "resolveAliasForChat" | "getSession" | "setSessionModel">;
51
+ sessions: Pick<SessionService, "listAllResolvedSessions" | "removeSession" | "useSession" | "resolveAliasForChat" | "getSession" | "setSessionModel" | "setDisplayName">;
50
52
  transport: Pick<SessionTransport, "setModel" | "getSessionModel">;
51
53
  createSessionWithTransport: (internalAlias: string, agent: string, workspace: string, model?: string) => Promise<ResolvedSession>;
52
54
  removeSessionWithTransport: (internalAlias: string) => Promise<{
@@ -136,6 +138,8 @@ export declare class ControlService {
136
138
  }>;
137
139
  /** Switch a session's model (acpx validates the id) and persist the override. */
138
140
  setSessionModel(chatKey: string, alias: string, modelId: string): Promise<void>;
141
+ /** Set (or clear) a session's relay-web display label and persist it. */
142
+ setSessionDisplayName(chatKey: string, alias: string, displayName: string): Promise<void>;
139
143
  /** Resolve a chat-scoped display alias to its ResolvedSession, or null. */
140
144
  private resolveControlSession;
141
145
  get events(): ControlEventBus;
@@ -116,6 +116,8 @@ export declare class SessionService {
116
116
  private toResolvedSession;
117
117
  /** Persist (or clear) a session's model override by internal alias. */
118
118
  setSessionModel(alias: string, modelId: string | undefined): Promise<void>;
119
+ /** Set (or clear) a session's relay-web display label. Identity (`alias`) is untouched. */
120
+ setDisplayName(alias: string, name?: string): Promise<void>;
119
121
  /** Persist (or clear) the model override of the chat's current session. */
120
122
  setCurrentSessionModel(chatKey: string, modelId: string | undefined): Promise<void>;
121
123
  setSessionTransportAgentCommand(alias: string, transportAgentCommand: string | undefined): Promise<void>;
@@ -29,6 +29,9 @@ export interface LogicalSession {
29
29
  mode_id?: string;
30
30
  /** Per-session LLM model override (e.g. `gpt-5.2[high]`); falls back to the agent config default. */
31
31
  model?: string;
32
+ /** Per-session cosmetic display label shown in the relay-web dashboard only.
33
+ * Never affects identity (`alias`), `/use`, or the transport session. Cleared → UI shows alias. */
34
+ display_name?: string;
32
35
  reply_mode?: "stream" | "final" | "verbose";
33
36
  /** True when the user archived this session: process closed, row greyed + sunk.
34
37
  * Cleared on the next useSession (restore-on-message). */
@@ -59,6 +59,9 @@ export interface ResolvedSession {
59
59
  * no `--model` is passed and acpx uses the agent adapter's default.
60
60
  */
61
61
  model?: string;
62
+ /** Cosmetic per-session display label (relay-web only). Mirrors LogicalSession.display_name;
63
+ * undefined when unset. Does not affect identity or transport. */
64
+ displayName?: string;
62
65
  workspace: string;
63
66
  transportSession: string;
64
67
  source?: "xacpx" | "agent-side";
@@ -207,6 +210,15 @@ export interface SessionTransport {
207
210
  * that can't delete omit it. A missing acpx session is a no-op (idempotent).
208
211
  */
209
212
  deleteSession?(session: ResolvedSession): Promise<void>;
213
+ /**
214
+ * Terminate the warm queue-owner process for this session, freeing its
215
+ * resources, WITHOUT closing the acpx session (no `closed` flag, no metadata
216
+ * change) — the session stays open and resumes with full history on the next
217
+ * prompt. Idempotent: a missing warm process or missing session is a no-op.
218
+ * Used by archive to free the process now instead of waiting for acpx's TTL.
219
+ * Optional: transports that can't reap omit it.
220
+ */
221
+ freeWarmProcess?(session: ResolvedSession): Promise<void>;
210
222
  /**
211
223
  * Read the underlying agent-native session id for an existing transport
212
224
  * session. Used by `/clear` to keep a native session native: the fresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",