@ganglion/xacpx 0.17.1 → 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.
|
@@ -5698,6 +5698,18 @@ function createDefaultQueueOwnerTerminator(_acpxCommand) {
|
|
|
5698
5698
|
await terminateAcpxQueueOwner(sessionId);
|
|
5699
5699
|
};
|
|
5700
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
|
+
}
|
|
5701
5713
|
async function terminateAcpxQueueOwner(sessionId) {
|
|
5702
5714
|
const lockPath = queueLockFilePath(sessionId);
|
|
5703
5715
|
let owner;
|
|
@@ -5740,6 +5752,88 @@ var init_acpx_queue_owner_launcher = __esm(() => {
|
|
|
5740
5752
|
init_i18n();
|
|
5741
5753
|
});
|
|
5742
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
|
+
|
|
5743
5837
|
// src/util/path.ts
|
|
5744
5838
|
import path2 from "node:path";
|
|
5745
5839
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -5782,10 +5876,10 @@ var init_path = __esm(() => {
|
|
|
5782
5876
|
// src/transport/codex-subagent-filter.ts
|
|
5783
5877
|
import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
|
|
5784
5878
|
import { homedir as homedir5 } from "node:os";
|
|
5785
|
-
import { join as
|
|
5879
|
+
import { join as join7 } from "node:path";
|
|
5786
5880
|
function resolveCodexHome(env = process.env) {
|
|
5787
5881
|
const fromEnv = env.CODEX_HOME?.trim();
|
|
5788
|
-
return fromEnv ? fromEnv :
|
|
5882
|
+
return fromEnv ? fromEnv : join7(homedir5(), ".codex");
|
|
5789
5883
|
}
|
|
5790
5884
|
function isPlainObject(value) {
|
|
5791
5885
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -5850,7 +5944,7 @@ function filterSubagentSessions(result, isSubagent) {
|
|
|
5850
5944
|
};
|
|
5851
5945
|
}
|
|
5852
5946
|
function nodeRolloutReader(home) {
|
|
5853
|
-
const root =
|
|
5947
|
+
const root = join7(home, "sessions");
|
|
5854
5948
|
return {
|
|
5855
5949
|
listRolloutPaths() {
|
|
5856
5950
|
const out = [];
|
|
@@ -5862,7 +5956,7 @@ function nodeRolloutReader(home) {
|
|
|
5862
5956
|
return;
|
|
5863
5957
|
}
|
|
5864
5958
|
for (const entry of entries) {
|
|
5865
|
-
const full =
|
|
5959
|
+
const full = join7(dir, entry.name);
|
|
5866
5960
|
if (entry.isDirectory())
|
|
5867
5961
|
walk(full);
|
|
5868
5962
|
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
|
|
@@ -5978,11 +6072,11 @@ var init_agent_session_list = __esm(() => {
|
|
|
5978
6072
|
// src/transport/acpx-session-files.ts
|
|
5979
6073
|
import { readdir, unlink as unlink2 } from "node:fs/promises";
|
|
5980
6074
|
import { homedir as homedir6 } from "node:os";
|
|
5981
|
-
import { join as
|
|
6075
|
+
import { join as join8 } from "node:path";
|
|
5982
6076
|
async function deleteAcpxSessionFiles(options) {
|
|
5983
|
-
const dir = options.sessionsDir ??
|
|
6077
|
+
const dir = options.sessionsDir ?? join8(homedir6(), ".acpx", "sessions");
|
|
5984
6078
|
const safeId = encodeURIComponent(options.acpxRecordId);
|
|
5985
|
-
await unlink2(
|
|
6079
|
+
await unlink2(join8(dir, `${safeId}.json`)).catch(() => {
|
|
5986
6080
|
return;
|
|
5987
6081
|
});
|
|
5988
6082
|
let entries;
|
|
@@ -5993,7 +6087,7 @@ async function deleteAcpxSessionFiles(options) {
|
|
|
5993
6087
|
}
|
|
5994
6088
|
const streamFiles = entries.filter((name) => name.startsWith(`${safeId}.stream.`));
|
|
5995
6089
|
for (const name of streamFiles) {
|
|
5996
|
-
await unlink2(
|
|
6090
|
+
await unlink2(join8(dir, name)).catch(() => {
|
|
5997
6091
|
return;
|
|
5998
6092
|
});
|
|
5999
6093
|
}
|
|
@@ -6260,7 +6354,7 @@ init_prompt_media();
|
|
|
6260
6354
|
init_streaming_prompt();
|
|
6261
6355
|
import { copyFile, readdir as readdir2 } from "node:fs/promises";
|
|
6262
6356
|
import { homedir as homedir7 } from "node:os";
|
|
6263
|
-
import { dirname as
|
|
6357
|
+
import { dirname as dirname4, join as join9, win32 } from "node:path";
|
|
6264
6358
|
import { spawn as spawn4 } from "node:child_process";
|
|
6265
6359
|
|
|
6266
6360
|
// src/bridge/parse-missing-optional-dep.ts
|
|
@@ -6279,6 +6373,7 @@ function parseMissingOptionalDep(text) {
|
|
|
6279
6373
|
// src/bridge/bridge-runtime.ts
|
|
6280
6374
|
init_discover_parent_package_paths();
|
|
6281
6375
|
init_acpx_queue_owner_launcher();
|
|
6376
|
+
init_daemon_files();
|
|
6282
6377
|
init_agent_session_list();
|
|
6283
6378
|
init_codex_subagent_filter();
|
|
6284
6379
|
init_acpx_session_files();
|
|
@@ -6343,6 +6438,7 @@ class BridgeRuntime {
|
|
|
6343
6438
|
});
|
|
6344
6439
|
}
|
|
6345
6440
|
async resumeAgentSession(input) {
|
|
6441
|
+
this.invalidateRecordIdCache(input);
|
|
6346
6442
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
6347
6443
|
"sessions",
|
|
6348
6444
|
"new",
|
|
@@ -6412,6 +6508,7 @@ class BridgeRuntime {
|
|
|
6412
6508
|
throw new Error(message);
|
|
6413
6509
|
}
|
|
6414
6510
|
async ensureSession(input, onProgress) {
|
|
6511
|
+
this.invalidateRecordIdCache(input);
|
|
6415
6512
|
try {
|
|
6416
6513
|
return await this.attemptEnsureSession(input, onProgress);
|
|
6417
6514
|
} catch (error) {
|
|
@@ -6526,7 +6623,7 @@ class BridgeRuntime {
|
|
|
6526
6623
|
const resolved = __require.resolve(`${candidate}/package.json`, {
|
|
6527
6624
|
paths: [process.cwd(), ...__require.resolve.paths(candidate) ?? []]
|
|
6528
6625
|
});
|
|
6529
|
-
return
|
|
6626
|
+
return dirname4(resolved);
|
|
6530
6627
|
} catch {
|
|
6531
6628
|
continue;
|
|
6532
6629
|
}
|
|
@@ -6713,6 +6810,7 @@ class BridgeRuntime {
|
|
|
6713
6810
|
};
|
|
6714
6811
|
}
|
|
6715
6812
|
async removeSession(input) {
|
|
6813
|
+
this.invalidateRecordIdCache(input);
|
|
6716
6814
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
6717
6815
|
"sessions",
|
|
6718
6816
|
"close",
|
|
@@ -6751,6 +6849,27 @@ class BridgeRuntime {
|
|
|
6751
6849
|
await terminateAcpxQueueOwner(acpxRecordId);
|
|
6752
6850
|
return {};
|
|
6753
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
|
+
}
|
|
6754
6873
|
async shutdown() {
|
|
6755
6874
|
return {};
|
|
6756
6875
|
}
|
|
@@ -6941,7 +7060,7 @@ async function tryRepairAcpxSessionIndex(deps = {}) {
|
|
|
6941
7060
|
if (!home) {
|
|
6942
7061
|
return false;
|
|
6943
7062
|
}
|
|
6944
|
-
const pathJoin = platform === "win32" ? win32.join :
|
|
7063
|
+
const pathJoin = platform === "win32" ? win32.join : join9;
|
|
6945
7064
|
const sessionsDir = pathJoin(home, ".acpx", "sessions");
|
|
6946
7065
|
const indexPath = pathJoin(sessionsDir, "index.json");
|
|
6947
7066
|
const readdirFn = deps.readdirFn ?? readdir2;
|
|
@@ -6991,6 +7110,7 @@ var BRIDGE_METHODS = new Set([
|
|
|
6991
7110
|
"removeSession",
|
|
6992
7111
|
"deleteSession",
|
|
6993
7112
|
"freeWarmProcess",
|
|
7113
|
+
"isSessionWarm",
|
|
6994
7114
|
"getAgentSessionId"
|
|
6995
7115
|
]);
|
|
6996
7116
|
var SESSION_SCOPED_METHODS = new Set([
|
|
@@ -7008,6 +7128,7 @@ var SESSION_SCOPED_METHODS = new Set([
|
|
|
7008
7128
|
"removeSession",
|
|
7009
7129
|
"deleteSession",
|
|
7010
7130
|
"freeWarmProcess",
|
|
7131
|
+
"isSessionWarm",
|
|
7011
7132
|
"getAgentSessionId"
|
|
7012
7133
|
]);
|
|
7013
7134
|
|
|
@@ -7068,7 +7189,7 @@ class BridgeServer {
|
|
|
7068
7189
|
if (!sessionKey) {
|
|
7069
7190
|
return await this.dispatch(requestId, method, params, writeLine);
|
|
7070
7191
|
}
|
|
7071
|
-
const lane = method === "cancel" ? "control" : "normal";
|
|
7192
|
+
const lane = method === "cancel" || method === "isSessionWarm" ? "control" : "normal";
|
|
7072
7193
|
return await this.scheduler.run(sessionKey, lane, () => this.dispatch(requestId, method, params, writeLine));
|
|
7073
7194
|
}
|
|
7074
7195
|
async dispatch(requestId, method, params, writeLine) {
|
|
@@ -7279,6 +7400,14 @@ class BridgeServer {
|
|
|
7279
7400
|
cwd: requireString(params, "cwd"),
|
|
7280
7401
|
name: requireString(params, "name")
|
|
7281
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
|
+
});
|
|
7282
7411
|
case "getAgentSessionId":
|
|
7283
7412
|
return await this.runtime.getAgentSessionId({
|
|
7284
7413
|
agent: requireString(params, "agent"),
|
package/dist/cli.js
CHANGED
|
@@ -32133,6 +32133,18 @@ function createDefaultQueueOwnerTerminator(_acpxCommand) {
|
|
|
32133
32133
|
await terminateAcpxQueueOwner(sessionId);
|
|
32134
32134
|
};
|
|
32135
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
|
+
}
|
|
32136
32148
|
async function terminateAcpxQueueOwner(sessionId) {
|
|
32137
32149
|
const lockPath = queueLockFilePath(sessionId);
|
|
32138
32150
|
let owner;
|
|
@@ -32989,6 +33001,10 @@ ${result.text}` : "" };
|
|
|
32989
33001
|
async freeWarmProcess(session3) {
|
|
32990
33002
|
await this.client.request("freeWarmProcess", this.toParams(session3));
|
|
32991
33003
|
}
|
|
33004
|
+
async isSessionWarm(session3) {
|
|
33005
|
+
const result = await this.client.request("isSessionWarm", this.toParams(session3));
|
|
33006
|
+
return result.warm === true;
|
|
33007
|
+
}
|
|
32992
33008
|
async getAgentSessionId(session3) {
|
|
32993
33009
|
const result = await this.client.request("getAgentSessionId", this.toParams(session3));
|
|
32994
33010
|
return result.agentSessionId;
|
|
@@ -34105,6 +34121,7 @@ class AcpxCliTransport {
|
|
|
34105
34121
|
this.resolveSpawnEnvironment = options.resolveSpawnEnvironment ?? resolveClaudeSpawnEnvironment;
|
|
34106
34122
|
}
|
|
34107
34123
|
async ensureSession(session3, _onProgress) {
|
|
34124
|
+
this.invalidateRecordIdCache(session3);
|
|
34108
34125
|
try {
|
|
34109
34126
|
await this.runEnsureSession(session3);
|
|
34110
34127
|
} catch (error2) {
|
|
@@ -34317,6 +34334,7 @@ ${baseText}` : "" };
|
|
|
34317
34334
|
};
|
|
34318
34335
|
}
|
|
34319
34336
|
async resumeAgentSession(session3, agentSessionId) {
|
|
34337
|
+
this.invalidateRecordIdCache(session3);
|
|
34320
34338
|
const args = this.buildArgs(session3, [
|
|
34321
34339
|
"sessions",
|
|
34322
34340
|
"new",
|
|
@@ -34337,6 +34355,7 @@ ${baseText}` : "" };
|
|
|
34337
34355
|
this.permissionPolicy = policy.permissionPolicy;
|
|
34338
34356
|
}
|
|
34339
34357
|
async removeSession(session3) {
|
|
34358
|
+
this.invalidateRecordIdCache(session3);
|
|
34340
34359
|
const result = await this.runCommandWithTimeout(this.runCommand, this.buildArgs(session3, [
|
|
34341
34360
|
"sessions",
|
|
34342
34361
|
"close",
|
|
@@ -34373,6 +34392,30 @@ ${baseText}` : "" };
|
|
|
34373
34392
|
}
|
|
34374
34393
|
await terminateAcpxQueueOwner(acpxRecordId);
|
|
34375
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
|
+
}
|
|
34376
34419
|
async hasSession(session3) {
|
|
34377
34420
|
const result = await this.runCommandWithTimeout(this.runCommand, this.buildArgs(session3, [
|
|
34378
34421
|
"sessions",
|
|
@@ -34705,6 +34748,7 @@ var init_acpx_cli_transport = __esm(() => {
|
|
|
34705
34748
|
init_node_pty_helper();
|
|
34706
34749
|
init_terminate_process_tree();
|
|
34707
34750
|
init_acpx_queue_owner_launcher();
|
|
34751
|
+
init_daemon_files();
|
|
34708
34752
|
init_agent_session_list();
|
|
34709
34753
|
init_codex_subagent_filter();
|
|
34710
34754
|
init_acpx_session_files();
|
|
@@ -36444,7 +36488,7 @@ class WorkspaceFs {
|
|
|
36444
36488
|
}
|
|
36445
36489
|
const files = [];
|
|
36446
36490
|
try {
|
|
36447
|
-
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" });
|
|
36448
36492
|
const fields = stdout2.split("\x00");
|
|
36449
36493
|
for (let i = 0;i < fields.length; i++) {
|
|
36450
36494
|
const field = fields[i];
|
|
@@ -36672,9 +36716,10 @@ class WorkspaceGit {
|
|
|
36672
36716
|
return (await this.runRaw(root, args)).trim();
|
|
36673
36717
|
}
|
|
36674
36718
|
async runRaw(root, args) {
|
|
36719
|
+
const fullArgs = ["-C", root, "-c", "gc.auto=0", ...args];
|
|
36675
36720
|
if (this.runGitOverride)
|
|
36676
|
-
return await this.runGitOverride(root,
|
|
36677
|
-
const result = await execFileAsync2("git",
|
|
36721
|
+
return await this.runGitOverride(root, fullArgs);
|
|
36722
|
+
const result = await execFileAsync2("git", fullArgs, {
|
|
36678
36723
|
timeout: GIT_TIMEOUT_MS2,
|
|
36679
36724
|
killSignal: "SIGKILL",
|
|
36680
36725
|
maxBuffer: GIT_MAX_BUFFER2
|
|
@@ -36950,19 +36995,21 @@ class WorkspaceGit {
|
|
|
36950
36995
|
...worktreePath ? { worktreePath } : {}
|
|
36951
36996
|
};
|
|
36952
36997
|
});
|
|
36953
|
-
const statusRaw = await this.runRaw(root, ["-c", "core.quotePath=false", "status", "--porcelain", "-z"]);
|
|
36954
36998
|
const files = [];
|
|
36955
|
-
|
|
36956
|
-
|
|
36957
|
-
const
|
|
36958
|
-
|
|
36959
|
-
|
|
36960
|
-
|
|
36961
|
-
|
|
36962
|
-
|
|
36963
|
-
|
|
36964
|
-
|
|
36965
|
-
|
|
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 {}
|
|
36966
37013
|
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
36967
37014
|
return {
|
|
36968
37015
|
workspace: workspace3,
|
|
@@ -37069,6 +37116,11 @@ class SessionTurnRunner {
|
|
|
37069
37116
|
if (wasArchived) {
|
|
37070
37117
|
this.deps.events.emit({ type: "sessions-changed" });
|
|
37071
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
|
+
}
|
|
37072
37124
|
this.deps.events.emit({
|
|
37073
37125
|
type: "turn-started",
|
|
37074
37126
|
chatKey: req.chatKey,
|
|
@@ -37685,6 +37737,7 @@ class ControlService {
|
|
|
37685
37737
|
if (!session3)
|
|
37686
37738
|
throw new Error("session not found");
|
|
37687
37739
|
await this.deps.sessions.setDisplayName(session3.alias, displayName);
|
|
37740
|
+
this.deps.events.emit({ type: "sessions-changed" });
|
|
37688
37741
|
}
|
|
37689
37742
|
async resolveControlSession(chatKey, alias) {
|
|
37690
37743
|
const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
|
|
@@ -37695,17 +37748,22 @@ class ControlService {
|
|
|
37695
37748
|
}
|
|
37696
37749
|
listSessions(chatKey) {
|
|
37697
37750
|
const channelId = getChannelIdFromChatKey(chatKey);
|
|
37698
|
-
return this.deps.sessions.listAllResolvedSessions().filter((session3) => isSessionAliasVisibleInChannel(session3.alias, channelId)).map((session3) =>
|
|
37699
|
-
|
|
37700
|
-
|
|
37701
|
-
|
|
37702
|
-
|
|
37703
|
-
|
|
37704
|
-
|
|
37705
|
-
|
|
37706
|
-
|
|
37707
|
-
|
|
37708
|
-
|
|
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
|
+
});
|
|
37709
37767
|
}
|
|
37710
37768
|
async listNativeSessions(_chatKey, agent3, workspace3) {
|
|
37711
37769
|
const sessions = await this.deps.listNativeSessions(agent3, workspace3);
|
|
@@ -37747,6 +37805,11 @@ class ControlService {
|
|
|
37747
37805
|
async archiveSession(chatKey, alias) {
|
|
37748
37806
|
const internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
|
|
37749
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);
|
|
37750
37813
|
this.deps.events.emit({ type: "sessions-changed" });
|
|
37751
37814
|
}
|
|
37752
37815
|
async unarchiveSession(chatKey, alias) {
|
|
@@ -37894,6 +37957,98 @@ var init_control_service = __esm(() => {
|
|
|
37894
37957
|
MODEL_SET_SETTLE_BUDGET_MS = 2 * (DEFAULT_MANAGEMENT_COMMAND_TIMEOUT_MS + BRIDGE_REQUEST_TIMEOUT_GRACE_MS);
|
|
37895
37958
|
});
|
|
37896
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
|
+
|
|
37897
38052
|
// src/control/terminal-service.ts
|
|
37898
38053
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
37899
38054
|
import { createRequire as createRequire6 } from "node:module";
|
|
@@ -38798,6 +38953,13 @@ async function buildApp(paths, deps = {}) {
|
|
|
38798
38953
|
const loadAgentRegistry = createAcpxAgentRegistryLoader({ logger });
|
|
38799
38954
|
uploadStore.cleanup();
|
|
38800
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();
|
|
38801
38963
|
const control = new ControlService({
|
|
38802
38964
|
logger,
|
|
38803
38965
|
agent: agent3,
|
|
@@ -38810,6 +38972,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
38810
38972
|
archiveSessionWithTransport: (internalAlias) => router3.archiveSessionWithTransport(internalAlias),
|
|
38811
38973
|
unarchiveSession: (internalAlias) => router3.unarchiveSession(internalAlias),
|
|
38812
38974
|
activeTurns,
|
|
38975
|
+
...sessionWarmth ? { sessionWarmth } : {},
|
|
38813
38976
|
scheduled: scheduledService,
|
|
38814
38977
|
orchestration: orchestration3,
|
|
38815
38978
|
events: controlEvents,
|
|
@@ -38945,6 +39108,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
38945
39108
|
reapStaleQueueOwners: () => reapWarmQueueOwners("startup"),
|
|
38946
39109
|
dispose: async () => {
|
|
38947
39110
|
scheduledScheduler.stop();
|
|
39111
|
+
sessionWarmth?.stop();
|
|
38948
39112
|
configWatcher.close();
|
|
38949
39113
|
clearInterval(uploadCleanupInterval);
|
|
38950
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
|