@ganglion/xacpx 0.20.3-beta.0 → 0.20.3-beta.1

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 <会话名>`
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 <会话名>`
@@ -41205,20 +41207,19 @@ async function handleSessions(context, chatKey) {
41205
41207
  async function handleSessionNew(context, chatKey, alias, agent3, workspace3, model) {
41206
41208
  const channelId = getChannelIdFromChatKey(chatKey);
41207
41209
  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) {
41210
+ const reserved = context.sessions.tryReserveFreeSessionAlias(internalAlias);
41211
+ if (!reserved) {
41214
41212
  const claimed = context.sessions.getResolvedSessionByInternalAlias(internalAlias);
41215
41213
  return {
41216
41214
  text: claimed ? t().session.sessionAlreadyExists(alias, claimed.agent, claimed.workspace) : t().session.sessionAlreadyExists(alias, agent3, workspace3)
41217
41215
  };
41218
41216
  }
41217
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
41218
+ const finalDisplayAlias = toDisplaySessionAlias(finalInternalAlias);
41219
+ const aliasWasAdjusted = finalInternalAlias !== internalAlias;
41219
41220
  try {
41220
- const stableTransportSession = `${workspace3}:${internalAlias}`;
41221
- const session3 = context.lifecycle.resolveSession(internalAlias, agent3, workspace3, context.sessions.buildFreshTransportSession(stableTransportSession));
41221
+ const stableTransportSession = `${workspace3}:${finalInternalAlias}`;
41222
+ const session3 = context.lifecycle.resolveSession(finalInternalAlias, agent3, workspace3, context.sessions.buildFreshTransportSession(stableTransportSession));
41222
41223
  const normalizedModel = model?.trim();
41223
41224
  if (normalizedModel) {
41224
41225
  session3.model = normalizedModel;
@@ -41234,18 +41235,27 @@ async function handleSessionNew(context, chatKey, alias, agent3, workspace3, mod
41234
41235
  } catch (error2) {
41235
41236
  return context.recovery.renderSessionCreationError(session3, error2);
41236
41237
  }
41237
- await context.sessions.attachSession(internalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
41238
+ await context.sessions.attachSession(finalInternalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
41238
41239
  if (normalizedModel) {
41239
- await context.sessions.setSessionModel(internalAlias, normalizedModel);
41240
+ await context.sessions.setSessionModel(finalInternalAlias, normalizedModel);
41240
41241
  }
41241
- await context.sessions.useSession(chatKey, internalAlias);
41242
- await refreshSessionTransportAgentCommandBestEffort(context, internalAlias, "session.agent_command_refresh_failed");
41242
+ await context.sessions.useSession(chatKey, finalInternalAlias);
41243
+ await refreshSessionTransportAgentCommandBestEffort(context, finalInternalAlias, "session.agent_command_refresh_failed");
41243
41244
  await context.logger.info("session.created", "created and selected logical session", {
41244
- alias: internalAlias,
41245
+ alias: finalInternalAlias,
41245
41246
  agent: agent3,
41246
41247
  workspace: workspace3
41247
41248
  });
41248
- return { text: t().session.sessionCreated(alias) };
41249
+ if (aliasWasAdjusted) {
41250
+ return {
41251
+ text: [
41252
+ t().session.sessionAliasCollided(alias, finalDisplayAlias),
41253
+ t().session.sessionCreated(finalDisplayAlias)
41254
+ ].join(`
41255
+ `)
41256
+ };
41257
+ }
41258
+ return { text: t().session.sessionCreated(finalDisplayAlias) };
41249
41259
  } finally {
41250
41260
  await releaseTransportReservation();
41251
41261
  }
@@ -42845,65 +42855,67 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent3, targe
42845
42855
  const baseAlias = `${workspace3.name}:${agent3}`;
42846
42856
  const channelId = getChannelIdFromChatKey(chatKey);
42847
42857
  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);
42858
+ const desiredAlias = scopedBase;
42859
+ const desiredDisplay = toDisplaySessionAlias(desiredAlias);
42860
+ if (!createNew && await hasLogicalSession(context, desiredAlias, chatKey)) {
42861
+ await context.sessions.useSession(chatKey, desiredAlias);
42852
42862
  await context.logger.info("session.shortcut.reused", "reused existing logical session", {
42853
- alias,
42863
+ alias: desiredAlias,
42854
42864
  workspace: workspace3.name,
42855
42865
  agent: agent3
42856
42866
  });
42857
42867
  return {
42858
42868
  text: [
42859
- t().shortcut.reuseHeader(display),
42869
+ t().shortcut.reuseHeader(desiredDisplay),
42860
42870
  t().shortcut.reuseWorkspace(workspace3.name),
42861
- t().shortcut.reuseSession(display)
42871
+ t().shortcut.reuseSession(desiredDisplay)
42862
42872
  ].join(`
42863
42873
  `)
42864
42874
  };
42865
42875
  }
42866
- const releaseAliasReservation = context.sessions.tryReserveNewSessionAlias(alias);
42867
- if (!releaseAliasReservation) {
42868
- return renderShortcutSessionCreationError(workspace3, display);
42876
+ const reserved = context.sessions.tryReserveFreeSessionAlias(desiredAlias);
42877
+ if (!reserved) {
42878
+ return renderShortcutSessionCreationError(workspace3, desiredDisplay);
42869
42879
  }
42880
+ const { alias: finalAlias, release: releaseAliasReservation } = reserved;
42881
+ const finalDisplay = toDisplaySessionAlias(finalAlias);
42870
42882
  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));
42883
+ const stableTransportSession = channelId === "weixin" ? finalAlias : context.sessions.buildDefaultTransportSessionForChat(chatKey, finalDisplay);
42884
+ const session3 = ops.resolveSession(finalAlias, agent3, workspace3.name, context.sessions.buildFreshTransportSession(stableTransportSession));
42873
42885
  const releaseTransportReservation = await ops.reserveTransportSession(stableTransportSession);
42874
42886
  try {
42875
42887
  try {
42876
42888
  await ops.ensureTransportSession(session3);
42877
42889
  const exists = await ops.checkTransportSession(session3);
42878
42890
  if (!exists) {
42879
- return renderShortcutSessionCreationError(workspace3, display);
42891
+ return renderShortcutSessionCreationError(workspace3, finalDisplay);
42880
42892
  }
42881
42893
  } catch (err) {
42882
42894
  if (err instanceof AutoInstallFailedError)
42883
42895
  throw err;
42884
- return renderShortcutSessionCreationError(workspace3, display);
42896
+ return renderShortcutSessionCreationError(workspace3, finalDisplay);
42885
42897
  }
42886
- await context.sessions.attachSession(alias, agent3, workspace3.name, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
42887
- await context.sessions.useSession(chatKey, alias);
42898
+ await context.sessions.attachSession(finalAlias, agent3, workspace3.name, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
42899
+ await context.sessions.useSession(chatKey, finalAlias);
42888
42900
  try {
42889
- await ops.refreshSessionTransportAgentCommand(alias);
42901
+ await ops.refreshSessionTransportAgentCommand(finalAlias);
42890
42902
  } catch (error2) {
42891
42903
  await context.logger.error("session.shortcut.agent_command_refresh_failed", "failed to refresh session agent command", {
42892
- alias,
42904
+ alias: finalAlias,
42893
42905
  error: error2 instanceof Error ? error2.message : String(error2)
42894
42906
  });
42895
42907
  }
42896
42908
  await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
42897
- alias,
42909
+ alias: finalAlias,
42898
42910
  workspace: workspace3.name,
42899
42911
  agent: agent3,
42900
42912
  workspaceReused: workspace3.reused
42901
42913
  });
42902
42914
  return {
42903
42915
  text: [
42904
- t().shortcut.createdHeader(display),
42916
+ t().shortcut.createdHeader(finalDisplay),
42905
42917
  workspace3.reused ? t().shortcut.createdReusedWorkspace(workspace3.name) : t().shortcut.createdNewWorkspace(workspace3.name, workspace3.cwd),
42906
- t().shortcut.createdNewSession(display)
42918
+ t().shortcut.createdNewSession(finalDisplay)
42907
42919
  ].join(`
42908
42920
  `)
42909
42921
  };
@@ -42950,16 +42962,6 @@ async function resolveShortcutWorkspace(context, target) {
42950
42962
  reused: false
42951
42963
  };
42952
42964
  }
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
42965
  async function hasLogicalSession(context, alias, chatKey) {
42964
42966
  const sessions = await context.sessions.listSessions(chatKey);
42965
42967
  return sessions.some((session3) => session3.internalAlias === alias);
@@ -43082,17 +43084,43 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43082
43084
  const existing = await context.sessions.findAttachedNativeSession(chatKey, nativeTarget.agent, session3.sessionId);
43083
43085
  if (existing) {
43084
43086
  await context.sessions.useSession(chatKey, existing.alias);
43085
- const displayAlias2 = toDisplaySessionAlias(existing.alias);
43087
+ const displayAlias = toDisplaySessionAlias(existing.alias);
43086
43088
  return {
43087
- text: t().nativeSession.alreadySwitched(nativeTarget.agentDisplayName, displayAlias2)
43089
+ text: t().nativeSession.alreadySwitched(nativeTarget.agentDisplayName, displayAlias)
43088
43090
  };
43089
43091
  }
43092
+ const channelId = getChannelIdFromChatKey(chatKey);
43090
43093
  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);
43094
+ let reserved = null;
43095
+ let finalDisplayAlias = requestedAlias;
43096
+ for (let attempt = 0;attempt < 5; attempt += 1) {
43097
+ const candidateDisplay = await allocateUniqueNativeAlias(context, chatKey, requestedAlias);
43098
+ const candidateInternal = scopeDisplayAliasToInternal(channelId, candidateDisplay);
43099
+ const release = context.sessions.tryReserveNewSessionAlias(candidateInternal);
43100
+ if (release) {
43101
+ reserved = { alias: candidateInternal, release };
43102
+ finalDisplayAlias = candidateDisplay;
43103
+ break;
43104
+ }
43105
+ }
43106
+ let finalInternalAlias;
43107
+ let releaseAliasReservation;
43108
+ if (reserved) {
43109
+ finalInternalAlias = reserved.alias;
43110
+ releaseAliasReservation = reserved.release;
43111
+ } else {
43112
+ const desiredInternal = scopeDisplayAliasToInternal(channelId, finalDisplayAlias);
43113
+ const free = context.sessions.tryReserveFreeSessionAlias(desiredInternal);
43114
+ if (!free) {
43115
+ return { text: t().session.sessionAlreadyExists(finalDisplayAlias, nativeTarget.agent, nativeTarget.workspace) };
43116
+ }
43117
+ finalInternalAlias = free.alias;
43118
+ releaseAliasReservation = free.release;
43119
+ finalDisplayAlias = toDisplaySessionAlias(finalInternalAlias);
43120
+ }
43121
+ const transportSession = context.sessions.buildDefaultTransportSessionForChat(chatKey, finalDisplayAlias);
43122
+ const resolvedSession = context.lifecycle.resolveSession(finalInternalAlias, nativeTarget.agent, nativeTarget.workspace, transportSession);
43123
+ const releaseTransportReservation = await context.lifecycle.reserveTransportSession(resolvedSession.transportSession);
43096
43124
  try {
43097
43125
  try {
43098
43126
  await context.transport.resumeAgentSession(resolvedSession, session3.sessionId);
@@ -43104,7 +43132,7 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43104
43132
  return { text: t().nativeSession.attachVerificationFailed(target.agentDisplayName) };
43105
43133
  }
43106
43134
  await context.sessions.attachNativeSession({
43107
- alias: internalAlias,
43135
+ alias: finalInternalAlias,
43108
43136
  agent: nativeTarget.agent,
43109
43137
  workspace: nativeTarget.workspace,
43110
43138
  transportSession,
@@ -43115,13 +43143,17 @@ async function attachNativeSession(context, chatKey, target, session3, alias) {
43115
43143
  title: session3.title,
43116
43144
  updatedAt: session3.updatedAt
43117
43145
  });
43118
- await context.sessions.useSession(chatKey, internalAlias);
43119
- await refreshAgentCommandBestEffort(context, internalAlias);
43146
+ await context.sessions.useSession(chatKey, finalInternalAlias);
43147
+ await refreshAgentCommandBestEffort(context, finalInternalAlias);
43120
43148
  return {
43121
- text: t().nativeSession.attachedAndSwitched(target.agentDisplayName, toDisplaySessionAlias(internalAlias))
43149
+ text: t().nativeSession.attachedAndSwitched(target.agentDisplayName, toDisplaySessionAlias(finalInternalAlias))
43122
43150
  };
43123
43151
  } finally {
43124
- await releaseReservation();
43152
+ try {
43153
+ await releaseTransportReservation();
43154
+ } finally {
43155
+ releaseAliasReservation();
43156
+ }
43125
43157
  }
43126
43158
  }
43127
43159
  async function resolveNativeTarget(context, chatKey, input) {
@@ -44384,13 +44416,14 @@ class SessionControlService {
44384
44416
  this.reserveLogicalTransportSession = deps.reserveLogicalTransportSession;
44385
44417
  }
44386
44418
  async createSessionWithTransport(internalAlias, agent3, workspace3, model) {
44387
- const releaseAliasReservation = this.sessions.tryReserveNewSessionAlias(internalAlias);
44388
- if (!releaseAliasReservation) {
44389
- throw new Error(`session "${internalAlias}" already exists`);
44419
+ const reserved = this.sessions.tryReserveFreeSessionAlias(internalAlias);
44420
+ if (!reserved) {
44421
+ throw new Error(`session "${internalAlias}" already exists and could not derive a free alias`);
44390
44422
  }
44423
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
44391
44424
  try {
44392
- const stableTransportSession = `${workspace3}:${internalAlias}`;
44393
- const session3 = this.sessions.resolveSession(internalAlias, agent3, workspace3, this.sessions.buildFreshTransportSession(stableTransportSession));
44425
+ const stableTransportSession = `${workspace3}:${finalInternalAlias}`;
44426
+ const session3 = this.sessions.resolveSession(finalInternalAlias, agent3, workspace3, this.sessions.buildFreshTransportSession(stableTransportSession));
44394
44427
  const normalizedModel = model?.trim();
44395
44428
  if (normalizedModel) {
44396
44429
  session3.model = normalizedModel;
@@ -44402,15 +44435,15 @@ class SessionControlService {
44402
44435
  if (!exists) {
44403
44436
  throw new Error(`transport session "${session3.transportSession}" could not be verified`);
44404
44437
  }
44405
- await this.sessions.attachSession(internalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
44438
+ await this.sessions.attachSession(finalInternalAlias, agent3, workspace3, session3.transportSession, session3.agentCommand, session3.acpxAgent, session3.agentArgv);
44406
44439
  if (normalizedModel) {
44407
- await this.sessions.setSessionModel(internalAlias, normalizedModel);
44440
+ await this.sessions.setSessionModel(finalInternalAlias, normalizedModel);
44408
44441
  }
44409
44442
  try {
44410
- await this.invoker.refreshSessionTransportAgentCommand(internalAlias);
44443
+ await this.invoker.refreshSessionTransportAgentCommand(finalInternalAlias);
44411
44444
  } catch (error2) {
44412
44445
  await this.logger.error("session.agent_command_refresh_failed", "failed to refresh session agent command", {
44413
- alias: internalAlias,
44446
+ alias: finalInternalAlias,
44414
44447
  error: error2 instanceof Error ? error2.message : String(error2)
44415
44448
  });
44416
44449
  }
@@ -44533,12 +44566,13 @@ class SessionControlService {
44533
44566
  if (!this.transport.resumeAgentSession) {
44534
44567
  throw new Error("the active transport does not support native sessions");
44535
44568
  }
44536
- const releaseAliasReservation = this.sessions.tryReserveNewSessionAlias(internalAlias);
44537
- if (!releaseAliasReservation) {
44538
- throw new Error(`session "${internalAlias}" already exists`);
44569
+ const reserved = this.sessions.tryReserveFreeSessionAlias(internalAlias);
44570
+ if (!reserved) {
44571
+ throw new Error(`session "${internalAlias}" already exists and could not derive a free alias`);
44539
44572
  }
44573
+ const { alias: finalInternalAlias, release: releaseAliasReservation } = reserved;
44540
44574
  try {
44541
- const session3 = this.sessions.resolveSession(internalAlias, agent3, workspace3, `${workspace3}:${internalAlias}`);
44575
+ const session3 = this.sessions.resolveSession(finalInternalAlias, agent3, workspace3, `${workspace3}:${finalInternalAlias}`);
44542
44576
  const release = await this.reserveLogicalTransportSession(session3.transportSession);
44543
44577
  try {
44544
44578
  await this.transport.resumeAgentSession(session3, agentSessionId);
@@ -44547,7 +44581,7 @@ class SessionControlService {
44547
44581
  throw new Error(`transport session "${session3.transportSession}" could not be verified`);
44548
44582
  }
44549
44583
  await this.sessions.attachNativeSession({
44550
- alias: internalAlias,
44584
+ alias: finalInternalAlias,
44551
44585
  agent: agent3,
44552
44586
  workspace: workspace3,
44553
44587
  transportSession: session3.transportSession,
@@ -44559,10 +44593,10 @@ class SessionControlService {
44559
44593
  ...nativeMeta?.updatedAt !== undefined ? { updatedAt: nativeMeta.updatedAt } : {}
44560
44594
  });
44561
44595
  try {
44562
- await this.invoker.refreshSessionTransportAgentCommand(internalAlias);
44596
+ await this.invoker.refreshSessionTransportAgentCommand(finalInternalAlias);
44563
44597
  } catch (error2) {
44564
44598
  await this.logger.error("session.native.agent_command_refresh_failed", "failed to refresh native session agent command", {
44565
- alias: internalAlias,
44599
+ alias: finalInternalAlias,
44566
44600
  error: error2 instanceof Error ? error2.message : String(error2)
44567
44601
  });
44568
44602
  }
@@ -49832,6 +49866,37 @@ class SessionService {
49832
49866
  this.pendingSessionAliasOperations.delete(alias);
49833
49867
  };
49834
49868
  }
49869
+ deriveFreeAlias(desiredAlias) {
49870
+ const taken = Object.keys(this.state.sessions);
49871
+ const takenSet = new Set(taken);
49872
+ if (!takenSet.has(desiredAlias))
49873
+ return desiredAlias;
49874
+ const sep4 = desiredAlias.lastIndexOf(":");
49875
+ const prefix = sep4 >= 0 ? desiredAlias.slice(0, sep4 + 1) : "";
49876
+ const base = sep4 >= 0 ? desiredAlias.slice(sep4 + 1) : desiredAlias;
49877
+ const suffixMatch = /-(\d+)$/.exec(base);
49878
+ let startN = 2;
49879
+ let stem = base;
49880
+ if (suffixMatch) {
49881
+ startN = Number(suffixMatch[1]) + 1;
49882
+ stem = base.slice(0, -suffixMatch[0].length);
49883
+ }
49884
+ for (let n = startN;n <= 9999; n += 1) {
49885
+ const candidate = `${prefix}${stem}-${n}`;
49886
+ if (!takenSet.has(candidate))
49887
+ return candidate;
49888
+ }
49889
+ throw new Error(`could not derive a free alias for "${desiredAlias}" after 9999 attempts`);
49890
+ }
49891
+ tryReserveFreeSessionAlias(desiredAlias) {
49892
+ for (let attempt = 0;attempt < 5; attempt += 1) {
49893
+ const alias = this.deriveFreeAlias(desiredAlias);
49894
+ const release = this.tryReserveNewSessionAlias(alias);
49895
+ if (release)
49896
+ return { alias, release };
49897
+ }
49898
+ return null;
49899
+ }
49835
49900
  buildFreshTransportSession(stableTransportSession) {
49836
49901
  const timestamp = Math.max(0, Math.trunc(this.now()));
49837
49902
  const nonce = randomBytes2(8).readBigUInt64BE().toString().padStart(20, "0");
@@ -57056,7 +57121,8 @@ class ControlService {
57056
57121
  const session3 = agentSessionId ? await this.deps.attachNativeSessionWithTransport(internalAlias, agent3, workspace3, agentSessionId) : await this.deps.createSessionWithTransport(internalAlias, agent3, workspace3, model);
57057
57122
  this.deps.events.emit({ type: "sessions-changed" });
57058
57123
  if (nativeHistory.length > 0) {
57059
- this.deps.events.emit({ type: "session-history", chatKey, sessionAlias: alias, messages: nativeHistory });
57124
+ const historyAlias = toDisplaySessionAlias(session3.alias);
57125
+ this.deps.events.emit({ type: "session-history", chatKey, sessionAlias: historyAlias, messages: nativeHistory });
57060
57126
  }
57061
57127
  return {
57062
57128
  alias: toDisplaySessionAlias(session3.alias),
@@ -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
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-beta.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",