@ganglion/xacpx 0.12.0 → 0.13.0

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.
@@ -940,6 +940,7 @@ ${detail}`,
940
940
  sessionBlockedByTasks: (alias, count) => `Session "${alias}" has ${count} unfinished task(s). Cancel or wait for them to complete first.`,
941
941
  sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
942
942
  sessionRemoved: (alias) => `Session "${alias}" removed.`,
943
+ sessionArchived: (alias) => `Archived session "${alias}". Send a message to restore it.`,
943
944
  sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
944
945
  sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
945
946
  sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
@@ -1697,6 +1698,9 @@ var init_channel_cli = __esm(() => {
1697
1698
  channelAdded: (type) => `Channel ${type} added`,
1698
1699
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
1699
1700
  channelRemoved: (id) => `Channel ${id} removed`,
1701
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
1702
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
1703
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
1700
1704
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
1701
1705
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
1702
1706
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -2032,6 +2036,7 @@ ${detail}`,
2032
2036
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
2033
2037
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
2034
2038
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
2039
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
2035
2040
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
2036
2041
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
2037
2042
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -2789,6 +2794,9 @@ var init_channel_cli2 = __esm(() => {
2789
2794
  channelAdded: (type) => `频道 ${type} 已添加`,
2790
2795
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
2791
2796
  channelRemoved: (id) => `频道 ${id} 已删除`,
2797
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
2798
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
2799
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
2792
2800
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
2793
2801
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
2794
2802
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -3418,6 +3426,31 @@ var init_agent_session_list = __esm(() => {
3418
3426
  init_path();
3419
3427
  });
3420
3428
 
3429
+ // src/transport/acpx-session-files.ts
3430
+ import { readdir, unlink as unlink2 } from "node:fs/promises";
3431
+ import { homedir as homedir4 } from "node:os";
3432
+ import { join as join3 } from "node:path";
3433
+ async function deleteAcpxSessionFiles(options) {
3434
+ const dir = options.sessionsDir ?? join3(homedir4(), ".acpx", "sessions");
3435
+ const safeId = encodeURIComponent(options.acpxRecordId);
3436
+ await unlink2(join3(dir, `${safeId}.json`)).catch(() => {
3437
+ return;
3438
+ });
3439
+ let entries;
3440
+ try {
3441
+ entries = await readdir(dir);
3442
+ } catch {
3443
+ return;
3444
+ }
3445
+ const streamFiles = entries.filter((name) => name.startsWith(`${safeId}.stream.`));
3446
+ for (const name of streamFiles) {
3447
+ await unlink2(join3(dir, name)).catch(() => {
3448
+ return;
3449
+ });
3450
+ }
3451
+ }
3452
+ var init_acpx_session_files = () => {};
3453
+
3421
3454
  // src/bridge/bridge-main.ts
3422
3455
  import { createInterface } from "node:readline";
3423
3456
 
@@ -3490,9 +3523,9 @@ init_terminate_process_tree();
3490
3523
  init_prompt_output();
3491
3524
  init_prompt_media();
3492
3525
  init_streaming_prompt();
3493
- import { copyFile, readdir } from "node:fs/promises";
3494
- import { homedir as homedir4 } from "node:os";
3495
- import { dirname as dirname2, join as join3, win32 } from "node:path";
3526
+ import { copyFile, readdir as readdir2 } from "node:fs/promises";
3527
+ import { homedir as homedir5 } from "node:os";
3528
+ import { dirname as dirname2, join as join4, win32 } from "node:path";
3496
3529
  import { spawn as spawn4 } from "node:child_process";
3497
3530
 
3498
3531
  // src/bridge/parse-missing-optional-dep.ts
@@ -3512,6 +3545,7 @@ function parseMissingOptionalDep(text) {
3512
3545
  init_discover_parent_package_paths();
3513
3546
  init_acpx_queue_owner_launcher();
3514
3547
  init_agent_session_list();
3548
+ init_acpx_session_files();
3515
3549
  class EnsureSessionFailedError extends Error {
3516
3550
  kind;
3517
3551
  data;
@@ -3792,7 +3826,7 @@ class BridgeRuntime {
3792
3826
  acpxRecordId = parsed.id;
3793
3827
  }
3794
3828
  const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
3795
- if (acpxRecordId) {
3829
+ if (acpxRecordId && /^[\w.:-]+$/.test(acpxRecordId) && acpxRecordId.length >= 8) {
3796
3830
  return { acpxRecordId, agentSessionId };
3797
3831
  }
3798
3832
  } catch {
@@ -3884,6 +3918,17 @@ class BridgeRuntime {
3884
3918
  }
3885
3919
  throw new Error(result.stderr || result.stdout || "sessions close failed");
3886
3920
  }
3921
+ async deleteSession(input) {
3922
+ let acpxRecordId;
3923
+ try {
3924
+ ({ acpxRecordId } = await this.readSessionRecord(input));
3925
+ } catch {
3926
+ return {};
3927
+ }
3928
+ await this.removeSession(input);
3929
+ await deleteAcpxSessionFiles({ acpxRecordId });
3930
+ return {};
3931
+ }
3887
3932
  async shutdown() {
3888
3933
  return {};
3889
3934
  }
@@ -4074,14 +4119,14 @@ async function tryRepairAcpxSessionIndex(deps = {}) {
4074
4119
  if (platform !== "win32") {
4075
4120
  return false;
4076
4121
  }
4077
- const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir4();
4122
+ const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir5();
4078
4123
  if (!home) {
4079
4124
  return false;
4080
4125
  }
4081
- const pathJoin = platform === "win32" ? win32.join : join3;
4126
+ const pathJoin = platform === "win32" ? win32.join : join4;
4082
4127
  const sessionsDir = pathJoin(home, ".acpx", "sessions");
4083
4128
  const indexPath = pathJoin(sessionsDir, "index.json");
4084
- const readdirFn = deps.readdirFn ?? readdir;
4129
+ const readdirFn = deps.readdirFn ?? readdir2;
4085
4130
  const copyFileFn = deps.copyFileFn ?? copyFile;
4086
4131
  let files;
4087
4132
  try {
@@ -4129,6 +4174,7 @@ var BRIDGE_METHODS = new Set([
4129
4174
  "getSessionModel",
4130
4175
  "cancel",
4131
4176
  "removeSession",
4177
+ "deleteSession",
4132
4178
  "getAgentSessionId"
4133
4179
  ]);
4134
4180
  var SESSION_SCOPED_METHODS = new Set([
@@ -4142,6 +4188,7 @@ var SESSION_SCOPED_METHODS = new Set([
4142
4188
  "getSessionModel",
4143
4189
  "cancel",
4144
4190
  "removeSession",
4191
+ "deleteSession",
4145
4192
  "getAgentSessionId"
4146
4193
  ]);
4147
4194
 
@@ -4349,6 +4396,13 @@ class BridgeServer {
4349
4396
  cwd: requireString(params, "cwd"),
4350
4397
  name: requireString(params, "name")
4351
4398
  });
4399
+ case "deleteSession":
4400
+ return await this.runtime.deleteSession({
4401
+ agent: requireString(params, "agent"),
4402
+ agentCommand: asOptionalString(params.agentCommand),
4403
+ cwd: requireString(params, "cwd"),
4404
+ name: requireString(params, "name")
4405
+ });
4352
4406
  case "getAgentSessionId":
4353
4407
  return await this.runtime.getAgentSessionId({
4354
4408
  agent: requireString(params, "agent"),
package/dist/cli.js CHANGED
@@ -141,6 +141,7 @@ ${detail}`,
141
141
  sessionBlockedByTasks: (alias, count) => `Session "${alias}" has ${count} unfinished task(s). Cancel or wait for them to complete first.`,
142
142
  sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
143
143
  sessionRemoved: (alias) => `Session "${alias}" removed.`,
144
+ sessionArchived: (alias) => `Archived session "${alias}". Send a message to restore it.`,
144
145
  sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
145
146
  sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
146
147
  sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
@@ -898,6 +899,9 @@ var init_channel_cli = __esm(() => {
898
899
  channelAdded: (type) => `Channel ${type} added`,
899
900
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
900
901
  channelRemoved: (id) => `Channel ${id} removed`,
902
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
903
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
904
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
901
905
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
902
906
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
903
907
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -1233,6 +1237,7 @@ ${detail}`,
1233
1237
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
1234
1238
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1235
1239
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1240
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
1236
1241
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1237
1242
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1238
1243
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -1990,6 +1995,9 @@ var init_channel_cli2 = __esm(() => {
1990
1995
  channelAdded: (type) => `频道 ${type} 已添加`,
1991
1996
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
1992
1997
  channelRemoved: (id) => `频道 ${id} 已删除`,
1998
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
1999
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
2000
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
1993
2001
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
1994
2002
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
1995
2003
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -4689,6 +4697,7 @@ function parseConfig(raw, options = {}) {
4689
4697
  ...typeof transport.command === "string" ? { command: transport.command } : {},
4690
4698
  ...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
4691
4699
  ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
4700
+ ...typeof transport.preferLocalAgents === "boolean" ? { preferLocalAgents: transport.preferLocalAgents } : {},
4692
4701
  type: transportType,
4693
4702
  permissionMode,
4694
4703
  nonInteractivePermissions,
@@ -20393,6 +20402,9 @@ function parseCommand(input) {
20393
20402
  if (command === "/session" && parts[1] === "rm" && parts[2] && parts.length === 3) {
20394
20403
  return { kind: "session.rm", alias: parts[2] };
20395
20404
  }
20405
+ if (command === "/session" && parts[1] === "archive" && parts[2] && parts.length === 3) {
20406
+ return { kind: "session.archive", alias: parts[2] };
20407
+ }
20396
20408
  if (command === "/ssn") {
20397
20409
  if (parts.length === 1) {
20398
20410
  return { kind: "session.native.list" };
@@ -20639,7 +20651,7 @@ function parseCommand(input) {
20639
20651
  return { kind: "session.shortcut.new", agent: parts[2], ...shortcutTarget };
20640
20652
  }
20641
20653
  }
20642
- if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset" && parts[1] !== "rm") {
20654
+ if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset" && parts[1] !== "rm" && parts[1] !== "archive") {
20643
20655
  const shortcutTarget = readSessionShortcutTarget(parts, 2);
20644
20656
  if (shortcutTarget) {
20645
20657
  return { kind: "session.shortcut", agent: parts[1], ...shortcutTarget };
@@ -21103,6 +21115,7 @@ var init_command_policy = __esm(() => {
21103
21115
  COMMAND_KIND_TO_LABEL = {
21104
21116
  "session.reset": "/clear",
21105
21117
  "session.rm": "/session rm",
21118
+ "session.archive": "/session archive",
21106
21119
  "session.tail": "/session tail",
21107
21120
  "replymode.set": "/replymode",
21108
21121
  "replymode.reset": "/replymode reset",
@@ -22320,9 +22333,9 @@ async function handleSessionRemove(context, chatKey, alias) {
22320
22333
  }
22321
22334
  let transportTeardownWarning;
22322
22335
  const shouldTeardownTransport = sharedAliasCount === 0;
22323
- if (shouldTeardownTransport && context.transport.removeSession) {
22336
+ if (shouldTeardownTransport && context.transport.deleteSession) {
22324
22337
  try {
22325
- await context.transport.removeSession(session3);
22338
+ await context.transport.deleteSession(session3);
22326
22339
  } catch (error2) {
22327
22340
  const message = error2 instanceof Error ? error2.message : String(error2);
22328
22341
  transportTeardownWarning = message;
@@ -22355,7 +22368,19 @@ async function handleSessionRemove(context, chatKey, alias) {
22355
22368
  return { text: lines.join(`
22356
22369
  `) };
22357
22370
  }
22371
+ async function handleSessionArchive(context, chatKey, alias, archive) {
22372
+ const internalAlias = await context.sessions.resolveAliasForChat(chatKey, alias);
22373
+ const session3 = await context.sessions.getSession(internalAlias);
22374
+ if (!session3) {
22375
+ return { text: t().session.sessionNotFound(alias) };
22376
+ }
22377
+ await archive(internalAlias);
22378
+ return { text: t().session.sessionArchived(alias) };
22379
+ }
22358
22380
  async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22381
+ if (session3.archived) {
22382
+ await context.sessions.setArchived(session3.alias, false);
22383
+ }
22359
22384
  const effectiveReplyMode = resolveEffectiveReplyMode(context.config, chatKey, session3.replyMode);
22360
22385
  if (!session3.replyMode)
22361
22386
  session3.replyMode = effectiveReplyMode;
@@ -24789,6 +24814,8 @@ class CommandRouter {
24789
24814
  return await handleSessionTail(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.lines);
24790
24815
  case "session.rm":
24791
24816
  return await handleSessionRemove(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
24817
+ case "session.archive":
24818
+ return await handleSessionArchive(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias, (internalAlias) => this.archiveSessionWithTransport(internalAlias));
24792
24819
  case "groups":
24793
24820
  return await handleGroupList(this.createHandlerContext(), chatKey, command.filter);
24794
24821
  case "group.new":
@@ -24937,6 +24964,82 @@ class CommandRouter {
24937
24964
  await release();
24938
24965
  }
24939
24966
  }
24967
+ async removeSessionWithTransport(internalAlias) {
24968
+ const session3 = await this.sessions.getSession(internalAlias);
24969
+ if (!session3) {
24970
+ throw new Error(`session "${internalAlias}" does not exist`);
24971
+ }
24972
+ if (this.orchestration) {
24973
+ const blocking = await this.orchestration.listSessionBlockingTasks(session3.transportSession);
24974
+ if (blocking.length > 0) {
24975
+ throw new Error(`session "${internalAlias}" has ${blocking.length} blocking task(s); cancel them before deleting`);
24976
+ }
24977
+ }
24978
+ const sharedAliasCount = this.sessions.countAliasesSharingTransport(session3.transportSession, internalAlias);
24979
+ const { wasActive } = await this.sessions.removeSession(internalAlias);
24980
+ if (this.orchestration) {
24981
+ try {
24982
+ await this.orchestration.purgeSessionReferences(session3.transportSession);
24983
+ } catch (error2) {
24984
+ await this.logger.error("session.orchestration_purge_failed", "failed to purge orchestration references after web remove", {
24985
+ alias: internalAlias,
24986
+ transportSession: session3.transportSession,
24987
+ message: error2 instanceof Error ? error2.message : String(error2)
24988
+ });
24989
+ }
24990
+ }
24991
+ let transportTornDown = false;
24992
+ let transportTeardownWarning;
24993
+ if (sharedAliasCount === 0 && this.transport.deleteSession) {
24994
+ try {
24995
+ await this.transport.deleteSession(session3);
24996
+ transportTornDown = true;
24997
+ } catch (error2) {
24998
+ transportTeardownWarning = error2 instanceof Error ? error2.message : String(error2);
24999
+ await this.logger.error("session.transport_delete_failed", "failed to delete acpx session after logical remove", {
25000
+ alias: internalAlias,
25001
+ transportSession: session3.transportSession,
25002
+ message: transportTeardownWarning
25003
+ });
25004
+ }
25005
+ }
25006
+ return {
25007
+ wasActive,
25008
+ sharedAliasCount,
25009
+ transportTornDown,
25010
+ ...transportTeardownWarning ? { transportTeardownWarning } : {}
25011
+ };
25012
+ }
25013
+ async archiveSessionWithTransport(internalAlias) {
25014
+ const session3 = await this.sessions.getSession(internalAlias);
25015
+ if (!session3) {
25016
+ throw new Error(`session "${internalAlias}" does not exist`);
25017
+ }
25018
+ if (this.activeTurns?.isActiveAnywhere(internalAlias)) {
25019
+ throw new Error(`session "${internalAlias}" has a running turn; stop it before archiving`);
25020
+ }
25021
+ const shared = this.sessions.countAliasesSharingTransport(session3.transportSession, internalAlias) > 0;
25022
+ if (!shared) {
25023
+ try {
25024
+ await this.transport.cancel(session3);
25025
+ } catch {}
25026
+ if (this.transport.removeSession) {
25027
+ try {
25028
+ await this.transport.removeSession(session3);
25029
+ } catch (error2) {
25030
+ await this.logger.error("session.archive_close_failed", "failed to close acpx session on archive", {
25031
+ alias: internalAlias,
25032
+ transportSession: session3.transportSession,
25033
+ message: error2 instanceof Error ? error2.message : String(error2)
25034
+ });
25035
+ }
25036
+ }
25037
+ }
25038
+ await this.sessions.setArchived(internalAlias, true);
25039
+ }
25040
+ async unarchiveSession(internalAlias) {
25041
+ await this.sessions.setArchived(internalAlias, false);
25042
+ }
24940
25043
  async listNativeSessionsForControl(agent3, workspace3) {
24941
25044
  const listAgentSessions = this.transport.listAgentSessions?.bind(this.transport);
24942
25045
  if (!listAgentSessions)
@@ -29653,6 +29756,10 @@ class SessionService {
29653
29756
  const previousCurrent = prevCtx?.current_session;
29654
29757
  const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
29655
29758
  session3.last_used_at = new Date().toISOString();
29759
+ if (session3.archived) {
29760
+ delete session3.archived;
29761
+ delete session3.archived_at;
29762
+ }
29656
29763
  const nextCtx = { ...prevCtx, current_session: internalAlias };
29657
29764
  if (carriedPrevious) {
29658
29765
  nextCtx.previous_session = carriedPrevious;
@@ -29854,6 +29961,22 @@ class SessionService {
29854
29961
  }
29855
29962
  return count;
29856
29963
  }
29964
+ async setArchived(alias, archived) {
29965
+ await this.mutate(async () => {
29966
+ const session3 = this.state.sessions[alias];
29967
+ if (!session3) {
29968
+ throw new Error(`session "${alias}" does not exist`);
29969
+ }
29970
+ if (archived) {
29971
+ session3.archived = true;
29972
+ session3.archived_at = new Date(this.now()).toISOString();
29973
+ } else {
29974
+ delete session3.archived;
29975
+ delete session3.archived_at;
29976
+ }
29977
+ await this.persist();
29978
+ });
29979
+ }
29857
29980
  async removeSession(alias) {
29858
29981
  return await this.mutate(async () => {
29859
29982
  const session3 = this.state.sessions[alias];
@@ -29968,7 +30091,8 @@ class SessionService {
29968
30091
  modeId: session3.mode_id,
29969
30092
  replyMode: session3.reply_mode,
29970
30093
  effectiveReplyMode,
29971
- cwd: workspaceConfig.cwd
30094
+ cwd: workspaceConfig.cwd,
30095
+ archived: session3.archived === true
29972
30096
  };
29973
30097
  }
29974
30098
  async setSessionModel(alias, modelId) {
@@ -31385,6 +31509,9 @@ ${result.text}` : "" };
31385
31509
  async removeSession(session3) {
31386
31510
  await this.client.request("removeSession", this.toParams(session3));
31387
31511
  }
31512
+ async deleteSession(session3) {
31513
+ await this.client.request("deleteSession", this.toParams(session3));
31514
+ }
31388
31515
  async getAgentSessionId(session3) {
31389
31516
  const result = await this.client.request("getAgentSessionId", this.toParams(session3));
31390
31517
  return result.agentSessionId;
@@ -31953,6 +32080,31 @@ var init_agent_session_list = __esm(() => {
31953
32080
  init_path();
31954
32081
  });
31955
32082
 
32083
+ // src/transport/acpx-session-files.ts
32084
+ import { readdir as readdir3, unlink as unlink2 } from "node:fs/promises";
32085
+ import { homedir as homedir9 } from "node:os";
32086
+ import { join as join19 } from "node:path";
32087
+ async function deleteAcpxSessionFiles(options) {
32088
+ const dir = options.sessionsDir ?? join19(homedir9(), ".acpx", "sessions");
32089
+ const safeId = encodeURIComponent(options.acpxRecordId);
32090
+ await unlink2(join19(dir, `${safeId}.json`)).catch(() => {
32091
+ return;
32092
+ });
32093
+ let entries;
32094
+ try {
32095
+ entries = await readdir3(dir);
32096
+ } catch {
32097
+ return;
32098
+ }
32099
+ const streamFiles = entries.filter((name) => name.startsWith(`${safeId}.stream.`));
32100
+ for (const name of streamFiles) {
32101
+ await unlink2(join19(dir, name)).catch(() => {
32102
+ return;
32103
+ });
32104
+ }
32105
+ }
32106
+ var init_acpx_session_files = () => {};
32107
+
31956
32108
  // src/transport/acpx-cli/acpx-cli-transport.ts
31957
32109
  import { createRequire as createRequire5 } from "node:module";
31958
32110
  import { spawn as spawn9 } from "node:child_process";
@@ -32211,6 +32363,16 @@ ${baseText}` : "" };
32211
32363
  const detail = normalizeCommandError(result) ?? `command failed with exit code ${result.code}`;
32212
32364
  throw new Error(detail);
32213
32365
  }
32366
+ async deleteSession(session3) {
32367
+ let acpxRecordId;
32368
+ try {
32369
+ ({ acpxRecordId } = await this.readSessionRecord(session3));
32370
+ } catch {
32371
+ return;
32372
+ }
32373
+ await this.removeSession(session3);
32374
+ await deleteAcpxSessionFiles({ acpxRecordId });
32375
+ }
32214
32376
  async hasSession(session3) {
32215
32377
  const result = await this.runCommand(this.command, this.buildArgs(session3, [
32216
32378
  "sessions",
@@ -32247,7 +32409,7 @@ ${baseText}` : "" };
32247
32409
  const parsed = JSON.parse(result.stdout);
32248
32410
  const acpxRecordId = typeof parsed.acpxRecordId === "string" ? parsed.acpxRecordId : typeof parsed.id === "string" ? parsed.id : undefined;
32249
32411
  const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
32250
- if (acpxRecordId) {
32412
+ if (acpxRecordId && /^[\w.:-]+$/.test(acpxRecordId) && acpxRecordId.length >= 8) {
32251
32413
  return { acpxRecordId, agentSessionId };
32252
32414
  }
32253
32415
  } catch {
@@ -32526,6 +32688,7 @@ var init_acpx_cli_transport = __esm(() => {
32526
32688
  init_terminate_process_tree();
32527
32689
  init_acpx_queue_owner_launcher();
32528
32690
  init_agent_session_list();
32691
+ init_acpx_session_files();
32529
32692
  require4 = createRequire5(import.meta.url);
32530
32693
  });
32531
32694
 
@@ -32997,8 +33160,8 @@ function createControlEventBus(logger2) {
32997
33160
 
32998
33161
  // src/transport/native-session-history.ts
32999
33162
  import { readFile as readFile14 } from "node:fs/promises";
33000
- import { homedir as homedir9 } from "node:os";
33001
- import { join as join19 } from "node:path";
33163
+ import { homedir as homedir10 } from "node:os";
33164
+ import { join as join20 } from "node:path";
33002
33165
  function classifyToolKind(name) {
33003
33166
  const n = name.toLowerCase();
33004
33167
  if (/(^|[^a-z])(read|cat|view|open)([^a-z]|$)/.test(n))
@@ -33110,8 +33273,8 @@ function mapAcpxMessagesToHistory(raw) {
33110
33273
  }
33111
33274
  async function readNativeSessionHistory(opts) {
33112
33275
  try {
33113
- const dir = opts.sessionsDir ?? join19(opts.homeDir ?? homedir9(), ".acpx", "sessions");
33114
- const indexRaw = await readFile14(join19(dir, "index.json"), "utf8").catch(() => null);
33276
+ const dir = opts.sessionsDir ?? join20(opts.homeDir ?? homedir10(), ".acpx", "sessions");
33277
+ const indexRaw = await readFile14(join20(dir, "index.json"), "utf8").catch(() => null);
33115
33278
  if (!indexRaw)
33116
33279
  return [];
33117
33280
  const index = JSON.parse(indexRaw);
@@ -33120,7 +33283,7 @@ async function readNativeSessionHistory(opts) {
33120
33283
  for (const entry of candidates) {
33121
33284
  if (!entry.file)
33122
33285
  continue;
33123
- const recRaw = await readFile14(join19(dir, entry.file), "utf8").catch(() => null);
33286
+ const recRaw = await readFile14(join20(dir, entry.file), "utf8").catch(() => null);
33124
33287
  if (!recRaw)
33125
33288
  continue;
33126
33289
  const record3 = JSON.parse(recRaw);
@@ -33138,14 +33301,14 @@ var init_native_session_history = () => {};
33138
33301
  // src/control/workspace-fs.ts
33139
33302
  import { execFile } from "node:child_process";
33140
33303
  import { promisify } from "node:util";
33141
- import { homedir as homedir10 } from "node:os";
33142
- import { readdir as readdir3, realpath, stat as stat3, open as open5 } from "node:fs/promises";
33304
+ import { homedir as homedir11 } from "node:os";
33305
+ import { readdir as readdir4, realpath, stat as stat3, open as open5 } from "node:fs/promises";
33143
33306
  import { isAbsolute as isAbsolute3, relative, resolve as resolve3, sep } from "node:path";
33144
33307
  function expandHome2(p) {
33145
33308
  if (p === "~")
33146
- return homedir10();
33309
+ return homedir11();
33147
33310
  if (p.startsWith("~/") || p.startsWith("~" + sep))
33148
- return resolve3(homedir10(), p.slice(2));
33311
+ return resolve3(homedir11(), p.slice(2));
33149
33312
  return p;
33150
33313
  }
33151
33314
 
@@ -33180,7 +33343,7 @@ class WorkspaceFs {
33180
33343
  }
33181
33344
  async listDirectory(workspace3, relPath) {
33182
33345
  const { abs, rel } = await this.resolve(workspace3, relPath);
33183
- const dirents = await readdir3(abs, { withFileTypes: true });
33346
+ const dirents = await readdir4(abs, { withFileTypes: true });
33184
33347
  const entries = [];
33185
33348
  for (const d of dirents.slice(0, MAX_ENTRIES)) {
33186
33349
  if (d.isDirectory()) {
@@ -33234,7 +33397,7 @@ class WorkspaceFs {
33234
33397
  const dir = queue.shift();
33235
33398
  let dirents;
33236
33399
  try {
33237
- dirents = await readdir3(dir, { withFileTypes: true });
33400
+ dirents = await readdir4(dir, { withFileTypes: true });
33238
33401
  } catch {
33239
33402
  continue;
33240
33403
  }
@@ -33383,7 +33546,8 @@ class ControlService {
33383
33546
  agent: session3.agent,
33384
33547
  workspace: session3.workspace,
33385
33548
  transportSession: session3.transportSession,
33386
- running: this.deps.activeTurns.isActiveAnywhere(session3.alias)
33549
+ running: this.deps.activeTurns.isActiveAnywhere(session3.alias),
33550
+ archived: session3.archived === true
33387
33551
  }));
33388
33552
  }
33389
33553
  async listNativeSessions(_chatKey, agent3, workspace3) {
@@ -33413,15 +33577,26 @@ class ControlService {
33413
33577
  agent: session3.agent,
33414
33578
  workspace: session3.workspace,
33415
33579
  transportSession: session3.transportSession,
33416
- running: false
33580
+ running: false,
33581
+ archived: false
33417
33582
  };
33418
33583
  }
33419
33584
  async removeSession(chatKey, alias) {
33420
33585
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
33421
- const result = await this.deps.sessions.removeSession(internalAlias);
33586
+ const result = await this.deps.removeSessionWithTransport(internalAlias);
33422
33587
  this.deps.events.emit({ type: "sessions-changed" });
33423
33588
  return result;
33424
33589
  }
33590
+ async archiveSession(chatKey, alias) {
33591
+ const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
33592
+ await this.deps.archiveSessionWithTransport(internalAlias);
33593
+ this.deps.events.emit({ type: "sessions-changed" });
33594
+ }
33595
+ async unarchiveSession(chatKey, alias) {
33596
+ const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
33597
+ await this.deps.unarchiveSession(internalAlias);
33598
+ this.deps.events.emit({ type: "sessions-changed" });
33599
+ }
33425
33600
  listAgents() {
33426
33601
  return this.deps.agents.list();
33427
33602
  }
@@ -33686,7 +33861,7 @@ var init_control_service = __esm(() => {
33686
33861
 
33687
33862
  // src/config/agent-catalog.ts
33688
33863
  import { existsSync as existsSync3 } from "node:fs";
33689
- import { delimiter as delimiter2, join as join20 } from "node:path";
33864
+ import { delimiter as delimiter2, join as join21 } from "node:path";
33690
33865
  function isBinaryOnPath(binary) {
33691
33866
  const path15 = process.env.PATH ?? "";
33692
33867
  const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
@@ -33695,7 +33870,7 @@ function isBinaryOnPath(binary) {
33695
33870
  continue;
33696
33871
  for (const ext of exts) {
33697
33872
  try {
33698
- if (existsSync3(join20(dir, binary + ext)))
33873
+ if (existsSync3(join21(dir, binary + ext)))
33699
33874
  return true;
33700
33875
  } catch {}
33701
33876
  }
@@ -33735,8 +33910,8 @@ __export(exports_main, {
33735
33910
  buildApp: () => buildApp
33736
33911
  });
33737
33912
  import { randomUUID as randomUUID3 } from "node:crypto";
33738
- import { homedir as homedir11 } from "node:os";
33739
- import { dirname as dirname12, join as join21 } from "node:path";
33913
+ import { homedir as homedir12 } from "node:os";
33914
+ import { dirname as dirname12, join as join22 } from "node:path";
33740
33915
  import { fileURLToPath as fileURLToPath5 } from "node:url";
33741
33916
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
33742
33917
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -34234,6 +34409,9 @@ async function buildApp(paths, deps = {}) {
34234
34409
  createSessionWithTransport: (internalAlias, agent4, workspace3, model) => router3.createSessionWithTransport(internalAlias, agent4, workspace3, model),
34235
34410
  listNativeSessions: (agent4, workspace3) => router3.listNativeSessionsForControl(agent4, workspace3),
34236
34411
  attachNativeSessionWithTransport: (internalAlias, agent4, workspace3, agentSessionId, nativeMeta) => router3.attachNativeSessionWithTransport(internalAlias, agent4, workspace3, agentSessionId, nativeMeta),
34412
+ removeSessionWithTransport: (internalAlias) => router3.removeSessionWithTransport(internalAlias),
34413
+ archiveSessionWithTransport: (internalAlias) => router3.archiveSessionWithTransport(internalAlias),
34414
+ unarchiveSession: (internalAlias) => router3.unarchiveSession(internalAlias),
34237
34415
  activeTurns,
34238
34416
  scheduled: scheduledService,
34239
34417
  orchestration: orchestration3,
@@ -34396,8 +34574,8 @@ async function main() {
34396
34574
  }
34397
34575
  }
34398
34576
  async function prepareChannelMedia(configPath, config4) {
34399
- const runtimeDir = join21(dirname12(configPath), "runtime");
34400
- const mediaRootDir = join21(runtimeDir, "media");
34577
+ const runtimeDir = join22(dirname12(configPath), "runtime");
34578
+ const mediaRootDir = join22(runtimeDir, "media");
34401
34579
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
34402
34580
  await mediaStore.cleanupExpired().catch((error2) => {
34403
34581
  console.error("[xacpx] media cleanup failed:", error2 instanceof Error ? error2.message : String(error2));
@@ -34406,16 +34584,16 @@ async function prepareChannelMedia(configPath, config4) {
34406
34584
  return { mediaStore, channelDeps: { mediaStore, allowedMediaRoots } };
34407
34585
  }
34408
34586
  function resolveRuntimePaths() {
34409
- const home = process.env.HOME ?? homedir11();
34587
+ const home = process.env.HOME ?? homedir12();
34410
34588
  if (!home) {
34411
34589
  throw new Error("Unable to resolve the current user home directory");
34412
34590
  }
34413
- const configPath = coreEnv("CONFIG") ?? join21(coreHomeDir(home), "config.json");
34414
- const runtimeDir = join21(dirname12(configPath), "runtime");
34591
+ const configPath = coreEnv("CONFIG") ?? join22(coreHomeDir(home), "config.json");
34592
+ const runtimeDir = join22(dirname12(configPath), "runtime");
34415
34593
  return {
34416
34594
  configPath,
34417
- statePath: coreEnv("STATE") ?? join21(coreHomeDir(home), "state.json"),
34418
- perfLogPath: join21(runtimeDir, "perf.log"),
34595
+ statePath: coreEnv("STATE") ?? join22(coreHomeDir(home), "state.json"),
34596
+ perfLogPath: join22(runtimeDir, "perf.log"),
34419
34597
  orchestrationSocketPath: coreEnv("ORCHESTRATION_SOCKET") ?? resolveDaemonOrchestrationSocketPath(runtimeDir)
34420
34598
  };
34421
34599
  }
@@ -34427,13 +34605,13 @@ function resolveBridgeEntryPath() {
34427
34605
  }
34428
34606
  function resolveAppLogPath(configPath) {
34429
34607
  const rootDir = dirname12(configPath);
34430
- const runtimeDir = join21(rootDir, "runtime");
34431
- return join21(runtimeDir, "app.log");
34608
+ const runtimeDir = join22(rootDir, "runtime");
34609
+ return join22(runtimeDir, "app.log");
34432
34610
  }
34433
34611
  function resolvePerfLogPath(configPath) {
34434
34612
  const rootDir = dirname12(configPath);
34435
- const runtimeDir = join21(rootDir, "runtime");
34436
- return join21(runtimeDir, "perf.log");
34613
+ const runtimeDir = join22(rootDir, "runtime");
34614
+ return join22(runtimeDir, "perf.log");
34437
34615
  }
34438
34616
  function resolveOrchestrationSocketPathFromConfigPath(configPath) {
34439
34617
  const runtimeDir = resolveRuntimeDirFromConfigPath(configPath);
@@ -34671,12 +34849,12 @@ var init_config_check = __esm(async () => {
34671
34849
  });
34672
34850
 
34673
34851
  // src/doctor/checks/daemon-check.ts
34674
- import { readdir as readdir4, readFile as readFile15, rm as rm10 } from "node:fs/promises";
34852
+ import { readdir as readdir5, readFile as readFile15, rm as rm10 } from "node:fs/promises";
34675
34853
  import { fileURLToPath as fileURLToPath6 } from "node:url";
34676
- import { homedir as homedir12 } from "node:os";
34677
- import { join as join22 } from "node:path";
34854
+ import { homedir as homedir13 } from "node:os";
34855
+ import { join as join23 } from "node:path";
34678
34856
  async function checkDaemon(options = {}) {
34679
- const home = options.home ?? process.env.HOME ?? homedir12();
34857
+ const home = options.home ?? process.env.HOME ?? homedir13();
34680
34858
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
34681
34859
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
34682
34860
  home,
@@ -34776,7 +34954,7 @@ async function detectStaleConsumerLockFix(runtimeDir, deps) {
34776
34954
  if (!fileName.endsWith(CONSUMER_LOCK_SUFFIX)) {
34777
34955
  continue;
34778
34956
  }
34779
- const lockPath = join22(runtimeDir, fileName);
34957
+ const lockPath = join23(runtimeDir, fileName);
34780
34958
  const lock2 = await deps.readConsumerLock(lockPath);
34781
34959
  if (lock2 && !deps.isProcessRunning(lock2.pid)) {
34782
34960
  stalePaths.push(lockPath);
@@ -34810,7 +34988,7 @@ async function detectStaleConsumerLockFix(runtimeDir, deps) {
34810
34988
  }
34811
34989
  async function defaultListConsumerLocks(runtimeDir) {
34812
34990
  try {
34813
- return await readdir4(runtimeDir);
34991
+ return await readdir5(runtimeDir);
34814
34992
  } catch {
34815
34993
  return [];
34816
34994
  }
@@ -34840,11 +35018,11 @@ var init_daemon_check = __esm(() => {
34840
35018
  });
34841
35019
 
34842
35020
  // src/doctor/checks/logs-check.ts
34843
- import { stat as stat4, readdir as readdir5 } from "node:fs/promises";
34844
- import { basename as basename3, join as join23 } from "node:path";
34845
- import { homedir as homedir13 } from "node:os";
35021
+ import { stat as stat4, readdir as readdir6 } from "node:fs/promises";
35022
+ import { basename as basename3, join as join24 } from "node:path";
35023
+ import { homedir as homedir14 } from "node:os";
34846
35024
  async function checkLogs(options = {}) {
34847
- const home = options.home ?? process.env.HOME ?? homedir13();
35025
+ const home = options.home ?? process.env.HOME ?? homedir14();
34848
35026
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
34849
35027
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
34850
35028
  home,
@@ -34876,7 +35054,7 @@ async function checkLogs(options = {}) {
34876
35054
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
34877
35055
  const files = [];
34878
35056
  for (const name of matched) {
34879
- const path15 = join23(paths.runtimeDir, name);
35057
+ const path15 = join24(paths.runtimeDir, name);
34880
35058
  try {
34881
35059
  const fileStat = await probe.stat(path15);
34882
35060
  if (fileStat.isDirectory()) {
@@ -34957,7 +35135,7 @@ function formatBytes(bytes) {
34957
35135
  function createLogsFsProbe() {
34958
35136
  return {
34959
35137
  stat: async (path15) => await stat4(path15),
34960
- readdir: async (path15) => await readdir5(path15)
35138
+ readdir: async (path15) => await readdir6(path15)
34961
35139
  };
34962
35140
  }
34963
35141
  function formatError6(error2) {
@@ -35026,9 +35204,9 @@ var init_orchestration_health = __esm(() => {
35026
35204
  });
35027
35205
 
35028
35206
  // src/doctor/checks/orchestration-socket-check.ts
35029
- import { homedir as homedir14 } from "node:os";
35207
+ import { homedir as homedir15 } from "node:os";
35030
35208
  async function checkOrchestrationSocket(options = {}) {
35031
- const home = options.home ?? process.env.HOME ?? homedir14();
35209
+ const home = options.home ?? process.env.HOME ?? homedir15();
35032
35210
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
35033
35211
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
35034
35212
  home,
@@ -35203,9 +35381,9 @@ var init_plugin_check = __esm(async () => {
35203
35381
  import { constants } from "node:fs";
35204
35382
  import { access as access4, stat as stat5 } from "node:fs/promises";
35205
35383
  import { dirname as dirname13 } from "node:path";
35206
- import { homedir as homedir15 } from "node:os";
35384
+ import { homedir as homedir16 } from "node:os";
35207
35385
  async function checkRuntime(options = {}) {
35208
- const home = options.home ?? process.env.HOME ?? homedir15();
35386
+ const home = options.home ?? process.env.HOME ?? homedir16();
35209
35387
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
35210
35388
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
35211
35389
  home,
@@ -35838,10 +36016,10 @@ var init_render_doctor = __esm(() => {
35838
36016
  });
35839
36017
 
35840
36018
  // src/doctor/doctor.ts
35841
- import { homedir as homedir16 } from "node:os";
35842
- import { join as join24 } from "node:path";
36019
+ import { homedir as homedir17 } from "node:os";
36020
+ import { join as join25 } from "node:path";
35843
36021
  async function runDoctor(options = {}, deps = {}) {
35844
- const home = deps.home ?? process.env.HOME ?? homedir16();
36022
+ const home = deps.home ?? process.env.HOME ?? homedir17();
35845
36023
  const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
35846
36024
  const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
35847
36025
  const runners = [
@@ -35994,8 +36172,8 @@ function resolveDoctorRuntimePaths(home, resolver) {
35994
36172
  return resolveRuntimePaths();
35995
36173
  }
35996
36174
  return {
35997
- configPath: join24(coreHomeDir(home), "config.json"),
35998
- statePath: join24(coreHomeDir(home), "state.json")
36175
+ configPath: join25(coreHomeDir(home), "config.json"),
36176
+ statePath: join25(coreHomeDir(home), "state.json")
35999
36177
  };
36000
36178
  }
36001
36179
  function depsUseExplicitRuntimeOverrides() {
@@ -36178,8 +36356,8 @@ var init_doctor2 = __esm(async () => {
36178
36356
  // src/cli.ts
36179
36357
  init_core_home();
36180
36358
  import { randomUUID as randomUUID4 } from "node:crypto";
36181
- import { homedir as homedir17 } from "node:os";
36182
- import { dirname as dirname14, join as join25, sep as sep2 } from "node:path";
36359
+ import { homedir as homedir18 } from "node:os";
36360
+ import { dirname as dirname14, join as join26, sep as sep2 } from "node:path";
36183
36361
  import { fileURLToPath as fileURLToPath7 } from "node:url";
36184
36362
 
36185
36363
  // src/runtime/migrate-core-home.ts
@@ -50901,6 +51079,7 @@ async function removeChannel(type, rawArgs, deps) {
50901
51079
  deps.print(restartFlags.message);
50902
51080
  return 1;
50903
51081
  }
51082
+ const keepCredentials = restartFlags.rest.includes("--keep-credentials");
50904
51083
  const config4 = await deps.loadConfig();
50905
51084
  ensureChannelsArray(config4);
50906
51085
  const channel = findChannel(config4.channels, type);
@@ -50915,6 +51094,16 @@ async function removeChannel(type, rawArgs, deps) {
50915
51094
  config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
50916
51095
  await deps.saveChannels(config4.channels);
50917
51096
  deps.print(t().channelCli.channelRemoved(channel.id));
51097
+ if (keepCredentials) {
51098
+ deps.print(t().channelCli.channelCredentialsKept(channel.id));
51099
+ } else if (deps.clearChannelCredentials) {
51100
+ try {
51101
+ await deps.clearChannelCredentials(channel);
51102
+ deps.print(t().channelCli.channelCredentialsCleared(channel.id));
51103
+ } catch (error2) {
51104
+ deps.print(t().channelCli.channelCredentialsClearFailed(channel.id, error2 instanceof Error ? error2.message : String(error2)));
51105
+ }
51106
+ }
50918
51107
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
50919
51108
  }
50920
51109
  async function setChannelEnabled(type, enabled, rawArgs, deps) {
@@ -52422,7 +52611,7 @@ async function createCliScheduledTaskService() {
52422
52611
  return new ScheduledTaskService(state, stateStore);
52423
52612
  }
52424
52613
  function resolveConfigPathForCurrentEnv() {
52425
- return coreEnv("CONFIG") ?? join25(coreHomeDir(requireHome2()), "config.json");
52614
+ return coreEnv("CONFIG") ?? join26(coreHomeDir(requireHome2()), "config.json");
52426
52615
  }
52427
52616
  function resolveDaemonPathsForCurrentConfig() {
52428
52617
  const configPath = resolveConfigPathForCurrentEnv();
@@ -52640,7 +52829,11 @@ async function createChannelCliDeps(input) {
52640
52829
  return { state: "indeterminate", pid: status.pid, reason: status.reason };
52641
52830
  return { state: "stopped" };
52642
52831
  },
52643
- restartDaemon: async () => await restartDaemonCli(controller, input.print)
52832
+ restartDaemon: async () => await restartDaemonCli(controller, input.print),
52833
+ clearChannelCredentials: async (channel) => {
52834
+ const { createMessageChannel: createMessageChannel2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
52835
+ createMessageChannel2(channel.type, channel).logout();
52836
+ }
52644
52837
  };
52645
52838
  return { ...base, ...input.overrides };
52646
52839
  }
@@ -52769,7 +52962,7 @@ function decodeFirstRunOnboarding(raw) {
52769
52962
  return null;
52770
52963
  }
52771
52964
  function requireHome2() {
52772
- const home = process.env.HOME ?? homedir17();
52965
+ const home = process.env.HOME ?? homedir18();
52773
52966
  if (!home) {
52774
52967
  throw new Error("Unable to resolve the current user home directory");
52775
52968
  }
@@ -52793,7 +52986,7 @@ function safeDaemonLogPaths() {
52793
52986
  const configPath = resolveConfigPathForCurrentEnv();
52794
52987
  const paths = resolveDaemonPathsForCurrentConfig();
52795
52988
  return {
52796
- appLog: join25(dirname14(configPath), "runtime", "app.log"),
52989
+ appLog: join26(dirname14(configPath), "runtime", "app.log"),
52797
52990
  stderrLog: paths.stderrLog
52798
52991
  };
52799
52992
  } catch {
@@ -38,6 +38,7 @@ export declare function handleCancel(context: SessionHandlerContext, chatKey: st
38
38
  export declare function handleSessionReset(context: SessionHandlerContext, chatKey: string): Promise<RouterResponse>;
39
39
  export declare function handleSessionTail(context: SessionHandlerContext, chatKey: string, lines?: number): Promise<RouterResponse>;
40
40
  export declare function handleSessionRemove(context: SessionHandlerContext, chatKey: string, alias: string): Promise<RouterResponse>;
41
+ export declare function handleSessionArchive(context: SessionHandlerContext, chatKey: string, alias: string, archive: (internalAlias: string) => Promise<void>): Promise<RouterResponse>;
41
42
  export declare function handlePromptWithSession(context: SessionHandlerContext, session: ResolvedSession, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
42
43
  used: number;
43
44
  size: number;
@@ -64,6 +64,9 @@ export type ParsedCommand = {
64
64
  } | {
65
65
  kind: "session.rm";
66
66
  alias: string;
67
+ } | {
68
+ kind: "session.archive";
69
+ alias: string;
67
70
  } | {
68
71
  kind: "delegate.request";
69
72
  targetAgent: string;
@@ -15,6 +15,7 @@ export interface ControlSessionInfo {
15
15
  workspace: string;
16
16
  transportSession: string;
17
17
  running: boolean;
18
+ archived: boolean;
18
19
  }
19
20
  export interface ControlAgentInfo {
20
21
  name: string;
@@ -37,6 +38,11 @@ export interface ControlServiceDeps {
37
38
  sessions: Pick<SessionService, "listAllResolvedSessions" | "removeSession" | "useSession" | "resolveAliasForChat" | "getSession" | "setSessionModel">;
38
39
  transport: Pick<SessionTransport, "setModel" | "getSessionModel">;
39
40
  createSessionWithTransport: (internalAlias: string, agent: string, workspace: string, model?: string) => Promise<ResolvedSession>;
41
+ removeSessionWithTransport: (internalAlias: string) => Promise<{
42
+ wasActive: boolean;
43
+ }>;
44
+ archiveSessionWithTransport: (internalAlias: string) => Promise<void>;
45
+ unarchiveSession: (internalAlias: string) => Promise<void>;
40
46
  listNativeSessions: (agent: string, workspace: string) => Promise<AgentSession[]>;
41
47
  attachNativeSessionWithTransport: (internalAlias: string, agent: string, workspace: string, agentSessionId: string, nativeMeta?: {
42
48
  title?: string | null;
@@ -121,6 +127,8 @@ export declare class ControlService {
121
127
  removeSession(chatKey: string, alias: string): Promise<{
122
128
  wasActive: boolean;
123
129
  }>;
130
+ archiveSession(chatKey: string, alias: string): Promise<void>;
131
+ unarchiveSession(chatKey: string, alias: string): Promise<void>;
124
132
  listAgents(): ControlAgentInfo[];
125
133
  listWorkspaces(): ControlWorkspaceInfo[];
126
134
  createWorkspace(name: string, cwd: string, description?: string): Promise<ControlWorkspaceInfo>;
@@ -50,6 +50,7 @@ export interface SessionMessages {
50
50
  sessionBlockedByTasks: (alias: string, count: number) => string;
51
51
  sessionBlockedByTasksHint: string;
52
52
  sessionRemoved: (alias: string) => string;
53
+ sessionArchived: (alias: string) => string;
53
54
  sessionRemovedWasActive: string;
54
55
  sessionRemovedWasActivePromoted: (alias: string) => string;
55
56
  sessionTransportShared: (transportSession: string, count: number) => string;
@@ -672,6 +673,9 @@ export interface ChannelCliMessages {
672
673
  channelAdded: (type: string) => string;
673
674
  cannotRemoveLastEnabled: string;
674
675
  channelRemoved: (id: string) => string;
676
+ channelCredentialsCleared: (id: string) => string;
677
+ channelCredentialsClearFailed: (id: string, error: string) => string;
678
+ channelCredentialsKept: (id: string) => string;
675
679
  cannotDisableLastEnabled: string;
676
680
  channelEnabledToggled: (id: string, enabled: boolean) => string;
677
681
  channelReplyModeSet: (id: string, mode: string) => string;
@@ -116,6 +116,7 @@ ${detail}`,
116
116
  sessionBlockedByTasks: (alias, count) => `Session "${alias}" has ${count} unfinished task(s). Cancel or wait for them to complete first.`,
117
117
  sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
118
118
  sessionRemoved: (alias) => `Session "${alias}" removed.`,
119
+ sessionArchived: (alias) => `Archived session "${alias}". Send a message to restore it.`,
119
120
  sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
120
121
  sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
121
122
  sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
@@ -873,6 +874,9 @@ var init_channel_cli = __esm(() => {
873
874
  channelAdded: (type) => `Channel ${type} added`,
874
875
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
875
876
  channelRemoved: (id) => `Channel ${id} removed`,
877
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
878
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
879
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
876
880
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
877
881
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
878
882
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -1208,6 +1212,7 @@ ${detail}`,
1208
1212
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
1209
1213
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1210
1214
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1215
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
1211
1216
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1212
1217
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1213
1218
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -1965,6 +1970,9 @@ var init_channel_cli2 = __esm(() => {
1965
1970
  channelAdded: (type) => `频道 ${type} 已添加`,
1966
1971
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
1967
1972
  channelRemoved: (id) => `频道 ${id} 已删除`,
1973
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
1974
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
1975
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
1968
1976
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
1969
1977
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
1970
1978
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -106,6 +106,7 @@ export declare class SessionService {
106
106
  getCurrentSession(chatKey: string): Promise<ResolvedSession | null>;
107
107
  listSessions(chatKey: string): Promise<SessionListItem[]>;
108
108
  countAliasesSharingTransport(transportSession: string, excludeAlias?: string): number;
109
+ setArchived(alias: string, archived: boolean): Promise<void>;
109
110
  removeSession(alias: string): Promise<{
110
111
  wasActive: boolean;
111
112
  }>;
@@ -30,6 +30,10 @@ export interface LogicalSession {
30
30
  /** Per-session LLM model override (e.g. `gpt-5.2[high]`); falls back to the agent config default. */
31
31
  model?: string;
32
32
  reply_mode?: "stream" | "final" | "verbose";
33
+ /** True when the user archived this session: process closed, row greyed + sunk.
34
+ * Cleared on the next useSession (restore-on-message). */
35
+ archived?: boolean;
36
+ archived_at?: string;
33
37
  created_at: string;
34
38
  last_used_at: string;
35
39
  }
@@ -55,6 +55,11 @@ export interface ResolvedSession {
55
55
  * persisted state by alias) does not apply.
56
56
  */
57
57
  transient?: boolean;
58
+ /**
59
+ * True when the user archived this session (process closed, greyed out in the
60
+ * web dashboard). Surfaced to the control/web path via ControlSessionInfo.
61
+ */
62
+ archived?: boolean;
58
63
  }
59
64
  export interface AgentSession {
60
65
  sessionId: string;
@@ -160,6 +165,13 @@ export interface SessionTransport {
160
165
  listAgentSessions?(query: AgentSessionListQuery): Promise<AgentSessionListResult | undefined>;
161
166
  resumeAgentSession?(session: ResolvedSession, agentSessionId: string): Promise<void>;
162
167
  removeSession?(session: ResolvedSession): Promise<void>;
168
+ /**
169
+ * Hard-delete the transport session AND its on-disk history: close the acpx
170
+ * process, then delete acpx's record files. Distinct from removeSession (=
171
+ * `acpx sessions close`, which keeps history for resume). Optional: transports
172
+ * that can't delete omit it. A missing acpx session is a no-op (idempotent).
173
+ */
174
+ deleteSession?(session: ResolvedSession): Promise<void>;
163
175
  /**
164
176
  * Read the underlying agent-native session id for an existing transport
165
177
  * session. Used by `/clear` to keep a native session native: the fresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",