@ganglion/xacpx 0.17.0 → 0.18.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.
@@ -2197,7 +2197,7 @@ function resolveClaudeConfigDir(env, homeDir, platform) {
2197
2197
  function installSettingsProfile(env, sourceConfigDir, policy, settings, options, platform) {
2198
2198
  const serialized = `${JSON.stringify(settings, null, 2)}
2199
2199
  `;
2200
- const digest = createHash("sha256").update(sourceConfigDir).update("\x00").update(policy).update("\x00").update(serialized).digest("hex").slice(0, 20);
2200
+ const digest = createHash("sha256").update(sourceConfigDir).update("\x00").update(policy).update("\x00").update(CLAUDE_SETTINGS_PROFILE_LAYOUT_VERSION).update("\x00").update(serialized).digest("hex").slice(0, 20);
2201
2201
  const profileDir = join(options.profileRoot ?? join(tmpdir(), "xacpx-claude-profiles"), digest);
2202
2202
  const settingsPath = join(profileDir, "settings.json");
2203
2203
  (options.writeProfile ?? writePrivateFileSync)(settingsPath, serialized);
@@ -2205,7 +2205,7 @@ function installSettingsProfile(env, sourceConfigDir, policy, settings, options,
2205
2205
  setEnvironmentValue(env, "CLAUDE_CONFIG_DIR", profileDir, platform);
2206
2206
  }
2207
2207
  function linkClaudeSessionState(sourceConfigDir, profileDir, platform) {
2208
- for (const name of CLAUDE_SESSION_STATE_DIRS) {
2208
+ for (const name of CLAUDE_PERSISTENT_SESSION_STATE_DIRS) {
2209
2209
  const source = join(sourceConfigDir, name);
2210
2210
  mkdirSync2(source, { recursive: true });
2211
2211
  const target = join(profileDir, name);
@@ -2302,20 +2302,18 @@ function setEnvironmentValue(env, name, value, platform) {
2302
2302
  deleteEnvironmentValue(env, name, platform);
2303
2303
  env[name] = value;
2304
2304
  }
2305
- var DEFAULT_CLAUDE_SETTINGS_POLICY = "provider-only", CLAUDE_SESSION_STATE_DIRS;
2305
+ var DEFAULT_CLAUDE_SETTINGS_POLICY = "provider-only", CLAUDE_SETTINGS_PROFILE_LAYOUT_VERSION = "persistent-state-links-v1", CLAUDE_PERSISTENT_SESSION_STATE_DIRS;
2306
2306
  var init_claude_settings_policy = __esm(() => {
2307
2307
  init_private_file();
2308
- CLAUDE_SESSION_STATE_DIRS = [
2308
+ CLAUDE_PERSISTENT_SESSION_STATE_DIRS = [
2309
2309
  "projects",
2310
2310
  "file-history",
2311
2311
  "plans",
2312
2312
  "todos",
2313
- "session-env",
2314
2313
  "tasks",
2315
2314
  "teams",
2316
2315
  "sessions",
2317
- "transcripts",
2318
- "shell-snapshots"
2316
+ "transcripts"
2319
2317
  ];
2320
2318
  });
2321
2319
 
@@ -5700,6 +5698,18 @@ function createDefaultQueueOwnerTerminator(_acpxCommand) {
5700
5698
  await terminateAcpxQueueOwner(sessionId);
5701
5699
  };
5702
5700
  }
5701
+ async function readQueueOwnerPid(sessionId) {
5702
+ let owner;
5703
+ try {
5704
+ owner = JSON.parse(await readFile(queueLockFilePath(sessionId), "utf8"));
5705
+ } catch {
5706
+ return;
5707
+ }
5708
+ if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
5709
+ return owner.pid;
5710
+ }
5711
+ return;
5712
+ }
5703
5713
  async function terminateAcpxQueueOwner(sessionId) {
5704
5714
  const lockPath = queueLockFilePath(sessionId);
5705
5715
  let owner;
@@ -5742,6 +5752,88 @@ var init_acpx_queue_owner_launcher = __esm(() => {
5742
5752
  init_i18n();
5743
5753
  });
5744
5754
 
5755
+ // src/runtime/core-home.ts
5756
+ import { existsSync as existsSync2 } from "node:fs";
5757
+ import { join as join4 } from "node:path";
5758
+ function coreHomeDir(home) {
5759
+ const primary = join4(home, CORE_HOME_DIR_NAME);
5760
+ if (existsSync2(primary))
5761
+ return primary;
5762
+ const legacy = join4(home, CORE_HOME_LEGACY_DIR_NAME);
5763
+ if (existsSync2(legacy))
5764
+ return legacy;
5765
+ return primary;
5766
+ }
5767
+ function coreHomeDisplayPath(...segments) {
5768
+ return ["~", CORE_HOME_DIR_NAME, ...segments].join("/");
5769
+ }
5770
+ var CORE_HOME_DIR_NAME = ".xacpx", CORE_HOME_LEGACY_DIR_NAME = ".weacpx";
5771
+ var init_core_home = () => {};
5772
+
5773
+ // src/orchestration/orchestration-ipc.ts
5774
+ import { createHash as createHash3 } from "node:crypto";
5775
+ import { join as join5 } from "node:path";
5776
+ function resolveOrchestrationEndpoint(runtimeDir, platform = process.platform) {
5777
+ if (platform === "win32") {
5778
+ const suffix = createHash3("sha256").update(runtimeDir).digest("hex").slice(0, 12);
5779
+ return {
5780
+ kind: "named-pipe",
5781
+ path: `\\\\.\\pipe\\xacpx-orchestration-${suffix}`
5782
+ };
5783
+ }
5784
+ return {
5785
+ kind: "unix",
5786
+ path: join5(runtimeDir, "orchestration.sock")
5787
+ };
5788
+ }
5789
+ function createOrchestrationEndpoint(path2, platform = process.platform) {
5790
+ return {
5791
+ kind: platform === "win32" || path2.startsWith("\\\\.\\pipe\\") ? "named-pipe" : "unix",
5792
+ path: path2
5793
+ };
5794
+ }
5795
+ function encodeOrchestrationRpcRequest(request) {
5796
+ return `${JSON.stringify(request)}
5797
+ `;
5798
+ }
5799
+ function encodeOrchestrationRpcResponse(response) {
5800
+ return `${JSON.stringify(response)}
5801
+ `;
5802
+ }
5803
+ var init_orchestration_ipc = () => {};
5804
+
5805
+ // src/daemon/daemon-files.ts
5806
+ import { dirname as dirname3, join as join6 } from "node:path";
5807
+ function resolveDaemonPaths(options) {
5808
+ const runtimeDir = options.runtimeDir ?? (options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : join6(coreHomeDir(options.home), "runtime"));
5809
+ return {
5810
+ runtimeDir,
5811
+ pidFile: join6(runtimeDir, "daemon.pid"),
5812
+ statusFile: join6(runtimeDir, "status.json"),
5813
+ stdoutLog: join6(runtimeDir, "stdout.log"),
5814
+ stderrLog: join6(runtimeDir, "stderr.log"),
5815
+ appLog: join6(runtimeDir, "app.log")
5816
+ };
5817
+ }
5818
+ function resolveRuntimeDirFromConfigPath(configPath) {
5819
+ return join6(dirname3(configPath), "runtime");
5820
+ }
5821
+ function resolveDaemonOrchestrationSocketPath(runtimeDir, platform = process.platform) {
5822
+ return resolveOrchestrationEndpoint(runtimeDir, platform).path;
5823
+ }
5824
+ function isProcessAlive(pid) {
5825
+ try {
5826
+ process.kill(pid, 0);
5827
+ return true;
5828
+ } catch (error) {
5829
+ return error.code === "EPERM";
5830
+ }
5831
+ }
5832
+ var init_daemon_files = __esm(() => {
5833
+ init_core_home();
5834
+ init_orchestration_ipc();
5835
+ });
5836
+
5745
5837
  // src/util/path.ts
5746
5838
  import path2 from "node:path";
5747
5839
  import { homedir as homedir4 } from "node:os";
@@ -5784,10 +5876,10 @@ var init_path = __esm(() => {
5784
5876
  // src/transport/codex-subagent-filter.ts
5785
5877
  import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
5786
5878
  import { homedir as homedir5 } from "node:os";
5787
- import { join as join4 } from "node:path";
5879
+ import { join as join7 } from "node:path";
5788
5880
  function resolveCodexHome(env = process.env) {
5789
5881
  const fromEnv = env.CODEX_HOME?.trim();
5790
- return fromEnv ? fromEnv : join4(homedir5(), ".codex");
5882
+ return fromEnv ? fromEnv : join7(homedir5(), ".codex");
5791
5883
  }
5792
5884
  function isPlainObject(value) {
5793
5885
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -5852,7 +5944,7 @@ function filterSubagentSessions(result, isSubagent) {
5852
5944
  };
5853
5945
  }
5854
5946
  function nodeRolloutReader(home) {
5855
- const root = join4(home, "sessions");
5947
+ const root = join7(home, "sessions");
5856
5948
  return {
5857
5949
  listRolloutPaths() {
5858
5950
  const out = [];
@@ -5864,7 +5956,7 @@ function nodeRolloutReader(home) {
5864
5956
  return;
5865
5957
  }
5866
5958
  for (const entry of entries) {
5867
- const full = join4(dir, entry.name);
5959
+ const full = join7(dir, entry.name);
5868
5960
  if (entry.isDirectory())
5869
5961
  walk(full);
5870
5962
  else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
@@ -5980,11 +6072,11 @@ var init_agent_session_list = __esm(() => {
5980
6072
  // src/transport/acpx-session-files.ts
5981
6073
  import { readdir, unlink as unlink2 } from "node:fs/promises";
5982
6074
  import { homedir as homedir6 } from "node:os";
5983
- import { join as join5 } from "node:path";
6075
+ import { join as join8 } from "node:path";
5984
6076
  async function deleteAcpxSessionFiles(options) {
5985
- const dir = options.sessionsDir ?? join5(homedir6(), ".acpx", "sessions");
6077
+ const dir = options.sessionsDir ?? join8(homedir6(), ".acpx", "sessions");
5986
6078
  const safeId = encodeURIComponent(options.acpxRecordId);
5987
- await unlink2(join5(dir, `${safeId}.json`)).catch(() => {
6079
+ await unlink2(join8(dir, `${safeId}.json`)).catch(() => {
5988
6080
  return;
5989
6081
  });
5990
6082
  let entries;
@@ -5995,7 +6087,7 @@ async function deleteAcpxSessionFiles(options) {
5995
6087
  }
5996
6088
  const streamFiles = entries.filter((name) => name.startsWith(`${safeId}.stream.`));
5997
6089
  for (const name of streamFiles) {
5998
- await unlink2(join5(dir, name)).catch(() => {
6090
+ await unlink2(join8(dir, name)).catch(() => {
5999
6091
  return;
6000
6092
  });
6001
6093
  }
@@ -6262,7 +6354,7 @@ init_prompt_media();
6262
6354
  init_streaming_prompt();
6263
6355
  import { copyFile, readdir as readdir2 } from "node:fs/promises";
6264
6356
  import { homedir as homedir7 } from "node:os";
6265
- import { dirname as dirname3, join as join6, win32 } from "node:path";
6357
+ import { dirname as dirname4, join as join9, win32 } from "node:path";
6266
6358
  import { spawn as spawn4 } from "node:child_process";
6267
6359
 
6268
6360
  // src/bridge/parse-missing-optional-dep.ts
@@ -6281,6 +6373,7 @@ function parseMissingOptionalDep(text) {
6281
6373
  // src/bridge/bridge-runtime.ts
6282
6374
  init_discover_parent_package_paths();
6283
6375
  init_acpx_queue_owner_launcher();
6376
+ init_daemon_files();
6284
6377
  init_agent_session_list();
6285
6378
  init_codex_subagent_filter();
6286
6379
  init_acpx_session_files();
@@ -6345,6 +6438,7 @@ class BridgeRuntime {
6345
6438
  });
6346
6439
  }
6347
6440
  async resumeAgentSession(input) {
6441
+ this.invalidateRecordIdCache(input);
6348
6442
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
6349
6443
  "sessions",
6350
6444
  "new",
@@ -6414,6 +6508,7 @@ class BridgeRuntime {
6414
6508
  throw new Error(message);
6415
6509
  }
6416
6510
  async ensureSession(input, onProgress) {
6511
+ this.invalidateRecordIdCache(input);
6417
6512
  try {
6418
6513
  return await this.attemptEnsureSession(input, onProgress);
6419
6514
  } catch (error) {
@@ -6528,7 +6623,7 @@ class BridgeRuntime {
6528
6623
  const resolved = __require.resolve(`${candidate}/package.json`, {
6529
6624
  paths: [process.cwd(), ...__require.resolve.paths(candidate) ?? []]
6530
6625
  });
6531
- return dirname3(resolved);
6626
+ return dirname4(resolved);
6532
6627
  } catch {
6533
6628
  continue;
6534
6629
  }
@@ -6715,6 +6810,7 @@ class BridgeRuntime {
6715
6810
  };
6716
6811
  }
6717
6812
  async removeSession(input) {
6813
+ this.invalidateRecordIdCache(input);
6718
6814
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
6719
6815
  "sessions",
6720
6816
  "close",
@@ -6753,6 +6849,27 @@ class BridgeRuntime {
6753
6849
  await terminateAcpxQueueOwner(acpxRecordId);
6754
6850
  return {};
6755
6851
  }
6852
+ recordIdCache = new Map;
6853
+ recordIdCacheKey(input) {
6854
+ return JSON.stringify([input.agent, input.agentCommand ?? null, input.cwd, input.name]);
6855
+ }
6856
+ invalidateRecordIdCache(input) {
6857
+ this.recordIdCache.delete(this.recordIdCacheKey(input));
6858
+ }
6859
+ async isSessionWarm(input) {
6860
+ const cacheKey = this.recordIdCacheKey(input);
6861
+ let acpxRecordId = this.recordIdCache.get(cacheKey);
6862
+ if (!acpxRecordId) {
6863
+ try {
6864
+ ({ acpxRecordId } = await this.readSessionRecord(input));
6865
+ } catch {
6866
+ return { warm: false };
6867
+ }
6868
+ this.recordIdCache.set(cacheKey, acpxRecordId);
6869
+ }
6870
+ const pid = await readQueueOwnerPid(acpxRecordId);
6871
+ return { warm: pid !== undefined && isProcessAlive(pid) };
6872
+ }
6756
6873
  async shutdown() {
6757
6874
  return {};
6758
6875
  }
@@ -6943,7 +7060,7 @@ async function tryRepairAcpxSessionIndex(deps = {}) {
6943
7060
  if (!home) {
6944
7061
  return false;
6945
7062
  }
6946
- const pathJoin = platform === "win32" ? win32.join : join6;
7063
+ const pathJoin = platform === "win32" ? win32.join : join9;
6947
7064
  const sessionsDir = pathJoin(home, ".acpx", "sessions");
6948
7065
  const indexPath = pathJoin(sessionsDir, "index.json");
6949
7066
  const readdirFn = deps.readdirFn ?? readdir2;
@@ -6993,6 +7110,7 @@ var BRIDGE_METHODS = new Set([
6993
7110
  "removeSession",
6994
7111
  "deleteSession",
6995
7112
  "freeWarmProcess",
7113
+ "isSessionWarm",
6996
7114
  "getAgentSessionId"
6997
7115
  ]);
6998
7116
  var SESSION_SCOPED_METHODS = new Set([
@@ -7010,6 +7128,7 @@ var SESSION_SCOPED_METHODS = new Set([
7010
7128
  "removeSession",
7011
7129
  "deleteSession",
7012
7130
  "freeWarmProcess",
7131
+ "isSessionWarm",
7013
7132
  "getAgentSessionId"
7014
7133
  ]);
7015
7134
 
@@ -7070,7 +7189,7 @@ class BridgeServer {
7070
7189
  if (!sessionKey) {
7071
7190
  return await this.dispatch(requestId, method, params, writeLine);
7072
7191
  }
7073
- const lane = method === "cancel" ? "control" : "normal";
7192
+ const lane = method === "cancel" || method === "isSessionWarm" ? "control" : "normal";
7074
7193
  return await this.scheduler.run(sessionKey, lane, () => this.dispatch(requestId, method, params, writeLine));
7075
7194
  }
7076
7195
  async dispatch(requestId, method, params, writeLine) {
@@ -7281,6 +7400,14 @@ class BridgeServer {
7281
7400
  cwd: requireString(params, "cwd"),
7282
7401
  name: requireString(params, "name")
7283
7402
  });
7403
+ case "isSessionWarm":
7404
+ return await this.runtime.isSessionWarm({
7405
+ agent: requireString(params, "agent"),
7406
+ ...agentExecutionSettings(params),
7407
+ agentCommand: asOptionalString(params.agentCommand),
7408
+ cwd: requireString(params, "cwd"),
7409
+ name: requireString(params, "name")
7410
+ });
7284
7411
  case "getAgentSessionId":
7285
7412
  return await this.runtime.getAgentSessionId({
7286
7413
  agent: requireString(params, "agent"),
package/dist/cli.js CHANGED
@@ -4624,7 +4624,7 @@ function resolveClaudeConfigDir(env, homeDir, platform) {
4624
4624
  function installSettingsProfile(env, sourceConfigDir, policy, settings, options, platform) {
4625
4625
  const serialized = `${JSON.stringify(settings, null, 2)}
4626
4626
  `;
4627
- const digest = createHash("sha256").update(sourceConfigDir).update("\x00").update(policy).update("\x00").update(serialized).digest("hex").slice(0, 20);
4627
+ const digest = createHash("sha256").update(sourceConfigDir).update("\x00").update(policy).update("\x00").update(CLAUDE_SETTINGS_PROFILE_LAYOUT_VERSION).update("\x00").update(serialized).digest("hex").slice(0, 20);
4628
4628
  const profileDir = join3(options.profileRoot ?? join3(tmpdir(), "xacpx-claude-profiles"), digest);
4629
4629
  const settingsPath = join3(profileDir, "settings.json");
4630
4630
  (options.writeProfile ?? writePrivateFileSync)(settingsPath, serialized);
@@ -4632,7 +4632,7 @@ function installSettingsProfile(env, sourceConfigDir, policy, settings, options,
4632
4632
  setEnvironmentValue(env, "CLAUDE_CONFIG_DIR", profileDir, platform);
4633
4633
  }
4634
4634
  function linkClaudeSessionState(sourceConfigDir, profileDir, platform) {
4635
- for (const name of CLAUDE_SESSION_STATE_DIRS) {
4635
+ for (const name of CLAUDE_PERSISTENT_SESSION_STATE_DIRS) {
4636
4636
  const source = join3(sourceConfigDir, name);
4637
4637
  mkdirSync3(source, { recursive: true });
4638
4638
  const target = join3(profileDir, name);
@@ -4729,20 +4729,18 @@ function setEnvironmentValue(env, name, value, platform) {
4729
4729
  deleteEnvironmentValue(env, name, platform);
4730
4730
  env[name] = value;
4731
4731
  }
4732
- var DEFAULT_CLAUDE_SETTINGS_POLICY = "provider-only", CLAUDE_SESSION_STATE_DIRS;
4732
+ var DEFAULT_CLAUDE_SETTINGS_POLICY = "provider-only", CLAUDE_SETTINGS_PROFILE_LAYOUT_VERSION = "persistent-state-links-v1", CLAUDE_PERSISTENT_SESSION_STATE_DIRS;
4733
4733
  var init_claude_settings_policy = __esm(() => {
4734
4734
  init_private_file();
4735
- CLAUDE_SESSION_STATE_DIRS = [
4735
+ CLAUDE_PERSISTENT_SESSION_STATE_DIRS = [
4736
4736
  "projects",
4737
4737
  "file-history",
4738
4738
  "plans",
4739
4739
  "todos",
4740
- "session-env",
4741
4740
  "tasks",
4742
4741
  "teams",
4743
4742
  "sessions",
4744
- "transcripts",
4745
- "shell-snapshots"
4743
+ "transcripts"
4746
4744
  ];
4747
4745
  });
4748
4746
 
@@ -32135,6 +32133,18 @@ function createDefaultQueueOwnerTerminator(_acpxCommand) {
32135
32133
  await terminateAcpxQueueOwner(sessionId);
32136
32134
  };
32137
32135
  }
32136
+ async function readQueueOwnerPid(sessionId) {
32137
+ let owner;
32138
+ try {
32139
+ owner = JSON.parse(await readFile13(queueLockFilePath(sessionId), "utf8"));
32140
+ } catch {
32141
+ return;
32142
+ }
32143
+ if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
32144
+ return owner.pid;
32145
+ }
32146
+ return;
32147
+ }
32138
32148
  async function terminateAcpxQueueOwner(sessionId) {
32139
32149
  const lockPath = queueLockFilePath(sessionId);
32140
32150
  let owner;
@@ -32991,6 +33001,10 @@ ${result.text}` : "" };
32991
33001
  async freeWarmProcess(session3) {
32992
33002
  await this.client.request("freeWarmProcess", this.toParams(session3));
32993
33003
  }
33004
+ async isSessionWarm(session3) {
33005
+ const result = await this.client.request("isSessionWarm", this.toParams(session3));
33006
+ return result.warm === true;
33007
+ }
32994
33008
  async getAgentSessionId(session3) {
32995
33009
  const result = await this.client.request("getAgentSessionId", this.toParams(session3));
32996
33010
  return result.agentSessionId;
@@ -34107,6 +34121,7 @@ class AcpxCliTransport {
34107
34121
  this.resolveSpawnEnvironment = options.resolveSpawnEnvironment ?? resolveClaudeSpawnEnvironment;
34108
34122
  }
34109
34123
  async ensureSession(session3, _onProgress) {
34124
+ this.invalidateRecordIdCache(session3);
34110
34125
  try {
34111
34126
  await this.runEnsureSession(session3);
34112
34127
  } catch (error2) {
@@ -34319,6 +34334,7 @@ ${baseText}` : "" };
34319
34334
  };
34320
34335
  }
34321
34336
  async resumeAgentSession(session3, agentSessionId) {
34337
+ this.invalidateRecordIdCache(session3);
34322
34338
  const args = this.buildArgs(session3, [
34323
34339
  "sessions",
34324
34340
  "new",
@@ -34339,6 +34355,7 @@ ${baseText}` : "" };
34339
34355
  this.permissionPolicy = policy.permissionPolicy;
34340
34356
  }
34341
34357
  async removeSession(session3) {
34358
+ this.invalidateRecordIdCache(session3);
34342
34359
  const result = await this.runCommandWithTimeout(this.runCommand, this.buildArgs(session3, [
34343
34360
  "sessions",
34344
34361
  "close",
@@ -34375,6 +34392,30 @@ ${baseText}` : "" };
34375
34392
  }
34376
34393
  await terminateAcpxQueueOwner(acpxRecordId);
34377
34394
  }
34395
+ recordIdCache = new Map;
34396
+ recordIdCacheKey(session3) {
34397
+ return JSON.stringify([session3.agent, session3.agentCommand ?? null, session3.cwd, session3.transportSession]);
34398
+ }
34399
+ invalidateRecordIdCache(session3) {
34400
+ this.recordIdCache.delete(this.recordIdCacheKey(session3));
34401
+ }
34402
+ async isSessionWarm(session3) {
34403
+ const cacheKey = this.recordIdCacheKey(session3);
34404
+ let acpxRecordId = this.recordIdCache.get(cacheKey);
34405
+ if (!acpxRecordId) {
34406
+ try {
34407
+ ({ acpxRecordId } = await this.readSessionRecord(session3));
34408
+ } catch {
34409
+ return false;
34410
+ }
34411
+ this.recordIdCache.set(cacheKey, acpxRecordId);
34412
+ }
34413
+ const pid = await readQueueOwnerPid(acpxRecordId);
34414
+ if (pid === undefined) {
34415
+ return false;
34416
+ }
34417
+ return isProcessAlive(pid);
34418
+ }
34378
34419
  async hasSession(session3) {
34379
34420
  const result = await this.runCommandWithTimeout(this.runCommand, this.buildArgs(session3, [
34380
34421
  "sessions",
@@ -34707,6 +34748,7 @@ var init_acpx_cli_transport = __esm(() => {
34707
34748
  init_node_pty_helper();
34708
34749
  init_terminate_process_tree();
34709
34750
  init_acpx_queue_owner_launcher();
34751
+ init_daemon_files();
34710
34752
  init_agent_session_list();
34711
34753
  init_codex_subagent_filter();
34712
34754
  init_acpx_session_files();
@@ -36446,7 +36488,7 @@ class WorkspaceFs {
36446
36488
  }
36447
36489
  const files = [];
36448
36490
  try {
36449
- const { stdout: stdout2 } = await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "status", "--porcelain", "-z"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" });
36491
+ const { stdout: stdout2 } = await execFileAsync("git", ["-C", root, "-c", "core.quotePath=false", "status", "--porcelain", "-z", "--untracked-files=all"], { maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT_MS, killSignal: "SIGKILL" });
36450
36492
  const fields = stdout2.split("\x00");
36451
36493
  for (let i = 0;i < fields.length; i++) {
36452
36494
  const field = fields[i];
@@ -36674,9 +36716,10 @@ class WorkspaceGit {
36674
36716
  return (await this.runRaw(root, args)).trim();
36675
36717
  }
36676
36718
  async runRaw(root, args) {
36719
+ const fullArgs = ["-C", root, "-c", "gc.auto=0", ...args];
36677
36720
  if (this.runGitOverride)
36678
- return await this.runGitOverride(root, args);
36679
- const result = await execFileAsync2("git", ["-C", root, ...args], {
36721
+ return await this.runGitOverride(root, fullArgs);
36722
+ const result = await execFileAsync2("git", fullArgs, {
36680
36723
  timeout: GIT_TIMEOUT_MS2,
36681
36724
  killSignal: "SIGKILL",
36682
36725
  maxBuffer: GIT_MAX_BUFFER2
@@ -36952,19 +36995,21 @@ class WorkspaceGit {
36952
36995
  ...worktreePath ? { worktreePath } : {}
36953
36996
  };
36954
36997
  });
36955
- const statusRaw = await this.runRaw(root, ["-c", "core.quotePath=false", "status", "--porcelain", "-z"]);
36956
36998
  const files = [];
36957
- const fields = statusRaw.split("\x00");
36958
- for (let index = 0;index < fields.length; index += 1) {
36959
- const field = fields[index];
36960
- if (!field)
36961
- continue;
36962
- const status = field.slice(0, 2);
36963
- const path14 = field.slice(3);
36964
- if (status[0] === "R" || status[0] === "C")
36965
- index += 1;
36966
- files.push({ path: path14, status });
36967
- }
36999
+ try {
37000
+ const statusRaw = await this.runRaw(root, ["-c", "core.quotePath=false", "status", "--porcelain", "-z", "--untracked-files=all"]);
37001
+ const fields = statusRaw.split("\x00");
37002
+ for (let index = 0;index < fields.length; index += 1) {
37003
+ const field = fields[index];
37004
+ if (!field)
37005
+ continue;
37006
+ const status = field.slice(0, 2);
37007
+ const path14 = field.slice(3);
37008
+ if (status[0] === "R" || status[0] === "C")
37009
+ index += 1;
37010
+ files.push({ path: path14, status });
37011
+ }
37012
+ } catch {}
36968
37013
  files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
36969
37014
  return {
36970
37015
  workspace: workspace3,
@@ -37071,6 +37116,11 @@ class SessionTurnRunner {
37071
37116
  if (wasArchived) {
37072
37117
  this.deps.events.emit({ type: "sessions-changed" });
37073
37118
  }
37119
+ if (internalAlias && this.deps.sessionWarmth) {
37120
+ const bound = await this.deps.sessions.getSession(internalAlias).catch(() => null);
37121
+ if (bound)
37122
+ this.deps.sessionWarmth.markWarm(bound);
37123
+ }
37074
37124
  this.deps.events.emit({
37075
37125
  type: "turn-started",
37076
37126
  chatKey: req.chatKey,
@@ -37687,6 +37737,7 @@ class ControlService {
37687
37737
  if (!session3)
37688
37738
  throw new Error("session not found");
37689
37739
  await this.deps.sessions.setDisplayName(session3.alias, displayName);
37740
+ this.deps.events.emit({ type: "sessions-changed" });
37690
37741
  }
37691
37742
  async resolveControlSession(chatKey, alias) {
37692
37743
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
@@ -37697,17 +37748,22 @@ class ControlService {
37697
37748
  }
37698
37749
  listSessions(chatKey) {
37699
37750
  const channelId = getChannelIdFromChatKey(chatKey);
37700
- return this.deps.sessions.listAllResolvedSessions().filter((session3) => isSessionAliasVisibleInChannel(session3.alias, channelId)).map((session3) => ({
37701
- alias: toDisplaySessionAlias(session3.alias),
37702
- agent: session3.agent,
37703
- workspace: session3.workspace,
37704
- transportSession: session3.transportSession,
37705
- running: this.deps.activeTurns.isActiveAnywhere(session3.alias),
37706
- archived: session3.archived === true,
37707
- ...session3.source === "agent-side" ? { native: true } : {},
37708
- ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
37709
- ...session3.displayName ? { displayName: session3.displayName } : {}
37710
- }));
37751
+ return this.deps.sessions.listAllResolvedSessions().filter((session3) => isSessionAliasVisibleInChannel(session3.alias, channelId)).map((session3) => {
37752
+ const running = this.deps.activeTurns.isActiveAnywhere(session3.alias);
37753
+ const warm = running ? true : this.deps.sessionWarmth?.isWarm(session3);
37754
+ return {
37755
+ alias: toDisplaySessionAlias(session3.alias),
37756
+ agent: session3.agent,
37757
+ workspace: session3.workspace,
37758
+ transportSession: session3.transportSession,
37759
+ running,
37760
+ archived: session3.archived === true,
37761
+ ...warm !== undefined ? { warm } : {},
37762
+ ...session3.source === "agent-side" ? { native: true } : {},
37763
+ ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
37764
+ ...session3.displayName ? { displayName: session3.displayName } : {}
37765
+ };
37766
+ });
37711
37767
  }
37712
37768
  async listNativeSessions(_chatKey, agent3, workspace3) {
37713
37769
  const sessions = await this.deps.listNativeSessions(agent3, workspace3);
@@ -37749,6 +37805,11 @@ class ControlService {
37749
37805
  async archiveSession(chatKey, alias) {
37750
37806
  const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
37751
37807
  await this.deps.archiveSessionWithTransport(internalAlias);
37808
+ const session3 = await this.deps.sessions.getSession(internalAlias).catch(() => {
37809
+ return;
37810
+ });
37811
+ if (session3)
37812
+ this.deps.sessionWarmth?.markCold(session3);
37752
37813
  this.deps.events.emit({ type: "sessions-changed" });
37753
37814
  }
37754
37815
  async unarchiveSession(chatKey, alias) {
@@ -37896,6 +37957,98 @@ var init_control_service = __esm(() => {
37896
37957
  MODEL_SET_SETTLE_BUDGET_MS = 2 * (DEFAULT_MANAGEMENT_COMMAND_TIMEOUT_MS + BRIDGE_REQUEST_TIMEOUT_GRACE_MS);
37897
37958
  });
37898
37959
 
37960
+ // src/control/session-warmth-tracker.ts
37961
+ class SessionWarmthTracker {
37962
+ listSessions;
37963
+ checkWarm;
37964
+ events;
37965
+ logger;
37966
+ intervalMs;
37967
+ setIntervalFn;
37968
+ clearIntervalFn;
37969
+ warmth = new Map;
37970
+ intervalHandle = null;
37971
+ ticking = false;
37972
+ constructor(deps) {
37973
+ this.listSessions = deps.listSessions;
37974
+ this.checkWarm = deps.isWarm;
37975
+ this.events = deps.events;
37976
+ this.logger = deps.logger;
37977
+ this.intervalMs = deps.intervalMs ?? 60000;
37978
+ this.setIntervalFn = deps.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
37979
+ this.clearIntervalFn = deps.clearIntervalFn ?? ((timer) => clearInterval(timer));
37980
+ }
37981
+ start() {
37982
+ if (this.intervalHandle !== null)
37983
+ return;
37984
+ this.intervalHandle = this.setIntervalFn(() => {
37985
+ this.tick();
37986
+ }, this.intervalMs);
37987
+ this.tick();
37988
+ }
37989
+ stop() {
37990
+ if (this.intervalHandle !== null) {
37991
+ this.clearIntervalFn(this.intervalHandle);
37992
+ this.intervalHandle = null;
37993
+ }
37994
+ }
37995
+ isWarm(session3) {
37996
+ return this.warmth.get(warmthKey(session3));
37997
+ }
37998
+ markWarm(session3) {
37999
+ this.warmth.set(warmthKey(session3), true);
38000
+ }
38001
+ markCold(session3) {
38002
+ this.warmth.set(warmthKey(session3), false);
38003
+ }
38004
+ async tick() {
38005
+ if (this.ticking)
38006
+ return;
38007
+ this.ticking = true;
38008
+ try {
38009
+ const sessions = this.listSessions();
38010
+ const seen = new Set;
38011
+ let flipped = false;
38012
+ for (const session3 of sessions) {
38013
+ const key = warmthKey(session3);
38014
+ if (seen.has(key))
38015
+ continue;
38016
+ seen.add(key);
38017
+ let warm;
38018
+ try {
38019
+ warm = await this.checkWarm(session3);
38020
+ } catch (error2) {
38021
+ await this.logger?.error("warmth.check_failed", "session warmth check threw; keeping previous value", {
38022
+ transportSession: session3.transportSession,
38023
+ message: error2 instanceof Error ? error2.message : String(error2)
38024
+ });
38025
+ continue;
38026
+ }
38027
+ if (this.warmth.get(key) !== warm) {
38028
+ this.warmth.set(key, warm);
38029
+ flipped = true;
38030
+ }
38031
+ }
38032
+ for (const key of [...this.warmth.keys()]) {
38033
+ if (!seen.has(key))
38034
+ this.warmth.delete(key);
38035
+ }
38036
+ if (flipped) {
38037
+ this.events.emit({ type: "sessions-changed" });
38038
+ }
38039
+ } catch (error2) {
38040
+ await this.logger?.error("warmth.tick_failed", "session warmth tick threw", {
38041
+ message: error2 instanceof Error ? error2.message : String(error2)
38042
+ });
38043
+ } finally {
38044
+ this.ticking = false;
38045
+ }
38046
+ }
38047
+ }
38048
+ function warmthKey(session3) {
38049
+ return JSON.stringify([session3.agent, session3.agentCommand ?? null, session3.cwd, session3.transportSession]);
38050
+ }
38051
+
37899
38052
  // src/control/terminal-service.ts
37900
38053
  import { randomUUID as randomUUID4 } from "node:crypto";
37901
38054
  import { createRequire as createRequire6 } from "node:module";
@@ -38800,6 +38953,13 @@ async function buildApp(paths, deps = {}) {
38800
38953
  const loadAgentRegistry = createAcpxAgentRegistryLoader({ logger });
38801
38954
  uploadStore.cleanup();
38802
38955
  const uploadCleanupInterval = setInterval(() => void uploadStore.cleanup().catch(() => {}), 60 * 60 * 1000);
38956
+ const sessionWarmth = transport.isSessionWarm ? new SessionWarmthTracker({
38957
+ listSessions: () => sessions.listAllResolvedSessions(),
38958
+ isWarm: (session3) => transport.isSessionWarm(session3),
38959
+ events: controlEvents,
38960
+ logger
38961
+ }) : undefined;
38962
+ sessionWarmth?.start();
38803
38963
  const control = new ControlService({
38804
38964
  logger,
38805
38965
  agent: agent3,
@@ -38812,6 +38972,7 @@ async function buildApp(paths, deps = {}) {
38812
38972
  archiveSessionWithTransport: (internalAlias) => router3.archiveSessionWithTransport(internalAlias),
38813
38973
  unarchiveSession: (internalAlias) => router3.unarchiveSession(internalAlias),
38814
38974
  activeTurns,
38975
+ ...sessionWarmth ? { sessionWarmth } : {},
38815
38976
  scheduled: scheduledService,
38816
38977
  orchestration: orchestration3,
38817
38978
  events: controlEvents,
@@ -38947,6 +39108,7 @@ async function buildApp(paths, deps = {}) {
38947
39108
  reapStaleQueueOwners: () => reapWarmQueueOwners("startup"),
38948
39109
  dispose: async () => {
38949
39110
  scheduledScheduler.stop();
39111
+ sessionWarmth?.stop();
38950
39112
  configWatcher.close();
38951
39113
  clearInterval(uploadCleanupInterval);
38952
39114
  terminalService.disposeAll();
@@ -25,6 +25,10 @@ export interface ControlSessionInfo {
25
25
  transportSession: string;
26
26
  running: boolean;
27
27
  archived: boolean;
28
+ /** Whether the session's agent process is currently alive (next prompt responds
29
+ * without a cold start). Omitted when unknown — e.g. the transport can't observe
30
+ * liveness or the warmth tracker hasn't sampled this session yet. */
31
+ warm?: boolean;
28
32
  /** True when this logical session was attached to an existing agent-side (native) rollout
29
33
  * rather than freshly created. Mirrors LogicalSession.source === "agent-side"; omitted for
30
34
  * fresh xacpx sessions so the wire stays minimal. */
@@ -72,6 +76,8 @@ export interface ControlServiceDeps {
72
76
  updatedAt?: string;
73
77
  }) => Promise<ResolvedSession>;
74
78
  activeTurns: Pick<ActiveTurnRegistry, "isActiveAnywhere">;
79
+ /** Warmth tracker view for the cold-session indicator; absent ⇒ `warm` omitted from listings. */
80
+ sessionWarmth?: Pick<import("./session-warmth-tracker").SessionWarmthTracker, "isWarm" | "markWarm" | "markCold">;
75
81
  scheduled: Pick<ScheduledTaskService, "listPending" | "listRecentForChat" | "createTask" | "cancelPending">;
76
82
  orchestration: Pick<OrchestrationService, "listTasks" | "getTask" | "requestTaskCancellation">;
77
83
  events: ControlEventBus;
@@ -26,7 +26,7 @@ export interface TurnResult {
26
26
  }
27
27
  export declare class SessionTurnRunner {
28
28
  private readonly deps;
29
- constructor(deps: Pick<ControlServiceDeps, "agent" | "sessions" | "events" | "uploadStore">);
29
+ constructor(deps: Pick<ControlServiceDeps, "agent" | "sessions" | "events" | "uploadStore" | "sessionWarmth">);
30
30
  run(req: TurnRequest, signal: AbortSignal, onActivity?: () => void): Promise<TurnResult>;
31
31
  /** Resolve a chat-scoped display alias to its ResolvedSession and read whether it is
32
32
  * in stream reply mode. Best-effort: any resolution failure falls back to batched
@@ -0,0 +1,42 @@
1
+ import type { AppLogger } from "../logging/app-logger";
2
+ import type { ResolvedSession } from "../transport/types";
3
+ import type { ControlEventBus } from "./control-event-bus";
4
+ export interface SessionWarmthTrackerDeps {
5
+ listSessions: () => ResolvedSession[];
6
+ isWarm: (session: ResolvedSession) => Promise<boolean>;
7
+ events: ControlEventBus;
8
+ logger?: AppLogger;
9
+ intervalMs?: number;
10
+ setIntervalFn?: (fn: () => void | Promise<void>, delay: number) => unknown;
11
+ clearIntervalFn?: (timer: unknown) => void;
12
+ }
13
+ /**
14
+ * Polls queue-owner process liveness for every logical session and emits a
15
+ * payload-free `sessions-changed` control event whenever any session's warmth
16
+ * flips (e.g. a silent TTL expiry), so connected dashboards re-fetch the list
17
+ * and refresh their cold indicators. `isWarm` gives the last observed value
18
+ * synchronously for `control.sessions.list`.
19
+ */
20
+ export declare class SessionWarmthTracker {
21
+ private readonly listSessions;
22
+ private readonly checkWarm;
23
+ private readonly events;
24
+ private readonly logger?;
25
+ private readonly intervalMs;
26
+ private readonly setIntervalFn;
27
+ private readonly clearIntervalFn;
28
+ private readonly warmth;
29
+ private intervalHandle;
30
+ private ticking;
31
+ constructor(deps: SessionWarmthTrackerDeps);
32
+ start(): void;
33
+ stop(): void;
34
+ /** Last observed warmth for this session's transport; undefined until first checked. */
35
+ isWarm(session: ResolvedSession): boolean | undefined;
36
+ /** Immediate corrections from call sites that just changed warmth themselves
37
+ * (archive kills the owner; a starting turn warms it). No event — the caller's
38
+ * own flow already emits one. */
39
+ markWarm(session: ResolvedSession): void;
40
+ markCold(session: ResolvedSession): void;
41
+ tick(): Promise<void>;
42
+ }
@@ -232,6 +232,13 @@ export interface SessionTransport {
232
232
  * Optional: transports that can't reap omit it.
233
233
  */
234
234
  freeWarmProcess?(session: ResolvedSession): Promise<void>;
235
+ /**
236
+ * Whether this session's warm queue-owner process is currently alive (=
237
+ * the next prompt responds immediately instead of cold-starting). False
238
+ * when the owner exited via TTL, archive, or any other reason. Optional:
239
+ * transports that can't observe process liveness omit it.
240
+ */
241
+ isSessionWarm?(session: ResolvedSession): Promise<boolean>;
235
242
  /**
236
243
  * Read the underlying agent-native session id for an existing transport
237
244
  * 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.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",