@ganglion/xacpx 0.20.3-beta.0 → 0.20.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.
@@ -4059,6 +4059,7 @@ var init_session = __esm(() => {
4059
4059
  `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
4060
4060
  ].join(`
4061
4061
  `),
4062
+ sessionAliasCollided: (desiredAlias, finalAlias) => `The requested name "${desiredAlias}" is already in use (by an archived session). Creating the new session as "${finalAlias}" instead.`,
4062
4063
  sessionAttachNotFound: (alias, agent, workspace) => [
4063
4064
  "No existing session found to attach.",
4064
4065
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -5184,6 +5185,7 @@ var init_session2 = __esm(() => {
5184
5185
  `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
5185
5186
  ].join(`
5186
5187
  `),
5188
+ sessionAliasCollided: (desiredAlias, finalAlias) => `你指定的会话名「${desiredAlias}」已被其他会话(含已睡眠/归档)占用,已自动改为「${finalAlias}」创建。`,
5187
5189
  sessionAttachNotFound: (alias, agent2, workspace2) => [
5188
5190
  "没有找到可绑定的已有会话。",
5189
5191
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -7225,8 +7227,28 @@ function deriveAgentAlias(driver, argv) {
7225
7227
  const hash = createHash5("sha256").update(identity).digest("hex").slice(0, 12);
7226
7228
  return `xacpx-managed-${driver}-${hash}`;
7227
7229
  }
7230
+ function isDerivedAgentArgv(driver, argv, runtimeRoot, platform) {
7231
+ if (!argv || argv.length === 0) {
7232
+ return false;
7233
+ }
7234
+ if (isManagedAdapterId(driver)) {
7235
+ const spec = MANAGED_ADAPTERS[driver];
7236
+ return argv[0] === "npx" && argv[1] === "-y" && argv.some((entry) => entry.startsWith(`${spec.packageName}@`)) || classifyRecordedPreinstalledAdapterCommand(renderAgentArgvIdentity(argv), {
7237
+ runtimeRoot,
7238
+ platform
7239
+ }) === driver;
7240
+ }
7241
+ if (driver === "hermes") {
7242
+ return argv[1]?.includes("hermes-acp-shim.") === true;
7243
+ }
7244
+ if (driver === "opencode" || driver === "kilocode") {
7245
+ return argv.length === 2 && argv[0] === driver && argv[1] === "acp";
7246
+ }
7247
+ return false;
7248
+ }
7228
7249
  var IDENTITY_SAFE_ARG_RE;
7229
7250
  var init_agent_launch = __esm(() => {
7251
+ init_adapter_catalog();
7230
7252
  IDENTITY_SAFE_ARG_RE = /^[A-Za-z0-9_@%+=:,./^~-]+$/u;
7231
7253
  });
7232
7254
 
package/dist/cli.js CHANGED
@@ -97,6 +97,7 @@ var init_session = __esm(() => {
97
97
  `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
98
98
  ].join(`
99
99
  `),
100
+ sessionAliasCollided: (desiredAlias, finalAlias) => `The requested name "${desiredAlias}" is already in use (by an archived session). Creating the new session as "${finalAlias}" instead.`,
100
101
  sessionAttachNotFound: (alias, agent, workspace) => [
101
102
  "No existing session found to attach.",
102
103
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -1222,6 +1223,7 @@ var init_session2 = __esm(() => {
1222
1223
  `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
1223
1224
  ].join(`
1224
1225
  `),
1226
+ sessionAliasCollided: (desiredAlias, finalAlias) => `你指定的会话名「${desiredAlias}」已被其他会话(含已睡眠/归档)占用,已自动改为「${finalAlias}」创建。`,
1225
1227
  sessionAttachNotFound: (alias, agent2, workspace2) => [
1226
1228
  "没有找到可绑定的已有会话。",
1227
1229
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -4620,8 +4622,28 @@ function deriveAgentAlias(driver, argv) {
4620
4622
  const hash = createHash3("sha256").update(identity).digest("hex").slice(0, 12);
4621
4623
  return `xacpx-managed-${driver}-${hash}`;
4622
4624
  }
4625
+ function isDerivedAgentArgv(driver, argv, runtimeRoot, platform) {
4626
+ if (!argv || argv.length === 0) {
4627
+ return false;
4628
+ }
4629
+ if (isManagedAdapterId(driver)) {
4630
+ const spec = MANAGED_ADAPTERS[driver];
4631
+ return argv[0] === "npx" && argv[1] === "-y" && argv.some((entry) => entry.startsWith(`${spec.packageName}@`)) || classifyRecordedPreinstalledAdapterCommand(renderAgentArgvIdentity(argv), {
4632
+ runtimeRoot,
4633
+ platform
4634
+ }) === driver;
4635
+ }
4636
+ if (driver === "hermes") {
4637
+ return argv[1]?.includes("hermes-acp-shim.") === true;
4638
+ }
4639
+ if (driver === "opencode" || driver === "kilocode") {
4640
+ return argv.length === 2 && argv[0] === driver && argv[1] === "acp";
4641
+ }
4642
+ return false;
4643
+ }
4623
4644
  var IDENTITY_SAFE_ARG_RE;
4624
4645
  var init_agent_launch = __esm(() => {
4646
+ init_adapter_catalog();
4625
4647
  IDENTITY_SAFE_ARG_RE = /^[A-Za-z0-9_@%+=:,./^~-]+$/u;
4626
4648
  });
4627
4649
 
@@ -6302,6 +6324,7 @@ class ConfigStore {
6302
6324
  agents[name] = {
6303
6325
  driver: agent3.driver,
6304
6326
  ...agent3.command ? { command: agent3.command } : {},
6327
+ ...agent3.argv ? { argv: [...agent3.argv] } : {},
6305
6328
  ...agent3.model ? { model: agent3.model } : {},
6306
6329
  ...agent3.settingsPolicy ? { settingsPolicy: agent3.settingsPolicy } : {}
6307
6330
  };
@@ -41205,20 +41228,19 @@ async function handleSessions(context, chatKey) {
41205
41228
  async function handleSessionNew(context, chatKey, alias, agent3, workspace3, model) {
41206
41229
  const channelId = getChannelIdFromChatKey(chatKey);
41207
41230
  const internalAlias = scopeDisplayAliasToInternal(channelId, alias);
41208
- const existing = context.sessions.getResolvedSessionByInternalAlias(internalAlias);
41209
- if (existing) {
41210
- return { text: t().session.sessionAlreadyExists(alias, existing.agent, existing.workspace) };
41211
- }
41212
- const releaseAliasReservation = context.sessions.tryReserveNewSessionAlias(internalAlias);
41213
- if (!releaseAliasReservation) {
41231
+ const reserved = context.sessions.tryReserveFreeSessionAlias(internalAlias);
41232
+ if (!reserved) {
41214
41233
  const claimed = context.sessions.getResolvedSessionByInternalAlias(internalAlias);
41215
41234
  return {
41216
41235
  text: claimed ? t().session.sessionAlreadyExists(alias, claimed.agent, claimed.workspace) : t().session.sessionAlreadyExists(alias, agent3, workspace3)
41217
41236
  };
41218
41237
  }
41238
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
41239
+ const finalDisplayAlias = toDisplaySessionAlias(finalInternalAlias);
41240
+ const aliasWasAdjusted = finalInternalAlias !== internalAlias;
41219
41241
  try {
41220
- const stableTransportSession = `${workspace3}:${internalAlias}`;
41221
- const session3 = context.lifecycle.resolveSession(internalAlias, agent3, workspace3, context.sessions.buildFreshTransportSession(stableTransportSession));
41242
+ const stableTransportSession = `${workspace3}:${finalInternalAlias}`;
41243
+ const session3 = context.lifecycle.resolveSession(finalInternalAlias, agent3, workspace3, context.sessions.buildFreshTransportSession(stableTransportSession));
41222
41244
  const normalizedModel = model?.trim();
41223
41245
  if (normalizedModel) {
41224
41246
  session3.model = normalizedModel;
@@ -41234,18 +41256,27 @@ async function handleSessionNew(context, chatKey, alias, agent3, workspace3, mod
41234
41256
  } catch (error2) {
41235
41257
  return context.recovery.renderSessionCreationError(session3, error2);
41236
41258
  }
41237
- await context.sessions.attachSession(internalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
41259
+ await context.sessions.attachSession(finalInternalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
41238
41260
  if (normalizedModel) {
41239
- await context.sessions.setSessionModel(internalAlias, normalizedModel);
41261
+ await context.sessions.setSessionModel(finalInternalAlias, normalizedModel);
41240
41262
  }
41241
- await context.sessions.useSession(chatKey, internalAlias);
41242
- await refreshSessionTransportAgentCommandBestEffort(context, internalAlias, "session.agent_command_refresh_failed");
41263
+ await context.sessions.useSession(chatKey, finalInternalAlias);
41264
+ await refreshSessionTransportAgentCommandBestEffort(context, finalInternalAlias, "session.agent_command_refresh_failed");
41243
41265
  await context.logger.info("session.created", "created and selected logical session", {
41244
- alias: internalAlias,
41266
+ alias: finalInternalAlias,
41245
41267
  agent: agent3,
41246
41268
  workspace: workspace3
41247
41269
  });
41248
- return { text: t().session.sessionCreated(alias) };
41270
+ if (aliasWasAdjusted) {
41271
+ return {
41272
+ text: [
41273
+ t().session.sessionAliasCollided(alias, finalDisplayAlias),
41274
+ t().session.sessionCreated(finalDisplayAlias)
41275
+ ].join(`
41276
+ `)
41277
+ };
41278
+ }
41279
+ return { text: t().session.sessionCreated(finalDisplayAlias) };
41249
41280
  } finally {
41250
41281
  await releaseTransportReservation();
41251
41282
  }
@@ -42845,65 +42876,67 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent3, targe
42845
42876
  const baseAlias = `${workspace3.name}:${agent3}`;
42846
42877
  const channelId = getChannelIdFromChatKey(chatKey);
42847
42878
  const scopedBase = scopeDisplayAliasToInternal(channelId, baseAlias);
42848
- const alias = createNew ? await allocateUniqueSessionAlias(context, scopedBase, chatKey) : scopedBase;
42849
- const display = toDisplaySessionAlias(alias);
42850
- if (!createNew && await hasLogicalSession(context, alias, chatKey)) {
42851
- await context.sessions.useSession(chatKey, alias);
42879
+ const desiredAlias = scopedBase;
42880
+ const desiredDisplay = toDisplaySessionAlias(desiredAlias);
42881
+ if (!createNew && await hasLogicalSession(context, desiredAlias, chatKey)) {
42882
+ await context.sessions.useSession(chatKey, desiredAlias);
42852
42883
  await context.logger.info("session.shortcut.reused", "reused existing logical session", {
42853
- alias,
42884
+ alias: desiredAlias,
42854
42885
  workspace: workspace3.name,
42855
42886
  agent: agent3
42856
42887
  });
42857
42888
  return {
42858
42889
  text: [
42859
- t().shortcut.reuseHeader(display),
42890
+ t().shortcut.reuseHeader(desiredDisplay),
42860
42891
  t().shortcut.reuseWorkspace(workspace3.name),
42861
- t().shortcut.reuseSession(display)
42892
+ t().shortcut.reuseSession(desiredDisplay)
42862
42893
  ].join(`
42863
42894
  `)
42864
42895
  };
42865
42896
  }
42866
- const releaseAliasReservation = context.sessions.tryReserveNewSessionAlias(alias);
42867
- if (!releaseAliasReservation) {
42868
- return renderShortcutSessionCreationError(workspace3, display);
42897
+ const reserved = context.sessions.tryReserveFreeSessionAlias(desiredAlias);
42898
+ if (!reserved) {
42899
+ return renderShortcutSessionCreationError(workspace3, desiredDisplay);
42869
42900
  }
42901
+ const { alias: finalAlias, release: releaseAliasReservation } = reserved;
42902
+ const finalDisplay = toDisplaySessionAlias(finalAlias);
42870
42903
  try {
42871
- const stableTransportSession = channelId === "weixin" ? alias : context.sessions.buildDefaultTransportSessionForChat(chatKey, display);
42872
- const session3 = ops.resolveSession(alias, agent3, workspace3.name, context.sessions.buildFreshTransportSession(stableTransportSession));
42904
+ const stableTransportSession = channelId === "weixin" ? finalAlias : context.sessions.buildDefaultTransportSessionForChat(chatKey, finalDisplay);
42905
+ const session3 = ops.resolveSession(finalAlias, agent3, workspace3.name, context.sessions.buildFreshTransportSession(stableTransportSession));
42873
42906
  const releaseTransportReservation = await ops.reserveTransportSession(stableTransportSession);
42874
42907
  try {
42875
42908
  try {
42876
42909
  await ops.ensureTransportSession(session3);
42877
42910
  const exists = await ops.checkTransportSession(session3);
42878
42911
  if (!exists) {
42879
- return renderShortcutSessionCreationError(workspace3, display);
42912
+ return renderShortcutSessionCreationError(workspace3, finalDisplay);
42880
42913
  }
42881
42914
  } catch (err) {
42882
42915
  if (err instanceof AutoInstallFailedError)
42883
42916
  throw err;
42884
- return renderShortcutSessionCreationError(workspace3, display);
42917
+ return renderShortcutSessionCreationError(workspace3, finalDisplay);
42885
42918
  }
42886
- await context.sessions.attachSession(alias, agent3, workspace3.name, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
42887
- await context.sessions.useSession(chatKey, alias);
42919
+ await context.sessions.attachSession(finalAlias, agent3, workspace3.name, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
42920
+ await context.sessions.useSession(chatKey, finalAlias);
42888
42921
  try {
42889
- await ops.refreshSessionTransportAgentCommand(alias);
42922
+ await ops.refreshSessionTransportAgentCommand(finalAlias);
42890
42923
  } catch (error2) {
42891
42924
  await context.logger.error("session.shortcut.agent_command_refresh_failed", "failed to refresh session agent command", {
42892
- alias,
42925
+ alias: finalAlias,
42893
42926
  error: error2 instanceof Error ? error2.message : String(error2)
42894
42927
  });
42895
42928
  }
42896
42929
  await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
42897
- alias,
42930
+ alias: finalAlias,
42898
42931
  workspace: workspace3.name,
42899
42932
  agent: agent3,
42900
42933
  workspaceReused: workspace3.reused
42901
42934
  });
42902
42935
  return {
42903
42936
  text: [
42904
- t().shortcut.createdHeader(display),
42937
+ t().shortcut.createdHeader(finalDisplay),
42905
42938
  workspace3.reused ? t().shortcut.createdReusedWorkspace(workspace3.name) : t().shortcut.createdNewWorkspace(workspace3.name, workspace3.cwd),
42906
- t().shortcut.createdNewSession(display)
42939
+ t().shortcut.createdNewSession(finalDisplay)
42907
42940
  ].join(`
42908
42941
  `)
42909
42942
  };
@@ -42950,16 +42983,6 @@ async function resolveShortcutWorkspace(context, target) {
42950
42983
  reused: false
42951
42984
  };
42952
42985
  }
42953
- async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
42954
- if (!await hasLogicalSession(context, baseAlias, chatKey)) {
42955
- return baseAlias;
42956
- }
42957
- let suffix = 2;
42958
- while (await hasLogicalSession(context, `${baseAlias}-${suffix}`, chatKey)) {
42959
- suffix += 1;
42960
- }
42961
- return `${baseAlias}-${suffix}`;
42962
- }
42963
42986
  async function hasLogicalSession(context, alias, chatKey) {
42964
42987
  const sessions = await context.sessions.listSessions(chatKey);
42965
42988
  return sessions.some((session3) => session3.internalAlias === alias);
@@ -43082,17 +43105,43 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43082
43105
  const existing = await context.sessions.findAttachedNativeSession(chatKey, nativeTarget.agent, session3.sessionId);
43083
43106
  if (existing) {
43084
43107
  await context.sessions.useSession(chatKey, existing.alias);
43085
- const displayAlias2 = toDisplaySessionAlias(existing.alias);
43108
+ const displayAlias = toDisplaySessionAlias(existing.alias);
43086
43109
  return {
43087
- text: t().nativeSession.alreadySwitched(nativeTarget.agentDisplayName, displayAlias2)
43110
+ text: t().nativeSession.alreadySwitched(nativeTarget.agentDisplayName, displayAlias)
43088
43111
  };
43089
43112
  }
43113
+ const channelId = getChannelIdFromChatKey(chatKey);
43090
43114
  const requestedAlias = alias?.trim() || buildDefaultNativeAlias(nativeTarget.agent, session3.sessionId);
43091
- const displayAlias = await allocateUniqueNativeAlias(context, chatKey, requestedAlias);
43092
- const internalAlias = scopeDisplayAliasToInternal(getChannelIdFromChatKey(chatKey), displayAlias);
43093
- const transportSession = context.sessions.buildDefaultTransportSessionForChat(chatKey, displayAlias);
43094
- const resolvedSession = context.lifecycle.resolveSession(internalAlias, nativeTarget.agent, nativeTarget.workspace, transportSession);
43095
- const releaseReservation = await context.lifecycle.reserveTransportSession(resolvedSession.transportSession);
43115
+ let reserved = null;
43116
+ let finalDisplayAlias = requestedAlias;
43117
+ for (let attempt = 0;attempt < 5; attempt += 1) {
43118
+ const candidateDisplay = await allocateUniqueNativeAlias(context, chatKey, requestedAlias);
43119
+ const candidateInternal = scopeDisplayAliasToInternal(channelId, candidateDisplay);
43120
+ const release = context.sessions.tryReserveNewSessionAlias(candidateInternal);
43121
+ if (release) {
43122
+ reserved = { alias: candidateInternal, release };
43123
+ finalDisplayAlias = candidateDisplay;
43124
+ break;
43125
+ }
43126
+ }
43127
+ let finalInternalAlias;
43128
+ let releaseAliasReservation;
43129
+ if (reserved) {
43130
+ finalInternalAlias = reserved.alias;
43131
+ releaseAliasReservation = reserved.release;
43132
+ } else {
43133
+ const desiredInternal = scopeDisplayAliasToInternal(channelId, finalDisplayAlias);
43134
+ const free = context.sessions.tryReserveFreeSessionAlias(desiredInternal);
43135
+ if (!free) {
43136
+ return { text: t().session.sessionAlreadyExists(finalDisplayAlias, nativeTarget.agent, nativeTarget.workspace) };
43137
+ }
43138
+ finalInternalAlias = free.alias;
43139
+ releaseAliasReservation = free.release;
43140
+ finalDisplayAlias = toDisplaySessionAlias(finalInternalAlias);
43141
+ }
43142
+ const transportSession = context.sessions.buildDefaultTransportSessionForChat(chatKey, finalDisplayAlias);
43143
+ const resolvedSession = context.lifecycle.resolveSession(finalInternalAlias, nativeTarget.agent, nativeTarget.workspace, transportSession);
43144
+ const releaseTransportReservation = await context.lifecycle.reserveTransportSession(resolvedSession.transportSession);
43096
43145
  try {
43097
43146
  try {
43098
43147
  await context.transport.resumeAgentSession(resolvedSession, session3.sessionId);
@@ -43104,7 +43153,7 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43104
43153
  return { text: t().nativeSession.attachVerificationFailed(target.agentDisplayName) };
43105
43154
  }
43106
43155
  await context.sessions.attachNativeSession({
43107
- alias: internalAlias,
43156
+ alias: finalInternalAlias,
43108
43157
  agent: nativeTarget.agent,
43109
43158
  workspace: nativeTarget.workspace,
43110
43159
  transportSession,
@@ -43115,13 +43164,17 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43115
43164
  title: session3.title,
43116
43165
  updatedAt: session3.updatedAt
43117
43166
  });
43118
- await context.sessions.useSession(chatKey, internalAlias);
43119
- await refreshAgentCommandBestEffort(context, internalAlias);
43167
+ await context.sessions.useSession(chatKey, finalInternalAlias);
43168
+ await refreshAgentCommandBestEffort(context, finalInternalAlias);
43120
43169
  return {
43121
- text: t().nativeSession.attachedAndSwitched(target.agentDisplayName, toDisplaySessionAlias(internalAlias))
43170
+ text: t().nativeSession.attachedAndSwitched(target.agentDisplayName, toDisplaySessionAlias(finalInternalAlias))
43122
43171
  };
43123
43172
  } finally {
43124
- await releaseReservation();
43173
+ try {
43174
+ await releaseTransportReservation();
43175
+ } finally {
43176
+ releaseAliasReservation();
43177
+ }
43125
43178
  }
43126
43179
  }
43127
43180
  async function resolveNativeTarget(context, chatKey, input) {
@@ -44384,13 +44437,14 @@ class SessionControlService {
44384
44437
  this.reserveLogicalTransportSession = deps.reserveLogicalTransportSession;
44385
44438
  }
44386
44439
  async createSessionWithTransport(internalAlias, agent3, workspace3, model) {
44387
- const releaseAliasReservation = this.sessions.tryReserveNewSessionAlias(internalAlias);
44388
- if (!releaseAliasReservation) {
44389
- throw new Error(`session "${internalAlias}" already exists`);
44440
+ const reserved = this.sessions.tryReserveFreeSessionAlias(internalAlias);
44441
+ if (!reserved) {
44442
+ throw new Error(`session "${internalAlias}" already exists and could not derive a free alias`);
44390
44443
  }
44444
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
44391
44445
  try {
44392
- const stableTransportSession = `${workspace3}:${internalAlias}`;
44393
- const session3 = this.sessions.resolveSession(internalAlias, agent3, workspace3, this.sessions.buildFreshTransportSession(stableTransportSession));
44446
+ const stableTransportSession = `${workspace3}:${finalInternalAlias}`;
44447
+ const session3 = this.sessions.resolveSession(finalInternalAlias, agent3, workspace3, this.sessions.buildFreshTransportSession(stableTransportSession));
44394
44448
  const normalizedModel = model?.trim();
44395
44449
  if (normalizedModel) {
44396
44450
  session3.model = normalizedModel;
@@ -44402,15 +44456,15 @@ class SessionControlService {
44402
44456
  if (!exists) {
44403
44457
  throw new Error(`transport session "${session3.transportSession}" could not be verified`);
44404
44458
  }
44405
- await this.sessions.attachSession(internalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
44459
+ await this.sessions.attachSession(finalInternalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
44406
44460
  if (normalizedModel) {
44407
- await this.sessions.setSessionModel(internalAlias, normalizedModel);
44461
+ await this.sessions.setSessionModel(finalInternalAlias, normalizedModel);
44408
44462
  }
44409
44463
  try {
44410
- await this.invoker.refreshSessionTransportAgentCommand(internalAlias);
44464
+ await this.invoker.refreshSessionTransportAgentCommand(finalInternalAlias);
44411
44465
  } catch (error2) {
44412
44466
  await this.logger.error("session.agent_command_refresh_failed", "failed to refresh session agent command", {
44413
- alias: internalAlias,
44467
+ alias: finalInternalAlias,
44414
44468
  error: error2 instanceof Error ? error2.message : String(error2)
44415
44469
  });
44416
44470
  }
@@ -44533,12 +44587,13 @@ class SessionControlService {
44533
44587
  if (!this.transport.resumeAgentSession) {
44534
44588
  throw new Error("the active transport does not support native sessions");
44535
44589
  }
44536
- const releaseAliasReservation = this.sessions.tryReserveNewSessionAlias(internalAlias);
44537
- if (!releaseAliasReservation) {
44538
- throw new Error(`session "${internalAlias}" already exists`);
44590
+ const reserved = this.sessions.tryReserveFreeSessionAlias(internalAlias);
44591
+ if (!reserved) {
44592
+ throw new Error(`session "${internalAlias}" already exists and could not derive a free alias`);
44539
44593
  }
44594
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
44540
44595
  try {
44541
- const session3 = this.sessions.resolveSession(internalAlias, agent3, workspace3, `${workspace3}:${internalAlias}`);
44596
+ const session3 = this.sessions.resolveSession(finalInternalAlias, agent3, workspace3, `${workspace3}:${finalInternalAlias}`);
44542
44597
  const release = await this.reserveLogicalTransportSession(session3.transportSession);
44543
44598
  try {
44544
44599
  await this.transport.resumeAgentSession(session3, agentSessionId);
@@ -44547,7 +44602,7 @@ class SessionControlService {
44547
44602
  throw new Error(`transport session "${session3.transportSession}" could not be verified`);
44548
44603
  }
44549
44604
  await this.sessions.attachNativeSession({
44550
- alias: internalAlias,
44605
+ alias: finalInternalAlias,
44551
44606
  agent: agent3,
44552
44607
  workspace: workspace3,
44553
44608
  transportSession: session3.transportSession,
@@ -44559,10 +44614,10 @@ class SessionControlService {
44559
44614
  ...nativeMeta?.updatedAt !== undefined ? { updatedAt: nativeMeta.updatedAt } : {}
44560
44615
  });
44561
44616
  try {
44562
- await this.invoker.refreshSessionTransportAgentCommand(internalAlias);
44617
+ await this.invoker.refreshSessionTransportAgentCommand(finalInternalAlias);
44563
44618
  } catch (error2) {
44564
44619
  await this.logger.error("session.native.agent_command_refresh_failed", "failed to refresh native session agent command", {
44565
- alias: internalAlias,
44620
+ alias: finalInternalAlias,
44566
44621
  error: error2 instanceof Error ? error2.message : String(error2)
44567
44622
  });
44568
44623
  }
@@ -49832,6 +49887,37 @@ class SessionService {
49832
49887
  this.pendingSessionAliasOperations.delete(alias);
49833
49888
  };
49834
49889
  }
49890
+ deriveFreeAlias(desiredAlias) {
49891
+ const taken = Object.keys(this.state.sessions);
49892
+ const takenSet = new Set(taken);
49893
+ if (!takenSet.has(desiredAlias))
49894
+ return desiredAlias;
49895
+ const sep4 = desiredAlias.lastIndexOf(":");
49896
+ const prefix = sep4 >= 0 ? desiredAlias.slice(0, sep4 + 1) : "";
49897
+ const base = sep4 >= 0 ? desiredAlias.slice(sep4 + 1) : desiredAlias;
49898
+ const suffixMatch = /-(\d+)$/.exec(base);
49899
+ let startN = 2;
49900
+ let stem = base;
49901
+ if (suffixMatch) {
49902
+ startN = Number(suffixMatch[1]) + 1;
49903
+ stem = base.slice(0, -suffixMatch[0].length);
49904
+ }
49905
+ for (let n = startN;n <= 9999; n += 1) {
49906
+ const candidate = `${prefix}${stem}-${n}`;
49907
+ if (!takenSet.has(candidate))
49908
+ return candidate;
49909
+ }
49910
+ throw new Error(`could not derive a free alias for "${desiredAlias}" after 9999 attempts`);
49911
+ }
49912
+ tryReserveFreeSessionAlias(desiredAlias) {
49913
+ for (let attempt = 0;attempt < 5; attempt += 1) {
49914
+ const alias = this.deriveFreeAlias(desiredAlias);
49915
+ const release = this.tryReserveNewSessionAlias(alias);
49916
+ if (release)
49917
+ return { alias, release };
49918
+ }
49919
+ return null;
49920
+ }
49835
49921
  buildFreshTransportSession(stableTransportSession) {
49836
49922
  const timestamp = Math.max(0, Math.trunc(this.now()));
49837
49923
  const nonce = randomBytes2(8).readBigUInt64BE().toString().padStart(20, "0");
@@ -50066,7 +50152,7 @@ class SessionService {
50066
50152
  platform,
50067
50153
  runtimeRoot: this.runtimeRoot
50068
50154
  });
50069
- if (current.rawCommand) {
50155
+ if (current.rawCommand || resolveAgentCommand(agentConfig.driver, agentConfig.command)) {
50070
50156
  return current;
50071
50157
  }
50072
50158
  const recordedArgv = isDerivedAgentArgv(agentConfig.driver, session3.transport_agent_argv, this.runtimeRoot, platform) ? undefined : session3.transport_agent_argv;
@@ -50086,9 +50172,6 @@ class SessionService {
50086
50172
  }) === agentConfig.driver);
50087
50173
  const recordedCommand = recordedIsDerived ? undefined : session3.transport_agent_command;
50088
50174
  if (recordedCommand) {
50089
- if (platform === "win32") {
50090
- throw new Error(`session "${session3.alias}" was created with a raw command that cannot be launched ` + "on Windows. Migrate the agent to an argv array in config: " + 'agents.<name>.argv = ["agent.exe", "--acp", ...]');
50091
- }
50092
50175
  return { acpxAgent: agentConfig.driver, rawCommand: recordedCommand, agentCommand: recordedCommand };
50093
50176
  }
50094
50177
  return current;
@@ -50265,25 +50348,6 @@ class SessionService {
50265
50348
  }
50266
50349
  }
50267
50350
  }
50268
- function isDerivedAgentArgv(driver, argv, runtimeRoot, platform) {
50269
- if (!argv || argv.length === 0) {
50270
- return false;
50271
- }
50272
- if (isManagedAdapterId(driver)) {
50273
- const spec = MANAGED_ADAPTERS[driver];
50274
- return argv[0] === "npx" && argv[1] === "-y" && argv.some((entry) => entry.startsWith(`${spec.packageName}@`)) || classifyRecordedPreinstalledAdapterCommand(renderAgentArgvIdentity(argv), {
50275
- runtimeRoot,
50276
- platform
50277
- }) === driver;
50278
- }
50279
- if (driver === "hermes") {
50280
- return argv[1]?.includes("hermes-acp-shim.") === true;
50281
- }
50282
- if (driver === "opencode" || driver === "kilocode") {
50283
- return argv.length === 2 && argv[0] === driver && argv[1] === "acp";
50284
- }
50285
- return false;
50286
- }
50287
50351
  var init_session_service = __esm(() => {
50288
50352
  init_resolve_agent_command();
50289
50353
  init_adapter_catalog();
@@ -57056,7 +57120,8 @@ class ControlService {
57056
57120
  const session3 = agentSessionId ? await this.deps.attachNativeSessionWithTransport(internalAlias, agent3, workspace3, agentSessionId) : await this.deps.createSessionWithTransport(internalAlias, agent3, workspace3, model);
57057
57121
  this.deps.events.emit({ type: "sessions-changed" });
57058
57122
  if (nativeHistory.length > 0) {
57059
- this.deps.events.emit({ type: "session-history", chatKey, sessionAlias: alias, messages: nativeHistory });
57123
+ const historyAlias = toDisplaySessionAlias(session3.alias);
57124
+ this.deps.events.emit({ type: "session-history", chatKey, sessionAlias: historyAlias, messages: nativeHistory });
57060
57125
  }
57061
57126
  return {
57062
57127
  alias: toDisplaySessionAlias(session3.alias),
@@ -8,16 +8,16 @@
8
8
  * - Structured launches carry `agentArgv` (exact boundaries, Windows-safe) and
9
9
  * `agentCommand` (the stable canonical identity acpx persists and keys its
10
10
  * session records by).
11
- * - `rawCommand` is the Unix-only legacy escape hatch passed as acpx `--agent`.
11
+ * - `rawCommand` is the legacy escape hatch / historical selector for acpx `--agent`.
12
12
  */
13
13
  export interface AgentLaunchSpec {
14
14
  /** acpx positional agent: bare driver or xacpx-managed alias. */
15
15
  acpxAgent: string;
16
16
  /** Stable acpx session identity for explicit launches. */
17
17
  agentCommand?: string;
18
- /** Exact executable and argument boundaries for overlay/migration. */
18
+ /** Exact executable and argument boundaries for structured / overlay launches. */
19
19
  agentArgv?: string[];
20
- /** Unix-only legacy raw override. */
20
+ /** Legacy raw override / historical session selector. */
21
21
  rawCommand?: string;
22
22
  }
23
23
  export declare function renderAgentArgvIdentity(argv: readonly string[]): string;
@@ -26,3 +26,9 @@ export declare function isValidAgentArgv(value: unknown): value is string[];
26
26
  /** Content-addressed overlay alias: stable per canonical argv across processes
27
27
  * and platforms; only acpx-safe name characters. */
28
28
  export declare function deriveAgentAlias(driver: string, argv: readonly string[]): string;
29
+ /** Detects an argv shape that `resolveLaunchSpec` treats as derived state
30
+ * (managed adapter pin, hermes shim, or local fallback for opencode /
31
+ * kilocode). A session with such argv has `recordedArgv` reset to undefined
32
+ * before step 2 in `resolveLaunchSpec`, so the sticky bypass does NOT
33
+ * apply — the session follows the current config / derived launch instead. */
34
+ export declare function isDerivedAgentArgv(driver: string, argv: string[] | undefined, runtimeRoot: string, platform: NodeJS.Platform): boolean;
@@ -12,6 +12,7 @@ export interface SessionMessages {
12
12
  sessionListItem: (alias: string, agent: string, workspace: string) => string;
13
13
  sessionCreated: (alias: string) => string;
14
14
  sessionAlreadyExists: (alias: string, agent: string, workspace: string) => string;
15
+ sessionAliasCollided: (desiredAlias: string, finalAlias: string) => string;
15
16
  sessionAttachNotFound: (alias: string, agent: string, workspace: string) => string;
16
17
  sessionAttached: (alias: string) => string;
17
18
  sessionLifecycleBusy: (alias: string) => string;
@@ -97,6 +97,7 @@ var init_session = __esm(() => {
97
97
  `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
98
98
  ].join(`
99
99
  `),
100
+ sessionAliasCollided: (desiredAlias, finalAlias) => `The requested name "${desiredAlias}" is already in use (by an archived session). Creating the new session as "${finalAlias}" instead.`,
100
101
  sessionAttachNotFound: (alias, agent, workspace) => [
101
102
  "No existing session found to attach.",
102
103
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -1222,6 +1223,7 @@ var init_session2 = __esm(() => {
1222
1223
  `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
1223
1224
  ].join(`
1224
1225
  `),
1226
+ sessionAliasCollided: (desiredAlias, finalAlias) => `你指定的会话名「${desiredAlias}」已被其他会话(含已睡眠/归档)占用,已自动改为「${finalAlias}」创建。`,
1225
1227
  sessionAttachNotFound: (alias, agent2, workspace2) => [
1226
1228
  "没有找到可绑定的已有会话。",
1227
1229
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -124,6 +124,30 @@ export declare class SessionService {
124
124
  tryReserveNewSessionAlias(alias: string): (() => void) | null;
125
125
  /** Exclusively claim any lifecycle operation that can replace an alias binding. */
126
126
  tryReserveSessionAliasOperation(alias: string): (() => void) | null;
127
+ /**
128
+ * Derive a free internal alias for a new session. When `desiredAlias` is already
129
+ * taken (by an active OR archived session), append `-2`, `-3`, … until a free
130
+ * slot is found.
131
+ *
132
+ * The alias may carry a channel prefix (`relay:demo`, `weixin:chat123/demo`); the
133
+ * numeric suffix is always appended to the last path segment after the rightmost
134
+ * `:` so prefix semantics are preserved (e.g. `relay:demo-2`, never `relay-2:demo`).
135
+ */
136
+ deriveFreeAlias(desiredAlias: string): string;
137
+ /**
138
+ * Convenience: atomically derive a free alias (see `deriveFreeAlias`) AND
139
+ * reserve it via `tryReserveNewSessionAlias`. If the derived alias loses the
140
+ * race between the derive step and the reserve step (extremely rare because
141
+ * callers typically run this while holding the state mutex), retry up to a
142
+ * small, bounded number of times.
143
+ *
144
+ * Returns the chosen alias together with the reservation release callback, or
145
+ * `null` if no reservation could be obtained.
146
+ */
147
+ tryReserveFreeSessionAlias(desiredAlias: string): {
148
+ alias: string;
149
+ release: () => void;
150
+ } | null;
127
151
  /**
128
152
  * Allocate a fresh acpx transport incarnation while preserving a stable logical
129
153
  * coordinator identity. Explicit "new session" paths use this; archive/restore
@@ -64,7 +64,7 @@ export interface ResolvedSession {
64
64
  * alias provisioned in ~/.acpx/config.json; bare drivers pass through.
65
65
  */
66
66
  acpxAgent?: string;
67
- /** Unix-only legacy raw override passed as acpx `--agent`. */
67
+ /** Legacy raw override / historical session selector passed as acpx `--agent`. */
68
68
  rawCommand?: string;
69
69
  /** Exact executable + argument boundaries for overlay/migration. */
70
70
  agentArgv?: string[];
@@ -124,7 +124,7 @@ export interface AgentSessionListQuery {
124
124
  agentCommand?: string;
125
125
  /** Positional acpx agent for list/show queries (overlay alias or bare driver). */
126
126
  acpxAgent?: string;
127
- /** Unix-only legacy raw override; the shared builder prefers it over acpxAgent. */
127
+ /** Legacy raw override / historical selector; the shared builder prefers it over acpxAgent. */
128
128
  rawCommand?: string;
129
129
  /** Resolved acpx driver for `agent` (e.g. a custom `my-codex` agent has driver `codex`). Used to gate driver-specific list filtering. */
130
130
  driver?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.20.3-beta.0",
3
+ "version": "0.20.3",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",