@ganglion/xacpx 0.15.3 → 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
@@ -20164,7 +20164,7 @@ async function resolveSessionAgentCommandFromIndex(session3) {
20164
20164
  const raw = await readFile12(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
20165
20165
  const parsed = JSON.parse(raw);
20166
20166
  const targetCwd = resolve2(session3.cwd);
20167
- const match = parsed.entries?.find((entry) => entry.name === session3.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
20167
+ const match = parsed.entries?.find((entry) => entry.name === session3.transportSession && typeof entry.cwd === "string" && resolve2(entry.cwd) === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
20168
20168
  return match?.agentCommand?.trim();
20169
20169
  } catch {
20170
20170
  return;
@@ -24733,7 +24733,7 @@ class CommandRouter {
24733
24733
  async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage, onCommands) {
24734
24734
  const startedAt = Date.now();
24735
24735
  let command = parseCommand(input);
24736
- if (metadata?.channel === "control" && command.kind !== "prompt") {
24736
+ if (metadata?.channel === "control" && command.kind !== "prompt" && command.kind !== "session.reset") {
24737
24737
  command = { kind: "prompt", text: input.trim() };
24738
24738
  }
24739
24739
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -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);
@@ -33861,7 +33935,9 @@ class ControlService {
33861
33935
  transportSession: session3.transportSession,
33862
33936
  running: this.deps.activeTurns.isActiveAnywhere(session3.alias),
33863
33937
  archived: session3.archived === true,
33864
- ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {}
33938
+ ...session3.source === "agent-side" ? { native: true } : {},
33939
+ ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
33940
+ ...session3.displayName ? { displayName: session3.displayName } : {}
33865
33941
  }));
33866
33942
  }
33867
33943
  async listNativeSessions(_chatKey, agent3, workspace3) {
@@ -34012,10 +34088,14 @@ class ControlService {
34012
34088
  resolveSettled = resolve4;
34013
34089
  });
34014
34090
  this.inFlight.set(key, { controller, settled });
34091
+ let internalAlias;
34015
34092
  let wasArchived = false;
34093
+ let priorTransportSession;
34016
34094
  try {
34017
- const internalAlias = await this.deps.sessions.resolveAliasForChat(params.chatKey, params.sessionAlias);
34018
- wasArchived = (await this.deps.sessions.getSession(internalAlias))?.archived === true;
34095
+ internalAlias = await this.deps.sessions.resolveAliasForChat(params.chatKey, params.sessionAlias);
34096
+ const prior = await this.deps.sessions.getSession(internalAlias);
34097
+ wasArchived = prior?.archived === true;
34098
+ priorTransportSession = prior?.transportSession;
34019
34099
  } catch {}
34020
34100
  try {
34021
34101
  await this.deps.sessions.useSession(params.chatKey, params.sessionAlias);
@@ -34158,6 +34238,14 @@ ${chunk}` : chunk
34158
34238
  } finally {
34159
34239
  this.inFlight.delete(key);
34160
34240
  resolveSettled();
34241
+ if (internalAlias && priorTransportSession) {
34242
+ try {
34243
+ const after = await this.deps.sessions.getSession(internalAlias);
34244
+ if (after && after.transportSession !== priorTransportSession) {
34245
+ this.deps.events.emit({ type: "sessions-changed" });
34246
+ }
34247
+ } catch {}
34248
+ }
34161
34249
  }
34162
34250
  }
34163
34251
  cancelTurn(chatKey, sessionAlias) {
@@ -18,11 +18,17 @@ export interface ControlSessionInfo {
18
18
  transportSession: string;
19
19
  running: boolean;
20
20
  archived: boolean;
21
+ /** True when this logical session was attached to an existing agent-side (native) rollout
22
+ * rather than freshly created. Mirrors LogicalSession.source === "agent-side"; omitted for
23
+ * fresh xacpx sessions so the wire stays minimal. */
24
+ native?: boolean;
21
25
  /** The agent adapter command this session runs (acpx-recorded, or the agent's resolved
22
26
  * default). Surfaced so the web can avoid seeding a new session's model picker from a
23
27
  * session on a different adapter version (whose advertised model ids may be in an
24
28
  * incompatible format). Omitted when unknown. */
25
29
  agentCommand?: string;
30
+ /** Cosmetic relay-web display label; omitted when unset so the wire stays minimal. */
31
+ displayName?: string;
26
32
  }
27
33
  export interface ControlAgentInfo {
28
34
  name: string;
@@ -42,7 +48,7 @@ export interface ControlWorkspaceInfo {
42
48
  }
43
49
  export interface ControlServiceDeps {
44
50
  agent: Pick<ChatAgent, "chat">;
45
- sessions: Pick<SessionService, "listAllResolvedSessions" | "removeSession" | "useSession" | "resolveAliasForChat" | "getSession" | "setSessionModel">;
51
+ sessions: Pick<SessionService, "listAllResolvedSessions" | "removeSession" | "useSession" | "resolveAliasForChat" | "getSession" | "setSessionModel" | "setDisplayName">;
46
52
  transport: Pick<SessionTransport, "setModel" | "getSessionModel">;
47
53
  createSessionWithTransport: (internalAlias: string, agent: string, workspace: string, model?: string) => Promise<ResolvedSession>;
48
54
  removeSessionWithTransport: (internalAlias: string) => Promise<{
@@ -132,6 +138,8 @@ export declare class ControlService {
132
138
  }>;
133
139
  /** Switch a session's model (acpx validates the id) and persist the override. */
134
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>;
135
143
  /** Resolve a chat-scoped display alias to its ResolvedSession, or null. */
136
144
  private resolveControlSession;
137
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.3",
3
+ "version": "0.15.5",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",