@ganglion/xacpx 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });
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
+ });
4813
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(".");
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";
4814
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;
@@ -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,7 +5691,7 @@ 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;
@@ -12124,10 +12371,24 @@ function createEmptyState() {
12124
12371
  var init_types = () => {};
12125
12372
 
12126
12373
  // src/state/state-store.ts
12127
- import { readFile as readFile5 } from "node:fs/promises";
12374
+ import { readFile as readFile6, rename, writeFile as writeFile3 } from "node:fs/promises";
12128
12375
  function isRecord2(value) {
12129
12376
  return typeof value === "object" && value !== null && !Array.isArray(value);
12130
12377
  }
12378
+ function sectionRecord(value, section, dropped) {
12379
+ if (value === undefined) {
12380
+ return {};
12381
+ }
12382
+ if (!isRecord2(value)) {
12383
+ dropped.push({
12384
+ section,
12385
+ key: "",
12386
+ reason: `field "${section}" is not an object; reset to empty`
12387
+ });
12388
+ return {};
12389
+ }
12390
+ return value;
12391
+ }
12131
12392
  function isString(value) {
12132
12393
  return typeof value === "string";
12133
12394
  }
@@ -12232,73 +12493,82 @@ function isHumanQuestionPackageRecord(value) {
12232
12493
  const messages = value.messages;
12233
12494
  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
12495
  }
12235
- function parseOrchestrationState(raw, path3) {
12496
+ function parseOrchestrationState(raw, dropped) {
12236
12497
  if (raw === undefined) {
12237
12498
  return createEmptyOrchestrationState();
12238
12499
  }
12239
12500
  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"`);
12501
+ dropped.push({
12502
+ section: "orchestration",
12503
+ key: "",
12504
+ reason: 'field "orchestration" is not an object; reset to empty'
12505
+ });
12506
+ return createEmptyOrchestrationState();
12269
12507
  }
12508
+ const tasks = sectionRecord(raw.tasks, "orchestration.tasks", dropped);
12509
+ const workerBindings = sectionRecord(raw.workerBindings, "orchestration.workerBindings", dropped);
12510
+ const groups = sectionRecord(raw.groups, "orchestration.groups", dropped);
12511
+ const humanQuestionPackages = sectionRecord(raw.humanQuestionPackages, "orchestration.humanQuestionPackages", dropped);
12512
+ const coordinatorQuestionState = sectionRecord(raw.coordinatorQuestionState, "orchestration.coordinatorQuestionState", dropped);
12513
+ const coordinatorRoutes = sectionRecord(raw.coordinatorRoutes, "orchestration.coordinatorRoutes", dropped);
12514
+ const externalCoordinators = sectionRecord(raw.externalCoordinators, "orchestration.externalCoordinators", dropped);
12270
12515
  const parsedTasks = {};
12271
- for (const [taskId, task] of Object.entries(tasks ?? {})) {
12516
+ for (const [taskId, task] of Object.entries(tasks)) {
12272
12517
  if (!isTaskRecord(task)) {
12273
- throw new Error(`state file "${path3}" contains an invalid orchestration task at "${taskId}"`);
12518
+ dropped.push({
12519
+ section: "orchestration.tasks",
12520
+ key: taskId,
12521
+ reason: "malformed orchestration task record"
12522
+ });
12523
+ continue;
12274
12524
  }
12275
12525
  parsedTasks[taskId] = task;
12276
12526
  }
12277
12527
  const parsedWorkerBindings = {};
12278
- for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
12528
+ for (const [workerSession, binding] of Object.entries(workerBindings)) {
12279
12529
  if (!isWorkerBindingRecord(binding)) {
12280
- throw new Error(`state file "${path3}" contains an invalid orchestration worker binding at "${workerSession}"`);
12530
+ dropped.push({
12531
+ section: "orchestration.workerBindings",
12532
+ key: workerSession,
12533
+ reason: "malformed orchestration worker binding record"
12534
+ });
12535
+ continue;
12281
12536
  }
12282
12537
  parsedWorkerBindings[workerSession] = binding;
12283
12538
  }
12284
12539
  const parsedGroups = {};
12285
- for (const [groupId, group] of Object.entries(groups ?? {})) {
12540
+ for (const [groupId, group] of Object.entries(groups)) {
12286
12541
  if (!isGroupRecord(group)) {
12287
- throw new Error(`state file "${path3}" contains an invalid orchestration group at "${groupId}"`);
12542
+ dropped.push({
12543
+ section: "orchestration.groups",
12544
+ key: groupId,
12545
+ reason: "malformed orchestration group record"
12546
+ });
12547
+ continue;
12288
12548
  }
12289
12549
  parsedGroups[groupId] = group;
12290
12550
  }
12291
12551
  const parsedHumanQuestionPackages = {};
12292
- for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages ?? {})) {
12552
+ for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages)) {
12293
12553
  if (!isHumanQuestionPackageRecord(packageRecord)) {
12294
- throw new Error(`state file "${path3}" contains an invalid human question package at "${packageId}"`);
12554
+ dropped.push({
12555
+ section: "orchestration.humanQuestionPackages",
12556
+ key: packageId,
12557
+ reason: "malformed human question package record"
12558
+ });
12559
+ continue;
12295
12560
  }
12296
12561
  parsedHumanQuestionPackages[packageId] = packageRecord;
12297
12562
  }
12298
12563
  const parsedCoordinatorQuestionState = {};
12299
- for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState ?? {})) {
12564
+ for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState)) {
12300
12565
  if (!isCoordinatorQuestionStateRecord(questionState)) {
12301
- throw new Error(`state file "${path3}" contains an invalid coordinator question state at "${coordinatorSession}"`);
12566
+ dropped.push({
12567
+ section: "orchestration.coordinatorQuestionState",
12568
+ key: coordinatorSession,
12569
+ reason: "malformed coordinator question state record"
12570
+ });
12571
+ continue;
12302
12572
  }
12303
12573
  parsedCoordinatorQuestionState[coordinatorSession] = {
12304
12574
  activePackageId: questionState.activePackageId,
@@ -12306,19 +12576,34 @@ function parseOrchestrationState(raw, path3) {
12306
12576
  };
12307
12577
  }
12308
12578
  const parsedCoordinatorRoutes = {};
12309
- for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes ?? {})) {
12579
+ for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes)) {
12310
12580
  if (!isCoordinatorRouteContextRecord(route)) {
12311
- throw new Error(`state file "${path3}" contains an invalid coordinator route at "${coordinatorSession}"`);
12581
+ dropped.push({
12582
+ section: "orchestration.coordinatorRoutes",
12583
+ key: coordinatorSession,
12584
+ reason: "malformed coordinator route record"
12585
+ });
12586
+ continue;
12312
12587
  }
12313
12588
  parsedCoordinatorRoutes[coordinatorSession] = route;
12314
12589
  }
12315
12590
  const parsedExternalCoordinators = {};
12316
- for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators ?? {})) {
12591
+ for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators)) {
12317
12592
  if (!isExternalCoordinatorRecord(externalCoordinator)) {
12318
- throw new Error(`state file "${path3}" contains an invalid external coordinator at "${coordinatorSession}"`);
12593
+ dropped.push({
12594
+ section: "orchestration.externalCoordinators",
12595
+ key: coordinatorSession,
12596
+ reason: "malformed external coordinator record"
12597
+ });
12598
+ continue;
12319
12599
  }
12320
12600
  if (externalCoordinator.coordinatorSession !== coordinatorSession) {
12321
- throw new Error(`state file "${path3}" contains an external coordinator key mismatch at "${coordinatorSession}"`);
12601
+ dropped.push({
12602
+ section: "orchestration.externalCoordinators",
12603
+ key: coordinatorSession,
12604
+ reason: `coordinatorSession "${externalCoordinator.coordinatorSession}" does not match map key`
12605
+ });
12606
+ continue;
12322
12607
  }
12323
12608
  parsedExternalCoordinators[coordinatorSession] = externalCoordinator;
12324
12609
  }
@@ -12344,11 +12629,12 @@ function isSessionRecord(value) {
12344
12629
  }
12345
12630
  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
12631
  }
12347
- function parseSessions(raw, path3) {
12632
+ function parseSessions(raw, dropped) {
12348
12633
  const sessions = {};
12349
12634
  for (const [alias, value] of Object.entries(raw)) {
12350
12635
  if (!isSessionRecord(value)) {
12351
- throw new Error(`state file "${path3}" contains malformed session record "${alias}"`);
12636
+ dropped.push({ section: "sessions", key: alias, reason: "malformed session record" });
12637
+ continue;
12352
12638
  }
12353
12639
  sessions[alias] = value;
12354
12640
  }
@@ -12357,11 +12643,12 @@ function parseSessions(raw, path3) {
12357
12643
  function isChatContextRecord(value) {
12358
12644
  return isRecord2(value) && isString(value.current_session);
12359
12645
  }
12360
- function parseChatContexts(raw, path3) {
12646
+ function parseChatContexts(raw, dropped) {
12361
12647
  const chatContexts = {};
12362
12648
  for (const [chatKey, value] of Object.entries(raw)) {
12363
12649
  if (!isChatContextRecord(value)) {
12364
- throw new Error(`state file "${path3}" contains malformed chat context record "${chatKey}"`);
12650
+ dropped.push({ section: "chat_contexts", key: chatKey, reason: "malformed chat context record" });
12651
+ continue;
12365
12652
  }
12366
12653
  chatContexts[chatKey] = value;
12367
12654
  }
@@ -12402,92 +12689,177 @@ function isScheduledTaskRecord(value) {
12402
12689
  return false;
12403
12690
  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
12691
  }
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
- }
12692
+ function parseScheduledTasks(raw, dropped) {
12693
+ const source = sectionRecord(raw, "scheduled_tasks", dropped);
12411
12694
  const tasks = {};
12412
- for (const [id, value] of Object.entries(raw)) {
12695
+ for (const [id, value] of Object.entries(source)) {
12413
12696
  if (!isScheduledTaskRecord(value) || value.id !== id) {
12414
- throw new Error(`state file "${path3}" contains malformed scheduled task record "${id}"`);
12697
+ dropped.push({ section: "scheduled_tasks", key: id, reason: "malformed scheduled task record" });
12698
+ continue;
12415
12699
  }
12416
12700
  tasks[id] = value;
12417
12701
  }
12418
12702
  return tasks;
12419
12703
  }
12420
- function parseState(raw, path3) {
12704
+ function parseState(raw, path3, dropped = []) {
12421
12705
  if (!isRecord2(raw)) {
12422
12706
  throw new Error(`state file "${path3}" must contain a JSON object`);
12423
12707
  }
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);
12708
+ const parsedSessions = parseSessions(sectionRecord(raw.sessions, "sessions", dropped), dropped);
12709
+ const orchestration3 = parseOrchestrationState(raw.orchestration, dropped);
12710
+ repairExternalCoordinatorIdentityCollisions(parsedSessions, orchestration3, dropped);
12435
12711
  return {
12436
12712
  sessions: parsedSessions,
12437
- chat_contexts: parseChatContexts(chatContexts, path3),
12713
+ chat_contexts: parseChatContexts(sectionRecord(raw.chat_contexts, "chat_contexts", dropped), dropped),
12438
12714
  native_session_lists: parseNativeSessionLists(raw.native_session_lists),
12439
12715
  orchestration: orchestration3,
12440
- scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
12716
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, dropped)
12441
12717
  };
12442
12718
  }
12443
- function validateExternalCoordinatorIdentityCollisions(sessions, orchestration3, path3) {
12719
+ function repairExternalCoordinatorIdentityCollisions(sessions, orchestration3, dropped) {
12444
12720
  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`);
12721
+ const conflict = findExternalCoordinatorConflict(coordinatorSession, sessions, orchestration3);
12722
+ if (!conflict) {
12723
+ continue;
12453
12724
  }
12725
+ delete orchestration3.externalCoordinators[coordinatorSession];
12726
+ dropped.push({
12727
+ section: "orchestration.externalCoordinators",
12728
+ key: coordinatorSession,
12729
+ reason: `conflicts with ${conflict}; dropped (re-registered on next coordinator connect)`
12730
+ });
12454
12731
  }
12455
12732
  }
12733
+ function findExternalCoordinatorConflict(coordinatorSession, sessions, orchestration3) {
12734
+ if (Object.values(sessions).some((session3) => session3.transport_session === coordinatorSession)) {
12735
+ return "a logical session";
12736
+ }
12737
+ if (orchestration3.workerBindings[coordinatorSession]) {
12738
+ return "a worker binding";
12739
+ }
12740
+ if (Object.values(orchestration3.tasks).some((task) => task.workerSession === coordinatorSession && (!isTerminalTaskStatus(task.status) || task.reviewPending !== undefined))) {
12741
+ return "an active task worker session";
12742
+ }
12743
+ return null;
12744
+ }
12456
12745
  function isTerminalTaskStatus(status) {
12457
12746
  return status === "completed" || status === "failed" || status === "cancelled";
12458
12747
  }
12459
12748
 
12460
12749
  class StateStore {
12461
12750
  path;
12462
- constructor(path3) {
12751
+ options;
12752
+ loadReport = null;
12753
+ constructor(path3, options = {}) {
12463
12754
  this.path = path3;
12755
+ this.options = options;
12756
+ }
12757
+ get lastLoadReport() {
12758
+ return this.loadReport;
12464
12759
  }
12465
12760
  async load() {
12761
+ this.loadReport = null;
12762
+ const read = await this.readAndParse();
12763
+ if (read.kind === "absent") {
12764
+ return createEmptyState();
12765
+ }
12766
+ if (read.kind === "corrupt") {
12767
+ return await this.recoverFromCorruptFile(read.reason);
12768
+ }
12769
+ if (read.dropped.length === 0) {
12770
+ return read.state;
12771
+ }
12772
+ const report = { dropped: read.dropped };
12773
+ const quarantinePath = `${this.path}.quarantine-${this.fileTimestamp()}`;
12466
12774
  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);
12775
+ const written = await (this.options.writeBackup ?? defaultWriteBackup)(quarantinePath, read.content);
12776
+ report.quarantinePath = typeof written === "string" ? written : quarantinePath;
12777
+ } catch (error2) {
12778
+ report.backupError = error2 instanceof Error ? error2.message : String(error2);
12779
+ }
12780
+ this.loadReport = report;
12781
+ return read.state;
12782
+ }
12783
+ async inspect() {
12784
+ const read = await this.readAndParse();
12785
+ if (read.kind === "absent") {
12786
+ return { state: createEmptyState(), report: null };
12787
+ }
12788
+ if (read.kind === "corrupt") {
12789
+ return {
12790
+ state: createEmptyState(),
12791
+ report: { dropped: [{ section: "file", key: this.path, reason: read.reason }] }
12792
+ };
12793
+ }
12794
+ return {
12795
+ state: read.state,
12796
+ report: read.dropped.length > 0 ? { dropped: read.dropped } : null
12797
+ };
12798
+ }
12799
+ async readAndParse() {
12800
+ let content;
12801
+ try {
12802
+ content = await readFile6(this.path, "utf8");
12480
12803
  } catch (error2) {
12481
12804
  if (error2.code === "ENOENT") {
12482
- return createEmptyState();
12805
+ return { kind: "absent" };
12483
12806
  }
12484
12807
  throw error2;
12485
12808
  }
12809
+ if (content.trim() === "") {
12810
+ return { kind: "absent" };
12811
+ }
12812
+ let parsed;
12813
+ try {
12814
+ parsed = JSON.parse(content);
12815
+ } catch (error2) {
12816
+ return {
12817
+ kind: "corrupt",
12818
+ reason: `invalid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
12819
+ };
12820
+ }
12821
+ if (!isRecord2(parsed)) {
12822
+ return { kind: "corrupt", reason: "top-level value is not an object" };
12823
+ }
12824
+ const dropped = [];
12825
+ return { kind: "parsed", state: parseState(parsed, this.path, dropped), dropped, content };
12486
12826
  }
12487
12827
  async save(state) {
12488
- await writePrivateFileAtomic(this.path, JSON.stringify(state, null, 2));
12828
+ await writePrivateFileAtomic(this.path, JSON.stringify({ version: STATE_FILE_VERSION, ...state }, null, 2));
12829
+ }
12830
+ fileTimestamp() {
12831
+ const now = this.options.now?.() ?? new Date;
12832
+ return now.toISOString().replace(/[:.]/g, "-");
12833
+ }
12834
+ async recoverFromCorruptFile(reason) {
12835
+ const corruptPath = `${this.path}.corrupt-${this.fileTimestamp()}`;
12836
+ const report = {
12837
+ dropped: [{ section: "file", key: this.path, reason }]
12838
+ };
12839
+ try {
12840
+ await rename(this.path, corruptPath);
12841
+ report.corruptPath = corruptPath;
12842
+ } catch (error2) {
12843
+ report.backupError = error2 instanceof Error ? error2.message : String(error2);
12844
+ }
12845
+ this.loadReport = report;
12846
+ return createEmptyState();
12847
+ }
12848
+ }
12849
+ async function defaultWriteBackup(targetPath, content) {
12850
+ for (let attempt = 0;; attempt += 1) {
12851
+ const candidate = attempt === 0 ? targetPath : `${targetPath}-${attempt}`;
12852
+ try {
12853
+ await writeFile3(candidate, content, { encoding: "utf8", mode: 384, flag: "wx" });
12854
+ return candidate;
12855
+ } catch (error2) {
12856
+ if (error2.code !== "EEXIST" || attempt >= 9) {
12857
+ throw error2;
12858
+ }
12859
+ }
12489
12860
  }
12490
12861
  }
12862
+ var STATE_FILE_VERSION = 1;
12491
12863
  var init_state_store = __esm(() => {
12492
12864
  init_private_file();
12493
12865
  init_types();
@@ -12726,14 +13098,23 @@ class ScheduledTaskService {
12726
13098
  return task;
12727
13099
  });
12728
13100
  }
12729
- listPending() {
13101
+ listPending(chatKey) {
13102
+ return this.listPendingAllChats().filter((task) => task.chat_key === chatKey);
13103
+ }
13104
+ listPendingAllChats() {
12730
13105
  return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
12731
13106
  }
12732
- async cancelPending(inputId) {
13107
+ async cancelPending(inputId, chatKey) {
13108
+ return await this.cancelPendingWhere(inputId, (task) => task.chat_key === chatKey);
13109
+ }
13110
+ async cancelPendingAnyChat(inputId) {
13111
+ return await this.cancelPendingWhere(inputId, () => true);
13112
+ }
13113
+ async cancelPendingWhere(inputId, allowed) {
12733
13114
  return await this.mutate(async () => {
12734
13115
  const id = normalizeId(inputId);
12735
13116
  const task = this.state.scheduled_tasks[id];
12736
- if (!task || task.status !== "pending")
13117
+ if (!task || task.status !== "pending" || !allowed(task))
12737
13118
  return false;
12738
13119
  task.status = "cancelled";
12739
13120
  task.cancelled_at = this.now().toISOString();
@@ -12765,7 +13146,7 @@ class ScheduledTaskService {
12765
13146
  async claimDueTasks() {
12766
13147
  return await this.mutate(async () => {
12767
13148
  const nowMs = this.now().getTime();
12768
- const due = this.listPending().filter((task) => Date.parse(task.execute_at) <= nowMs);
13149
+ const due = this.listPendingAllChats().filter((task) => Date.parse(task.execute_at) <= nowMs);
12769
13150
  if (due.length === 0)
12770
13151
  return [];
12771
13152
  const at = this.now().toISOString();
@@ -12774,7 +13155,16 @@ class ScheduledTaskService {
12774
13155
  task.triggered_at = at;
12775
13156
  this.claimedInThisSession.add(task.id);
12776
13157
  }
12777
- await this.save();
13158
+ try {
13159
+ await this.save();
13160
+ } catch (error2) {
13161
+ for (const task of due) {
13162
+ task.status = "pending";
13163
+ delete task.triggered_at;
13164
+ this.claimedInThisSession.delete(task.id);
13165
+ }
13166
+ throw error2;
13167
+ }
12778
13168
  return due.map((task) => ({ ...task }));
12779
13169
  });
12780
13170
  }
@@ -12832,20 +13222,20 @@ var init_scheduled_service = () => {};
12832
13222
 
12833
13223
  // src/plugins/plugin-home.ts
12834
13224
  import { readFileSync as readFileSync2 } from "node:fs";
12835
- import { copyFile, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
13225
+ import { copyFile, mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
12836
13226
  import { homedir as homedir3 } from "node:os";
12837
- import { dirname as dirname6, join as join6 } from "node:path";
13227
+ import { dirname as dirname4, join as join6 } from "node:path";
12838
13228
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12839
13229
  function resolveCoreRoot() {
12840
13230
  try {
12841
- let dir = dirname6(fileURLToPath2(import.meta.url));
13231
+ let dir = dirname4(fileURLToPath2(import.meta.url));
12842
13232
  for (let depth = 0;depth < 12; depth++) {
12843
13233
  try {
12844
13234
  const pkg = JSON.parse(readFileSync2(join6(dir, "package.json"), "utf-8"));
12845
13235
  if (pkg.name && CORE_ROOT_NAMES.includes(pkg.name))
12846
13236
  return dir;
12847
13237
  } catch {}
12848
- const parent = dirname6(dir);
13238
+ const parent = dirname4(dir);
12849
13239
  if (parent === dir)
12850
13240
  break;
12851
13241
  dir = parent;
@@ -12863,7 +13253,7 @@ async function ensureCoreResolution(pluginHome) {
12863
13253
  for (const name of SHIM_SPECIFIERS) {
12864
13254
  const targetDir = join6(pluginHome, "node_modules", name);
12865
13255
  const dstJs = join6(targetDir, "plugin-api.js");
12866
- await mkdir6(targetDir, { recursive: true });
13256
+ await mkdir4(targetDir, { recursive: true });
12867
13257
  try {
12868
13258
  await copyFile(srcJs, dstJs);
12869
13259
  } catch (error2) {
@@ -12906,7 +13296,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12906
13296
  const manifestPath = join6(pluginHome, "package.json");
12907
13297
  let raw;
12908
13298
  try {
12909
- raw = await readFile6(manifestPath, "utf8");
13299
+ raw = await readFile7(manifestPath, "utf8");
12910
13300
  } catch {
12911
13301
  return false;
12912
13302
  }
@@ -12924,7 +13314,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12924
13314
  return true;
12925
13315
  }
12926
13316
  async function ensurePluginHome(pluginHome) {
12927
- await mkdir6(pluginHome, { recursive: true, mode: 448 });
13317
+ await mkdir4(pluginHome, { recursive: true, mode: 448 });
12928
13318
  await writeFile4(join6(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
12929
13319
  `, { flag: "wx" }).catch((error2) => {
12930
13320
  if (error2.code !== "EEXIST")
@@ -15540,7 +15930,7 @@ function createConversationExecutor() {
15540
15930
  var DEFAULT_SESSION_KEY = "__chat__";
15541
15931
 
15542
15932
  // 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";
15933
+ import { access as access2, mkdir as mkdir5, readdir, rm as rm5, stat, writeFile as writeFile5 } from "node:fs/promises";
15544
15934
  import path7 from "node:path";
15545
15935
 
15546
15936
  class RuntimeMediaStore {
@@ -15558,7 +15948,7 @@ class RuntimeMediaStore {
15558
15948
  const safeMessageId = safePathSegment(input.messageId || "message");
15559
15949
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
15560
15950
  const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
15561
- await mkdir7(dir, { recursive: true });
15951
+ await mkdir5(dir, { recursive: true, mode: 448 });
15562
15952
  const resolvedRoot = path7.resolve(this.rootDir);
15563
15953
  const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
15564
15954
  if (!isPathInside(resolvedFile, resolvedRoot)) {
@@ -16947,15 +17337,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16947
17337
  await drainPendingFinalForJx(ctx);
16948
17338
  return { handled: true };
16949
17339
  }
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
17340
  default:
16960
17341
  return { handled: false };
16961
17342
  }
@@ -16968,7 +17349,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16968
17349
  }
16969
17350
  }
16970
17351
  var init_slash_commands = __esm(() => {
16971
- init_accounts();
16972
17352
  init_logger();
16973
17353
  init_i18n();
16974
17354
  init_final_heads_up();
@@ -16984,14 +17364,14 @@ function normalizeMediaArray(media) {
16984
17364
  }
16985
17365
 
16986
17366
  // 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";
17367
+ import { readdir as readdir2, rename as rename2, rm as rm6, stat as stat2 } from "node:fs/promises";
17368
+ import { basename, dirname as dirname5, join as join8 } from "node:path";
16989
17369
  async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
16990
17370
  let currentSize = 0;
16991
17371
  try {
16992
17372
  currentSize = (await stat2(filePath)).size;
16993
17373
  } catch (error2) {
16994
- if (!isMissingFileError2(error2)) {
17374
+ if (!isMissingFileError3(error2)) {
16995
17375
  throw error2;
16996
17376
  }
16997
17377
  }
@@ -17009,24 +17389,24 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
17009
17389
  for (let index = maxFiles - 1;index >= 1; index -= 1) {
17010
17390
  const source = `${filePath}.${index}`;
17011
17391
  try {
17012
- await rename(source, `${filePath}.${index + 1}`);
17392
+ await rename2(source, `${filePath}.${index + 1}`);
17013
17393
  } catch (error2) {
17014
- if (!isMissingFileError2(error2)) {
17394
+ if (!isMissingFileError3(error2)) {
17015
17395
  throw error2;
17016
17396
  }
17017
17397
  }
17018
17398
  }
17019
- await rename(filePath, `${filePath}.1`);
17399
+ await rename2(filePath, `${filePath}.1`);
17020
17400
  }
17021
17401
  async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
17022
- const parentDir = dirname7(filePath);
17402
+ const parentDir = dirname5(filePath);
17023
17403
  const prefix = `${basename(filePath)}.`;
17024
17404
  const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
17025
17405
  let files = [];
17026
17406
  try {
17027
17407
  files = await readdir2(parentDir);
17028
17408
  } catch (error2) {
17029
- if (isMissingFileError2(error2)) {
17409
+ if (isMissingFileError3(error2)) {
17030
17410
  return;
17031
17411
  }
17032
17412
  throw error2;
@@ -17036,23 +17416,31 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
17036
17416
  continue;
17037
17417
  }
17038
17418
  const candidate = join8(parentDir, file);
17039
- const details = await stat2(candidate);
17419
+ let details;
17420
+ try {
17421
+ details = await stat2(candidate);
17422
+ } catch (error2) {
17423
+ if (isMissingFileError3(error2)) {
17424
+ continue;
17425
+ }
17426
+ throw error2;
17427
+ }
17040
17428
  if (details.mtime.getTime() < cutoff) {
17041
17429
  await rm6(candidate, { force: true });
17042
17430
  }
17043
17431
  }
17044
17432
  }
17045
- function isMissingFileError2(error2) {
17433
+ function isMissingFileError3(error2) {
17046
17434
  return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
17047
17435
  }
17048
17436
  var init_rotating_file_writer = () => {};
17049
17437
 
17050
17438
  // src/perf/perf-log-writer.ts
17051
17439
  import { appendFile as fsAppendFile, mkdir as fsMkdir } from "node:fs/promises";
17052
- import { dirname as dirname8 } from "node:path";
17440
+ import { dirname as dirname6 } from "node:path";
17053
17441
  function createPerfLogWriter(options) {
17054
17442
  const append = options.appendImpl ?? ((p, d) => fsAppendFile(p, d, "utf8"));
17055
- const mkdir8 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17443
+ const mkdir6 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17056
17444
  return;
17057
17445
  }));
17058
17446
  const now = options.now ?? (() => new Date);
@@ -17099,7 +17487,7 @@ function createPerfLogWriter(options) {
17099
17487
  return;
17100
17488
  const data = batch.join("");
17101
17489
  try {
17102
- await mkdir8(dirname8(options.filePath), { recursive: true });
17490
+ await mkdir6(dirname6(options.filePath), { recursive: true, mode: 448 });
17103
17491
  await rotateIfNeeded(options.filePath, Buffer.byteLength(data), options.maxSizeBytes, options.maxFiles);
17104
17492
  await append(options.filePath, data);
17105
17493
  consecutiveFailures = 0;
@@ -17630,13 +18018,39 @@ async function handleWeixinMessageTurn(full, deps) {
17630
18018
  deps.errLog(`weixin.final.dropped reason=backgrounded_no_store kind=text chatKey=${to}`);
17631
18019
  } else {
17632
18020
  const rawChunks = chunkFinalText(finalText, MAX_FINAL_CHUNK_BYTES);
18021
+ const sendAllParkedNotice = async (count) => {
18022
+ try {
18023
+ await sendMessageWeixin({
18024
+ to,
18025
+ text: t().misc.finalAllParked(count),
18026
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
18027
+ });
18028
+ } catch (noticeErr) {
18029
+ deps.errLog(`weixin.final.parked_notice_failed chatKey=${to} err=${String(noticeErr)}`);
18030
+ }
18031
+ };
18032
+ const buildPendingChunk = (text, seq, total) => {
18033
+ const entry = { text, seq, total };
18034
+ if (contextToken !== undefined)
18035
+ entry.contextToken = contextToken;
18036
+ if (deps.accountId !== undefined)
18037
+ entry.accountId = deps.accountId;
18038
+ return entry;
18039
+ };
17633
18040
  if (rawChunks.length > 0) {
17634
18041
  const total = rawChunks.length;
17635
18042
  if (total === 1) {
17636
18043
  const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
17637
18044
  if (!reserved) {
17638
- finalDropped = true;
17639
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
18045
+ if (deps.enqueuePendingFinal) {
18046
+ deps.enqueuePendingFinal(to, [buildPendingChunk(rawChunks[0], 1, 1)]);
18047
+ finalChunksPending = 1;
18048
+ deps.errLog(`weixin.final.parked reason=quota_exhausted kind=text chatKey=${to}`);
18049
+ await sendAllParkedNotice(1);
18050
+ } else {
18051
+ finalDropped = true;
18052
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
18053
+ }
17640
18054
  } else {
17641
18055
  await sendMessageWeixin({
17642
18056
  to,
@@ -17693,16 +18107,11 @@ ${buildFinalHeadsUp({
17693
18107
  const restToPark = prefixed.slice(sent);
17694
18108
  finalChunksPending = restToPark.length;
17695
18109
  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
- });
18110
+ const pending = restToPark.map((text, idx) => buildPendingChunk(text, sent + idx + 1, total));
17705
18111
  deps.enqueuePendingFinal(to, pending);
18112
+ if (sent === 0) {
18113
+ await sendAllParkedNotice(restToPark.length);
18114
+ }
17706
18115
  }
17707
18116
  }
17708
18117
  }
@@ -17898,7 +18307,7 @@ function shouldFetchTypingConfig(textBody) {
17898
18307
  const command = parseSlashCommand(textBody);
17899
18308
  if (!command)
17900
18309
  return true;
17901
- return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug", "/logout"].includes(command);
18310
+ return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug"].includes(command);
17902
18311
  }
17903
18312
  async function monitorWeixinProvider(opts) {
17904
18313
  const {
@@ -18621,7 +19030,7 @@ ${buildFinalHeadsUp({
18621
19030
  let sent = 0;
18622
19031
  for (let index = 0;index < wave.length; index += 1) {
18623
19032
  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 });
19033
+ 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
19034
  break;
18626
19035
  }
18627
19036
  const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
@@ -18630,7 +19039,7 @@ ${buildFinalHeadsUp({
18630
19039
  sent += 1;
18631
19040
  }
18632
19041
  const restToPark = chunks.slice(sent);
18633
- if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
19042
+ if (restToPark.length > 0 && deps.enqueuePendingFinal) {
18634
19043
  const pending = restToPark.map((text, index) => {
18635
19044
  const entry = { text, seq: sent + index + 1, total };
18636
19045
  if (deliveryContextToken)
@@ -18640,6 +19049,12 @@ ${buildFinalHeadsUp({
18640
19049
  return entry;
18641
19050
  });
18642
19051
  deps.enqueuePendingFinal(quotaKey, pending);
19052
+ if (sent === 0) {
19053
+ const noticeDelivered = await sendTextViaAvailableAccount(t().misc.finalAllParked(restToPark.length), "scheduled.final_parked_notice_failed");
19054
+ if (!noticeDelivered) {
19055
+ await deps.logger.info("scheduled.final_parked_notice_failed", "scheduled parked-final notice could not be delivered", { chatKey: input.chatKey, parked: restToPark.length });
19056
+ }
19057
+ }
18643
19058
  }
18644
19059
  }
18645
19060
  async function sendTextViaAvailableAccount(text, errorEvent) {
@@ -18677,8 +19092,8 @@ var init_scheduled_turn = __esm(() => {
18677
19092
  });
18678
19093
 
18679
19094
  // 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";
19095
+ import { mkdir as mkdir6, open as open3, readFile as readFile8, rm as rm7 } from "node:fs/promises";
19096
+ import { dirname as dirname7, join as join9 } from "node:path";
18682
19097
  import { homedir as homedir4 } from "node:os";
18683
19098
  function createWeixinConsumerLock(options = {}) {
18684
19099
  const lockFilePath = options.lockFilePath ?? join9(coreHomeDir(homedir4()), "runtime", "weixin-consumer.lock.json");
@@ -18686,7 +19101,7 @@ function createWeixinConsumerLock(options = {}) {
18686
19101
  const onDiagnostic = options.onDiagnostic;
18687
19102
  return {
18688
19103
  async acquire(meta2) {
18689
- await mkdir8(dirname9(lockFilePath), { recursive: true });
19104
+ await mkdir6(dirname7(lockFilePath), { recursive: true, mode: 448 });
18690
19105
  while (true) {
18691
19106
  try {
18692
19107
  const handle = await open3(lockFilePath, "wx");
@@ -18759,7 +19174,7 @@ function createWeixinConsumerLock(options = {}) {
18759
19174
  }
18760
19175
  async function loadLockMetadata(path14) {
18761
19176
  try {
18762
- const raw = await readFile7(path14, "utf8");
19177
+ const raw = await readFile8(path14, "utf8");
18763
19178
  const parsed = JSON.parse(raw);
18764
19179
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
18765
19180
  return null;
@@ -18824,6 +19239,13 @@ class WeixinChannel {
18824
19239
  logout() {
18825
19240
  logout();
18826
19241
  }
19242
+ stop() {
19243
+ this.agent = null;
19244
+ this.quota = null;
19245
+ this.logger = null;
19246
+ this.markDelivered = null;
19247
+ this.markFailed = null;
19248
+ }
18827
19249
  createConsumerLock(options) {
18828
19250
  return createWeixinConsumerLock({
18829
19251
  ...options?.lockFilePath ? { lockFilePath: options.lockFilePath } : {},
@@ -19404,8 +19826,8 @@ var init_bootstrap = __esm(() => {
19404
19826
  });
19405
19827
 
19406
19828
  // 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";
19829
+ import { appendFile, chmod as chmod3, mkdir as mkdir7 } from "node:fs/promises";
19830
+ import { dirname as dirname9 } from "node:path";
19409
19831
  function createNoopAppLogger() {
19410
19832
  return {
19411
19833
  debug: async () => {},
@@ -19419,6 +19841,7 @@ function createAppLogger(options) {
19419
19841
  const now = options.now ?? (() => new Date);
19420
19842
  let writeChain = Promise.resolve();
19421
19843
  let modeEnsured = false;
19844
+ let writeErrorLatched = false;
19422
19845
  return {
19423
19846
  debug: async (event, message, context) => {
19424
19847
  await enqueueWrite("debug", event, message, context);
@@ -19430,14 +19853,21 @@ function createAppLogger(options) {
19430
19853
  await enqueueWrite("error", event, message, context);
19431
19854
  },
19432
19855
  cleanup: async () => {
19433
- await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19856
+ try {
19857
+ await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19858
+ } catch {}
19434
19859
  },
19435
19860
  flush: async () => {
19436
19861
  await writeChain;
19437
19862
  }
19438
19863
  };
19439
19864
  function enqueueWrite(level, event, message, context = {}) {
19440
- const writePromise = writeChain.catch(() => {}).then(() => writeLog2(level, event, message, context));
19865
+ const writePromise = writeChain.then(() => writeLog2(level, event, message, context)).catch((error2) => {
19866
+ if (!writeErrorLatched) {
19867
+ writeErrorLatched = true;
19868
+ console.error("[xacpx] app-logger: log file write failed — further write errors will be suppressed.", error2 instanceof Error ? error2.message : String(error2));
19869
+ }
19870
+ });
19441
19871
  writeChain = writePromise;
19442
19872
  return writePromise;
19443
19873
  }
@@ -19446,13 +19876,14 @@ function createAppLogger(options) {
19446
19876
  return;
19447
19877
  }
19448
19878
  const line = formatLogLine(now(), level, event, message, context);
19449
- await mkdir9(dirname11(options.filePath), { recursive: true });
19879
+ await mkdir7(dirname9(options.filePath), { recursive: true, mode: 448 });
19450
19880
  if (!modeEnsured) {
19451
19881
  modeEnsured = true;
19452
- await chmod2(options.filePath, 384).catch(() => {});
19882
+ await chmod3(options.filePath, 384).catch(() => {});
19453
19883
  }
19454
19884
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
19455
19885
  await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
19886
+ writeErrorLatched = false;
19456
19887
  }
19457
19888
  }
19458
19889
  function formatLogLine(time3, level, event, message, context) {
@@ -19481,7 +19912,7 @@ var init_app_logger = __esm(() => {
19481
19912
  });
19482
19913
 
19483
19914
  // src/transport/acpx-session-index.ts
19484
- import { readFile as readFile11 } from "node:fs/promises";
19915
+ import { readFile as readFile12 } from "node:fs/promises";
19485
19916
  import { homedir as homedir5 } from "node:os";
19486
19917
  import { resolve as resolve2 } from "node:path";
19487
19918
  async function resolveSessionAgentCommandFromIndex(session3) {
@@ -19490,7 +19921,7 @@ async function resolveSessionAgentCommandFromIndex(session3) {
19490
19921
  return;
19491
19922
  }
19492
19923
  try {
19493
- const raw = await readFile11(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
19924
+ const raw = await readFile12(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
19494
19925
  const parsed = JSON.parse(raw);
19495
19926
  const targetCwd = resolve2(session3.cwd);
19496
19927
  const match = parsed.entries?.find((entry) => entry.name === session3.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
@@ -19672,7 +20103,9 @@ function parseCommand(input) {
19672
20103
  if (!trimmed.startsWith("/")) {
19673
20104
  return { kind: "prompt", text: trimmed };
19674
20105
  }
19675
- const parts = tokenizeCommand(trimmed);
20106
+ const tokens = tokenizeCommand(trimmed);
20107
+ const parts = tokens.map((token) => token.value);
20108
+ const rawTail = (index) => index < tokens.length ? trimmed.slice(tokens[index]?.start ?? 0) : "";
19676
20109
  const command = normalizeCommand(parts[0] ?? "");
19677
20110
  if (command === "/help" && parts.length === 1)
19678
20111
  return { kind: "help" };
@@ -19781,7 +20214,7 @@ function parseCommand(input) {
19781
20214
  return { kind: "invalid", text: trimmed, recognizedCommand: "/session" };
19782
20215
  }
19783
20216
  if (command === "/group" && parts[1] === "new" && parts.length > 2) {
19784
- const title = parts.slice(2).join(" ");
20217
+ const title = rawTail(2);
19785
20218
  if (title.trim().length > 0) {
19786
20219
  return { kind: "group.new", title };
19787
20220
  }
@@ -19805,7 +20238,7 @@ function parseCommand(input) {
19805
20238
  }
19806
20239
  break;
19807
20240
  }
19808
- const task = parts.slice(index).join(" ");
20241
+ const task = rawTail(index);
19809
20242
  if (groupId.trim().length > 0 && targetAgent.trim().length > 0 && task.trim().length > 0) {
19810
20243
  return {
19811
20244
  kind: "group.delegate",
@@ -19859,7 +20292,7 @@ function parseCommand(input) {
19859
20292
  return { kind: "agent.rm", name: parts[2] };
19860
20293
  }
19861
20294
  if ((command === "/delegate" || command === "/dg") && parts[1]) {
19862
- const parsedDelegate = parseDelegateRequest(parts);
20295
+ const parsedDelegate = parseDelegateRequest(parts, rawTail);
19863
20296
  if (parsedDelegate) {
19864
20297
  return parsedDelegate;
19865
20298
  }
@@ -19887,7 +20320,11 @@ function parseCommand(input) {
19887
20320
  if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
19888
20321
  return { kind: "later.cancel", id: parts[2] };
19889
20322
  }
19890
- return { kind: "later.create", tokens: parts.slice(1) };
20323
+ return {
20324
+ kind: "later.create",
20325
+ tokens: parts.slice(1),
20326
+ tails: tokens.slice(1).map((_, index) => rawTail(index + 1))
20327
+ };
19891
20328
  }
19892
20329
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
19893
20330
  const name = parts[2];
@@ -20140,17 +20577,18 @@ function readNativeAttachCommand(parts, identifierIndex) {
20140
20577
  return { kind: "session.native.attach", identifier };
20141
20578
  }
20142
20579
  function normalizeCommand(command) {
20143
- if (command === "/ss")
20580
+ const lowered = command.toLowerCase();
20581
+ if (lowered === "/ss")
20144
20582
  return "/session";
20145
- if (command === "/ws")
20583
+ if (lowered === "/ws")
20146
20584
  return "/workspace";
20147
- if (command === "/pm")
20585
+ if (lowered === "/pm")
20148
20586
  return "/permission";
20149
- if (command === "/stop")
20587
+ if (lowered === "/stop")
20150
20588
  return "/cancel";
20151
- if (command === "/lt")
20589
+ if (lowered === "/lt")
20152
20590
  return "/later";
20153
- return command;
20591
+ return lowered;
20154
20592
  }
20155
20593
  function isRecognizedCommand(command) {
20156
20594
  return isKnownXacpxCommandPrefix(command);
@@ -20173,31 +20611,41 @@ function toNonInteractivePermission(value) {
20173
20611
  function tokenizeCommand(input) {
20174
20612
  const tokens = [];
20175
20613
  let current = "";
20176
- let quote = null;
20614
+ let start2 = -1;
20615
+ let closingQuote = null;
20616
+ let offset = 0;
20177
20617
  for (const char of input) {
20178
- if (quote) {
20179
- if (char === quote) {
20180
- quote = null;
20618
+ const charStart = offset;
20619
+ offset += char.length;
20620
+ if (closingQuote) {
20621
+ if (char === closingQuote) {
20622
+ closingQuote = null;
20181
20623
  } else {
20182
20624
  current += char;
20183
20625
  }
20184
20626
  continue;
20185
20627
  }
20186
- if (char === '"' || char === "'") {
20187
- quote = char;
20628
+ const close = QUOTE_PAIRS[char];
20629
+ if (close) {
20630
+ if (start2 === -1)
20631
+ start2 = charStart;
20632
+ closingQuote = close;
20188
20633
  continue;
20189
20634
  }
20190
20635
  if (/\s/.test(char)) {
20191
20636
  if (current.length > 0) {
20192
- tokens.push(current);
20637
+ tokens.push({ value: current, start: start2 });
20193
20638
  current = "";
20194
20639
  }
20640
+ start2 = -1;
20195
20641
  continue;
20196
20642
  }
20643
+ if (start2 === -1)
20644
+ start2 = charStart;
20197
20645
  current += char;
20198
20646
  }
20199
20647
  if (current.length > 0) {
20200
- tokens.push(current);
20648
+ tokens.push({ value: current, start: start2 });
20201
20649
  }
20202
20650
  return tokens;
20203
20651
  }
@@ -20240,7 +20688,7 @@ function parseListFilterFlags(parts, validStatuses) {
20240
20688
  }
20241
20689
  return { filter, ok: true };
20242
20690
  }
20243
- function parseDelegateRequest(parts) {
20691
+ function parseDelegateRequest(parts, rawTail) {
20244
20692
  const targetAgent = parts[1];
20245
20693
  if (!targetAgent) {
20246
20694
  return null;
@@ -20268,7 +20716,7 @@ function parseDelegateRequest(parts) {
20268
20716
  }
20269
20717
  break;
20270
20718
  }
20271
- const task = parts.slice(index).join(" ");
20719
+ const task = rawTail(index);
20272
20720
  if (task.trim().length === 0) {
20273
20721
  return null;
20274
20722
  }
@@ -20280,9 +20728,16 @@ function parseDelegateRequest(parts) {
20280
20728
  task
20281
20729
  };
20282
20730
  }
20283
- var TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20731
+ var QUOTE_PAIRS, TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20284
20732
  var init_parse_command = __esm(() => {
20285
20733
  init_command_list();
20734
+ QUOTE_PAIRS = {
20735
+ '"': '"',
20736
+ "'": "'",
20737
+ "“": "”",
20738
+ "‘": "’",
20739
+ """: """
20740
+ };
20286
20741
  TASK_STATUS_VALUES = [
20287
20742
  "pending",
20288
20743
  "needs_confirmation",
@@ -20295,7 +20750,50 @@ var init_parse_command = __esm(() => {
20295
20750
  });
20296
20751
 
20297
20752
  // src/commands/command-policy.ts
20753
+ function isInternalScheduledTurn(metadata) {
20754
+ return Boolean(metadata?.scheduledSessionAlias || metadata?.scheduledSessionDescriptor);
20755
+ }
20756
+ function resolveChannelOwnerIds(config4, channel) {
20757
+ if (!config4 || !channel) {
20758
+ return;
20759
+ }
20760
+ let configured = false;
20761
+ const ids = new Set;
20762
+ if (config4.channel?.type === channel && config4.channel.ownerIds) {
20763
+ configured = true;
20764
+ for (const id of config4.channel.ownerIds)
20765
+ ids.add(id);
20766
+ }
20767
+ for (const entry of config4.channels ?? []) {
20768
+ if ((entry.type === channel || entry.id === channel) && entry.ownerIds) {
20769
+ configured = true;
20770
+ for (const id of entry.ownerIds)
20771
+ ids.add(id);
20772
+ }
20773
+ }
20774
+ return configured ? [...ids] : undefined;
20775
+ }
20776
+ function withEffectiveOwner(metadata, config4) {
20777
+ if (!metadata?.channel || isInternalScheduledTurn(metadata)) {
20778
+ return metadata;
20779
+ }
20780
+ const ownerIds = resolveChannelOwnerIds(config4, metadata.channel);
20781
+ if (ownerIds === undefined) {
20782
+ return metadata;
20783
+ }
20784
+ const isOwner = metadata.isOwner === true || typeof metadata.senderId === "string" && ownerIds.includes(metadata.senderId);
20785
+ return { ...metadata, isOwner };
20786
+ }
20298
20787
  function authorizeCommandForChat(command, metadata) {
20788
+ if (metadata?.channel && !isInternalScheduledTurn(metadata) && metadata.chatType !== "direct" && metadata.chatType !== "group") {
20789
+ if (GROUP_PUBLIC_COMMAND_KINDS.has(command.kind)) {
20790
+ return { allowed: true };
20791
+ }
20792
+ return {
20793
+ allowed: false,
20794
+ reason: "chat-type-missing"
20795
+ };
20796
+ }
20299
20797
  if (metadata?.chatType !== "group") {
20300
20798
  return { allowed: true };
20301
20799
  }
@@ -20310,7 +20808,14 @@ function authorizeCommandForChat(command, metadata) {
20310
20808
  reason: "group-owner-required"
20311
20809
  };
20312
20810
  }
20313
- function renderCommandAccessDenied(command) {
20811
+ function renderCommandAccessDenied(command, reason) {
20812
+ if (reason === "chat-type-missing") {
20813
+ return [
20814
+ `⚠️ ${renderCommandLabel(command)}${t().misc.commandAccessDeniedChatTypeMissingSuffix}`,
20815
+ t().misc.commandAccessDeniedChatTypeMissingHint
20816
+ ].join(`
20817
+ `);
20818
+ }
20314
20819
  return [
20315
20820
  `⚠️ ${renderCommandLabel(command)}${t().misc.commandAccessDeniedSuffix}`,
20316
20821
  t().misc.commandAccessDeniedHint
@@ -20436,13 +20941,18 @@ async function handlePermissionModeSet(context, mode) {
20436
20941
  return { text: p.noWritableConfig };
20437
20942
  }
20438
20943
  const previous = cloneAppConfig(context.config);
20944
+ const previousRaw = await context.configStore.getRawValue(["transport", "permissionMode"]);
20439
20945
  const updated = await context.configStore.updateTransport({
20440
20946
  permissionMode: mode
20441
20947
  });
20442
20948
  try {
20443
20949
  await context.transport.updatePermissionPolicy?.(updated.transport);
20444
20950
  } catch (error2) {
20445
- await context.configStore.save(previous);
20951
+ if (previousRaw.present) {
20952
+ await context.configStore.setRawValue(["transport", "permissionMode"], previousRaw.value);
20953
+ } else {
20954
+ await context.configStore.unsetRawValue(["transport", "permissionMode"]);
20955
+ }
20446
20956
  context.replaceConfig(previous);
20447
20957
  throw error2;
20448
20958
  }
@@ -20458,13 +20968,18 @@ async function handlePermissionAutoSet(context, policy) {
20458
20968
  return { text: p.noWritableConfig };
20459
20969
  }
20460
20970
  const previous = cloneAppConfig(context.config);
20971
+ const previousRaw = await context.configStore.getRawValue(["transport", "nonInteractivePermissions"]);
20461
20972
  const updated = await context.configStore.updateTransport({
20462
20973
  nonInteractivePermissions: policy
20463
20974
  });
20464
20975
  try {
20465
20976
  await context.transport.updatePermissionPolicy?.(updated.transport);
20466
20977
  } catch (error2) {
20467
- await context.configStore.save(previous);
20978
+ if (previousRaw.present) {
20979
+ await context.configStore.setRawValue(["transport", "nonInteractivePermissions"], previousRaw.value);
20980
+ } else {
20981
+ await context.configStore.unsetRawValue(["transport", "nonInteractivePermissions"]);
20982
+ }
20468
20983
  context.replaceConfig(previous);
20469
20984
  throw error2;
20470
20985
  }
@@ -20514,99 +21029,83 @@ async function handleConfigSet(context, path14, rawValue) {
20514
21029
  if (!context.config || !context.configStore) {
20515
21030
  return { text: c.noWritableConfig };
20516
21031
  }
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 };
21032
+ const plan = planSupportedConfigUpdate(context.config, path14, rawValue);
21033
+ if ("error" in plan) {
21034
+ return { text: plan.error };
20522
21035
  }
20523
- await context.configStore.save(updated);
21036
+ const previousConfig = cloneAppConfig(context.config);
21037
+ const previousRaw = await context.configStore.getRawValue(plan.rawPath);
21038
+ const updated = await context.configStore.setRawValue(plan.rawPath, plan.value);
20524
21039
  if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
20525
21040
  try {
20526
21041
  await context.transport.updatePermissionPolicy?.(updated.transport);
20527
21042
  } catch (error2) {
20528
- await context.configStore.save(previous);
20529
- context.replaceConfig(previous);
21043
+ if (previousRaw.present) {
21044
+ await context.configStore.setRawValue(plan.rawPath, previousRaw.value);
21045
+ } else {
21046
+ await context.configStore.unsetRawValue(plan.rawPath);
21047
+ }
21048
+ context.replaceConfig(previousConfig);
20530
21049
  throw error2;
20531
21050
  }
20532
21051
  }
20533
21052
  context.replaceConfig(updated);
20534
- return { text: c.updated(path14, result.renderedValue) };
21053
+ return { text: c.updated(path14, plan.renderedValue) };
20535
21054
  }
20536
- function applySupportedConfigUpdate(config4, path14, rawValue) {
21055
+ function planSupportedConfigUpdate(config4, path14, rawValue) {
20537
21056
  const c = t().config;
20538
21057
  switch (path14) {
20539
21058
  case "language": {
20540
21059
  if (!isLocale(rawValue))
20541
21060
  return { error: c.languageInvalid };
20542
- config4.language = rawValue;
20543
- return { renderedValue: rawValue };
21061
+ return { rawPath: ["language"], value: rawValue, renderedValue: rawValue };
20544
21062
  }
20545
21063
  case "transport.type": {
20546
21064
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
20547
21065
  if (!parsed)
20548
21066
  return { error: c.transportTypeInvalid };
20549
- config4.transport.type = parsed;
20550
- return { renderedValue: parsed };
21067
+ return { rawPath: ["transport", "type"], value: parsed, renderedValue: parsed };
20551
21068
  }
20552
21069
  case "transport.command":
20553
21070
  if (!rawValue.trim())
20554
21071
  return { error: c.transportCommandEmpty };
20555
- config4.transport.command = rawValue;
20556
- return { renderedValue: rawValue };
21072
+ return { rawPath: ["transport", "command"], value: rawValue, renderedValue: rawValue };
20557
21073
  case "transport.sessionInitTimeoutMs": {
20558
21074
  const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
20559
21075
  if ("error" in parsed)
20560
21076
  return parsed;
20561
- config4.transport.sessionInitTimeoutMs = parsed.value;
20562
- return { renderedValue: String(parsed.value) };
21077
+ return { rawPath: ["transport", "sessionInitTimeoutMs"], value: parsed.value, renderedValue: String(parsed.value) };
20563
21078
  }
20564
21079
  case "transport.permissionMode": {
20565
21080
  const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
20566
21081
  if (!parsed)
20567
21082
  return { error: c.transportPermissionModeInvalid };
20568
- config4.transport.permissionMode = parsed;
20569
- return { renderedValue: parsed };
21083
+ return { rawPath: ["transport", "permissionMode"], value: parsed, renderedValue: parsed };
20570
21084
  }
20571
21085
  case "transport.nonInteractivePermissions": {
20572
21086
  const parsed = parseEnum(rawValue, ["deny", "fail"]);
20573
21087
  if (!parsed)
20574
21088
  return { error: c.transportNonInteractiveInvalid };
20575
- config4.transport.nonInteractivePermissions = parsed;
20576
- return { renderedValue: parsed };
21089
+ return { rawPath: ["transport", "nonInteractivePermissions"], value: parsed, renderedValue: parsed };
20577
21090
  }
20578
21091
  case "transport.permissionPolicy":
20579
21092
  if (!rawValue.trim())
20580
21093
  return { error: c.transportPermissionPolicyEmpty };
20581
- config4.transport.permissionPolicy = rawValue;
20582
- return { renderedValue: rawValue };
21094
+ return { rawPath: ["transport", "permissionPolicy"], value: rawValue, renderedValue: rawValue };
20583
21095
  case "logging.level": {
20584
21096
  const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
20585
21097
  if (!parsed)
20586
21098
  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) };
21099
+ return { rawPath: ["logging", "level"], value: parsed, renderedValue: parsed };
20603
21100
  }
21101
+ case "logging.maxSizeBytes":
21102
+ case "logging.maxFiles":
20604
21103
  case "logging.retentionDays": {
20605
- const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
21104
+ const field = path14.slice("logging.".length);
21105
+ const parsed = parsePositiveNumber(rawValue, path14);
20606
21106
  if ("error" in parsed)
20607
21107
  return parsed;
20608
- config4.logging.retentionDays = parsed.value;
20609
- return { renderedValue: String(parsed.value) };
21108
+ return { rawPath: ["logging", field], value: parsed.value, renderedValue: String(parsed.value) };
20610
21109
  }
20611
21110
  case "channel.type":
20612
21111
  return { error: c.channelTypeDisabled };
@@ -20614,15 +21113,15 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20614
21113
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20615
21114
  if (!parsed)
20616
21115
  return { error: c.channelReplyModeInvalid };
20617
- config4.channel.replyMode = parsed;
20618
- return { renderedValue: parsed };
21116
+ return { rawPath: ["channel", "replyMode"], value: parsed, renderedValue: parsed };
20619
21117
  }
20620
21118
  case "wechat.replyMode": {
20621
21119
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20622
21120
  if (!parsed)
20623
21121
  return { error: c.wechatReplyModeInvalid };
20624
- config4.channel.replyMode = parsed;
20625
21122
  return {
21123
+ rawPath: ["channel", "replyMode"],
21124
+ value: parsed,
20626
21125
  renderedValue: c.wechatReplyModeMapped(parsed)
20627
21126
  };
20628
21127
  }
@@ -20630,42 +21129,30 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20630
21129
  const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
20631
21130
  if (agentMatch) {
20632
21131
  const [, name, field] = agentMatch;
20633
- if (!name || !field) {
21132
+ if (!name || !field || isPrototypePollutingKey(name)) {
20634
21133
  return { error: c.pathNotSupported(path14) };
20635
21134
  }
20636
- const agent3 = config4.agents[name];
20637
- if (!agent3) {
21135
+ if (!Object.hasOwn(config4.agents, name)) {
20638
21136
  return { error: c.agentNotFound(name) };
20639
21137
  }
20640
21138
  if (!rawValue.trim()) {
20641
21139
  return { error: c.fieldEmpty(path14) };
20642
21140
  }
20643
- if (field === "driver") {
20644
- agent3.driver = rawValue;
20645
- } else {
20646
- agent3.command = rawValue;
20647
- }
20648
- return { renderedValue: rawValue };
21141
+ return { rawPath: ["agents", name, field], value: rawValue, renderedValue: rawValue };
20649
21142
  }
20650
21143
  const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
20651
21144
  if (workspaceMatch) {
20652
21145
  const [, name, field] = workspaceMatch;
20653
- if (!name || !field) {
21146
+ if (!name || !field || isPrototypePollutingKey(name)) {
20654
21147
  return { error: c.pathNotSupported(path14) };
20655
21148
  }
20656
- const workspace3 = config4.workspaces[name];
20657
- if (!workspace3) {
21149
+ if (!Object.hasOwn(config4.workspaces, name)) {
20658
21150
  return { error: c.workspaceNotFound(name) };
20659
21151
  }
20660
21152
  if (!rawValue.trim()) {
20661
21153
  return { error: c.fieldEmpty(path14) };
20662
21154
  }
20663
- if (field === "cwd") {
20664
- workspace3.cwd = rawValue;
20665
- } else {
20666
- workspace3.description = rawValue;
20667
- }
20668
- return { renderedValue: rawValue };
21155
+ return { rawPath: ["workspaces", name, field], value: rawValue, renderedValue: rawValue };
20669
21156
  }
20670
21157
  const channelMatch = path14.match(/^channels\.([^.]+)\.replyMode$/);
20671
21158
  if (channelMatch) {
@@ -20681,11 +21168,21 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20681
21168
  if (!parsed) {
20682
21169
  return { error: c.channelRuntimeReplyModeInvalid(id) };
20683
21170
  }
20684
- channel.replyMode = parsed;
20685
- return { renderedValue: parsed };
21171
+ return {
21172
+ rawPath: [
21173
+ "channels",
21174
+ { id, createWith: { id: channel.id, type: channel.type, enabled: channel.enabled } },
21175
+ "replyMode"
21176
+ ],
21177
+ value: parsed,
21178
+ renderedValue: parsed
21179
+ };
20686
21180
  }
20687
21181
  return { error: c.pathNotSupported(path14) };
20688
21182
  }
21183
+ function isPrototypePollutingKey(key) {
21184
+ return key === "__proto__" || key === "constructor" || key === "prototype";
21185
+ }
20689
21186
  function parseEnum(value, allowed) {
20690
21187
  return allowed.includes(value) ? value : null;
20691
21188
  }
@@ -21227,6 +21724,10 @@ async function handleSessions(context, chatKey) {
21227
21724
  async function handleSessionNew(context, chatKey, alias, agent3, workspace3) {
21228
21725
  const channelId = getChannelIdFromChatKey(chatKey);
21229
21726
  const internalAlias = scopeDisplayAliasToInternal(channelId, alias);
21727
+ const existing = context.sessions.getResolvedSessionByInternalAlias(internalAlias);
21728
+ if (existing) {
21729
+ return { text: t().session.sessionAlreadyExists(alias, existing.agent, existing.workspace) };
21730
+ }
21230
21731
  const session3 = context.lifecycle.resolveSession(internalAlias, agent3, workspace3, `${workspace3}:${internalAlias}`);
21231
21732
  const releaseTransportReservation = await context.lifecycle.reserveTransportSession(session3.transportSession);
21232
21733
  try {
@@ -21497,7 +21998,9 @@ async function handleSessionRemove(context, chatKey, alias) {
21497
21998
  }
21498
21999
  }
21499
22000
  const sharedAliasCount = context.sessions.countAliasesSharingTransport(session3.transportSession, internalAlias);
22001
+ const wasCurrentInThisChat = context.sessions.peekCurrentSessionAlias(chatKey) === internalAlias;
21500
22002
  const { wasActive } = await context.sessions.removeSession(internalAlias);
22003
+ const promotedAlias = wasCurrentInThisChat ? context.sessions.peekCurrentSessionAlias(chatKey) || undefined : undefined;
21501
22004
  let orchestrationPurgeWarning;
21502
22005
  if (context.orchestration) {
21503
22006
  try {
@@ -21535,7 +22038,7 @@ async function handleSessionRemove(context, chatKey, alias) {
21535
22038
  const s = t().session;
21536
22039
  const lines = [s.sessionRemoved(alias)];
21537
22040
  if (wasActive) {
21538
- lines.push(s.sessionRemovedWasActive);
22041
+ lines.push(promotedAlias ? s.sessionRemovedWasActivePromoted(toDisplaySessionAlias(promotedAlias)) : s.sessionRemovedWasActive);
21539
22042
  }
21540
22043
  if (!shouldTeardownTransport) {
21541
22044
  lines.push(s.sessionTransportShared(session3.transportSession, sharedAliasCount));
@@ -22363,7 +22866,7 @@ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {})
22363
22866
  name = allocateWorkspaceName(base, context.config.workspaces);
22364
22867
  notice = w.nameSanitized(workspaceName, name);
22365
22868
  }
22366
- const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
22869
+ const updated = await context.configStore.upsertWorkspace(name, cwd);
22367
22870
  context.replaceConfig(updated);
22368
22871
  const savedLine = w.saved(name);
22369
22872
  return { text: notice ? `${notice}
@@ -22479,10 +22982,10 @@ function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValu
22479
22982
  if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
22480
22983
  return { ok: false, code: "missing_message" };
22481
22984
  const delta = executeAt.getTime() - now.getTime();
22985
+ if (isNaN(delta) || delta > LATER_MAX_DELAY_MS)
22986
+ return { ok: false, code: "out_of_range" };
22482
22987
  if (delta < LATER_MIN_DELAY_MS)
22483
22988
  return { ok: false, code: "too_soon" };
22484
- if (delta > LATER_MAX_DELAY_MS)
22485
- return { ok: false, code: "out_of_range" };
22486
22989
  return { ok: true, executeAt, messageStartIndex };
22487
22990
  }
22488
22991
  var WEEKDAYS, ZH_MIN = "分钟", ZH_HOUR = "小时", ZH_DAY_UNIT = "天", ZH_TODAY = "今天", ZH_TOMORROW = "明天", ZH_DAY_AFTER = "后天", ZH_AFTER = "后", ZH_RELATIVE_RE;
@@ -22558,16 +23061,17 @@ function laterHelp() {
22558
23061
  function handleLaterHelp() {
22559
23062
  return { text: renderLaterHelp() };
22560
23063
  }
22561
- async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
23064
+ async function handleLaterCreate(tokens, tails, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
22562
23065
  const l = t().later;
22563
- let rest = tokens;
23066
+ let restStart = 0;
22564
23067
  const seenFlags = new Set;
22565
23068
  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);
23069
+ while (restStart < tokens.length && (tokens[restStart] === "--bind" || tokens[restStart] === "--temp")) {
23070
+ seenFlags.add(tokens[restStart] ?? "");
23071
+ flagMode = tokens[restStart] === "--bind" ? "bound" : "temp";
23072
+ restStart += 1;
22570
23073
  }
23074
+ const rest = tokens.slice(restStart);
22571
23075
  if (seenFlags.size > 1) {
22572
23076
  return { text: l.bindAndTempMutuallyExclusive };
22573
23077
  }
@@ -22588,7 +23092,7 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22588
23092
  if (!result.ok) {
22589
23093
  return { text: renderTimeParseError(result.code, result.value) };
22590
23094
  }
22591
- const message = rest.slice(result.messageStartIndex).join(" ").trim();
23095
+ const message = (tails[restStart + result.messageStartIndex] ?? "").trim();
22592
23096
  if (message.startsWith("/")) {
22593
23097
  return {
22594
23098
  text: [
@@ -22612,12 +23116,12 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22612
23116
  });
22613
23117
  return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSession.alias)) };
22614
23118
  }
22615
- function handleLaterList(scheduled) {
22616
- const tasks = scheduled.listPending();
23119
+ function handleLaterList(scheduled, chatKey) {
23120
+ const tasks = scheduled.listPending(chatKey);
22617
23121
  return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
22618
23122
  }
22619
- async function handleLaterCancel(id, scheduled) {
22620
- const ok = await scheduled.cancelPending(id);
23123
+ async function handleLaterCancel(id, scheduled, chatKey) {
23124
+ const ok = await scheduled.cancelPending(id, chatKey);
22621
23125
  const displayId = id.replace(/^#/, "").toLowerCase();
22622
23126
  if (ok) {
22623
23127
  return { text: t().later.cancelSuccess(displayId) };
@@ -23454,7 +23958,7 @@ var init_session_recovery_handler = __esm(() => {
23454
23958
  // src/recovery/auto-install-optional-dep.ts
23455
23959
  import { spawn as spawn5 } from "node:child_process";
23456
23960
  import { createWriteStream } from "node:fs";
23457
- import { mkdir as mkdir10 } from "node:fs/promises";
23961
+ import { mkdir as mkdir8 } from "node:fs/promises";
23458
23962
  import { homedir as homedir6 } from "node:os";
23459
23963
  import { join as join14 } from "node:path";
23460
23964
  async function autoInstallOptionalDep(pkg, parentPackages, options = {}) {
@@ -23572,7 +24076,7 @@ ${err.message}`, reason: "spawn" });
23572
24076
  });
23573
24077
  }, defaultLogSink = async () => {
23574
24078
  const dir = join14(coreHomeDir(homedir6()), "logs");
23575
- await mkdir10(dir, { recursive: true });
24079
+ await mkdir8(dir, { recursive: true });
23576
24080
  const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
23577
24081
  const path14 = join14(dir, `auto-install-${timestamp}.log`);
23578
24082
  const stream = createWriteStream(path14, { flags: "a" });
@@ -23601,7 +24105,7 @@ import { spawn as spawn6 } from "node:child_process";
23601
24105
  import { createRequire as createRequire3 } from "node:module";
23602
24106
  import { access as access3 } from "node:fs/promises";
23603
24107
  import { homedir as homedir7 } from "node:os";
23604
- import { dirname as dirname12, join as join15 } from "node:path";
24108
+ import { dirname as dirname10, join as join15 } from "node:path";
23605
24109
  function deriveParentPackageName(platformPackage) {
23606
24110
  return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
23607
24111
  }
@@ -23674,7 +24178,7 @@ function defaultResolveFromCwd(name, cwd) {
23674
24178
  const pkgJson = require2.resolve(`${name}/package.json`, {
23675
24179
  paths: [cwd, ...require2.resolve.paths(name) ?? []]
23676
24180
  });
23677
- return dirname12(pkgJson);
24181
+ return dirname10(pkgJson);
23678
24182
  } catch {
23679
24183
  return null;
23680
24184
  }
@@ -23809,11 +24313,11 @@ async function handleSessionResetCommand(context, ops, chatKey) {
23809
24313
  chatKey,
23810
24314
  native: wasNative && Boolean(freshAgentSessionId)
23811
24315
  });
23812
- if (wasNative && context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
24316
+ if (context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
23813
24317
  try {
23814
24318
  await context.transport.removeSession(previous);
23815
24319
  } catch (error2) {
23816
- await context.logger.info("session.reset.close_previous_failed", "failed to close previous native session after reset", {
24320
+ await context.logger.info("session.reset.close_previous_failed", "failed to close previous session after reset", {
23817
24321
  transportSession: previous.transportSession,
23818
24322
  error: error2 instanceof Error ? error2.message : String(error2)
23819
24323
  });
@@ -23875,9 +24379,20 @@ class CommandRouter {
23875
24379
  chatKey,
23876
24380
  kind: command.kind
23877
24381
  });
24382
+ await this.refreshConfigFromStore();
24383
+ perfSpan?.mark("router.config_refreshed");
24384
+ metadata = withEffectiveOwner(metadata, this.config);
23878
24385
  const access4 = authorizeCommandForChat(command, metadata);
23879
24386
  perfSpan?.mark("router.authorized", { decision: access4.allowed ? "allow" : "deny" });
23880
24387
  if (!access4.allowed) {
24388
+ if (access4.reason === "chat-type-missing") {
24389
+ await this.logger.error("channel.chat_type_missing", "channel turn carried no chatType; denying privileged command (channel metadata contract violation)", {
24390
+ chatKey,
24391
+ kind: command.kind,
24392
+ channel: metadata?.channel,
24393
+ senderId: metadata?.senderId
24394
+ });
24395
+ }
23881
24396
  await this.logger.info("command.blocked", "blocked command by chat policy", {
23882
24397
  chatKey,
23883
24398
  kind: command.kind,
@@ -23885,10 +24400,8 @@ class CommandRouter {
23885
24400
  channel: metadata?.channel,
23886
24401
  senderId: metadata?.senderId
23887
24402
  });
23888
- return { text: renderCommandAccessDenied(command) };
24403
+ return { text: renderCommandAccessDenied(command, access4.reason) };
23889
24404
  }
23890
- await this.refreshConfigFromStore();
23891
- perfSpan?.mark("router.config_refreshed");
23892
24405
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
23893
24406
  switch (command.kind) {
23894
24407
  case "invalid":
@@ -23990,7 +24503,7 @@ class CommandRouter {
23990
24503
  case "later.list":
23991
24504
  if (!this.scheduled)
23992
24505
  return { text: t().later.serviceNotEnabled };
23993
- return handleLaterList(this.scheduled);
24506
+ return handleLaterList(this.scheduled, chatKey);
23994
24507
  case "later.create": {
23995
24508
  if (!this.scheduled)
23996
24509
  return { text: t().later.serviceNotEnabled };
@@ -23998,12 +24511,12 @@ class CommandRouter {
23998
24511
  return { text: renderLaterUnsupportedChannel() };
23999
24512
  }
24000
24513
  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);
24514
+ 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
24515
  }
24003
24516
  case "later.cancel":
24004
24517
  if (!this.scheduled)
24005
24518
  return { text: t().later.serviceNotEnabled };
24006
- return await handleLaterCancel(command.id, this.scheduled);
24519
+ return await handleLaterCancel(command.id, this.scheduled, chatKey);
24007
24520
  case "prompt": {
24008
24521
  const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
24009
24522
  if (metadata?.scheduledSessionDescriptor) {
@@ -24507,7 +25020,7 @@ var init_console_agent = __esm(() => {
24507
25020
  });
24508
25021
 
24509
25022
  // src/orchestration/orchestration-server.ts
24510
- import { rm as rm8 } from "node:fs/promises";
25023
+ import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
24511
25024
  import { createServer } from "node:net";
24512
25025
 
24513
25026
  class OrchestrationServer {
@@ -24554,6 +25067,7 @@ class OrchestrationServer {
24554
25067
  });
24555
25068
  });
24556
25069
  await this.listenWithUnixSocketRecovery();
25070
+ await this.hardenUnixSocketPermissions();
24557
25071
  this.started = true;
24558
25072
  }
24559
25073
  async stop() {
@@ -24742,17 +25256,19 @@ class OrchestrationServer {
24742
25256
  return task;
24743
25257
  }
24744
25258
  parseRequestDelegateRpcInput(params) {
24745
- requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId"], "params");
25259
+ requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId", "parallel"], "params");
24746
25260
  const cwd = requireOptionalString(params, "cwd");
24747
25261
  const role = requireOptionalString(params, "role");
24748
25262
  const groupId = requireOptionalString(params, "groupId");
25263
+ const parallel = requireOptionalBoolean(params, "parallel");
24749
25264
  return {
24750
25265
  sourceHandle: requireString(params, "sourceHandle"),
24751
25266
  targetAgent: requireString(params, "targetAgent"),
24752
25267
  task: requireString(params, "task"),
24753
25268
  ...cwd !== undefined ? { cwd } : {},
24754
25269
  ...role !== undefined ? { role } : {},
24755
- ...groupId !== undefined ? { groupId } : {}
25270
+ ...groupId !== undefined ? { groupId } : {},
25271
+ ...parallel !== undefined ? { parallel } : {}
24756
25272
  };
24757
25273
  }
24758
25274
  parseTaskListFilter(params) {
@@ -24835,6 +25351,17 @@ class OrchestrationServer {
24835
25351
  whatIsNeeded: requireString(params, "whatIsNeeded")
24836
25352
  };
24837
25353
  }
25354
+ async hardenUnixSocketPermissions() {
25355
+ if (this.endpoint.kind !== "unix") {
25356
+ return;
25357
+ }
25358
+ const chmodFile = this.deps.chmodFile ?? chmod4;
25359
+ try {
25360
+ await chmodFile(this.endpoint.path, 384);
25361
+ } catch (error2) {
25362
+ this.deps.onSocketHardenError?.(error2);
25363
+ }
25364
+ }
24838
25365
  async cleanupEndpoint() {
24839
25366
  if (this.endpoint.kind !== "unix") {
24840
25367
  return;
@@ -28316,18 +28843,39 @@ class ScheduledTaskScheduler {
28316
28843
  return;
28317
28844
  this.ticking = true;
28318
28845
  try {
28319
- const dueTasks = await this.service.claimDueTasks();
28846
+ let dueTasks;
28847
+ try {
28848
+ dueTasks = await this.service.claimDueTasks();
28849
+ } catch (claimError) {
28850
+ await this.logger?.error("scheduled.claim.failed", "claimDueTasks threw; skipping tick", { message: claimError instanceof Error ? claimError.message : String(claimError) });
28851
+ return;
28852
+ }
28320
28853
  for (const task of dueTasks) {
28321
28854
  try {
28322
28855
  await this.dispatchWithTimeout(task);
28323
- await this.service.markExecuted(task.id);
28324
28856
  } catch (error2) {
28325
28857
  const message = error2 instanceof Error ? error2.message : String(error2);
28326
28858
  await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
28327
28859
  taskId: task.id,
28328
28860
  message
28329
28861
  });
28330
- await this.service.markFailed(task.id, error2);
28862
+ try {
28863
+ await this.service.markFailed(task.id, error2);
28864
+ } catch (markError) {
28865
+ await this.logger?.error("scheduled.dispatch.mark_failed", "markFailed threw; task state may be stale", {
28866
+ taskId: task.id,
28867
+ message: markError instanceof Error ? markError.message : String(markError)
28868
+ });
28869
+ }
28870
+ continue;
28871
+ }
28872
+ try {
28873
+ await this.service.markExecuted(task.id);
28874
+ } catch (markError) {
28875
+ await this.logger?.error("scheduled.dispatch.mark_executed_failed", "markExecuted threw after a successful dispatch; leaving task state for startup reconciliation", {
28876
+ taskId: task.id,
28877
+ message: markError instanceof Error ? markError.message : String(markError)
28878
+ });
28331
28879
  }
28332
28880
  }
28333
28881
  } finally {
@@ -28507,12 +29055,12 @@ var init_scheduled_route_create = __esm(() => {
28507
29055
 
28508
29056
  // src/scheduled/scheduled-route-manage.ts
28509
29057
  async function listScheduledTasksFromRoute(input, deps) {
28510
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
28511
- return deps.scheduled.listPending();
29058
+ const route = resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
29059
+ return deps.scheduled.listPending(route.chatKey);
28512
29060
  }
28513
29061
  async function cancelScheduledTaskFromRoute(input, deps) {
28514
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
28515
- const cancelled = await deps.scheduled.cancelPending(input.id);
29062
+ const route = resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
29063
+ const cancelled = await deps.scheduled.cancelPending(input.id, route.chatKey);
28516
29064
  return { id: normalizeId(input.id), cancelled };
28517
29065
  }
28518
29066
  function resolveOwnedCoordinatorRoute(coordinatorSession, state, label) {
@@ -28557,25 +29105,37 @@ class SessionService {
28557
29105
  const seen = new Set;
28558
29106
  const resolved = [];
28559
29107
  for (const session3 of Object.values(this.state.sessions)) {
28560
- if (seen.has(session3.transport_session)) {
29108
+ let candidate;
29109
+ try {
29110
+ candidate = this.toResolvedSession(session3);
29111
+ } catch {
28561
29112
  continue;
28562
29113
  }
28563
- seen.add(session3.transport_session);
28564
- try {
28565
- resolved.push(this.toResolvedSession(session3));
28566
- } catch {}
29114
+ const key = JSON.stringify([
29115
+ candidate.agent,
29116
+ candidate.agentCommand ?? null,
29117
+ candidate.cwd,
29118
+ candidate.transportSession
29119
+ ]);
29120
+ if (seen.has(key)) {
29121
+ continue;
29122
+ }
29123
+ seen.add(key);
29124
+ resolved.push(candidate);
28567
29125
  }
28568
29126
  return resolved;
28569
29127
  }
28570
29128
  resolveSession(alias, agent3, workspace3, transportSession) {
28571
29129
  this.validateSession(alias, agent3, workspace3);
29130
+ const existing = this.state.sessions[alias];
29131
+ const sameAgentExisting = existing && existing.agent === agent3 ? existing : undefined;
28572
29132
  return this.toResolvedSession({
28573
29133
  alias,
28574
29134
  agent: agent3,
28575
29135
  workspace: workspace3,
28576
29136
  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(),
29137
+ transport_agent_command: sameAgentExisting?.transport_agent_command,
29138
+ created_at: existing?.created_at ?? new Date().toISOString(),
28579
29139
  last_used_at: new Date().toISOString()
28580
29140
  });
28581
29141
  }
@@ -28647,10 +29207,13 @@ class SessionService {
28647
29207
  const previousCurrent = prevCtx?.current_session;
28648
29208
  const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
28649
29209
  session3.last_used_at = new Date().toISOString();
28650
- this.state.chat_contexts[chatKey] = {
28651
- current_session: internalAlias,
28652
- ...carriedPrevious ? { previous_session: carriedPrevious } : {}
28653
- };
29210
+ const nextCtx = { ...prevCtx, current_session: internalAlias };
29211
+ if (carriedPrevious) {
29212
+ nextCtx.previous_session = carriedPrevious;
29213
+ } else {
29214
+ delete nextCtx.previous_session;
29215
+ }
29216
+ this.state.chat_contexts[chatKey] = nextCtx;
28654
29217
  await this.persist();
28655
29218
  return {
28656
29219
  alias: toDisplaySessionAlias(session3.alias),
@@ -28677,10 +29240,13 @@ class SessionService {
28677
29240
  }
28678
29241
  const currentInternal = ctx?.current_session;
28679
29242
  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
- };
29243
+ const nextCtx = { ...ctx, current_session: prevInternal };
29244
+ if (currentInternal && currentInternal !== prevInternal) {
29245
+ nextCtx.previous_session = currentInternal;
29246
+ } else {
29247
+ delete nextCtx.previous_session;
29248
+ }
29249
+ this.state.chat_contexts[chatKey] = nextCtx;
28684
29250
  await this.persist();
28685
29251
  return {
28686
29252
  alias: toDisplaySessionAlias(prevSession.alias),
@@ -28851,13 +29417,26 @@ class SessionService {
28851
29417
  const wasActive = Object.values(this.state.chat_contexts).some((ctx) => ctx.current_session === alias);
28852
29418
  delete this.state.sessions[alias];
28853
29419
  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
29420
  if (ctx.previous_session === alias) {
28859
29421
  delete ctx.previous_session;
28860
29422
  }
29423
+ if (ctx.current_session === alias) {
29424
+ if (ctx.previous_session) {
29425
+ ctx.current_session = ctx.previous_session;
29426
+ delete ctx.previous_session;
29427
+ } else {
29428
+ ctx.current_session = "";
29429
+ }
29430
+ }
29431
+ if (ctx.background_results && alias in ctx.background_results) {
29432
+ delete ctx.background_results[alias];
29433
+ if (Object.keys(ctx.background_results).length === 0) {
29434
+ delete ctx.background_results;
29435
+ }
29436
+ }
29437
+ if (!ctx.current_session && !ctx.previous_session && !ctx.background_results) {
29438
+ delete this.state.chat_contexts[chatKey];
29439
+ }
28861
29440
  }
28862
29441
  await this.persist();
28863
29442
  return { wasActive };
@@ -28971,6 +29550,7 @@ class SessionService {
28971
29550
  throw new Error(`transport session "${transportSession}" conflicts with an external coordinator`);
28972
29551
  }
28973
29552
  const existingSession = this.state.sessions[alias];
29553
+ const sameAgentExisting = existingSession && existingSession.agent === agent3 ? existingSession : undefined;
28974
29554
  const now = new Date(this.now()).toISOString();
28975
29555
  const normalizedTransportAgentCommand = transportAgentCommand?.trim();
28976
29556
  const session3 = {
@@ -28983,9 +29563,9 @@ class SessionService {
28983
29563
  agent_session_title: native?.title ?? undefined,
28984
29564
  agent_session_updated_at: native?.updatedAt,
28985
29565
  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,
29566
+ ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : sameAgentExisting?.transport_agent_command ? { transport_agent_command: sameAgentExisting.transport_agent_command } : {},
29567
+ mode_id: sameAgentExisting?.mode_id,
29568
+ reply_mode: sameAgentExisting?.reply_mode,
28989
29569
  created_at: existingSession?.created_at ?? now,
28990
29570
  last_used_at: now
28991
29571
  };
@@ -29431,15 +30011,15 @@ class AcpxBridgeClient {
29431
30011
  onEvent
29432
30012
  });
29433
30013
  try {
29434
- const didWrite = this.writeLine(encodeBridgeRequest({
30014
+ this.writeLine(encodeBridgeRequest({
29435
30015
  id,
29436
30016
  method,
29437
30017
  params
29438
- }));
29439
- if (didWrite === false) {
29440
- this.pending.delete(id);
29441
- reject(new Error("bridge write buffer is full"));
29442
- }
30018
+ }), (error2) => {
30019
+ if (error2 && this.pending.delete(id)) {
30020
+ reject(error2);
30021
+ }
30022
+ });
29443
30023
  } catch (error2) {
29444
30024
  this.pending.delete(id);
29445
30025
  reject(error2);
@@ -29519,6 +30099,17 @@ class AcpxBridgeClient {
29519
30099
  }
29520
30100
  }
29521
30101
  }
30102
+ function buildBridgeSpawnEnv(options = {}) {
30103
+ return {
30104
+ XACPX_LANG: getLocale(),
30105
+ XACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
30106
+ XACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
30107
+ XACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
30108
+ ...typeof options.permissionPolicy === "string" && options.permissionPolicy.trim().length > 0 ? { XACPX_BRIDGE_PERMISSION_POLICY: options.permissionPolicy } : {},
30109
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { XACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {},
30110
+ ...typeof options.sessionInitTimeoutMs === "number" && Number.isFinite(options.sessionInitTimeoutMs) && options.sessionInitTimeoutMs > 0 ? { XACPX_BRIDGE_SESSION_INIT_TIMEOUT_MS: String(options.sessionInitTimeoutMs) } : {}
30111
+ };
30112
+ }
29522
30113
  function buildBridgeSpawnSpec(options) {
29523
30114
  if (options.execPath.endsWith("bun")) {
29524
30115
  return {
@@ -29541,15 +30132,17 @@ async function spawnAcpxBridgeClient(options = {}) {
29541
30132
  cwd: options.cwd ?? process.cwd(),
29542
30133
  env: {
29543
30134
  ...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) } : {}
30135
+ ...buildBridgeSpawnEnv(options)
29549
30136
  },
29550
30137
  stdio: ["pipe", "pipe", "inherit"]
29551
30138
  });
29552
- const client = new AcpxBridgeClient((line) => child.stdin.write(line));
30139
+ const client = manageBridgeChild(child);
30140
+ await client.waitUntilReady();
30141
+ return client;
30142
+ }
30143
+ function manageBridgeChild(child) {
30144
+ const client = new AcpxBridgeClient((line, onWriteError) => child.stdin.write(line, onWriteError));
30145
+ child.stdin.on("error", () => {});
29553
30146
  const output = createInterface({
29554
30147
  input: child.stdout,
29555
30148
  crlfDelay: Infinity
@@ -29575,7 +30168,6 @@ async function spawnAcpxBridgeClient(options = {}) {
29575
30168
  await terminateProcessTree(child.pid ?? 0, { detachedProcessGroup: false });
29576
30169
  }
29577
30170
  };
29578
- await client.waitUntilReady();
29579
30171
  return client;
29580
30172
  }
29581
30173
  function awaitable(executor) {
@@ -30417,19 +31009,19 @@ var init_streaming_prompt = __esm(() => {
30417
31009
 
30418
31010
  // src/transport/acpx-cli/node-pty-helper.ts
30419
31011
  import { chmod as chmodFs } from "node:fs/promises";
30420
- import { dirname as dirname13, join as join16 } from "node:path";
31012
+ import { dirname as dirname11, join as join16 } from "node:path";
30421
31013
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
30422
31014
  if (platform === "win32") {
30423
31015
  return null;
30424
31016
  }
30425
- return join16(dirname13(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
31017
+ return join16(dirname11(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
30426
31018
  }
30427
- async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
31019
+ async function ensureNodePtyHelperExecutable(helperPath, chmod5 = chmodFs) {
30428
31020
  if (!helperPath) {
30429
31021
  return;
30430
31022
  }
30431
31023
  try {
30432
- await chmod3(helperPath, 493);
31024
+ await chmod5(helperPath, 493);
30433
31025
  } catch (error2) {
30434
31026
  if (error2.code === "ENOENT") {
30435
31027
  return;
@@ -30442,7 +31034,7 @@ var init_node_pty_helper = () => {};
30442
31034
  // src/transport/acpx-queue-owner-launcher.ts
30443
31035
  import { createHash as createHash3 } from "node:crypto";
30444
31036
  import { spawn as spawn8 } from "node:child_process";
30445
- import { readFile as readFile12, unlink } from "node:fs/promises";
31037
+ import { readFile as readFile13, unlink } from "node:fs/promises";
30446
31038
  import { homedir as homedir8 } from "node:os";
30447
31039
  import { join as join17 } from "node:path";
30448
31040
  function buildXacpxMcpServerSpec(input) {
@@ -30605,7 +31197,7 @@ async function terminateAcpxQueueOwner(sessionId) {
30605
31197
  const lockPath = queueLockFilePath(sessionId);
30606
31198
  let owner;
30607
31199
  try {
30608
- owner = JSON.parse(await readFile12(lockPath, "utf8"));
31200
+ owner = JSON.parse(await readFile13(lockPath, "utf8"));
30609
31201
  } catch {
30610
31202
  return;
30611
31203
  }
@@ -31264,10 +31856,11 @@ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
31264
31856
  const timeoutMs = deps.timeoutMs ?? 5000;
31265
31857
  const seen = new Set;
31266
31858
  const unique = targets.filter((target) => {
31267
- if (seen.has(target.transportSession)) {
31859
+ const key = JSON.stringify([target.agent, target.agentCommand ?? null, target.cwd, target.transportSession]);
31860
+ if (seen.has(key)) {
31268
31861
  return false;
31269
31862
  }
31270
- seen.add(target.transportSession);
31863
+ seen.add(key);
31271
31864
  return true;
31272
31865
  });
31273
31866
  let terminated = 0;
@@ -31419,10 +32012,21 @@ class MessageChannelRegistry {
31419
32012
  throw new Error("all channels failed to start");
31420
32013
  }
31421
32014
  }
31422
- stopAll() {
32015
+ async stopAll() {
32016
+ let firstError;
31423
32017
  for (const channel of this.channels.values()) {
31424
- channel.logout();
32018
+ try {
32019
+ if (channel.stop) {
32020
+ await channel.stop();
32021
+ } else {
32022
+ channel.logout();
32023
+ }
32024
+ } catch (error2) {
32025
+ firstError ??= error2;
32026
+ }
31425
32027
  }
32028
+ if (firstError !== undefined)
32029
+ throw firstError;
31426
32030
  }
31427
32031
  getByChatKey(chatKey) {
31428
32032
  return this.channels.get(getChannelIdFromChatKey(chatKey)) ?? null;
@@ -31670,7 +32274,7 @@ __export(exports_main, {
31670
32274
  });
31671
32275
  import { randomUUID as randomUUID3 } from "node:crypto";
31672
32276
  import { homedir as homedir9 } from "node:os";
31673
- import { dirname as dirname14, join as join18 } from "node:path";
32277
+ import { dirname as dirname12, join as join18 } from "node:path";
31674
32278
  import { fileURLToPath as fileURLToPath5 } from "node:url";
31675
32279
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
31676
32280
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -31742,6 +32346,35 @@ async function buildApp(paths, deps = {}) {
31742
32346
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config4.transport.command });
31743
32347
  const stateStore = new StateStore(paths.statePath);
31744
32348
  const state = await stateStore.load();
32349
+ const stateLoadReport = stateStore.lastLoadReport;
32350
+ if (stateLoadReport) {
32351
+ for (const record3 of stateLoadReport.dropped) {
32352
+ await logger2.error("state.record_quarantined", "dropped malformed state.json record", {
32353
+ statePath: paths.statePath,
32354
+ section: record3.section,
32355
+ key: record3.key,
32356
+ reason: record3.reason
32357
+ });
32358
+ }
32359
+ if (stateLoadReport.corruptPath) {
32360
+ await logger2.error("state.file_corrupt", "state.json was unreadable; renamed aside and starting empty", {
32361
+ statePath: paths.statePath,
32362
+ corruptPath: stateLoadReport.corruptPath
32363
+ });
32364
+ }
32365
+ if (stateLoadReport.quarantinePath) {
32366
+ await logger2.error("state.file_quarantined", "original state.json backed up before dropping records", {
32367
+ statePath: paths.statePath,
32368
+ quarantinePath: stateLoadReport.quarantinePath
32369
+ });
32370
+ }
32371
+ if (stateLoadReport.backupError) {
32372
+ await logger2.error("state.quarantine_backup_failed", "failed to back up the original state.json", {
32373
+ statePath: paths.statePath,
32374
+ message: stateLoadReport.backupError
32375
+ });
32376
+ }
32377
+ }
31745
32378
  const stateMutex = new AsyncMutex;
31746
32379
  const debouncedStateStore = new DebouncedStateStore({
31747
32380
  delegate: stateStore,
@@ -31761,7 +32394,9 @@ async function buildApp(paths, deps = {}) {
31761
32394
  bridgeEntryPath: resolveBridgeEntryPath(),
31762
32395
  permissionMode: config4.transport.permissionMode,
31763
32396
  nonInteractivePermissions: config4.transport.nonInteractivePermissions,
31764
- ...typeof config4.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config4.transport.queueOwnerTtlSeconds } : {}
32397
+ ...typeof config4.transport.permissionPolicy === "string" ? { permissionPolicy: config4.transport.permissionPolicy } : {},
32398
+ ...typeof config4.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config4.transport.queueOwnerTtlSeconds } : {},
32399
+ ...typeof config4.transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: config4.transport.sessionInitTimeoutMs } : {}
31765
32400
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config4.transport, command: acpxCommand });
31766
32401
  const quota = new QuotaManager({
31767
32402
  onInbound: (chatKey) => {
@@ -32114,6 +32749,9 @@ async function buildApp(paths, deps = {}) {
32114
32749
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration3, config4, logger2, deps.channel ?? null);
32115
32750
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
32116
32751
  const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration3, {
32752
+ onSocketHardenError: (error2) => {
32753
+ 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) });
32754
+ },
32117
32755
  createScheduledTaskFromRoute: async (input) => await createScheduledTaskFromRoute(input, {
32118
32756
  state,
32119
32757
  config: config4,
@@ -32251,7 +32889,7 @@ async function main() {
32251
32889
  }
32252
32890
  }
32253
32891
  async function prepareChannelMedia(configPath, config4) {
32254
- const runtimeDir = join18(dirname14(configPath), "runtime");
32892
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32255
32893
  const mediaRootDir = join18(runtimeDir, "media");
32256
32894
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
32257
32895
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -32266,7 +32904,7 @@ function resolveRuntimePaths() {
32266
32904
  throw new Error("Unable to resolve the current user home directory");
32267
32905
  }
32268
32906
  const configPath = coreEnv("CONFIG") ?? join18(coreHomeDir(home), "config.json");
32269
- const runtimeDir = join18(dirname14(configPath), "runtime");
32907
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32270
32908
  return {
32271
32909
  configPath,
32272
32910
  statePath: coreEnv("STATE") ?? join18(coreHomeDir(home), "state.json"),
@@ -32281,12 +32919,12 @@ function resolveBridgeEntryPath() {
32281
32919
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
32282
32920
  }
32283
32921
  function resolveAppLogPath(configPath) {
32284
- const rootDir = dirname14(configPath);
32922
+ const rootDir = dirname12(configPath);
32285
32923
  const runtimeDir = join18(rootDir, "runtime");
32286
32924
  return join18(runtimeDir, "app.log");
32287
32925
  }
32288
32926
  function resolvePerfLogPath(configPath) {
32289
- const rootDir = dirname14(configPath);
32927
+ const rootDir = dirname12(configPath);
32290
32928
  const runtimeDir = join18(rootDir, "runtime");
32291
32929
  return join18(runtimeDir, "perf.log");
32292
32930
  }
@@ -32682,7 +33320,7 @@ var init_orchestration_health = __esm(() => {
32682
33320
  // src/doctor/checks/runtime-check.ts
32683
33321
  import { constants } from "node:fs";
32684
33322
  import { access as access4, stat as stat3 } from "node:fs/promises";
32685
- import { dirname as dirname15 } from "node:path";
33323
+ import { dirname as dirname13 } from "node:path";
32686
33324
  import { homedir as homedir11 } from "node:os";
32687
33325
  async function checkRuntime(options = {}) {
32688
33326
  const home = options.home ?? process.env.HOME ?? homedir11();
@@ -32783,7 +33421,7 @@ async function checkFileCreatable(label, path15, probe, platform) {
32783
33421
  detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
32784
33422
  };
32785
33423
  }
32786
- const parentCheck = await checkCreatableAncestorDirectory(dirname15(path15), probe, platform);
33424
+ const parentCheck = await checkCreatableAncestorDirectory(dirname13(path15), probe, platform);
32787
33425
  if (!parentCheck.ok) {
32788
33426
  return {
32789
33427
  ok: false,
@@ -32819,7 +33457,7 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
32819
33457
  blockingPath: path15
32820
33458
  };
32821
33459
  }
32822
- const parent = dirname15(path15);
33460
+ const parent = dirname13(path15);
32823
33461
  if (parent === path15) {
32824
33462
  return {
32825
33463
  ok: false,
@@ -33344,11 +33982,13 @@ async function defaultCheckOrchestrationHealth(deps) {
33344
33982
  }
33345
33983
  try {
33346
33984
  const store = new StateStore(deps.runtimePaths.statePath);
33347
- return await checkOrchestrationHealth({
33348
- loadState: () => store.load(),
33985
+ const inspection = await store.inspect();
33986
+ const result = await checkOrchestrationHealth({
33987
+ loadState: async () => inspection.state,
33349
33988
  now: () => new Date,
33350
33989
  heartbeatThresholdSeconds: config4.orchestration.progressHeartbeatSeconds
33351
33990
  });
33991
+ return applyStateInspectionReport(result, inspection.report, deps.runtimePaths.statePath);
33352
33992
  } catch (error2) {
33353
33993
  return {
33354
33994
  id: "orchestration",
@@ -33359,6 +33999,27 @@ async function defaultCheckOrchestrationHealth(deps) {
33359
33999
  };
33360
34000
  }
33361
34001
  }
34002
+ function applyStateInspectionReport(result, report, statePath) {
34003
+ if (!report) {
34004
+ return result;
34005
+ }
34006
+ const fileCorrupt = report.dropped.some((record3) => record3.section === "file");
34007
+ const details = [
34008
+ ...result.details ?? [],
34009
+ `state path: ${statePath}`,
34010
+ ...report.dropped.map((record3) => record3.section === "file" ? `state.json is unreadable: ${record3.reason}` : `invalid state record ${record3.section}["${record3.key}"]: ${record3.reason}`)
34011
+ ];
34012
+ return {
34013
+ ...result,
34014
+ severity: result.severity === "fail" ? "fail" : "warn",
34015
+ 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}`,
34016
+ details,
34017
+ suggestions: [
34018
+ ...result.suggestions ?? [],
34019
+ 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"
34020
+ ]
34021
+ };
34022
+ }
33362
34023
  function formatError9(error2) {
33363
34024
  return error2 instanceof Error ? error2.message : String(error2);
33364
34025
  }
@@ -33401,7 +34062,7 @@ var init_doctor2 = __esm(async () => {
33401
34062
  init_core_home();
33402
34063
  import { randomUUID as randomUUID4 } from "node:crypto";
33403
34064
  import { homedir as homedir13 } from "node:os";
33404
- import { dirname as dirname16, join as join20, sep } from "node:path";
34065
+ import { dirname as dirname14, join as join20, sep } from "node:path";
33405
34066
  import { fileURLToPath as fileURLToPath7 } from "node:url";
33406
34067
 
33407
34068
  // src/runtime/migrate-core-home.ts
@@ -33485,8 +34146,8 @@ init_daemon_files();
33485
34146
 
33486
34147
  // src/daemon/daemon-runtime.ts
33487
34148
  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";
34149
+ init_private_runtime_dir();
34150
+ import { rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
33490
34151
 
33491
34152
  class DaemonRuntime {
33492
34153
  paths;
@@ -33512,8 +34173,8 @@ class DaemonRuntime {
33512
34173
  stdout_log: this.paths.stdoutLog,
33513
34174
  stderr_log: this.paths.stderrLog
33514
34175
  };
33515
- await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
33516
- await writeFile3(this.paths.pidFile, `${this.options.pid}
34176
+ await ensurePrivateRuntimeDir(this.paths.runtimeDir);
34177
+ await writeFile2(this.paths.pidFile, `${this.options.pid}
33517
34178
  `);
33518
34179
  await this.statusStore.save(this.currentStatus);
33519
34180
  }
@@ -46188,7 +46849,7 @@ function buildXacpxMcpToolRegistry(input) {
46188
46849
  });
46189
46850
  tools.push({
46190
46851
  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.",
46852
+ 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
46853
  inputSchema: exports_external.object({}).strict(),
46193
46854
  handler: async () => await asToolResult(async () => {
46194
46855
  const tasks = await transport.scheduledList({ coordinatorSession });
@@ -46206,7 +46867,7 @@ function buildXacpxMcpToolRegistry(input) {
46206
46867
  });
46207
46868
  tools.push({
46208
46869
  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.",
46870
+ 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
46871
  inputSchema: exports_external.object({
46211
46872
  id: exports_external.string().min(1).describe("The scheduled task id, e.g. 'k8f2' (a leading # is allowed).")
46212
46873
  }).strict(),
@@ -47416,7 +48077,10 @@ async function maybeRunFirstUseOnboarding(input) {
47416
48077
  const agentExisted = Boolean(input.config.agents[agentName]);
47417
48078
  input.config.workspaces[workspaceName] = { cwd };
47418
48079
  input.config.agents[agentName] = template;
47419
- await input.saveConfig(input.config);
48080
+ await input.saveFirstRunConfig({
48081
+ workspace: { name: workspaceName, cwd },
48082
+ agent: { name: agentName, config: template }
48083
+ });
47420
48084
  const alias = `${workspaceName}:${agentName}`;
47421
48085
  input.deps.print(t().misc.onboardingCreatedWorkspace(workspaceName, alias));
47422
48086
  return {
@@ -47439,8 +48103,8 @@ function resolveTemplateChoice(answer, names) {
47439
48103
  // src/cli-update.ts
47440
48104
  init_plugin_home();
47441
48105
  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";
48106
+ import { readFile as readFile9 } from "node:fs/promises";
48107
+ import { dirname as dirname8, join as join11 } from "node:path";
47444
48108
  import { fileURLToPath as fileURLToPath3 } from "node:url";
47445
48109
 
47446
48110
  // src/plugins/package-manager.ts
@@ -47448,9 +48112,14 @@ init_plugin_home();
47448
48112
  import { spawn as spawn3 } from "node:child_process";
47449
48113
  import { rm as rm4 } from "node:fs/promises";
47450
48114
  import { join as join7 } from "node:path";
48115
+ function shellSpawnPlan(args) {
48116
+ const shell = process.platform === "win32";
48117
+ return { shell, args: shell ? args.map((arg) => `"${arg}"`) : args };
48118
+ }
47451
48119
  async function defaultRunCommand(command, args, options) {
47452
48120
  await new Promise((resolve, reject) => {
47453
- const child = spawn3(command, args, { cwd: options.cwd, stdio: "inherit" });
48121
+ const plan = shellSpawnPlan(args);
48122
+ const child = spawn3(command, plan.args, { cwd: options.cwd, stdio: "inherit", shell: plan.shell });
47454
48123
  child.on("error", reject);
47455
48124
  child.on("exit", (code) => {
47456
48125
  if (code === 0)
@@ -47462,7 +48131,8 @@ async function defaultRunCommand(command, args, options) {
47462
48131
  }
47463
48132
  async function silentRun(command, args, options) {
47464
48133
  await new Promise((resolve, reject) => {
47465
- const child = spawn3(command, args, { cwd: options.cwd, stdio: "ignore" });
48134
+ const plan = shellSpawnPlan(args);
48135
+ const child = spawn3(command, plan.args, { cwd: options.cwd, stdio: "ignore", shell: plan.shell });
47466
48136
  child.on("error", reject);
47467
48137
  child.on("exit", (code) => {
47468
48138
  if (code === 0)
@@ -47638,7 +48308,7 @@ async function handleUpdateCli(args, deps) {
47638
48308
  }
47639
48309
  if (selected.targets.some((target) => target.kind === "plugin")) {
47640
48310
  config4.plugins = updatedPlugins;
47641
- await deps.saveConfig(config4);
48311
+ await deps.savePlugins(updatedPlugins);
47642
48312
  }
47643
48313
  return 0;
47644
48314
  }
@@ -47748,9 +48418,10 @@ function compareSemver2(a, b) {
47748
48418
  return 0;
47749
48419
  return left.prerelease ? -1 : 1;
47750
48420
  }
48421
+ var spawnUsesShell = () => process.platform === "win32";
47751
48422
  async function runCapture(command, args) {
47752
48423
  return await new Promise((resolve, reject) => {
47753
- const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
48424
+ const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: spawnUsesShell() });
47754
48425
  let stdout2 = "";
47755
48426
  let stderr = "";
47756
48427
  child.stdout.setEncoding("utf8");
@@ -47767,7 +48438,7 @@ async function runCapture(command, args) {
47767
48438
  }
47768
48439
  async function runInherit(command, args) {
47769
48440
  await new Promise((resolve, reject) => {
47770
- const child = spawn4(command, args, { stdio: "inherit" });
48441
+ const child = spawn4(command, args, { stdio: "inherit", shell: spawnUsesShell() });
47771
48442
  child.on("error", reject);
47772
48443
  child.on("exit", (code) => {
47773
48444
  if (code === 0)
@@ -47779,10 +48450,10 @@ async function runInherit(command, args) {
47779
48450
  }
47780
48451
  async function readPackageName() {
47781
48452
  try {
47782
- const here = dirname10(fileURLToPath3(import.meta.url));
48453
+ const here = dirname8(fileURLToPath3(import.meta.url));
47783
48454
  for (const candidate of [join11(here, "..", "package.json"), join11(here, "..", "..", "package.json")]) {
47784
48455
  try {
47785
- const parsed = JSON.parse(await readFile8(candidate, "utf8"));
48456
+ const parsed = JSON.parse(await readFile9(candidate, "utf8"));
47786
48457
  if (typeof parsed.name === "string" && parsed.name.trim())
47787
48458
  return parsed.name.trim();
47788
48459
  } catch {}
@@ -48024,7 +48695,7 @@ async function addChannel(type, rawArgs, deps) {
48024
48695
  return 1;
48025
48696
  }
48026
48697
  config4.channels = [...config4.channels ?? [], candidate];
48027
- await deps.saveConfig(config4);
48698
+ await deps.saveChannels(config4.channels);
48028
48699
  deps.print(t().channelCli.channelAdded(type));
48029
48700
  for (const line of provider.renderSummary(candidate))
48030
48701
  deps.print(line);
@@ -48125,7 +48796,7 @@ async function removeChannel(type, rawArgs, deps) {
48125
48796
  return 1;
48126
48797
  }
48127
48798
  config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
48128
- await deps.saveConfig(config4);
48799
+ await deps.saveChannels(config4.channels);
48129
48800
  deps.print(t().channelCli.channelRemoved(channel.id));
48130
48801
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48131
48802
  }
@@ -48147,7 +48818,7 @@ async function setChannelEnabled(type, enabled, rawArgs, deps) {
48147
48818
  return 1;
48148
48819
  }
48149
48820
  channel.enabled = enabled;
48150
- await deps.saveConfig(config4);
48821
+ await deps.saveChannels(config4.channels);
48151
48822
  deps.print(t().channelCli.channelEnabledToggled(channel.id, enabled));
48152
48823
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48153
48824
  }
@@ -48169,7 +48840,7 @@ async function setChannelReplyMode(type, mode, rawArgs, deps) {
48169
48840
  return 1;
48170
48841
  }
48171
48842
  channel.replyMode = mode;
48172
- await deps.saveConfig(config4);
48843
+ await deps.saveChannels(config4.channels);
48173
48844
  deps.print(t().channelCli.channelReplyModeSet(channel.id, mode));
48174
48845
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48175
48846
  }
@@ -48315,7 +48986,7 @@ async function addChannelAccount(type, accountId, rawArgs, deps) {
48315
48986
  deps.print(validation.map((issue2) => issue2.message).join(";"));
48316
48987
  return 1;
48317
48988
  }
48318
- await deps.saveConfig(config4);
48989
+ await deps.saveChannels(config4.channels);
48319
48990
  deps.print(t().channelCli.channelAccountAdded(type, accountId));
48320
48991
  if (reEnabledChannel)
48321
48992
  deps.print(t().channelCli.channelReEnabled(type));
@@ -48355,7 +49026,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48355
49026
  return 1;
48356
49027
  }
48357
49028
  config4.channels = config4.channels.filter((channel) => channel.id !== existing.id);
48358
- await deps.saveConfig(config4);
49029
+ await deps.saveChannels(config4.channels);
48359
49030
  deps.print(t().channelCli.channelAccountRemovedWithChannel(type, accountId));
48360
49031
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48361
49032
  }
@@ -48372,7 +49043,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48372
49043
  deps.print(t().channelCli.channelAccountDefaultSwitched(options.defaultAccount));
48373
49044
  }
48374
49045
  existing.options = options;
48375
- await deps.saveConfig(config4);
49046
+ await deps.saveChannels(config4.channels);
48376
49047
  deps.print(t().channelCli.channelAccountRemoved(type, accountId));
48377
49048
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48378
49049
  }
@@ -48426,7 +49097,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48426
49097
  return 1;
48427
49098
  }
48428
49099
  existing.options = options;
48429
- await deps.saveConfig(config4);
49100
+ await deps.saveChannels(config4.channels);
48430
49101
  deps.print(t().channelCli.channelAccountEnabledToggled(type, accountId, enabled));
48431
49102
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48432
49103
  }
@@ -48434,7 +49105,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48434
49105
  // src/plugins/plugin-cli.ts
48435
49106
  init_core_home();
48436
49107
  init_plugin_home();
48437
- import { readFile as readFile10 } from "node:fs/promises";
49108
+ import { readFile as readFile11 } from "node:fs/promises";
48438
49109
  import { isAbsolute, join as join13, resolve } from "node:path";
48439
49110
  init_plugin_loader();
48440
49111
  init_validate_plugin();
@@ -48444,14 +49115,15 @@ init_channel_scope();
48444
49115
  init_plugin_loader();
48445
49116
  init_validate_plugin();
48446
49117
  init_known_plugins();
48447
- import { readFile as readFile9 } from "node:fs/promises";
49118
+ init_plugin_renames();
49119
+ import { readFile as readFile10 } from "node:fs/promises";
48448
49120
  import { join as join12 } from "node:path";
48449
49121
  function suggestedPluginPackageForChannel(type) {
48450
49122
  return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
48451
49123
  }
48452
49124
  async function readDependencyEntries(pluginHome) {
48453
49125
  try {
48454
- const raw = await readFile9(join12(pluginHome, "package.json"), "utf8");
49126
+ const raw = await readFile10(join12(pluginHome, "package.json"), "utf8");
48455
49127
  const parsed = JSON.parse(raw);
48456
49128
  const out = {};
48457
49129
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -48475,8 +49147,8 @@ async function inspectPlugins(input) {
48475
49147
  }
48476
49148
  const importPlugin = input.importPlugin ?? importPluginFromHome;
48477
49149
  const allConfigured = input.config.plugins;
48478
- const filterByName = input.pluginName ?? null;
48479
- if (filterByName && !allConfigured.some((plugin) => plugin.name === filterByName)) {
49150
+ const filterByName = input.pluginName ? normalizePluginPackageName(input.pluginName) : null;
49151
+ if (filterByName && !allConfigured.some((plugin) => normalizePluginPackageName(plugin.name) === filterByName)) {
48480
49152
  return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
48481
49153
  }
48482
49154
  const pushIfRelevant = (issue2) => {
@@ -48523,6 +49195,8 @@ async function inspectPlugins(input) {
48523
49195
  }
48524
49196
  const builtInChannelTypes = new Set(listKnownChannelIds());
48525
49197
  for (const channel of input.config.channels) {
49198
+ if (channel.enabled === false)
49199
+ continue;
48526
49200
  if (builtInChannelTypes.has(channel.type))
48527
49201
  continue;
48528
49202
  const provider = channelProviders.get(channel.type);
@@ -48550,12 +49224,31 @@ async function inspectPlugins(input) {
48550
49224
  init_known_plugins();
48551
49225
  init_plugin_renames();
48552
49226
  init_i18n();
49227
+ function findPluginSpecViolation(spec, platform) {
49228
+ if (spec.includes('"'))
49229
+ return "double-quote";
49230
+ if (platform === "win32" && spec.includes("%"))
49231
+ return "percent-on-windows";
49232
+ return null;
49233
+ }
49234
+ function invalidSpecMessage(specs, platform) {
49235
+ for (const spec of specs) {
49236
+ if (!spec)
49237
+ continue;
49238
+ const violation = findPluginSpecViolation(spec, platform);
49239
+ if (violation === "double-quote")
49240
+ return t().pluginCli.pluginSpecHasDoubleQuote(spec);
49241
+ if (violation === "percent-on-windows")
49242
+ return t().pluginCli.pluginSpecHasPercentOnWindows(spec);
49243
+ }
49244
+ return null;
49245
+ }
48553
49246
  function looksLikePath(spec) {
48554
49247
  return spec === "." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith(".\\") || spec.startsWith("..\\") || spec.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(spec) || isAbsolute(spec);
48555
49248
  }
48556
49249
  async function readDependencyEntries2(pluginHome) {
48557
49250
  try {
48558
- const raw = await readFile10(join13(pluginHome, "package.json"), "utf8");
49251
+ const raw = await readFile11(join13(pluginHome, "package.json"), "utf8");
48559
49252
  const parsed = JSON.parse(raw);
48560
49253
  const out = {};
48561
49254
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -48581,7 +49274,7 @@ async function resolveLocalPluginName(installSpec, pluginHome, namesBeforeInstal
48581
49274
  return name;
48582
49275
  }
48583
49276
  try {
48584
- const raw = await readFile10(join13(installSpec, "package.json"), "utf8");
49277
+ const raw = await readFile11(join13(installSpec, "package.json"), "utf8");
48585
49278
  const parsed = JSON.parse(raw);
48586
49279
  if (typeof parsed.name === "string" && parsed.name.trim())
48587
49280
  return parsed.name.trim();
@@ -48724,6 +49417,11 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48724
49417
  deps.print(t().pluginCli.unrecognizedArgs(flags.rest.join(" ")));
48725
49418
  return 1;
48726
49419
  }
49420
+ const invalidSpec = invalidSpecMessage([packageSpec, flags.version], deps.platform ?? process.platform);
49421
+ if (invalidSpec) {
49422
+ deps.print(invalidSpec);
49423
+ return 1;
49424
+ }
48727
49425
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48728
49426
  await ensurePluginHome(pluginHome);
48729
49427
  const installSpec = looksLikePath(packageSpec) && !isAbsolute(packageSpec) ? resolve(process.cwd(), packageSpec) : packageSpec;
@@ -48761,7 +49459,7 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48761
49459
  } else {
48762
49460
  config4.plugins = [...config4.plugins, next];
48763
49461
  }
48764
- await deps.saveConfig(config4);
49462
+ await deps.savePlugins(config4.plugins);
48765
49463
  deps.print(t().pluginCli.pluginInstalled(recordedName));
48766
49464
  if (summary.channels.length > 0) {
48767
49465
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
@@ -48787,7 +49485,7 @@ async function removePlugin(packageName, rawArgs, deps) {
48787
49485
  }
48788
49486
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48789
49487
  const validate = deps.validateInstalledPlugin ?? ((name) => validateInstalledPluginDefault(name, pluginHome));
48790
- const guard = await dependencyGuard(packageName, config4, validate);
49488
+ const guard = await dependencyGuard(existing.name, config4, validate);
48791
49489
  if (!guard.allow) {
48792
49490
  if (guard.reason)
48793
49491
  deps.print(guard.reason);
@@ -48797,14 +49495,14 @@ async function removePlugin(packageName, rawArgs, deps) {
48797
49495
  await removePluginPackage({ packageName: name, pluginHome });
48798
49496
  });
48799
49497
  try {
48800
- await remove(packageName);
49498
+ await remove(existing.name);
48801
49499
  } catch (error2) {
48802
- deps.print(t().pluginCli.pluginUninstallFailed(packageName, describeError(error2)));
49500
+ deps.print(t().pluginCli.pluginUninstallFailed(existing.name, describeError(error2)));
48803
49501
  return 1;
48804
49502
  }
48805
- config4.plugins = config4.plugins.filter((entry) => entry.name !== packageName);
48806
- await deps.saveConfig(config4);
48807
- deps.print(t().pluginCli.pluginRemoved(packageName));
49503
+ config4.plugins = config4.plugins.filter((entry) => entry.name !== existing.name);
49504
+ await deps.savePlugins(config4.plugins);
49505
+ deps.print(t().pluginCli.pluginRemoved(existing.name));
48808
49506
  return await maybeRestartAfterMutation2(flags.restart, deps);
48809
49507
  }
48810
49508
  async function updatePlugins(args, deps) {
@@ -48824,6 +49522,11 @@ async function updatePlugins(args, deps) {
48824
49522
  deps.print("--all cannot be combined with --version");
48825
49523
  return 1;
48826
49524
  }
49525
+ const invalidSpec = invalidSpecMessage([flags.version], deps.platform ?? process.platform);
49526
+ if (invalidSpec) {
49527
+ deps.print(invalidSpec);
49528
+ return 1;
49529
+ }
48827
49530
  const config4 = await deps.loadConfig();
48828
49531
  ensurePluginsArray(config4);
48829
49532
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
@@ -48880,7 +49583,7 @@ async function updatePlugins(args, deps) {
48880
49583
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
48881
49584
  }
48882
49585
  }
48883
- await deps.saveConfig(config4);
49586
+ await deps.savePlugins(config4.plugins);
48884
49587
  return await maybeRestartAfterMutation2(flags.restart, deps);
48885
49588
  }
48886
49589
  async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
@@ -48911,7 +49614,7 @@ async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
48911
49614
  }
48912
49615
  }
48913
49616
  existing.enabled = enabled;
48914
- await deps.saveConfig(config4);
49617
+ await deps.savePlugins(config4.plugins);
48915
49618
  deps.print(t().pluginCli.pluginEnabledToggled(packageName, enabled));
48916
49619
  return await maybeRestartAfterMutation2(flags.restart, deps);
48917
49620
  }
@@ -49429,7 +50132,9 @@ async function defaultUpdate(args, input) {
49429
50132
  const store = await createCliConfigStore();
49430
50133
  const deps = {
49431
50134
  loadConfig: async () => await store.load(),
49432
- saveConfig: async (config4) => await store.save(config4),
50135
+ savePlugins: async (plugins) => {
50136
+ await store.replacePlugins(plugins);
50137
+ },
49433
50138
  readCurrentVersion: readVersion,
49434
50139
  print: input.print,
49435
50140
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -49438,6 +50143,27 @@ async function defaultUpdate(args, input) {
49438
50143
  };
49439
50144
  return await handleUpdateCli(args, deps);
49440
50145
  }
50146
+ function warnStateLoadReport(store, writeStderr = (text) => process.stderr.write(text)) {
50147
+ const report = store.lastLoadReport;
50148
+ if (!report)
50149
+ return;
50150
+ for (const record3 of report.dropped) {
50151
+ writeStderr(`[xacpx] state.record_quarantined section=${record3.section}${record3.key ? ` key=${record3.key}` : ""} reason=${record3.reason}
50152
+ `);
50153
+ }
50154
+ if (report.corruptPath) {
50155
+ writeStderr(`[xacpx] state.file_corrupt unreadable state.json renamed to ${report.corruptPath}
50156
+ `);
50157
+ }
50158
+ if (report.quarantinePath) {
50159
+ writeStderr(`[xacpx] state.file_quarantined original state.json backed up to ${report.quarantinePath}
50160
+ `);
50161
+ }
50162
+ if (report.backupError) {
50163
+ writeStderr(`[xacpx] state.quarantine_backup_failed ${report.backupError}
50164
+ `);
50165
+ }
50166
+ }
49441
50167
  async function runOnboardingBeforeStart(input) {
49442
50168
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49443
50169
  await ensureConfigExists(runtimePaths.configPath);
@@ -49445,10 +50171,14 @@ async function runOnboardingBeforeStart(input) {
49445
50171
  const stateStore = new StateStore(runtimePaths.statePath);
49446
50172
  const config4 = await configStore.load();
49447
50173
  const state = await stateStore.load();
50174
+ warnStateLoadReport(stateStore);
49448
50175
  const result = await maybeRunFirstUseOnboarding({
49449
50176
  config: config4,
49450
50177
  state,
49451
- saveConfig: async (next) => await configStore.save(next),
50178
+ saveFirstRunConfig: async ({ workspace: workspace3, agent: agent3 }) => {
50179
+ await configStore.upsertWorkspace(workspace3.name, workspace3.cwd);
50180
+ await configStore.upsertAgent(agent3.name, agent3.config);
50181
+ },
49452
50182
  deps: {
49453
50183
  print: input.print,
49454
50184
  cwd: input.cwd,
@@ -49659,7 +50389,7 @@ async function handleLaterCli(args, deps) {
49659
50389
  }
49660
50390
  async function laterList(print) {
49661
50391
  const scheduled = await createCliScheduledTaskService();
49662
- print(renderLaterList(scheduled.listPending(), (alias) => toDisplaySessionAlias(alias)));
50392
+ print(renderLaterList(scheduled.listPendingAllChats(), (alias) => toDisplaySessionAlias(alias)));
49663
50393
  return 0;
49664
50394
  }
49665
50395
  async function laterCancel(rawId, print) {
@@ -49669,7 +50399,7 @@ async function laterCancel(rawId, print) {
49669
50399
  return 1;
49670
50400
  }
49671
50401
  const scheduled = await createCliScheduledTaskService();
49672
- const ok = await scheduled.cancelPending(id);
50402
+ const ok = await scheduled.cancelPendingAnyChat(id);
49673
50403
  if (!ok) {
49674
50404
  print(t().cli.laterNotFound(id));
49675
50405
  print(t().cli.laterNotFoundHint);
@@ -49682,6 +50412,7 @@ async function createCliScheduledTaskService() {
49682
50412
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49683
50413
  const stateStore = new StateStore(runtimePaths.statePath);
49684
50414
  const state = await stateStore.load();
50415
+ warnStateLoadReport(stateStore);
49685
50416
  return new ScheduledTaskService(state, stateStore);
49686
50417
  }
49687
50418
  function resolveConfigPathForCurrentEnv() {
@@ -49784,14 +50515,12 @@ async function createFirstRunSession(runtime, plan) {
49784
50515
  }
49785
50516
  async function rollbackFirstRunConfig(runtime, plan) {
49786
50517
  try {
49787
- const config4 = await runtime.configStore.load();
49788
- if (!plan.rollback.workspaceExisted && config4.workspaces[plan.workspace]) {
49789
- delete config4.workspaces[plan.workspace];
50518
+ if (!plan.rollback.workspaceExisted) {
50519
+ await runtime.configStore.removeWorkspace(plan.workspace);
49790
50520
  }
49791
- if (!plan.rollback.agentExisted && config4.agents[plan.agent]) {
49792
- delete config4.agents[plan.agent];
50521
+ if (!plan.rollback.agentExisted) {
50522
+ await runtime.configStore.removeAgent(plan.agent);
49793
50523
  }
49794
- await runtime.configStore.save(config4);
49795
50524
  } catch (error2) {
49796
50525
  await runtime.logger.error("onboarding.rollback_failed", "failed to roll back first-run config", {
49797
50526
  alias: plan.alias,
@@ -49823,7 +50552,9 @@ async function defaultMcpStdio(args, deps = {}) {
49823
50552
  await ensureConfigExists(runtimePaths.configPath);
49824
50553
  const config4 = await loadConfig(runtimePaths.configPath);
49825
50554
  availableAgents = Object.keys(config4.agents);
49826
- const state = await new StateStore(runtimePaths.statePath).load();
50555
+ const stateStore = new StateStore(runtimePaths.statePath);
50556
+ const state = await stateStore.load();
50557
+ warnStateLoadReport(stateStore, deps.stderr ?? ((text) => process.stderr.write(text)));
49827
50558
  const resolveIdentity = createMcpStdioIdentityResolver({
49828
50559
  parsedCoordinatorSession,
49829
50560
  sourceHandle,
@@ -49887,7 +50618,9 @@ async function createChannelCliDeps(input) {
49887
50618
  const controller = input.controller ?? createDefaultController();
49888
50619
  const base = {
49889
50620
  loadConfig: async () => await store.load(),
49890
- saveConfig: async (config4) => await store.save(config4),
50621
+ saveChannels: async (channels) => {
50622
+ await store.replaceChannels(channels);
50623
+ },
49891
50624
  print: input.print,
49892
50625
  stderr: input.stderr ?? ((text) => process.stderr.write(text)),
49893
50626
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -49910,7 +50643,9 @@ async function createPluginCliDeps(input) {
49910
50643
  const controller = input.controller ?? createDefaultController();
49911
50644
  const base = {
49912
50645
  loadConfig: async () => await store.load(),
49913
- saveConfig: async (config4) => await store.save(config4),
50646
+ savePlugins: async (plugins) => {
50647
+ await store.replacePlugins(plugins);
50648
+ },
49914
50649
  print: input.print,
49915
50650
  isInteractive: input.isInteractive ?? defaultIsInteractive,
49916
50651
  promptText: input.promptText ?? defaultPromptText,
@@ -50052,7 +50787,7 @@ function safeDaemonLogPaths() {
50052
50787
  const configPath = resolveConfigPathForCurrentEnv();
50053
50788
  const paths = resolveDaemonPathsForCurrentConfig();
50054
50789
  return {
50055
- appLog: join20(dirname16(configPath), "runtime", "app.log"),
50790
+ appLog: join20(dirname14(configPath), "runtime", "app.log"),
50056
50791
  stderrLog: paths.stderrLog
50057
50792
  };
50058
50793
  } catch {