@ganglion/xacpx 0.9.3 → 0.10.1

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.
package/dist/cli.js CHANGED
@@ -93,6 +93,11 @@ var init_session = __esm(() => {
93
93
  currentLabel: "[current]",
94
94
  sessionListItem: (alias, agent, workspace) => `- ${alias} (${agent} @ ${workspace})`,
95
95
  sessionCreated: (alias) => `Session "${alias}" created and switched.`,
96
+ sessionAlreadyExists: (alias, agent, workspace) => [
97
+ `Session "${alias}" already exists (${agent} @ ${workspace}).`,
98
+ `Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
99
+ ].join(`
100
+ `),
96
101
  sessionAttachNotFound: (alias, agent, workspace) => [
97
102
  "No existing session found to attach.",
98
103
  `Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
@@ -129,6 +134,7 @@ var init_session = __esm(() => {
129
134
  sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
130
135
  sessionRemoved: (alias) => `Session "${alias}" removed.`,
131
136
  sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
137
+ sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
132
138
  sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
133
139
  sessionOrchestrationPurgeFailed: (warning) => `Note: failed to purge orchestration references (${warning}). Run /tasks clean manually to clean up.`,
134
140
  sessionTransportTeardownFailed: (warning) => `Note: backend session could not be closed automatically (${warning}). Run acpx sessions close manually if needed.`,
@@ -422,7 +428,7 @@ var init_later = __esm(() => {
422
428
  helpNote2: "Time must be at least 10 seconds and at most 7 days away",
423
429
  helpNote3: "By default runs in a new temporary session that is destroyed after completion",
424
430
  helpNote4: "Use --bind to send to the session that was current when the task was created (configurable via later.defaultMode)",
425
- helpNote5: "/lt list shows all pending tasks globally; in group chats only the owner can cancel",
431
+ helpNote5: "/lt list shows only this chat's pending tasks; in group chats only the owner can cancel",
426
432
  helpNote6: "Scheduling slash-prefixed xacpx commands is not supported",
427
433
  helpNote7: "Full time format reference: docs/later-command.md"
428
434
  };
@@ -915,6 +921,8 @@ var init_plugin_cli = __esm(() => {
915
921
  noPlugins: "No plugins installed yet.",
916
922
  pluginListHeader: "Plugins:",
917
923
  unrecognizedArgs: (args) => `Unrecognized arguments: ${args}`,
924
+ pluginSpecHasDoubleQuote: (spec) => `Invalid plugin spec ${spec}: double quotes (") are never valid in an npm package spec.`,
925
+ pluginSpecHasPercentOnWindows: (spec) => `Invalid plugin spec ${spec}: "%" would be mangled by cmd.exe on Windows. Install the package with npm directly instead.`,
918
926
  pluginInstallFailed: (packageSpec, error) => `Plugin ${packageSpec} install failed: ${error}`,
919
927
  pluginValidateFailed: (recordedName, error) => `Plugin ${recordedName} validation failed: ${error}`,
920
928
  pluginInstalled: (recordedName) => `Plugin ${recordedName} installed`,
@@ -1026,8 +1034,6 @@ var init_weixin = __esm(() => {
1026
1034
  debugEnabled: "Debug mode enabled",
1027
1035
  debugDisabled: "Debug mode disabled",
1028
1036
  sessionCleared: "✅ Session cleared. Starting a fresh conversation.",
1029
- noAccountsLoggedIn: "No accounts are currently logged in.",
1030
- logoutSuccess: "✅ Logged out. All account credentials cleared.",
1031
1037
  commandFailed: (detail) => `❌ Command failed: ${detail}`
1032
1038
  };
1033
1039
  });
@@ -1065,6 +1071,7 @@ var init_misc = __esm(() => {
1065
1071
  quotaOverflowSummary: (count) => `(${count} progress updates omitted due to message limit; see final result below)`,
1066
1072
  finalHeadsUp: (total, sentSoFar, remaining) => `—
1067
1073
  \uD83D\uDCC4 Result: ${total} parts total, ${sentSoFar} sent. Reply /jx to see the next ${remaining} parts.`,
1074
+ finalAllParked: (count) => `\uD83D\uDCC4 Message limit reached: the result (${count} parts) is parked. Reply /jx to receive it.`,
1068
1075
  quotedMessagePrefix: (parts) => `[Quote: ${parts}]`,
1069
1076
  scheduledTaskFailed: (message) => `Scheduled task failed: ${message}`,
1070
1077
  orchestrationTaskCompleted: (taskId, workerSession, result) => `Delegation task "${taskId}" completed
@@ -1093,6 +1100,8 @@ var init_misc = __esm(() => {
1093
1100
  delegateQPackageInstr3: "Do not forward the human's exact words to the worker",
1094
1101
  commandAccessDeniedSuffix: " is restricted to group owner only.",
1095
1102
  commandAccessDeniedHint: "To perform control operations, have the owner send them in the group, or use a private chat.",
1103
+ commandAccessDeniedChatTypeMissingSuffix: " was blocked: this channel did not report the chat type (direct or group), so control commands are disabled here.",
1104
+ commandAccessDeniedChatTypeMissingHint: "Read-only commands and prompts still work. This is a channel metadata issue — update or report the channel plugin.",
1096
1105
  commandLabelThisMessage: "This message",
1097
1106
  sessionResetNoCurrentSession: "No session is currently selected. Run /session new ... or /use <alias> first.",
1098
1107
  sessionResetFailed: (alias) => `Session "${alias}" reset failed. The new backend session was not created, please try again later.`,
@@ -1163,6 +1172,11 @@ var init_session2 = __esm(() => {
1163
1172
  currentLabel: "[当前]",
1164
1173
  sessionListItem: (alias, agent2, workspace2) => `- ${alias} (${agent2} @ ${workspace2})`,
1165
1174
  sessionCreated: (alias) => `会话「${alias}」已创建并切换`,
1175
+ sessionAlreadyExists: (alias, agent2, workspace2) => [
1176
+ `会话「${alias}」已存在(${agent2} @ ${workspace2})。`,
1177
+ `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
1178
+ ].join(`
1179
+ `),
1166
1180
  sessionAttachNotFound: (alias, agent2, workspace2) => [
1167
1181
  "没有找到可绑定的已有会话。",
1168
1182
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -1199,6 +1213,7 @@ var init_session2 = __esm(() => {
1199
1213
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1200
1214
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1201
1215
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1216
+ sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1202
1217
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
1203
1218
  sessionOrchestrationPurgeFailed: (warning) => `提示:清理任务编排引用失败(${warning}),请稍后执行 /tasks clean 手动清理。`,
1204
1219
  sessionTransportTeardownFailed: (warning) => `提示:后端会话未能自动关闭(${warning}),如有残留请手动执行 acpx sessions close。`,
@@ -1492,7 +1507,7 @@ var init_later2 = __esm(() => {
1492
1507
  helpNote2: "时间必须在 10 秒之后、7 天之内",
1493
1508
  helpNote3: "默认在为本次任务新建的临时会话里执行,跑完即销毁",
1494
1509
  helpNote4: "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
1495
- helpNote5: "/lt list 显示全局待执行任务;群聊中只有群主可取消",
1510
+ helpNote5: "/lt list 只显示本聊天创建的待执行任务;群聊中只有群主可取消",
1496
1511
  helpNote6: "不支持延迟执行 / 开头的 xacpx 命令",
1497
1512
  helpNote7: "完整时间格式与说明见 docs/later-command.md"
1498
1513
  };
@@ -1985,6 +2000,8 @@ var init_plugin_cli2 = __esm(() => {
1985
2000
  noPlugins: "还没有安装插件。",
1986
2001
  pluginListHeader: "插件:",
1987
2002
  unrecognizedArgs: (args) => `未识别的参数:${args}`,
2003
+ pluginSpecHasDoubleQuote: (spec) => `非法插件 spec ${spec}:npm 包 spec 不允许包含双引号 (")。`,
2004
+ pluginSpecHasPercentOnWindows: (spec) => `非法插件 spec ${spec}:Windows 上 cmd.exe 会展开 %,无法安全传递。请改用 npm 直接安装该包。`,
1988
2005
  pluginInstallFailed: (packageSpec, error) => `插件 ${packageSpec} 安装失败:${error}`,
1989
2006
  pluginValidateFailed: (recordedName, error) => `插件 ${recordedName} 校验失败:${error}`,
1990
2007
  pluginInstalled: (recordedName) => `插件 ${recordedName} 已安装`,
@@ -2096,8 +2113,6 @@ var init_weixin2 = __esm(() => {
2096
2113
  debugEnabled: "Debug 模式已开启",
2097
2114
  debugDisabled: "Debug 模式已关闭",
2098
2115
  sessionCleared: "✅ 会话已清除,重新开始对话",
2099
- noAccountsLoggedIn: "当前没有已登录的账号",
2100
- logoutSuccess: "✅ 已退出登录,清除所有账号凭证",
2101
2116
  commandFailed: (detail) => `❌ 指令执行失败: ${detail}`
2102
2117
  };
2103
2118
  });
@@ -2135,6 +2150,7 @@ var init_misc2 = __esm(() => {
2135
2150
  quotaOverflowSummary: (count) => `(因消息次数限制省略 ${count} 条进度,请继续查看下方最终结果)`,
2136
2151
  finalHeadsUp: (total, sentSoFar, remaining) => `—
2137
2152
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`,
2153
+ finalAllParked: (count) => `\uD83D\uDCC4 已达消息上限:结果共 ${count} 段已暂存。回复 /jx 接收。`,
2138
2154
  quotedMessagePrefix: (parts) => `[引用: ${parts}]`,
2139
2155
  scheduledTaskFailed: (message) => `定时任务执行失败:${message}`,
2140
2156
  orchestrationTaskCompleted: (taskId, workerSession, result) => `委派任务「${taskId}」已完成
@@ -2163,6 +2179,8 @@ var init_misc2 = __esm(() => {
2163
2179
  delegateQPackageInstr3: "不要直接把 human 原话转发给 worker",
2164
2180
  commandAccessDeniedSuffix: " 仅限群创建者/频道 owner 使用。",
2165
2181
  commandAccessDeniedHint: "如果需要执行控制类操作,请由 owner 在群内发送,或改用私聊。",
2182
+ commandAccessDeniedChatTypeMissingSuffix: " 已被拦截:该频道未上报会话类型(直聊/群聊),控制类命令在此暂不可用。",
2183
+ commandAccessDeniedChatTypeMissingHint: "只读命令与普通对话不受影响。这是频道元数据问题,请升级或反馈该频道插件。",
2166
2184
  commandLabelThisMessage: "该消息",
2167
2185
  sessionResetNoCurrentSession: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。",
2168
2186
  sessionResetFailed: (alias) => `会话「${alias}」重置失败。
@@ -4212,7 +4230,7 @@ var require_lib = __commonJS((exports, module) => {
4212
4230
  import { chmod, mkdir, writeFile } from "node:fs/promises";
4213
4231
  import { chmodSync, mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
4214
4232
  import { dirname } from "node:path";
4215
- async function writePrivateFileAtomic(path, content) {
4233
+ async function withPrivateFileLock(path, fn) {
4216
4234
  await mkdir(dirname(path), { recursive: true });
4217
4235
  const release = await lockfile.lock(path, {
4218
4236
  realpath: false,
@@ -4226,23 +4244,31 @@ async function writePrivateFileAtomic(path, content) {
4226
4244
  }
4227
4245
  });
4228
4246
  try {
4229
- try {
4230
- await retryTransientWriteErrors(async () => import_write_file_atomic.default(path, content, {
4231
- mode: PRIVATE_FILE_MODE,
4232
- encoding: "utf8",
4233
- fsync: true
4234
- }));
4235
- } catch (error) {
4236
- if (!isTransientWriteError(error, process.platform)) {
4237
- throw error;
4238
- }
4239
- await writeFile(path, content, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
4240
- await chmod(path, PRIVATE_FILE_MODE).catch(() => {});
4241
- }
4247
+ return await fn((content) => writePrivateFileAtomicUnlocked(path, content));
4242
4248
  } finally {
4243
4249
  await release();
4244
4250
  }
4245
4251
  }
4252
+ async function writePrivateFileAtomic(path, content) {
4253
+ await withPrivateFileLock(path, async (writeLocked) => {
4254
+ await writeLocked(content);
4255
+ });
4256
+ }
4257
+ async function writePrivateFileAtomicUnlocked(path, content) {
4258
+ try {
4259
+ await retryTransientWriteErrors(async () => import_write_file_atomic.default(path, content, {
4260
+ mode: PRIVATE_FILE_MODE,
4261
+ encoding: "utf8",
4262
+ fsync: true
4263
+ }));
4264
+ } catch (error) {
4265
+ if (!isTransientWriteError(error, process.platform)) {
4266
+ throw error;
4267
+ }
4268
+ await writeFile(path, content, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
4269
+ await chmod(path, PRIVATE_FILE_MODE).catch(() => {});
4270
+ }
4271
+ }
4246
4272
  function writePrivateFileSync(path, content, deps = {}) {
4247
4273
  mkdirSync2(dirname(path), { recursive: true });
4248
4274
  const platform = deps.platform ?? process.platform;
@@ -4399,6 +4425,14 @@ function isRecord(value) {
4399
4425
  function isReplyMode(value) {
4400
4426
  return value === "stream" || value === "final" || value === "verbose";
4401
4427
  }
4428
+ function parseOwnerIds(value, path2) {
4429
+ if (value === undefined)
4430
+ return;
4431
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
4432
+ throw new Error(`${path2} must be an array of non-empty strings`);
4433
+ }
4434
+ return value.map((entry) => entry.trim());
4435
+ }
4402
4436
  function parseChannelConfig(channel, legacyWechat) {
4403
4437
  if (channel !== undefined) {
4404
4438
  if (!isRecord(channel)) {
@@ -4411,6 +4445,7 @@ function parseChannelConfig(channel, legacyWechat) {
4411
4445
  throw new Error("channel.replyMode must be stream, final, or verbose");
4412
4446
  }
4413
4447
  const type = typeof channel.type === "string" ? channel.type : "weixin";
4448
+ const ownerIds = parseOwnerIds(channel.ownerIds, "channel.ownerIds");
4414
4449
  let options = undefined;
4415
4450
  if ("feishu" in channel && isRecord(channel.feishu)) {
4416
4451
  options = channel.feishu;
@@ -4420,6 +4455,7 @@ function parseChannelConfig(channel, legacyWechat) {
4420
4455
  return {
4421
4456
  type,
4422
4457
  replyMode: isReplyMode(channel.replyMode) ? channel.replyMode : DEFAULT_CHANNEL_CONFIG.replyMode,
4458
+ ...ownerIds ? { ownerIds } : {},
4423
4459
  ...options ? { options } : {}
4424
4460
  };
4425
4461
  }
@@ -4658,6 +4694,7 @@ function parseRuntimeChannelConfig(raw, index) {
4658
4694
  if ("replyMode" in raw && !isReplyMode(raw.replyMode)) {
4659
4695
  throw new Error(`channels[${index}].replyMode must be stream, final, or verbose`);
4660
4696
  }
4697
+ const ownerIds = parseOwnerIds(raw.ownerIds, `channels[${index}].ownerIds`);
4661
4698
  let options = undefined;
4662
4699
  if ("feishu" in raw && isRecord(raw.feishu)) {
4663
4700
  options = raw.feishu;
@@ -4669,6 +4706,7 @@ function parseRuntimeChannelConfig(raw, index) {
4669
4706
  type: raw.type,
4670
4707
  enabled,
4671
4708
  ...isReplyMode(raw.replyMode) ? { replyMode: raw.replyMode } : {},
4709
+ ...ownerIds ? { ownerIds } : {},
4672
4710
  ...options ? { options } : {}
4673
4711
  };
4674
4712
  }
@@ -4753,6 +4791,8 @@ var init_load_config = __esm(() => {
4753
4791
  });
4754
4792
 
4755
4793
  // src/config/config-store.ts
4794
+ import { readFile as readFile2 } from "node:fs/promises";
4795
+
4756
4796
  class ConfigStore {
4757
4797
  path;
4758
4798
  constructor(path2) {
@@ -4761,60 +4801,250 @@ class ConfigStore {
4761
4801
  async load() {
4762
4802
  return await loadConfig(this.path);
4763
4803
  }
4764
- async save(config3) {
4765
- await writePrivateFileAtomic(this.path, `${JSON.stringify(config3, null, 2)}
4766
- `);
4804
+ async getRawValue(path2) {
4805
+ return readRawConfigValue((await this.readRaw()).raw, path2);
4806
+ }
4807
+ async setRawValue(path2, value) {
4808
+ return await this.patchRaw((raw) => {
4809
+ setRawConfigValue(raw, path2, value);
4810
+ });
4811
+ }
4812
+ async unsetRawValue(path2) {
4813
+ return await this.patchRaw((raw) => {
4814
+ unsetRawConfigValue(raw, path2);
4815
+ });
4767
4816
  }
4768
4817
  async upsertWorkspace(name, cwd, description) {
4769
- const config3 = await this.load();
4770
- const workspace3 = {
4771
- cwd,
4772
- ...description ? { description } : {}
4773
- };
4774
- config3.workspaces[name] = workspace3;
4775
- await this.save(config3);
4776
- return config3;
4818
+ assertSafeConfigKey(name);
4819
+ return await this.patchRaw((raw) => {
4820
+ const workspaces = ensureRecordAt(raw, "workspaces");
4821
+ workspaces[name] = {
4822
+ cwd,
4823
+ ...description ? { description } : {}
4824
+ };
4825
+ });
4777
4826
  }
4778
4827
  async removeWorkspace(name) {
4779
- const config3 = await this.load();
4780
- delete config3.workspaces[name];
4781
- await this.save(config3);
4782
- return config3;
4828
+ assertSafeConfigKey(name);
4829
+ return await this.patchRaw((raw) => {
4830
+ deleteRecordEntry(raw, "workspaces", name);
4831
+ });
4783
4832
  }
4784
4833
  async upsertAgent(name, agent3) {
4785
- const config3 = await this.load();
4786
- config3.agents[name] = agent3;
4787
- await this.save(config3);
4788
- return config3;
4834
+ assertSafeConfigKey(name);
4835
+ return await this.patchRaw((raw) => {
4836
+ const agents = ensureRecordAt(raw, "agents");
4837
+ agents[name] = {
4838
+ driver: agent3.driver,
4839
+ ...agent3.command ? { command: agent3.command } : {}
4840
+ };
4841
+ });
4789
4842
  }
4790
4843
  async removeAgent(name) {
4791
- const config3 = await this.load();
4792
- delete config3.agents[name];
4793
- await this.save(config3);
4794
- return config3;
4795
- }
4796
- async updateTransport(transport) {
4797
- const config3 = await this.load();
4798
- config3.transport = {
4799
- ...config3.transport,
4800
- ...transport
4801
- };
4802
- await this.save(config3);
4803
- return config3;
4804
- }
4805
- async updateChannel(channel) {
4806
- const config3 = await this.load();
4807
- config3.channel = {
4808
- ...config3.channel,
4809
- ...channel
4810
- };
4811
- await this.save(config3);
4812
- return config3;
4844
+ assertSafeConfigKey(name);
4845
+ return await this.patchRaw((raw) => {
4846
+ deleteRecordEntry(raw, "agents", name);
4847
+ });
4848
+ }
4849
+ async updateTransport(patch) {
4850
+ return await this.patchRaw((raw) => {
4851
+ applyRecordPatch(ensureRecordAt(raw, "transport"), patch);
4852
+ });
4853
+ }
4854
+ async updateChannel(patch) {
4855
+ return await this.patchRaw((raw) => {
4856
+ applyRecordPatch(ensureRecordAt(raw, "channel"), patch);
4857
+ });
4813
4858
  }
4859
+ async replacePlugins(plugins) {
4860
+ return await this.patchRaw((raw) => {
4861
+ raw.plugins = clonePlain(plugins);
4862
+ });
4863
+ }
4864
+ async replaceChannels(channels) {
4865
+ return await this.patchRaw((raw) => {
4866
+ raw.channels = clonePlain(channels);
4867
+ });
4868
+ }
4869
+ async patchRaw(mutate) {
4870
+ return await withPrivateFileLock(this.path, async (writeLocked) => {
4871
+ const { raw, existed } = await this.readRaw();
4872
+ const doc = existed ? raw : { transport: {}, agents: {}, workspaces: {} };
4873
+ mutate(doc);
4874
+ const parsed = parseConfig({ transport: {}, agents: {}, workspaces: {}, ...doc });
4875
+ await writeLocked(serializeRawConfig(doc));
4876
+ return parsed;
4877
+ });
4878
+ }
4879
+ async readRaw() {
4880
+ let text;
4881
+ try {
4882
+ text = await readFile2(this.path, "utf8");
4883
+ } catch (error) {
4884
+ if (isMissingFileError(error)) {
4885
+ return { raw: {}, existed: false };
4886
+ }
4887
+ throw error;
4888
+ }
4889
+ let parsed;
4890
+ try {
4891
+ parsed = JSON.parse(text);
4892
+ } catch (error) {
4893
+ throw new Error(`refusing to modify ${this.path}: it is not valid JSON (${error instanceof Error ? error.message : String(error)}); fix the file and retry`);
4894
+ }
4895
+ if (!isPlainRecord(parsed)) {
4896
+ throw new Error(`refusing to modify ${this.path}: the top level must be a JSON object`);
4897
+ }
4898
+ return { raw: parsed, existed: true };
4899
+ }
4900
+ }
4901
+ function serializeRawConfig(raw) {
4902
+ return `${JSON.stringify(raw, null, 2)}
4903
+ `;
4904
+ }
4905
+ function readRawConfigValue(root, path2) {
4906
+ const lastKey = requireObjectKeyTail(path2);
4907
+ let container = root;
4908
+ for (const segment of path2.slice(0, -1)) {
4909
+ const next = descendForRead(container, segment);
4910
+ if (!next.ok) {
4911
+ return { present: false };
4912
+ }
4913
+ container = next.value;
4914
+ }
4915
+ if (!isPlainRecord(container) || !Object.hasOwn(container, lastKey)) {
4916
+ return { present: false };
4917
+ }
4918
+ return { present: true, value: container[lastKey] };
4919
+ }
4920
+ function setRawConfigValue(root, path2, value) {
4921
+ const lastKey = requireObjectKeyTail(path2);
4922
+ let container = root;
4923
+ for (let index = 0;index < path2.length - 1; index += 1) {
4924
+ const segment = path2[index];
4925
+ const nextSegment = path2[index + 1];
4926
+ container = descendForWrite(container, segment, typeof nextSegment !== "string", path2);
4927
+ }
4928
+ if (!isPlainRecord(container)) {
4929
+ throw new Error(`config path "${describeRawConfigPath(path2)}" does not address an object`);
4930
+ }
4931
+ container[lastKey] = value;
4932
+ }
4933
+ function unsetRawConfigValue(root, path2) {
4934
+ const lastKey = requireObjectKeyTail(path2);
4935
+ let container = root;
4936
+ for (const segment of path2.slice(0, -1)) {
4937
+ const next = descendForRead(container, segment);
4938
+ if (!next.ok) {
4939
+ return;
4940
+ }
4941
+ container = next.value;
4942
+ }
4943
+ if (isPlainRecord(container)) {
4944
+ delete container[lastKey];
4945
+ }
4946
+ }
4947
+ function descendForRead(container, segment) {
4948
+ if (typeof segment === "string") {
4949
+ assertSafeConfigKey(segment);
4950
+ if (!isPlainRecord(container) || !Object.hasOwn(container, segment)) {
4951
+ return { ok: false };
4952
+ }
4953
+ return { ok: true, value: container[segment] };
4954
+ }
4955
+ if (!Array.isArray(container)) {
4956
+ return { ok: false };
4957
+ }
4958
+ const entry = container.find((item) => isPlainRecord(item) && item.id === segment.id);
4959
+ return entry === undefined ? { ok: false } : { ok: true, value: entry };
4960
+ }
4961
+ function descendForWrite(container, segment, nextIsArrayEntry, path2) {
4962
+ if (typeof segment === "string") {
4963
+ assertSafeConfigKey(segment);
4964
+ if (!isPlainRecord(container)) {
4965
+ throw new Error(`config path "${describeRawConfigPath(path2)}" does not address an object at "${segment}"`);
4966
+ }
4967
+ let existing = container[segment];
4968
+ if (existing === undefined) {
4969
+ existing = nextIsArrayEntry ? [] : {};
4970
+ container[segment] = existing;
4971
+ }
4972
+ if (!isPlainRecord(existing) && !Array.isArray(existing)) {
4973
+ throw new Error(`refusing to overwrite config key "${segment}" (path "${describeRawConfigPath(path2)}"): it is not an object`);
4974
+ }
4975
+ return existing;
4976
+ }
4977
+ if (!Array.isArray(container)) {
4978
+ throw new Error(`config path "${describeRawConfigPath(path2)}" expects an array before [id=${segment.id}]`);
4979
+ }
4980
+ const entry = container.find((item) => isPlainRecord(item) && item.id === segment.id);
4981
+ if (entry !== undefined) {
4982
+ return entry;
4983
+ }
4984
+ if (!segment.createWith) {
4985
+ throw new Error(`config path "${describeRawConfigPath(path2)}" has no entry with id "${segment.id}"`);
4986
+ }
4987
+ const created = clonePlain(segment.createWith);
4988
+ container.push(created);
4989
+ return created;
4990
+ }
4991
+ function requireObjectKeyTail(path2) {
4992
+ const last = path2[path2.length - 1];
4993
+ if (typeof last !== "string") {
4994
+ throw new Error("raw config path must end with an object key");
4995
+ }
4996
+ assertSafeConfigKey(last);
4997
+ return last;
4998
+ }
4999
+ function assertSafeConfigKey(key) {
5000
+ if (PROTOTYPE_POLLUTING_KEYS.has(key)) {
5001
+ throw new Error(`refusing to use unsafe config key "${key}"`);
5002
+ }
5003
+ }
5004
+ function describeRawConfigPath(path2) {
5005
+ return path2.map((segment) => typeof segment === "string" ? segment : `[id=${segment.id}]`).join(".");
4814
5006
  }
5007
+ function ensureRecordAt(raw, key) {
5008
+ const existing = raw[key];
5009
+ if (existing === undefined) {
5010
+ const created = {};
5011
+ raw[key] = created;
5012
+ return created;
5013
+ }
5014
+ if (!isPlainRecord(existing)) {
5015
+ throw new Error(`refusing to overwrite config key "${key}": it is not a JSON object`);
5016
+ }
5017
+ return existing;
5018
+ }
5019
+ function deleteRecordEntry(raw, section, name) {
5020
+ const record = raw[section];
5021
+ if (isPlainRecord(record)) {
5022
+ delete record[name];
5023
+ }
5024
+ }
5025
+ function applyRecordPatch(target, patch) {
5026
+ for (const [key, value] of Object.entries(patch)) {
5027
+ if (value === undefined) {
5028
+ delete target[key];
5029
+ } else {
5030
+ target[key] = value;
5031
+ }
5032
+ }
5033
+ }
5034
+ function clonePlain(value) {
5035
+ return JSON.parse(JSON.stringify(value));
5036
+ }
5037
+ function isPlainRecord(value) {
5038
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5039
+ }
5040
+ function isMissingFileError(error) {
5041
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
5042
+ }
5043
+ var PROTOTYPE_POLLUTING_KEYS;
4815
5044
  var init_config_store = __esm(() => {
4816
5045
  init_private_file();
4817
5046
  init_load_config();
5047
+ PROTOTYPE_POLLUTING_KEYS = new Set(["__proto__", "constructor", "prototype"]);
4818
5048
  });
4819
5049
 
4820
5050
  // src/config/default-workspace.ts
@@ -4827,16 +5057,16 @@ var init_default_workspace = __esm(() => {
4827
5057
  });
4828
5058
 
4829
5059
  // src/config/ensure-config.ts
4830
- import { readFile as readFile2 } from "node:fs/promises";
5060
+ import { readFile as readFile3 } from "node:fs/promises";
4831
5061
  async function ensureConfigExists(path2, options = {}) {
4832
5062
  try {
4833
5063
  await loadConfig(path2);
4834
5064
  } catch (error) {
4835
- if (!isMissingFileError(error)) {
5065
+ if (!isMissingFileError2(error)) {
4836
5066
  throw error;
4837
5067
  }
4838
- const store = new ConfigStore(path2);
4839
- await store.save(await loadDefaultConfigTemplate(options));
5068
+ const seed = await loadDefaultConfigTemplate(options);
5069
+ await writePrivateFileAtomic(path2, serializeRawConfig(seed));
4840
5070
  }
4841
5071
  }
4842
5072
  async function loadDefaultConfigTemplate(options = {}) {
@@ -4844,7 +5074,7 @@ async function loadDefaultConfigTemplate(options = {}) {
4844
5074
  try {
4845
5075
  return normalizeDefaultConfigTemplate(await options.readDefaultConfigTemplate());
4846
5076
  } catch (error) {
4847
- if (!isMissingFileError(error)) {
5077
+ if (!isMissingFileError2(error)) {
4848
5078
  throw error;
4849
5079
  }
4850
5080
  }
@@ -4856,10 +5086,10 @@ async function loadDefaultConfigTemplate(options = {}) {
4856
5086
  let raw;
4857
5087
  for (const candidate of candidates) {
4858
5088
  try {
4859
- raw = await readFile2(candidate, "utf8");
5089
+ raw = await readFile3(candidate, "utf8");
4860
5090
  break;
4861
5091
  } catch (error) {
4862
- if (!isMissingFileError(error)) {
5092
+ if (!isMissingFileError2(error)) {
4863
5093
  throw error;
4864
5094
  }
4865
5095
  }
@@ -4872,7 +5102,14 @@ async function loadDefaultConfigTemplate(options = {}) {
4872
5102
  function normalizeDefaultConfigTemplate(raw) {
4873
5103
  const template = parseConfig(raw);
4874
5104
  return {
4875
- ...template,
5105
+ transport: {
5106
+ type: template.transport.type,
5107
+ ...template.transport.command ? { command: template.transport.command } : {}
5108
+ },
5109
+ channel: {
5110
+ type: template.channel.type,
5111
+ replyMode: template.channel.replyMode
5112
+ },
4876
5113
  agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent3]) => [
4877
5114
  name,
4878
5115
  {
@@ -4885,32 +5122,18 @@ function normalizeDefaultConfigTemplate(raw) {
4885
5122
  }
4886
5123
  };
4887
5124
  }
4888
- function isMissingFileError(error) {
5125
+ function isMissingFileError2(error) {
4889
5126
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4890
5127
  }
4891
5128
  var BUILTIN_DEFAULT_CONFIG_TEMPLATE;
4892
5129
  var init_ensure_config = __esm(() => {
5130
+ init_private_file();
4893
5131
  init_config_store();
4894
5132
  init_default_workspace();
4895
5133
  init_load_config();
4896
5134
  BUILTIN_DEFAULT_CONFIG_TEMPLATE = {
4897
5135
  transport: {
4898
- type: "acpx-bridge",
4899
- sessionInitTimeoutMs: 120000,
4900
- permissionMode: "approve-all",
4901
- nonInteractivePermissions: "deny"
4902
- },
4903
- logging: {
4904
- level: "info",
4905
- maxSizeBytes: 2 * 1024 * 1024,
4906
- maxFiles: 5,
4907
- retentionDays: 7,
4908
- perf: {
4909
- enabled: false,
4910
- maxSizeBytes: 5242880,
4911
- maxFiles: 3,
4912
- retentionDays: 7
4913
- }
5136
+ type: "acpx-bridge"
4914
5137
  },
4915
5138
  channel: {
4916
5139
  type: "weixin",
@@ -4922,8 +5145,7 @@ var init_ensure_config = __esm(() => {
4922
5145
  },
4923
5146
  workspaces: {
4924
5147
  [DEFAULT_HOME_WORKSPACE_NAME]: { ...DEFAULT_HOME_WORKSPACE }
4925
- },
4926
- plugins: []
5148
+ }
4927
5149
  };
4928
5150
  });
4929
5151
 
@@ -5004,7 +5226,7 @@ var init_agent_templates = __esm(() => {
5004
5226
  });
5005
5227
 
5006
5228
  // src/daemon/daemon-status.ts
5007
- import { mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
5229
+ import { mkdir as mkdir2, readFile as readFile4, rm } from "node:fs/promises";
5008
5230
  import { dirname as dirname2 } from "node:path";
5009
5231
 
5010
5232
  class DaemonStatusStore {
@@ -5014,7 +5236,7 @@ class DaemonStatusStore {
5014
5236
  }
5015
5237
  async load() {
5016
5238
  try {
5017
- const content = await readFile3(this.path, "utf8");
5239
+ const content = await readFile4(this.path, "utf8");
5018
5240
  if (content.trim() === "") {
5019
5241
  return null;
5020
5242
  }
@@ -5030,18 +5252,37 @@ class DaemonStatusStore {
5030
5252
  }
5031
5253
  }
5032
5254
  async save(status) {
5033
- await mkdir2(dirname2(this.path), { recursive: true });
5034
- await writeFile2(this.path, JSON.stringify(status, null, 2));
5255
+ await mkdir2(dirname2(this.path), { recursive: true, mode: 448 });
5256
+ await import_write_file_atomic2.default(this.path, JSON.stringify(status, null, 2), { encoding: "utf8" });
5035
5257
  }
5036
5258
  async clear() {
5037
5259
  await rm(this.path, { force: true });
5038
5260
  }
5039
5261
  }
5040
- var init_daemon_status = () => {};
5262
+ var import_write_file_atomic2;
5263
+ var init_daemon_status = __esm(() => {
5264
+ import_write_file_atomic2 = __toESM(require_lib(), 1);
5265
+ });
5266
+
5267
+ // src/daemon/private-runtime-dir.ts
5268
+ import { chmod as chmod2, mkdir as mkdir3 } from "node:fs/promises";
5269
+ async function ensurePrivateRuntimeDir(runtimeDir, options = {}) {
5270
+ await mkdir3(runtimeDir, { recursive: true, mode: 448 });
5271
+ const platform = options.platform ?? process.platform;
5272
+ if (platform === "win32") {
5273
+ return;
5274
+ }
5275
+ const chmodImpl = options.chmodImpl ?? chmod2;
5276
+ try {
5277
+ await chmodImpl(runtimeDir, 448);
5278
+ } catch (error) {
5279
+ options.onChmodError?.(error);
5280
+ }
5281
+ }
5282
+ var init_private_runtime_dir = () => {};
5041
5283
 
5042
5284
  // src/daemon/daemon-controller.ts
5043
- import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
5044
- import { dirname as dirname3 } from "node:path";
5285
+ import { open, readFile as readFile5, rm as rm2 } from "node:fs/promises";
5045
5286
 
5046
5287
  class DaemonController {
5047
5288
  paths;
@@ -5060,7 +5301,7 @@ class DaemonController {
5060
5301
  this.deps = deps;
5061
5302
  this.statusStore = new DaemonStatusStore(paths.statusFile);
5062
5303
  this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
5063
- this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
5304
+ this.startupTimeoutMs = deps.startupTimeoutMs ?? 1e4;
5064
5305
  this.onboardingStartupTimeoutMs = deps.onboardingStartupTimeoutMs ?? 300000;
5065
5306
  this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
5066
5307
  this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
@@ -5129,7 +5370,7 @@ class DaemonController {
5129
5370
  }
5130
5371
  async loadPid() {
5131
5372
  try {
5132
- const content = await readFile4(this.paths.pidFile, "utf8");
5373
+ const content = await readFile5(this.paths.pidFile, "utf8");
5133
5374
  const pid = Number(content.trim());
5134
5375
  return Number.isFinite(pid) && pid > 0 ? pid : null;
5135
5376
  } catch (error) {
@@ -5140,7 +5381,7 @@ class DaemonController {
5140
5381
  }
5141
5382
  }
5142
5383
  async openPidFileExclusive() {
5143
- await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
5384
+ await ensurePrivateRuntimeDir(this.paths.runtimeDir);
5144
5385
  try {
5145
5386
  return await open(this.paths.pidFile, "wx", 384);
5146
5387
  } catch (error) {
@@ -5194,6 +5435,7 @@ class DaemonController {
5194
5435
  }
5195
5436
  var init_daemon_controller = __esm(() => {
5196
5437
  init_daemon_status();
5438
+ init_private_runtime_dir();
5197
5439
  });
5198
5440
 
5199
5441
  // src/process/terminate-process-tree.ts
@@ -5245,13 +5487,15 @@ async function defaultRunProcessCommand(command, args) {
5245
5487
  var init_terminate_process_tree = () => {};
5246
5488
 
5247
5489
  // src/daemon/create-daemon-controller.ts
5248
- import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
5490
+ import { open as open2 } from "node:fs/promises";
5249
5491
  import { spawn as spawn2 } from "node:child_process";
5250
5492
  function createDaemonController(paths, options) {
5251
5493
  return new DaemonController(paths, {
5252
5494
  isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
5253
5495
  spawnDetached: async (spawnOptions) => {
5254
- await mkdir4(paths.runtimeDir, { recursive: true });
5496
+ await ensurePrivateRuntimeDir(paths.runtimeDir, {
5497
+ ...options.platform ? { platform: options.platform } : {}
5498
+ });
5255
5499
  const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
5256
5500
  const stderrHandle = await open2(paths.stderrLog, "a", 384);
5257
5501
  await stdoutHandle.chmod(384).catch(() => {});
@@ -5323,8 +5567,10 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
5323
5567
  function buildWindowsLauncherScript() {
5324
5568
  const script = [
5325
5569
  "$env:XACPX_DAEMON_RUN = '1'",
5570
+ `$arg0 = '"' + $env:XACPX_DAEMON_ARG0 + '"'`,
5571
+ `$arg1 = '"' + $env:XACPX_DAEMON_ARG1 + '"'`,
5326
5572
  "$process = Start-Process -FilePath $env:XACPX_DAEMON_COMMAND `",
5327
- " -ArgumentList @($env:XACPX_DAEMON_ARG0, $env:XACPX_DAEMON_ARG1) `",
5573
+ " -ArgumentList @($arg0, $arg1) `",
5328
5574
  " -WorkingDirectory $env:XACPX_DAEMON_CWD `",
5329
5575
  " -RedirectStandardOutput $env:XACPX_DAEMON_STDOUT `",
5330
5576
  " -RedirectStandardError $env:XACPX_DAEMON_STDERR `",
@@ -5395,6 +5641,7 @@ async function defaultTerminateProcess(pid) {
5395
5641
  }
5396
5642
  var init_create_daemon_controller = __esm(() => {
5397
5643
  init_daemon_controller();
5644
+ init_private_runtime_dir();
5398
5645
  init_terminate_process_tree();
5399
5646
  });
5400
5647
 
@@ -5431,7 +5678,7 @@ function encodeOrchestrationRpcResponse(response) {
5431
5678
  var init_orchestration_ipc = () => {};
5432
5679
 
5433
5680
  // src/daemon/daemon-files.ts
5434
- import { dirname as dirname4, join as join4 } from "node:path";
5681
+ import { dirname as dirname3, join as join4 } from "node:path";
5435
5682
  function resolveDaemonPaths(options) {
5436
5683
  const runtimeDir = options.runtimeDir ?? (options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : join4(coreHomeDir(options.home), "runtime"));
5437
5684
  return {
@@ -5444,11 +5691,19 @@ function resolveDaemonPaths(options) {
5444
5691
  };
5445
5692
  }
5446
5693
  function resolveRuntimeDirFromConfigPath(configPath) {
5447
- return join4(dirname4(configPath), "runtime");
5694
+ return join4(dirname3(configPath), "runtime");
5448
5695
  }
5449
5696
  function resolveDaemonOrchestrationSocketPath(runtimeDir, platform = process.platform) {
5450
5697
  return resolveOrchestrationEndpoint(runtimeDir, platform).path;
5451
5698
  }
5699
+ function isProcessAlive(pid) {
5700
+ try {
5701
+ process.kill(pid, 0);
5702
+ return true;
5703
+ } catch (error) {
5704
+ return error.code === "EPERM";
5705
+ }
5706
+ }
5452
5707
  var init_daemon_files = __esm(() => {
5453
5708
  init_core_home();
5454
5709
  init_orchestration_ipc();
@@ -12124,10 +12379,24 @@ function createEmptyState() {
12124
12379
  var init_types = () => {};
12125
12380
 
12126
12381
  // src/state/state-store.ts
12127
- import { readFile as readFile5 } from "node:fs/promises";
12382
+ import { readFile as readFile6, rename, writeFile as writeFile3 } from "node:fs/promises";
12128
12383
  function isRecord2(value) {
12129
12384
  return typeof value === "object" && value !== null && !Array.isArray(value);
12130
12385
  }
12386
+ function sectionRecord(value, section, dropped) {
12387
+ if (value === undefined) {
12388
+ return {};
12389
+ }
12390
+ if (!isRecord2(value)) {
12391
+ dropped.push({
12392
+ section,
12393
+ key: "",
12394
+ reason: `field "${section}" is not an object; reset to empty`
12395
+ });
12396
+ return {};
12397
+ }
12398
+ return value;
12399
+ }
12131
12400
  function isString(value) {
12132
12401
  return typeof value === "string";
12133
12402
  }
@@ -12232,73 +12501,82 @@ function isHumanQuestionPackageRecord(value) {
12232
12501
  const messages = value.messages;
12233
12502
  return isString(value.packageId) && isString(value.coordinatorSession) && (value.status === "active" || value.status === "closed") && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.closedAt) && Array.isArray(initialTaskIds) && initialTaskIds.every(isString) && Array.isArray(openTaskIds) && openTaskIds.every(isString) && Array.isArray(resolvedTaskIds) && resolvedTaskIds.every(isString) && Array.isArray(messages) && messages.every(isHumanQuestionPackageMessageRecord) && isOptionalString(value.awaitingReplyMessageId);
12234
12503
  }
12235
- function parseOrchestrationState(raw, path3) {
12504
+ function parseOrchestrationState(raw, dropped) {
12236
12505
  if (raw === undefined) {
12237
12506
  return createEmptyOrchestrationState();
12238
12507
  }
12239
12508
  if (!isRecord2(raw)) {
12240
- throw new Error(`state file "${path3}" must contain an object field "orchestration"`);
12241
- }
12242
- const tasks = raw.tasks;
12243
- if (tasks !== undefined && !isRecord2(tasks)) {
12244
- throw new Error(`state file "${path3}" must contain an object field "orchestration.tasks"`);
12245
- }
12246
- const workerBindings = raw.workerBindings;
12247
- if (workerBindings !== undefined && !isRecord2(workerBindings)) {
12248
- throw new Error(`state file "${path3}" must contain an object field "orchestration.workerBindings"`);
12249
- }
12250
- const groups = raw.groups;
12251
- if (groups !== undefined && !isRecord2(groups)) {
12252
- throw new Error(`state file "${path3}" must contain an object field "orchestration.groups"`);
12253
- }
12254
- const humanQuestionPackages = raw.humanQuestionPackages;
12255
- if (humanQuestionPackages !== undefined && !isRecord2(humanQuestionPackages)) {
12256
- throw new Error(`state file "${path3}" must contain an object field "orchestration.humanQuestionPackages"`);
12257
- }
12258
- const coordinatorQuestionState = raw.coordinatorQuestionState;
12259
- if (coordinatorQuestionState !== undefined && !isRecord2(coordinatorQuestionState)) {
12260
- throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorQuestionState"`);
12261
- }
12262
- const coordinatorRoutes = raw.coordinatorRoutes;
12263
- if (coordinatorRoutes !== undefined && !isRecord2(coordinatorRoutes)) {
12264
- throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorRoutes"`);
12265
- }
12266
- const externalCoordinators = raw.externalCoordinators;
12267
- if (externalCoordinators !== undefined && !isRecord2(externalCoordinators)) {
12268
- throw new Error(`state file "${path3}" must contain an object field "orchestration.externalCoordinators"`);
12509
+ dropped.push({
12510
+ section: "orchestration",
12511
+ key: "",
12512
+ reason: 'field "orchestration" is not an object; reset to empty'
12513
+ });
12514
+ return createEmptyOrchestrationState();
12269
12515
  }
12516
+ const tasks = sectionRecord(raw.tasks, "orchestration.tasks", dropped);
12517
+ const workerBindings = sectionRecord(raw.workerBindings, "orchestration.workerBindings", dropped);
12518
+ const groups = sectionRecord(raw.groups, "orchestration.groups", dropped);
12519
+ const humanQuestionPackages = sectionRecord(raw.humanQuestionPackages, "orchestration.humanQuestionPackages", dropped);
12520
+ const coordinatorQuestionState = sectionRecord(raw.coordinatorQuestionState, "orchestration.coordinatorQuestionState", dropped);
12521
+ const coordinatorRoutes = sectionRecord(raw.coordinatorRoutes, "orchestration.coordinatorRoutes", dropped);
12522
+ const externalCoordinators = sectionRecord(raw.externalCoordinators, "orchestration.externalCoordinators", dropped);
12270
12523
  const parsedTasks = {};
12271
- for (const [taskId, task] of Object.entries(tasks ?? {})) {
12524
+ for (const [taskId, task] of Object.entries(tasks)) {
12272
12525
  if (!isTaskRecord(task)) {
12273
- throw new Error(`state file "${path3}" contains an invalid orchestration task at "${taskId}"`);
12526
+ dropped.push({
12527
+ section: "orchestration.tasks",
12528
+ key: taskId,
12529
+ reason: "malformed orchestration task record"
12530
+ });
12531
+ continue;
12274
12532
  }
12275
12533
  parsedTasks[taskId] = task;
12276
12534
  }
12277
12535
  const parsedWorkerBindings = {};
12278
- for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
12536
+ for (const [workerSession, binding] of Object.entries(workerBindings)) {
12279
12537
  if (!isWorkerBindingRecord(binding)) {
12280
- throw new Error(`state file "${path3}" contains an invalid orchestration worker binding at "${workerSession}"`);
12538
+ dropped.push({
12539
+ section: "orchestration.workerBindings",
12540
+ key: workerSession,
12541
+ reason: "malformed orchestration worker binding record"
12542
+ });
12543
+ continue;
12281
12544
  }
12282
12545
  parsedWorkerBindings[workerSession] = binding;
12283
12546
  }
12284
12547
  const parsedGroups = {};
12285
- for (const [groupId, group] of Object.entries(groups ?? {})) {
12548
+ for (const [groupId, group] of Object.entries(groups)) {
12286
12549
  if (!isGroupRecord(group)) {
12287
- throw new Error(`state file "${path3}" contains an invalid orchestration group at "${groupId}"`);
12550
+ dropped.push({
12551
+ section: "orchestration.groups",
12552
+ key: groupId,
12553
+ reason: "malformed orchestration group record"
12554
+ });
12555
+ continue;
12288
12556
  }
12289
12557
  parsedGroups[groupId] = group;
12290
12558
  }
12291
12559
  const parsedHumanQuestionPackages = {};
12292
- for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages ?? {})) {
12560
+ for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages)) {
12293
12561
  if (!isHumanQuestionPackageRecord(packageRecord)) {
12294
- throw new Error(`state file "${path3}" contains an invalid human question package at "${packageId}"`);
12562
+ dropped.push({
12563
+ section: "orchestration.humanQuestionPackages",
12564
+ key: packageId,
12565
+ reason: "malformed human question package record"
12566
+ });
12567
+ continue;
12295
12568
  }
12296
12569
  parsedHumanQuestionPackages[packageId] = packageRecord;
12297
12570
  }
12298
12571
  const parsedCoordinatorQuestionState = {};
12299
- for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState ?? {})) {
12572
+ for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState)) {
12300
12573
  if (!isCoordinatorQuestionStateRecord(questionState)) {
12301
- throw new Error(`state file "${path3}" contains an invalid coordinator question state at "${coordinatorSession}"`);
12574
+ dropped.push({
12575
+ section: "orchestration.coordinatorQuestionState",
12576
+ key: coordinatorSession,
12577
+ reason: "malformed coordinator question state record"
12578
+ });
12579
+ continue;
12302
12580
  }
12303
12581
  parsedCoordinatorQuestionState[coordinatorSession] = {
12304
12582
  activePackageId: questionState.activePackageId,
@@ -12306,19 +12584,34 @@ function parseOrchestrationState(raw, path3) {
12306
12584
  };
12307
12585
  }
12308
12586
  const parsedCoordinatorRoutes = {};
12309
- for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes ?? {})) {
12587
+ for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes)) {
12310
12588
  if (!isCoordinatorRouteContextRecord(route)) {
12311
- throw new Error(`state file "${path3}" contains an invalid coordinator route at "${coordinatorSession}"`);
12589
+ dropped.push({
12590
+ section: "orchestration.coordinatorRoutes",
12591
+ key: coordinatorSession,
12592
+ reason: "malformed coordinator route record"
12593
+ });
12594
+ continue;
12312
12595
  }
12313
12596
  parsedCoordinatorRoutes[coordinatorSession] = route;
12314
12597
  }
12315
12598
  const parsedExternalCoordinators = {};
12316
- for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators ?? {})) {
12599
+ for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators)) {
12317
12600
  if (!isExternalCoordinatorRecord(externalCoordinator)) {
12318
- throw new Error(`state file "${path3}" contains an invalid external coordinator at "${coordinatorSession}"`);
12601
+ dropped.push({
12602
+ section: "orchestration.externalCoordinators",
12603
+ key: coordinatorSession,
12604
+ reason: "malformed external coordinator record"
12605
+ });
12606
+ continue;
12319
12607
  }
12320
12608
  if (externalCoordinator.coordinatorSession !== coordinatorSession) {
12321
- throw new Error(`state file "${path3}" contains an external coordinator key mismatch at "${coordinatorSession}"`);
12609
+ dropped.push({
12610
+ section: "orchestration.externalCoordinators",
12611
+ key: coordinatorSession,
12612
+ reason: `coordinatorSession "${externalCoordinator.coordinatorSession}" does not match map key`
12613
+ });
12614
+ continue;
12322
12615
  }
12323
12616
  parsedExternalCoordinators[coordinatorSession] = externalCoordinator;
12324
12617
  }
@@ -12344,11 +12637,12 @@ function isSessionRecord(value) {
12344
12637
  }
12345
12638
  return isString(value.alias) && isString(value.agent) && isString(value.workspace) && isString(value.transport_session) && isSessionSource(value.source) && isOptionalString(value.agent_session_id) && isOptionalString(value.agent_session_title) && isOptionalString(value.agent_session_updated_at) && isOptionalString(value.attached_at) && isOptionalString(value.transport_agent_command) && isOptionalString(value.mode_id) && (value.reply_mode === undefined || isReplyMode2(value.reply_mode)) && isString(value.created_at) && isString(value.last_used_at);
12346
12639
  }
12347
- function parseSessions(raw, path3) {
12640
+ function parseSessions(raw, dropped) {
12348
12641
  const sessions = {};
12349
12642
  for (const [alias, value] of Object.entries(raw)) {
12350
12643
  if (!isSessionRecord(value)) {
12351
- throw new Error(`state file "${path3}" contains malformed session record "${alias}"`);
12644
+ dropped.push({ section: "sessions", key: alias, reason: "malformed session record" });
12645
+ continue;
12352
12646
  }
12353
12647
  sessions[alias] = value;
12354
12648
  }
@@ -12357,11 +12651,12 @@ function parseSessions(raw, path3) {
12357
12651
  function isChatContextRecord(value) {
12358
12652
  return isRecord2(value) && isString(value.current_session);
12359
12653
  }
12360
- function parseChatContexts(raw, path3) {
12654
+ function parseChatContexts(raw, dropped) {
12361
12655
  const chatContexts = {};
12362
12656
  for (const [chatKey, value] of Object.entries(raw)) {
12363
12657
  if (!isChatContextRecord(value)) {
12364
- throw new Error(`state file "${path3}" contains malformed chat context record "${chatKey}"`);
12658
+ dropped.push({ section: "chat_contexts", key: chatKey, reason: "malformed chat context record" });
12659
+ continue;
12365
12660
  }
12366
12661
  chatContexts[chatKey] = value;
12367
12662
  }
@@ -12402,56 +12697,58 @@ function isScheduledTaskRecord(value) {
12402
12697
  return false;
12403
12698
  return isString(value.id) && isString(value.chat_key) && isString(value.session_alias) && isString(value.execute_at) && isString(value.message) && isScheduledTaskStatus(value.status) && isString(value.created_at) && isOptionalString(value.account_id) && isOptionalString(value.reply_context_token) && isOptionalString(value.source_label) && isOptionalString(value.triggered_at) && isOptionalString(value.executed_at) && isOptionalString(value.cancelled_at) && isOptionalString(value.missed_at) && isOptionalString(value.failed_at) && isOptionalString(value.last_error) && isOptionalScheduledSessionMode(value.session_mode) && isOptionalString(value.agent) && isOptionalString(value.workspace);
12404
12699
  }
12405
- function parseScheduledTasks(raw, path3) {
12406
- if (raw === undefined)
12407
- return {};
12408
- if (!isRecord2(raw)) {
12409
- throw new Error(`state file "${path3}" must contain an object field "scheduled_tasks"`);
12410
- }
12700
+ function parseScheduledTasks(raw, dropped) {
12701
+ const source = sectionRecord(raw, "scheduled_tasks", dropped);
12411
12702
  const tasks = {};
12412
- for (const [id, value] of Object.entries(raw)) {
12703
+ for (const [id, value] of Object.entries(source)) {
12413
12704
  if (!isScheduledTaskRecord(value) || value.id !== id) {
12414
- throw new Error(`state file "${path3}" contains malformed scheduled task record "${id}"`);
12705
+ dropped.push({ section: "scheduled_tasks", key: id, reason: "malformed scheduled task record" });
12706
+ continue;
12415
12707
  }
12416
12708
  tasks[id] = value;
12417
12709
  }
12418
12710
  return tasks;
12419
12711
  }
12420
- function parseState(raw, path3) {
12712
+ function parseState(raw, path3, dropped = []) {
12421
12713
  if (!isRecord2(raw)) {
12422
12714
  throw new Error(`state file "${path3}" must contain a JSON object`);
12423
12715
  }
12424
- const sessions = raw.sessions;
12425
- if (!isRecord2(sessions)) {
12426
- throw new Error(`state file "${path3}" must contain an object field "sessions"`);
12427
- }
12428
- const chatContexts = raw.chat_contexts;
12429
- if (!isRecord2(chatContexts)) {
12430
- throw new Error(`state file "${path3}" must contain an object field "chat_contexts"`);
12431
- }
12432
- const parsedSessions = parseSessions(sessions, path3);
12433
- const orchestration3 = parseOrchestrationState(raw.orchestration, path3);
12434
- validateExternalCoordinatorIdentityCollisions(parsedSessions, orchestration3, path3);
12716
+ const parsedSessions = parseSessions(sectionRecord(raw.sessions, "sessions", dropped), dropped);
12717
+ const orchestration3 = parseOrchestrationState(raw.orchestration, dropped);
12718
+ repairExternalCoordinatorIdentityCollisions(parsedSessions, orchestration3, dropped);
12435
12719
  return {
12436
12720
  sessions: parsedSessions,
12437
- chat_contexts: parseChatContexts(chatContexts, path3),
12721
+ chat_contexts: parseChatContexts(sectionRecord(raw.chat_contexts, "chat_contexts", dropped), dropped),
12438
12722
  native_session_lists: parseNativeSessionLists(raw.native_session_lists),
12439
12723
  orchestration: orchestration3,
12440
- scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
12724
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, dropped)
12441
12725
  };
12442
12726
  }
12443
- function validateExternalCoordinatorIdentityCollisions(sessions, orchestration3, path3) {
12727
+ function repairExternalCoordinatorIdentityCollisions(sessions, orchestration3, dropped) {
12444
12728
  for (const coordinatorSession of Object.keys(orchestration3.externalCoordinators)) {
12445
- if (Object.values(sessions).some((session3) => session3.transport_session === coordinatorSession)) {
12446
- throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a logical session`);
12447
- }
12448
- if (orchestration3.workerBindings[coordinatorSession]) {
12449
- throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a worker binding`);
12450
- }
12451
- if (Object.values(orchestration3.tasks).some((task) => task.workerSession === coordinatorSession && (!isTerminalTaskStatus(task.status) || task.reviewPending !== undefined))) {
12452
- throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with an active task worker session`);
12729
+ const conflict = findExternalCoordinatorConflict(coordinatorSession, sessions, orchestration3);
12730
+ if (!conflict) {
12731
+ continue;
12453
12732
  }
12733
+ delete orchestration3.externalCoordinators[coordinatorSession];
12734
+ dropped.push({
12735
+ section: "orchestration.externalCoordinators",
12736
+ key: coordinatorSession,
12737
+ reason: `conflicts with ${conflict}; dropped (re-registered on next coordinator connect)`
12738
+ });
12739
+ }
12740
+ }
12741
+ function findExternalCoordinatorConflict(coordinatorSession, sessions, orchestration3) {
12742
+ if (Object.values(sessions).some((session3) => session3.transport_session === coordinatorSession)) {
12743
+ return "a logical session";
12744
+ }
12745
+ if (orchestration3.workerBindings[coordinatorSession]) {
12746
+ return "a worker binding";
12454
12747
  }
12748
+ if (Object.values(orchestration3.tasks).some((task) => task.workerSession === coordinatorSession && (!isTerminalTaskStatus(task.status) || task.reviewPending !== undefined))) {
12749
+ return "an active task worker session";
12750
+ }
12751
+ return null;
12455
12752
  }
12456
12753
  function isTerminalTaskStatus(status) {
12457
12754
  return status === "completed" || status === "failed" || status === "cancelled";
@@ -12459,35 +12756,118 @@ function isTerminalTaskStatus(status) {
12459
12756
 
12460
12757
  class StateStore {
12461
12758
  path;
12462
- constructor(path3) {
12759
+ options;
12760
+ loadReport = null;
12761
+ constructor(path3, options = {}) {
12463
12762
  this.path = path3;
12763
+ this.options = options;
12764
+ }
12765
+ get lastLoadReport() {
12766
+ return this.loadReport;
12464
12767
  }
12465
12768
  async load() {
12769
+ this.loadReport = null;
12770
+ const read = await this.readAndParse();
12771
+ if (read.kind === "absent") {
12772
+ return createEmptyState();
12773
+ }
12774
+ if (read.kind === "corrupt") {
12775
+ return await this.recoverFromCorruptFile(read.reason);
12776
+ }
12777
+ if (read.dropped.length === 0) {
12778
+ return read.state;
12779
+ }
12780
+ const report = { dropped: read.dropped };
12781
+ const quarantinePath = `${this.path}.quarantine-${this.fileTimestamp()}`;
12466
12782
  try {
12467
- const content = await readFile5(this.path, "utf8");
12468
- if (content.trim() === "") {
12469
- return createEmptyState();
12470
- }
12471
- let parsed;
12472
- try {
12473
- parsed = JSON.parse(content);
12474
- } catch (error2) {
12475
- throw new Error(`failed to parse state file "${this.path}"`, {
12476
- cause: error2
12477
- });
12478
- }
12479
- return parseState(parsed, this.path);
12783
+ const written = await (this.options.writeBackup ?? defaultWriteBackup)(quarantinePath, read.content);
12784
+ report.quarantinePath = typeof written === "string" ? written : quarantinePath;
12785
+ } catch (error2) {
12786
+ report.backupError = error2 instanceof Error ? error2.message : String(error2);
12787
+ }
12788
+ this.loadReport = report;
12789
+ return read.state;
12790
+ }
12791
+ async inspect() {
12792
+ const read = await this.readAndParse();
12793
+ if (read.kind === "absent") {
12794
+ return { state: createEmptyState(), report: null };
12795
+ }
12796
+ if (read.kind === "corrupt") {
12797
+ return {
12798
+ state: createEmptyState(),
12799
+ report: { dropped: [{ section: "file", key: this.path, reason: read.reason }] }
12800
+ };
12801
+ }
12802
+ return {
12803
+ state: read.state,
12804
+ report: read.dropped.length > 0 ? { dropped: read.dropped } : null
12805
+ };
12806
+ }
12807
+ async readAndParse() {
12808
+ let content;
12809
+ try {
12810
+ content = await readFile6(this.path, "utf8");
12480
12811
  } catch (error2) {
12481
12812
  if (error2.code === "ENOENT") {
12482
- return createEmptyState();
12813
+ return { kind: "absent" };
12483
12814
  }
12484
12815
  throw error2;
12485
12816
  }
12817
+ if (content.trim() === "") {
12818
+ return { kind: "absent" };
12819
+ }
12820
+ let parsed;
12821
+ try {
12822
+ parsed = JSON.parse(content);
12823
+ } catch (error2) {
12824
+ return {
12825
+ kind: "corrupt",
12826
+ reason: `invalid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
12827
+ };
12828
+ }
12829
+ if (!isRecord2(parsed)) {
12830
+ return { kind: "corrupt", reason: "top-level value is not an object" };
12831
+ }
12832
+ const dropped = [];
12833
+ return { kind: "parsed", state: parseState(parsed, this.path, dropped), dropped, content };
12486
12834
  }
12487
12835
  async save(state) {
12488
- await writePrivateFileAtomic(this.path, JSON.stringify(state, null, 2));
12836
+ await writePrivateFileAtomic(this.path, JSON.stringify({ version: STATE_FILE_VERSION, ...state }, null, 2));
12837
+ }
12838
+ fileTimestamp() {
12839
+ const now = this.options.now?.() ?? new Date;
12840
+ return now.toISOString().replace(/[:.]/g, "-");
12841
+ }
12842
+ async recoverFromCorruptFile(reason) {
12843
+ const corruptPath = `${this.path}.corrupt-${this.fileTimestamp()}`;
12844
+ const report = {
12845
+ dropped: [{ section: "file", key: this.path, reason }]
12846
+ };
12847
+ try {
12848
+ await rename(this.path, corruptPath);
12849
+ report.corruptPath = corruptPath;
12850
+ } catch (error2) {
12851
+ report.backupError = error2 instanceof Error ? error2.message : String(error2);
12852
+ }
12853
+ this.loadReport = report;
12854
+ return createEmptyState();
12855
+ }
12856
+ }
12857
+ async function defaultWriteBackup(targetPath, content) {
12858
+ for (let attempt = 0;; attempt += 1) {
12859
+ const candidate = attempt === 0 ? targetPath : `${targetPath}-${attempt}`;
12860
+ try {
12861
+ await writeFile3(candidate, content, { encoding: "utf8", mode: 384, flag: "wx" });
12862
+ return candidate;
12863
+ } catch (error2) {
12864
+ if (error2.code !== "EEXIST" || attempt >= 9) {
12865
+ throw error2;
12866
+ }
12867
+ }
12489
12868
  }
12490
12869
  }
12870
+ var STATE_FILE_VERSION = 1;
12491
12871
  var init_state_store = __esm(() => {
12492
12872
  init_private_file();
12493
12873
  init_types();
@@ -12726,14 +13106,23 @@ class ScheduledTaskService {
12726
13106
  return task;
12727
13107
  });
12728
13108
  }
12729
- listPending() {
13109
+ listPending(chatKey) {
13110
+ return this.listPendingAllChats().filter((task) => task.chat_key === chatKey);
13111
+ }
13112
+ listPendingAllChats() {
12730
13113
  return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
12731
13114
  }
12732
- async cancelPending(inputId) {
13115
+ async cancelPending(inputId, chatKey) {
13116
+ return await this.cancelPendingWhere(inputId, (task) => task.chat_key === chatKey);
13117
+ }
13118
+ async cancelPendingAnyChat(inputId) {
13119
+ return await this.cancelPendingWhere(inputId, () => true);
13120
+ }
13121
+ async cancelPendingWhere(inputId, allowed) {
12733
13122
  return await this.mutate(async () => {
12734
13123
  const id = normalizeId(inputId);
12735
13124
  const task = this.state.scheduled_tasks[id];
12736
- if (!task || task.status !== "pending")
13125
+ if (!task || task.status !== "pending" || !allowed(task))
12737
13126
  return false;
12738
13127
  task.status = "cancelled";
12739
13128
  task.cancelled_at = this.now().toISOString();
@@ -12765,7 +13154,7 @@ class ScheduledTaskService {
12765
13154
  async claimDueTasks() {
12766
13155
  return await this.mutate(async () => {
12767
13156
  const nowMs = this.now().getTime();
12768
- const due = this.listPending().filter((task) => Date.parse(task.execute_at) <= nowMs);
13157
+ const due = this.listPendingAllChats().filter((task) => Date.parse(task.execute_at) <= nowMs);
12769
13158
  if (due.length === 0)
12770
13159
  return [];
12771
13160
  const at = this.now().toISOString();
@@ -12774,7 +13163,16 @@ class ScheduledTaskService {
12774
13163
  task.triggered_at = at;
12775
13164
  this.claimedInThisSession.add(task.id);
12776
13165
  }
12777
- await this.save();
13166
+ try {
13167
+ await this.save();
13168
+ } catch (error2) {
13169
+ for (const task of due) {
13170
+ task.status = "pending";
13171
+ delete task.triggered_at;
13172
+ this.claimedInThisSession.delete(task.id);
13173
+ }
13174
+ throw error2;
13175
+ }
12778
13176
  return due.map((task) => ({ ...task }));
12779
13177
  });
12780
13178
  }
@@ -12832,20 +13230,20 @@ var init_scheduled_service = () => {};
12832
13230
 
12833
13231
  // src/plugins/plugin-home.ts
12834
13232
  import { readFileSync as readFileSync2 } from "node:fs";
12835
- import { copyFile, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
13233
+ import { copyFile, mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
12836
13234
  import { homedir as homedir3 } from "node:os";
12837
- import { dirname as dirname6, join as join6 } from "node:path";
13235
+ import { dirname as dirname4, join as join6 } from "node:path";
12838
13236
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12839
13237
  function resolveCoreRoot() {
12840
13238
  try {
12841
- let dir = dirname6(fileURLToPath2(import.meta.url));
13239
+ let dir = dirname4(fileURLToPath2(import.meta.url));
12842
13240
  for (let depth = 0;depth < 12; depth++) {
12843
13241
  try {
12844
13242
  const pkg = JSON.parse(readFileSync2(join6(dir, "package.json"), "utf-8"));
12845
13243
  if (pkg.name && CORE_ROOT_NAMES.includes(pkg.name))
12846
13244
  return dir;
12847
13245
  } catch {}
12848
- const parent = dirname6(dir);
13246
+ const parent = dirname4(dir);
12849
13247
  if (parent === dir)
12850
13248
  break;
12851
13249
  dir = parent;
@@ -12863,7 +13261,7 @@ async function ensureCoreResolution(pluginHome) {
12863
13261
  for (const name of SHIM_SPECIFIERS) {
12864
13262
  const targetDir = join6(pluginHome, "node_modules", name);
12865
13263
  const dstJs = join6(targetDir, "plugin-api.js");
12866
- await mkdir6(targetDir, { recursive: true });
13264
+ await mkdir4(targetDir, { recursive: true });
12867
13265
  try {
12868
13266
  await copyFile(srcJs, dstJs);
12869
13267
  } catch (error2) {
@@ -12906,7 +13304,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12906
13304
  const manifestPath = join6(pluginHome, "package.json");
12907
13305
  let raw;
12908
13306
  try {
12909
- raw = await readFile6(manifestPath, "utf8");
13307
+ raw = await readFile7(manifestPath, "utf8");
12910
13308
  } catch {
12911
13309
  return false;
12912
13310
  }
@@ -12924,7 +13322,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12924
13322
  return true;
12925
13323
  }
12926
13324
  async function ensurePluginHome(pluginHome) {
12927
- await mkdir6(pluginHome, { recursive: true, mode: 448 });
13325
+ await mkdir4(pluginHome, { recursive: true, mode: 448 });
12928
13326
  await writeFile4(join6(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
12929
13327
  `, { flag: "wx" }).catch((error2) => {
12930
13328
  if (error2.code !== "EEXIST")
@@ -15540,7 +15938,7 @@ function createConversationExecutor() {
15540
15938
  var DEFAULT_SESSION_KEY = "__chat__";
15541
15939
 
15542
15940
  // src/channels/media-store.ts
15543
- import { access as access2, mkdir as mkdir7, readdir, rm as rm5, stat, writeFile as writeFile5 } from "node:fs/promises";
15941
+ import { access as access2, mkdir as mkdir5, readdir, rm as rm5, stat, writeFile as writeFile5 } from "node:fs/promises";
15544
15942
  import path7 from "node:path";
15545
15943
 
15546
15944
  class RuntimeMediaStore {
@@ -15558,7 +15956,7 @@ class RuntimeMediaStore {
15558
15956
  const safeMessageId = safePathSegment(input.messageId || "message");
15559
15957
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
15560
15958
  const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
15561
- await mkdir7(dir, { recursive: true });
15959
+ await mkdir5(dir, { recursive: true, mode: 448 });
15562
15960
  const resolvedRoot = path7.resolve(this.rootDir);
15563
15961
  const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
15564
15962
  if (!isPathInside(resolvedFile, resolvedRoot)) {
@@ -16947,15 +17345,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16947
17345
  await drainPendingFinalForJx(ctx);
16948
17346
  return { handled: true };
16949
17347
  }
16950
- case "/logout": {
16951
- if (listWeixinAccountIds().length === 0) {
16952
- await sendReply(ctx, t().weixin.noAccountsLoggedIn);
16953
- return { handled: true };
16954
- }
16955
- clearAllWeixinAccounts();
16956
- await sendReply(ctx, t().weixin.logoutSuccess);
16957
- return { handled: true };
16958
- }
16959
17348
  default:
16960
17349
  return { handled: false };
16961
17350
  }
@@ -16968,7 +17357,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16968
17357
  }
16969
17358
  }
16970
17359
  var init_slash_commands = __esm(() => {
16971
- init_accounts();
16972
17360
  init_logger();
16973
17361
  init_i18n();
16974
17362
  init_final_heads_up();
@@ -16984,14 +17372,14 @@ function normalizeMediaArray(media) {
16984
17372
  }
16985
17373
 
16986
17374
  // src/logging/rotating-file-writer.ts
16987
- import { readdir as readdir2, rename, rm as rm6, stat as stat2 } from "node:fs/promises";
16988
- import { basename, dirname as dirname7, join as join8 } from "node:path";
17375
+ import { readdir as readdir2, rename as rename2, rm as rm6, stat as stat2 } from "node:fs/promises";
17376
+ import { basename, dirname as dirname5, join as join8 } from "node:path";
16989
17377
  async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
16990
17378
  let currentSize = 0;
16991
17379
  try {
16992
17380
  currentSize = (await stat2(filePath)).size;
16993
17381
  } catch (error2) {
16994
- if (!isMissingFileError2(error2)) {
17382
+ if (!isMissingFileError3(error2)) {
16995
17383
  throw error2;
16996
17384
  }
16997
17385
  }
@@ -17009,24 +17397,24 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
17009
17397
  for (let index = maxFiles - 1;index >= 1; index -= 1) {
17010
17398
  const source = `${filePath}.${index}`;
17011
17399
  try {
17012
- await rename(source, `${filePath}.${index + 1}`);
17400
+ await rename2(source, `${filePath}.${index + 1}`);
17013
17401
  } catch (error2) {
17014
- if (!isMissingFileError2(error2)) {
17402
+ if (!isMissingFileError3(error2)) {
17015
17403
  throw error2;
17016
17404
  }
17017
17405
  }
17018
17406
  }
17019
- await rename(filePath, `${filePath}.1`);
17407
+ await rename2(filePath, `${filePath}.1`);
17020
17408
  }
17021
17409
  async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
17022
- const parentDir = dirname7(filePath);
17410
+ const parentDir = dirname5(filePath);
17023
17411
  const prefix = `${basename(filePath)}.`;
17024
17412
  const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
17025
17413
  let files = [];
17026
17414
  try {
17027
17415
  files = await readdir2(parentDir);
17028
17416
  } catch (error2) {
17029
- if (isMissingFileError2(error2)) {
17417
+ if (isMissingFileError3(error2)) {
17030
17418
  return;
17031
17419
  }
17032
17420
  throw error2;
@@ -17036,23 +17424,31 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
17036
17424
  continue;
17037
17425
  }
17038
17426
  const candidate = join8(parentDir, file);
17039
- const details = await stat2(candidate);
17427
+ let details;
17428
+ try {
17429
+ details = await stat2(candidate);
17430
+ } catch (error2) {
17431
+ if (isMissingFileError3(error2)) {
17432
+ continue;
17433
+ }
17434
+ throw error2;
17435
+ }
17040
17436
  if (details.mtime.getTime() < cutoff) {
17041
17437
  await rm6(candidate, { force: true });
17042
17438
  }
17043
17439
  }
17044
17440
  }
17045
- function isMissingFileError2(error2) {
17441
+ function isMissingFileError3(error2) {
17046
17442
  return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
17047
17443
  }
17048
17444
  var init_rotating_file_writer = () => {};
17049
17445
 
17050
17446
  // src/perf/perf-log-writer.ts
17051
17447
  import { appendFile as fsAppendFile, mkdir as fsMkdir } from "node:fs/promises";
17052
- import { dirname as dirname8 } from "node:path";
17448
+ import { dirname as dirname6 } from "node:path";
17053
17449
  function createPerfLogWriter(options) {
17054
17450
  const append = options.appendImpl ?? ((p, d) => fsAppendFile(p, d, "utf8"));
17055
- const mkdir8 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17451
+ const mkdir6 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17056
17452
  return;
17057
17453
  }));
17058
17454
  const now = options.now ?? (() => new Date);
@@ -17099,7 +17495,7 @@ function createPerfLogWriter(options) {
17099
17495
  return;
17100
17496
  const data = batch.join("");
17101
17497
  try {
17102
- await mkdir8(dirname8(options.filePath), { recursive: true });
17498
+ await mkdir6(dirname6(options.filePath), { recursive: true, mode: 448 });
17103
17499
  await rotateIfNeeded(options.filePath, Buffer.byteLength(data), options.maxSizeBytes, options.maxFiles);
17104
17500
  await append(options.filePath, data);
17105
17501
  consecutiveFailures = 0;
@@ -17630,13 +18026,39 @@ async function handleWeixinMessageTurn(full, deps) {
17630
18026
  deps.errLog(`weixin.final.dropped reason=backgrounded_no_store kind=text chatKey=${to}`);
17631
18027
  } else {
17632
18028
  const rawChunks = chunkFinalText(finalText, MAX_FINAL_CHUNK_BYTES);
18029
+ const sendAllParkedNotice = async (count) => {
18030
+ try {
18031
+ await sendMessageWeixin({
18032
+ to,
18033
+ text: t().misc.finalAllParked(count),
18034
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
18035
+ });
18036
+ } catch (noticeErr) {
18037
+ deps.errLog(`weixin.final.parked_notice_failed chatKey=${to} err=${String(noticeErr)}`);
18038
+ }
18039
+ };
18040
+ const buildPendingChunk = (text, seq, total) => {
18041
+ const entry = { text, seq, total };
18042
+ if (contextToken !== undefined)
18043
+ entry.contextToken = contextToken;
18044
+ if (deps.accountId !== undefined)
18045
+ entry.accountId = deps.accountId;
18046
+ return entry;
18047
+ };
17633
18048
  if (rawChunks.length > 0) {
17634
18049
  const total = rawChunks.length;
17635
18050
  if (total === 1) {
17636
18051
  const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
17637
18052
  if (!reserved) {
17638
- finalDropped = true;
17639
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
18053
+ if (deps.enqueuePendingFinal) {
18054
+ deps.enqueuePendingFinal(to, [buildPendingChunk(rawChunks[0], 1, 1)]);
18055
+ finalChunksPending = 1;
18056
+ deps.errLog(`weixin.final.parked reason=quota_exhausted kind=text chatKey=${to}`);
18057
+ await sendAllParkedNotice(1);
18058
+ } else {
18059
+ finalDropped = true;
18060
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
18061
+ }
17640
18062
  } else {
17641
18063
  await sendMessageWeixin({
17642
18064
  to,
@@ -17693,16 +18115,11 @@ ${buildFinalHeadsUp({
17693
18115
  const restToPark = prefixed.slice(sent);
17694
18116
  finalChunksPending = restToPark.length;
17695
18117
  if (restToPark.length > 0 && deps.enqueuePendingFinal) {
17696
- const pending = restToPark.map((text, idx) => {
17697
- const seq = sent + idx + 1;
17698
- const entry = { text, seq, total };
17699
- if (contextToken !== undefined)
17700
- entry.contextToken = contextToken;
17701
- if (deps.accountId !== undefined)
17702
- entry.accountId = deps.accountId;
17703
- return entry;
17704
- });
18118
+ const pending = restToPark.map((text, idx) => buildPendingChunk(text, sent + idx + 1, total));
17705
18119
  deps.enqueuePendingFinal(to, pending);
18120
+ if (sent === 0) {
18121
+ await sendAllParkedNotice(restToPark.length);
18122
+ }
17706
18123
  }
17707
18124
  }
17708
18125
  }
@@ -17898,7 +18315,7 @@ function shouldFetchTypingConfig(textBody) {
17898
18315
  const command = parseSlashCommand(textBody);
17899
18316
  if (!command)
17900
18317
  return true;
17901
- return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug", "/logout"].includes(command);
18318
+ return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug"].includes(command);
17902
18319
  }
17903
18320
  async function monitorWeixinProvider(opts) {
17904
18321
  const {
@@ -18621,7 +19038,7 @@ ${buildFinalHeadsUp({
18621
19038
  let sent = 0;
18622
19039
  for (let index = 0;index < wave.length; index += 1) {
18623
19040
  if (!deps.reserveFinal(quotaKey)) {
18624
- await deps.logger.info("scheduled.final_dropped", "scheduled turn final response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted", chunk: index + 1, total });
19041
+ await deps.logger.info(deps.enqueuePendingFinal ? "scheduled.final_parked" : "scheduled.final_dropped", deps.enqueuePendingFinal ? "scheduled turn final response parked due to quota" : "scheduled turn final response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted", chunk: index + 1, total });
18625
19042
  break;
18626
19043
  }
18627
19044
  const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
@@ -18630,7 +19047,7 @@ ${buildFinalHeadsUp({
18630
19047
  sent += 1;
18631
19048
  }
18632
19049
  const restToPark = chunks.slice(sent);
18633
- if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
19050
+ if (restToPark.length > 0 && deps.enqueuePendingFinal) {
18634
19051
  const pending = restToPark.map((text, index) => {
18635
19052
  const entry = { text, seq: sent + index + 1, total };
18636
19053
  if (deliveryContextToken)
@@ -18640,6 +19057,12 @@ ${buildFinalHeadsUp({
18640
19057
  return entry;
18641
19058
  });
18642
19059
  deps.enqueuePendingFinal(quotaKey, pending);
19060
+ if (sent === 0) {
19061
+ const noticeDelivered = await sendTextViaAvailableAccount(t().misc.finalAllParked(restToPark.length), "scheduled.final_parked_notice_failed");
19062
+ if (!noticeDelivered) {
19063
+ await deps.logger.info("scheduled.final_parked_notice_failed", "scheduled parked-final notice could not be delivered", { chatKey: input.chatKey, parked: restToPark.length });
19064
+ }
19065
+ }
18643
19066
  }
18644
19067
  }
18645
19068
  async function sendTextViaAvailableAccount(text, errorEvent) {
@@ -18677,8 +19100,8 @@ var init_scheduled_turn = __esm(() => {
18677
19100
  });
18678
19101
 
18679
19102
  // src/weixin/monitor/consumer-lock.ts
18680
- import { mkdir as mkdir8, open as open3, readFile as readFile7, rm as rm7 } from "node:fs/promises";
18681
- import { dirname as dirname9, join as join9 } from "node:path";
19103
+ import { mkdir as mkdir6, open as open3, readFile as readFile8, rm as rm7 } from "node:fs/promises";
19104
+ import { dirname as dirname7, join as join9 } from "node:path";
18682
19105
  import { homedir as homedir4 } from "node:os";
18683
19106
  function createWeixinConsumerLock(options = {}) {
18684
19107
  const lockFilePath = options.lockFilePath ?? join9(coreHomeDir(homedir4()), "runtime", "weixin-consumer.lock.json");
@@ -18686,7 +19109,7 @@ function createWeixinConsumerLock(options = {}) {
18686
19109
  const onDiagnostic = options.onDiagnostic;
18687
19110
  return {
18688
19111
  async acquire(meta2) {
18689
- await mkdir8(dirname9(lockFilePath), { recursive: true });
19112
+ await mkdir6(dirname7(lockFilePath), { recursive: true, mode: 448 });
18690
19113
  while (true) {
18691
19114
  try {
18692
19115
  const handle = await open3(lockFilePath, "wx");
@@ -18759,7 +19182,7 @@ function createWeixinConsumerLock(options = {}) {
18759
19182
  }
18760
19183
  async function loadLockMetadata(path14) {
18761
19184
  try {
18762
- const raw = await readFile7(path14, "utf8");
19185
+ const raw = await readFile8(path14, "utf8");
18763
19186
  const parsed = JSON.parse(raw);
18764
19187
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
18765
19188
  return null;
@@ -18824,6 +19247,13 @@ class WeixinChannel {
18824
19247
  logout() {
18825
19248
  logout();
18826
19249
  }
19250
+ stop() {
19251
+ this.agent = null;
19252
+ this.quota = null;
19253
+ this.logger = null;
19254
+ this.markDelivered = null;
19255
+ this.markFailed = null;
19256
+ }
18827
19257
  createConsumerLock(options) {
18828
19258
  return createWeixinConsumerLock({
18829
19259
  ...options?.lockFilePath ? { lockFilePath: options.lockFilePath } : {},
@@ -19393,6 +19823,122 @@ var init_plugin_loader = __esm(() => {
19393
19823
  init_plugin_home();
19394
19824
  });
19395
19825
 
19826
+ // src/plugins/plugin-doctor.ts
19827
+ import { readFile as readFile10 } from "node:fs/promises";
19828
+ import { join as join12 } from "node:path";
19829
+ function suggestedPluginPackageForChannel(type) {
19830
+ return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
19831
+ }
19832
+ async function readDependencyEntries(pluginHome) {
19833
+ try {
19834
+ const raw = await readFile10(join12(pluginHome, "package.json"), "utf8");
19835
+ const parsed = JSON.parse(raw);
19836
+ const out = {};
19837
+ for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
19838
+ if (typeof value === "string")
19839
+ out[name] = value;
19840
+ }
19841
+ return out;
19842
+ } catch (error2) {
19843
+ const message = error2 instanceof Error ? error2.message : String(error2);
19844
+ throw new Error(`failed to read plugin home package.json: ${message}`);
19845
+ }
19846
+ }
19847
+ async function inspectPlugins(input) {
19848
+ const issues = [];
19849
+ let dependencies;
19850
+ try {
19851
+ dependencies = await readDependencyEntries(input.pluginHome);
19852
+ } catch (error2) {
19853
+ const message = error2 instanceof Error ? error2.message : String(error2);
19854
+ return [{ level: "error", message }];
19855
+ }
19856
+ const importPlugin = input.importPlugin ?? importPluginFromHome;
19857
+ const allConfigured = input.config.plugins;
19858
+ const filterByName = input.pluginName ? normalizePluginPackageName(input.pluginName) : null;
19859
+ if (filterByName && !allConfigured.some((plugin) => normalizePluginPackageName(plugin.name) === filterByName)) {
19860
+ return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
19861
+ }
19862
+ const pushIfRelevant = (issue2) => {
19863
+ if (!filterByName || issue2.plugin === filterByName)
19864
+ issues.push(issue2);
19865
+ };
19866
+ const channelProviders = new Map;
19867
+ for (const configPlugin of allConfigured) {
19868
+ if (!(configPlugin.name in dependencies)) {
19869
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `package not installed in plugin home; run xacpx plugin add ${configPlugin.name}`, suggestion: `xacpx plugin add ${configPlugin.name} && xacpx restart` });
19870
+ continue;
19871
+ }
19872
+ let moduleValue;
19873
+ try {
19874
+ moduleValue = await importPlugin(configPlugin.name, input.pluginHome);
19875
+ } catch (error2) {
19876
+ const message = error2 instanceof Error ? error2.message : String(error2);
19877
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `failed to import plugin: ${message}`, suggestion: `xacpx plugin add ${configPlugin.name} && xacpx restart` });
19878
+ continue;
19879
+ }
19880
+ try {
19881
+ const plugin = validateWeacpxPlugin(moduleValue, configPlugin.name, {
19882
+ ...input.currentXacpxVersion !== undefined ? { currentXacpxVersion: input.currentXacpxVersion } : {}
19883
+ });
19884
+ const channels = plugin.channels ?? [];
19885
+ const channelTypes = channels.map((channel) => channel.type);
19886
+ for (const type of channelTypes) {
19887
+ const existing = channelProviders.get(type);
19888
+ if (existing) {
19889
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `channel type ${type} is already provided by ${existing.plugin}` });
19890
+ } else {
19891
+ channelProviders.set(type, { plugin: configPlugin.name, enabled: configPlugin.enabled });
19892
+ }
19893
+ }
19894
+ pushIfRelevant({
19895
+ level: configPlugin.enabled ? "ok" : "warn",
19896
+ plugin: configPlugin.name,
19897
+ message: configPlugin.enabled ? `plugin is installed and valid; channels: ${channelTypes.length > 0 ? channelTypes.join(", ") : "none"}` : `plugin is installed and valid but disabled; run xacpx plugin enable ${configPlugin.name}`,
19898
+ ...configPlugin.enabled ? {} : { suggestion: `xacpx plugin enable ${configPlugin.name}` }
19899
+ });
19900
+ } catch (error2) {
19901
+ const message = error2 instanceof Error ? error2.message : String(error2);
19902
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message });
19903
+ }
19904
+ }
19905
+ const builtInChannelTypes = new Set(listKnownChannelIds());
19906
+ for (const channel of input.config.channels) {
19907
+ if (channel.enabled === false)
19908
+ continue;
19909
+ if (builtInChannelTypes.has(channel.type))
19910
+ continue;
19911
+ const provider = channelProviders.get(channel.type);
19912
+ if (!provider) {
19913
+ if (!filterByName) {
19914
+ const suggestedPackage = suggestedPluginPackageForChannel(channel.type);
19915
+ issues.push({
19916
+ level: "error",
19917
+ message: `channel ${channel.type} is configured but no enabled plugin provides it; run xacpx plugin add ${suggestedPackage} or another plugin that provides type "${channel.type}"`,
19918
+ suggestion: `xacpx plugin add ${suggestedPackage}`
19919
+ });
19920
+ }
19921
+ continue;
19922
+ }
19923
+ if (!provider.enabled) {
19924
+ pushIfRelevant({
19925
+ level: "error",
19926
+ plugin: provider.plugin,
19927
+ message: `channel ${channel.type} is configured but provider plugin is disabled; run xacpx plugin enable ${provider.plugin}`,
19928
+ suggestion: `xacpx plugin enable ${provider.plugin}`
19929
+ });
19930
+ }
19931
+ }
19932
+ return issues;
19933
+ }
19934
+ var init_plugin_doctor = __esm(() => {
19935
+ init_channel_scope();
19936
+ init_plugin_loader();
19937
+ init_validate_plugin();
19938
+ init_known_plugins();
19939
+ init_plugin_renames();
19940
+ });
19941
+
19396
19942
  // src/channels/bootstrap.ts
19397
19943
  function bootstrapBuiltinChannels() {
19398
19944
  bootstrapBuiltinChannelFactories();
@@ -19404,8 +19950,8 @@ var init_bootstrap = __esm(() => {
19404
19950
  });
19405
19951
 
19406
19952
  // src/logging/app-logger.ts
19407
- import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
19408
- import { dirname as dirname11 } from "node:path";
19953
+ import { appendFile, chmod as chmod3, mkdir as mkdir7 } from "node:fs/promises";
19954
+ import { dirname as dirname9 } from "node:path";
19409
19955
  function createNoopAppLogger() {
19410
19956
  return {
19411
19957
  debug: async () => {},
@@ -19419,6 +19965,7 @@ function createAppLogger(options) {
19419
19965
  const now = options.now ?? (() => new Date);
19420
19966
  let writeChain = Promise.resolve();
19421
19967
  let modeEnsured = false;
19968
+ let writeErrorLatched = false;
19422
19969
  return {
19423
19970
  debug: async (event, message, context) => {
19424
19971
  await enqueueWrite("debug", event, message, context);
@@ -19430,14 +19977,21 @@ function createAppLogger(options) {
19430
19977
  await enqueueWrite("error", event, message, context);
19431
19978
  },
19432
19979
  cleanup: async () => {
19433
- await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19980
+ try {
19981
+ await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19982
+ } catch {}
19434
19983
  },
19435
19984
  flush: async () => {
19436
19985
  await writeChain;
19437
19986
  }
19438
19987
  };
19439
19988
  function enqueueWrite(level, event, message, context = {}) {
19440
- const writePromise = writeChain.catch(() => {}).then(() => writeLog2(level, event, message, context));
19989
+ const writePromise = writeChain.then(() => writeLog2(level, event, message, context)).catch((error2) => {
19990
+ if (!writeErrorLatched) {
19991
+ writeErrorLatched = true;
19992
+ console.error("[xacpx] app-logger: log file write failed — further write errors will be suppressed.", error2 instanceof Error ? error2.message : String(error2));
19993
+ }
19994
+ });
19441
19995
  writeChain = writePromise;
19442
19996
  return writePromise;
19443
19997
  }
@@ -19446,13 +20000,14 @@ function createAppLogger(options) {
19446
20000
  return;
19447
20001
  }
19448
20002
  const line = formatLogLine(now(), level, event, message, context);
19449
- await mkdir9(dirname11(options.filePath), { recursive: true });
20003
+ await mkdir7(dirname9(options.filePath), { recursive: true, mode: 448 });
19450
20004
  if (!modeEnsured) {
19451
20005
  modeEnsured = true;
19452
- await chmod2(options.filePath, 384).catch(() => {});
20006
+ await chmod3(options.filePath, 384).catch(() => {});
19453
20007
  }
19454
20008
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
19455
20009
  await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
20010
+ writeErrorLatched = false;
19456
20011
  }
19457
20012
  }
19458
20013
  function formatLogLine(time3, level, event, message, context) {
@@ -19481,7 +20036,7 @@ var init_app_logger = __esm(() => {
19481
20036
  });
19482
20037
 
19483
20038
  // src/transport/acpx-session-index.ts
19484
- import { readFile as readFile11 } from "node:fs/promises";
20039
+ import { readFile as readFile12 } from "node:fs/promises";
19485
20040
  import { homedir as homedir5 } from "node:os";
19486
20041
  import { resolve as resolve2 } from "node:path";
19487
20042
  async function resolveSessionAgentCommandFromIndex(session3) {
@@ -19490,7 +20045,7 @@ async function resolveSessionAgentCommandFromIndex(session3) {
19490
20045
  return;
19491
20046
  }
19492
20047
  try {
19493
- const raw = await readFile11(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
20048
+ const raw = await readFile12(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
19494
20049
  const parsed = JSON.parse(raw);
19495
20050
  const targetCwd = resolve2(session3.cwd);
19496
20051
  const match = parsed.entries?.find((entry) => entry.name === session3.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
@@ -19672,7 +20227,9 @@ function parseCommand(input) {
19672
20227
  if (!trimmed.startsWith("/")) {
19673
20228
  return { kind: "prompt", text: trimmed };
19674
20229
  }
19675
- const parts = tokenizeCommand(trimmed);
20230
+ const tokens = tokenizeCommand(trimmed);
20231
+ const parts = tokens.map((token) => token.value);
20232
+ const rawTail = (index) => index < tokens.length ? trimmed.slice(tokens[index]?.start ?? 0) : "";
19676
20233
  const command = normalizeCommand(parts[0] ?? "");
19677
20234
  if (command === "/help" && parts.length === 1)
19678
20235
  return { kind: "help" };
@@ -19781,7 +20338,7 @@ function parseCommand(input) {
19781
20338
  return { kind: "invalid", text: trimmed, recognizedCommand: "/session" };
19782
20339
  }
19783
20340
  if (command === "/group" && parts[1] === "new" && parts.length > 2) {
19784
- const title = parts.slice(2).join(" ");
20341
+ const title = rawTail(2);
19785
20342
  if (title.trim().length > 0) {
19786
20343
  return { kind: "group.new", title };
19787
20344
  }
@@ -19805,7 +20362,7 @@ function parseCommand(input) {
19805
20362
  }
19806
20363
  break;
19807
20364
  }
19808
- const task = parts.slice(index).join(" ");
20365
+ const task = rawTail(index);
19809
20366
  if (groupId.trim().length > 0 && targetAgent.trim().length > 0 && task.trim().length > 0) {
19810
20367
  return {
19811
20368
  kind: "group.delegate",
@@ -19859,7 +20416,7 @@ function parseCommand(input) {
19859
20416
  return { kind: "agent.rm", name: parts[2] };
19860
20417
  }
19861
20418
  if ((command === "/delegate" || command === "/dg") && parts[1]) {
19862
- const parsedDelegate = parseDelegateRequest(parts);
20419
+ const parsedDelegate = parseDelegateRequest(parts, rawTail);
19863
20420
  if (parsedDelegate) {
19864
20421
  return parsedDelegate;
19865
20422
  }
@@ -19887,7 +20444,11 @@ function parseCommand(input) {
19887
20444
  if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
19888
20445
  return { kind: "later.cancel", id: parts[2] };
19889
20446
  }
19890
- return { kind: "later.create", tokens: parts.slice(1) };
20447
+ return {
20448
+ kind: "later.create",
20449
+ tokens: parts.slice(1),
20450
+ tails: tokens.slice(1).map((_, index) => rawTail(index + 1))
20451
+ };
19891
20452
  }
19892
20453
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
19893
20454
  const name = parts[2];
@@ -20140,17 +20701,18 @@ function readNativeAttachCommand(parts, identifierIndex) {
20140
20701
  return { kind: "session.native.attach", identifier };
20141
20702
  }
20142
20703
  function normalizeCommand(command) {
20143
- if (command === "/ss")
20704
+ const lowered = command.toLowerCase();
20705
+ if (lowered === "/ss")
20144
20706
  return "/session";
20145
- if (command === "/ws")
20707
+ if (lowered === "/ws")
20146
20708
  return "/workspace";
20147
- if (command === "/pm")
20709
+ if (lowered === "/pm")
20148
20710
  return "/permission";
20149
- if (command === "/stop")
20711
+ if (lowered === "/stop")
20150
20712
  return "/cancel";
20151
- if (command === "/lt")
20713
+ if (lowered === "/lt")
20152
20714
  return "/later";
20153
- return command;
20715
+ return lowered;
20154
20716
  }
20155
20717
  function isRecognizedCommand(command) {
20156
20718
  return isKnownXacpxCommandPrefix(command);
@@ -20173,31 +20735,41 @@ function toNonInteractivePermission(value) {
20173
20735
  function tokenizeCommand(input) {
20174
20736
  const tokens = [];
20175
20737
  let current = "";
20176
- let quote = null;
20738
+ let start2 = -1;
20739
+ let closingQuote = null;
20740
+ let offset = 0;
20177
20741
  for (const char of input) {
20178
- if (quote) {
20179
- if (char === quote) {
20180
- quote = null;
20742
+ const charStart = offset;
20743
+ offset += char.length;
20744
+ if (closingQuote) {
20745
+ if (char === closingQuote) {
20746
+ closingQuote = null;
20181
20747
  } else {
20182
20748
  current += char;
20183
20749
  }
20184
20750
  continue;
20185
20751
  }
20186
- if (char === '"' || char === "'") {
20187
- quote = char;
20752
+ const close = QUOTE_PAIRS[char];
20753
+ if (close) {
20754
+ if (start2 === -1)
20755
+ start2 = charStart;
20756
+ closingQuote = close;
20188
20757
  continue;
20189
20758
  }
20190
20759
  if (/\s/.test(char)) {
20191
20760
  if (current.length > 0) {
20192
- tokens.push(current);
20761
+ tokens.push({ value: current, start: start2 });
20193
20762
  current = "";
20194
20763
  }
20764
+ start2 = -1;
20195
20765
  continue;
20196
20766
  }
20767
+ if (start2 === -1)
20768
+ start2 = charStart;
20197
20769
  current += char;
20198
20770
  }
20199
20771
  if (current.length > 0) {
20200
- tokens.push(current);
20772
+ tokens.push({ value: current, start: start2 });
20201
20773
  }
20202
20774
  return tokens;
20203
20775
  }
@@ -20240,7 +20812,7 @@ function parseListFilterFlags(parts, validStatuses) {
20240
20812
  }
20241
20813
  return { filter, ok: true };
20242
20814
  }
20243
- function parseDelegateRequest(parts) {
20815
+ function parseDelegateRequest(parts, rawTail) {
20244
20816
  const targetAgent = parts[1];
20245
20817
  if (!targetAgent) {
20246
20818
  return null;
@@ -20268,7 +20840,7 @@ function parseDelegateRequest(parts) {
20268
20840
  }
20269
20841
  break;
20270
20842
  }
20271
- const task = parts.slice(index).join(" ");
20843
+ const task = rawTail(index);
20272
20844
  if (task.trim().length === 0) {
20273
20845
  return null;
20274
20846
  }
@@ -20280,9 +20852,16 @@ function parseDelegateRequest(parts) {
20280
20852
  task
20281
20853
  };
20282
20854
  }
20283
- var TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20855
+ var QUOTE_PAIRS, TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20284
20856
  var init_parse_command = __esm(() => {
20285
20857
  init_command_list();
20858
+ QUOTE_PAIRS = {
20859
+ '"': '"',
20860
+ "'": "'",
20861
+ "“": "”",
20862
+ "‘": "’",
20863
+ """: """
20864
+ };
20286
20865
  TASK_STATUS_VALUES = [
20287
20866
  "pending",
20288
20867
  "needs_confirmation",
@@ -20295,7 +20874,50 @@ var init_parse_command = __esm(() => {
20295
20874
  });
20296
20875
 
20297
20876
  // src/commands/command-policy.ts
20877
+ function isInternalScheduledTurn(metadata) {
20878
+ return Boolean(metadata?.scheduledSessionAlias || metadata?.scheduledSessionDescriptor);
20879
+ }
20880
+ function resolveChannelOwnerIds(config4, channel) {
20881
+ if (!config4 || !channel) {
20882
+ return;
20883
+ }
20884
+ let configured = false;
20885
+ const ids = new Set;
20886
+ if (config4.channel?.type === channel && config4.channel.ownerIds) {
20887
+ configured = true;
20888
+ for (const id of config4.channel.ownerIds)
20889
+ ids.add(id);
20890
+ }
20891
+ for (const entry of config4.channels ?? []) {
20892
+ if ((entry.type === channel || entry.id === channel) && entry.ownerIds) {
20893
+ configured = true;
20894
+ for (const id of entry.ownerIds)
20895
+ ids.add(id);
20896
+ }
20897
+ }
20898
+ return configured ? [...ids] : undefined;
20899
+ }
20900
+ function withEffectiveOwner(metadata, config4) {
20901
+ if (!metadata?.channel || isInternalScheduledTurn(metadata)) {
20902
+ return metadata;
20903
+ }
20904
+ const ownerIds = resolveChannelOwnerIds(config4, metadata.channel);
20905
+ if (ownerIds === undefined) {
20906
+ return metadata;
20907
+ }
20908
+ const isOwner = metadata.isOwner === true || typeof metadata.senderId === "string" && ownerIds.includes(metadata.senderId);
20909
+ return { ...metadata, isOwner };
20910
+ }
20298
20911
  function authorizeCommandForChat(command, metadata) {
20912
+ if (metadata?.channel && !isInternalScheduledTurn(metadata) && metadata.chatType !== "direct" && metadata.chatType !== "group") {
20913
+ if (GROUP_PUBLIC_COMMAND_KINDS.has(command.kind)) {
20914
+ return { allowed: true };
20915
+ }
20916
+ return {
20917
+ allowed: false,
20918
+ reason: "chat-type-missing"
20919
+ };
20920
+ }
20299
20921
  if (metadata?.chatType !== "group") {
20300
20922
  return { allowed: true };
20301
20923
  }
@@ -20310,7 +20932,14 @@ function authorizeCommandForChat(command, metadata) {
20310
20932
  reason: "group-owner-required"
20311
20933
  };
20312
20934
  }
20313
- function renderCommandAccessDenied(command) {
20935
+ function renderCommandAccessDenied(command, reason) {
20936
+ if (reason === "chat-type-missing") {
20937
+ return [
20938
+ `⚠️ ${renderCommandLabel(command)}${t().misc.commandAccessDeniedChatTypeMissingSuffix}`,
20939
+ t().misc.commandAccessDeniedChatTypeMissingHint
20940
+ ].join(`
20941
+ `);
20942
+ }
20314
20943
  return [
20315
20944
  `⚠️ ${renderCommandLabel(command)}${t().misc.commandAccessDeniedSuffix}`,
20316
20945
  t().misc.commandAccessDeniedHint
@@ -20436,13 +21065,18 @@ async function handlePermissionModeSet(context, mode) {
20436
21065
  return { text: p.noWritableConfig };
20437
21066
  }
20438
21067
  const previous = cloneAppConfig(context.config);
21068
+ const previousRaw = await context.configStore.getRawValue(["transport", "permissionMode"]);
20439
21069
  const updated = await context.configStore.updateTransport({
20440
21070
  permissionMode: mode
20441
21071
  });
20442
21072
  try {
20443
21073
  await context.transport.updatePermissionPolicy?.(updated.transport);
20444
21074
  } catch (error2) {
20445
- await context.configStore.save(previous);
21075
+ if (previousRaw.present) {
21076
+ await context.configStore.setRawValue(["transport", "permissionMode"], previousRaw.value);
21077
+ } else {
21078
+ await context.configStore.unsetRawValue(["transport", "permissionMode"]);
21079
+ }
20446
21080
  context.replaceConfig(previous);
20447
21081
  throw error2;
20448
21082
  }
@@ -20458,13 +21092,18 @@ async function handlePermissionAutoSet(context, policy) {
20458
21092
  return { text: p.noWritableConfig };
20459
21093
  }
20460
21094
  const previous = cloneAppConfig(context.config);
21095
+ const previousRaw = await context.configStore.getRawValue(["transport", "nonInteractivePermissions"]);
20461
21096
  const updated = await context.configStore.updateTransport({
20462
21097
  nonInteractivePermissions: policy
20463
21098
  });
20464
21099
  try {
20465
21100
  await context.transport.updatePermissionPolicy?.(updated.transport);
20466
21101
  } catch (error2) {
20467
- await context.configStore.save(previous);
21102
+ if (previousRaw.present) {
21103
+ await context.configStore.setRawValue(["transport", "nonInteractivePermissions"], previousRaw.value);
21104
+ } else {
21105
+ await context.configStore.unsetRawValue(["transport", "nonInteractivePermissions"]);
21106
+ }
20468
21107
  context.replaceConfig(previous);
20469
21108
  throw error2;
20470
21109
  }
@@ -20514,99 +21153,83 @@ async function handleConfigSet(context, path14, rawValue) {
20514
21153
  if (!context.config || !context.configStore) {
20515
21154
  return { text: c.noWritableConfig };
20516
21155
  }
20517
- const previous = cloneAppConfig(context.config);
20518
- const updated = cloneAppConfig(context.config);
20519
- const result = applySupportedConfigUpdate(updated, path14, rawValue);
20520
- if ("error" in result) {
20521
- return { text: result.error };
21156
+ const plan = planSupportedConfigUpdate(context.config, path14, rawValue);
21157
+ if ("error" in plan) {
21158
+ return { text: plan.error };
20522
21159
  }
20523
- await context.configStore.save(updated);
21160
+ const previousConfig = cloneAppConfig(context.config);
21161
+ const previousRaw = await context.configStore.getRawValue(plan.rawPath);
21162
+ const updated = await context.configStore.setRawValue(plan.rawPath, plan.value);
20524
21163
  if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
20525
21164
  try {
20526
21165
  await context.transport.updatePermissionPolicy?.(updated.transport);
20527
21166
  } catch (error2) {
20528
- await context.configStore.save(previous);
20529
- context.replaceConfig(previous);
21167
+ if (previousRaw.present) {
21168
+ await context.configStore.setRawValue(plan.rawPath, previousRaw.value);
21169
+ } else {
21170
+ await context.configStore.unsetRawValue(plan.rawPath);
21171
+ }
21172
+ context.replaceConfig(previousConfig);
20530
21173
  throw error2;
20531
21174
  }
20532
21175
  }
20533
21176
  context.replaceConfig(updated);
20534
- return { text: c.updated(path14, result.renderedValue) };
21177
+ return { text: c.updated(path14, plan.renderedValue) };
20535
21178
  }
20536
- function applySupportedConfigUpdate(config4, path14, rawValue) {
21179
+ function planSupportedConfigUpdate(config4, path14, rawValue) {
20537
21180
  const c = t().config;
20538
21181
  switch (path14) {
20539
21182
  case "language": {
20540
21183
  if (!isLocale(rawValue))
20541
21184
  return { error: c.languageInvalid };
20542
- config4.language = rawValue;
20543
- return { renderedValue: rawValue };
21185
+ return { rawPath: ["language"], value: rawValue, renderedValue: rawValue };
20544
21186
  }
20545
21187
  case "transport.type": {
20546
21188
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
20547
21189
  if (!parsed)
20548
21190
  return { error: c.transportTypeInvalid };
20549
- config4.transport.type = parsed;
20550
- return { renderedValue: parsed };
21191
+ return { rawPath: ["transport", "type"], value: parsed, renderedValue: parsed };
20551
21192
  }
20552
21193
  case "transport.command":
20553
21194
  if (!rawValue.trim())
20554
21195
  return { error: c.transportCommandEmpty };
20555
- config4.transport.command = rawValue;
20556
- return { renderedValue: rawValue };
21196
+ return { rawPath: ["transport", "command"], value: rawValue, renderedValue: rawValue };
20557
21197
  case "transport.sessionInitTimeoutMs": {
20558
21198
  const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
20559
21199
  if ("error" in parsed)
20560
21200
  return parsed;
20561
- config4.transport.sessionInitTimeoutMs = parsed.value;
20562
- return { renderedValue: String(parsed.value) };
21201
+ return { rawPath: ["transport", "sessionInitTimeoutMs"], value: parsed.value, renderedValue: String(parsed.value) };
20563
21202
  }
20564
21203
  case "transport.permissionMode": {
20565
21204
  const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
20566
21205
  if (!parsed)
20567
21206
  return { error: c.transportPermissionModeInvalid };
20568
- config4.transport.permissionMode = parsed;
20569
- return { renderedValue: parsed };
21207
+ return { rawPath: ["transport", "permissionMode"], value: parsed, renderedValue: parsed };
20570
21208
  }
20571
21209
  case "transport.nonInteractivePermissions": {
20572
21210
  const parsed = parseEnum(rawValue, ["deny", "fail"]);
20573
21211
  if (!parsed)
20574
21212
  return { error: c.transportNonInteractiveInvalid };
20575
- config4.transport.nonInteractivePermissions = parsed;
20576
- return { renderedValue: parsed };
21213
+ return { rawPath: ["transport", "nonInteractivePermissions"], value: parsed, renderedValue: parsed };
20577
21214
  }
20578
21215
  case "transport.permissionPolicy":
20579
21216
  if (!rawValue.trim())
20580
21217
  return { error: c.transportPermissionPolicyEmpty };
20581
- config4.transport.permissionPolicy = rawValue;
20582
- return { renderedValue: rawValue };
21218
+ return { rawPath: ["transport", "permissionPolicy"], value: rawValue, renderedValue: rawValue };
20583
21219
  case "logging.level": {
20584
21220
  const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
20585
21221
  if (!parsed)
20586
21222
  return { error: c.loggingLevelInvalid };
20587
- config4.logging.level = parsed;
20588
- return { renderedValue: parsed };
20589
- }
20590
- case "logging.maxSizeBytes": {
20591
- const parsed = parsePositiveNumber(rawValue, "logging.maxSizeBytes");
20592
- if ("error" in parsed)
20593
- return parsed;
20594
- config4.logging.maxSizeBytes = parsed.value;
20595
- return { renderedValue: String(parsed.value) };
20596
- }
20597
- case "logging.maxFiles": {
20598
- const parsed = parsePositiveNumber(rawValue, "logging.maxFiles");
20599
- if ("error" in parsed)
20600
- return parsed;
20601
- config4.logging.maxFiles = parsed.value;
20602
- return { renderedValue: String(parsed.value) };
21223
+ return { rawPath: ["logging", "level"], value: parsed, renderedValue: parsed };
20603
21224
  }
21225
+ case "logging.maxSizeBytes":
21226
+ case "logging.maxFiles":
20604
21227
  case "logging.retentionDays": {
20605
- const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
21228
+ const field = path14.slice("logging.".length);
21229
+ const parsed = parsePositiveNumber(rawValue, path14);
20606
21230
  if ("error" in parsed)
20607
21231
  return parsed;
20608
- config4.logging.retentionDays = parsed.value;
20609
- return { renderedValue: String(parsed.value) };
21232
+ return { rawPath: ["logging", field], value: parsed.value, renderedValue: String(parsed.value) };
20610
21233
  }
20611
21234
  case "channel.type":
20612
21235
  return { error: c.channelTypeDisabled };
@@ -20614,15 +21237,15 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20614
21237
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20615
21238
  if (!parsed)
20616
21239
  return { error: c.channelReplyModeInvalid };
20617
- config4.channel.replyMode = parsed;
20618
- return { renderedValue: parsed };
21240
+ return { rawPath: ["channel", "replyMode"], value: parsed, renderedValue: parsed };
20619
21241
  }
20620
21242
  case "wechat.replyMode": {
20621
21243
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20622
21244
  if (!parsed)
20623
21245
  return { error: c.wechatReplyModeInvalid };
20624
- config4.channel.replyMode = parsed;
20625
21246
  return {
21247
+ rawPath: ["channel", "replyMode"],
21248
+ value: parsed,
20626
21249
  renderedValue: c.wechatReplyModeMapped(parsed)
20627
21250
  };
20628
21251
  }
@@ -20630,42 +21253,30 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20630
21253
  const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
20631
21254
  if (agentMatch) {
20632
21255
  const [, name, field] = agentMatch;
20633
- if (!name || !field) {
21256
+ if (!name || !field || isPrototypePollutingKey(name)) {
20634
21257
  return { error: c.pathNotSupported(path14) };
20635
21258
  }
20636
- const agent3 = config4.agents[name];
20637
- if (!agent3) {
21259
+ if (!Object.hasOwn(config4.agents, name)) {
20638
21260
  return { error: c.agentNotFound(name) };
20639
21261
  }
20640
21262
  if (!rawValue.trim()) {
20641
21263
  return { error: c.fieldEmpty(path14) };
20642
21264
  }
20643
- if (field === "driver") {
20644
- agent3.driver = rawValue;
20645
- } else {
20646
- agent3.command = rawValue;
20647
- }
20648
- return { renderedValue: rawValue };
21265
+ return { rawPath: ["agents", name, field], value: rawValue, renderedValue: rawValue };
20649
21266
  }
20650
21267
  const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
20651
21268
  if (workspaceMatch) {
20652
21269
  const [, name, field] = workspaceMatch;
20653
- if (!name || !field) {
21270
+ if (!name || !field || isPrototypePollutingKey(name)) {
20654
21271
  return { error: c.pathNotSupported(path14) };
20655
21272
  }
20656
- const workspace3 = config4.workspaces[name];
20657
- if (!workspace3) {
21273
+ if (!Object.hasOwn(config4.workspaces, name)) {
20658
21274
  return { error: c.workspaceNotFound(name) };
20659
21275
  }
20660
21276
  if (!rawValue.trim()) {
20661
21277
  return { error: c.fieldEmpty(path14) };
20662
21278
  }
20663
- if (field === "cwd") {
20664
- workspace3.cwd = rawValue;
20665
- } else {
20666
- workspace3.description = rawValue;
20667
- }
20668
- return { renderedValue: rawValue };
21279
+ return { rawPath: ["workspaces", name, field], value: rawValue, renderedValue: rawValue };
20669
21280
  }
20670
21281
  const channelMatch = path14.match(/^channels\.([^.]+)\.replyMode$/);
20671
21282
  if (channelMatch) {
@@ -20681,11 +21292,21 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20681
21292
  if (!parsed) {
20682
21293
  return { error: c.channelRuntimeReplyModeInvalid(id) };
20683
21294
  }
20684
- channel.replyMode = parsed;
20685
- return { renderedValue: parsed };
21295
+ return {
21296
+ rawPath: [
21297
+ "channels",
21298
+ { id, createWith: { id: channel.id, type: channel.type, enabled: channel.enabled } },
21299
+ "replyMode"
21300
+ ],
21301
+ value: parsed,
21302
+ renderedValue: parsed
21303
+ };
20686
21304
  }
20687
21305
  return { error: c.pathNotSupported(path14) };
20688
21306
  }
21307
+ function isPrototypePollutingKey(key) {
21308
+ return key === "__proto__" || key === "constructor" || key === "prototype";
21309
+ }
20689
21310
  function parseEnum(value, allowed) {
20690
21311
  return allowed.includes(value) ? value : null;
20691
21312
  }
@@ -21227,6 +21848,10 @@ async function handleSessions(context, chatKey) {
21227
21848
  async function handleSessionNew(context, chatKey, alias, agent3, workspace3) {
21228
21849
  const channelId = getChannelIdFromChatKey(chatKey);
21229
21850
  const internalAlias = scopeDisplayAliasToInternal(channelId, alias);
21851
+ const existing = context.sessions.getResolvedSessionByInternalAlias(internalAlias);
21852
+ if (existing) {
21853
+ return { text: t().session.sessionAlreadyExists(alias, existing.agent, existing.workspace) };
21854
+ }
21230
21855
  const session3 = context.lifecycle.resolveSession(internalAlias, agent3, workspace3, `${workspace3}:${internalAlias}`);
21231
21856
  const releaseTransportReservation = await context.lifecycle.reserveTransportSession(session3.transportSession);
21232
21857
  try {
@@ -21497,7 +22122,9 @@ async function handleSessionRemove(context, chatKey, alias) {
21497
22122
  }
21498
22123
  }
21499
22124
  const sharedAliasCount = context.sessions.countAliasesSharingTransport(session3.transportSession, internalAlias);
22125
+ const wasCurrentInThisChat = context.sessions.peekCurrentSessionAlias(chatKey) === internalAlias;
21500
22126
  const { wasActive } = await context.sessions.removeSession(internalAlias);
22127
+ const promotedAlias = wasCurrentInThisChat ? context.sessions.peekCurrentSessionAlias(chatKey) || undefined : undefined;
21501
22128
  let orchestrationPurgeWarning;
21502
22129
  if (context.orchestration) {
21503
22130
  try {
@@ -21535,7 +22162,7 @@ async function handleSessionRemove(context, chatKey, alias) {
21535
22162
  const s = t().session;
21536
22163
  const lines = [s.sessionRemoved(alias)];
21537
22164
  if (wasActive) {
21538
- lines.push(s.sessionRemovedWasActive);
22165
+ lines.push(promotedAlias ? s.sessionRemovedWasActivePromoted(toDisplaySessionAlias(promotedAlias)) : s.sessionRemovedWasActive);
21539
22166
  }
21540
22167
  if (!shouldTeardownTransport) {
21541
22168
  lines.push(s.sessionTransportShared(session3.transportSession, sharedAliasCount));
@@ -22363,7 +22990,7 @@ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {})
22363
22990
  name = allocateWorkspaceName(base, context.config.workspaces);
22364
22991
  notice = w.nameSanitized(workspaceName, name);
22365
22992
  }
22366
- const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
22993
+ const updated = await context.configStore.upsertWorkspace(name, cwd);
22367
22994
  context.replaceConfig(updated);
22368
22995
  const savedLine = w.saved(name);
22369
22996
  return { text: notice ? `${notice}
@@ -22479,10 +23106,10 @@ function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValu
22479
23106
  if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
22480
23107
  return { ok: false, code: "missing_message" };
22481
23108
  const delta = executeAt.getTime() - now.getTime();
23109
+ if (isNaN(delta) || delta > LATER_MAX_DELAY_MS)
23110
+ return { ok: false, code: "out_of_range" };
22482
23111
  if (delta < LATER_MIN_DELAY_MS)
22483
23112
  return { ok: false, code: "too_soon" };
22484
- if (delta > LATER_MAX_DELAY_MS)
22485
- return { ok: false, code: "out_of_range" };
22486
23113
  return { ok: true, executeAt, messageStartIndex };
22487
23114
  }
22488
23115
  var WEEKDAYS, ZH_MIN = "分钟", ZH_HOUR = "小时", ZH_DAY_UNIT = "天", ZH_TODAY = "今天", ZH_TOMORROW = "明天", ZH_DAY_AFTER = "后天", ZH_AFTER = "后", ZH_RELATIVE_RE;
@@ -22558,16 +23185,17 @@ function laterHelp() {
22558
23185
  function handleLaterHelp() {
22559
23186
  return { text: renderLaterHelp() };
22560
23187
  }
22561
- async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
23188
+ async function handleLaterCreate(tokens, tails, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
22562
23189
  const l = t().later;
22563
- let rest = tokens;
23190
+ let restStart = 0;
22564
23191
  const seenFlags = new Set;
22565
23192
  let flagMode;
22566
- while (rest.length > 0 && (rest[0] === "--bind" || rest[0] === "--temp")) {
22567
- seenFlags.add(rest[0]);
22568
- flagMode = rest[0] === "--bind" ? "bound" : "temp";
22569
- rest = rest.slice(1);
23193
+ while (restStart < tokens.length && (tokens[restStart] === "--bind" || tokens[restStart] === "--temp")) {
23194
+ seenFlags.add(tokens[restStart] ?? "");
23195
+ flagMode = tokens[restStart] === "--bind" ? "bound" : "temp";
23196
+ restStart += 1;
22570
23197
  }
23198
+ const rest = tokens.slice(restStart);
22571
23199
  if (seenFlags.size > 1) {
22572
23200
  return { text: l.bindAndTempMutuallyExclusive };
22573
23201
  }
@@ -22588,7 +23216,7 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22588
23216
  if (!result.ok) {
22589
23217
  return { text: renderTimeParseError(result.code, result.value) };
22590
23218
  }
22591
- const message = rest.slice(result.messageStartIndex).join(" ").trim();
23219
+ const message = (tails[restStart + result.messageStartIndex] ?? "").trim();
22592
23220
  if (message.startsWith("/")) {
22593
23221
  return {
22594
23222
  text: [
@@ -22612,12 +23240,12 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22612
23240
  });
22613
23241
  return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSession.alias)) };
22614
23242
  }
22615
- function handleLaterList(scheduled) {
22616
- const tasks = scheduled.listPending();
23243
+ function handleLaterList(scheduled, chatKey) {
23244
+ const tasks = scheduled.listPending(chatKey);
22617
23245
  return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
22618
23246
  }
22619
- async function handleLaterCancel(id, scheduled) {
22620
- const ok = await scheduled.cancelPending(id);
23247
+ async function handleLaterCancel(id, scheduled, chatKey) {
23248
+ const ok = await scheduled.cancelPending(id, chatKey);
22621
23249
  const displayId = id.replace(/^#/, "").toLowerCase();
22622
23250
  if (ok) {
22623
23251
  return { text: t().later.cancelSuccess(displayId) };
@@ -23454,7 +24082,7 @@ var init_session_recovery_handler = __esm(() => {
23454
24082
  // src/recovery/auto-install-optional-dep.ts
23455
24083
  import { spawn as spawn5 } from "node:child_process";
23456
24084
  import { createWriteStream } from "node:fs";
23457
- import { mkdir as mkdir10 } from "node:fs/promises";
24085
+ import { mkdir as mkdir8 } from "node:fs/promises";
23458
24086
  import { homedir as homedir6 } from "node:os";
23459
24087
  import { join as join14 } from "node:path";
23460
24088
  async function autoInstallOptionalDep(pkg, parentPackages, options = {}) {
@@ -23572,7 +24200,7 @@ ${err.message}`, reason: "spawn" });
23572
24200
  });
23573
24201
  }, defaultLogSink = async () => {
23574
24202
  const dir = join14(coreHomeDir(homedir6()), "logs");
23575
- await mkdir10(dir, { recursive: true });
24203
+ await mkdir8(dir, { recursive: true });
23576
24204
  const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
23577
24205
  const path14 = join14(dir, `auto-install-${timestamp}.log`);
23578
24206
  const stream = createWriteStream(path14, { flags: "a" });
@@ -23601,7 +24229,7 @@ import { spawn as spawn6 } from "node:child_process";
23601
24229
  import { createRequire as createRequire3 } from "node:module";
23602
24230
  import { access as access3 } from "node:fs/promises";
23603
24231
  import { homedir as homedir7 } from "node:os";
23604
- import { dirname as dirname12, join as join15 } from "node:path";
24232
+ import { dirname as dirname10, join as join15 } from "node:path";
23605
24233
  function deriveParentPackageName(platformPackage) {
23606
24234
  return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
23607
24235
  }
@@ -23674,7 +24302,7 @@ function defaultResolveFromCwd(name, cwd) {
23674
24302
  const pkgJson = require2.resolve(`${name}/package.json`, {
23675
24303
  paths: [cwd, ...require2.resolve.paths(name) ?? []]
23676
24304
  });
23677
- return dirname12(pkgJson);
24305
+ return dirname10(pkgJson);
23678
24306
  } catch {
23679
24307
  return null;
23680
24308
  }
@@ -23809,11 +24437,11 @@ async function handleSessionResetCommand(context, ops, chatKey) {
23809
24437
  chatKey,
23810
24438
  native: wasNative && Boolean(freshAgentSessionId)
23811
24439
  });
23812
- if (wasNative && context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
24440
+ if (context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
23813
24441
  try {
23814
24442
  await context.transport.removeSession(previous);
23815
24443
  } catch (error2) {
23816
- await context.logger.info("session.reset.close_previous_failed", "failed to close previous native session after reset", {
24444
+ await context.logger.info("session.reset.close_previous_failed", "failed to close previous session after reset", {
23817
24445
  transportSession: previous.transportSession,
23818
24446
  error: error2 instanceof Error ? error2.message : String(error2)
23819
24447
  });
@@ -23875,9 +24503,20 @@ class CommandRouter {
23875
24503
  chatKey,
23876
24504
  kind: command.kind
23877
24505
  });
24506
+ await this.refreshConfigFromStore();
24507
+ perfSpan?.mark("router.config_refreshed");
24508
+ metadata = withEffectiveOwner(metadata, this.config);
23878
24509
  const access4 = authorizeCommandForChat(command, metadata);
23879
24510
  perfSpan?.mark("router.authorized", { decision: access4.allowed ? "allow" : "deny" });
23880
24511
  if (!access4.allowed) {
24512
+ if (access4.reason === "chat-type-missing") {
24513
+ await this.logger.error("channel.chat_type_missing", "channel turn carried no chatType; denying privileged command (channel metadata contract violation)", {
24514
+ chatKey,
24515
+ kind: command.kind,
24516
+ channel: metadata?.channel,
24517
+ senderId: metadata?.senderId
24518
+ });
24519
+ }
23881
24520
  await this.logger.info("command.blocked", "blocked command by chat policy", {
23882
24521
  chatKey,
23883
24522
  kind: command.kind,
@@ -23885,10 +24524,8 @@ class CommandRouter {
23885
24524
  channel: metadata?.channel,
23886
24525
  senderId: metadata?.senderId
23887
24526
  });
23888
- return { text: renderCommandAccessDenied(command) };
24527
+ return { text: renderCommandAccessDenied(command, access4.reason) };
23889
24528
  }
23890
- await this.refreshConfigFromStore();
23891
- perfSpan?.mark("router.config_refreshed");
23892
24529
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
23893
24530
  switch (command.kind) {
23894
24531
  case "invalid":
@@ -23990,7 +24627,7 @@ class CommandRouter {
23990
24627
  case "later.list":
23991
24628
  if (!this.scheduled)
23992
24629
  return { text: t().later.serviceNotEnabled };
23993
- return handleLaterList(this.scheduled);
24630
+ return handleLaterList(this.scheduled, chatKey);
23994
24631
  case "later.create": {
23995
24632
  if (!this.scheduled)
23996
24633
  return { text: t().later.serviceNotEnabled };
@@ -23998,12 +24635,12 @@ class CommandRouter {
23998
24635
  return { text: renderLaterUnsupportedChannel() };
23999
24636
  }
24000
24637
  const currentSession = await this.sessions.getCurrentSession(chatKey);
24001
- return await handleLaterCreate(command.tokens, this.scheduled, chatKey, currentSession ? { alias: currentSession.alias, agent: currentSession.agent, workspace: currentSession.workspace } : null, this.config?.later?.defaultMode === "bind" ? "bound" : "temp", accountId, replyContextToken);
24638
+ return await handleLaterCreate(command.tokens, command.tails, this.scheduled, chatKey, currentSession ? { alias: currentSession.alias, agent: currentSession.agent, workspace: currentSession.workspace } : null, this.config?.later?.defaultMode === "bind" ? "bound" : "temp", accountId, replyContextToken);
24002
24639
  }
24003
24640
  case "later.cancel":
24004
24641
  if (!this.scheduled)
24005
24642
  return { text: t().later.serviceNotEnabled };
24006
- return await handleLaterCancel(command.id, this.scheduled);
24643
+ return await handleLaterCancel(command.id, this.scheduled, chatKey);
24007
24644
  case "prompt": {
24008
24645
  const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
24009
24646
  if (metadata?.scheduledSessionDescriptor) {
@@ -24507,7 +25144,7 @@ var init_console_agent = __esm(() => {
24507
25144
  });
24508
25145
 
24509
25146
  // src/orchestration/orchestration-server.ts
24510
- import { rm as rm8 } from "node:fs/promises";
25147
+ import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
24511
25148
  import { createServer } from "node:net";
24512
25149
 
24513
25150
  class OrchestrationServer {
@@ -24554,6 +25191,7 @@ class OrchestrationServer {
24554
25191
  });
24555
25192
  });
24556
25193
  await this.listenWithUnixSocketRecovery();
25194
+ await this.hardenUnixSocketPermissions();
24557
25195
  this.started = true;
24558
25196
  }
24559
25197
  async stop() {
@@ -24742,17 +25380,19 @@ class OrchestrationServer {
24742
25380
  return task;
24743
25381
  }
24744
25382
  parseRequestDelegateRpcInput(params) {
24745
- requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId"], "params");
25383
+ requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId", "parallel"], "params");
24746
25384
  const cwd = requireOptionalString(params, "cwd");
24747
25385
  const role = requireOptionalString(params, "role");
24748
25386
  const groupId = requireOptionalString(params, "groupId");
25387
+ const parallel = requireOptionalBoolean(params, "parallel");
24749
25388
  return {
24750
25389
  sourceHandle: requireString(params, "sourceHandle"),
24751
25390
  targetAgent: requireString(params, "targetAgent"),
24752
25391
  task: requireString(params, "task"),
24753
25392
  ...cwd !== undefined ? { cwd } : {},
24754
25393
  ...role !== undefined ? { role } : {},
24755
- ...groupId !== undefined ? { groupId } : {}
25394
+ ...groupId !== undefined ? { groupId } : {},
25395
+ ...parallel !== undefined ? { parallel } : {}
24756
25396
  };
24757
25397
  }
24758
25398
  parseTaskListFilter(params) {
@@ -24835,6 +25475,17 @@ class OrchestrationServer {
24835
25475
  whatIsNeeded: requireString(params, "whatIsNeeded")
24836
25476
  };
24837
25477
  }
25478
+ async hardenUnixSocketPermissions() {
25479
+ if (this.endpoint.kind !== "unix") {
25480
+ return;
25481
+ }
25482
+ const chmodFile = this.deps.chmodFile ?? chmod4;
25483
+ try {
25484
+ await chmodFile(this.endpoint.path, 384);
25485
+ } catch (error2) {
25486
+ this.deps.onSocketHardenError?.(error2);
25487
+ }
25488
+ }
24838
25489
  async cleanupEndpoint() {
24839
25490
  if (this.endpoint.kind !== "unix") {
24840
25491
  return;
@@ -28316,18 +28967,39 @@ class ScheduledTaskScheduler {
28316
28967
  return;
28317
28968
  this.ticking = true;
28318
28969
  try {
28319
- const dueTasks = await this.service.claimDueTasks();
28970
+ let dueTasks;
28971
+ try {
28972
+ dueTasks = await this.service.claimDueTasks();
28973
+ } catch (claimError) {
28974
+ await this.logger?.error("scheduled.claim.failed", "claimDueTasks threw; skipping tick", { message: claimError instanceof Error ? claimError.message : String(claimError) });
28975
+ return;
28976
+ }
28320
28977
  for (const task of dueTasks) {
28321
28978
  try {
28322
28979
  await this.dispatchWithTimeout(task);
28323
- await this.service.markExecuted(task.id);
28324
28980
  } catch (error2) {
28325
28981
  const message = error2 instanceof Error ? error2.message : String(error2);
28326
28982
  await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
28327
28983
  taskId: task.id,
28328
28984
  message
28329
28985
  });
28330
- await this.service.markFailed(task.id, error2);
28986
+ try {
28987
+ await this.service.markFailed(task.id, error2);
28988
+ } catch (markError) {
28989
+ await this.logger?.error("scheduled.dispatch.mark_failed", "markFailed threw; task state may be stale", {
28990
+ taskId: task.id,
28991
+ message: markError instanceof Error ? markError.message : String(markError)
28992
+ });
28993
+ }
28994
+ continue;
28995
+ }
28996
+ try {
28997
+ await this.service.markExecuted(task.id);
28998
+ } catch (markError) {
28999
+ await this.logger?.error("scheduled.dispatch.mark_executed_failed", "markExecuted threw after a successful dispatch; leaving task state for startup reconciliation", {
29000
+ taskId: task.id,
29001
+ message: markError instanceof Error ? markError.message : String(markError)
29002
+ });
28331
29003
  }
28332
29004
  }
28333
29005
  } finally {
@@ -28507,12 +29179,12 @@ var init_scheduled_route_create = __esm(() => {
28507
29179
 
28508
29180
  // src/scheduled/scheduled-route-manage.ts
28509
29181
  async function listScheduledTasksFromRoute(input, deps) {
28510
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
28511
- return deps.scheduled.listPending();
29182
+ const route = resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
29183
+ return deps.scheduled.listPending(route.chatKey);
28512
29184
  }
28513
29185
  async function cancelScheduledTaskFromRoute(input, deps) {
28514
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
28515
- const cancelled = await deps.scheduled.cancelPending(input.id);
29186
+ const route = resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
29187
+ const cancelled = await deps.scheduled.cancelPending(input.id, route.chatKey);
28516
29188
  return { id: normalizeId(input.id), cancelled };
28517
29189
  }
28518
29190
  function resolveOwnedCoordinatorRoute(coordinatorSession, state, label) {
@@ -28557,25 +29229,37 @@ class SessionService {
28557
29229
  const seen = new Set;
28558
29230
  const resolved = [];
28559
29231
  for (const session3 of Object.values(this.state.sessions)) {
28560
- if (seen.has(session3.transport_session)) {
29232
+ let candidate;
29233
+ try {
29234
+ candidate = this.toResolvedSession(session3);
29235
+ } catch {
28561
29236
  continue;
28562
29237
  }
28563
- seen.add(session3.transport_session);
28564
- try {
28565
- resolved.push(this.toResolvedSession(session3));
28566
- } catch {}
29238
+ const key = JSON.stringify([
29239
+ candidate.agent,
29240
+ candidate.agentCommand ?? null,
29241
+ candidate.cwd,
29242
+ candidate.transportSession
29243
+ ]);
29244
+ if (seen.has(key)) {
29245
+ continue;
29246
+ }
29247
+ seen.add(key);
29248
+ resolved.push(candidate);
28567
29249
  }
28568
29250
  return resolved;
28569
29251
  }
28570
29252
  resolveSession(alias, agent3, workspace3, transportSession) {
28571
29253
  this.validateSession(alias, agent3, workspace3);
29254
+ const existing = this.state.sessions[alias];
29255
+ const sameAgentExisting = existing && existing.agent === agent3 ? existing : undefined;
28572
29256
  return this.toResolvedSession({
28573
29257
  alias,
28574
29258
  agent: agent3,
28575
29259
  workspace: workspace3,
28576
29260
  transport_session: transportSession,
28577
- transport_agent_command: this.state.sessions[alias]?.transport_agent_command,
28578
- created_at: this.state.sessions[alias]?.created_at ?? new Date().toISOString(),
29261
+ transport_agent_command: sameAgentExisting?.transport_agent_command,
29262
+ created_at: existing?.created_at ?? new Date().toISOString(),
28579
29263
  last_used_at: new Date().toISOString()
28580
29264
  });
28581
29265
  }
@@ -28647,10 +29331,13 @@ class SessionService {
28647
29331
  const previousCurrent = prevCtx?.current_session;
28648
29332
  const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
28649
29333
  session3.last_used_at = new Date().toISOString();
28650
- this.state.chat_contexts[chatKey] = {
28651
- current_session: internalAlias,
28652
- ...carriedPrevious ? { previous_session: carriedPrevious } : {}
28653
- };
29334
+ const nextCtx = { ...prevCtx, current_session: internalAlias };
29335
+ if (carriedPrevious) {
29336
+ nextCtx.previous_session = carriedPrevious;
29337
+ } else {
29338
+ delete nextCtx.previous_session;
29339
+ }
29340
+ this.state.chat_contexts[chatKey] = nextCtx;
28654
29341
  await this.persist();
28655
29342
  return {
28656
29343
  alias: toDisplaySessionAlias(session3.alias),
@@ -28677,10 +29364,13 @@ class SessionService {
28677
29364
  }
28678
29365
  const currentInternal = ctx?.current_session;
28679
29366
  prevSession.last_used_at = new Date().toISOString();
28680
- this.state.chat_contexts[chatKey] = {
28681
- current_session: prevInternal,
28682
- ...currentInternal && currentInternal !== prevInternal ? { previous_session: currentInternal } : {}
28683
- };
29367
+ const nextCtx = { ...ctx, current_session: prevInternal };
29368
+ if (currentInternal && currentInternal !== prevInternal) {
29369
+ nextCtx.previous_session = currentInternal;
29370
+ } else {
29371
+ delete nextCtx.previous_session;
29372
+ }
29373
+ this.state.chat_contexts[chatKey] = nextCtx;
28684
29374
  await this.persist();
28685
29375
  return {
28686
29376
  alias: toDisplaySessionAlias(prevSession.alias),
@@ -28851,13 +29541,26 @@ class SessionService {
28851
29541
  const wasActive = Object.values(this.state.chat_contexts).some((ctx) => ctx.current_session === alias);
28852
29542
  delete this.state.sessions[alias];
28853
29543
  for (const [chatKey, ctx] of Object.entries(this.state.chat_contexts)) {
28854
- if (ctx.current_session === alias) {
28855
- delete this.state.chat_contexts[chatKey];
28856
- continue;
28857
- }
28858
29544
  if (ctx.previous_session === alias) {
28859
29545
  delete ctx.previous_session;
28860
29546
  }
29547
+ if (ctx.current_session === alias) {
29548
+ if (ctx.previous_session) {
29549
+ ctx.current_session = ctx.previous_session;
29550
+ delete ctx.previous_session;
29551
+ } else {
29552
+ ctx.current_session = "";
29553
+ }
29554
+ }
29555
+ if (ctx.background_results && alias in ctx.background_results) {
29556
+ delete ctx.background_results[alias];
29557
+ if (Object.keys(ctx.background_results).length === 0) {
29558
+ delete ctx.background_results;
29559
+ }
29560
+ }
29561
+ if (!ctx.current_session && !ctx.previous_session && !ctx.background_results) {
29562
+ delete this.state.chat_contexts[chatKey];
29563
+ }
28861
29564
  }
28862
29565
  await this.persist();
28863
29566
  return { wasActive };
@@ -28971,6 +29674,7 @@ class SessionService {
28971
29674
  throw new Error(`transport session "${transportSession}" conflicts with an external coordinator`);
28972
29675
  }
28973
29676
  const existingSession = this.state.sessions[alias];
29677
+ const sameAgentExisting = existingSession && existingSession.agent === agent3 ? existingSession : undefined;
28974
29678
  const now = new Date(this.now()).toISOString();
28975
29679
  const normalizedTransportAgentCommand = transportAgentCommand?.trim();
28976
29680
  const session3 = {
@@ -28983,9 +29687,9 @@ class SessionService {
28983
29687
  agent_session_title: native?.title ?? undefined,
28984
29688
  agent_session_updated_at: native?.updatedAt,
28985
29689
  attached_at: native ? now : undefined,
28986
- ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
28987
- mode_id: existingSession?.mode_id,
28988
- reply_mode: existingSession?.reply_mode,
29690
+ ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : sameAgentExisting?.transport_agent_command ? { transport_agent_command: sameAgentExisting.transport_agent_command } : {},
29691
+ mode_id: sameAgentExisting?.mode_id,
29692
+ reply_mode: sameAgentExisting?.reply_mode,
28989
29693
  created_at: existingSession?.created_at ?? now,
28990
29694
  last_used_at: now
28991
29695
  };
@@ -29235,8 +29939,9 @@ async function runConsole(paths, deps) {
29235
29939
  throw error2;
29236
29940
  }
29237
29941
  }
29238
- await runtime.reapStaleQueueOwners();
29942
+ const reapPromise = Promise.resolve(runtime.reapStaleQueueOwners()).catch(() => {});
29239
29943
  if (deps.beforeReady) {
29944
+ await reapPromise;
29240
29945
  await deps.beforeReady(runtime);
29241
29946
  }
29242
29947
  if (deps.daemonRuntime) {
@@ -29250,6 +29955,7 @@ async function runConsole(paths, deps) {
29250
29955
  deps.daemonRuntime?.heartbeat().catch(() => {});
29251
29956
  }, deps.heartbeatIntervalMs ?? 30000);
29252
29957
  }
29958
+ await reapPromise;
29253
29959
  const channelStartPromise = deps.channels.startAll({
29254
29960
  agent: runtime.agent,
29255
29961
  abortSignal: shutdownController.signal,
@@ -29431,15 +30137,15 @@ class AcpxBridgeClient {
29431
30137
  onEvent
29432
30138
  });
29433
30139
  try {
29434
- const didWrite = this.writeLine(encodeBridgeRequest({
30140
+ this.writeLine(encodeBridgeRequest({
29435
30141
  id,
29436
30142
  method,
29437
30143
  params
29438
- }));
29439
- if (didWrite === false) {
29440
- this.pending.delete(id);
29441
- reject(new Error("bridge write buffer is full"));
29442
- }
30144
+ }), (error2) => {
30145
+ if (error2 && this.pending.delete(id)) {
30146
+ reject(error2);
30147
+ }
30148
+ });
29443
30149
  } catch (error2) {
29444
30150
  this.pending.delete(id);
29445
30151
  reject(error2);
@@ -29519,6 +30225,17 @@ class AcpxBridgeClient {
29519
30225
  }
29520
30226
  }
29521
30227
  }
30228
+ function buildBridgeSpawnEnv(options = {}) {
30229
+ return {
30230
+ XACPX_LANG: getLocale(),
30231
+ XACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
30232
+ XACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
30233
+ XACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
30234
+ ...typeof options.permissionPolicy === "string" && options.permissionPolicy.trim().length > 0 ? { XACPX_BRIDGE_PERMISSION_POLICY: options.permissionPolicy } : {},
30235
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { XACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {},
30236
+ ...typeof options.sessionInitTimeoutMs === "number" && Number.isFinite(options.sessionInitTimeoutMs) && options.sessionInitTimeoutMs > 0 ? { XACPX_BRIDGE_SESSION_INIT_TIMEOUT_MS: String(options.sessionInitTimeoutMs) } : {}
30237
+ };
30238
+ }
29522
30239
  function buildBridgeSpawnSpec(options) {
29523
30240
  if (options.execPath.endsWith("bun")) {
29524
30241
  return {
@@ -29541,15 +30258,17 @@ async function spawnAcpxBridgeClient(options = {}) {
29541
30258
  cwd: options.cwd ?? process.cwd(),
29542
30259
  env: {
29543
30260
  ...process.env,
29544
- XACPX_LANG: getLocale(),
29545
- XACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
29546
- XACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
29547
- XACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
29548
- ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { XACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
30261
+ ...buildBridgeSpawnEnv(options)
29549
30262
  },
29550
30263
  stdio: ["pipe", "pipe", "inherit"]
29551
30264
  });
29552
- const client = new AcpxBridgeClient((line) => child.stdin.write(line));
30265
+ const client = manageBridgeChild(child);
30266
+ await client.waitUntilReady();
30267
+ return client;
30268
+ }
30269
+ function manageBridgeChild(child) {
30270
+ const client = new AcpxBridgeClient((line, onWriteError) => child.stdin.write(line, onWriteError));
30271
+ child.stdin.on("error", () => {});
29553
30272
  const output = createInterface({
29554
30273
  input: child.stdout,
29555
30274
  crlfDelay: Infinity
@@ -29575,7 +30294,6 @@ async function spawnAcpxBridgeClient(options = {}) {
29575
30294
  await terminateProcessTree(child.pid ?? 0, { detachedProcessGroup: false });
29576
30295
  }
29577
30296
  };
29578
- await client.waitUntilReady();
29579
30297
  return client;
29580
30298
  }
29581
30299
  function awaitable(executor) {
@@ -30417,19 +31135,19 @@ var init_streaming_prompt = __esm(() => {
30417
31135
 
30418
31136
  // src/transport/acpx-cli/node-pty-helper.ts
30419
31137
  import { chmod as chmodFs } from "node:fs/promises";
30420
- import { dirname as dirname13, join as join16 } from "node:path";
31138
+ import { dirname as dirname11, join as join16 } from "node:path";
30421
31139
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
30422
31140
  if (platform === "win32") {
30423
31141
  return null;
30424
31142
  }
30425
- return join16(dirname13(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
31143
+ return join16(dirname11(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
30426
31144
  }
30427
- async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
31145
+ async function ensureNodePtyHelperExecutable(helperPath, chmod5 = chmodFs) {
30428
31146
  if (!helperPath) {
30429
31147
  return;
30430
31148
  }
30431
31149
  try {
30432
- await chmod3(helperPath, 493);
31150
+ await chmod5(helperPath, 493);
30433
31151
  } catch (error2) {
30434
31152
  if (error2.code === "ENOENT") {
30435
31153
  return;
@@ -30442,7 +31160,7 @@ var init_node_pty_helper = () => {};
30442
31160
  // src/transport/acpx-queue-owner-launcher.ts
30443
31161
  import { createHash as createHash3 } from "node:crypto";
30444
31162
  import { spawn as spawn8 } from "node:child_process";
30445
- import { readFile as readFile12, unlink } from "node:fs/promises";
31163
+ import { readFile as readFile13, unlink } from "node:fs/promises";
30446
31164
  import { homedir as homedir8 } from "node:os";
30447
31165
  import { join as join17 } from "node:path";
30448
31166
  function buildXacpxMcpServerSpec(input) {
@@ -30605,7 +31323,7 @@ async function terminateAcpxQueueOwner(sessionId) {
30605
31323
  const lockPath = queueLockFilePath(sessionId);
30606
31324
  let owner;
30607
31325
  try {
30608
- owner = JSON.parse(await readFile12(lockPath, "utf8"));
31326
+ owner = JSON.parse(await readFile13(lockPath, "utf8"));
30609
31327
  } catch {
30610
31328
  return;
30611
31329
  }
@@ -31264,10 +31982,11 @@ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
31264
31982
  const timeoutMs = deps.timeoutMs ?? 5000;
31265
31983
  const seen = new Set;
31266
31984
  const unique = targets.filter((target) => {
31267
- if (seen.has(target.transportSession)) {
31985
+ const key = JSON.stringify([target.agent, target.agentCommand ?? null, target.cwd, target.transportSession]);
31986
+ if (seen.has(key)) {
31268
31987
  return false;
31269
31988
  }
31270
- seen.add(target.transportSession);
31989
+ seen.add(key);
31271
31990
  return true;
31272
31991
  });
31273
31992
  let terminated = 0;
@@ -31419,10 +32138,21 @@ class MessageChannelRegistry {
31419
32138
  throw new Error("all channels failed to start");
31420
32139
  }
31421
32140
  }
31422
- stopAll() {
32141
+ async stopAll() {
32142
+ let firstError;
31423
32143
  for (const channel of this.channels.values()) {
31424
- channel.logout();
32144
+ try {
32145
+ if (channel.stop) {
32146
+ await channel.stop();
32147
+ } else {
32148
+ channel.logout();
32149
+ }
32150
+ } catch (error2) {
32151
+ firstError ??= error2;
32152
+ }
31425
32153
  }
32154
+ if (firstError !== undefined)
32155
+ throw firstError;
31426
32156
  }
31427
32157
  getByChatKey(chatKey) {
31428
32158
  return this.channels.get(getChannelIdFromChatKey(chatKey)) ?? null;
@@ -31670,7 +32400,7 @@ __export(exports_main, {
31670
32400
  });
31671
32401
  import { randomUUID as randomUUID3 } from "node:crypto";
31672
32402
  import { homedir as homedir9 } from "node:os";
31673
- import { dirname as dirname14, join as join18 } from "node:path";
32403
+ import { dirname as dirname12, join as join18 } from "node:path";
31674
32404
  import { fileURLToPath as fileURLToPath5 } from "node:url";
31675
32405
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
31676
32406
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -31742,6 +32472,35 @@ async function buildApp(paths, deps = {}) {
31742
32472
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config4.transport.command });
31743
32473
  const stateStore = new StateStore(paths.statePath);
31744
32474
  const state = await stateStore.load();
32475
+ const stateLoadReport = stateStore.lastLoadReport;
32476
+ if (stateLoadReport) {
32477
+ for (const record3 of stateLoadReport.dropped) {
32478
+ await logger2.error("state.record_quarantined", "dropped malformed state.json record", {
32479
+ statePath: paths.statePath,
32480
+ section: record3.section,
32481
+ key: record3.key,
32482
+ reason: record3.reason
32483
+ });
32484
+ }
32485
+ if (stateLoadReport.corruptPath) {
32486
+ await logger2.error("state.file_corrupt", "state.json was unreadable; renamed aside and starting empty", {
32487
+ statePath: paths.statePath,
32488
+ corruptPath: stateLoadReport.corruptPath
32489
+ });
32490
+ }
32491
+ if (stateLoadReport.quarantinePath) {
32492
+ await logger2.error("state.file_quarantined", "original state.json backed up before dropping records", {
32493
+ statePath: paths.statePath,
32494
+ quarantinePath: stateLoadReport.quarantinePath
32495
+ });
32496
+ }
32497
+ if (stateLoadReport.backupError) {
32498
+ await logger2.error("state.quarantine_backup_failed", "failed to back up the original state.json", {
32499
+ statePath: paths.statePath,
32500
+ message: stateLoadReport.backupError
32501
+ });
32502
+ }
32503
+ }
31745
32504
  const stateMutex = new AsyncMutex;
31746
32505
  const debouncedStateStore = new DebouncedStateStore({
31747
32506
  delegate: stateStore,
@@ -31761,7 +32520,9 @@ async function buildApp(paths, deps = {}) {
31761
32520
  bridgeEntryPath: resolveBridgeEntryPath(),
31762
32521
  permissionMode: config4.transport.permissionMode,
31763
32522
  nonInteractivePermissions: config4.transport.nonInteractivePermissions,
31764
- ...typeof config4.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config4.transport.queueOwnerTtlSeconds } : {}
32523
+ ...typeof config4.transport.permissionPolicy === "string" ? { permissionPolicy: config4.transport.permissionPolicy } : {},
32524
+ ...typeof config4.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config4.transport.queueOwnerTtlSeconds } : {},
32525
+ ...typeof config4.transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: config4.transport.sessionInitTimeoutMs } : {}
31765
32526
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config4.transport, command: acpxCommand });
31766
32527
  const quota = new QuotaManager({
31767
32528
  onInbound: (chatKey) => {
@@ -32114,6 +32875,9 @@ async function buildApp(paths, deps = {}) {
32114
32875
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration3, config4, logger2, deps.channel ?? null);
32115
32876
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
32116
32877
  const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration3, {
32878
+ onSocketHardenError: (error2) => {
32879
+ logger2.error("orchestration.socket.chmod_failed", "failed to restrict orchestration socket to owner-only (0600); falling back to runtime dir permissions", { message: error2 instanceof Error ? error2.message : String(error2) });
32880
+ },
32117
32881
  createScheduledTaskFromRoute: async (input) => await createScheduledTaskFromRoute(input, {
32118
32882
  state,
32119
32883
  config: config4,
@@ -32251,7 +33015,7 @@ async function main() {
32251
33015
  }
32252
33016
  }
32253
33017
  async function prepareChannelMedia(configPath, config4) {
32254
- const runtimeDir = join18(dirname14(configPath), "runtime");
33018
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32255
33019
  const mediaRootDir = join18(runtimeDir, "media");
32256
33020
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
32257
33021
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -32266,7 +33030,7 @@ function resolveRuntimePaths() {
32266
33030
  throw new Error("Unable to resolve the current user home directory");
32267
33031
  }
32268
33032
  const configPath = coreEnv("CONFIG") ?? join18(coreHomeDir(home), "config.json");
32269
- const runtimeDir = join18(dirname14(configPath), "runtime");
33033
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32270
33034
  return {
32271
33035
  configPath,
32272
33036
  statePath: coreEnv("STATE") ?? join18(coreHomeDir(home), "state.json"),
@@ -32281,12 +33045,12 @@ function resolveBridgeEntryPath() {
32281
33045
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
32282
33046
  }
32283
33047
  function resolveAppLogPath(configPath) {
32284
- const rootDir = dirname14(configPath);
33048
+ const rootDir = dirname12(configPath);
32285
33049
  const runtimeDir = join18(rootDir, "runtime");
32286
33050
  return join18(runtimeDir, "app.log");
32287
33051
  }
32288
33052
  function resolvePerfLogPath(configPath) {
32289
- const rootDir = dirname14(configPath);
33053
+ const rootDir = dirname12(configPath);
32290
33054
  const runtimeDir = join18(rootDir, "runtime");
32291
33055
  return join18(runtimeDir, "perf.log");
32292
33056
  }
@@ -32523,8 +33287,10 @@ var init_config_check = __esm(async () => {
32523
33287
  });
32524
33288
 
32525
33289
  // src/doctor/checks/daemon-check.ts
33290
+ import { readdir as readdir3, readFile as readFile14, rm as rm10 } from "node:fs/promises";
32526
33291
  import { fileURLToPath as fileURLToPath6 } from "node:url";
32527
33292
  import { homedir as homedir10 } from "node:os";
33293
+ import { join as join19 } from "node:path";
32528
33294
  async function checkDaemon(options = {}) {
32529
33295
  const home = options.home ?? process.env.HOME ?? homedir10();
32530
33296
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
@@ -32532,15 +33298,25 @@ async function checkDaemon(options = {}) {
32532
33298
  home,
32533
33299
  ...runtimeDir ? { runtimeDir } : {}
32534
33300
  });
33301
+ const isProcessRunning = options.isProcessRunning ?? isProcessAlive;
33302
+ const listConsumerLocks = options.listConsumerLocks ?? defaultListConsumerLocks;
33303
+ const readConsumerLock = options.readConsumerLock ?? defaultReadConsumerLock;
33304
+ const removeConsumerLock = options.removeConsumerLock ?? defaultRemoveConsumerLock;
32535
33305
  const controller = createDaemonController(paths, {
32536
33306
  processExecPath: options.processExecPath ?? process.execPath,
32537
33307
  cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
32538
33308
  cwd: options.cwd ?? process.cwd(),
32539
33309
  env: options.env ?? process.env,
32540
- isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning5
33310
+ isProcessRunning
32541
33311
  });
32542
33312
  try {
32543
33313
  const status = await controller.getStatus();
33314
+ const staleLockFix = status.state === "stopped" ? await detectStaleConsumerLockFix(paths.runtimeDir, {
33315
+ isProcessRunning,
33316
+ listConsumerLocks,
33317
+ readConsumerLock,
33318
+ removeConsumerLock
33319
+ }) : undefined;
32544
33320
  switch (status.state) {
32545
33321
  case "running":
32546
33322
  return {
@@ -32562,6 +33338,7 @@ async function checkDaemon(options = {}) {
32562
33338
  summary: status.stale ? "daemon was stopped and stale runtime files were cleared" : "daemon is not running",
32563
33339
  details: status.stale ? ["stale runtime files were cleared"] : undefined,
32564
33340
  suggestions: ["run: xacpx start"],
33341
+ ...staleLockFix ? { fixes: [staleLockFix] } : {},
32565
33342
  metadata: {
32566
33343
  paths,
32567
33344
  status
@@ -32608,25 +33385,210 @@ async function checkDaemon(options = {}) {
32608
33385
  };
32609
33386
  }
32610
33387
  }
32611
- function defaultIsProcessRunning5(pid) {
33388
+ async function detectStaleConsumerLockFix(runtimeDir, deps) {
33389
+ const lockFiles = await deps.listConsumerLocks(runtimeDir);
33390
+ const stalePaths = [];
33391
+ for (const fileName of lockFiles) {
33392
+ if (!fileName.endsWith(CONSUMER_LOCK_SUFFIX)) {
33393
+ continue;
33394
+ }
33395
+ const lockPath = join19(runtimeDir, fileName);
33396
+ const lock2 = await deps.readConsumerLock(lockPath);
33397
+ if (lock2 && !deps.isProcessRunning(lock2.pid)) {
33398
+ stalePaths.push(lockPath);
33399
+ }
33400
+ }
33401
+ if (stalePaths.length === 0) {
33402
+ return;
33403
+ }
33404
+ return {
33405
+ id: "daemon.clear-stale-lock",
33406
+ title: "remove stale consumer lock(s)",
33407
+ run: async () => {
33408
+ const removed = [];
33409
+ let skipped = 0;
33410
+ for (const lockPath of stalePaths) {
33411
+ const lock2 = await deps.readConsumerLock(lockPath);
33412
+ if (!lock2 || deps.isProcessRunning(lock2.pid)) {
33413
+ skipped += 1;
33414
+ continue;
33415
+ }
33416
+ await deps.removeConsumerLock(lockPath);
33417
+ removed.push(lockPath);
33418
+ }
33419
+ const skippedNote = skipped > 0 ? `; left ${skipped} no-longer-stale lock(s) alone` : "";
33420
+ return {
33421
+ ok: true,
33422
+ message: removed.length > 0 ? `removed ${removed.length} stale consumer lock(s): ${removed.join(", ")}${skippedNote}` : `no locks removed${skippedNote}`
33423
+ };
33424
+ }
33425
+ };
33426
+ }
33427
+ async function defaultListConsumerLocks(runtimeDir) {
32612
33428
  try {
32613
- process.kill(pid, 0);
32614
- return true;
33429
+ return await readdir3(runtimeDir);
32615
33430
  } catch {
32616
- return false;
33431
+ return [];
33432
+ }
33433
+ }
33434
+ async function defaultReadConsumerLock(path15) {
33435
+ try {
33436
+ const raw = await readFile14(path15, "utf8");
33437
+ const parsed = JSON.parse(raw);
33438
+ return typeof parsed.pid === "number" ? { pid: parsed.pid } : null;
33439
+ } catch {
33440
+ return null;
32617
33441
  }
32618
33442
  }
33443
+ async function defaultRemoveConsumerLock(path15) {
33444
+ await rm10(path15, { force: true });
33445
+ }
32619
33446
  function resolveCliEntryPath() {
32620
33447
  return process.argv[1] ?? fileURLToPath6(import.meta.url);
32621
33448
  }
32622
33449
  function formatError5(error2) {
32623
33450
  return error2 instanceof Error ? error2.message : String(error2);
32624
33451
  }
33452
+ var CONSUMER_LOCK_SUFFIX = "-consumer.lock.json";
32625
33453
  var init_daemon_check = __esm(() => {
32626
33454
  init_create_daemon_controller();
32627
33455
  init_daemon_files();
32628
33456
  });
32629
33457
 
33458
+ // src/doctor/checks/logs-check.ts
33459
+ import { stat as stat3, readdir as readdir4 } from "node:fs/promises";
33460
+ import { basename as basename3, join as join20 } from "node:path";
33461
+ import { homedir as homedir11 } from "node:os";
33462
+ async function checkLogs(options = {}) {
33463
+ const home = options.home ?? process.env.HOME ?? homedir11();
33464
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
33465
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
33466
+ home,
33467
+ ...runtimeDir ? { runtimeDir } : {}
33468
+ });
33469
+ const probe = options.probe ?? createLogsFsProbe();
33470
+ const singleFileWarnBytes = options.singleFileWarnBytes ?? DEFAULT_SINGLE_FILE_WARN_BYTES;
33471
+ const totalWarnBytes = options.totalWarnBytes ?? DEFAULT_TOTAL_WARN_BYTES;
33472
+ let entries;
33473
+ try {
33474
+ const dirStat = await probe.stat(paths.runtimeDir);
33475
+ if (!dirStat.isDirectory()) {
33476
+ return skip("runtime log directory could not be read", [
33477
+ `runtimeDir: ${paths.runtimeDir} (exists but is not a directory)`
33478
+ ]);
33479
+ }
33480
+ entries = await probe.readdir(paths.runtimeDir);
33481
+ } catch (error2) {
33482
+ if (isMissingPathError(error2)) {
33483
+ return skip("no runtime logs yet", [`runtimeDir: ${paths.runtimeDir} (missing)`]);
33484
+ }
33485
+ return skip("runtime log directory could not be read", [
33486
+ `runtimeDir: ${paths.runtimeDir}`,
33487
+ `error: ${formatError6(error2)}`
33488
+ ]);
33489
+ }
33490
+ const baseNames = [basename3(paths.appLog), basename3(paths.stdoutLog), basename3(paths.stderrLog)];
33491
+ const tracked = new Set(baseNames);
33492
+ const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
33493
+ const files = [];
33494
+ for (const name of matched) {
33495
+ const path15 = join20(paths.runtimeDir, name);
33496
+ try {
33497
+ const fileStat = await probe.stat(path15);
33498
+ if (fileStat.isDirectory()) {
33499
+ continue;
33500
+ }
33501
+ files.push({ name, path: path15, size: fileStat.size });
33502
+ } catch {
33503
+ continue;
33504
+ }
33505
+ }
33506
+ const total = files.reduce((sum, file) => sum + file.size, 0);
33507
+ const largestSingle = files.reduce((max, file) => Math.max(max, file.size), 0);
33508
+ const overSingle = files.some((file) => file.size > singleFileWarnBytes);
33509
+ const overTotal = total > totalWarnBytes;
33510
+ const sorted = [...files].sort((a, b) => b.size - a.size);
33511
+ const details = [
33512
+ ...sorted.map((file) => `${file.name}: ${formatBytes(file.size)}`),
33513
+ `total: ${formatBytes(total)}`
33514
+ ];
33515
+ if (overSingle || overTotal) {
33516
+ const reason = overSingle ? `largest single log is ${formatBytes(largestSingle)}` : `total is ${formatBytes(total)}`;
33517
+ return {
33518
+ id: "logs",
33519
+ label: "Logs",
33520
+ severity: "warn",
33521
+ summary: `log growth high: ${reason} (total ${formatBytes(total)})`,
33522
+ details,
33523
+ suggestions: [
33524
+ "logs are large; check disk space and that log rotation is configured (logging.maxSizeBytes / maxFiles)"
33525
+ ]
33526
+ };
33527
+ }
33528
+ return {
33529
+ id: "logs",
33530
+ label: "Logs",
33531
+ severity: "pass",
33532
+ summary: `logs total ${formatBytes(total)}`,
33533
+ details
33534
+ };
33535
+ }
33536
+ function skip(summary, details) {
33537
+ return {
33538
+ id: "logs",
33539
+ label: "Logs",
33540
+ severity: "skip",
33541
+ summary,
33542
+ details
33543
+ };
33544
+ }
33545
+ function isTrackedLogName(name, baseNames) {
33546
+ if (baseNames.has(name)) {
33547
+ return true;
33548
+ }
33549
+ for (const base of baseNames) {
33550
+ const prefix = `${base}.`;
33551
+ if (name.startsWith(prefix)) {
33552
+ const suffix = name.slice(prefix.length);
33553
+ if (/^\d+$/.test(suffix) && Number(suffix) > 0) {
33554
+ return true;
33555
+ }
33556
+ }
33557
+ }
33558
+ return false;
33559
+ }
33560
+ function formatBytes(bytes) {
33561
+ if (bytes < 1024) {
33562
+ return `${bytes} B`;
33563
+ }
33564
+ const units = ["KB", "MB", "GB", "TB"];
33565
+ let value = bytes / 1024;
33566
+ let unitIndex = 0;
33567
+ while (value >= 1024 && unitIndex < units.length - 1) {
33568
+ value /= 1024;
33569
+ unitIndex += 1;
33570
+ }
33571
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
33572
+ }
33573
+ function createLogsFsProbe() {
33574
+ return {
33575
+ stat: async (path15) => await stat3(path15),
33576
+ readdir: async (path15) => await readdir4(path15)
33577
+ };
33578
+ }
33579
+ function formatError6(error2) {
33580
+ return error2 instanceof Error ? error2.message : String(error2);
33581
+ }
33582
+ function isMissingPathError(error2) {
33583
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && (error2.code === "ENOENT" || error2.code === "ENOTDIR");
33584
+ }
33585
+ var DEFAULT_SINGLE_FILE_WARN_BYTES, DEFAULT_TOTAL_WARN_BYTES;
33586
+ var init_logs_check = __esm(() => {
33587
+ init_daemon_files();
33588
+ DEFAULT_SINGLE_FILE_WARN_BYTES = 50 * 1024 * 1024;
33589
+ DEFAULT_TOTAL_WARN_BYTES = 200 * 1024 * 1024;
33590
+ });
33591
+
32630
33592
  // src/doctor/checks/orchestration-health.ts
32631
33593
  async function checkOrchestrationHealth(options) {
32632
33594
  const state = await options.loadState();
@@ -32679,13 +33641,187 @@ var init_orchestration_health = __esm(() => {
32679
33641
  init_i18n();
32680
33642
  });
32681
33643
 
33644
+ // src/doctor/checks/orchestration-socket-check.ts
33645
+ import { homedir as homedir12 } from "node:os";
33646
+ async function checkOrchestrationSocket(options = {}) {
33647
+ const home = options.home ?? process.env.HOME ?? homedir12();
33648
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
33649
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
33650
+ home,
33651
+ ...runtimeDir ? { runtimeDir } : {}
33652
+ });
33653
+ const getDaemonStatus = options.getDaemonStatus ?? ((p) => defaultGetDaemonStatus(p, options));
33654
+ const probe = options.canConnectToEndpoint ?? canConnectToEndpoint;
33655
+ const resolveEndpoint = options.resolveOrchestrationEndpoint ?? ((dir) => resolveOrchestrationEndpoint(dir));
33656
+ let status;
33657
+ try {
33658
+ status = await getDaemonStatus(paths);
33659
+ } catch (error2) {
33660
+ return {
33661
+ id: "orchestration-socket",
33662
+ label: "Orchestration IPC",
33663
+ severity: "skip",
33664
+ summary: "daemon status could not be read",
33665
+ details: [`runtime dir: ${paths.runtimeDir}`, `error: ${formatError7(error2)}`]
33666
+ };
33667
+ }
33668
+ if (status.state === "stopped") {
33669
+ return {
33670
+ id: "orchestration-socket",
33671
+ label: "Orchestration IPC",
33672
+ severity: "skip",
33673
+ summary: "daemon stopped"
33674
+ };
33675
+ }
33676
+ let endpoint;
33677
+ let reachable;
33678
+ try {
33679
+ endpoint = resolveEndpoint(paths.runtimeDir);
33680
+ reachable = await probe(endpoint.path);
33681
+ } catch (error2) {
33682
+ return {
33683
+ id: "orchestration-socket",
33684
+ label: "Orchestration IPC",
33685
+ severity: "skip",
33686
+ summary: "orchestration IPC liveness could not be probed",
33687
+ details: [`runtime dir: ${paths.runtimeDir}`, `error: ${formatError7(error2)}`]
33688
+ };
33689
+ }
33690
+ if (reachable) {
33691
+ return {
33692
+ id: "orchestration-socket",
33693
+ label: "Orchestration IPC",
33694
+ severity: "pass",
33695
+ summary: "orchestration IPC is accepting connections",
33696
+ details: [`endpoint: ${endpoint.path}`]
33697
+ };
33698
+ }
33699
+ return {
33700
+ id: "orchestration-socket",
33701
+ label: "Orchestration IPC",
33702
+ severity: "fail",
33703
+ summary: "daemon is running but orchestration IPC is not accepting connections",
33704
+ details: [`endpoint: ${endpoint.path}`],
33705
+ suggestions: ["run: xacpx restart"]
33706
+ };
33707
+ }
33708
+ async function defaultGetDaemonStatus(paths, options) {
33709
+ const controller = createDaemonController(paths, {
33710
+ processExecPath: options.processExecPath ?? process.execPath,
33711
+ cliEntryPath: options.cliEntryPath ?? process.argv[1] ?? "",
33712
+ cwd: options.cwd ?? process.cwd(),
33713
+ env: options.env ?? process.env,
33714
+ isProcessRunning: options.isProcessRunning ?? isProcessAlive
33715
+ });
33716
+ return await controller.getStatus();
33717
+ }
33718
+ function formatError7(error2) {
33719
+ return error2 instanceof Error ? error2.message : String(error2);
33720
+ }
33721
+ var init_orchestration_socket_check = __esm(() => {
33722
+ init_create_daemon_controller();
33723
+ init_daemon_files();
33724
+ init_endpoint_probe();
33725
+ init_orchestration_ipc();
33726
+ });
33727
+
33728
+ // src/doctor/checks/plugin-check.ts
33729
+ async function checkPlugins(options = {}) {
33730
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
33731
+ let config4;
33732
+ try {
33733
+ config4 = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
33734
+ } catch (error2) {
33735
+ return {
33736
+ id: "plugins",
33737
+ label: "Plugins",
33738
+ severity: "skip",
33739
+ summary: "plugin check skipped because configuration could not be loaded",
33740
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError8(error2)}`],
33741
+ suggestions: ["fix the Config check first, then run: xacpx doctor"]
33742
+ };
33743
+ }
33744
+ if (!hasPluginSurface(config4)) {
33745
+ return {
33746
+ id: "plugins",
33747
+ label: "Plugins",
33748
+ severity: "skip",
33749
+ summary: "no plugins configured"
33750
+ };
33751
+ }
33752
+ const pluginHome = (options.resolvePluginHome ?? resolvePluginHome)({ home: options.home });
33753
+ const inspect = options.inspectPlugins ?? inspectPlugins;
33754
+ let issues;
33755
+ try {
33756
+ issues = await inspect({
33757
+ config: config4,
33758
+ pluginHome,
33759
+ currentXacpxVersion: options.currentXacpxVersion ?? XACPX_CORE_VERSION
33760
+ });
33761
+ } catch (error2) {
33762
+ return {
33763
+ id: "plugins",
33764
+ label: "Plugins",
33765
+ severity: "fail",
33766
+ summary: "plugin health check failed",
33767
+ details: [`plugin home: ${pluginHome}`, `error: ${formatError8(error2)}`]
33768
+ };
33769
+ }
33770
+ const errorCount = issues.filter((issue2) => issue2.level === "error").length;
33771
+ const warnCount = issues.filter((issue2) => issue2.level === "warn").length;
33772
+ const severity = errorCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
33773
+ const problemCount = errorCount + warnCount;
33774
+ return {
33775
+ id: "plugins",
33776
+ label: "Plugins",
33777
+ severity,
33778
+ summary: problemCount > 0 ? `${problemCount} plugin issue(s)` : "all plugins healthy",
33779
+ details: issues.filter((issue2) => issue2.level !== "ok").map(formatIssueDetail),
33780
+ suggestions: collectSuggestions(issues),
33781
+ metadata: { pluginHome, errorCount, warnCount }
33782
+ };
33783
+ }
33784
+ function hasPluginSurface(config4) {
33785
+ if ((config4.plugins ?? []).length > 0) {
33786
+ return true;
33787
+ }
33788
+ const builtInChannelTypes = new Set(listKnownChannelIds());
33789
+ return (config4.channels ?? []).some((channel) => channel.enabled !== false && !builtInChannelTypes.has(channel.type));
33790
+ }
33791
+ function formatIssueDetail(issue2) {
33792
+ return issue2.plugin ? `${issue2.plugin}: ${issue2.message}` : issue2.message;
33793
+ }
33794
+ function collectSuggestions(issues) {
33795
+ const suggestions = [];
33796
+ const seen = new Set;
33797
+ for (const issue2 of issues) {
33798
+ const suggestion = issue2.suggestion;
33799
+ if (suggestion && !seen.has(suggestion)) {
33800
+ seen.add(suggestion);
33801
+ suggestions.push(`run: ${suggestion}`);
33802
+ }
33803
+ }
33804
+ return suggestions;
33805
+ }
33806
+ function formatError8(error2) {
33807
+ return error2 instanceof Error ? error2.message : String(error2);
33808
+ }
33809
+ var init_plugin_check = __esm(async () => {
33810
+ init_load_config();
33811
+ init_channel_scope();
33812
+ init_plugin_doctor();
33813
+ init_plugin_home();
33814
+ init_version();
33815
+ await init_main();
33816
+ });
33817
+
32682
33818
  // src/doctor/checks/runtime-check.ts
32683
33819
  import { constants } from "node:fs";
32684
- import { access as access4, stat as stat3 } from "node:fs/promises";
32685
- import { dirname as dirname15 } from "node:path";
32686
- import { homedir as homedir11 } from "node:os";
33820
+ import { access as access4, stat as stat4 } from "node:fs/promises";
33821
+ import { dirname as dirname13 } from "node:path";
33822
+ import { homedir as homedir13 } from "node:os";
32687
33823
  async function checkRuntime(options = {}) {
32688
- const home = options.home ?? process.env.HOME ?? homedir11();
33824
+ const home = options.home ?? process.env.HOME ?? homedir13();
32689
33825
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
32690
33826
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
32691
33827
  home,
@@ -32711,6 +33847,20 @@ async function checkRuntime(options = {}) {
32711
33847
  details: checks3.map((check) => check.detail)
32712
33848
  };
32713
33849
  }
33850
+ const privacy = await inspectRuntimeDirPrivacy(paths.runtimeDir, probe, platform);
33851
+ if (privacy.needsRepair) {
33852
+ return {
33853
+ id: "runtime",
33854
+ label: "Runtime",
33855
+ severity: "warn",
33856
+ summary: "daemon runtime dir should be private (mode 0700)",
33857
+ details: [...checks3.map((check) => check.detail), privacy.detail],
33858
+ fixes: [createEnsurePrivateDirFix(paths.runtimeDir, options.ensurePrivateRuntimeDir)],
33859
+ metadata: {
33860
+ paths
33861
+ }
33862
+ };
33863
+ }
32714
33864
  return {
32715
33865
  id: "runtime",
32716
33866
  label: "Runtime",
@@ -32722,9 +33872,50 @@ async function checkRuntime(options = {}) {
32722
33872
  }
32723
33873
  };
32724
33874
  }
33875
+ async function inspectRuntimeDirPrivacy(runtimeDir, probe, platform) {
33876
+ if (platform === "win32") {
33877
+ return { needsRepair: false, detail: "" };
33878
+ }
33879
+ try {
33880
+ const stats = await probe.stat(runtimeDir);
33881
+ if (typeof stats.mode !== "number") {
33882
+ return { needsRepair: false, detail: "" };
33883
+ }
33884
+ const mode = stats.mode & 511;
33885
+ if (mode === PRIVATE_DIR_MODE) {
33886
+ return { needsRepair: false, detail: "" };
33887
+ }
33888
+ return {
33889
+ needsRepair: true,
33890
+ detail: `runtimeDir: ${runtimeDir} (mode ${formatMode(mode)} is not 0700; group/other access should be removed)`
33891
+ };
33892
+ } catch (error2) {
33893
+ if (isMissingPathError2(error2)) {
33894
+ return {
33895
+ needsRepair: true,
33896
+ detail: `runtimeDir: ${runtimeDir} (missing; will be created with mode 0700)`
33897
+ };
33898
+ }
33899
+ return { needsRepair: false, detail: "" };
33900
+ }
33901
+ }
33902
+ function createEnsurePrivateDirFix(runtimeDir, ensureImpl) {
33903
+ const ensure = ensureImpl ?? ((dir) => ensurePrivateRuntimeDir(dir));
33904
+ return {
33905
+ id: "runtime.ensure-private-dir",
33906
+ title: "create/repair runtime dir with mode 0700",
33907
+ run: async () => {
33908
+ await ensure(runtimeDir);
33909
+ return { ok: true, message: `runtime dir ${runtimeDir} created/repaired with mode 0700` };
33910
+ }
33911
+ };
33912
+ }
33913
+ function formatMode(mode) {
33914
+ return `0${(mode & 511).toString(8)}`;
33915
+ }
32725
33916
  function createRuntimeFsProbe() {
32726
33917
  return {
32727
- stat: async (path15) => await stat3(path15),
33918
+ stat: async (path15) => await stat4(path15),
32728
33919
  access: async (path15, mode) => await access4(path15, mode)
32729
33920
  };
32730
33921
  }
@@ -32743,10 +33934,10 @@ async function checkDirectoryCreatable(label, path15, probe, platform) {
32743
33934
  detail: `${label}: ${path15} (writable)`
32744
33935
  };
32745
33936
  } catch (error2) {
32746
- if (!isMissingPathError(error2)) {
33937
+ if (!isMissingPathError2(error2)) {
32747
33938
  return {
32748
33939
  ok: false,
32749
- detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
33940
+ detail: `${label}: ${path15} (unusable: ${formatError9(error2)})`
32750
33941
  };
32751
33942
  }
32752
33943
  const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
@@ -32777,13 +33968,13 @@ async function checkFileCreatable(label, path15, probe, platform) {
32777
33968
  detail: `${label}: ${path15} (writable)`
32778
33969
  };
32779
33970
  } catch (error2) {
32780
- if (!isMissingPathError(error2)) {
33971
+ if (!isMissingPathError2(error2)) {
32781
33972
  return {
32782
33973
  ok: false,
32783
- detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
33974
+ detail: `${label}: ${path15} (unusable: ${formatError9(error2)})`
32784
33975
  };
32785
33976
  }
32786
- const parentCheck = await checkCreatableAncestorDirectory(dirname15(path15), probe, platform);
33977
+ const parentCheck = await checkCreatableAncestorDirectory(dirname13(path15), probe, platform);
32787
33978
  if (!parentCheck.ok) {
32788
33979
  return {
32789
33980
  ok: false,
@@ -32812,14 +34003,14 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
32812
34003
  creatableFrom: path15
32813
34004
  };
32814
34005
  } catch (error2) {
32815
- if (!isMissingPathError(error2)) {
34006
+ if (!isMissingPathError2(error2)) {
32816
34007
  return {
32817
34008
  ok: false,
32818
34009
  creatableFrom: path15,
32819
34010
  blockingPath: path15
32820
34011
  };
32821
34012
  }
32822
- const parent = dirname15(path15);
34013
+ const parent = dirname13(path15);
32823
34014
  if (parent === path15) {
32824
34015
  return {
32825
34016
  ok: false,
@@ -32840,21 +34031,22 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
32840
34031
  function directoryAccessMode(platform) {
32841
34032
  return platform === "win32" ? constants.W_OK : DIRECTORY_USABLE;
32842
34033
  }
32843
- function isMissingPathError(error2) {
34034
+ function isMissingPathError2(error2) {
32844
34035
  return isErrnoError(error2) && (error2.code === "ENOENT" || error2.code === "ENOTDIR");
32845
34036
  }
32846
34037
  function isErrnoError(error2) {
32847
34038
  return typeof error2 === "object" && error2 !== null && "code" in error2;
32848
34039
  }
32849
- function formatError6(error2) {
34040
+ function formatError9(error2) {
32850
34041
  if (error2 instanceof Error) {
32851
34042
  return error2.message;
32852
34043
  }
32853
34044
  return String(error2);
32854
34045
  }
32855
- var DIRECTORY_USABLE;
34046
+ var DIRECTORY_USABLE, PRIVATE_DIR_MODE = 448;
32856
34047
  var init_runtime_check = __esm(() => {
32857
34048
  init_daemon_files();
34049
+ init_private_runtime_dir();
32858
34050
  DIRECTORY_USABLE = constants.W_OK | constants.X_OK;
32859
34051
  });
32860
34052
 
@@ -32966,7 +34158,7 @@ async function checkSmoke(options = {}, deps = {}) {
32966
34158
  label: "Smoke",
32967
34159
  severity: "fail",
32968
34160
  summary: "smoke transport probe failed",
32969
- details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError7(error2)}`]
34161
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError10(error2)}`]
32970
34162
  };
32971
34163
  }
32972
34164
  }
@@ -33093,7 +34285,7 @@ function buildDetails3(options) {
33093
34285
  }
33094
34286
  return details;
33095
34287
  }
33096
- function formatError7(error2) {
34288
+ function formatError10(error2) {
33097
34289
  return error2 instanceof Error ? error2.message : String(error2);
33098
34290
  }
33099
34291
  var SMOKE_PROMPT = "Reply with exactly: ok";
@@ -33118,7 +34310,7 @@ async function checkWechat(options = {}) {
33118
34310
  } catch (error2) {
33119
34311
  return {
33120
34312
  accountId,
33121
- error: formatError8(error2)
34313
+ error: formatError11(error2)
33122
34314
  };
33123
34315
  }
33124
34316
  });
@@ -33159,7 +34351,7 @@ function buildVerboseDetails(loggedIn, verbose, accounts) {
33159
34351
  }
33160
34352
  return details;
33161
34353
  }
33162
- function formatError8(error2) {
34354
+ function formatError11(error2) {
33163
34355
  return error2 instanceof Error ? error2.message : String(error2);
33164
34356
  }
33165
34357
  var init_wechat_check = __esm(() => {
@@ -33168,43 +34360,60 @@ var init_wechat_check = __esm(() => {
33168
34360
 
33169
34361
  // src/doctor/render-doctor.ts
33170
34362
  function renderDoctor(report, options = {}) {
33171
- return options.verbose ? renderVerboseDoctor(report) : renderDefaultDoctor(report);
34363
+ const fixMode = options.fix === true;
34364
+ return options.verbose ? renderVerboseDoctor(report, fixMode) : renderDefaultDoctor(report, fixMode);
33172
34365
  }
33173
- function renderDefaultDoctor(report) {
34366
+ function renderDefaultDoctor(report, fixMode) {
33174
34367
  const lines = [];
33175
34368
  for (const check of report.checks) {
33176
- lines.push(renderCheckLine(check));
34369
+ lines.push(renderCheckLine(check, fixMode));
33177
34370
  }
34371
+ appendRepairs(lines, report, fixMode);
33178
34372
  lines.push(renderSummaryLine(report.checks));
33179
- const suggestions = collectSuggestions(report.checks);
33180
- if (suggestions.length > 0) {
33181
- lines.push("Next steps:");
33182
- for (const suggestion of suggestions) {
33183
- lines.push(`- ${suggestion}`);
33184
- }
33185
- }
34373
+ appendNextSteps(lines, report.checks);
33186
34374
  return lines;
33187
34375
  }
33188
- function renderVerboseDoctor(report) {
34376
+ function renderVerboseDoctor(report, fixMode) {
33189
34377
  const lines = [];
33190
34378
  for (const check of report.checks) {
33191
- lines.push(renderCheckLine(check));
34379
+ lines.push(renderCheckLine(check, fixMode));
33192
34380
  for (const detail of check.details ?? []) {
33193
34381
  lines.push(` detail: ${detail}`);
33194
34382
  }
33195
34383
  }
34384
+ appendRepairs(lines, report, fixMode);
33196
34385
  lines.push(renderSummaryLine(report.checks));
33197
- const suggestions = collectSuggestions(report.checks);
34386
+ appendNextSteps(lines, report.checks);
34387
+ return lines;
34388
+ }
34389
+ function appendNextSteps(lines, checks3) {
34390
+ const suggestions = collectSuggestions2(checks3);
33198
34391
  if (suggestions.length > 0) {
33199
34392
  lines.push("Next steps:");
33200
34393
  for (const suggestion of suggestions) {
33201
34394
  lines.push(`- ${suggestion}`);
33202
34395
  }
33203
34396
  }
33204
- return lines;
33205
34397
  }
33206
- function renderCheckLine(check) {
33207
- return `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
34398
+ function appendRepairs(lines, report, fixMode) {
34399
+ if (!fixMode) {
34400
+ return;
34401
+ }
34402
+ const repairs = report.repairs ?? [];
34403
+ if (repairs.length === 0) {
34404
+ return;
34405
+ }
34406
+ lines.push("Repairs:");
34407
+ for (const repair of repairs) {
34408
+ lines.push(`- ${repair.title}: ${repair.status} (${repair.message})`);
34409
+ }
34410
+ }
34411
+ function renderCheckLine(check, fixMode) {
34412
+ const base = `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
34413
+ if (!fixMode && (check.fixes?.length ?? 0) > 0) {
34414
+ return `${base} (fixable — run: xacpx doctor --fix)`;
34415
+ }
34416
+ return base;
33208
34417
  }
33209
34418
  function renderSummaryLine(checks3) {
33210
34419
  const counts = summarizeChecks(checks3);
@@ -33216,7 +34425,7 @@ function summarizeChecks(checks3) {
33216
34425
  return counts;
33217
34426
  }, { pass: 0, warn: 0, fail: 0, skip: 0 });
33218
34427
  }
33219
- function collectSuggestions(checks3) {
34428
+ function collectSuggestions2(checks3) {
33220
34429
  const seen = new Set;
33221
34430
  const suggestions = [];
33222
34431
  for (const check of checks3) {
@@ -33241,47 +34450,112 @@ var init_render_doctor = __esm(() => {
33241
34450
  });
33242
34451
 
33243
34452
  // src/doctor/doctor.ts
33244
- import { homedir as homedir12 } from "node:os";
33245
- import { join as join19 } from "node:path";
34453
+ import { homedir as homedir14 } from "node:os";
34454
+ import { join as join21 } from "node:path";
33246
34455
  async function runDoctor(options = {}, deps = {}) {
33247
- const home = deps.home ?? process.env.HOME ?? homedir12();
34456
+ const home = deps.home ?? process.env.HOME ?? homedir14();
33248
34457
  const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
33249
34458
  const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
34459
+ const runners = [
34460
+ {
34461
+ id: "config",
34462
+ run: () => (deps.checkConfig ?? checkConfig)({
34463
+ loadConfig: sharedLoadConfig,
34464
+ resolveRuntimePaths: () => runtimePaths
34465
+ })
34466
+ },
34467
+ {
34468
+ id: "runtime",
34469
+ run: () => (deps.checkRuntime ?? checkRuntime)({
34470
+ home,
34471
+ configPath: runtimePaths.configPath
34472
+ })
34473
+ },
34474
+ {
34475
+ id: "logs",
34476
+ run: () => (deps.checkLogs ?? checkLogs)({
34477
+ home,
34478
+ configPath: runtimePaths.configPath
34479
+ })
34480
+ },
34481
+ {
34482
+ id: "daemon",
34483
+ run: () => (deps.checkDaemon ?? checkDaemon)({
34484
+ home,
34485
+ configPath: runtimePaths.configPath
34486
+ })
34487
+ },
34488
+ {
34489
+ id: "wechat",
34490
+ run: () => (deps.checkWechat ?? checkWechat)({
34491
+ verbose: options.verbose
34492
+ })
34493
+ },
34494
+ {
34495
+ id: "acpx",
34496
+ run: () => (deps.checkAcpx ?? checkAcpx)({
34497
+ verbose: options.verbose,
34498
+ loadConfig: sharedLoadConfig,
34499
+ resolveRuntimePaths: () => runtimePaths
34500
+ })
34501
+ },
34502
+ {
34503
+ id: "bridge",
34504
+ run: () => (deps.checkBridge ?? checkBridge)({
34505
+ verbose: options.verbose,
34506
+ loadConfig: sharedLoadConfig,
34507
+ resolveRuntimePaths: () => runtimePaths
34508
+ })
34509
+ },
34510
+ {
34511
+ id: "plugins",
34512
+ run: () => (deps.checkPlugins ?? checkPlugins)({
34513
+ home,
34514
+ loadConfig: sharedLoadConfig,
34515
+ resolveRuntimePaths: () => runtimePaths
34516
+ })
34517
+ },
34518
+ {
34519
+ id: "orchestration",
34520
+ run: () => (deps.checkOrchestrationHealth ?? (() => defaultCheckOrchestrationHealth({
34521
+ runtimePaths,
34522
+ loadConfig: sharedLoadConfig,
34523
+ isDaemonRunning: deps.isDaemonRunning ?? (() => defaultIsDaemonRunning(home, runtimePaths, deps.getDaemonStatus))
34524
+ })))()
34525
+ },
34526
+ {
34527
+ id: "orchestration-socket",
34528
+ run: () => (deps.checkOrchestrationSocket ?? checkOrchestrationSocket)({
34529
+ home,
34530
+ configPath: runtimePaths.configPath
34531
+ })
34532
+ },
34533
+ {
34534
+ id: "smoke",
34535
+ run: () => options.smoke === true ? (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
34536
+ resolveRuntimePaths: () => runtimePaths,
34537
+ loadConfig: sharedLoadConfig
34538
+ })))(options) : Promise.resolve(createSmokeSkipResult("smoke probe not requested"))
34539
+ }
34540
+ ];
34541
+ const runnersById = new Map(runners.map((runner) => [runner.id, runner.run]));
33250
34542
  const checks3 = [];
33251
- checks3.push(await (deps.checkConfig ?? checkConfig)({
33252
- loadConfig: sharedLoadConfig,
33253
- resolveRuntimePaths: () => runtimePaths
33254
- }));
33255
- checks3.push(await (deps.checkRuntime ?? checkRuntime)({
33256
- home,
33257
- configPath: runtimePaths.configPath
33258
- }));
33259
- checks3.push(await (deps.checkDaemon ?? checkDaemon)({
33260
- home,
33261
- configPath: runtimePaths.configPath
33262
- }));
33263
- checks3.push(await (deps.checkWechat ?? checkWechat)({
33264
- verbose: options.verbose
33265
- }));
33266
- checks3.push(await (deps.checkAcpx ?? checkAcpx)({
33267
- verbose: options.verbose,
33268
- loadConfig: sharedLoadConfig,
33269
- resolveRuntimePaths: () => runtimePaths
33270
- }));
33271
- checks3.push(await (deps.checkBridge ?? checkBridge)({
33272
- verbose: options.verbose,
33273
- loadConfig: sharedLoadConfig,
33274
- resolveRuntimePaths: () => runtimePaths
33275
- }));
33276
- checks3.push(await (deps.checkOrchestrationHealth ?? (() => defaultCheckOrchestrationHealth({
33277
- runtimePaths,
33278
- loadConfig: sharedLoadConfig
33279
- })))());
33280
- checks3.push(options.smoke === true ? await (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
33281
- resolveRuntimePaths: () => runtimePaths,
33282
- loadConfig: sharedLoadConfig
33283
- })))(options) : createSmokeSkipResult("smoke probe not requested"));
34543
+ for (const runner of runners) {
34544
+ checks3.push(await runner.run());
34545
+ }
33284
34546
  const report = { checks: checks3 };
34547
+ if (options.fix === true) {
34548
+ const { repairs, repairedCheckIds } = await applyRepairs(checks3);
34549
+ report.repairs = repairs;
34550
+ for (const checkId of repairedCheckIds) {
34551
+ const index = checks3.findIndex((check) => check.id === checkId);
34552
+ const rerun = runnersById.get(checkId);
34553
+ if (index === -1 || !rerun) {
34554
+ continue;
34555
+ }
34556
+ checks3[index] = await rerun();
34557
+ }
34558
+ }
33285
34559
  const output = (deps.renderDoctor ?? renderDoctor)(report, options);
33286
34560
  return {
33287
34561
  report,
@@ -33289,6 +34563,41 @@ async function runDoctor(options = {}, deps = {}) {
33289
34563
  exitCode: checks3.some((check) => check.severity === "fail") ? 1 : 0
33290
34564
  };
33291
34565
  }
34566
+ async function applyRepairs(checks3) {
34567
+ const repairs = [];
34568
+ const repairedCheckIds = [];
34569
+ for (const check of checks3) {
34570
+ for (const fix of check.fixes ?? []) {
34571
+ if (fix.withheld !== undefined) {
34572
+ repairs.push({
34573
+ checkId: check.id,
34574
+ fixId: fix.id,
34575
+ title: fix.title,
34576
+ status: "skipped",
34577
+ message: fix.withheld
34578
+ });
34579
+ continue;
34580
+ }
34581
+ let outcome;
34582
+ try {
34583
+ outcome = await fix.run();
34584
+ } catch (error2) {
34585
+ outcome = { ok: false, message: formatError12(error2) };
34586
+ }
34587
+ repairs.push({
34588
+ checkId: check.id,
34589
+ fixId: fix.id,
34590
+ title: fix.title,
34591
+ status: outcome.ok ? "applied" : "failed",
34592
+ message: outcome.message
34593
+ });
34594
+ if (outcome.ok && !repairedCheckIds.includes(check.id)) {
34595
+ repairedCheckIds.push(check.id);
34596
+ }
34597
+ }
34598
+ }
34599
+ return { repairs, repairedCheckIds };
34600
+ }
33292
34601
  function resolveDoctorRuntimePaths(home, resolver) {
33293
34602
  if (resolver) {
33294
34603
  return resolver();
@@ -33297,8 +34606,8 @@ function resolveDoctorRuntimePaths(home, resolver) {
33297
34606
  return resolveRuntimePaths();
33298
34607
  }
33299
34608
  return {
33300
- configPath: join19(coreHomeDir(home), "config.json"),
33301
- statePath: join19(coreHomeDir(home), "state.json")
34609
+ configPath: join21(coreHomeDir(home), "config.json"),
34610
+ statePath: join21(coreHomeDir(home), "state.json")
33302
34611
  };
33303
34612
  }
33304
34613
  function depsUseExplicitRuntimeOverrides() {
@@ -33338,36 +34647,116 @@ async function defaultCheckOrchestrationHealth(deps) {
33338
34647
  label: "Orchestration",
33339
34648
  severity: "skip",
33340
34649
  summary: "orchestration check skipped because configuration could not be loaded",
33341
- details: [`config path: ${deps.runtimePaths.configPath}`, `error: ${formatError9(error2)}`],
34650
+ details: [`config path: ${deps.runtimePaths.configPath}`, `error: ${formatError12(error2)}`],
33342
34651
  suggestions: ["fix the Config check first, then run: xacpx doctor"]
33343
34652
  };
33344
34653
  }
33345
34654
  try {
33346
34655
  const store = new StateStore(deps.runtimePaths.statePath);
33347
- return await checkOrchestrationHealth({
33348
- loadState: () => store.load(),
34656
+ const inspection = await store.inspect();
34657
+ const result = await checkOrchestrationHealth({
34658
+ loadState: async () => inspection.state,
33349
34659
  now: () => new Date,
33350
34660
  heartbeatThresholdSeconds: config4.orchestration.progressHeartbeatSeconds
33351
34661
  });
34662
+ const daemonRunning = inspection.report ? await deps.isDaemonRunning() : false;
34663
+ return applyStateInspectionReport(result, inspection.report, deps.runtimePaths.statePath, daemonRunning, deps.isDaemonRunning);
33352
34664
  } catch (error2) {
33353
34665
  return {
33354
34666
  id: "orchestration",
33355
34667
  label: "Orchestration",
33356
34668
  severity: "fail",
33357
34669
  summary: "orchestration health check failed",
33358
- details: [`state path: ${deps.runtimePaths.statePath}`, `error: ${formatError9(error2)}`]
34670
+ details: [`state path: ${deps.runtimePaths.statePath}`, `error: ${formatError12(error2)}`]
33359
34671
  };
33360
34672
  }
33361
34673
  }
33362
- function formatError9(error2) {
34674
+ function applyStateInspectionReport(result, report, statePath, daemonRunning, isDaemonRunning) {
34675
+ if (!report) {
34676
+ return result;
34677
+ }
34678
+ const fileCorrupt = report.dropped.some((record3) => record3.section === "file");
34679
+ const details = [
34680
+ ...result.details ?? [],
34681
+ `state path: ${statePath}`,
34682
+ ...report.dropped.map((record3) => record3.section === "file" ? `state.json is unreadable: ${record3.reason}` : `invalid state record ${record3.section}["${record3.key}"]: ${record3.reason}`)
34683
+ ];
34684
+ return {
34685
+ ...result,
34686
+ severity: result.severity === "fail" ? "fail" : "warn",
34687
+ summary: fileCorrupt ? `state.json is unreadable and will be reset (renamed to state.json.corrupt-*) at next daemon startup; ${result.summary}` : `state.json has ${report.dropped.length} invalid record(s) that will be quarantined at next daemon startup; ${result.summary}`,
34688
+ details,
34689
+ suggestions: [
34690
+ ...result.suggestions ?? [],
34691
+ fileCorrupt ? "back up the state file before the next daemon start if you want to attempt manual recovery" : "the daemon backs the original file up as state.json.quarantine-* before dropping these records"
34692
+ ],
34693
+ fixes: [createStateQuarantineFix(statePath, daemonRunning, isDaemonRunning)]
34694
+ };
34695
+ }
34696
+ function createStateQuarantineFix(statePath, daemonRunning, isDaemonRunning) {
34697
+ return {
34698
+ id: "state.quarantine",
34699
+ title: "quarantine invalid state.json records",
34700
+ ...daemonRunning ? { withheld: "stop the daemon first: xacpx stop" } : {},
34701
+ run: async () => {
34702
+ if (await isDaemonRunning()) {
34703
+ return {
34704
+ ok: false,
34705
+ message: "a daemon started since detection; stop it first: xacpx stop"
34706
+ };
34707
+ }
34708
+ const store = new StateStore(statePath);
34709
+ await store.load();
34710
+ const report = store.lastLoadReport;
34711
+ if (!report) {
34712
+ return { ok: true, message: "state.json was already valid; nothing to quarantine" };
34713
+ }
34714
+ if (report.corruptPath) {
34715
+ return { ok: true, message: `state.json was unreadable; renamed to ${report.corruptPath} and reset` };
34716
+ }
34717
+ const backup = report.quarantinePath ? ` (original backed up to ${report.quarantinePath})` : "";
34718
+ return {
34719
+ ok: true,
34720
+ message: `quarantined ${report.dropped.length} invalid state.json record(s)${backup}`
34721
+ };
34722
+ }
34723
+ };
34724
+ }
34725
+ async function defaultIsDaemonRunning(home, runtimePaths, getDaemonStatus = () => defaultGetDaemonStatus2(home, runtimePaths)) {
34726
+ try {
34727
+ const status = await getDaemonStatus();
34728
+ return status.state === "running" || status.state === "indeterminate";
34729
+ } catch {
34730
+ return true;
34731
+ }
34732
+ }
34733
+ async function defaultGetDaemonStatus2(home, runtimePaths) {
34734
+ const paths = resolveDaemonPaths({
34735
+ home,
34736
+ runtimeDir: resolveRuntimeDirFromConfigPath(runtimePaths.configPath)
34737
+ });
34738
+ const controller = createDaemonController(paths, {
34739
+ processExecPath: process.execPath,
34740
+ cliEntryPath: process.argv[1] ?? "",
34741
+ cwd: process.cwd(),
34742
+ env: process.env,
34743
+ isProcessRunning: isProcessAlive
34744
+ });
34745
+ return await controller.getStatus();
34746
+ }
34747
+ function formatError12(error2) {
33363
34748
  return error2 instanceof Error ? error2.message : String(error2);
33364
34749
  }
33365
34750
  var init_doctor = __esm(async () => {
33366
34751
  init_core_home();
33367
34752
  init_load_config();
34753
+ init_create_daemon_controller();
34754
+ init_daemon_files();
33368
34755
  init_state_store();
33369
34756
  init_daemon_check();
34757
+ init_logs_check();
33370
34758
  init_orchestration_health();
34759
+ init_orchestration_socket_check();
33371
34760
  init_runtime_check();
33372
34761
  init_wechat_check();
33373
34762
  init_render_doctor();
@@ -33376,6 +34765,7 @@ var init_doctor = __esm(async () => {
33376
34765
  init_acpx_check(),
33377
34766
  init_bridge_check(),
33378
34767
  init_config_check(),
34768
+ init_plugin_check(),
33379
34769
  init_smoke_check()
33380
34770
  ]);
33381
34771
  });
@@ -33400,8 +34790,8 @@ var init_doctor2 = __esm(async () => {
33400
34790
  // src/cli.ts
33401
34791
  init_core_home();
33402
34792
  import { randomUUID as randomUUID4 } from "node:crypto";
33403
- import { homedir as homedir13 } from "node:os";
33404
- import { dirname as dirname16, join as join20, sep } from "node:path";
34793
+ import { homedir as homedir15 } from "node:os";
34794
+ import { dirname as dirname14, join as join22, sep } from "node:path";
33405
34795
  import { fileURLToPath as fileURLToPath7 } from "node:url";
33406
34796
 
33407
34797
  // src/runtime/migrate-core-home.ts
@@ -33485,8 +34875,8 @@ init_daemon_files();
33485
34875
 
33486
34876
  // src/daemon/daemon-runtime.ts
33487
34877
  init_daemon_status();
33488
- import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
33489
- import { dirname as dirname5 } from "node:path";
34878
+ init_private_runtime_dir();
34879
+ import { rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
33490
34880
 
33491
34881
  class DaemonRuntime {
33492
34882
  paths;
@@ -33512,8 +34902,8 @@ class DaemonRuntime {
33512
34902
  stdout_log: this.paths.stdoutLog,
33513
34903
  stderr_log: this.paths.stderrLog
33514
34904
  };
33515
- await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
33516
- await writeFile3(this.paths.pidFile, `${this.options.pid}
34905
+ await ensurePrivateRuntimeDir(this.paths.runtimeDir);
34906
+ await writeFile2(this.paths.pidFile, `${this.options.pid}
33517
34907
  `);
33518
34908
  await this.statusStore.save(this.currentStatus);
33519
34909
  }
@@ -46188,7 +47578,7 @@ function buildXacpxMcpToolRegistry(input) {
46188
47578
  });
46189
47579
  tools.push({
46190
47580
  name: "scheduled_list",
46191
- description: "List pending one-shot scheduled tasks (global). Use to recover task ids before cancelling, or to see what is scheduled. Owner-only in group chats. Routing and account are resolved from the current session; pass no other arguments.",
47581
+ description: "List pending one-shot scheduled tasks created in the current chat. Use to recover task ids before cancelling, or to see what is scheduled. Owner-only in group chats. Routing and account are resolved from the current session; pass no other arguments.",
46192
47582
  inputSchema: exports_external.object({}).strict(),
46193
47583
  handler: async () => await asToolResult(async () => {
46194
47584
  const tasks = await transport.scheduledList({ coordinatorSession });
@@ -46206,7 +47596,7 @@ function buildXacpxMcpToolRegistry(input) {
46206
47596
  });
46207
47597
  tools.push({
46208
47598
  name: "scheduled_cancel",
46209
- description: "Cancel a pending scheduled task by id. Owner-only in group chats. Returns whether a pending task with that id was found and cancelled. Routing is resolved from the current session.",
47599
+ description: "Cancel a pending scheduled task by id (only tasks created in the current chat). Owner-only in group chats. Returns whether a pending task with that id was found and cancelled. Routing is resolved from the current session.",
46210
47600
  inputSchema: exports_external.object({
46211
47601
  id: exports_external.string().min(1).describe("The scheduled task id, e.g. 'k8f2' (a leading # is allowed).")
46212
47602
  }).strict(),
@@ -47416,7 +48806,10 @@ async function maybeRunFirstUseOnboarding(input) {
47416
48806
  const agentExisted = Boolean(input.config.agents[agentName]);
47417
48807
  input.config.workspaces[workspaceName] = { cwd };
47418
48808
  input.config.agents[agentName] = template;
47419
- await input.saveConfig(input.config);
48809
+ await input.saveFirstRunConfig({
48810
+ workspace: { name: workspaceName, cwd },
48811
+ agent: { name: agentName, config: template }
48812
+ });
47420
48813
  const alias = `${workspaceName}:${agentName}`;
47421
48814
  input.deps.print(t().misc.onboardingCreatedWorkspace(workspaceName, alias));
47422
48815
  return {
@@ -47439,8 +48832,8 @@ function resolveTemplateChoice(answer, names) {
47439
48832
  // src/cli-update.ts
47440
48833
  init_plugin_home();
47441
48834
  import { spawn as spawn4 } from "node:child_process";
47442
- import { readFile as readFile8 } from "node:fs/promises";
47443
- import { dirname as dirname10, join as join11 } from "node:path";
48835
+ import { readFile as readFile9 } from "node:fs/promises";
48836
+ import { dirname as dirname8, join as join11 } from "node:path";
47444
48837
  import { fileURLToPath as fileURLToPath3 } from "node:url";
47445
48838
 
47446
48839
  // src/plugins/package-manager.ts
@@ -47448,9 +48841,14 @@ init_plugin_home();
47448
48841
  import { spawn as spawn3 } from "node:child_process";
47449
48842
  import { rm as rm4 } from "node:fs/promises";
47450
48843
  import { join as join7 } from "node:path";
48844
+ function shellSpawnPlan(args) {
48845
+ const shell = process.platform === "win32";
48846
+ return { shell, args: shell ? args.map((arg) => `"${arg}"`) : args };
48847
+ }
47451
48848
  async function defaultRunCommand(command, args, options) {
47452
48849
  await new Promise((resolve, reject) => {
47453
- const child = spawn3(command, args, { cwd: options.cwd, stdio: "inherit" });
48850
+ const plan = shellSpawnPlan(args);
48851
+ const child = spawn3(command, plan.args, { cwd: options.cwd, stdio: "inherit", shell: plan.shell });
47454
48852
  child.on("error", reject);
47455
48853
  child.on("exit", (code) => {
47456
48854
  if (code === 0)
@@ -47462,7 +48860,8 @@ async function defaultRunCommand(command, args, options) {
47462
48860
  }
47463
48861
  async function silentRun(command, args, options) {
47464
48862
  await new Promise((resolve, reject) => {
47465
- const child = spawn3(command, args, { cwd: options.cwd, stdio: "ignore" });
48863
+ const plan = shellSpawnPlan(args);
48864
+ const child = spawn3(command, plan.args, { cwd: options.cwd, stdio: "ignore", shell: plan.shell });
47466
48865
  child.on("error", reject);
47467
48866
  child.on("exit", (code) => {
47468
48867
  if (code === 0)
@@ -47638,7 +49037,7 @@ async function handleUpdateCli(args, deps) {
47638
49037
  }
47639
49038
  if (selected.targets.some((target) => target.kind === "plugin")) {
47640
49039
  config4.plugins = updatedPlugins;
47641
- await deps.saveConfig(config4);
49040
+ await deps.savePlugins(updatedPlugins);
47642
49041
  }
47643
49042
  return 0;
47644
49043
  }
@@ -47748,9 +49147,10 @@ function compareSemver2(a, b) {
47748
49147
  return 0;
47749
49148
  return left.prerelease ? -1 : 1;
47750
49149
  }
49150
+ var spawnUsesShell = () => process.platform === "win32";
47751
49151
  async function runCapture(command, args) {
47752
49152
  return await new Promise((resolve, reject) => {
47753
- const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
49153
+ const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: spawnUsesShell() });
47754
49154
  let stdout2 = "";
47755
49155
  let stderr = "";
47756
49156
  child.stdout.setEncoding("utf8");
@@ -47767,7 +49167,7 @@ async function runCapture(command, args) {
47767
49167
  }
47768
49168
  async function runInherit(command, args) {
47769
49169
  await new Promise((resolve, reject) => {
47770
- const child = spawn4(command, args, { stdio: "inherit" });
49170
+ const child = spawn4(command, args, { stdio: "inherit", shell: spawnUsesShell() });
47771
49171
  child.on("error", reject);
47772
49172
  child.on("exit", (code) => {
47773
49173
  if (code === 0)
@@ -47779,10 +49179,10 @@ async function runInherit(command, args) {
47779
49179
  }
47780
49180
  async function readPackageName() {
47781
49181
  try {
47782
- const here = dirname10(fileURLToPath3(import.meta.url));
49182
+ const here = dirname8(fileURLToPath3(import.meta.url));
47783
49183
  for (const candidate of [join11(here, "..", "package.json"), join11(here, "..", "..", "package.json")]) {
47784
49184
  try {
47785
- const parsed = JSON.parse(await readFile8(candidate, "utf8"));
49185
+ const parsed = JSON.parse(await readFile9(candidate, "utf8"));
47786
49186
  if (typeof parsed.name === "string" && parsed.name.trim())
47787
49187
  return parsed.name.trim();
47788
49188
  } catch {}
@@ -48024,7 +49424,7 @@ async function addChannel(type, rawArgs, deps) {
48024
49424
  return 1;
48025
49425
  }
48026
49426
  config4.channels = [...config4.channels ?? [], candidate];
48027
- await deps.saveConfig(config4);
49427
+ await deps.saveChannels(config4.channels);
48028
49428
  deps.print(t().channelCli.channelAdded(type));
48029
49429
  for (const line of provider.renderSummary(candidate))
48030
49430
  deps.print(line);
@@ -48125,7 +49525,7 @@ async function removeChannel(type, rawArgs, deps) {
48125
49525
  return 1;
48126
49526
  }
48127
49527
  config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
48128
- await deps.saveConfig(config4);
49528
+ await deps.saveChannels(config4.channels);
48129
49529
  deps.print(t().channelCli.channelRemoved(channel.id));
48130
49530
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48131
49531
  }
@@ -48147,7 +49547,7 @@ async function setChannelEnabled(type, enabled, rawArgs, deps) {
48147
49547
  return 1;
48148
49548
  }
48149
49549
  channel.enabled = enabled;
48150
- await deps.saveConfig(config4);
49550
+ await deps.saveChannels(config4.channels);
48151
49551
  deps.print(t().channelCli.channelEnabledToggled(channel.id, enabled));
48152
49552
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48153
49553
  }
@@ -48169,7 +49569,7 @@ async function setChannelReplyMode(type, mode, rawArgs, deps) {
48169
49569
  return 1;
48170
49570
  }
48171
49571
  channel.replyMode = mode;
48172
- await deps.saveConfig(config4);
49572
+ await deps.saveChannels(config4.channels);
48173
49573
  deps.print(t().channelCli.channelReplyModeSet(channel.id, mode));
48174
49574
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48175
49575
  }
@@ -48315,7 +49715,7 @@ async function addChannelAccount(type, accountId, rawArgs, deps) {
48315
49715
  deps.print(validation.map((issue2) => issue2.message).join(";"));
48316
49716
  return 1;
48317
49717
  }
48318
- await deps.saveConfig(config4);
49718
+ await deps.saveChannels(config4.channels);
48319
49719
  deps.print(t().channelCli.channelAccountAdded(type, accountId));
48320
49720
  if (reEnabledChannel)
48321
49721
  deps.print(t().channelCli.channelReEnabled(type));
@@ -48355,7 +49755,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48355
49755
  return 1;
48356
49756
  }
48357
49757
  config4.channels = config4.channels.filter((channel) => channel.id !== existing.id);
48358
- await deps.saveConfig(config4);
49758
+ await deps.saveChannels(config4.channels);
48359
49759
  deps.print(t().channelCli.channelAccountRemovedWithChannel(type, accountId));
48360
49760
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48361
49761
  }
@@ -48372,7 +49772,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48372
49772
  deps.print(t().channelCli.channelAccountDefaultSwitched(options.defaultAccount));
48373
49773
  }
48374
49774
  existing.options = options;
48375
- await deps.saveConfig(config4);
49775
+ await deps.saveChannels(config4.channels);
48376
49776
  deps.print(t().channelCli.channelAccountRemoved(type, accountId));
48377
49777
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48378
49778
  }
@@ -48426,7 +49826,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48426
49826
  return 1;
48427
49827
  }
48428
49828
  existing.options = options;
48429
- await deps.saveConfig(config4);
49829
+ await deps.saveChannels(config4.channels);
48430
49830
  deps.print(t().channelCli.channelAccountEnabledToggled(type, accountId, enabled));
48431
49831
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48432
49832
  }
@@ -48434,128 +49834,39 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48434
49834
  // src/plugins/plugin-cli.ts
48435
49835
  init_core_home();
48436
49836
  init_plugin_home();
48437
- import { readFile as readFile10 } from "node:fs/promises";
49837
+ import { readFile as readFile11 } from "node:fs/promises";
48438
49838
  import { isAbsolute, join as join13, resolve } from "node:path";
48439
49839
  init_plugin_loader();
48440
49840
  init_validate_plugin();
48441
-
48442
- // src/plugins/plugin-doctor.ts
48443
- init_channel_scope();
48444
- init_plugin_loader();
48445
- init_validate_plugin();
49841
+ init_plugin_doctor();
48446
49842
  init_known_plugins();
48447
- import { readFile as readFile9 } from "node:fs/promises";
48448
- import { join as join12 } from "node:path";
48449
- function suggestedPluginPackageForChannel(type) {
48450
- return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
48451
- }
48452
- async function readDependencyEntries(pluginHome) {
48453
- try {
48454
- const raw = await readFile9(join12(pluginHome, "package.json"), "utf8");
48455
- const parsed = JSON.parse(raw);
48456
- const out = {};
48457
- for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
48458
- if (typeof value === "string")
48459
- out[name] = value;
48460
- }
48461
- return out;
48462
- } catch (error2) {
48463
- const message = error2 instanceof Error ? error2.message : String(error2);
48464
- throw new Error(`failed to read plugin home package.json: ${message}`);
48465
- }
49843
+ init_plugin_renames();
49844
+ init_i18n();
49845
+ function findPluginSpecViolation(spec, platform) {
49846
+ if (spec.includes('"'))
49847
+ return "double-quote";
49848
+ if (platform === "win32" && spec.includes("%"))
49849
+ return "percent-on-windows";
49850
+ return null;
48466
49851
  }
48467
- async function inspectPlugins(input) {
48468
- const issues = [];
48469
- let dependencies;
48470
- try {
48471
- dependencies = await readDependencyEntries(input.pluginHome);
48472
- } catch (error2) {
48473
- const message = error2 instanceof Error ? error2.message : String(error2);
48474
- return [{ level: "error", message }];
48475
- }
48476
- const importPlugin = input.importPlugin ?? importPluginFromHome;
48477
- const allConfigured = input.config.plugins;
48478
- const filterByName = input.pluginName ?? null;
48479
- if (filterByName && !allConfigured.some((plugin) => plugin.name === filterByName)) {
48480
- return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
48481
- }
48482
- const pushIfRelevant = (issue2) => {
48483
- if (!filterByName || issue2.plugin === filterByName)
48484
- issues.push(issue2);
48485
- };
48486
- const channelProviders = new Map;
48487
- for (const configPlugin of allConfigured) {
48488
- if (!(configPlugin.name in dependencies)) {
48489
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `package not installed in plugin home; run xacpx plugin add ${configPlugin.name}` });
49852
+ function invalidSpecMessage(specs, platform) {
49853
+ for (const spec of specs) {
49854
+ if (!spec)
48490
49855
  continue;
48491
- }
48492
- let moduleValue;
48493
- try {
48494
- moduleValue = await importPlugin(configPlugin.name, input.pluginHome);
48495
- } catch (error2) {
48496
- const message = error2 instanceof Error ? error2.message : String(error2);
48497
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `failed to import plugin: ${message}` });
48498
- continue;
48499
- }
48500
- try {
48501
- const plugin = validateWeacpxPlugin(moduleValue, configPlugin.name, {
48502
- ...input.currentXacpxVersion !== undefined ? { currentXacpxVersion: input.currentXacpxVersion } : {}
48503
- });
48504
- const channels = plugin.channels ?? [];
48505
- const channelTypes = channels.map((channel) => channel.type);
48506
- for (const type of channelTypes) {
48507
- const existing = channelProviders.get(type);
48508
- if (existing) {
48509
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `channel type ${type} is already provided by ${existing.plugin}` });
48510
- } else {
48511
- channelProviders.set(type, { plugin: configPlugin.name, enabled: configPlugin.enabled });
48512
- }
48513
- }
48514
- pushIfRelevant({
48515
- level: configPlugin.enabled ? "ok" : "warn",
48516
- plugin: configPlugin.name,
48517
- message: configPlugin.enabled ? `plugin is installed and valid; channels: ${channelTypes.length > 0 ? channelTypes.join(", ") : "none"}` : `plugin is installed and valid but disabled; run xacpx plugin enable ${configPlugin.name}`
48518
- });
48519
- } catch (error2) {
48520
- const message = error2 instanceof Error ? error2.message : String(error2);
48521
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message });
48522
- }
49856
+ const violation = findPluginSpecViolation(spec, platform);
49857
+ if (violation === "double-quote")
49858
+ return t().pluginCli.pluginSpecHasDoubleQuote(spec);
49859
+ if (violation === "percent-on-windows")
49860
+ return t().pluginCli.pluginSpecHasPercentOnWindows(spec);
48523
49861
  }
48524
- const builtInChannelTypes = new Set(listKnownChannelIds());
48525
- for (const channel of input.config.channels) {
48526
- if (builtInChannelTypes.has(channel.type))
48527
- continue;
48528
- const provider = channelProviders.get(channel.type);
48529
- if (!provider) {
48530
- if (!filterByName) {
48531
- issues.push({
48532
- level: "error",
48533
- message: `channel ${channel.type} is configured but no enabled plugin provides it; run xacpx plugin add ${suggestedPluginPackageForChannel(channel.type)} or another plugin that provides type "${channel.type}"`
48534
- });
48535
- }
48536
- continue;
48537
- }
48538
- if (!provider.enabled) {
48539
- pushIfRelevant({
48540
- level: "error",
48541
- plugin: provider.plugin,
48542
- message: `channel ${channel.type} is configured but provider plugin is disabled; run xacpx plugin enable ${provider.plugin}`
48543
- });
48544
- }
48545
- }
48546
- return issues;
49862
+ return null;
48547
49863
  }
48548
-
48549
- // src/plugins/plugin-cli.ts
48550
- init_known_plugins();
48551
- init_plugin_renames();
48552
- init_i18n();
48553
49864
  function looksLikePath(spec) {
48554
49865
  return spec === "." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith(".\\") || spec.startsWith("..\\") || spec.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(spec) || isAbsolute(spec);
48555
49866
  }
48556
49867
  async function readDependencyEntries2(pluginHome) {
48557
49868
  try {
48558
- const raw = await readFile10(join13(pluginHome, "package.json"), "utf8");
49869
+ const raw = await readFile11(join13(pluginHome, "package.json"), "utf8");
48559
49870
  const parsed = JSON.parse(raw);
48560
49871
  const out = {};
48561
49872
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -48581,7 +49892,7 @@ async function resolveLocalPluginName(installSpec, pluginHome, namesBeforeInstal
48581
49892
  return name;
48582
49893
  }
48583
49894
  try {
48584
- const raw = await readFile10(join13(installSpec, "package.json"), "utf8");
49895
+ const raw = await readFile11(join13(installSpec, "package.json"), "utf8");
48585
49896
  const parsed = JSON.parse(raw);
48586
49897
  if (typeof parsed.name === "string" && parsed.name.trim())
48587
49898
  return parsed.name.trim();
@@ -48724,6 +50035,11 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48724
50035
  deps.print(t().pluginCli.unrecognizedArgs(flags.rest.join(" ")));
48725
50036
  return 1;
48726
50037
  }
50038
+ const invalidSpec = invalidSpecMessage([packageSpec, flags.version], deps.platform ?? process.platform);
50039
+ if (invalidSpec) {
50040
+ deps.print(invalidSpec);
50041
+ return 1;
50042
+ }
48727
50043
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48728
50044
  await ensurePluginHome(pluginHome);
48729
50045
  const installSpec = looksLikePath(packageSpec) && !isAbsolute(packageSpec) ? resolve(process.cwd(), packageSpec) : packageSpec;
@@ -48761,7 +50077,7 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48761
50077
  } else {
48762
50078
  config4.plugins = [...config4.plugins, next];
48763
50079
  }
48764
- await deps.saveConfig(config4);
50080
+ await deps.savePlugins(config4.plugins);
48765
50081
  deps.print(t().pluginCli.pluginInstalled(recordedName));
48766
50082
  if (summary.channels.length > 0) {
48767
50083
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
@@ -48787,7 +50103,7 @@ async function removePlugin(packageName, rawArgs, deps) {
48787
50103
  }
48788
50104
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48789
50105
  const validate = deps.validateInstalledPlugin ?? ((name) => validateInstalledPluginDefault(name, pluginHome));
48790
- const guard = await dependencyGuard(packageName, config4, validate);
50106
+ const guard = await dependencyGuard(existing.name, config4, validate);
48791
50107
  if (!guard.allow) {
48792
50108
  if (guard.reason)
48793
50109
  deps.print(guard.reason);
@@ -48797,14 +50113,14 @@ async function removePlugin(packageName, rawArgs, deps) {
48797
50113
  await removePluginPackage({ packageName: name, pluginHome });
48798
50114
  });
48799
50115
  try {
48800
- await remove(packageName);
50116
+ await remove(existing.name);
48801
50117
  } catch (error2) {
48802
- deps.print(t().pluginCli.pluginUninstallFailed(packageName, describeError(error2)));
50118
+ deps.print(t().pluginCli.pluginUninstallFailed(existing.name, describeError(error2)));
48803
50119
  return 1;
48804
50120
  }
48805
- config4.plugins = config4.plugins.filter((entry) => entry.name !== packageName);
48806
- await deps.saveConfig(config4);
48807
- deps.print(t().pluginCli.pluginRemoved(packageName));
50121
+ config4.plugins = config4.plugins.filter((entry) => entry.name !== existing.name);
50122
+ await deps.savePlugins(config4.plugins);
50123
+ deps.print(t().pluginCli.pluginRemoved(existing.name));
48808
50124
  return await maybeRestartAfterMutation2(flags.restart, deps);
48809
50125
  }
48810
50126
  async function updatePlugins(args, deps) {
@@ -48824,6 +50140,11 @@ async function updatePlugins(args, deps) {
48824
50140
  deps.print("--all cannot be combined with --version");
48825
50141
  return 1;
48826
50142
  }
50143
+ const invalidSpec = invalidSpecMessage([flags.version], deps.platform ?? process.platform);
50144
+ if (invalidSpec) {
50145
+ deps.print(invalidSpec);
50146
+ return 1;
50147
+ }
48827
50148
  const config4 = await deps.loadConfig();
48828
50149
  ensurePluginsArray(config4);
48829
50150
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
@@ -48880,7 +50201,7 @@ async function updatePlugins(args, deps) {
48880
50201
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
48881
50202
  }
48882
50203
  }
48883
- await deps.saveConfig(config4);
50204
+ await deps.savePlugins(config4.plugins);
48884
50205
  return await maybeRestartAfterMutation2(flags.restart, deps);
48885
50206
  }
48886
50207
  async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
@@ -48911,7 +50232,7 @@ async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
48911
50232
  }
48912
50233
  }
48913
50234
  existing.enabled = enabled;
48914
- await deps.saveConfig(config4);
50235
+ await deps.savePlugins(config4.plugins);
48915
50236
  deps.print(t().pluginCli.pluginEnabledToggled(packageName, enabled));
48916
50237
  return await maybeRestartAfterMutation2(flags.restart, deps);
48917
50238
  }
@@ -49429,7 +50750,9 @@ async function defaultUpdate(args, input) {
49429
50750
  const store = await createCliConfigStore();
49430
50751
  const deps = {
49431
50752
  loadConfig: async () => await store.load(),
49432
- saveConfig: async (config4) => await store.save(config4),
50753
+ savePlugins: async (plugins) => {
50754
+ await store.replacePlugins(plugins);
50755
+ },
49433
50756
  readCurrentVersion: readVersion,
49434
50757
  print: input.print,
49435
50758
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -49438,6 +50761,27 @@ async function defaultUpdate(args, input) {
49438
50761
  };
49439
50762
  return await handleUpdateCli(args, deps);
49440
50763
  }
50764
+ function warnStateLoadReport(store, writeStderr = (text) => process.stderr.write(text)) {
50765
+ const report = store.lastLoadReport;
50766
+ if (!report)
50767
+ return;
50768
+ for (const record3 of report.dropped) {
50769
+ writeStderr(`[xacpx] state.record_quarantined section=${record3.section}${record3.key ? ` key=${record3.key}` : ""} reason=${record3.reason}
50770
+ `);
50771
+ }
50772
+ if (report.corruptPath) {
50773
+ writeStderr(`[xacpx] state.file_corrupt unreadable state.json renamed to ${report.corruptPath}
50774
+ `);
50775
+ }
50776
+ if (report.quarantinePath) {
50777
+ writeStderr(`[xacpx] state.file_quarantined original state.json backed up to ${report.quarantinePath}
50778
+ `);
50779
+ }
50780
+ if (report.backupError) {
50781
+ writeStderr(`[xacpx] state.quarantine_backup_failed ${report.backupError}
50782
+ `);
50783
+ }
50784
+ }
49441
50785
  async function runOnboardingBeforeStart(input) {
49442
50786
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49443
50787
  await ensureConfigExists(runtimePaths.configPath);
@@ -49445,10 +50789,14 @@ async function runOnboardingBeforeStart(input) {
49445
50789
  const stateStore = new StateStore(runtimePaths.statePath);
49446
50790
  const config4 = await configStore.load();
49447
50791
  const state = await stateStore.load();
50792
+ warnStateLoadReport(stateStore);
49448
50793
  const result = await maybeRunFirstUseOnboarding({
49449
50794
  config: config4,
49450
50795
  state,
49451
- saveConfig: async (next) => await configStore.save(next),
50796
+ saveFirstRunConfig: async ({ workspace: workspace3, agent: agent3 }) => {
50797
+ await configStore.upsertWorkspace(workspace3.name, workspace3.cwd);
50798
+ await configStore.upsertAgent(agent3.name, agent3.config);
50799
+ },
49452
50800
  deps: {
49453
50801
  print: input.print,
49454
50802
  cwd: input.cwd,
@@ -49659,7 +51007,7 @@ async function handleLaterCli(args, deps) {
49659
51007
  }
49660
51008
  async function laterList(print) {
49661
51009
  const scheduled = await createCliScheduledTaskService();
49662
- print(renderLaterList(scheduled.listPending(), (alias) => toDisplaySessionAlias(alias)));
51010
+ print(renderLaterList(scheduled.listPendingAllChats(), (alias) => toDisplaySessionAlias(alias)));
49663
51011
  return 0;
49664
51012
  }
49665
51013
  async function laterCancel(rawId, print) {
@@ -49669,7 +51017,7 @@ async function laterCancel(rawId, print) {
49669
51017
  return 1;
49670
51018
  }
49671
51019
  const scheduled = await createCliScheduledTaskService();
49672
- const ok = await scheduled.cancelPending(id);
51020
+ const ok = await scheduled.cancelPendingAnyChat(id);
49673
51021
  if (!ok) {
49674
51022
  print(t().cli.laterNotFound(id));
49675
51023
  print(t().cli.laterNotFoundHint);
@@ -49682,10 +51030,11 @@ async function createCliScheduledTaskService() {
49682
51030
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49683
51031
  const stateStore = new StateStore(runtimePaths.statePath);
49684
51032
  const state = await stateStore.load();
51033
+ warnStateLoadReport(stateStore);
49685
51034
  return new ScheduledTaskService(state, stateStore);
49686
51035
  }
49687
51036
  function resolveConfigPathForCurrentEnv() {
49688
- return coreEnv("CONFIG") ?? join20(coreHomeDir(requireHome2()), "config.json");
51037
+ return coreEnv("CONFIG") ?? join22(coreHomeDir(requireHome2()), "config.json");
49689
51038
  }
49690
51039
  function resolveDaemonPathsForCurrentConfig() {
49691
51040
  const configPath = resolveConfigPathForCurrentEnv();
@@ -49784,14 +51133,12 @@ async function createFirstRunSession(runtime, plan) {
49784
51133
  }
49785
51134
  async function rollbackFirstRunConfig(runtime, plan) {
49786
51135
  try {
49787
- const config4 = await runtime.configStore.load();
49788
- if (!plan.rollback.workspaceExisted && config4.workspaces[plan.workspace]) {
49789
- delete config4.workspaces[plan.workspace];
51136
+ if (!plan.rollback.workspaceExisted) {
51137
+ await runtime.configStore.removeWorkspace(plan.workspace);
49790
51138
  }
49791
- if (!plan.rollback.agentExisted && config4.agents[plan.agent]) {
49792
- delete config4.agents[plan.agent];
51139
+ if (!plan.rollback.agentExisted) {
51140
+ await runtime.configStore.removeAgent(plan.agent);
49793
51141
  }
49794
- await runtime.configStore.save(config4);
49795
51142
  } catch (error2) {
49796
51143
  await runtime.logger.error("onboarding.rollback_failed", "failed to roll back first-run config", {
49797
51144
  alias: plan.alias,
@@ -49823,7 +51170,9 @@ async function defaultMcpStdio(args, deps = {}) {
49823
51170
  await ensureConfigExists(runtimePaths.configPath);
49824
51171
  const config4 = await loadConfig(runtimePaths.configPath);
49825
51172
  availableAgents = Object.keys(config4.agents);
49826
- const state = await new StateStore(runtimePaths.statePath).load();
51173
+ const stateStore = new StateStore(runtimePaths.statePath);
51174
+ const state = await stateStore.load();
51175
+ warnStateLoadReport(stateStore, deps.stderr ?? ((text) => process.stderr.write(text)));
49827
51176
  const resolveIdentity = createMcpStdioIdentityResolver({
49828
51177
  parsedCoordinatorSession,
49829
51178
  sourceHandle,
@@ -49887,7 +51236,9 @@ async function createChannelCliDeps(input) {
49887
51236
  const controller = input.controller ?? createDefaultController();
49888
51237
  const base = {
49889
51238
  loadConfig: async () => await store.load(),
49890
- saveConfig: async (config4) => await store.save(config4),
51239
+ saveChannels: async (channels) => {
51240
+ await store.replaceChannels(channels);
51241
+ },
49891
51242
  print: input.print,
49892
51243
  stderr: input.stderr ?? ((text) => process.stderr.write(text)),
49893
51244
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -49910,7 +51261,9 @@ async function createPluginCliDeps(input) {
49910
51261
  const controller = input.controller ?? createDefaultController();
49911
51262
  const base = {
49912
51263
  loadConfig: async () => await store.load(),
49913
- saveConfig: async (config4) => await store.save(config4),
51264
+ savePlugins: async (plugins) => {
51265
+ await store.replacePlugins(plugins);
51266
+ },
49914
51267
  print: input.print,
49915
51268
  isInteractive: input.isInteractive ?? defaultIsInteractive,
49916
51269
  promptText: input.promptText ?? defaultPromptText,
@@ -50028,7 +51381,7 @@ function decodeFirstRunOnboarding(raw) {
50028
51381
  return null;
50029
51382
  }
50030
51383
  function requireHome2() {
50031
- const home = process.env.HOME ?? homedir13();
51384
+ const home = process.env.HOME ?? homedir15();
50032
51385
  if (!home) {
50033
51386
  throw new Error("Unable to resolve the current user home directory");
50034
51387
  }
@@ -50052,7 +51405,7 @@ function safeDaemonLogPaths() {
50052
51405
  const configPath = resolveConfigPathForCurrentEnv();
50053
51406
  const paths = resolveDaemonPathsForCurrentConfig();
50054
51407
  return {
50055
- appLog: join20(dirname16(configPath), "runtime", "app.log"),
51408
+ appLog: join22(dirname14(configPath), "runtime", "app.log"),
50056
51409
  stderrLog: paths.stderrLog
50057
51410
  };
50058
51411
  } catch {
@@ -50076,6 +51429,9 @@ function parseDoctorArgs(args) {
50076
51429
  case "--smoke":
50077
51430
  options.smoke = true;
50078
51431
  break;
51432
+ case "--fix":
51433
+ options.fix = true;
51434
+ break;
50079
51435
  case "--agent": {
50080
51436
  const value = args[index + 1];
50081
51437
  if (!value || value.startsWith("--")) {