weacpx 0.4.9 → 0.4.10

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.
@@ -1042,6 +1042,13 @@ function normalizeBridgePermissionMode(value) {
1042
1042
  function normalizeBridgeNonInteractivePermissions(value) {
1043
1043
  return value === "deny" || value === "fail" ? value : "deny";
1044
1044
  }
1045
+ function normalizeBridgeQueueOwnerTtlSeconds(value) {
1046
+ if (value === undefined) {
1047
+ return;
1048
+ }
1049
+ const parsed = Number(value);
1050
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
1051
+ }
1045
1052
 
1046
1053
  // src/bridge/bridge-server.ts
1047
1054
  init_prompt_output();
@@ -1125,7 +1132,8 @@ class BridgeRuntime {
1125
1132
  queueOwnerLauncher;
1126
1133
  acpxVerboseSupported = undefined;
1127
1134
  constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner, repairSessionIndex = tryRepairAcpxSessionIndex, queueOwnerLauncher = new AcpxQueueOwnerLauncher({
1128
- acpxCommand: command
1135
+ acpxCommand: command,
1136
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { ttlMs: options.queueOwnerTtlSeconds * 1000 } : {}
1129
1137
  })) {
1130
1138
  this.command = command;
1131
1139
  this.run = run;
@@ -1381,13 +1389,21 @@ class BridgeRuntime {
1381
1389
  "--json-strict",
1382
1390
  "--cwd",
1383
1391
  input.cwd,
1384
- ...this.buildPermissionArgs()
1392
+ ...this.buildPermissionArgs(),
1393
+ ...this.buildQueueOwnerTtlArgs()
1385
1394
  ];
1386
1395
  if (input.agentCommand) {
1387
1396
  return [...prefix, "--agent", input.agentCommand, ...tail];
1388
1397
  }
1389
1398
  return [...prefix, input.agent, ...tail];
1390
1399
  }
1400
+ buildQueueOwnerTtlArgs() {
1401
+ const ttl = this.options.queueOwnerTtlSeconds;
1402
+ if (typeof ttl !== "number" || !Number.isFinite(ttl)) {
1403
+ return [];
1404
+ }
1405
+ return ["--ttl", String(ttl)];
1406
+ }
1391
1407
  buildPermissionArgs() {
1392
1408
  const permissionMode = this.options.permissionMode ?? "approve-all";
1393
1409
  const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
@@ -1924,7 +1940,8 @@ async function processBridgeInput(options) {
1924
1940
  async function runBridgeMain() {
1925
1941
  const server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
1926
1942
  permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
1927
- nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
1943
+ nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS),
1944
+ queueOwnerTtlSeconds: normalizeBridgeQueueOwnerTtlSeconds(process.env.WEACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS)
1928
1945
  }));
1929
1946
  const input = createInterface({
1930
1947
  input: process.stdin,
package/dist/cli.js CHANGED
@@ -1999,6 +1999,7 @@ var require_lib = __commonJS((exports, module) => {
1999
1999
 
2000
2000
  // src/util/private-file.ts
2001
2001
  import { chmod, mkdir, writeFile } from "node:fs/promises";
2002
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2002
2003
  import { dirname } from "node:path";
2003
2004
  async function writePrivateFileAtomic(path, content) {
2004
2005
  await mkdir(dirname(path), { recursive: true });
@@ -2031,6 +2032,25 @@ async function writePrivateFileAtomic(path, content) {
2031
2032
  await release();
2032
2033
  }
2033
2034
  }
2035
+ function writePrivateFileSync(path, content, deps = {}) {
2036
+ mkdirSync(dirname(path), { recursive: true });
2037
+ const platform = deps.platform ?? process.platform;
2038
+ const atomicWrite = deps.atomicWrite ?? ((p, c) => import_write_file_atomic.default.sync(p, c, { mode: PRIVATE_FILE_MODE, encoding: "utf8", fsync: true }));
2039
+ try {
2040
+ atomicWrite(path, content);
2041
+ } catch (error) {
2042
+ if (!isTransientWriteError(error, platform)) {
2043
+ throw error;
2044
+ }
2045
+ const directWrite = deps.directWrite ?? ((p, c) => {
2046
+ writeFileSync(p, c, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
2047
+ try {
2048
+ chmodSync(p, PRIVATE_FILE_MODE);
2049
+ } catch {}
2050
+ });
2051
+ directWrite(path, content);
2052
+ }
2053
+ }
2034
2054
  async function retryTransientWriteErrors(run, options = {}) {
2035
2055
  const platform = options.platform ?? process.platform;
2036
2056
  const maxAttempts = options.maxAttempts ?? WRITE_RETRY_MAX_ATTEMPTS;
@@ -2206,6 +2226,9 @@ function parseConfig(raw, options = {}) {
2206
2226
  throw new Error("transport.permissionPolicy must be a non-empty string");
2207
2227
  }
2208
2228
  }
2229
+ if ("queueOwnerTtlSeconds" in transport && (typeof transport.queueOwnerTtlSeconds !== "number" || !Number.isFinite(transport.queueOwnerTtlSeconds) || transport.queueOwnerTtlSeconds < 0)) {
2230
+ throw new Error("transport.queueOwnerTtlSeconds must be a non-negative number (0 = keep alive forever)");
2231
+ }
2209
2232
  if (!isRecord(raw.agents)) {
2210
2233
  throw new Error("agents must be an object");
2211
2234
  }
@@ -2302,7 +2325,8 @@ function parseConfig(raw, options = {}) {
2302
2325
  ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
2303
2326
  type: transportType,
2304
2327
  permissionMode,
2305
- nonInteractivePermissions
2328
+ nonInteractivePermissions,
2329
+ queueOwnerTtlSeconds: typeof transport.queueOwnerTtlSeconds === "number" ? transport.queueOwnerTtlSeconds : DEFAULT_QUEUE_OWNER_TTL_SECONDS
2306
2330
  },
2307
2331
  logging: {
2308
2332
  level: resolvedLoggingLevel,
@@ -2428,7 +2452,7 @@ function parseOrchestrationConfig(raw) {
2428
2452
  maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
2429
2453
  };
2430
2454
  }
2431
- var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2455
+ var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_QUEUE_OWNER_TTL_SECONDS = 1800, DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2432
2456
  var init_load_config = __esm(() => {
2433
2457
  init_workspace_path();
2434
2458
  DEFAULT_PERF_LOG_CONFIG = {
@@ -2729,7 +2753,7 @@ class DaemonStatusStore {
2729
2753
  var init_daemon_status = () => {};
2730
2754
 
2731
2755
  // src/daemon/daemon-controller.ts
2732
- import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
2756
+ import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
2733
2757
  import { dirname as dirname3 } from "node:path";
2734
2758
 
2735
2759
  class DaemonController {
@@ -2788,9 +2812,19 @@ class DaemonController {
2788
2812
  if (current.state === "indeterminate") {
2789
2813
  throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
2790
2814
  }
2791
- await this.statusStore.clear();
2792
- const pid = await this.deps.spawnDetached(options);
2793
- await this.writePid(pid);
2815
+ const pidHandle = await this.openPidFileExclusive();
2816
+ let pid;
2817
+ try {
2818
+ await this.statusStore.clear();
2819
+ pid = await this.deps.spawnDetached(options);
2820
+ await pidHandle.write(`${pid}
2821
+ `);
2822
+ } catch (error) {
2823
+ await pidHandle.close().catch(() => {});
2824
+ await rm2(this.paths.pidFile, { force: true }).catch(() => {});
2825
+ throw error;
2826
+ }
2827
+ await pidHandle.close();
2794
2828
  await this.waitForStartupMetadata(pid, options.firstRunOnboarding ? this.onboardingStartupTimeoutMs : this.startupTimeoutMs, options.startupWait);
2795
2829
  return { state: "started", pid };
2796
2830
  }
@@ -2818,10 +2852,16 @@ class DaemonController {
2818
2852
  throw error;
2819
2853
  }
2820
2854
  }
2821
- async writePid(pid) {
2855
+ async openPidFileExclusive() {
2822
2856
  await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
2823
- await writeFile3(this.paths.pidFile, `${pid}
2824
- `);
2857
+ try {
2858
+ return await open(this.paths.pidFile, "wx", 384);
2859
+ } catch (error) {
2860
+ if (error.code === "EEXIST") {
2861
+ throw new Error(`weacpx daemon pid file already exists (${this.paths.pidFile}); another start may be in progress`);
2862
+ }
2863
+ throw error;
2864
+ }
2825
2865
  }
2826
2866
  async clearRuntimeFiles() {
2827
2867
  await rm2(this.paths.pidFile, { force: true });
@@ -2918,15 +2958,17 @@ async function defaultRunProcessCommand(command, args) {
2918
2958
  var init_terminate_process_tree = () => {};
2919
2959
 
2920
2960
  // src/daemon/create-daemon-controller.ts
2921
- import { mkdir as mkdir4, open } from "node:fs/promises";
2961
+ import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
2922
2962
  import { spawn as spawn2 } from "node:child_process";
2923
2963
  function createDaemonController(paths, options) {
2924
2964
  return new DaemonController(paths, {
2925
2965
  isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
2926
2966
  spawnDetached: async (spawnOptions) => {
2927
2967
  await mkdir4(paths.runtimeDir, { recursive: true });
2928
- const stdoutHandle = await open(paths.stdoutLog, "a");
2929
- const stderrHandle = await open(paths.stderrLog, "a");
2968
+ const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
2969
+ const stderrHandle = await open2(paths.stderrLog, "a", 384);
2970
+ await stdoutHandle.chmod(384).catch(() => {});
2971
+ await stderrHandle.chmod(384).catch(() => {});
2930
2972
  try {
2931
2973
  return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd, spawnOptions));
2932
2974
  } finally {
@@ -10014,7 +10056,7 @@ var init_state_store = __esm(() => {
10014
10056
  });
10015
10057
 
10016
10058
  // src/plugins/plugin-home.ts
10017
- import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
10059
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
10018
10060
  import { homedir as homedir3 } from "node:os";
10019
10061
  import { join as join3 } from "node:path";
10020
10062
  function coerceMissing(value) {
@@ -10040,7 +10082,7 @@ function resolvePluginHome(input = {}) {
10040
10082
  }
10041
10083
  async function ensurePluginHome(pluginHome) {
10042
10084
  await mkdir6(pluginHome, { recursive: true, mode: 448 });
10043
- await writeFile5(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10085
+ await writeFile4(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10044
10086
  `, { flag: "wx" }).catch((error2) => {
10045
10087
  if (error2.code !== "EEXIST")
10046
10088
  throw error2;
@@ -10179,8 +10221,6 @@ function loadWeixinAccount(accountId) {
10179
10221
  return null;
10180
10222
  }
10181
10223
  function saveWeixinAccount(accountId, update) {
10182
- const dir = resolveAccountsDir();
10183
- ensureDirSync(dir);
10184
10224
  const existing = loadWeixinAccount(accountId) ?? {};
10185
10225
  const token = update.token?.trim() || existing.token;
10186
10226
  const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
@@ -10190,11 +10230,7 @@ function saveWeixinAccount(accountId, update) {
10190
10230
  ...baseUrl ? { baseUrl } : {},
10191
10231
  ...userId ? { userId } : {}
10192
10232
  };
10193
- const filePath = resolveAccountPath(accountId);
10194
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
10195
- try {
10196
- fs3.chmodSync(filePath, 384);
10197
- } catch {}
10233
+ writePrivateFileSync(resolveAccountPath(accountId), JSON.stringify(data, null, 2));
10198
10234
  }
10199
10235
  function clearWeixinAccount(accountId) {
10200
10236
  try {
@@ -10295,6 +10331,7 @@ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://
10295
10331
  var init_accounts = __esm(() => {
10296
10332
  init_ensure_dir();
10297
10333
  init_state_dir();
10334
+ init_private_file();
10298
10335
  });
10299
10336
 
10300
10337
  // src/weixin/util/logger.ts
@@ -12298,8 +12335,7 @@ function persistContextTokens(accountId) {
12298
12335
  }
12299
12336
  const filePath = resolveContextTokenFilePath(accountId);
12300
12337
  try {
12301
- fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
12302
- fs5.writeFileSync(filePath, JSON.stringify(tokens), "utf-8");
12338
+ writePrivateFileSync(filePath, JSON.stringify(tokens));
12303
12339
  } catch (err) {
12304
12340
  logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
12305
12341
  }
@@ -12422,6 +12458,7 @@ var init_inbound = __esm(() => {
12422
12458
  init_random();
12423
12459
  init_types2();
12424
12460
  init_state_dir();
12461
+ init_private_file();
12425
12462
  contextTokenStore = new Map;
12426
12463
  });
12427
12464
 
@@ -12544,7 +12581,7 @@ function createConversationExecutor() {
12544
12581
  }
12545
12582
 
12546
12583
  // src/channels/media-store.ts
12547
- import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
12584
+ import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile5 } from "node:fs/promises";
12548
12585
  import path7 from "node:path";
12549
12586
 
12550
12587
  class RuntimeMediaStore {
@@ -12568,7 +12605,7 @@ class RuntimeMediaStore {
12568
12605
  if (!isPathInside(resolvedFile, resolvedRoot)) {
12569
12606
  throw new Error("media path escapes runtime media root");
12570
12607
  }
12571
- await writeFile6(resolvedFile, input.buffer);
12608
+ await writeFile5(resolvedFile, input.buffer);
12572
12609
  return {
12573
12610
  kind: input.kind,
12574
12611
  filePath: resolvedFile,
@@ -14789,13 +14826,11 @@ function loadGetUpdatesBuf(filePath) {
14789
14826
  return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
14790
14827
  }
14791
14828
  function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
14792
- const dir = path13.dirname(filePath);
14793
- ensureDirSync(dir);
14794
- fs10.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
14829
+ writePrivateFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0));
14795
14830
  }
14796
14831
  var init_sync_buf = __esm(() => {
14797
14832
  init_accounts();
14798
- init_ensure_dir();
14833
+ init_private_file();
14799
14834
  init_state_dir();
14800
14835
  });
14801
14836
 
@@ -15408,7 +15443,7 @@ var init_deliver_coordinator_message = __esm(() => {
15408
15443
  });
15409
15444
 
15410
15445
  // src/weixin/monitor/consumer-lock.ts
15411
- import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15446
+ import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15412
15447
  import { dirname as dirname8, join as join5 } from "node:path";
15413
15448
  import { homedir as homedir4 } from "node:os";
15414
15449
  function createWeixinConsumerLock(options = {}) {
@@ -15420,7 +15455,7 @@ function createWeixinConsumerLock(options = {}) {
15420
15455
  await mkdir8(dirname8(lockFilePath), { recursive: true });
15421
15456
  while (true) {
15422
15457
  try {
15423
- const handle = await open2(lockFilePath, "wx");
15458
+ const handle = await open3(lockFilePath, "wx");
15424
15459
  try {
15425
15460
  await handle.writeFile(`${JSON.stringify(meta2, null, 2)}
15426
15461
  `, "utf8");
@@ -16105,21 +16140,27 @@ async function loadConfiguredPlugins(input) {
16105
16140
  const importPlugin = input.importPlugin ?? importPluginFromHome;
16106
16141
  const loaded = [];
16107
16142
  for (const config2 of enabled) {
16108
- let moduleValue;
16109
16143
  try {
16110
- moduleValue = await importPlugin(config2.name, pluginHome);
16144
+ let moduleValue;
16145
+ try {
16146
+ moduleValue = await importPlugin(config2.name, pluginHome);
16147
+ } catch (error2) {
16148
+ const message = error2 instanceof Error ? error2.message : String(error2);
16149
+ throw new Error(`failed to load plugin ${config2.name}: ${message}`);
16150
+ }
16151
+ const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
16152
+ ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
16153
+ });
16154
+ const channels = plugin.channels ?? [];
16155
+ for (const channel of channels) {
16156
+ registerChannelPlugin(channel);
16157
+ }
16158
+ loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
16111
16159
  } catch (error2) {
16112
- const message = error2 instanceof Error ? error2.message : String(error2);
16113
- throw new Error(`failed to load plugin ${config2.name}: ${message}`);
16114
- }
16115
- const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
16116
- ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
16117
- });
16118
- const channels = plugin.channels ?? [];
16119
- for (const channel of channels) {
16120
- registerChannelPlugin(channel);
16160
+ if (!input.onPluginError)
16161
+ throw error2;
16162
+ input.onPluginError({ name: config2.name, error: error2 });
16121
16163
  }
16122
- loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
16123
16164
  }
16124
16165
  return loaded;
16125
16166
  }
@@ -16140,7 +16181,7 @@ var init_bootstrap = __esm(() => {
16140
16181
  });
16141
16182
 
16142
16183
  // src/logging/app-logger.ts
16143
- import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
16184
+ import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
16144
16185
  import { dirname as dirname10 } from "node:path";
16145
16186
  function createNoopAppLogger() {
16146
16187
  return {
@@ -16154,6 +16195,7 @@ function createNoopAppLogger() {
16154
16195
  function createAppLogger(options) {
16155
16196
  const now = options.now ?? (() => new Date);
16156
16197
  let writeChain = Promise.resolve();
16198
+ let modeEnsured = false;
16157
16199
  return {
16158
16200
  debug: async (event, message, context) => {
16159
16201
  await enqueueWrite("debug", event, message, context);
@@ -16182,8 +16224,12 @@ function createAppLogger(options) {
16182
16224
  }
16183
16225
  const line = formatLogLine(now(), level, event, message, context);
16184
16226
  await mkdir9(dirname10(options.filePath), { recursive: true });
16227
+ if (!modeEnsured) {
16228
+ modeEnsured = true;
16229
+ await chmod2(options.filePath, 384).catch(() => {});
16230
+ }
16185
16231
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
16186
- await appendFile(options.filePath, line, "utf8");
16232
+ await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
16187
16233
  }
16188
16234
  }
16189
16235
  function formatLogLine(time3, level, event, message, context) {
@@ -23934,6 +23980,20 @@ class SessionService {
23934
23980
  async createSession(alias, agent, workspace) {
23935
23981
  return await this.createLogicalSession(alias, agent, workspace, `${workspace}:${alias}`);
23936
23982
  }
23983
+ listAllResolvedSessions() {
23984
+ const seen = new Set;
23985
+ const resolved = [];
23986
+ for (const session of Object.values(this.state.sessions)) {
23987
+ if (seen.has(session.transport_session)) {
23988
+ continue;
23989
+ }
23990
+ seen.add(session.transport_session);
23991
+ try {
23992
+ resolved.push(this.toResolvedSession(session));
23993
+ } catch {}
23994
+ }
23995
+ return resolved;
23996
+ }
23937
23997
  resolveSession(alias, agent, workspace, transportSession) {
23938
23998
  this.validateSession(alias, agent, workspace);
23939
23999
  return this.toResolvedSession({
@@ -24621,7 +24681,8 @@ async function spawnAcpxBridgeClient(options = {}) {
24621
24681
  ...process.env,
24622
24682
  WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
24623
24683
  WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
24624
- WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
24684
+ WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
24685
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { WEACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
24625
24686
  },
24626
24687
  stdio: ["pipe", "pipe", "inherit"]
24627
24688
  });
@@ -25049,7 +25110,7 @@ var init_spawn_command = __esm(() => {
25049
25110
  });
25050
25111
 
25051
25112
  // src/transport/prompt-media.ts
25052
- import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
25113
+ import { mkdtemp, open as open4, rm as rm8, writeFile as writeFile7 } from "node:fs/promises";
25053
25114
  import { tmpdir as defaultTmpdir } from "node:os";
25054
25115
  import path14 from "node:path";
25055
25116
  import { pathToFileURL as pathToFileURL2 } from "node:url";
@@ -25118,7 +25179,7 @@ async function writeStructuredPromptBlocks(blocks, deps) {
25118
25179
  }
25119
25180
  }
25120
25181
  async function readImageFileBounded(filePath, maxBytes) {
25121
- const handle = await open3(filePath, "r");
25182
+ const handle = await open4(filePath, "r");
25122
25183
  try {
25123
25184
  const imageStats = await handle.stat();
25124
25185
  if (!imageStats.isFile()) {
@@ -25173,7 +25234,7 @@ var init_prompt_media = __esm(() => {
25173
25234
  defaultStructuredPromptFileDeps = {
25174
25235
  readImageFile: readImageFileBounded,
25175
25236
  mkdtemp,
25176
- writeFile: writeFile8,
25237
+ writeFile: writeFile7,
25177
25238
  rm: rm8,
25178
25239
  tmpdir: defaultTmpdir
25179
25240
  };
@@ -25480,12 +25541,12 @@ function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
25480
25541
  }
25481
25542
  return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
25482
25543
  }
25483
- async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
25544
+ async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
25484
25545
  if (!helperPath) {
25485
25546
  return;
25486
25547
  }
25487
25548
  try {
25488
- await chmod2(helperPath, 493);
25549
+ await chmod3(helperPath, 493);
25489
25550
  } catch (error2) {
25490
25551
  if (error2.code === "ENOENT") {
25491
25552
  return;
@@ -25778,6 +25839,7 @@ class AcpxCliTransport {
25778
25839
  permissionMode;
25779
25840
  nonInteractivePermissions;
25780
25841
  permissionPolicy;
25842
+ queueOwnerTtlSeconds;
25781
25843
  runCommand;
25782
25844
  runPtyCommand;
25783
25845
  queueOwnerLauncher;
@@ -25788,10 +25850,12 @@ class AcpxCliTransport {
25788
25850
  this.permissionMode = options.permissionMode ?? "approve-all";
25789
25851
  this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
25790
25852
  this.permissionPolicy = options.permissionPolicy;
25853
+ this.queueOwnerTtlSeconds = options.queueOwnerTtlSeconds;
25791
25854
  this.runCommand = runCommand;
25792
25855
  this.runPtyCommand = runPtyCommand;
25793
25856
  this.queueOwnerLauncher = queueOwnerLauncher ?? new AcpxQueueOwnerLauncher({
25794
- acpxCommand: this.command
25857
+ acpxCommand: this.command,
25858
+ ...typeof this.queueOwnerTtlSeconds === "number" && Number.isFinite(this.queueOwnerTtlSeconds) ? { ttlMs: this.queueOwnerTtlSeconds * 1000 } : {}
25795
25859
  });
25796
25860
  this.streamingHooks = streamingHooks;
25797
25861
  }
@@ -26115,7 +26179,8 @@ ${baseText}` : "" };
26115
26179
  "--json-strict",
26116
26180
  "--cwd",
26117
26181
  session.cwd,
26118
- ...this.buildPermissionArgs()
26182
+ ...this.buildPermissionArgs(),
26183
+ ...this.buildQueueOwnerTtlArgs()
26119
26184
  ];
26120
26185
  const tail2 = promptFile ? ["prompt", "-s", session.transportSession, "--file", promptFile] : ["prompt", "-s", session.transportSession, text];
26121
26186
  if (session.agentCommand) {
@@ -26123,6 +26188,12 @@ ${baseText}` : "" };
26123
26188
  }
26124
26189
  return [...prefix, session.agent, ...tail2];
26125
26190
  }
26191
+ buildQueueOwnerTtlArgs() {
26192
+ if (typeof this.queueOwnerTtlSeconds !== "number" || !Number.isFinite(this.queueOwnerTtlSeconds)) {
26193
+ return [];
26194
+ }
26195
+ return ["--ttl", String(this.queueOwnerTtlSeconds)];
26196
+ }
26126
26197
  buildPermissionArgs() {
26127
26198
  const modeFlag = permissionModeToFlag(this.permissionMode);
26128
26199
  const args = [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
@@ -26169,6 +26240,146 @@ var init_acpx_cli_transport = __esm(() => {
26169
26240
  require4 = createRequire5(import.meta.url);
26170
26241
  });
26171
26242
 
26243
+ // src/transport/queue-owner-reaper.ts
26244
+ import { spawn as spawn10 } from "node:child_process";
26245
+ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
26246
+ const resolveRecordId = deps.resolveRecordId ?? defaultResolveRecordId;
26247
+ const terminate = deps.terminate ?? terminateAcpxQueueOwner;
26248
+ const timeoutMs = deps.timeoutMs ?? 5000;
26249
+ const seen = new Set;
26250
+ const unique = targets.filter((target) => {
26251
+ if (seen.has(target.transportSession)) {
26252
+ return false;
26253
+ }
26254
+ seen.add(target.transportSession);
26255
+ return true;
26256
+ });
26257
+ let terminated = 0;
26258
+ const reapOne = async (target) => {
26259
+ try {
26260
+ const recordId = await resolveRecordId(acpxCommand, target);
26261
+ if (!recordId) {
26262
+ return;
26263
+ }
26264
+ await terminate(recordId);
26265
+ terminated += 1;
26266
+ } catch (error2) {
26267
+ deps.onError?.(target, error2);
26268
+ }
26269
+ };
26270
+ await settleWithinTimeout(Promise.all(unique.map(reapOne)), timeoutMs);
26271
+ return { terminated, attempted: unique.length };
26272
+ }
26273
+ function settleWithinTimeout(work, timeoutMs) {
26274
+ return new Promise((resolve3) => {
26275
+ let settled = false;
26276
+ const finish = () => {
26277
+ if (!settled) {
26278
+ settled = true;
26279
+ resolve3();
26280
+ }
26281
+ };
26282
+ const timer = setTimeout(finish, timeoutMs);
26283
+ if (typeof timer.unref === "function") {
26284
+ timer.unref();
26285
+ }
26286
+ work.then(() => {
26287
+ clearTimeout(timer);
26288
+ finish();
26289
+ }, () => {
26290
+ clearTimeout(timer);
26291
+ finish();
26292
+ });
26293
+ });
26294
+ }
26295
+ async function defaultResolveRecordId(acpxCommand, target) {
26296
+ const args = [
26297
+ "--format",
26298
+ "quiet",
26299
+ "--cwd",
26300
+ target.cwd,
26301
+ ...target.agentCommand ? ["--agent", target.agentCommand] : [target.agent],
26302
+ "sessions",
26303
+ "show",
26304
+ target.transportSession
26305
+ ];
26306
+ const spawnSpec = resolveSpawnCommand(acpxCommand, args);
26307
+ const result = await runCapture2(spawnSpec.command, spawnSpec.args, 4000);
26308
+ if (result.code !== 0) {
26309
+ return null;
26310
+ }
26311
+ return parseRecordId(result.stdout);
26312
+ }
26313
+ function parseRecordId(stdout2) {
26314
+ try {
26315
+ const parsed = JSON.parse(stdout2);
26316
+ if (typeof parsed.acpxRecordId === "string") {
26317
+ return parsed.acpxRecordId;
26318
+ }
26319
+ if (typeof parsed.id === "string") {
26320
+ return parsed.id;
26321
+ }
26322
+ } catch {
26323
+ const firstLine = stdout2.trim().split(/\r?\n/, 1)[0];
26324
+ if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
26325
+ return firstLine;
26326
+ }
26327
+ }
26328
+ return null;
26329
+ }
26330
+ function runCapture2(command, args, timeoutMs) {
26331
+ return new Promise((resolve3) => {
26332
+ const child = spawn10(command, args, { stdio: ["ignore", "pipe", "ignore"] });
26333
+ let stdout2 = "";
26334
+ let done = false;
26335
+ const finish = (code) => {
26336
+ if (done) {
26337
+ return;
26338
+ }
26339
+ done = true;
26340
+ clearTimeout(timer);
26341
+ resolve3({ code, stdout: stdout2 });
26342
+ };
26343
+ const timer = setTimeout(() => {
26344
+ child.kill("SIGKILL");
26345
+ finish(1);
26346
+ }, timeoutMs);
26347
+ child.stdout?.on("data", (chunk) => {
26348
+ stdout2 += String(chunk);
26349
+ });
26350
+ child.once("error", () => finish(1));
26351
+ child.once("close", (code) => finish(code ?? 1));
26352
+ });
26353
+ }
26354
+ var init_queue_owner_reaper = __esm(() => {
26355
+ init_spawn_command();
26356
+ init_acpx_queue_owner_launcher();
26357
+ });
26358
+
26359
+ // src/transport/collect-reap-targets.ts
26360
+ function workerBindingReapTargets(orchestration, config2) {
26361
+ const targets = [];
26362
+ for (const [workerSession, binding] of Object.entries(orchestration.workerBindings)) {
26363
+ const agentConfig = config2.agents[binding.targetAgent];
26364
+ if (!agentConfig) {
26365
+ continue;
26366
+ }
26367
+ const cwd = binding.cwd ?? config2.workspaces[binding.workspace]?.cwd;
26368
+ if (!cwd) {
26369
+ continue;
26370
+ }
26371
+ const agentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
26372
+ targets.push({
26373
+ agent: binding.targetAgent,
26374
+ ...agentCommand ? { agentCommand } : {},
26375
+ cwd,
26376
+ transportSession: workerSession
26377
+ });
26378
+ }
26379
+ return targets;
26380
+ }
26381
+ var init_collect_reap_targets = () => {};
26382
+
26172
26383
  // src/channels/channel-registry.ts
26173
26384
  var exports_channel_registry = {};
26174
26385
  __export(exports_channel_registry, {
@@ -26364,7 +26575,11 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
26364
26575
  if (thresholdSeconds <= 0) {
26365
26576
  return;
26366
26577
  }
26578
+ let ticking = false;
26367
26579
  return setInterval(async () => {
26580
+ if (ticking)
26581
+ return;
26582
+ ticking = true;
26368
26583
  try {
26369
26584
  const tasks = await orchestration.listHeartbeatTasks(thresholdSeconds);
26370
26585
  for (const task of tasks) {
@@ -26385,6 +26600,8 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
26385
26600
  await logger2.error("orchestration.heartbeat.check_failed", "heartbeat check failed", {
26386
26601
  message: error2 instanceof Error ? error2.message : String(error2)
26387
26602
  });
26603
+ } finally {
26604
+ ticking = false;
26388
26605
  }
26389
26606
  }, 60000);
26390
26607
  }
@@ -26437,7 +26654,8 @@ async function buildApp(paths, deps = {}) {
26437
26654
  acpxCommand,
26438
26655
  bridgeEntryPath: resolveBridgeEntryPath(),
26439
26656
  permissionMode: config2.transport.permissionMode,
26440
- nonInteractivePermissions: config2.transport.nonInteractivePermissions
26657
+ nonInteractivePermissions: config2.transport.nonInteractivePermissions,
26658
+ ...typeof config2.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config2.transport.queueOwnerTtlSeconds } : {}
26441
26659
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config2.transport, command: acpxCommand });
26442
26660
  const quota = new QuotaManager({
26443
26661
  onInbound: (chatKey) => {
@@ -26812,6 +27030,35 @@ async function buildApp(paths, deps = {}) {
26812
27030
  clearInterval(progressHeartbeatInterval);
26813
27031
  }
26814
27032
  await Promise.allSettled([...pendingWorkerDispatches]);
27033
+ try {
27034
+ const targets = [
27035
+ ...sessions.listAllResolvedSessions().map((session) => ({
27036
+ agent: session.agent,
27037
+ ...session.agentCommand ? { agentCommand: session.agentCommand } : {},
27038
+ cwd: session.cwd,
27039
+ transportSession: session.transportSession
27040
+ })),
27041
+ ...workerBindingReapTargets(state.orchestration, config2)
27042
+ ];
27043
+ if (targets.length > 0) {
27044
+ const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
27045
+ onError: (target, error2) => {
27046
+ logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner on shutdown", {
27047
+ transport_session: target.transportSession,
27048
+ error: error2 instanceof Error ? error2.message : String(error2)
27049
+ }).catch(() => {});
27050
+ }
27051
+ });
27052
+ await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners on shutdown", {
27053
+ terminated,
27054
+ attempted
27055
+ }).catch(() => {});
27056
+ }
27057
+ } catch (err) {
27058
+ await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed during shutdown", {
27059
+ error: err instanceof Error ? err.message : String(err)
27060
+ }).catch(() => {});
27061
+ }
26815
27062
  await debouncedStateStore.dispose();
26816
27063
  if ("dispose" in transport && typeof transport.dispose === "function") {
26817
27064
  await transport.dispose();
@@ -26842,7 +27089,12 @@ async function main() {
26842
27089
  await ensureConfigExists(paths.configPath);
26843
27090
  const startupConfig = await loadConfig(paths.configPath);
26844
27091
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
26845
- await loadConfiguredPlugins2({ plugins: startupConfig.plugins });
27092
+ await loadConfiguredPlugins2({
27093
+ plugins: startupConfig.plugins,
27094
+ onPluginError: ({ name, error: error2 }) => {
27095
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
27096
+ }
27097
+ });
26846
27098
  const { channelDeps } = await prepareChannelMedia(paths.configPath, startupConfig);
26847
27099
  const channelRegistry = new MessageChannelRegistry(createMessageChannels2(startupConfig.channels, channelDeps));
26848
27100
  await runConsole(paths, {
@@ -26926,6 +27178,8 @@ var init_main = __esm(async () => {
26926
27178
  init_acpx_bridge_client();
26927
27179
  init_acpx_bridge_transport();
26928
27180
  init_acpx_cli_transport();
27181
+ init_queue_owner_reaper();
27182
+ init_collect_reap_targets();
26929
27183
  init_channel_registry();
26930
27184
  init_media_store();
26931
27185
  init_quota_errors();
@@ -26936,7 +27190,7 @@ var init_main = __esm(async () => {
26936
27190
  });
26937
27191
 
26938
27192
  // src/doctor/checks/acpx-check.ts
26939
- import { spawn as spawn10 } from "node:child_process";
27193
+ import { spawn as spawn11 } from "node:child_process";
26940
27194
  async function checkAcpx(options = {}) {
26941
27195
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
26942
27196
  try {
@@ -26983,7 +27237,7 @@ function buildDetails(metadata, version2, verbose) {
26983
27237
  async function defaultRunVersion(command) {
26984
27238
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
26985
27239
  return await new Promise((resolve3, reject) => {
26986
- const child = spawn10(spawnSpec.command, spawnSpec.args, {
27240
+ const child = spawn11(spawnSpec.command, spawnSpec.args, {
26987
27241
  stdio: ["ignore", "pipe", "pipe"]
26988
27242
  });
26989
27243
  let stdout2 = "";
@@ -28004,7 +28258,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
28004
28258
 
28005
28259
  // src/daemon/daemon-runtime.ts
28006
28260
  init_daemon_status();
28007
- import { mkdir as mkdir5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
28261
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
28008
28262
  import { dirname as dirname5 } from "node:path";
28009
28263
 
28010
28264
  class DaemonRuntime {
@@ -28032,7 +28286,7 @@ class DaemonRuntime {
28032
28286
  stderr_log: this.paths.stderrLog
28033
28287
  };
28034
28288
  await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
28035
- await writeFile4(this.paths.pidFile, `${this.options.pid}
28289
+ await writeFile3(this.paths.pidFile, `${this.options.pid}
28036
28290
  `);
28037
28291
  await this.statusStore.save(this.currentStatus);
28038
28292
  }
@@ -43916,7 +44170,12 @@ async function defaultRun(options = {}) {
43916
44170
  await ensureConfigExists(runtimePaths.configPath);
43917
44171
  const config2 = await loadConfig(runtimePaths.configPath);
43918
44172
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
43919
- await loadConfiguredPlugins2({ plugins: config2.plugins });
44173
+ await loadConfiguredPlugins2({
44174
+ plugins: config2.plugins,
44175
+ onPluginError: ({ name, error: error2 }) => {
44176
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
44177
+ }
44178
+ });
43920
44179
  const { createMessageChannels: createMessageChannels2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
43921
44180
  const { MessageChannelRegistry: MessageChannelRegistry2 } = await Promise.resolve().then(() => (init_channel_registry(), exports_channel_registry));
43922
44181
  const daemonPaths = resolveDaemonPathsForCurrentConfig();
@@ -19,6 +19,13 @@ export interface TransportConfig {
19
19
  permissionMode: PermissionMode;
20
20
  nonInteractivePermissions: NonInteractivePermissions;
21
21
  permissionPolicy?: string;
22
+ /**
23
+ * Idle TTL (seconds) passed to acpx as `--ttl` on prompt commands. Governs how
24
+ * long the acpx queue owner (and the warm ACP agent it holds) survives between
25
+ * prompts, so follow-up messages in a conversation skip the agent cold start.
26
+ * `0` keeps the owner alive forever. Defaults to 1800 (30 min).
27
+ */
28
+ queueOwnerTtlSeconds?: number;
22
29
  }
23
30
  export type LoggingLevel = "error" | "info" | "debug";
24
31
  export interface PerfLogConfig {
@@ -0,0 +1,26 @@
1
+ export declare function writePrivateFileAtomic(path: string, content: string): Promise<void>;
2
+ /**
3
+ * Synchronous private-file write for hot-path callers that cannot await
4
+ * (e.g. per-message weixin credential/sync-buf/context-token persistence).
5
+ * Atomic via write-file-atomic's temp+rename, created at 0600 so the secret is
6
+ * never momentarily world-readable. No cross-process lock: weixin's per-account
7
+ * consumer lock already serializes the single writing daemon.
8
+ */
9
+ interface WritePrivateFileSyncDeps {
10
+ platform?: NodeJS.Platform;
11
+ atomicWrite?: (path: string, content: string) => void;
12
+ directWrite?: (path: string, content: string) => void;
13
+ }
14
+ export declare function writePrivateFileSync(path: string, content: string, deps?: WritePrivateFileSyncDeps): void;
15
+ interface RetryTransientWriteOptions {
16
+ platform?: NodeJS.Platform;
17
+ maxAttempts?: number;
18
+ baseDelayMs?: number;
19
+ maxDelayMs?: number;
20
+ delay?: (ms: number) => Promise<void>;
21
+ }
22
+ export declare function retryTransientWriteErrors(run: () => Promise<void>, options?: RetryTransientWriteOptions): Promise<void>;
23
+ export declare const __privateFileForTests: {
24
+ retryTransientWriteErrors: typeof retryTransientWriteErrors;
25
+ };
26
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weacpx",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",