@ganglion/xacpx 0.12.1 → 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.`,
@@ -2035,6 +2036,7 @@ ${detail}`,
2035
2036
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
2036
2037
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
2037
2038
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
2039
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
2038
2040
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
2039
2041
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
2040
2042
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -3424,6 +3426,31 @@ var init_agent_session_list = __esm(() => {
3424
3426
  init_path();
3425
3427
  });
3426
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
+
3427
3454
  // src/bridge/bridge-main.ts
3428
3455
  import { createInterface } from "node:readline";
3429
3456
 
@@ -3496,9 +3523,9 @@ init_terminate_process_tree();
3496
3523
  init_prompt_output();
3497
3524
  init_prompt_media();
3498
3525
  init_streaming_prompt();
3499
- import { copyFile, readdir } from "node:fs/promises";
3500
- import { homedir as homedir4 } from "node:os";
3501
- 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";
3502
3529
  import { spawn as spawn4 } from "node:child_process";
3503
3530
 
3504
3531
  // src/bridge/parse-missing-optional-dep.ts
@@ -3518,6 +3545,7 @@ function parseMissingOptionalDep(text) {
3518
3545
  init_discover_parent_package_paths();
3519
3546
  init_acpx_queue_owner_launcher();
3520
3547
  init_agent_session_list();
3548
+ init_acpx_session_files();
3521
3549
  class EnsureSessionFailedError extends Error {
3522
3550
  kind;
3523
3551
  data;
@@ -3798,7 +3826,7 @@ class BridgeRuntime {
3798
3826
  acpxRecordId = parsed.id;
3799
3827
  }
3800
3828
  const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
3801
- if (acpxRecordId) {
3829
+ if (acpxRecordId && /^[\w.:-]+$/.test(acpxRecordId) && acpxRecordId.length >= 8) {
3802
3830
  return { acpxRecordId, agentSessionId };
3803
3831
  }
3804
3832
  } catch {
@@ -3890,6 +3918,17 @@ class BridgeRuntime {
3890
3918
  }
3891
3919
  throw new Error(result.stderr || result.stdout || "sessions close failed");
3892
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
+ }
3893
3932
  async shutdown() {
3894
3933
  return {};
3895
3934
  }
@@ -4080,14 +4119,14 @@ async function tryRepairAcpxSessionIndex(deps = {}) {
4080
4119
  if (platform !== "win32") {
4081
4120
  return false;
4082
4121
  }
4083
- const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir4();
4122
+ const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir5();
4084
4123
  if (!home) {
4085
4124
  return false;
4086
4125
  }
4087
- const pathJoin = platform === "win32" ? win32.join : join3;
4126
+ const pathJoin = platform === "win32" ? win32.join : join4;
4088
4127
  const sessionsDir = pathJoin(home, ".acpx", "sessions");
4089
4128
  const indexPath = pathJoin(sessionsDir, "index.json");
4090
- const readdirFn = deps.readdirFn ?? readdir;
4129
+ const readdirFn = deps.readdirFn ?? readdir2;
4091
4130
  const copyFileFn = deps.copyFileFn ?? copyFile;
4092
4131
  let files;
4093
4132
  try {
@@ -4135,6 +4174,7 @@ var BRIDGE_METHODS = new Set([
4135
4174
  "getSessionModel",
4136
4175
  "cancel",
4137
4176
  "removeSession",
4177
+ "deleteSession",
4138
4178
  "getAgentSessionId"
4139
4179
  ]);
4140
4180
  var SESSION_SCOPED_METHODS = new Set([
@@ -4148,6 +4188,7 @@ var SESSION_SCOPED_METHODS = new Set([
4148
4188
  "getSessionModel",
4149
4189
  "cancel",
4150
4190
  "removeSession",
4191
+ "deleteSession",
4151
4192
  "getAgentSessionId"
4152
4193
  ]);
4153
4194
 
@@ -4355,6 +4396,13 @@ class BridgeServer {
4355
4396
  cwd: requireString(params, "cwd"),
4356
4397
  name: requireString(params, "name")
4357
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
+ });
4358
4406
  case "getAgentSessionId":
4359
4407
  return await this.runtime.getAgentSessionId({
4360
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.`,
@@ -1236,6 +1237,7 @@ ${detail}`,
1236
1237
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
1237
1238
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1238
1239
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1240
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
1239
1241
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1240
1242
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1241
1243
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -4695,6 +4697,7 @@ function parseConfig(raw, options = {}) {
4695
4697
  ...typeof transport.command === "string" ? { command: transport.command } : {},
4696
4698
  ...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
4697
4699
  ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
4700
+ ...typeof transport.preferLocalAgents === "boolean" ? { preferLocalAgents: transport.preferLocalAgents } : {},
4698
4701
  type: transportType,
4699
4702
  permissionMode,
4700
4703
  nonInteractivePermissions,
@@ -20399,6 +20402,9 @@ function parseCommand(input) {
20399
20402
  if (command === "/session" && parts[1] === "rm" && parts[2] && parts.length === 3) {
20400
20403
  return { kind: "session.rm", alias: parts[2] };
20401
20404
  }
20405
+ if (command === "/session" && parts[1] === "archive" && parts[2] && parts.length === 3) {
20406
+ return { kind: "session.archive", alias: parts[2] };
20407
+ }
20402
20408
  if (command === "/ssn") {
20403
20409
  if (parts.length === 1) {
20404
20410
  return { kind: "session.native.list" };
@@ -20645,7 +20651,7 @@ function parseCommand(input) {
20645
20651
  return { kind: "session.shortcut.new", agent: parts[2], ...shortcutTarget };
20646
20652
  }
20647
20653
  }
20648
- 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") {
20649
20655
  const shortcutTarget = readSessionShortcutTarget(parts, 2);
20650
20656
  if (shortcutTarget) {
20651
20657
  return { kind: "session.shortcut", agent: parts[1], ...shortcutTarget };
@@ -21109,6 +21115,7 @@ var init_command_policy = __esm(() => {
21109
21115
  COMMAND_KIND_TO_LABEL = {
21110
21116
  "session.reset": "/clear",
21111
21117
  "session.rm": "/session rm",
21118
+ "session.archive": "/session archive",
21112
21119
  "session.tail": "/session tail",
21113
21120
  "replymode.set": "/replymode",
21114
21121
  "replymode.reset": "/replymode reset",
@@ -22326,9 +22333,9 @@ async function handleSessionRemove(context, chatKey, alias) {
22326
22333
  }
22327
22334
  let transportTeardownWarning;
22328
22335
  const shouldTeardownTransport = sharedAliasCount === 0;
22329
- if (shouldTeardownTransport && context.transport.removeSession) {
22336
+ if (shouldTeardownTransport && context.transport.deleteSession) {
22330
22337
  try {
22331
- await context.transport.removeSession(session3);
22338
+ await context.transport.deleteSession(session3);
22332
22339
  } catch (error2) {
22333
22340
  const message = error2 instanceof Error ? error2.message : String(error2);
22334
22341
  transportTeardownWarning = message;
@@ -22361,7 +22368,19 @@ async function handleSessionRemove(context, chatKey, alias) {
22361
22368
  return { text: lines.join(`
22362
22369
  `) };
22363
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
+ }
22364
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
+ }
22365
22384
  const effectiveReplyMode = resolveEffectiveReplyMode(context.config, chatKey, session3.replyMode);
22366
22385
  if (!session3.replyMode)
22367
22386
  session3.replyMode = effectiveReplyMode;
@@ -24795,6 +24814,8 @@ class CommandRouter {
24795
24814
  return await handleSessionTail(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.lines);
24796
24815
  case "session.rm":
24797
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));
24798
24819
  case "groups":
24799
24820
  return await handleGroupList(this.createHandlerContext(), chatKey, command.filter);
24800
24821
  case "group.new":
@@ -24943,6 +24964,82 @@ class CommandRouter {
24943
24964
  await release();
24944
24965
  }
24945
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
+ }
24946
25043
  async listNativeSessionsForControl(agent3, workspace3) {
24947
25044
  const listAgentSessions = this.transport.listAgentSessions?.bind(this.transport);
24948
25045
  if (!listAgentSessions)
@@ -29659,6 +29756,10 @@ class SessionService {
29659
29756
  const previousCurrent = prevCtx?.current_session;
29660
29757
  const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
29661
29758
  session3.last_used_at = new Date().toISOString();
29759
+ if (session3.archived) {
29760
+ delete session3.archived;
29761
+ delete session3.archived_at;
29762
+ }
29662
29763
  const nextCtx = { ...prevCtx, current_session: internalAlias };
29663
29764
  if (carriedPrevious) {
29664
29765
  nextCtx.previous_session = carriedPrevious;
@@ -29860,6 +29961,22 @@ class SessionService {
29860
29961
  }
29861
29962
  return count;
29862
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
+ }
29863
29980
  async removeSession(alias) {
29864
29981
  return await this.mutate(async () => {
29865
29982
  const session3 = this.state.sessions[alias];
@@ -29974,7 +30091,8 @@ class SessionService {
29974
30091
  modeId: session3.mode_id,
29975
30092
  replyMode: session3.reply_mode,
29976
30093
  effectiveReplyMode,
29977
- cwd: workspaceConfig.cwd
30094
+ cwd: workspaceConfig.cwd,
30095
+ archived: session3.archived === true
29978
30096
  };
29979
30097
  }
29980
30098
  async setSessionModel(alias, modelId) {
@@ -31391,6 +31509,9 @@ ${result.text}` : "" };
31391
31509
  async removeSession(session3) {
31392
31510
  await this.client.request("removeSession", this.toParams(session3));
31393
31511
  }
31512
+ async deleteSession(session3) {
31513
+ await this.client.request("deleteSession", this.toParams(session3));
31514
+ }
31394
31515
  async getAgentSessionId(session3) {
31395
31516
  const result = await this.client.request("getAgentSessionId", this.toParams(session3));
31396
31517
  return result.agentSessionId;
@@ -31959,6 +32080,31 @@ var init_agent_session_list = __esm(() => {
31959
32080
  init_path();
31960
32081
  });
31961
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
+
31962
32108
  // src/transport/acpx-cli/acpx-cli-transport.ts
31963
32109
  import { createRequire as createRequire5 } from "node:module";
31964
32110
  import { spawn as spawn9 } from "node:child_process";
@@ -32217,6 +32363,16 @@ ${baseText}` : "" };
32217
32363
  const detail = normalizeCommandError(result) ?? `command failed with exit code ${result.code}`;
32218
32364
  throw new Error(detail);
32219
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
+ }
32220
32376
  async hasSession(session3) {
32221
32377
  const result = await this.runCommand(this.command, this.buildArgs(session3, [
32222
32378
  "sessions",
@@ -32253,7 +32409,7 @@ ${baseText}` : "" };
32253
32409
  const parsed = JSON.parse(result.stdout);
32254
32410
  const acpxRecordId = typeof parsed.acpxRecordId === "string" ? parsed.acpxRecordId : typeof parsed.id === "string" ? parsed.id : undefined;
32255
32411
  const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
32256
- if (acpxRecordId) {
32412
+ if (acpxRecordId && /^[\w.:-]+$/.test(acpxRecordId) && acpxRecordId.length >= 8) {
32257
32413
  return { acpxRecordId, agentSessionId };
32258
32414
  }
32259
32415
  } catch {
@@ -32532,6 +32688,7 @@ var init_acpx_cli_transport = __esm(() => {
32532
32688
  init_terminate_process_tree();
32533
32689
  init_acpx_queue_owner_launcher();
32534
32690
  init_agent_session_list();
32691
+ init_acpx_session_files();
32535
32692
  require4 = createRequire5(import.meta.url);
32536
32693
  });
32537
32694
 
@@ -33003,8 +33160,8 @@ function createControlEventBus(logger2) {
33003
33160
 
33004
33161
  // src/transport/native-session-history.ts
33005
33162
  import { readFile as readFile14 } from "node:fs/promises";
33006
- import { homedir as homedir9 } from "node:os";
33007
- import { join as join19 } from "node:path";
33163
+ import { homedir as homedir10 } from "node:os";
33164
+ import { join as join20 } from "node:path";
33008
33165
  function classifyToolKind(name) {
33009
33166
  const n = name.toLowerCase();
33010
33167
  if (/(^|[^a-z])(read|cat|view|open)([^a-z]|$)/.test(n))
@@ -33116,8 +33273,8 @@ function mapAcpxMessagesToHistory(raw) {
33116
33273
  }
33117
33274
  async function readNativeSessionHistory(opts) {
33118
33275
  try {
33119
- const dir = opts.sessionsDir ?? join19(opts.homeDir ?? homedir9(), ".acpx", "sessions");
33120
- 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);
33121
33278
  if (!indexRaw)
33122
33279
  return [];
33123
33280
  const index = JSON.parse(indexRaw);
@@ -33126,7 +33283,7 @@ async function readNativeSessionHistory(opts) {
33126
33283
  for (const entry of candidates) {
33127
33284
  if (!entry.file)
33128
33285
  continue;
33129
- const recRaw = await readFile14(join19(dir, entry.file), "utf8").catch(() => null);
33286
+ const recRaw = await readFile14(join20(dir, entry.file), "utf8").catch(() => null);
33130
33287
  if (!recRaw)
33131
33288
  continue;
33132
33289
  const record3 = JSON.parse(recRaw);
@@ -33144,14 +33301,14 @@ var init_native_session_history = () => {};
33144
33301
  // src/control/workspace-fs.ts
33145
33302
  import { execFile } from "node:child_process";
33146
33303
  import { promisify } from "node:util";
33147
- import { homedir as homedir10 } from "node:os";
33148
- 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";
33149
33306
  import { isAbsolute as isAbsolute3, relative, resolve as resolve3, sep } from "node:path";
33150
33307
  function expandHome2(p) {
33151
33308
  if (p === "~")
33152
- return homedir10();
33309
+ return homedir11();
33153
33310
  if (p.startsWith("~/") || p.startsWith("~" + sep))
33154
- return resolve3(homedir10(), p.slice(2));
33311
+ return resolve3(homedir11(), p.slice(2));
33155
33312
  return p;
33156
33313
  }
33157
33314
 
@@ -33186,7 +33343,7 @@ class WorkspaceFs {
33186
33343
  }
33187
33344
  async listDirectory(workspace3, relPath) {
33188
33345
  const { abs, rel } = await this.resolve(workspace3, relPath);
33189
- const dirents = await readdir3(abs, { withFileTypes: true });
33346
+ const dirents = await readdir4(abs, { withFileTypes: true });
33190
33347
  const entries = [];
33191
33348
  for (const d of dirents.slice(0, MAX_ENTRIES)) {
33192
33349
  if (d.isDirectory()) {
@@ -33240,7 +33397,7 @@ class WorkspaceFs {
33240
33397
  const dir = queue.shift();
33241
33398
  let dirents;
33242
33399
  try {
33243
- dirents = await readdir3(dir, { withFileTypes: true });
33400
+ dirents = await readdir4(dir, { withFileTypes: true });
33244
33401
  } catch {
33245
33402
  continue;
33246
33403
  }
@@ -33389,7 +33546,8 @@ class ControlService {
33389
33546
  agent: session3.agent,
33390
33547
  workspace: session3.workspace,
33391
33548
  transportSession: session3.transportSession,
33392
- running: this.deps.activeTurns.isActiveAnywhere(session3.alias)
33549
+ running: this.deps.activeTurns.isActiveAnywhere(session3.alias),
33550
+ archived: session3.archived === true
33393
33551
  }));
33394
33552
  }
33395
33553
  async listNativeSessions(_chatKey, agent3, workspace3) {
@@ -33419,15 +33577,26 @@ class ControlService {
33419
33577
  agent: session3.agent,
33420
33578
  workspace: session3.workspace,
33421
33579
  transportSession: session3.transportSession,
33422
- running: false
33580
+ running: false,
33581
+ archived: false
33423
33582
  };
33424
33583
  }
33425
33584
  async removeSession(chatKey, alias) {
33426
33585
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
33427
- const result = await this.deps.sessions.removeSession(internalAlias);
33586
+ const result = await this.deps.removeSessionWithTransport(internalAlias);
33428
33587
  this.deps.events.emit({ type: "sessions-changed" });
33429
33588
  return result;
33430
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
+ }
33431
33600
  listAgents() {
33432
33601
  return this.deps.agents.list();
33433
33602
  }
@@ -33692,7 +33861,7 @@ var init_control_service = __esm(() => {
33692
33861
 
33693
33862
  // src/config/agent-catalog.ts
33694
33863
  import { existsSync as existsSync3 } from "node:fs";
33695
- import { delimiter as delimiter2, join as join20 } from "node:path";
33864
+ import { delimiter as delimiter2, join as join21 } from "node:path";
33696
33865
  function isBinaryOnPath(binary) {
33697
33866
  const path15 = process.env.PATH ?? "";
33698
33867
  const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
@@ -33701,7 +33870,7 @@ function isBinaryOnPath(binary) {
33701
33870
  continue;
33702
33871
  for (const ext of exts) {
33703
33872
  try {
33704
- if (existsSync3(join20(dir, binary + ext)))
33873
+ if (existsSync3(join21(dir, binary + ext)))
33705
33874
  return true;
33706
33875
  } catch {}
33707
33876
  }
@@ -33741,8 +33910,8 @@ __export(exports_main, {
33741
33910
  buildApp: () => buildApp
33742
33911
  });
33743
33912
  import { randomUUID as randomUUID3 } from "node:crypto";
33744
- import { homedir as homedir11 } from "node:os";
33745
- 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";
33746
33915
  import { fileURLToPath as fileURLToPath5 } from "node:url";
33747
33916
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
33748
33917
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -34240,6 +34409,9 @@ async function buildApp(paths, deps = {}) {
34240
34409
  createSessionWithTransport: (internalAlias, agent4, workspace3, model) => router3.createSessionWithTransport(internalAlias, agent4, workspace3, model),
34241
34410
  listNativeSessions: (agent4, workspace3) => router3.listNativeSessionsForControl(agent4, workspace3),
34242
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),
34243
34415
  activeTurns,
34244
34416
  scheduled: scheduledService,
34245
34417
  orchestration: orchestration3,
@@ -34402,8 +34574,8 @@ async function main() {
34402
34574
  }
34403
34575
  }
34404
34576
  async function prepareChannelMedia(configPath, config4) {
34405
- const runtimeDir = join21(dirname12(configPath), "runtime");
34406
- const mediaRootDir = join21(runtimeDir, "media");
34577
+ const runtimeDir = join22(dirname12(configPath), "runtime");
34578
+ const mediaRootDir = join22(runtimeDir, "media");
34407
34579
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
34408
34580
  await mediaStore.cleanupExpired().catch((error2) => {
34409
34581
  console.error("[xacpx] media cleanup failed:", error2 instanceof Error ? error2.message : String(error2));
@@ -34412,16 +34584,16 @@ async function prepareChannelMedia(configPath, config4) {
34412
34584
  return { mediaStore, channelDeps: { mediaStore, allowedMediaRoots } };
34413
34585
  }
34414
34586
  function resolveRuntimePaths() {
34415
- const home = process.env.HOME ?? homedir11();
34587
+ const home = process.env.HOME ?? homedir12();
34416
34588
  if (!home) {
34417
34589
  throw new Error("Unable to resolve the current user home directory");
34418
34590
  }
34419
- const configPath = coreEnv("CONFIG") ?? join21(coreHomeDir(home), "config.json");
34420
- const runtimeDir = join21(dirname12(configPath), "runtime");
34591
+ const configPath = coreEnv("CONFIG") ?? join22(coreHomeDir(home), "config.json");
34592
+ const runtimeDir = join22(dirname12(configPath), "runtime");
34421
34593
  return {
34422
34594
  configPath,
34423
- statePath: coreEnv("STATE") ?? join21(coreHomeDir(home), "state.json"),
34424
- perfLogPath: join21(runtimeDir, "perf.log"),
34595
+ statePath: coreEnv("STATE") ?? join22(coreHomeDir(home), "state.json"),
34596
+ perfLogPath: join22(runtimeDir, "perf.log"),
34425
34597
  orchestrationSocketPath: coreEnv("ORCHESTRATION_SOCKET") ?? resolveDaemonOrchestrationSocketPath(runtimeDir)
34426
34598
  };
34427
34599
  }
@@ -34433,13 +34605,13 @@ function resolveBridgeEntryPath() {
34433
34605
  }
34434
34606
  function resolveAppLogPath(configPath) {
34435
34607
  const rootDir = dirname12(configPath);
34436
- const runtimeDir = join21(rootDir, "runtime");
34437
- return join21(runtimeDir, "app.log");
34608
+ const runtimeDir = join22(rootDir, "runtime");
34609
+ return join22(runtimeDir, "app.log");
34438
34610
  }
34439
34611
  function resolvePerfLogPath(configPath) {
34440
34612
  const rootDir = dirname12(configPath);
34441
- const runtimeDir = join21(rootDir, "runtime");
34442
- return join21(runtimeDir, "perf.log");
34613
+ const runtimeDir = join22(rootDir, "runtime");
34614
+ return join22(runtimeDir, "perf.log");
34443
34615
  }
34444
34616
  function resolveOrchestrationSocketPathFromConfigPath(configPath) {
34445
34617
  const runtimeDir = resolveRuntimeDirFromConfigPath(configPath);
@@ -34677,12 +34849,12 @@ var init_config_check = __esm(async () => {
34677
34849
  });
34678
34850
 
34679
34851
  // src/doctor/checks/daemon-check.ts
34680
- 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";
34681
34853
  import { fileURLToPath as fileURLToPath6 } from "node:url";
34682
- import { homedir as homedir12 } from "node:os";
34683
- import { join as join22 } from "node:path";
34854
+ import { homedir as homedir13 } from "node:os";
34855
+ import { join as join23 } from "node:path";
34684
34856
  async function checkDaemon(options = {}) {
34685
- const home = options.home ?? process.env.HOME ?? homedir12();
34857
+ const home = options.home ?? process.env.HOME ?? homedir13();
34686
34858
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
34687
34859
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
34688
34860
  home,
@@ -34782,7 +34954,7 @@ async function detectStaleConsumerLockFix(runtimeDir, deps) {
34782
34954
  if (!fileName.endsWith(CONSUMER_LOCK_SUFFIX)) {
34783
34955
  continue;
34784
34956
  }
34785
- const lockPath = join22(runtimeDir, fileName);
34957
+ const lockPath = join23(runtimeDir, fileName);
34786
34958
  const lock2 = await deps.readConsumerLock(lockPath);
34787
34959
  if (lock2 && !deps.isProcessRunning(lock2.pid)) {
34788
34960
  stalePaths.push(lockPath);
@@ -34816,7 +34988,7 @@ async function detectStaleConsumerLockFix(runtimeDir, deps) {
34816
34988
  }
34817
34989
  async function defaultListConsumerLocks(runtimeDir) {
34818
34990
  try {
34819
- return await readdir4(runtimeDir);
34991
+ return await readdir5(runtimeDir);
34820
34992
  } catch {
34821
34993
  return [];
34822
34994
  }
@@ -34846,11 +35018,11 @@ var init_daemon_check = __esm(() => {
34846
35018
  });
34847
35019
 
34848
35020
  // src/doctor/checks/logs-check.ts
34849
- import { stat as stat4, readdir as readdir5 } from "node:fs/promises";
34850
- import { basename as basename3, join as join23 } from "node:path";
34851
- 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";
34852
35024
  async function checkLogs(options = {}) {
34853
- const home = options.home ?? process.env.HOME ?? homedir13();
35025
+ const home = options.home ?? process.env.HOME ?? homedir14();
34854
35026
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
34855
35027
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
34856
35028
  home,
@@ -34882,7 +35054,7 @@ async function checkLogs(options = {}) {
34882
35054
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
34883
35055
  const files = [];
34884
35056
  for (const name of matched) {
34885
- const path15 = join23(paths.runtimeDir, name);
35057
+ const path15 = join24(paths.runtimeDir, name);
34886
35058
  try {
34887
35059
  const fileStat = await probe.stat(path15);
34888
35060
  if (fileStat.isDirectory()) {
@@ -34963,7 +35135,7 @@ function formatBytes(bytes) {
34963
35135
  function createLogsFsProbe() {
34964
35136
  return {
34965
35137
  stat: async (path15) => await stat4(path15),
34966
- readdir: async (path15) => await readdir5(path15)
35138
+ readdir: async (path15) => await readdir6(path15)
34967
35139
  };
34968
35140
  }
34969
35141
  function formatError6(error2) {
@@ -35032,9 +35204,9 @@ var init_orchestration_health = __esm(() => {
35032
35204
  });
35033
35205
 
35034
35206
  // src/doctor/checks/orchestration-socket-check.ts
35035
- import { homedir as homedir14 } from "node:os";
35207
+ import { homedir as homedir15 } from "node:os";
35036
35208
  async function checkOrchestrationSocket(options = {}) {
35037
- const home = options.home ?? process.env.HOME ?? homedir14();
35209
+ const home = options.home ?? process.env.HOME ?? homedir15();
35038
35210
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
35039
35211
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
35040
35212
  home,
@@ -35209,9 +35381,9 @@ var init_plugin_check = __esm(async () => {
35209
35381
  import { constants } from "node:fs";
35210
35382
  import { access as access4, stat as stat5 } from "node:fs/promises";
35211
35383
  import { dirname as dirname13 } from "node:path";
35212
- import { homedir as homedir15 } from "node:os";
35384
+ import { homedir as homedir16 } from "node:os";
35213
35385
  async function checkRuntime(options = {}) {
35214
- const home = options.home ?? process.env.HOME ?? homedir15();
35386
+ const home = options.home ?? process.env.HOME ?? homedir16();
35215
35387
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
35216
35388
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
35217
35389
  home,
@@ -35844,10 +36016,10 @@ var init_render_doctor = __esm(() => {
35844
36016
  });
35845
36017
 
35846
36018
  // src/doctor/doctor.ts
35847
- import { homedir as homedir16 } from "node:os";
35848
- import { join as join24 } from "node:path";
36019
+ import { homedir as homedir17 } from "node:os";
36020
+ import { join as join25 } from "node:path";
35849
36021
  async function runDoctor(options = {}, deps = {}) {
35850
- const home = deps.home ?? process.env.HOME ?? homedir16();
36022
+ const home = deps.home ?? process.env.HOME ?? homedir17();
35851
36023
  const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
35852
36024
  const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
35853
36025
  const runners = [
@@ -36000,8 +36172,8 @@ function resolveDoctorRuntimePaths(home, resolver) {
36000
36172
  return resolveRuntimePaths();
36001
36173
  }
36002
36174
  return {
36003
- configPath: join24(coreHomeDir(home), "config.json"),
36004
- statePath: join24(coreHomeDir(home), "state.json")
36175
+ configPath: join25(coreHomeDir(home), "config.json"),
36176
+ statePath: join25(coreHomeDir(home), "state.json")
36005
36177
  };
36006
36178
  }
36007
36179
  function depsUseExplicitRuntimeOverrides() {
@@ -36184,8 +36356,8 @@ var init_doctor2 = __esm(async () => {
36184
36356
  // src/cli.ts
36185
36357
  init_core_home();
36186
36358
  import { randomUUID as randomUUID4 } from "node:crypto";
36187
- import { homedir as homedir17 } from "node:os";
36188
- 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";
36189
36361
  import { fileURLToPath as fileURLToPath7 } from "node:url";
36190
36362
 
36191
36363
  // src/runtime/migrate-core-home.ts
@@ -52439,7 +52611,7 @@ async function createCliScheduledTaskService() {
52439
52611
  return new ScheduledTaskService(state, stateStore);
52440
52612
  }
52441
52613
  function resolveConfigPathForCurrentEnv() {
52442
- return coreEnv("CONFIG") ?? join25(coreHomeDir(requireHome2()), "config.json");
52614
+ return coreEnv("CONFIG") ?? join26(coreHomeDir(requireHome2()), "config.json");
52443
52615
  }
52444
52616
  function resolveDaemonPathsForCurrentConfig() {
52445
52617
  const configPath = resolveConfigPathForCurrentEnv();
@@ -52790,7 +52962,7 @@ function decodeFirstRunOnboarding(raw) {
52790
52962
  return null;
52791
52963
  }
52792
52964
  function requireHome2() {
52793
- const home = process.env.HOME ?? homedir17();
52965
+ const home = process.env.HOME ?? homedir18();
52794
52966
  if (!home) {
52795
52967
  throw new Error("Unable to resolve the current user home directory");
52796
52968
  }
@@ -52814,7 +52986,7 @@ function safeDaemonLogPaths() {
52814
52986
  const configPath = resolveConfigPathForCurrentEnv();
52815
52987
  const paths = resolveDaemonPathsForCurrentConfig();
52816
52988
  return {
52817
- appLog: join25(dirname14(configPath), "runtime", "app.log"),
52989
+ appLog: join26(dirname14(configPath), "runtime", "app.log"),
52818
52990
  stderrLog: paths.stderrLog
52819
52991
  };
52820
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;
@@ -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.`,
@@ -1211,6 +1212,7 @@ ${detail}`,
1211
1212
  sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
1212
1213
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1213
1214
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1215
+ sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
1214
1216
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1215
1217
  sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1216
1218
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
@@ -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.1",
3
+ "version": "0.13.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",