@ganglion/xacpx 0.20.2 → 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.
@@ -3524,18 +3524,19 @@ function buildToolUseEvent(update, driver) {
3524
3524
  function normalizeCursorPlanUpdate(state, update) {
3525
3525
  if (state.driver !== "cursor")
3526
3526
  return;
3527
- const title = normalizeCursorToolName(update.title);
3528
- if (!CURSOR_PLAN_TOOL_NAMES.has(title))
3527
+ if (!CURSOR_PLAN_TOOL_NAMES.has(cursorToolIdentity(update)))
3529
3528
  return;
3530
3529
  const input = cursorToolInput(update.rawInput);
3531
- if (!Array.isArray(input?.todos))
3530
+ const output = cursorToolInput(update.rawOutput);
3531
+ const todos = readCursorTodoList(input) ?? readCursorTodoList(output);
3532
+ if (todos === undefined)
3532
3533
  return;
3533
- const merge = input.merge === true;
3534
- if (input.todos.length === 0) {
3534
+ const merge = input?.merge === true || output?.merge === true;
3535
+ if (todos.length === 0) {
3535
3536
  state.cursorPlanEntries.clear();
3536
3537
  return [];
3537
3538
  }
3538
- const patches = input.todos.map((todo, index) => parseCursorPlanTodo(todo, index)).filter((todo) => todo !== undefined);
3539
+ const patches = todos.map((todo, index) => parseCursorPlanTodo(todo, index)).filter((todo) => todo !== undefined);
3539
3540
  if (patches.length === 0)
3540
3541
  return;
3541
3542
  if (!merge)
@@ -3555,6 +3556,15 @@ function normalizeCursorPlanUpdate(state, update) {
3555
3556
  }
3556
3557
  return [...state.cursorPlanEntries.values()];
3557
3558
  }
3559
+ function readCursorTodoList(input) {
3560
+ if (!input)
3561
+ return;
3562
+ for (const key of ["todos", "todoList"]) {
3563
+ if (Array.isArray(input[key]))
3564
+ return input[key];
3565
+ }
3566
+ return;
3567
+ }
3558
3568
  function parseCursorPlanTodo(value, index) {
3559
3569
  if (!isRecord2(value))
3560
3570
  return;
@@ -3612,6 +3622,11 @@ function cursorToolInput(rawInput) {
3612
3622
  function normalizeCursorToolName(title) {
3613
3623
  return (title ?? "").trim().toLowerCase().replace(/[\s_-]+/g, "");
3614
3624
  }
3625
+ function cursorToolIdentity(update) {
3626
+ const input = cursorToolInput(update.rawInput);
3627
+ const declared = typeof input?.[CURSOR_TOOL_NAME_KEY] === "string" ? input[CURSOR_TOOL_NAME_KEY] : undefined;
3628
+ return normalizeCursorToolName(declared ?? update.title);
3629
+ }
3615
3630
  function normalizeToolKind(update, driver) {
3616
3631
  const kindRaw = update?.kind?.trim().toLowerCase() ?? "";
3617
3632
  switch (kindRaw) {
@@ -3624,12 +3639,14 @@ function normalizeToolKind(update, driver) {
3624
3639
  }
3625
3640
  if (driver !== "cursor")
3626
3641
  return "other";
3627
- switch (normalizeCursorToolName(update?.title)) {
3642
+ switch (cursorToolIdentity(update ?? {})) {
3628
3643
  case "read":
3629
3644
  case "readfile":
3630
3645
  return "read";
3631
3646
  case "grep":
3632
3647
  case "glob":
3648
+ case "find":
3649
+ case "codebasesearch":
3633
3650
  case "search":
3634
3651
  return "search";
3635
3652
  case "shell":
@@ -3647,6 +3664,8 @@ function normalizeToolKind(update, driver) {
3647
3664
  case "runsubagent":
3648
3665
  case "switchmode":
3649
3666
  case "todowrite":
3667
+ case "updatetodos":
3668
+ case "todoread":
3650
3669
  case "createplan":
3651
3670
  case "updateplan":
3652
3671
  case "plan":
@@ -3656,8 +3675,7 @@ function normalizeToolKind(update, driver) {
3656
3675
  }
3657
3676
  }
3658
3677
  function isCursorSubagentInput(rawInput, title) {
3659
- const normalizedTitle = normalizeCursorToolName(title);
3660
- if (!CURSOR_SUBAGENT_TOOL_NAMES.has(normalizedTitle))
3678
+ if (!CURSOR_SUBAGENT_TOOL_NAMES.has(cursorToolIdentity({ title, rawInput })))
3661
3679
  return false;
3662
3680
  const input = cursorToolInput(rawInput);
3663
3681
  return input !== undefined && readFirstString(input, ["subagent_type", "subagentType", "agent", "agentType", "prompt", "instructions"]) !== undefined;
@@ -3832,7 +3850,7 @@ function isGenericToolTitle(kind, title) {
3832
3850
  }
3833
3851
  return false;
3834
3852
  }
3835
- var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, CURSOR_PLAN_TOOL_NAMES, CURSOR_SUBAGENT_TOOL_NAMES, USAGE_BREAKDOWN_FIELDS;
3853
+ var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, CURSOR_TOOL_NAME_KEY = "_toolName", CURSOR_PLAN_TOOL_NAMES, CURSOR_SUBAGENT_TOOL_NAMES, USAGE_BREAKDOWN_FIELDS;
3836
3854
  var init_streaming_prompt = __esm(() => {
3837
3855
  init_background_followup();
3838
3856
  init_tool_kind_emoji();
@@ -3842,7 +3860,14 @@ var init_streaming_prompt = __esm(() => {
3842
3860
  LINE_BREAK_AT_END = /\r?\n[\t ]*$/;
3843
3861
  LINE_BREAK_AT_START = /^[\t ]*\r?\n/;
3844
3862
  PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r$/;
3845
- CURSOR_PLAN_TOOL_NAMES = new Set(["todowrite", "createplan", "updateplan", "plan"]);
3863
+ CURSOR_PLAN_TOOL_NAMES = new Set([
3864
+ "todowrite",
3865
+ "createplan",
3866
+ "updateplan",
3867
+ "plan",
3868
+ "updatetodos",
3869
+ "todoread"
3870
+ ]);
3846
3871
  CURSOR_SUBAGENT_TOOL_NAMES = new Set(["task", "delegate", "runsubagent", "subagent"]);
3847
3872
  USAGE_BREAKDOWN_FIELDS = [
3848
3873
  ["inputTokens", ["inputTokens", "input_tokens"]],
@@ -4034,6 +4059,7 @@ var init_session = __esm(() => {
4034
4059
  `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
4035
4060
  ].join(`
4036
4061
  `),
4062
+ sessionAliasCollided: (desiredAlias, finalAlias) => `The requested name "${desiredAlias}" is already in use (by an archived session). Creating the new session as "${finalAlias}" instead.`,
4037
4063
  sessionAttachNotFound: (alias, agent, workspace) => [
4038
4064
  "No existing session found to attach.",
4039
4065
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -5159,6 +5185,7 @@ var init_session2 = __esm(() => {
5159
5185
  `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
5160
5186
  ].join(`
5161
5187
  `),
5188
+ sessionAliasCollided: (desiredAlias, finalAlias) => `你指定的会话名「${desiredAlias}」已被其他会话(含已睡眠/归档)占用,已自动改为「${finalAlias}」创建。`,
5162
5189
  sessionAttachNotFound: (alias, agent2, workspace2) => [
5163
5190
  "没有找到可绑定的已有会话。",
5164
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");
@@ -52917,18 +52982,19 @@ function buildToolUseEvent(update, driver) {
52917
52982
  function normalizeCursorPlanUpdate(state, update) {
52918
52983
  if (state.driver !== "cursor")
52919
52984
  return;
52920
- const title = normalizeCursorToolName(update.title);
52921
- if (!CURSOR_PLAN_TOOL_NAMES.has(title))
52985
+ if (!CURSOR_PLAN_TOOL_NAMES.has(cursorToolIdentity(update)))
52922
52986
  return;
52923
52987
  const input = cursorToolInput(update.rawInput);
52924
- if (!Array.isArray(input?.todos))
52988
+ const output = cursorToolInput(update.rawOutput);
52989
+ const todos = readCursorTodoList(input) ?? readCursorTodoList(output);
52990
+ if (todos === undefined)
52925
52991
  return;
52926
- const merge2 = input.merge === true;
52927
- if (input.todos.length === 0) {
52992
+ const merge2 = input?.merge === true || output?.merge === true;
52993
+ if (todos.length === 0) {
52928
52994
  state.cursorPlanEntries.clear();
52929
52995
  return [];
52930
52996
  }
52931
- const patches = input.todos.map((todo, index) => parseCursorPlanTodo(todo, index)).filter((todo) => todo !== undefined);
52997
+ const patches = todos.map((todo, index) => parseCursorPlanTodo(todo, index)).filter((todo) => todo !== undefined);
52932
52998
  if (patches.length === 0)
52933
52999
  return;
52934
53000
  if (!merge2)
@@ -52948,6 +53014,15 @@ function normalizeCursorPlanUpdate(state, update) {
52948
53014
  }
52949
53015
  return [...state.cursorPlanEntries.values()];
52950
53016
  }
53017
+ function readCursorTodoList(input) {
53018
+ if (!input)
53019
+ return;
53020
+ for (const key of ["todos", "todoList"]) {
53021
+ if (Array.isArray(input[key]))
53022
+ return input[key];
53023
+ }
53024
+ return;
53025
+ }
52951
53026
  function parseCursorPlanTodo(value, index) {
52952
53027
  if (!isRecord4(value))
52953
53028
  return;
@@ -53005,6 +53080,11 @@ function cursorToolInput(rawInput) {
53005
53080
  function normalizeCursorToolName(title) {
53006
53081
  return (title ?? "").trim().toLowerCase().replace(/[\s_-]+/g, "");
53007
53082
  }
53083
+ function cursorToolIdentity(update) {
53084
+ const input = cursorToolInput(update.rawInput);
53085
+ const declared = typeof input?.[CURSOR_TOOL_NAME_KEY] === "string" ? input[CURSOR_TOOL_NAME_KEY] : undefined;
53086
+ return normalizeCursorToolName(declared ?? update.title);
53087
+ }
53008
53088
  function normalizeToolKind(update, driver) {
53009
53089
  const kindRaw = update?.kind?.trim().toLowerCase() ?? "";
53010
53090
  switch (kindRaw) {
@@ -53017,12 +53097,14 @@ function normalizeToolKind(update, driver) {
53017
53097
  }
53018
53098
  if (driver !== "cursor")
53019
53099
  return "other";
53020
- switch (normalizeCursorToolName(update?.title)) {
53100
+ switch (cursorToolIdentity(update ?? {})) {
53021
53101
  case "read":
53022
53102
  case "readfile":
53023
53103
  return "read";
53024
53104
  case "grep":
53025
53105
  case "glob":
53106
+ case "find":
53107
+ case "codebasesearch":
53026
53108
  case "search":
53027
53109
  return "search";
53028
53110
  case "shell":
@@ -53040,6 +53122,8 @@ function normalizeToolKind(update, driver) {
53040
53122
  case "runsubagent":
53041
53123
  case "switchmode":
53042
53124
  case "todowrite":
53125
+ case "updatetodos":
53126
+ case "todoread":
53043
53127
  case "createplan":
53044
53128
  case "updateplan":
53045
53129
  case "plan":
@@ -53049,8 +53133,7 @@ function normalizeToolKind(update, driver) {
53049
53133
  }
53050
53134
  }
53051
53135
  function isCursorSubagentInput(rawInput, title) {
53052
- const normalizedTitle = normalizeCursorToolName(title);
53053
- if (!CURSOR_SUBAGENT_TOOL_NAMES.has(normalizedTitle))
53136
+ if (!CURSOR_SUBAGENT_TOOL_NAMES.has(cursorToolIdentity({ title, rawInput })))
53054
53137
  return false;
53055
53138
  const input = cursorToolInput(rawInput);
53056
53139
  return input !== undefined && readFirstString(input, ["subagent_type", "subagentType", "agent", "agentType", "prompt", "instructions"]) !== undefined;
@@ -53225,7 +53308,7 @@ function isGenericToolTitle(kind, title) {
53225
53308
  }
53226
53309
  return false;
53227
53310
  }
53228
- var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, CURSOR_PLAN_TOOL_NAMES, CURSOR_SUBAGENT_TOOL_NAMES, USAGE_BREAKDOWN_FIELDS;
53311
+ var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, CURSOR_TOOL_NAME_KEY = "_toolName", CURSOR_PLAN_TOOL_NAMES, CURSOR_SUBAGENT_TOOL_NAMES, USAGE_BREAKDOWN_FIELDS;
53229
53312
  var init_streaming_prompt = __esm(() => {
53230
53313
  init_background_followup();
53231
53314
  init_tool_kind_emoji();
@@ -53235,7 +53318,14 @@ var init_streaming_prompt = __esm(() => {
53235
53318
  LINE_BREAK_AT_END = /\r?\n[\t ]*$/;
53236
53319
  LINE_BREAK_AT_START = /^[\t ]*\r?\n/;
53237
53320
  PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r$/;
53238
- CURSOR_PLAN_TOOL_NAMES = new Set(["todowrite", "createplan", "updateplan", "plan"]);
53321
+ CURSOR_PLAN_TOOL_NAMES = new Set([
53322
+ "todowrite",
53323
+ "createplan",
53324
+ "updateplan",
53325
+ "plan",
53326
+ "updatetodos",
53327
+ "todoread"
53328
+ ]);
53239
53329
  CURSOR_SUBAGENT_TOOL_NAMES = new Set(["task", "delegate", "runsubagent", "subagent"]);
53240
53330
  USAGE_BREAKDOWN_FIELDS = [
53241
53331
  ["inputTokens", ["inputTokens", "input_tokens"]],
@@ -57031,7 +57121,8 @@ class ControlService {
57031
57121
  const session3 = agentSessionId ? await this.deps.attachNativeSessionWithTransport(internalAlias, agent3, workspace3, agentSessionId) : await this.deps.createSessionWithTransport(internalAlias, agent3, workspace3, model);
57032
57122
  this.deps.events.emit({ type: "sessions-changed" });
57033
57123
  if (nativeHistory.length > 0) {
57034
- 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 });
57035
57126
  }
57036
57127
  return {
57037
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.2",
3
+ "version": "0.20.3-beta.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",