@ganglion/xacpx 0.9.2 → 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
  };
@@ -851,7 +857,6 @@ var init_cli_update = __esm(() => {
851
857
  updateFailed: (name, error) => `${name} update failed: ${error}`,
852
858
  targetNotFound: (name) => `Update target not found: ${name}`,
853
859
  targetVersionUnknown: (name) => `${name}: cannot check latest version; skipped.`,
854
- targetNotPinned: (name) => `${name} has no recorded version; use \`xacpx plugin update ${name}\` or specify a version explicitly.`,
855
860
  multiTargetNonInteractive: "Installed plugins detected; in non-interactive mode use `xacpx update --all` or `xacpx update <name>`.",
856
861
  selectionPrompt: "Select items to update (numbers, comma-separated; a=all; Enter to cancel): ",
857
862
  selectionInvalid: (part) => `Invalid selection: ${part}`,
@@ -916,6 +921,8 @@ var init_plugin_cli = __esm(() => {
916
921
  noPlugins: "No plugins installed yet.",
917
922
  pluginListHeader: "Plugins:",
918
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.`,
919
926
  pluginInstallFailed: (packageSpec, error) => `Plugin ${packageSpec} install failed: ${error}`,
920
927
  pluginValidateFailed: (recordedName, error) => `Plugin ${recordedName} validation failed: ${error}`,
921
928
  pluginInstalled: (recordedName) => `Plugin ${recordedName} installed`,
@@ -1027,8 +1034,6 @@ var init_weixin = __esm(() => {
1027
1034
  debugEnabled: "Debug mode enabled",
1028
1035
  debugDisabled: "Debug mode disabled",
1029
1036
  sessionCleared: "✅ Session cleared. Starting a fresh conversation.",
1030
- noAccountsLoggedIn: "No accounts are currently logged in.",
1031
- logoutSuccess: "✅ Logged out. All account credentials cleared.",
1032
1037
  commandFailed: (detail) => `❌ Command failed: ${detail}`
1033
1038
  };
1034
1039
  });
@@ -1066,6 +1071,7 @@ var init_misc = __esm(() => {
1066
1071
  quotaOverflowSummary: (count) => `(${count} progress updates omitted due to message limit; see final result below)`,
1067
1072
  finalHeadsUp: (total, sentSoFar, remaining) => `—
1068
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.`,
1069
1075
  quotedMessagePrefix: (parts) => `[Quote: ${parts}]`,
1070
1076
  scheduledTaskFailed: (message) => `Scheduled task failed: ${message}`,
1071
1077
  orchestrationTaskCompleted: (taskId, workerSession, result) => `Delegation task "${taskId}" completed
@@ -1094,6 +1100,8 @@ var init_misc = __esm(() => {
1094
1100
  delegateQPackageInstr3: "Do not forward the human's exact words to the worker",
1095
1101
  commandAccessDeniedSuffix: " is restricted to group owner only.",
1096
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.",
1097
1105
  commandLabelThisMessage: "This message",
1098
1106
  sessionResetNoCurrentSession: "No session is currently selected. Run /session new ... or /use <alias> first.",
1099
1107
  sessionResetFailed: (alias) => `Session "${alias}" reset failed. The new backend session was not created, please try again later.`,
@@ -1164,6 +1172,11 @@ var init_session2 = __esm(() => {
1164
1172
  currentLabel: "[当前]",
1165
1173
  sessionListItem: (alias, agent2, workspace2) => `- ${alias} (${agent2} @ ${workspace2})`,
1166
1174
  sessionCreated: (alias) => `会话「${alias}」已创建并切换`,
1175
+ sessionAlreadyExists: (alias, agent2, workspace2) => [
1176
+ `会话「${alias}」已存在(${agent2} @ ${workspace2})。`,
1177
+ `发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
1178
+ ].join(`
1179
+ `),
1167
1180
  sessionAttachNotFound: (alias, agent2, workspace2) => [
1168
1181
  "没有找到可绑定的已有会话。",
1169
1182
  `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
@@ -1200,6 +1213,7 @@ var init_session2 = __esm(() => {
1200
1213
  sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
1201
1214
  sessionRemoved: (alias) => `已删除会话「${alias}」。`,
1202
1215
  sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
1216
+ sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
1203
1217
  sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
1204
1218
  sessionOrchestrationPurgeFailed: (warning) => `提示:清理任务编排引用失败(${warning}),请稍后执行 /tasks clean 手动清理。`,
1205
1219
  sessionTransportTeardownFailed: (warning) => `提示:后端会话未能自动关闭(${warning}),如有残留请手动执行 acpx sessions close。`,
@@ -1493,7 +1507,7 @@ var init_later2 = __esm(() => {
1493
1507
  helpNote2: "时间必须在 10 秒之后、7 天之内",
1494
1508
  helpNote3: "默认在为本次任务新建的临时会话里执行,跑完即销毁",
1495
1509
  helpNote4: "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
1496
- helpNote5: "/lt list 显示全局待执行任务;群聊中只有群主可取消",
1510
+ helpNote5: "/lt list 只显示本聊天创建的待执行任务;群聊中只有群主可取消",
1497
1511
  helpNote6: "不支持延迟执行 / 开头的 xacpx 命令",
1498
1512
  helpNote7: "完整时间格式与说明见 docs/later-command.md"
1499
1513
  };
@@ -1922,7 +1936,6 @@ var init_cli_update2 = __esm(() => {
1922
1936
  updateFailed: (name, error) => `${name} 更新失败:${error}`,
1923
1937
  targetNotFound: (name) => `没有找到更新项:${name}`,
1924
1938
  targetVersionUnknown: (name) => `${name} 无法检查最新版本,已跳过。`,
1925
- targetNotPinned: (name) => `${name} 未记录当前版本;请先使用 \`xacpx plugin update ${name}\` 或显式选择版本。`,
1926
1939
  multiTargetNonInteractive: "检测到已安装插件;非交互模式请使用 `xacpx update --all` 或 `xacpx update <name>`。",
1927
1940
  selectionPrompt: "请选择要更新的项目(数字,逗号分隔,a=全部,回车取消):",
1928
1941
  selectionInvalid: (part) => `无效选择:${part}`,
@@ -1987,6 +2000,8 @@ var init_plugin_cli2 = __esm(() => {
1987
2000
  noPlugins: "还没有安装插件。",
1988
2001
  pluginListHeader: "插件:",
1989
2002
  unrecognizedArgs: (args) => `未识别的参数:${args}`,
2003
+ pluginSpecHasDoubleQuote: (spec) => `非法插件 spec ${spec}:npm 包 spec 不允许包含双引号 (")。`,
2004
+ pluginSpecHasPercentOnWindows: (spec) => `非法插件 spec ${spec}:Windows 上 cmd.exe 会展开 %,无法安全传递。请改用 npm 直接安装该包。`,
1990
2005
  pluginInstallFailed: (packageSpec, error) => `插件 ${packageSpec} 安装失败:${error}`,
1991
2006
  pluginValidateFailed: (recordedName, error) => `插件 ${recordedName} 校验失败:${error}`,
1992
2007
  pluginInstalled: (recordedName) => `插件 ${recordedName} 已安装`,
@@ -2098,8 +2113,6 @@ var init_weixin2 = __esm(() => {
2098
2113
  debugEnabled: "Debug 模式已开启",
2099
2114
  debugDisabled: "Debug 模式已关闭",
2100
2115
  sessionCleared: "✅ 会话已清除,重新开始对话",
2101
- noAccountsLoggedIn: "当前没有已登录的账号",
2102
- logoutSuccess: "✅ 已退出登录,清除所有账号凭证",
2103
2116
  commandFailed: (detail) => `❌ 指令执行失败: ${detail}`
2104
2117
  };
2105
2118
  });
@@ -2137,6 +2150,7 @@ var init_misc2 = __esm(() => {
2137
2150
  quotaOverflowSummary: (count) => `(因消息次数限制省略 ${count} 条进度,请继续查看下方最终结果)`,
2138
2151
  finalHeadsUp: (total, sentSoFar, remaining) => `—
2139
2152
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`,
2153
+ finalAllParked: (count) => `\uD83D\uDCC4 已达消息上限:结果共 ${count} 段已暂存。回复 /jx 接收。`,
2140
2154
  quotedMessagePrefix: (parts) => `[引用: ${parts}]`,
2141
2155
  scheduledTaskFailed: (message) => `定时任务执行失败:${message}`,
2142
2156
  orchestrationTaskCompleted: (taskId, workerSession, result) => `委派任务「${taskId}」已完成
@@ -2165,6 +2179,8 @@ var init_misc2 = __esm(() => {
2165
2179
  delegateQPackageInstr3: "不要直接把 human 原话转发给 worker",
2166
2180
  commandAccessDeniedSuffix: " 仅限群创建者/频道 owner 使用。",
2167
2181
  commandAccessDeniedHint: "如果需要执行控制类操作,请由 owner 在群内发送,或改用私聊。",
2182
+ commandAccessDeniedChatTypeMissingSuffix: " 已被拦截:该频道未上报会话类型(直聊/群聊),控制类命令在此暂不可用。",
2183
+ commandAccessDeniedChatTypeMissingHint: "只读命令与普通对话不受影响。这是频道元数据问题,请升级或反馈该频道插件。",
2168
2184
  commandLabelThisMessage: "该消息",
2169
2185
  sessionResetNoCurrentSession: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。",
2170
2186
  sessionResetFailed: (alias) => `会话「${alias}」重置失败。
@@ -4214,7 +4230,7 @@ var require_lib = __commonJS((exports, module) => {
4214
4230
  import { chmod, mkdir, writeFile } from "node:fs/promises";
4215
4231
  import { chmodSync, mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
4216
4232
  import { dirname } from "node:path";
4217
- async function writePrivateFileAtomic(path, content) {
4233
+ async function withPrivateFileLock(path, fn) {
4218
4234
  await mkdir(dirname(path), { recursive: true });
4219
4235
  const release = await lockfile.lock(path, {
4220
4236
  realpath: false,
@@ -4228,23 +4244,31 @@ async function writePrivateFileAtomic(path, content) {
4228
4244
  }
4229
4245
  });
4230
4246
  try {
4231
- try {
4232
- await retryTransientWriteErrors(async () => import_write_file_atomic.default(path, content, {
4233
- mode: PRIVATE_FILE_MODE,
4234
- encoding: "utf8",
4235
- fsync: true
4236
- }));
4237
- } catch (error) {
4238
- if (!isTransientWriteError(error, process.platform)) {
4239
- throw error;
4240
- }
4241
- await writeFile(path, content, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
4242
- await chmod(path, PRIVATE_FILE_MODE).catch(() => {});
4243
- }
4247
+ return await fn((content) => writePrivateFileAtomicUnlocked(path, content));
4244
4248
  } finally {
4245
4249
  await release();
4246
4250
  }
4247
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
+ }
4248
4272
  function writePrivateFileSync(path, content, deps = {}) {
4249
4273
  mkdirSync2(dirname(path), { recursive: true });
4250
4274
  const platform = deps.platform ?? process.platform;
@@ -4401,6 +4425,14 @@ function isRecord(value) {
4401
4425
  function isReplyMode(value) {
4402
4426
  return value === "stream" || value === "final" || value === "verbose";
4403
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
+ }
4404
4436
  function parseChannelConfig(channel, legacyWechat) {
4405
4437
  if (channel !== undefined) {
4406
4438
  if (!isRecord(channel)) {
@@ -4413,6 +4445,7 @@ function parseChannelConfig(channel, legacyWechat) {
4413
4445
  throw new Error("channel.replyMode must be stream, final, or verbose");
4414
4446
  }
4415
4447
  const type = typeof channel.type === "string" ? channel.type : "weixin";
4448
+ const ownerIds = parseOwnerIds(channel.ownerIds, "channel.ownerIds");
4416
4449
  let options = undefined;
4417
4450
  if ("feishu" in channel && isRecord(channel.feishu)) {
4418
4451
  options = channel.feishu;
@@ -4422,6 +4455,7 @@ function parseChannelConfig(channel, legacyWechat) {
4422
4455
  return {
4423
4456
  type,
4424
4457
  replyMode: isReplyMode(channel.replyMode) ? channel.replyMode : DEFAULT_CHANNEL_CONFIG.replyMode,
4458
+ ...ownerIds ? { ownerIds } : {},
4425
4459
  ...options ? { options } : {}
4426
4460
  };
4427
4461
  }
@@ -4660,6 +4694,7 @@ function parseRuntimeChannelConfig(raw, index) {
4660
4694
  if ("replyMode" in raw && !isReplyMode(raw.replyMode)) {
4661
4695
  throw new Error(`channels[${index}].replyMode must be stream, final, or verbose`);
4662
4696
  }
4697
+ const ownerIds = parseOwnerIds(raw.ownerIds, `channels[${index}].ownerIds`);
4663
4698
  let options = undefined;
4664
4699
  if ("feishu" in raw && isRecord(raw.feishu)) {
4665
4700
  options = raw.feishu;
@@ -4671,6 +4706,7 @@ function parseRuntimeChannelConfig(raw, index) {
4671
4706
  type: raw.type,
4672
4707
  enabled,
4673
4708
  ...isReplyMode(raw.replyMode) ? { replyMode: raw.replyMode } : {},
4709
+ ...ownerIds ? { ownerIds } : {},
4674
4710
  ...options ? { options } : {}
4675
4711
  };
4676
4712
  }
@@ -4755,6 +4791,8 @@ var init_load_config = __esm(() => {
4755
4791
  });
4756
4792
 
4757
4793
  // src/config/config-store.ts
4794
+ import { readFile as readFile2 } from "node:fs/promises";
4795
+
4758
4796
  class ConfigStore {
4759
4797
  path;
4760
4798
  constructor(path2) {
@@ -4763,60 +4801,250 @@ class ConfigStore {
4763
4801
  async load() {
4764
4802
  return await loadConfig(this.path);
4765
4803
  }
4766
- async save(config3) {
4767
- await writePrivateFileAtomic(this.path, `${JSON.stringify(config3, null, 2)}
4768
- `);
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
+ });
4769
4816
  }
4770
4817
  async upsertWorkspace(name, cwd, description) {
4771
- const config3 = await this.load();
4772
- const workspace3 = {
4773
- cwd,
4774
- ...description ? { description } : {}
4775
- };
4776
- config3.workspaces[name] = workspace3;
4777
- await this.save(config3);
4778
- 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
+ });
4779
4826
  }
4780
4827
  async removeWorkspace(name) {
4781
- const config3 = await this.load();
4782
- delete config3.workspaces[name];
4783
- await this.save(config3);
4784
- return config3;
4828
+ assertSafeConfigKey(name);
4829
+ return await this.patchRaw((raw) => {
4830
+ deleteRecordEntry(raw, "workspaces", name);
4831
+ });
4785
4832
  }
4786
4833
  async upsertAgent(name, agent3) {
4787
- const config3 = await this.load();
4788
- config3.agents[name] = agent3;
4789
- await this.save(config3);
4790
- 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
+ });
4791
4842
  }
4792
4843
  async removeAgent(name) {
4793
- const config3 = await this.load();
4794
- delete config3.agents[name];
4795
- await this.save(config3);
4796
- return config3;
4797
- }
4798
- async updateTransport(transport) {
4799
- const config3 = await this.load();
4800
- config3.transport = {
4801
- ...config3.transport,
4802
- ...transport
4803
- };
4804
- await this.save(config3);
4805
- return config3;
4806
- }
4807
- async updateChannel(channel) {
4808
- const config3 = await this.load();
4809
- config3.channel = {
4810
- ...config3.channel,
4811
- ...channel
4812
- };
4813
- await this.save(config3);
4814
- 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
+ });
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];
4815
5023
  }
4816
5024
  }
5025
+ function applyRecordPatch(target, patch) {
5026
+ for (const [key, value] of Object.entries(patch)) {
5027
+ if (value === undefined) {
5028
+ delete target[key];
5029
+ } else {
5030
+ target[key] = value;
5031
+ }
5032
+ }
5033
+ }
5034
+ function clonePlain(value) {
5035
+ return JSON.parse(JSON.stringify(value));
5036
+ }
5037
+ function isPlainRecord(value) {
5038
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5039
+ }
5040
+ function isMissingFileError(error) {
5041
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
5042
+ }
5043
+ var PROTOTYPE_POLLUTING_KEYS;
4817
5044
  var init_config_store = __esm(() => {
4818
5045
  init_private_file();
4819
5046
  init_load_config();
5047
+ PROTOTYPE_POLLUTING_KEYS = new Set(["__proto__", "constructor", "prototype"]);
4820
5048
  });
4821
5049
 
4822
5050
  // src/config/default-workspace.ts
@@ -4829,16 +5057,16 @@ var init_default_workspace = __esm(() => {
4829
5057
  });
4830
5058
 
4831
5059
  // src/config/ensure-config.ts
4832
- import { readFile as readFile2 } from "node:fs/promises";
5060
+ import { readFile as readFile3 } from "node:fs/promises";
4833
5061
  async function ensureConfigExists(path2, options = {}) {
4834
5062
  try {
4835
5063
  await loadConfig(path2);
4836
5064
  } catch (error) {
4837
- if (!isMissingFileError(error)) {
5065
+ if (!isMissingFileError2(error)) {
4838
5066
  throw error;
4839
5067
  }
4840
- const store = new ConfigStore(path2);
4841
- await store.save(await loadDefaultConfigTemplate(options));
5068
+ const seed = await loadDefaultConfigTemplate(options);
5069
+ await writePrivateFileAtomic(path2, serializeRawConfig(seed));
4842
5070
  }
4843
5071
  }
4844
5072
  async function loadDefaultConfigTemplate(options = {}) {
@@ -4846,7 +5074,7 @@ async function loadDefaultConfigTemplate(options = {}) {
4846
5074
  try {
4847
5075
  return normalizeDefaultConfigTemplate(await options.readDefaultConfigTemplate());
4848
5076
  } catch (error) {
4849
- if (!isMissingFileError(error)) {
5077
+ if (!isMissingFileError2(error)) {
4850
5078
  throw error;
4851
5079
  }
4852
5080
  }
@@ -4858,10 +5086,10 @@ async function loadDefaultConfigTemplate(options = {}) {
4858
5086
  let raw;
4859
5087
  for (const candidate of candidates) {
4860
5088
  try {
4861
- raw = await readFile2(candidate, "utf8");
5089
+ raw = await readFile3(candidate, "utf8");
4862
5090
  break;
4863
5091
  } catch (error) {
4864
- if (!isMissingFileError(error)) {
5092
+ if (!isMissingFileError2(error)) {
4865
5093
  throw error;
4866
5094
  }
4867
5095
  }
@@ -4874,7 +5102,14 @@ async function loadDefaultConfigTemplate(options = {}) {
4874
5102
  function normalizeDefaultConfigTemplate(raw) {
4875
5103
  const template = parseConfig(raw);
4876
5104
  return {
4877
- ...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
+ },
4878
5113
  agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent3]) => [
4879
5114
  name,
4880
5115
  {
@@ -4887,32 +5122,18 @@ function normalizeDefaultConfigTemplate(raw) {
4887
5122
  }
4888
5123
  };
4889
5124
  }
4890
- function isMissingFileError(error) {
5125
+ function isMissingFileError2(error) {
4891
5126
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4892
5127
  }
4893
5128
  var BUILTIN_DEFAULT_CONFIG_TEMPLATE;
4894
5129
  var init_ensure_config = __esm(() => {
5130
+ init_private_file();
4895
5131
  init_config_store();
4896
5132
  init_default_workspace();
4897
5133
  init_load_config();
4898
5134
  BUILTIN_DEFAULT_CONFIG_TEMPLATE = {
4899
5135
  transport: {
4900
- type: "acpx-bridge",
4901
- sessionInitTimeoutMs: 120000,
4902
- permissionMode: "approve-all",
4903
- nonInteractivePermissions: "deny"
4904
- },
4905
- logging: {
4906
- level: "info",
4907
- maxSizeBytes: 2 * 1024 * 1024,
4908
- maxFiles: 5,
4909
- retentionDays: 7,
4910
- perf: {
4911
- enabled: false,
4912
- maxSizeBytes: 5242880,
4913
- maxFiles: 3,
4914
- retentionDays: 7
4915
- }
5136
+ type: "acpx-bridge"
4916
5137
  },
4917
5138
  channel: {
4918
5139
  type: "weixin",
@@ -4924,8 +5145,7 @@ var init_ensure_config = __esm(() => {
4924
5145
  },
4925
5146
  workspaces: {
4926
5147
  [DEFAULT_HOME_WORKSPACE_NAME]: { ...DEFAULT_HOME_WORKSPACE }
4927
- },
4928
- plugins: []
5148
+ }
4929
5149
  };
4930
5150
  });
4931
5151
 
@@ -5006,7 +5226,7 @@ var init_agent_templates = __esm(() => {
5006
5226
  });
5007
5227
 
5008
5228
  // src/daemon/daemon-status.ts
5009
- 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";
5010
5230
  import { dirname as dirname2 } from "node:path";
5011
5231
 
5012
5232
  class DaemonStatusStore {
@@ -5016,7 +5236,7 @@ class DaemonStatusStore {
5016
5236
  }
5017
5237
  async load() {
5018
5238
  try {
5019
- const content = await readFile3(this.path, "utf8");
5239
+ const content = await readFile4(this.path, "utf8");
5020
5240
  if (content.trim() === "") {
5021
5241
  return null;
5022
5242
  }
@@ -5032,18 +5252,37 @@ class DaemonStatusStore {
5032
5252
  }
5033
5253
  }
5034
5254
  async save(status) {
5035
- await mkdir2(dirname2(this.path), { recursive: true });
5036
- 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" });
5037
5257
  }
5038
5258
  async clear() {
5039
5259
  await rm(this.path, { force: true });
5040
5260
  }
5041
5261
  }
5042
- 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 = () => {};
5043
5283
 
5044
5284
  // src/daemon/daemon-controller.ts
5045
- import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
5046
- import { dirname as dirname3 } from "node:path";
5285
+ import { open, readFile as readFile5, rm as rm2 } from "node:fs/promises";
5047
5286
 
5048
5287
  class DaemonController {
5049
5288
  paths;
@@ -5131,7 +5370,7 @@ class DaemonController {
5131
5370
  }
5132
5371
  async loadPid() {
5133
5372
  try {
5134
- const content = await readFile4(this.paths.pidFile, "utf8");
5373
+ const content = await readFile5(this.paths.pidFile, "utf8");
5135
5374
  const pid = Number(content.trim());
5136
5375
  return Number.isFinite(pid) && pid > 0 ? pid : null;
5137
5376
  } catch (error) {
@@ -5142,7 +5381,7 @@ class DaemonController {
5142
5381
  }
5143
5382
  }
5144
5383
  async openPidFileExclusive() {
5145
- await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
5384
+ await ensurePrivateRuntimeDir(this.paths.runtimeDir);
5146
5385
  try {
5147
5386
  return await open(this.paths.pidFile, "wx", 384);
5148
5387
  } catch (error) {
@@ -5196,6 +5435,7 @@ class DaemonController {
5196
5435
  }
5197
5436
  var init_daemon_controller = __esm(() => {
5198
5437
  init_daemon_status();
5438
+ init_private_runtime_dir();
5199
5439
  });
5200
5440
 
5201
5441
  // src/process/terminate-process-tree.ts
@@ -5247,13 +5487,15 @@ async function defaultRunProcessCommand(command, args) {
5247
5487
  var init_terminate_process_tree = () => {};
5248
5488
 
5249
5489
  // src/daemon/create-daemon-controller.ts
5250
- import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
5490
+ import { open as open2 } from "node:fs/promises";
5251
5491
  import { spawn as spawn2 } from "node:child_process";
5252
5492
  function createDaemonController(paths, options) {
5253
5493
  return new DaemonController(paths, {
5254
5494
  isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
5255
5495
  spawnDetached: async (spawnOptions) => {
5256
- await mkdir4(paths.runtimeDir, { recursive: true });
5496
+ await ensurePrivateRuntimeDir(paths.runtimeDir, {
5497
+ ...options.platform ? { platform: options.platform } : {}
5498
+ });
5257
5499
  const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
5258
5500
  const stderrHandle = await open2(paths.stderrLog, "a", 384);
5259
5501
  await stdoutHandle.chmod(384).catch(() => {});
@@ -5325,8 +5567,10 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
5325
5567
  function buildWindowsLauncherScript() {
5326
5568
  const script = [
5327
5569
  "$env:XACPX_DAEMON_RUN = '1'",
5570
+ `$arg0 = '"' + $env:XACPX_DAEMON_ARG0 + '"'`,
5571
+ `$arg1 = '"' + $env:XACPX_DAEMON_ARG1 + '"'`,
5328
5572
  "$process = Start-Process -FilePath $env:XACPX_DAEMON_COMMAND `",
5329
- " -ArgumentList @($env:XACPX_DAEMON_ARG0, $env:XACPX_DAEMON_ARG1) `",
5573
+ " -ArgumentList @($arg0, $arg1) `",
5330
5574
  " -WorkingDirectory $env:XACPX_DAEMON_CWD `",
5331
5575
  " -RedirectStandardOutput $env:XACPX_DAEMON_STDOUT `",
5332
5576
  " -RedirectStandardError $env:XACPX_DAEMON_STDERR `",
@@ -5397,6 +5641,7 @@ async function defaultTerminateProcess(pid) {
5397
5641
  }
5398
5642
  var init_create_daemon_controller = __esm(() => {
5399
5643
  init_daemon_controller();
5644
+ init_private_runtime_dir();
5400
5645
  init_terminate_process_tree();
5401
5646
  });
5402
5647
 
@@ -5433,7 +5678,7 @@ function encodeOrchestrationRpcResponse(response) {
5433
5678
  var init_orchestration_ipc = () => {};
5434
5679
 
5435
5680
  // src/daemon/daemon-files.ts
5436
- import { dirname as dirname4, join as join4 } from "node:path";
5681
+ import { dirname as dirname3, join as join4 } from "node:path";
5437
5682
  function resolveDaemonPaths(options) {
5438
5683
  const runtimeDir = options.runtimeDir ?? (options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : join4(coreHomeDir(options.home), "runtime"));
5439
5684
  return {
@@ -5446,7 +5691,7 @@ function resolveDaemonPaths(options) {
5446
5691
  };
5447
5692
  }
5448
5693
  function resolveRuntimeDirFromConfigPath(configPath) {
5449
- return join4(dirname4(configPath), "runtime");
5694
+ return join4(dirname3(configPath), "runtime");
5450
5695
  }
5451
5696
  function resolveDaemonOrchestrationSocketPath(runtimeDir, platform = process.platform) {
5452
5697
  return resolveOrchestrationEndpoint(runtimeDir, platform).path;
@@ -11959,6 +12204,41 @@ var init_version = __esm(() => {
11959
12204
  XACPX_CORE_VERSION = readVersion();
11960
12205
  });
11961
12206
 
12207
+ // src/orchestration/endpoint-probe.ts
12208
+ import { createConnection } from "node:net";
12209
+ async function canConnectToEndpoint(path3, timeoutMs) {
12210
+ return await new Promise((resolve) => {
12211
+ const socket = createConnection(path3);
12212
+ let settled = false;
12213
+ let timer;
12214
+ const finish = (result) => {
12215
+ if (settled) {
12216
+ return;
12217
+ }
12218
+ settled = true;
12219
+ if (timer) {
12220
+ clearTimeout(timer);
12221
+ }
12222
+ socket.destroy();
12223
+ resolve(result);
12224
+ };
12225
+ if (timeoutMs !== undefined && timeoutMs > 0) {
12226
+ timer = setTimeout(() => finish(true), timeoutMs);
12227
+ timer.unref?.();
12228
+ }
12229
+ socket.once("connect", () => finish(true));
12230
+ socket.once("error", (error2) => {
12231
+ const code = error2.code;
12232
+ if (code === "ENOENT" || code === "ECONNREFUSED") {
12233
+ finish(false);
12234
+ return;
12235
+ }
12236
+ finish(true);
12237
+ });
12238
+ });
12239
+ }
12240
+ var init_endpoint_probe = () => {};
12241
+
11962
12242
  // src/orchestration/task-watch-timeouts.ts
11963
12243
  var DEFAULT_TASK_WATCH_TIMEOUT_MS = 60000, MAX_TASK_WATCH_TIMEOUT_MS, DEFAULT_TASK_WATCH_POLL_INTERVAL_MS = 1000, MAX_TASK_WATCH_POLL_INTERVAL_MS = 1e4, TASK_WATCH_RPC_TIMEOUT_PADDING_MS = 5000;
11964
12244
  var init_task_watch_timeouts = __esm(() => {
@@ -12014,6 +12294,14 @@ function escapeRegExp(s) {
12014
12294
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12015
12295
  }
12016
12296
 
12297
+ // src/orchestration/coordinator-identity.ts
12298
+ function stableCoordinatorSession(transportSession) {
12299
+ return transportSession.replace(/:reset-\d+$/, "");
12300
+ }
12301
+ function sameCoordinatorSession(a, b) {
12302
+ return stableCoordinatorSession(a) === stableCoordinatorSession(b);
12303
+ }
12304
+
12017
12305
  // src/util/text.ts
12018
12306
  function truncateText(text, maxLength, ellipsis = "…") {
12019
12307
  if (text.length <= maxLength)
@@ -12083,10 +12371,24 @@ function createEmptyState() {
12083
12371
  var init_types = () => {};
12084
12372
 
12085
12373
  // src/state/state-store.ts
12086
- import { readFile as readFile5 } from "node:fs/promises";
12374
+ import { readFile as readFile6, rename, writeFile as writeFile3 } from "node:fs/promises";
12087
12375
  function isRecord2(value) {
12088
12376
  return typeof value === "object" && value !== null && !Array.isArray(value);
12089
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
+ }
12090
12392
  function isString(value) {
12091
12393
  return typeof value === "string";
12092
12394
  }
@@ -12191,73 +12493,82 @@ function isHumanQuestionPackageRecord(value) {
12191
12493
  const messages = value.messages;
12192
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);
12193
12495
  }
12194
- function parseOrchestrationState(raw, path3) {
12496
+ function parseOrchestrationState(raw, dropped) {
12195
12497
  if (raw === undefined) {
12196
12498
  return createEmptyOrchestrationState();
12197
12499
  }
12198
12500
  if (!isRecord2(raw)) {
12199
- throw new Error(`state file "${path3}" must contain an object field "orchestration"`);
12200
- }
12201
- const tasks = raw.tasks;
12202
- if (tasks !== undefined && !isRecord2(tasks)) {
12203
- throw new Error(`state file "${path3}" must contain an object field "orchestration.tasks"`);
12204
- }
12205
- const workerBindings = raw.workerBindings;
12206
- if (workerBindings !== undefined && !isRecord2(workerBindings)) {
12207
- throw new Error(`state file "${path3}" must contain an object field "orchestration.workerBindings"`);
12208
- }
12209
- const groups = raw.groups;
12210
- if (groups !== undefined && !isRecord2(groups)) {
12211
- throw new Error(`state file "${path3}" must contain an object field "orchestration.groups"`);
12212
- }
12213
- const humanQuestionPackages = raw.humanQuestionPackages;
12214
- if (humanQuestionPackages !== undefined && !isRecord2(humanQuestionPackages)) {
12215
- throw new Error(`state file "${path3}" must contain an object field "orchestration.humanQuestionPackages"`);
12216
- }
12217
- const coordinatorQuestionState = raw.coordinatorQuestionState;
12218
- if (coordinatorQuestionState !== undefined && !isRecord2(coordinatorQuestionState)) {
12219
- throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorQuestionState"`);
12220
- }
12221
- const coordinatorRoutes = raw.coordinatorRoutes;
12222
- if (coordinatorRoutes !== undefined && !isRecord2(coordinatorRoutes)) {
12223
- throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorRoutes"`);
12224
- }
12225
- const externalCoordinators = raw.externalCoordinators;
12226
- if (externalCoordinators !== undefined && !isRecord2(externalCoordinators)) {
12227
- 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();
12228
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);
12229
12515
  const parsedTasks = {};
12230
- for (const [taskId, task] of Object.entries(tasks ?? {})) {
12516
+ for (const [taskId, task] of Object.entries(tasks)) {
12231
12517
  if (!isTaskRecord(task)) {
12232
- 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;
12233
12524
  }
12234
12525
  parsedTasks[taskId] = task;
12235
12526
  }
12236
12527
  const parsedWorkerBindings = {};
12237
- for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
12528
+ for (const [workerSession, binding] of Object.entries(workerBindings)) {
12238
12529
  if (!isWorkerBindingRecord(binding)) {
12239
- 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;
12240
12536
  }
12241
12537
  parsedWorkerBindings[workerSession] = binding;
12242
12538
  }
12243
12539
  const parsedGroups = {};
12244
- for (const [groupId, group] of Object.entries(groups ?? {})) {
12540
+ for (const [groupId, group] of Object.entries(groups)) {
12245
12541
  if (!isGroupRecord(group)) {
12246
- 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;
12247
12548
  }
12248
12549
  parsedGroups[groupId] = group;
12249
12550
  }
12250
12551
  const parsedHumanQuestionPackages = {};
12251
- for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages ?? {})) {
12552
+ for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages)) {
12252
12553
  if (!isHumanQuestionPackageRecord(packageRecord)) {
12253
- 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;
12254
12560
  }
12255
12561
  parsedHumanQuestionPackages[packageId] = packageRecord;
12256
12562
  }
12257
12563
  const parsedCoordinatorQuestionState = {};
12258
- for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState ?? {})) {
12564
+ for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState)) {
12259
12565
  if (!isCoordinatorQuestionStateRecord(questionState)) {
12260
- 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;
12261
12572
  }
12262
12573
  parsedCoordinatorQuestionState[coordinatorSession] = {
12263
12574
  activePackageId: questionState.activePackageId,
@@ -12265,19 +12576,34 @@ function parseOrchestrationState(raw, path3) {
12265
12576
  };
12266
12577
  }
12267
12578
  const parsedCoordinatorRoutes = {};
12268
- for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes ?? {})) {
12579
+ for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes)) {
12269
12580
  if (!isCoordinatorRouteContextRecord(route)) {
12270
- 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;
12271
12587
  }
12272
12588
  parsedCoordinatorRoutes[coordinatorSession] = route;
12273
12589
  }
12274
12590
  const parsedExternalCoordinators = {};
12275
- for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators ?? {})) {
12591
+ for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators)) {
12276
12592
  if (!isExternalCoordinatorRecord(externalCoordinator)) {
12277
- 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;
12278
12599
  }
12279
12600
  if (externalCoordinator.coordinatorSession !== coordinatorSession) {
12280
- 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;
12281
12607
  }
12282
12608
  parsedExternalCoordinators[coordinatorSession] = externalCoordinator;
12283
12609
  }
@@ -12303,11 +12629,12 @@ function isSessionRecord(value) {
12303
12629
  }
12304
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);
12305
12631
  }
12306
- function parseSessions(raw, path3) {
12632
+ function parseSessions(raw, dropped) {
12307
12633
  const sessions = {};
12308
12634
  for (const [alias, value] of Object.entries(raw)) {
12309
12635
  if (!isSessionRecord(value)) {
12310
- throw new Error(`state file "${path3}" contains malformed session record "${alias}"`);
12636
+ dropped.push({ section: "sessions", key: alias, reason: "malformed session record" });
12637
+ continue;
12311
12638
  }
12312
12639
  sessions[alias] = value;
12313
12640
  }
@@ -12316,11 +12643,12 @@ function parseSessions(raw, path3) {
12316
12643
  function isChatContextRecord(value) {
12317
12644
  return isRecord2(value) && isString(value.current_session);
12318
12645
  }
12319
- function parseChatContexts(raw, path3) {
12646
+ function parseChatContexts(raw, dropped) {
12320
12647
  const chatContexts = {};
12321
12648
  for (const [chatKey, value] of Object.entries(raw)) {
12322
12649
  if (!isChatContextRecord(value)) {
12323
- 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;
12324
12652
  }
12325
12653
  chatContexts[chatKey] = value;
12326
12654
  }
@@ -12361,56 +12689,58 @@ function isScheduledTaskRecord(value) {
12361
12689
  return false;
12362
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);
12363
12691
  }
12364
- function parseScheduledTasks(raw, path3) {
12365
- if (raw === undefined)
12366
- return {};
12367
- if (!isRecord2(raw)) {
12368
- throw new Error(`state file "${path3}" must contain an object field "scheduled_tasks"`);
12369
- }
12692
+ function parseScheduledTasks(raw, dropped) {
12693
+ const source = sectionRecord(raw, "scheduled_tasks", dropped);
12370
12694
  const tasks = {};
12371
- for (const [id, value] of Object.entries(raw)) {
12695
+ for (const [id, value] of Object.entries(source)) {
12372
12696
  if (!isScheduledTaskRecord(value) || value.id !== id) {
12373
- 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;
12374
12699
  }
12375
12700
  tasks[id] = value;
12376
12701
  }
12377
12702
  return tasks;
12378
12703
  }
12379
- function parseState(raw, path3) {
12704
+ function parseState(raw, path3, dropped = []) {
12380
12705
  if (!isRecord2(raw)) {
12381
12706
  throw new Error(`state file "${path3}" must contain a JSON object`);
12382
12707
  }
12383
- const sessions = raw.sessions;
12384
- if (!isRecord2(sessions)) {
12385
- throw new Error(`state file "${path3}" must contain an object field "sessions"`);
12386
- }
12387
- const chatContexts = raw.chat_contexts;
12388
- if (!isRecord2(chatContexts)) {
12389
- throw new Error(`state file "${path3}" must contain an object field "chat_contexts"`);
12390
- }
12391
- const parsedSessions = parseSessions(sessions, path3);
12392
- const orchestration3 = parseOrchestrationState(raw.orchestration, path3);
12393
- 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);
12394
12711
  return {
12395
12712
  sessions: parsedSessions,
12396
- chat_contexts: parseChatContexts(chatContexts, path3),
12713
+ chat_contexts: parseChatContexts(sectionRecord(raw.chat_contexts, "chat_contexts", dropped), dropped),
12397
12714
  native_session_lists: parseNativeSessionLists(raw.native_session_lists),
12398
12715
  orchestration: orchestration3,
12399
- scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
12716
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, dropped)
12400
12717
  };
12401
12718
  }
12402
- function validateExternalCoordinatorIdentityCollisions(sessions, orchestration3, path3) {
12719
+ function repairExternalCoordinatorIdentityCollisions(sessions, orchestration3, dropped) {
12403
12720
  for (const coordinatorSession of Object.keys(orchestration3.externalCoordinators)) {
12404
- if (Object.values(sessions).some((session3) => session3.transport_session === coordinatorSession)) {
12405
- throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a logical session`);
12406
- }
12407
- if (orchestration3.workerBindings[coordinatorSession]) {
12408
- throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a worker binding`);
12409
- }
12410
- if (Object.values(orchestration3.tasks).some((task) => task.workerSession === coordinatorSession && (!isTerminalTaskStatus(task.status) || task.reviewPending !== undefined))) {
12411
- 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;
12412
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
+ });
12731
+ }
12732
+ }
12733
+ function findExternalCoordinatorConflict(coordinatorSession, sessions, orchestration3) {
12734
+ if (Object.values(sessions).some((session3) => session3.transport_session === coordinatorSession)) {
12735
+ return "a logical session";
12413
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;
12414
12744
  }
12415
12745
  function isTerminalTaskStatus(status) {
12416
12746
  return status === "completed" || status === "failed" || status === "cancelled";
@@ -12418,35 +12748,118 @@ function isTerminalTaskStatus(status) {
12418
12748
 
12419
12749
  class StateStore {
12420
12750
  path;
12421
- constructor(path3) {
12751
+ options;
12752
+ loadReport = null;
12753
+ constructor(path3, options = {}) {
12422
12754
  this.path = path3;
12755
+ this.options = options;
12756
+ }
12757
+ get lastLoadReport() {
12758
+ return this.loadReport;
12423
12759
  }
12424
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()}`;
12425
12774
  try {
12426
- const content = await readFile5(this.path, "utf8");
12427
- if (content.trim() === "") {
12428
- return createEmptyState();
12429
- }
12430
- let parsed;
12431
- try {
12432
- parsed = JSON.parse(content);
12433
- } catch (error2) {
12434
- throw new Error(`failed to parse state file "${this.path}"`, {
12435
- cause: error2
12436
- });
12437
- }
12438
- 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");
12439
12803
  } catch (error2) {
12440
12804
  if (error2.code === "ENOENT") {
12441
- return createEmptyState();
12805
+ return { kind: "absent" };
12442
12806
  }
12443
12807
  throw error2;
12444
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 };
12445
12826
  }
12446
12827
  async save(state) {
12447
- 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
+ }
12448
12860
  }
12449
12861
  }
12862
+ var STATE_FILE_VERSION = 1;
12450
12863
  var init_state_store = __esm(() => {
12451
12864
  init_private_file();
12452
12865
  init_types();
@@ -12685,14 +13098,23 @@ class ScheduledTaskService {
12685
13098
  return task;
12686
13099
  });
12687
13100
  }
12688
- listPending() {
13101
+ listPending(chatKey) {
13102
+ return this.listPendingAllChats().filter((task) => task.chat_key === chatKey);
13103
+ }
13104
+ listPendingAllChats() {
12689
13105
  return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
12690
13106
  }
12691
- 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) {
12692
13114
  return await this.mutate(async () => {
12693
13115
  const id = normalizeId(inputId);
12694
13116
  const task = this.state.scheduled_tasks[id];
12695
- if (!task || task.status !== "pending")
13117
+ if (!task || task.status !== "pending" || !allowed(task))
12696
13118
  return false;
12697
13119
  task.status = "cancelled";
12698
13120
  task.cancelled_at = this.now().toISOString();
@@ -12724,7 +13146,7 @@ class ScheduledTaskService {
12724
13146
  async claimDueTasks() {
12725
13147
  return await this.mutate(async () => {
12726
13148
  const nowMs = this.now().getTime();
12727
- 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);
12728
13150
  if (due.length === 0)
12729
13151
  return [];
12730
13152
  const at = this.now().toISOString();
@@ -12733,7 +13155,16 @@ class ScheduledTaskService {
12733
13155
  task.triggered_at = at;
12734
13156
  this.claimedInThisSession.add(task.id);
12735
13157
  }
12736
- 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
+ }
12737
13168
  return due.map((task) => ({ ...task }));
12738
13169
  });
12739
13170
  }
@@ -12791,20 +13222,20 @@ var init_scheduled_service = () => {};
12791
13222
 
12792
13223
  // src/plugins/plugin-home.ts
12793
13224
  import { readFileSync as readFileSync2 } from "node:fs";
12794
- 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";
12795
13226
  import { homedir as homedir3 } from "node:os";
12796
- import { dirname as dirname6, join as join6 } from "node:path";
13227
+ import { dirname as dirname4, join as join6 } from "node:path";
12797
13228
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12798
13229
  function resolveCoreRoot() {
12799
13230
  try {
12800
- let dir = dirname6(fileURLToPath2(import.meta.url));
13231
+ let dir = dirname4(fileURLToPath2(import.meta.url));
12801
13232
  for (let depth = 0;depth < 12; depth++) {
12802
13233
  try {
12803
13234
  const pkg = JSON.parse(readFileSync2(join6(dir, "package.json"), "utf-8"));
12804
13235
  if (pkg.name && CORE_ROOT_NAMES.includes(pkg.name))
12805
13236
  return dir;
12806
13237
  } catch {}
12807
- const parent = dirname6(dir);
13238
+ const parent = dirname4(dir);
12808
13239
  if (parent === dir)
12809
13240
  break;
12810
13241
  dir = parent;
@@ -12822,7 +13253,7 @@ async function ensureCoreResolution(pluginHome) {
12822
13253
  for (const name of SHIM_SPECIFIERS) {
12823
13254
  const targetDir = join6(pluginHome, "node_modules", name);
12824
13255
  const dstJs = join6(targetDir, "plugin-api.js");
12825
- await mkdir6(targetDir, { recursive: true });
13256
+ await mkdir4(targetDir, { recursive: true });
12826
13257
  try {
12827
13258
  await copyFile(srcJs, dstJs);
12828
13259
  } catch (error2) {
@@ -12865,7 +13296,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12865
13296
  const manifestPath = join6(pluginHome, "package.json");
12866
13297
  let raw;
12867
13298
  try {
12868
- raw = await readFile6(manifestPath, "utf8");
13299
+ raw = await readFile7(manifestPath, "utf8");
12869
13300
  } catch {
12870
13301
  return false;
12871
13302
  }
@@ -12883,7 +13314,7 @@ async function normalizePluginHomeManifest(pluginHome) {
12883
13314
  return true;
12884
13315
  }
12885
13316
  async function ensurePluginHome(pluginHome) {
12886
- await mkdir6(pluginHome, { recursive: true, mode: 448 });
13317
+ await mkdir4(pluginHome, { recursive: true, mode: 448 });
12887
13318
  await writeFile4(join6(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
12888
13319
  `, { flag: "wx" }).catch((error2) => {
12889
13320
  if (error2.code !== "EEXIST")
@@ -15499,7 +15930,7 @@ function createConversationExecutor() {
15499
15930
  var DEFAULT_SESSION_KEY = "__chat__";
15500
15931
 
15501
15932
  // src/channels/media-store.ts
15502
- 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";
15503
15934
  import path7 from "node:path";
15504
15935
 
15505
15936
  class RuntimeMediaStore {
@@ -15517,7 +15948,7 @@ class RuntimeMediaStore {
15517
15948
  const safeMessageId = safePathSegment(input.messageId || "message");
15518
15949
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
15519
15950
  const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
15520
- await mkdir7(dir, { recursive: true });
15951
+ await mkdir5(dir, { recursive: true, mode: 448 });
15521
15952
  const resolvedRoot = path7.resolve(this.rootDir);
15522
15953
  const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
15523
15954
  if (!isPathInside(resolvedFile, resolvedRoot)) {
@@ -16906,15 +17337,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16906
17337
  await drainPendingFinalForJx(ctx);
16907
17338
  return { handled: true };
16908
17339
  }
16909
- case "/logout": {
16910
- if (listWeixinAccountIds().length === 0) {
16911
- await sendReply(ctx, t().weixin.noAccountsLoggedIn);
16912
- return { handled: true };
16913
- }
16914
- clearAllWeixinAccounts();
16915
- await sendReply(ctx, t().weixin.logoutSuccess);
16916
- return { handled: true };
16917
- }
16918
17340
  default:
16919
17341
  return { handled: false };
16920
17342
  }
@@ -16927,7 +17349,6 @@ async function handleSlashCommand(content, ctx, receivedAt, eventTimestamp) {
16927
17349
  }
16928
17350
  }
16929
17351
  var init_slash_commands = __esm(() => {
16930
- init_accounts();
16931
17352
  init_logger();
16932
17353
  init_i18n();
16933
17354
  init_final_heads_up();
@@ -16943,14 +17364,14 @@ function normalizeMediaArray(media) {
16943
17364
  }
16944
17365
 
16945
17366
  // src/logging/rotating-file-writer.ts
16946
- import { readdir as readdir2, rename, rm as rm6, stat as stat2 } from "node:fs/promises";
16947
- 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";
16948
17369
  async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
16949
17370
  let currentSize = 0;
16950
17371
  try {
16951
17372
  currentSize = (await stat2(filePath)).size;
16952
17373
  } catch (error2) {
16953
- if (!isMissingFileError2(error2)) {
17374
+ if (!isMissingFileError3(error2)) {
16954
17375
  throw error2;
16955
17376
  }
16956
17377
  }
@@ -16968,24 +17389,24 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
16968
17389
  for (let index = maxFiles - 1;index >= 1; index -= 1) {
16969
17390
  const source = `${filePath}.${index}`;
16970
17391
  try {
16971
- await rename(source, `${filePath}.${index + 1}`);
17392
+ await rename2(source, `${filePath}.${index + 1}`);
16972
17393
  } catch (error2) {
16973
- if (!isMissingFileError2(error2)) {
17394
+ if (!isMissingFileError3(error2)) {
16974
17395
  throw error2;
16975
17396
  }
16976
17397
  }
16977
17398
  }
16978
- await rename(filePath, `${filePath}.1`);
17399
+ await rename2(filePath, `${filePath}.1`);
16979
17400
  }
16980
17401
  async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
16981
- const parentDir = dirname7(filePath);
17402
+ const parentDir = dirname5(filePath);
16982
17403
  const prefix = `${basename(filePath)}.`;
16983
17404
  const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
16984
17405
  let files = [];
16985
17406
  try {
16986
17407
  files = await readdir2(parentDir);
16987
17408
  } catch (error2) {
16988
- if (isMissingFileError2(error2)) {
17409
+ if (isMissingFileError3(error2)) {
16989
17410
  return;
16990
17411
  }
16991
17412
  throw error2;
@@ -16995,23 +17416,31 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
16995
17416
  continue;
16996
17417
  }
16997
17418
  const candidate = join8(parentDir, file);
16998
- 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
+ }
16999
17428
  if (details.mtime.getTime() < cutoff) {
17000
17429
  await rm6(candidate, { force: true });
17001
17430
  }
17002
17431
  }
17003
17432
  }
17004
- function isMissingFileError2(error2) {
17433
+ function isMissingFileError3(error2) {
17005
17434
  return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
17006
17435
  }
17007
17436
  var init_rotating_file_writer = () => {};
17008
17437
 
17009
17438
  // src/perf/perf-log-writer.ts
17010
17439
  import { appendFile as fsAppendFile, mkdir as fsMkdir } from "node:fs/promises";
17011
- import { dirname as dirname8 } from "node:path";
17440
+ import { dirname as dirname6 } from "node:path";
17012
17441
  function createPerfLogWriter(options) {
17013
17442
  const append = options.appendImpl ?? ((p, d) => fsAppendFile(p, d, "utf8"));
17014
- const mkdir8 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17443
+ const mkdir6 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
17015
17444
  return;
17016
17445
  }));
17017
17446
  const now = options.now ?? (() => new Date);
@@ -17058,7 +17487,7 @@ function createPerfLogWriter(options) {
17058
17487
  return;
17059
17488
  const data = batch.join("");
17060
17489
  try {
17061
- await mkdir8(dirname8(options.filePath), { recursive: true });
17490
+ await mkdir6(dirname6(options.filePath), { recursive: true, mode: 448 });
17062
17491
  await rotateIfNeeded(options.filePath, Buffer.byteLength(data), options.maxSizeBytes, options.maxFiles);
17063
17492
  await append(options.filePath, data);
17064
17493
  consecutiveFailures = 0;
@@ -17589,13 +18018,39 @@ async function handleWeixinMessageTurn(full, deps) {
17589
18018
  deps.errLog(`weixin.final.dropped reason=backgrounded_no_store kind=text chatKey=${to}`);
17590
18019
  } else {
17591
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
+ };
17592
18040
  if (rawChunks.length > 0) {
17593
18041
  const total = rawChunks.length;
17594
18042
  if (total === 1) {
17595
18043
  const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
17596
18044
  if (!reserved) {
17597
- finalDropped = true;
17598
- 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
+ }
17599
18054
  } else {
17600
18055
  await sendMessageWeixin({
17601
18056
  to,
@@ -17652,16 +18107,11 @@ ${buildFinalHeadsUp({
17652
18107
  const restToPark = prefixed.slice(sent);
17653
18108
  finalChunksPending = restToPark.length;
17654
18109
  if (restToPark.length > 0 && deps.enqueuePendingFinal) {
17655
- const pending = restToPark.map((text, idx) => {
17656
- const seq = sent + idx + 1;
17657
- const entry = { text, seq, total };
17658
- if (contextToken !== undefined)
17659
- entry.contextToken = contextToken;
17660
- if (deps.accountId !== undefined)
17661
- entry.accountId = deps.accountId;
17662
- return entry;
17663
- });
18110
+ const pending = restToPark.map((text, idx) => buildPendingChunk(text, sent + idx + 1, total));
17664
18111
  deps.enqueuePendingFinal(to, pending);
18112
+ if (sent === 0) {
18113
+ await sendAllParkedNotice(restToPark.length);
18114
+ }
17665
18115
  }
17666
18116
  }
17667
18117
  }
@@ -17857,7 +18307,7 @@ function shouldFetchTypingConfig(textBody) {
17857
18307
  const command = parseSlashCommand(textBody);
17858
18308
  if (!command)
17859
18309
  return true;
17860
- return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug", "/logout"].includes(command);
18310
+ return !["/cancel", "/stop", "/jx", "/echo", "/toggle-debug"].includes(command);
17861
18311
  }
17862
18312
  async function monitorWeixinProvider(opts) {
17863
18313
  const {
@@ -18580,7 +19030,7 @@ ${buildFinalHeadsUp({
18580
19030
  let sent = 0;
18581
19031
  for (let index = 0;index < wave.length; index += 1) {
18582
19032
  if (!deps.reserveFinal(quotaKey)) {
18583
- 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 });
18584
19034
  break;
18585
19035
  }
18586
19036
  const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
@@ -18589,7 +19039,7 @@ ${buildFinalHeadsUp({
18589
19039
  sent += 1;
18590
19040
  }
18591
19041
  const restToPark = chunks.slice(sent);
18592
- if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
19042
+ if (restToPark.length > 0 && deps.enqueuePendingFinal) {
18593
19043
  const pending = restToPark.map((text, index) => {
18594
19044
  const entry = { text, seq: sent + index + 1, total };
18595
19045
  if (deliveryContextToken)
@@ -18599,6 +19049,12 @@ ${buildFinalHeadsUp({
18599
19049
  return entry;
18600
19050
  });
18601
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
+ }
18602
19058
  }
18603
19059
  }
18604
19060
  async function sendTextViaAvailableAccount(text, errorEvent) {
@@ -18636,8 +19092,8 @@ var init_scheduled_turn = __esm(() => {
18636
19092
  });
18637
19093
 
18638
19094
  // src/weixin/monitor/consumer-lock.ts
18639
- import { mkdir as mkdir8, open as open3, readFile as readFile7, rm as rm7 } from "node:fs/promises";
18640
- 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";
18641
19097
  import { homedir as homedir4 } from "node:os";
18642
19098
  function createWeixinConsumerLock(options = {}) {
18643
19099
  const lockFilePath = options.lockFilePath ?? join9(coreHomeDir(homedir4()), "runtime", "weixin-consumer.lock.json");
@@ -18645,7 +19101,7 @@ function createWeixinConsumerLock(options = {}) {
18645
19101
  const onDiagnostic = options.onDiagnostic;
18646
19102
  return {
18647
19103
  async acquire(meta2) {
18648
- await mkdir8(dirname9(lockFilePath), { recursive: true });
19104
+ await mkdir6(dirname7(lockFilePath), { recursive: true, mode: 448 });
18649
19105
  while (true) {
18650
19106
  try {
18651
19107
  const handle = await open3(lockFilePath, "wx");
@@ -18718,7 +19174,7 @@ function createWeixinConsumerLock(options = {}) {
18718
19174
  }
18719
19175
  async function loadLockMetadata(path14) {
18720
19176
  try {
18721
- const raw = await readFile7(path14, "utf8");
19177
+ const raw = await readFile8(path14, "utf8");
18722
19178
  const parsed = JSON.parse(raw);
18723
19179
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
18724
19180
  return null;
@@ -18783,6 +19239,13 @@ class WeixinChannel {
18783
19239
  logout() {
18784
19240
  logout();
18785
19241
  }
19242
+ stop() {
19243
+ this.agent = null;
19244
+ this.quota = null;
19245
+ this.logger = null;
19246
+ this.markDelivered = null;
19247
+ this.markFailed = null;
19248
+ }
18786
19249
  createConsumerLock(options) {
18787
19250
  return createWeixinConsumerLock({
18788
19251
  ...options?.lockFilePath ? { lockFilePath: options.lockFilePath } : {},
@@ -19363,8 +19826,8 @@ var init_bootstrap = __esm(() => {
19363
19826
  });
19364
19827
 
19365
19828
  // src/logging/app-logger.ts
19366
- import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
19367
- 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";
19368
19831
  function createNoopAppLogger() {
19369
19832
  return {
19370
19833
  debug: async () => {},
@@ -19378,6 +19841,7 @@ function createAppLogger(options) {
19378
19841
  const now = options.now ?? (() => new Date);
19379
19842
  let writeChain = Promise.resolve();
19380
19843
  let modeEnsured = false;
19844
+ let writeErrorLatched = false;
19381
19845
  return {
19382
19846
  debug: async (event, message, context) => {
19383
19847
  await enqueueWrite("debug", event, message, context);
@@ -19389,14 +19853,21 @@ function createAppLogger(options) {
19389
19853
  await enqueueWrite("error", event, message, context);
19390
19854
  },
19391
19855
  cleanup: async () => {
19392
- await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19856
+ try {
19857
+ await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays, now);
19858
+ } catch {}
19393
19859
  },
19394
19860
  flush: async () => {
19395
19861
  await writeChain;
19396
19862
  }
19397
19863
  };
19398
19864
  function enqueueWrite(level, event, message, context = {}) {
19399
- 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
+ });
19400
19871
  writeChain = writePromise;
19401
19872
  return writePromise;
19402
19873
  }
@@ -19405,13 +19876,14 @@ function createAppLogger(options) {
19405
19876
  return;
19406
19877
  }
19407
19878
  const line = formatLogLine(now(), level, event, message, context);
19408
- await mkdir9(dirname11(options.filePath), { recursive: true });
19879
+ await mkdir7(dirname9(options.filePath), { recursive: true, mode: 448 });
19409
19880
  if (!modeEnsured) {
19410
19881
  modeEnsured = true;
19411
- await chmod2(options.filePath, 384).catch(() => {});
19882
+ await chmod3(options.filePath, 384).catch(() => {});
19412
19883
  }
19413
19884
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
19414
19885
  await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
19886
+ writeErrorLatched = false;
19415
19887
  }
19416
19888
  }
19417
19889
  function formatLogLine(time3, level, event, message, context) {
@@ -19440,7 +19912,7 @@ var init_app_logger = __esm(() => {
19440
19912
  });
19441
19913
 
19442
19914
  // src/transport/acpx-session-index.ts
19443
- import { readFile as readFile11 } from "node:fs/promises";
19915
+ import { readFile as readFile12 } from "node:fs/promises";
19444
19916
  import { homedir as homedir5 } from "node:os";
19445
19917
  import { resolve as resolve2 } from "node:path";
19446
19918
  async function resolveSessionAgentCommandFromIndex(session3) {
@@ -19449,7 +19921,7 @@ async function resolveSessionAgentCommandFromIndex(session3) {
19449
19921
  return;
19450
19922
  }
19451
19923
  try {
19452
- const raw = await readFile11(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
19924
+ const raw = await readFile12(resolve2(home, ".acpx", "sessions", "index.json"), "utf8");
19453
19925
  const parsed = JSON.parse(raw);
19454
19926
  const targetCwd = resolve2(session3.cwd);
19455
19927
  const match = parsed.entries?.find((entry) => entry.name === session3.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
@@ -19631,7 +20103,9 @@ function parseCommand(input) {
19631
20103
  if (!trimmed.startsWith("/")) {
19632
20104
  return { kind: "prompt", text: trimmed };
19633
20105
  }
19634
- 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) : "";
19635
20109
  const command = normalizeCommand(parts[0] ?? "");
19636
20110
  if (command === "/help" && parts.length === 1)
19637
20111
  return { kind: "help" };
@@ -19740,7 +20214,7 @@ function parseCommand(input) {
19740
20214
  return { kind: "invalid", text: trimmed, recognizedCommand: "/session" };
19741
20215
  }
19742
20216
  if (command === "/group" && parts[1] === "new" && parts.length > 2) {
19743
- const title = parts.slice(2).join(" ");
20217
+ const title = rawTail(2);
19744
20218
  if (title.trim().length > 0) {
19745
20219
  return { kind: "group.new", title };
19746
20220
  }
@@ -19764,7 +20238,7 @@ function parseCommand(input) {
19764
20238
  }
19765
20239
  break;
19766
20240
  }
19767
- const task = parts.slice(index).join(" ");
20241
+ const task = rawTail(index);
19768
20242
  if (groupId.trim().length > 0 && targetAgent.trim().length > 0 && task.trim().length > 0) {
19769
20243
  return {
19770
20244
  kind: "group.delegate",
@@ -19818,7 +20292,7 @@ function parseCommand(input) {
19818
20292
  return { kind: "agent.rm", name: parts[2] };
19819
20293
  }
19820
20294
  if ((command === "/delegate" || command === "/dg") && parts[1]) {
19821
- const parsedDelegate = parseDelegateRequest(parts);
20295
+ const parsedDelegate = parseDelegateRequest(parts, rawTail);
19822
20296
  if (parsedDelegate) {
19823
20297
  return parsedDelegate;
19824
20298
  }
@@ -19846,7 +20320,11 @@ function parseCommand(input) {
19846
20320
  if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
19847
20321
  return { kind: "later.cancel", id: parts[2] };
19848
20322
  }
19849
- 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
+ };
19850
20328
  }
19851
20329
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
19852
20330
  const name = parts[2];
@@ -20099,17 +20577,18 @@ function readNativeAttachCommand(parts, identifierIndex) {
20099
20577
  return { kind: "session.native.attach", identifier };
20100
20578
  }
20101
20579
  function normalizeCommand(command) {
20102
- if (command === "/ss")
20580
+ const lowered = command.toLowerCase();
20581
+ if (lowered === "/ss")
20103
20582
  return "/session";
20104
- if (command === "/ws")
20583
+ if (lowered === "/ws")
20105
20584
  return "/workspace";
20106
- if (command === "/pm")
20585
+ if (lowered === "/pm")
20107
20586
  return "/permission";
20108
- if (command === "/stop")
20587
+ if (lowered === "/stop")
20109
20588
  return "/cancel";
20110
- if (command === "/lt")
20589
+ if (lowered === "/lt")
20111
20590
  return "/later";
20112
- return command;
20591
+ return lowered;
20113
20592
  }
20114
20593
  function isRecognizedCommand(command) {
20115
20594
  return isKnownXacpxCommandPrefix(command);
@@ -20132,31 +20611,41 @@ function toNonInteractivePermission(value) {
20132
20611
  function tokenizeCommand(input) {
20133
20612
  const tokens = [];
20134
20613
  let current = "";
20135
- let quote = null;
20614
+ let start2 = -1;
20615
+ let closingQuote = null;
20616
+ let offset = 0;
20136
20617
  for (const char of input) {
20137
- if (quote) {
20138
- if (char === quote) {
20139
- quote = null;
20618
+ const charStart = offset;
20619
+ offset += char.length;
20620
+ if (closingQuote) {
20621
+ if (char === closingQuote) {
20622
+ closingQuote = null;
20140
20623
  } else {
20141
20624
  current += char;
20142
20625
  }
20143
20626
  continue;
20144
20627
  }
20145
- if (char === '"' || char === "'") {
20146
- quote = char;
20628
+ const close = QUOTE_PAIRS[char];
20629
+ if (close) {
20630
+ if (start2 === -1)
20631
+ start2 = charStart;
20632
+ closingQuote = close;
20147
20633
  continue;
20148
20634
  }
20149
20635
  if (/\s/.test(char)) {
20150
20636
  if (current.length > 0) {
20151
- tokens.push(current);
20637
+ tokens.push({ value: current, start: start2 });
20152
20638
  current = "";
20153
20639
  }
20640
+ start2 = -1;
20154
20641
  continue;
20155
20642
  }
20643
+ if (start2 === -1)
20644
+ start2 = charStart;
20156
20645
  current += char;
20157
20646
  }
20158
20647
  if (current.length > 0) {
20159
- tokens.push(current);
20648
+ tokens.push({ value: current, start: start2 });
20160
20649
  }
20161
20650
  return tokens;
20162
20651
  }
@@ -20199,7 +20688,7 @@ function parseListFilterFlags(parts, validStatuses) {
20199
20688
  }
20200
20689
  return { filter, ok: true };
20201
20690
  }
20202
- function parseDelegateRequest(parts) {
20691
+ function parseDelegateRequest(parts, rawTail) {
20203
20692
  const targetAgent = parts[1];
20204
20693
  if (!targetAgent) {
20205
20694
  return null;
@@ -20227,7 +20716,7 @@ function parseDelegateRequest(parts) {
20227
20716
  }
20228
20717
  break;
20229
20718
  }
20230
- const task = parts.slice(index).join(" ");
20719
+ const task = rawTail(index);
20231
20720
  if (task.trim().length === 0) {
20232
20721
  return null;
20233
20722
  }
@@ -20239,9 +20728,16 @@ function parseDelegateRequest(parts) {
20239
20728
  task
20240
20729
  };
20241
20730
  }
20242
- var TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20731
+ var QUOTE_PAIRS, TASK_STATUS_VALUES, GROUP_STATUS_VALUES;
20243
20732
  var init_parse_command = __esm(() => {
20244
20733
  init_command_list();
20734
+ QUOTE_PAIRS = {
20735
+ '"': '"',
20736
+ "'": "'",
20737
+ "“": "”",
20738
+ "‘": "’",
20739
+ """: """
20740
+ };
20245
20741
  TASK_STATUS_VALUES = [
20246
20742
  "pending",
20247
20743
  "needs_confirmation",
@@ -20254,7 +20750,50 @@ var init_parse_command = __esm(() => {
20254
20750
  });
20255
20751
 
20256
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
+ }
20257
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
+ }
20258
20797
  if (metadata?.chatType !== "group") {
20259
20798
  return { allowed: true };
20260
20799
  }
@@ -20269,7 +20808,14 @@ function authorizeCommandForChat(command, metadata) {
20269
20808
  reason: "group-owner-required"
20270
20809
  };
20271
20810
  }
20272
- 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
+ }
20273
20819
  return [
20274
20820
  `⚠️ ${renderCommandLabel(command)}${t().misc.commandAccessDeniedSuffix}`,
20275
20821
  t().misc.commandAccessDeniedHint
@@ -20395,13 +20941,18 @@ async function handlePermissionModeSet(context, mode) {
20395
20941
  return { text: p.noWritableConfig };
20396
20942
  }
20397
20943
  const previous = cloneAppConfig(context.config);
20944
+ const previousRaw = await context.configStore.getRawValue(["transport", "permissionMode"]);
20398
20945
  const updated = await context.configStore.updateTransport({
20399
20946
  permissionMode: mode
20400
20947
  });
20401
20948
  try {
20402
20949
  await context.transport.updatePermissionPolicy?.(updated.transport);
20403
20950
  } catch (error2) {
20404
- 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
+ }
20405
20956
  context.replaceConfig(previous);
20406
20957
  throw error2;
20407
20958
  }
@@ -20417,13 +20968,18 @@ async function handlePermissionAutoSet(context, policy) {
20417
20968
  return { text: p.noWritableConfig };
20418
20969
  }
20419
20970
  const previous = cloneAppConfig(context.config);
20971
+ const previousRaw = await context.configStore.getRawValue(["transport", "nonInteractivePermissions"]);
20420
20972
  const updated = await context.configStore.updateTransport({
20421
20973
  nonInteractivePermissions: policy
20422
20974
  });
20423
20975
  try {
20424
20976
  await context.transport.updatePermissionPolicy?.(updated.transport);
20425
20977
  } catch (error2) {
20426
- 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
+ }
20427
20983
  context.replaceConfig(previous);
20428
20984
  throw error2;
20429
20985
  }
@@ -20473,99 +21029,83 @@ async function handleConfigSet(context, path14, rawValue) {
20473
21029
  if (!context.config || !context.configStore) {
20474
21030
  return { text: c.noWritableConfig };
20475
21031
  }
20476
- const previous = cloneAppConfig(context.config);
20477
- const updated = cloneAppConfig(context.config);
20478
- const result = applySupportedConfigUpdate(updated, path14, rawValue);
20479
- if ("error" in result) {
20480
- return { text: result.error };
21032
+ const plan = planSupportedConfigUpdate(context.config, path14, rawValue);
21033
+ if ("error" in plan) {
21034
+ return { text: plan.error };
20481
21035
  }
20482
- 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);
20483
21039
  if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
20484
21040
  try {
20485
21041
  await context.transport.updatePermissionPolicy?.(updated.transport);
20486
21042
  } catch (error2) {
20487
- await context.configStore.save(previous);
20488
- 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);
20489
21049
  throw error2;
20490
21050
  }
20491
21051
  }
20492
21052
  context.replaceConfig(updated);
20493
- return { text: c.updated(path14, result.renderedValue) };
21053
+ return { text: c.updated(path14, plan.renderedValue) };
20494
21054
  }
20495
- function applySupportedConfigUpdate(config4, path14, rawValue) {
21055
+ function planSupportedConfigUpdate(config4, path14, rawValue) {
20496
21056
  const c = t().config;
20497
21057
  switch (path14) {
20498
21058
  case "language": {
20499
21059
  if (!isLocale(rawValue))
20500
21060
  return { error: c.languageInvalid };
20501
- config4.language = rawValue;
20502
- return { renderedValue: rawValue };
21061
+ return { rawPath: ["language"], value: rawValue, renderedValue: rawValue };
20503
21062
  }
20504
21063
  case "transport.type": {
20505
21064
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
20506
21065
  if (!parsed)
20507
21066
  return { error: c.transportTypeInvalid };
20508
- config4.transport.type = parsed;
20509
- return { renderedValue: parsed };
21067
+ return { rawPath: ["transport", "type"], value: parsed, renderedValue: parsed };
20510
21068
  }
20511
21069
  case "transport.command":
20512
21070
  if (!rawValue.trim())
20513
21071
  return { error: c.transportCommandEmpty };
20514
- config4.transport.command = rawValue;
20515
- return { renderedValue: rawValue };
21072
+ return { rawPath: ["transport", "command"], value: rawValue, renderedValue: rawValue };
20516
21073
  case "transport.sessionInitTimeoutMs": {
20517
21074
  const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
20518
21075
  if ("error" in parsed)
20519
21076
  return parsed;
20520
- config4.transport.sessionInitTimeoutMs = parsed.value;
20521
- return { renderedValue: String(parsed.value) };
21077
+ return { rawPath: ["transport", "sessionInitTimeoutMs"], value: parsed.value, renderedValue: String(parsed.value) };
20522
21078
  }
20523
21079
  case "transport.permissionMode": {
20524
21080
  const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
20525
21081
  if (!parsed)
20526
21082
  return { error: c.transportPermissionModeInvalid };
20527
- config4.transport.permissionMode = parsed;
20528
- return { renderedValue: parsed };
21083
+ return { rawPath: ["transport", "permissionMode"], value: parsed, renderedValue: parsed };
20529
21084
  }
20530
21085
  case "transport.nonInteractivePermissions": {
20531
21086
  const parsed = parseEnum(rawValue, ["deny", "fail"]);
20532
21087
  if (!parsed)
20533
21088
  return { error: c.transportNonInteractiveInvalid };
20534
- config4.transport.nonInteractivePermissions = parsed;
20535
- return { renderedValue: parsed };
21089
+ return { rawPath: ["transport", "nonInteractivePermissions"], value: parsed, renderedValue: parsed };
20536
21090
  }
20537
21091
  case "transport.permissionPolicy":
20538
21092
  if (!rawValue.trim())
20539
21093
  return { error: c.transportPermissionPolicyEmpty };
20540
- config4.transport.permissionPolicy = rawValue;
20541
- return { renderedValue: rawValue };
21094
+ return { rawPath: ["transport", "permissionPolicy"], value: rawValue, renderedValue: rawValue };
20542
21095
  case "logging.level": {
20543
21096
  const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
20544
21097
  if (!parsed)
20545
21098
  return { error: c.loggingLevelInvalid };
20546
- config4.logging.level = parsed;
20547
- return { renderedValue: parsed };
20548
- }
20549
- case "logging.maxSizeBytes": {
20550
- const parsed = parsePositiveNumber(rawValue, "logging.maxSizeBytes");
20551
- if ("error" in parsed)
20552
- return parsed;
20553
- config4.logging.maxSizeBytes = parsed.value;
20554
- return { renderedValue: String(parsed.value) };
20555
- }
20556
- case "logging.maxFiles": {
20557
- const parsed = parsePositiveNumber(rawValue, "logging.maxFiles");
20558
- if ("error" in parsed)
20559
- return parsed;
20560
- config4.logging.maxFiles = parsed.value;
20561
- return { renderedValue: String(parsed.value) };
21099
+ return { rawPath: ["logging", "level"], value: parsed, renderedValue: parsed };
20562
21100
  }
21101
+ case "logging.maxSizeBytes":
21102
+ case "logging.maxFiles":
20563
21103
  case "logging.retentionDays": {
20564
- const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
21104
+ const field = path14.slice("logging.".length);
21105
+ const parsed = parsePositiveNumber(rawValue, path14);
20565
21106
  if ("error" in parsed)
20566
21107
  return parsed;
20567
- config4.logging.retentionDays = parsed.value;
20568
- return { renderedValue: String(parsed.value) };
21108
+ return { rawPath: ["logging", field], value: parsed.value, renderedValue: String(parsed.value) };
20569
21109
  }
20570
21110
  case "channel.type":
20571
21111
  return { error: c.channelTypeDisabled };
@@ -20573,15 +21113,15 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20573
21113
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20574
21114
  if (!parsed)
20575
21115
  return { error: c.channelReplyModeInvalid };
20576
- config4.channel.replyMode = parsed;
20577
- return { renderedValue: parsed };
21116
+ return { rawPath: ["channel", "replyMode"], value: parsed, renderedValue: parsed };
20578
21117
  }
20579
21118
  case "wechat.replyMode": {
20580
21119
  const parsed = parseEnum(rawValue, ["stream", "final", "verbose"]);
20581
21120
  if (!parsed)
20582
21121
  return { error: c.wechatReplyModeInvalid };
20583
- config4.channel.replyMode = parsed;
20584
21122
  return {
21123
+ rawPath: ["channel", "replyMode"],
21124
+ value: parsed,
20585
21125
  renderedValue: c.wechatReplyModeMapped(parsed)
20586
21126
  };
20587
21127
  }
@@ -20589,42 +21129,30 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20589
21129
  const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
20590
21130
  if (agentMatch) {
20591
21131
  const [, name, field] = agentMatch;
20592
- if (!name || !field) {
21132
+ if (!name || !field || isPrototypePollutingKey(name)) {
20593
21133
  return { error: c.pathNotSupported(path14) };
20594
21134
  }
20595
- const agent3 = config4.agents[name];
20596
- if (!agent3) {
21135
+ if (!Object.hasOwn(config4.agents, name)) {
20597
21136
  return { error: c.agentNotFound(name) };
20598
21137
  }
20599
21138
  if (!rawValue.trim()) {
20600
21139
  return { error: c.fieldEmpty(path14) };
20601
21140
  }
20602
- if (field === "driver") {
20603
- agent3.driver = rawValue;
20604
- } else {
20605
- agent3.command = rawValue;
20606
- }
20607
- return { renderedValue: rawValue };
21141
+ return { rawPath: ["agents", name, field], value: rawValue, renderedValue: rawValue };
20608
21142
  }
20609
21143
  const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
20610
21144
  if (workspaceMatch) {
20611
21145
  const [, name, field] = workspaceMatch;
20612
- if (!name || !field) {
21146
+ if (!name || !field || isPrototypePollutingKey(name)) {
20613
21147
  return { error: c.pathNotSupported(path14) };
20614
21148
  }
20615
- const workspace3 = config4.workspaces[name];
20616
- if (!workspace3) {
21149
+ if (!Object.hasOwn(config4.workspaces, name)) {
20617
21150
  return { error: c.workspaceNotFound(name) };
20618
21151
  }
20619
21152
  if (!rawValue.trim()) {
20620
21153
  return { error: c.fieldEmpty(path14) };
20621
21154
  }
20622
- if (field === "cwd") {
20623
- workspace3.cwd = rawValue;
20624
- } else {
20625
- workspace3.description = rawValue;
20626
- }
20627
- return { renderedValue: rawValue };
21155
+ return { rawPath: ["workspaces", name, field], value: rawValue, renderedValue: rawValue };
20628
21156
  }
20629
21157
  const channelMatch = path14.match(/^channels\.([^.]+)\.replyMode$/);
20630
21158
  if (channelMatch) {
@@ -20640,11 +21168,21 @@ function applySupportedConfigUpdate(config4, path14, rawValue) {
20640
21168
  if (!parsed) {
20641
21169
  return { error: c.channelRuntimeReplyModeInvalid(id) };
20642
21170
  }
20643
- channel.replyMode = parsed;
20644
- 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
+ };
20645
21180
  }
20646
21181
  return { error: c.pathNotSupported(path14) };
20647
21182
  }
21183
+ function isPrototypePollutingKey(key) {
21184
+ return key === "__proto__" || key === "constructor" || key === "prototype";
21185
+ }
20648
21186
  function parseEnum(value, allowed) {
20649
21187
  return allowed.includes(value) ? value : null;
20650
21188
  }
@@ -21186,6 +21724,10 @@ async function handleSessions(context, chatKey) {
21186
21724
  async function handleSessionNew(context, chatKey, alias, agent3, workspace3) {
21187
21725
  const channelId = getChannelIdFromChatKey(chatKey);
21188
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
+ }
21189
21731
  const session3 = context.lifecycle.resolveSession(internalAlias, agent3, workspace3, `${workspace3}:${internalAlias}`);
21190
21732
  const releaseTransportReservation = await context.lifecycle.reserveTransportSession(session3.transportSession);
21191
21733
  try {
@@ -21456,7 +21998,9 @@ async function handleSessionRemove(context, chatKey, alias) {
21456
21998
  }
21457
21999
  }
21458
22000
  const sharedAliasCount = context.sessions.countAliasesSharingTransport(session3.transportSession, internalAlias);
22001
+ const wasCurrentInThisChat = context.sessions.peekCurrentSessionAlias(chatKey) === internalAlias;
21459
22002
  const { wasActive } = await context.sessions.removeSession(internalAlias);
22003
+ const promotedAlias = wasCurrentInThisChat ? context.sessions.peekCurrentSessionAlias(chatKey) || undefined : undefined;
21460
22004
  let orchestrationPurgeWarning;
21461
22005
  if (context.orchestration) {
21462
22006
  try {
@@ -21494,7 +22038,7 @@ async function handleSessionRemove(context, chatKey, alias) {
21494
22038
  const s = t().session;
21495
22039
  const lines = [s.sessionRemoved(alias)];
21496
22040
  if (wasActive) {
21497
- lines.push(s.sessionRemovedWasActive);
22041
+ lines.push(promotedAlias ? s.sessionRemovedWasActivePromoted(toDisplaySessionAlias(promotedAlias)) : s.sessionRemovedWasActive);
21498
22042
  }
21499
22043
  if (!shouldTeardownTransport) {
21500
22044
  lines.push(s.sessionTransportShared(session3.transportSession, sharedAliasCount));
@@ -21516,7 +22060,7 @@ async function promptWithSession(context, session3, chatKey, text, reply, replyC
21516
22060
  if (context.orchestration) {
21517
22061
  try {
21518
22062
  await context.orchestration.recordCoordinatorRouteContext?.({
21519
- coordinatorSession: session3.transportSession,
22063
+ coordinatorSession: stableCoordinatorSession(session3.transportSession),
21520
22064
  chatKey,
21521
22065
  sessionAlias: session3.alias,
21522
22066
  ...replyContextToken ? { replyContextToken } : {},
@@ -21599,7 +22143,7 @@ async function preparePromptWithFallback(context, session3, chatKey, text, reply
21599
22143
  try {
21600
22144
  return await buildCoordinatorPrompt({
21601
22145
  orchestration: orchestration3,
21602
- coordinatorSession: session3.transportSession,
22146
+ coordinatorSession: stableCoordinatorSession(session3.transportSession),
21603
22147
  chatKey,
21604
22148
  userText: text,
21605
22149
  ...replyContextToken ? { replyContextToken } : {},
@@ -21934,6 +22478,7 @@ async function handleDelegateRequest(context, chatKey, targetAgent, task, role,
21934
22478
  if (!session3) {
21935
22479
  return { text: t().orchestration.noCurrentSession };
21936
22480
  }
22481
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
21937
22482
  const orchestration3 = getOrchestration(context);
21938
22483
  if (!orchestration3) {
21939
22484
  return { text: renderOrchestrationUnavailable() };
@@ -21941,7 +22486,7 @@ async function handleDelegateRequest(context, chatKey, targetAgent, task, role,
21941
22486
  const result = await orchestration3.requestDelegate({
21942
22487
  sourceHandle: session3.transportSession,
21943
22488
  sourceKind: "coordinator",
21944
- coordinatorSession: session3.transportSession,
22489
+ coordinatorSession,
21945
22490
  workspace: session3.workspace,
21946
22491
  targetAgent,
21947
22492
  task,
@@ -21958,12 +22503,13 @@ async function handleGroupCreate(context, chatKey, title) {
21958
22503
  if (!session3) {
21959
22504
  return { text: t().orchestration.noCurrentSession };
21960
22505
  }
22506
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
21961
22507
  const orchestration3 = getOrchestration(context);
21962
22508
  if (!orchestration3) {
21963
22509
  return { text: renderOrchestrationUnavailable() };
21964
22510
  }
21965
22511
  const group = await orchestration3.createGroup({
21966
- coordinatorSession: session3.transportSession,
22512
+ coordinatorSession,
21967
22513
  title
21968
22514
  });
21969
22515
  return { text: renderGroupCreated(group) };
@@ -21973,12 +22519,13 @@ async function handleGroupList(context, chatKey, filter) {
21973
22519
  if (!session3) {
21974
22520
  return { text: t().orchestration.noCurrentSession };
21975
22521
  }
22522
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
21976
22523
  const orchestration3 = getOrchestration(context);
21977
22524
  if (!orchestration3) {
21978
22525
  return { text: renderOrchestrationUnavailable() };
21979
22526
  }
21980
22527
  const groups = await orchestration3.listGroupSummaries({
21981
- coordinatorSession: session3.transportSession,
22528
+ coordinatorSession,
21982
22529
  ...filter ?? {}
21983
22530
  });
21984
22531
  return { text: renderGroupList(groups) };
@@ -21988,13 +22535,14 @@ async function handleGroupGet(context, chatKey, groupId) {
21988
22535
  if (!session3) {
21989
22536
  return { text: t().orchestration.noCurrentSession };
21990
22537
  }
22538
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
21991
22539
  const orchestration3 = getOrchestration(context);
21992
22540
  if (!orchestration3) {
21993
22541
  return { text: renderOrchestrationUnavailable() };
21994
22542
  }
21995
22543
  const group = await orchestration3.getGroupSummary({
21996
22544
  groupId,
21997
- coordinatorSession: session3.transportSession
22545
+ coordinatorSession
21998
22546
  });
21999
22547
  if (!group) {
22000
22548
  return { text: t().orchestration.groupNotFound };
@@ -22006,20 +22554,21 @@ async function handleGroupCancel(context, chatKey, groupId) {
22006
22554
  if (!session3) {
22007
22555
  return { text: t().orchestration.noCurrentSession };
22008
22556
  }
22557
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22009
22558
  const orchestration3 = getOrchestration(context);
22010
22559
  if (!orchestration3) {
22011
22560
  return { text: renderOrchestrationUnavailable() };
22012
22561
  }
22013
22562
  const group = await orchestration3.getGroupSummary({
22014
22563
  groupId,
22015
- coordinatorSession: session3.transportSession
22564
+ coordinatorSession
22016
22565
  });
22017
22566
  if (!group) {
22018
22567
  return { text: t().orchestration.groupNotFound };
22019
22568
  }
22020
22569
  const cancelled = await orchestration3.cancelGroup({
22021
22570
  groupId,
22022
- coordinatorSession: session3.transportSession
22571
+ coordinatorSession
22023
22572
  });
22024
22573
  return { text: renderGroupCancelSuccess(cancelled) };
22025
22574
  }
@@ -22028,13 +22577,14 @@ async function handleGroupDelegate(context, chatKey, groupId, targetAgent, task,
22028
22577
  if (!session3) {
22029
22578
  return { text: t().orchestration.noCurrentSession };
22030
22579
  }
22580
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22031
22581
  const orchestration3 = getOrchestration(context);
22032
22582
  if (!orchestration3) {
22033
22583
  return { text: renderOrchestrationUnavailable() };
22034
22584
  }
22035
22585
  const group = await orchestration3.getGroupSummary({
22036
22586
  groupId,
22037
- coordinatorSession: session3.transportSession
22587
+ coordinatorSession
22038
22588
  });
22039
22589
  if (!group) {
22040
22590
  return { text: t().orchestration.groupNotFound };
@@ -22046,12 +22596,13 @@ async function handleTaskList(context, chatKey, filter) {
22046
22596
  if (!session3) {
22047
22597
  return { text: t().orchestration.noCurrentSession };
22048
22598
  }
22599
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22049
22600
  const orchestration3 = getOrchestration(context);
22050
22601
  if (!orchestration3) {
22051
22602
  return { text: renderOrchestrationUnavailable() };
22052
22603
  }
22053
22604
  const tasks = await orchestration3.listTasks({
22054
- coordinatorSession: session3.transportSession,
22605
+ coordinatorSession,
22055
22606
  ...filter ?? {}
22056
22607
  });
22057
22608
  return { text: renderTaskList2(tasks) };
@@ -22061,12 +22612,13 @@ async function handleTaskGet(context, chatKey, taskId) {
22061
22612
  if (!session3) {
22062
22613
  return { text: t().orchestration.noCurrentSession };
22063
22614
  }
22615
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22064
22616
  const orchestration3 = getOrchestration(context);
22065
22617
  if (!orchestration3) {
22066
22618
  return { text: renderOrchestrationUnavailable() };
22067
22619
  }
22068
22620
  const task = await orchestration3.getTask(taskId);
22069
- if (!task || task.coordinatorSession !== session3.transportSession) {
22621
+ if (!task || !sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
22070
22622
  return { text: t().orchestration.taskNotFound };
22071
22623
  }
22072
22624
  return { text: renderTaskSummary2(task) };
@@ -22076,12 +22628,13 @@ async function handleTaskApprove(context, chatKey, taskId) {
22076
22628
  if (!session3) {
22077
22629
  return { text: t().orchestration.noCurrentSession };
22078
22630
  }
22631
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22079
22632
  const orchestration3 = getOrchestration(context);
22080
22633
  if (!orchestration3) {
22081
22634
  return { text: renderOrchestrationUnavailable() };
22082
22635
  }
22083
22636
  const task = await orchestration3.getTask(taskId);
22084
- if (!task || task.coordinatorSession !== session3.transportSession) {
22637
+ if (!task || !sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
22085
22638
  return { text: t().orchestration.taskNotFound };
22086
22639
  }
22087
22640
  if (task.status !== "needs_confirmation") {
@@ -22089,7 +22642,7 @@ async function handleTaskApprove(context, chatKey, taskId) {
22089
22642
  }
22090
22643
  const approved = await orchestration3.approveTask({
22091
22644
  taskId,
22092
- coordinatorSession: session3.transportSession
22645
+ coordinatorSession
22093
22646
  });
22094
22647
  return { text: renderTaskApprovalSuccess2(approved) };
22095
22648
  }
@@ -22098,12 +22651,13 @@ async function handleTaskReject(context, chatKey, taskId) {
22098
22651
  if (!session3) {
22099
22652
  return { text: t().orchestration.noCurrentSession };
22100
22653
  }
22654
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22101
22655
  const orchestration3 = getOrchestration(context);
22102
22656
  if (!orchestration3) {
22103
22657
  return { text: renderOrchestrationUnavailable() };
22104
22658
  }
22105
22659
  const task = await orchestration3.getTask(taskId);
22106
- if (!task || task.coordinatorSession !== session3.transportSession) {
22660
+ if (!task || !sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
22107
22661
  return { text: t().orchestration.taskNotFound };
22108
22662
  }
22109
22663
  if (task.status !== "needs_confirmation") {
@@ -22111,7 +22665,7 @@ async function handleTaskReject(context, chatKey, taskId) {
22111
22665
  }
22112
22666
  const rejected = await orchestration3.cancelTask({
22113
22667
  taskId,
22114
- coordinatorSession: session3.transportSession
22668
+ coordinatorSession
22115
22669
  });
22116
22670
  return { text: renderTaskRejectSuccess(rejected) };
22117
22671
  }
@@ -22120,17 +22674,18 @@ async function handleTaskCancel(context, chatKey, taskId) {
22120
22674
  if (!session3) {
22121
22675
  return { text: t().orchestration.noCurrentSession };
22122
22676
  }
22677
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22123
22678
  const orchestration3 = getOrchestration(context);
22124
22679
  if (!orchestration3) {
22125
22680
  return { text: renderOrchestrationUnavailable() };
22126
22681
  }
22127
22682
  const task = await orchestration3.getTask(taskId);
22128
- if (!task || task.coordinatorSession !== session3.transportSession) {
22683
+ if (!task || !sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
22129
22684
  return { text: t().orchestration.taskNotFound };
22130
22685
  }
22131
22686
  const cancelled = await orchestration3.requestTaskCancellation({
22132
22687
  taskId,
22133
- coordinatorSession: session3.transportSession
22688
+ coordinatorSession
22134
22689
  });
22135
22690
  return { text: renderTaskCancelSuccess(cancelled) };
22136
22691
  }
@@ -22139,11 +22694,12 @@ async function handleTasksClean(context, chatKey) {
22139
22694
  if (!session3) {
22140
22695
  return { text: t().orchestration.noCurrentSession };
22141
22696
  }
22697
+ const coordinatorSession = stableCoordinatorSession(session3.transportSession);
22142
22698
  const orchestration3 = getOrchestration(context);
22143
22699
  if (!orchestration3) {
22144
22700
  return { text: renderOrchestrationUnavailable() };
22145
22701
  }
22146
- const result = await orchestration3.cleanTasks(session3.transportSession);
22702
+ const result = await orchestration3.cleanTasks(coordinatorSession);
22147
22703
  return { text: renderTasksCleanResult(result.removedTasks, result.removedBindings) };
22148
22704
  }
22149
22705
  async function getCurrentSession(context, chatKey) {
@@ -22310,7 +22866,7 @@ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {})
22310
22866
  name = allocateWorkspaceName(base, context.config.workspaces);
22311
22867
  notice = w.nameSanitized(workspaceName, name);
22312
22868
  }
22313
- const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
22869
+ const updated = await context.configStore.upsertWorkspace(name, cwd);
22314
22870
  context.replaceConfig(updated);
22315
22871
  const savedLine = w.saved(name);
22316
22872
  return { text: notice ? `${notice}
@@ -22426,10 +22982,10 @@ function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValu
22426
22982
  if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
22427
22983
  return { ok: false, code: "missing_message" };
22428
22984
  const delta = executeAt.getTime() - now.getTime();
22985
+ if (isNaN(delta) || delta > LATER_MAX_DELAY_MS)
22986
+ return { ok: false, code: "out_of_range" };
22429
22987
  if (delta < LATER_MIN_DELAY_MS)
22430
22988
  return { ok: false, code: "too_soon" };
22431
- if (delta > LATER_MAX_DELAY_MS)
22432
- return { ok: false, code: "out_of_range" };
22433
22989
  return { ok: true, executeAt, messageStartIndex };
22434
22990
  }
22435
22991
  var WEEKDAYS, ZH_MIN = "分钟", ZH_HOUR = "小时", ZH_DAY_UNIT = "天", ZH_TODAY = "今天", ZH_TOMORROW = "明天", ZH_DAY_AFTER = "后天", ZH_AFTER = "后", ZH_RELATIVE_RE;
@@ -22505,16 +23061,17 @@ function laterHelp() {
22505
23061
  function handleLaterHelp() {
22506
23062
  return { text: renderLaterHelp() };
22507
23063
  }
22508
- async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
23064
+ async function handleLaterCreate(tokens, tails, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
22509
23065
  const l = t().later;
22510
- let rest = tokens;
23066
+ let restStart = 0;
22511
23067
  const seenFlags = new Set;
22512
23068
  let flagMode;
22513
- while (rest.length > 0 && (rest[0] === "--bind" || rest[0] === "--temp")) {
22514
- seenFlags.add(rest[0]);
22515
- flagMode = rest[0] === "--bind" ? "bound" : "temp";
22516
- 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;
22517
23073
  }
23074
+ const rest = tokens.slice(restStart);
22518
23075
  if (seenFlags.size > 1) {
22519
23076
  return { text: l.bindAndTempMutuallyExclusive };
22520
23077
  }
@@ -22535,7 +23092,7 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22535
23092
  if (!result.ok) {
22536
23093
  return { text: renderTimeParseError(result.code, result.value) };
22537
23094
  }
22538
- const message = rest.slice(result.messageStartIndex).join(" ").trim();
23095
+ const message = (tails[restStart + result.messageStartIndex] ?? "").trim();
22539
23096
  if (message.startsWith("/")) {
22540
23097
  return {
22541
23098
  text: [
@@ -22559,12 +23116,12 @@ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, def
22559
23116
  });
22560
23117
  return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSession.alias)) };
22561
23118
  }
22562
- function handleLaterList(scheduled) {
22563
- const tasks = scheduled.listPending();
23119
+ function handleLaterList(scheduled, chatKey) {
23120
+ const tasks = scheduled.listPending(chatKey);
22564
23121
  return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
22565
23122
  }
22566
- async function handleLaterCancel(id, scheduled) {
22567
- const ok = await scheduled.cancelPending(id);
23123
+ async function handleLaterCancel(id, scheduled, chatKey) {
23124
+ const ok = await scheduled.cancelPending(id, chatKey);
22568
23125
  const displayId = id.replace(/^#/, "").toLowerCase();
22569
23126
  if (ok) {
22570
23127
  return { text: t().later.cancelSuccess(displayId) };
@@ -23401,7 +23958,7 @@ var init_session_recovery_handler = __esm(() => {
23401
23958
  // src/recovery/auto-install-optional-dep.ts
23402
23959
  import { spawn as spawn5 } from "node:child_process";
23403
23960
  import { createWriteStream } from "node:fs";
23404
- import { mkdir as mkdir10 } from "node:fs/promises";
23961
+ import { mkdir as mkdir8 } from "node:fs/promises";
23405
23962
  import { homedir as homedir6 } from "node:os";
23406
23963
  import { join as join14 } from "node:path";
23407
23964
  async function autoInstallOptionalDep(pkg, parentPackages, options = {}) {
@@ -23519,7 +24076,7 @@ ${err.message}`, reason: "spawn" });
23519
24076
  });
23520
24077
  }, defaultLogSink = async () => {
23521
24078
  const dir = join14(coreHomeDir(homedir6()), "logs");
23522
- await mkdir10(dir, { recursive: true });
24079
+ await mkdir8(dir, { recursive: true });
23523
24080
  const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
23524
24081
  const path14 = join14(dir, `auto-install-${timestamp}.log`);
23525
24082
  const stream = createWriteStream(path14, { flags: "a" });
@@ -23548,7 +24105,7 @@ import { spawn as spawn6 } from "node:child_process";
23548
24105
  import { createRequire as createRequire3 } from "node:module";
23549
24106
  import { access as access3 } from "node:fs/promises";
23550
24107
  import { homedir as homedir7 } from "node:os";
23551
- import { dirname as dirname12, join as join15 } from "node:path";
24108
+ import { dirname as dirname10, join as join15 } from "node:path";
23552
24109
  function deriveParentPackageName(platformPackage) {
23553
24110
  return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
23554
24111
  }
@@ -23621,7 +24178,7 @@ function defaultResolveFromCwd(name, cwd) {
23621
24178
  const pkgJson = require2.resolve(`${name}/package.json`, {
23622
24179
  paths: [cwd, ...require2.resolve.paths(name) ?? []]
23623
24180
  });
23624
- return dirname12(pkgJson);
24181
+ return dirname10(pkgJson);
23625
24182
  } catch {
23626
24183
  return null;
23627
24184
  }
@@ -23756,11 +24313,11 @@ async function handleSessionResetCommand(context, ops, chatKey) {
23756
24313
  chatKey,
23757
24314
  native: wasNative && Boolean(freshAgentSessionId)
23758
24315
  });
23759
- if (wasNative && context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
24316
+ if (context.transport.removeSession && context.sessions.countAliasesSharingTransport(previous.transportSession) === 0) {
23760
24317
  try {
23761
24318
  await context.transport.removeSession(previous);
23762
24319
  } catch (error2) {
23763
- 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", {
23764
24321
  transportSession: previous.transportSession,
23765
24322
  error: error2 instanceof Error ? error2.message : String(error2)
23766
24323
  });
@@ -23822,9 +24379,20 @@ class CommandRouter {
23822
24379
  chatKey,
23823
24380
  kind: command.kind
23824
24381
  });
24382
+ await this.refreshConfigFromStore();
24383
+ perfSpan?.mark("router.config_refreshed");
24384
+ metadata = withEffectiveOwner(metadata, this.config);
23825
24385
  const access4 = authorizeCommandForChat(command, metadata);
23826
24386
  perfSpan?.mark("router.authorized", { decision: access4.allowed ? "allow" : "deny" });
23827
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
+ }
23828
24396
  await this.logger.info("command.blocked", "blocked command by chat policy", {
23829
24397
  chatKey,
23830
24398
  kind: command.kind,
@@ -23832,10 +24400,8 @@ class CommandRouter {
23832
24400
  channel: metadata?.channel,
23833
24401
  senderId: metadata?.senderId
23834
24402
  });
23835
- return { text: renderCommandAccessDenied(command) };
24403
+ return { text: renderCommandAccessDenied(command, access4.reason) };
23836
24404
  }
23837
- await this.refreshConfigFromStore();
23838
- perfSpan?.mark("router.config_refreshed");
23839
24405
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
23840
24406
  switch (command.kind) {
23841
24407
  case "invalid":
@@ -23937,7 +24503,7 @@ class CommandRouter {
23937
24503
  case "later.list":
23938
24504
  if (!this.scheduled)
23939
24505
  return { text: t().later.serviceNotEnabled };
23940
- return handleLaterList(this.scheduled);
24506
+ return handleLaterList(this.scheduled, chatKey);
23941
24507
  case "later.create": {
23942
24508
  if (!this.scheduled)
23943
24509
  return { text: t().later.serviceNotEnabled };
@@ -23945,12 +24511,12 @@ class CommandRouter {
23945
24511
  return { text: renderLaterUnsupportedChannel() };
23946
24512
  }
23947
24513
  const currentSession = await this.sessions.getCurrentSession(chatKey);
23948
- 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);
23949
24515
  }
23950
24516
  case "later.cancel":
23951
24517
  if (!this.scheduled)
23952
24518
  return { text: t().later.serviceNotEnabled };
23953
- return await handleLaterCancel(command.id, this.scheduled);
24519
+ return await handleLaterCancel(command.id, this.scheduled, chatKey);
23954
24520
  case "prompt": {
23955
24521
  const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
23956
24522
  if (metadata?.scheduledSessionDescriptor) {
@@ -24204,7 +24770,7 @@ class CommandRouter {
24204
24770
  return await this.measureTransportCall("has_session", session3, () => this.transport.hasSession(session3));
24205
24771
  }
24206
24772
  async promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan) {
24207
- session3.mcpCoordinatorSession ??= session3.transportSession;
24773
+ session3.mcpCoordinatorSession ??= stableCoordinatorSession(session3.transportSession);
24208
24774
  let done = false;
24209
24775
  let abortRequested = false;
24210
24776
  let cancelOnAbort;
@@ -24454,8 +25020,8 @@ var init_console_agent = __esm(() => {
24454
25020
  });
24455
25021
 
24456
25022
  // src/orchestration/orchestration-server.ts
24457
- import { rm as rm8 } from "node:fs/promises";
24458
- import { createConnection as createConnection2, createServer } from "node:net";
25023
+ import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
25024
+ import { createServer } from "node:net";
24459
25025
 
24460
25026
  class OrchestrationServer {
24461
25027
  endpoint;
@@ -24501,6 +25067,7 @@ class OrchestrationServer {
24501
25067
  });
24502
25068
  });
24503
25069
  await this.listenWithUnixSocketRecovery();
25070
+ await this.hardenUnixSocketPermissions();
24504
25071
  this.started = true;
24505
25072
  }
24506
25073
  async stop() {
@@ -24683,23 +25250,25 @@ class OrchestrationServer {
24683
25250
  if (!task) {
24684
25251
  return null;
24685
25252
  }
24686
- if (task.coordinatorSession !== coordinatorSession) {
25253
+ if (!sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
24687
25254
  return null;
24688
25255
  }
24689
25256
  return task;
24690
25257
  }
24691
25258
  parseRequestDelegateRpcInput(params) {
24692
- requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId"], "params");
25259
+ requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId", "parallel"], "params");
24693
25260
  const cwd = requireOptionalString(params, "cwd");
24694
25261
  const role = requireOptionalString(params, "role");
24695
25262
  const groupId = requireOptionalString(params, "groupId");
25263
+ const parallel = requireOptionalBoolean(params, "parallel");
24696
25264
  return {
24697
25265
  sourceHandle: requireString(params, "sourceHandle"),
24698
25266
  targetAgent: requireString(params, "targetAgent"),
24699
25267
  task: requireString(params, "task"),
24700
25268
  ...cwd !== undefined ? { cwd } : {},
24701
25269
  ...role !== undefined ? { role } : {},
24702
- ...groupId !== undefined ? { groupId } : {}
25270
+ ...groupId !== undefined ? { groupId } : {},
25271
+ ...parallel !== undefined ? { parallel } : {}
24703
25272
  };
24704
25273
  }
24705
25274
  parseTaskListFilter(params) {
@@ -24782,6 +25351,17 @@ class OrchestrationServer {
24782
25351
  whatIsNeeded: requireString(params, "whatIsNeeded")
24783
25352
  };
24784
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
+ }
24785
25365
  async cleanupEndpoint() {
24786
25366
  if (this.endpoint.kind !== "unix") {
24787
25367
  return;
@@ -24948,29 +25528,6 @@ function requireTaskQuestions(params, key) {
24948
25528
  };
24949
25529
  });
24950
25530
  }
24951
- async function canConnectToEndpoint(path14) {
24952
- return await new Promise((resolve3) => {
24953
- const socket = createConnection2(path14);
24954
- let settled = false;
24955
- const finish = (result) => {
24956
- if (settled) {
24957
- return;
24958
- }
24959
- settled = true;
24960
- socket.destroy();
24961
- resolve3(result);
24962
- };
24963
- socket.once("connect", () => finish(true));
24964
- socket.once("error", (error2) => {
24965
- const code = error2.code;
24966
- if (code === "ENOENT" || code === "ECONNREFUSED") {
24967
- finish(false);
24968
- return;
24969
- }
24970
- finish(true);
24971
- });
24972
- });
24973
- }
24974
25531
  async function listen(server, path14) {
24975
25532
  await new Promise((resolve3, reject) => {
24976
25533
  const onError = (error2) => {
@@ -24993,6 +25550,7 @@ var OrchestrationInvalidRequestError, ORCHESTRATION_RPC_METHODS;
24993
25550
  var init_orchestration_server = __esm(() => {
24994
25551
  init_orchestration_ipc();
24995
25552
  init_task_watch_timeouts();
25553
+ init_endpoint_probe();
24996
25554
  OrchestrationInvalidRequestError = class OrchestrationInvalidRequestError extends Error {
24997
25555
  };
24998
25556
  ORCHESTRATION_RPC_METHODS = new Set([
@@ -25193,7 +25751,7 @@ class OrchestrationService {
25193
25751
  async getGroupSummary(input) {
25194
25752
  const state = await this.deps.loadState();
25195
25753
  const group = this.ensureGroups(state)[input.groupId];
25196
- if (!group || group.coordinatorSession !== input.coordinatorSession) {
25754
+ if (!group || !sameCoordinatorSession(group.coordinatorSession, input.coordinatorSession)) {
25197
25755
  return null;
25198
25756
  }
25199
25757
  return this.buildGroupSummary(group, Object.values(state.orchestration.tasks).filter((task) => task.groupId === input.groupId));
@@ -25205,7 +25763,7 @@ class OrchestrationService {
25205
25763
  const now = this.deps.now().getTime();
25206
25764
  const sortField = input.sort ?? "updatedAt";
25207
25765
  const order = input.order ?? "desc";
25208
- return Object.values(this.ensureGroups(state)).filter((group) => group.coordinatorSession === input.coordinatorSession).map((group) => ({
25766
+ return Object.values(this.ensureGroups(state)).filter((group) => sameCoordinatorSession(group.coordinatorSession, input.coordinatorSession)).map((group) => ({
25209
25767
  group,
25210
25768
  summary: this.buildGroupSummary(group, tasks.filter((task) => task.groupId === group.groupId))
25211
25769
  })).filter(({ summary }) => {
@@ -25695,7 +26253,7 @@ class OrchestrationService {
25695
26253
  const workerSession = task.workerSession;
25696
26254
  const taskStillOwnsWorkerSession = current?.workerSession === workerSession;
25697
26255
  const currentBinding = state.orchestration.workerBindings[workerSession];
25698
- const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
26256
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && sameCoordinatorSession(currentBinding.coordinatorSession, task.coordinatorSession) && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
25699
26257
  const otherActiveOwner = Object.values(state.orchestration.tasks).some((candidate) => candidate.taskId !== task.taskId && candidate.workerSession === workerSession && (!this.isTerminalStatus(candidate.status) || candidate.reviewPending !== undefined));
25700
26258
  const restoreOrDeleteBinding = () => {
25701
26259
  if (!bindingStillBelongsToThisStartup || otherActiveOwner) {
@@ -25754,7 +26312,7 @@ class OrchestrationService {
25754
26312
  current.updatedAt = now;
25755
26313
  this.bumpGroupUpdated(state, current.groupId, now);
25756
26314
  const currentBinding = state.orchestration.workerBindings[workerSession];
25757
- const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
26315
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && sameCoordinatorSession(currentBinding.coordinatorSession, task.coordinatorSession) && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
25758
26316
  const otherActiveOwner = Object.values(state.orchestration.tasks).some((candidate) => candidate.taskId !== task.taskId && candidate.workerSession === workerSession && (!this.isTerminalStatus(candidate.status) || candidate.reviewPending !== undefined));
25759
26317
  if (bindingStillBelongsToThisStartup && !otherActiveOwner) {
25760
26318
  if (input.previousBinding) {
@@ -25773,7 +26331,7 @@ class OrchestrationService {
25773
26331
  const state = await this.deps.loadState();
25774
26332
  const workerSession = task.workerSession;
25775
26333
  const currentBinding = state.orchestration.workerBindings[workerSession];
25776
- const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
26334
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && sameCoordinatorSession(currentBinding.coordinatorSession, task.coordinatorSession) && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
25777
26335
  if (!bindingStillBelongsToThisStartup) {
25778
26336
  return false;
25779
26337
  }
@@ -25946,7 +26504,7 @@ class OrchestrationService {
25946
26504
  while (true) {
25947
26505
  const state = await this.deps.loadState();
25948
26506
  const task = state.orchestration.tasks[input.taskId];
25949
- if (!task || task.coordinatorSession !== input.coordinatorSession) {
26507
+ if (!task || !sameCoordinatorSession(task.coordinatorSession, input.coordinatorSession)) {
25950
26508
  return { status: "not_found", task: null, events: [], nextAfterSeq: afterSeq };
25951
26509
  }
25952
26510
  const snapshot = { ...task };
@@ -26004,7 +26562,8 @@ class OrchestrationService {
26004
26562
  return await this.mutate(async () => {
26005
26563
  const state = await this.deps.loadState();
26006
26564
  const now = this.deps.now().toISOString();
26007
- const existing = this.ensureCoordinatorRoutes(state)[input.coordinatorSession];
26565
+ const routeKey = stableCoordinatorSession(input.coordinatorSession);
26566
+ const existing = this.ensureCoordinatorRoutes(state)[routeKey];
26008
26567
  const sameChat = existing?.chatKey === input.chatKey;
26009
26568
  const hasAccountId = input.accountId !== undefined;
26010
26569
  const hasReplyContextToken = input.replyContextToken !== undefined;
@@ -26018,14 +26577,14 @@ class OrchestrationService {
26018
26577
  replyContextToken: existing.replyContextToken
26019
26578
  } : undefined;
26020
26579
  const route = {
26021
- coordinatorSession: input.coordinatorSession,
26580
+ coordinatorSession: routeKey,
26022
26581
  chatKey: input.chatKey,
26023
26582
  ...input.sessionAlias ? { sessionAlias: input.sessionAlias } : {},
26024
26583
  ...replyRoute ? replyRoute : {},
26025
26584
  ...buildCoordinatorRouteChatMetadata(input, sameChat ? existing : undefined),
26026
26585
  updatedAt: now
26027
26586
  };
26028
- this.ensureCoordinatorRoutes(state)[input.coordinatorSession] = route;
26587
+ this.ensureCoordinatorRoutes(state)[routeKey] = route;
26029
26588
  await this.deps.saveState(state);
26030
26589
  return { ...route };
26031
26590
  });
@@ -26269,7 +26828,7 @@ class OrchestrationService {
26269
26828
  return task;
26270
26829
  });
26271
26830
  const now = this.deps.now().toISOString();
26272
- const route = this.snapshotCoordinatorDeliveryRoute(this.ensureCoordinatorRoutes(state)[input.coordinatorSession]);
26831
+ const route = this.snapshotCoordinatorDeliveryRoute(this.ensureCoordinatorRoutes(state)[stableCoordinatorSession(input.coordinatorSession)]);
26273
26832
  if (coordinatorState.activePackageId) {
26274
26833
  const activePackage = this.ensureHumanQuestionPackages(state)[coordinatorState.activePackageId];
26275
26834
  if (!activePackage) {
@@ -26371,7 +26930,7 @@ class OrchestrationService {
26371
26930
  if (!packageRecord) {
26372
26931
  throw new Error(`package "${input.packageId}" does not exist`);
26373
26932
  }
26374
- if (packageRecord.coordinatorSession !== input.coordinatorSession) {
26933
+ if (!sameCoordinatorSession(packageRecord.coordinatorSession, input.coordinatorSession)) {
26375
26934
  throw new Error(`package "${input.packageId}" belongs to coordinator "${packageRecord.coordinatorSession}", not "${input.coordinatorSession}"`);
26376
26935
  }
26377
26936
  if (packageRecord.status !== "active") {
@@ -26386,7 +26945,7 @@ class OrchestrationService {
26386
26945
  }
26387
26946
  let route = this.resolveFrozenPackageMessageRoute(message);
26388
26947
  if (!route) {
26389
- route = this.snapshotCoordinatorDeliveryRoute(this.ensureCoordinatorRoutes(state)[input.coordinatorSession]) ?? null;
26948
+ route = this.snapshotCoordinatorDeliveryRoute(this.ensureCoordinatorRoutes(state)[stableCoordinatorSession(input.coordinatorSession)]) ?? null;
26390
26949
  if (route) {
26391
26950
  Object.assign(message, this.serializeFrozenDeliveryRoute(route));
26392
26951
  }
@@ -26455,7 +27014,7 @@ class OrchestrationService {
26455
27014
  if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
26456
27015
  return null;
26457
27016
  }
26458
- const coordinatorState = state.orchestration.coordinatorQuestionState[coordinatorSession];
27017
+ const coordinatorState = state.orchestration.coordinatorQuestionState[stableCoordinatorSession(coordinatorSession)];
26459
27018
  const activePackageId = coordinatorState?.activePackageId;
26460
27019
  if (!activePackageId) {
26461
27020
  return null;
@@ -26595,7 +27154,7 @@ class OrchestrationService {
26595
27154
  const bindings = state.orchestration.workerBindings;
26596
27155
  const terminalTaskIds = [];
26597
27156
  for (const [taskId, task] of Object.entries(tasks)) {
26598
- if (task.coordinatorSession === coordinatorSession && this.isTerminalStatus(task.status) && task.reviewPending === undefined) {
27157
+ if (sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && this.isTerminalStatus(task.status) && task.reviewPending === undefined) {
26599
27158
  terminalTaskIds.push(taskId);
26600
27159
  }
26601
27160
  }
@@ -26605,7 +27164,7 @@ class OrchestrationService {
26605
27164
  const remainingWorkerSessions = new Set(Object.values(tasks).map((task) => task.workerSession).filter(Boolean));
26606
27165
  let removedBindings = 0;
26607
27166
  for (const [workerSession, binding] of Object.entries(bindings)) {
26608
- if (binding.coordinatorSession !== coordinatorSession) {
27167
+ if (!sameCoordinatorSession(binding.coordinatorSession, coordinatorSession)) {
26609
27168
  continue;
26610
27169
  }
26611
27170
  if (!remainingWorkerSessions.has(workerSession)) {
@@ -26625,16 +27184,17 @@ class OrchestrationService {
26625
27184
  }
26626
27185
  async listSessionBlockingTasks(transportSession) {
26627
27186
  const state = await this.deps.loadState();
26628
- return Object.values(state.orchestration.tasks).filter((task) => (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined) && (task.coordinatorSession === transportSession || task.workerSession === transportSession)).map((task) => ({ ...task }));
27187
+ return Object.values(state.orchestration.tasks).filter((task) => (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined) && (sameCoordinatorSession(task.coordinatorSession, transportSession) || task.workerSession !== undefined && sameCoordinatorSession(task.workerSession, transportSession))).map((task) => ({ ...task }));
26629
27188
  }
26630
27189
  async purgeSessionReferences(transportSession) {
26631
27190
  return await this.mutate(async () => {
26632
27191
  const state = await this.deps.loadState();
27192
+ const sessionIdentity = stableCoordinatorSession(transportSession);
26633
27193
  const tasks = state.orchestration.tasks;
26634
27194
  const bindings = state.orchestration.workerBindings;
26635
27195
  const removedTaskIds = [];
26636
27196
  for (const [taskId, task] of Object.entries(tasks)) {
26637
- if (this.isTerminalStatus(task.status) && task.reviewPending === undefined && (task.coordinatorSession === transportSession || task.workerSession === transportSession)) {
27197
+ if (this.isTerminalStatus(task.status) && task.reviewPending === undefined && (sameCoordinatorSession(task.coordinatorSession, transportSession) || task.workerSession !== undefined && sameCoordinatorSession(task.workerSession, transportSession))) {
26638
27198
  removedTaskIds.push(taskId);
26639
27199
  }
26640
27200
  }
@@ -26644,14 +27204,14 @@ class OrchestrationService {
26644
27204
  const remainingWorkerSessions = new Set(Object.values(tasks).map((task) => task.workerSession).filter(Boolean));
26645
27205
  let removedBindings = 0;
26646
27206
  for (const [workerSession, binding] of Object.entries(bindings)) {
26647
- const shouldPurgeBinding = workerSession === transportSession || binding.coordinatorSession === transportSession;
27207
+ const shouldPurgeBinding = sameCoordinatorSession(workerSession, transportSession) || sameCoordinatorSession(binding.coordinatorSession, transportSession);
26648
27208
  if (shouldPurgeBinding && !remainingWorkerSessions.has(workerSession)) {
26649
27209
  delete bindings[workerSession];
26650
27210
  removedBindings += 1;
26651
27211
  }
26652
27212
  }
26653
- const removedEmptyGroups = this.removeEmptyGroupsForCoordinator(state, transportSession);
26654
- const removedCoordinatorMetadata = this.removeCoordinatorMetadataIfUnused(state, transportSession);
27213
+ const removedEmptyGroups = this.removeEmptyGroupsForCoordinator(state, sessionIdentity);
27214
+ const removedCoordinatorMetadata = this.removeCoordinatorMetadataIfUnused(state, sessionIdentity);
26655
27215
  if (removedTaskIds.length > 0 || removedBindings > 0 || removedEmptyGroups || removedCoordinatorMetadata) {
26656
27216
  await this.deps.saveState(state);
26657
27217
  }
@@ -26661,106 +27221,28 @@ class OrchestrationService {
26661
27221
  };
26662
27222
  });
26663
27223
  }
26664
- async purgeExpiredResetCoordinators(input) {
26665
- try {
26666
- if (!Number.isFinite(input.cutoffDays) || input.cutoffDays < 0) {
26667
- throw new Error(`cutoffDays must be a non-negative number, got ${String(input.cutoffDays)}`);
26668
- }
26669
- const result = await this.mutate(async () => {
26670
- const state = await this.deps.loadState();
26671
- const candidates = this.collectResetCoordinatorCandidates(state);
26672
- const activeTransportSessions = new Set(Object.values(state.sessions).map((session3) => session3.transport_session));
26673
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
26674
- const cutoffMs = input.cutoffDays * MS_PER_DAY;
26675
- const nowMs = this.deps.now().getTime();
26676
- let purgedCoordinators = 0;
26677
- const removed = {
26678
- tasks: 0,
26679
- workerBindings: 0,
26680
- groups: 0,
26681
- coordinatorRoutes: 0,
26682
- humanQuestionPackages: 0,
26683
- coordinatorQuestionState: 0
26684
- };
26685
- for (const coordinatorSession of candidates) {
26686
- if (activeTransportSessions.has(coordinatorSession)) {
26687
- continue;
26688
- }
26689
- const activityAtMs = this.resolveResetCoordinatorActivityAtMs(state, coordinatorSession);
26690
- if (activityAtMs === null) {
26691
- continue;
26692
- }
26693
- if (nowMs - activityAtMs <= cutoffMs) {
26694
- continue;
26695
- }
26696
- const delta = this.cascadeRemoveCoordinatorRecords(state, coordinatorSession);
26697
- const changed = delta.tasks > 0 || delta.workerBindings > 0 || delta.groups > 0 || delta.coordinatorRoutes > 0 || delta.humanQuestionPackages > 0 || delta.coordinatorQuestionState > 0;
26698
- if (!changed) {
26699
- continue;
26700
- }
26701
- purgedCoordinators += 1;
26702
- removed.tasks += delta.tasks;
26703
- removed.workerBindings += delta.workerBindings;
26704
- removed.groups += delta.groups;
26705
- removed.coordinatorRoutes += delta.coordinatorRoutes;
26706
- removed.humanQuestionPackages += delta.humanQuestionPackages;
26707
- removed.coordinatorQuestionState += delta.coordinatorQuestionState;
26708
- }
26709
- const removedAny = removed.tasks > 0 || removed.workerBindings > 0 || removed.groups > 0 || removed.coordinatorRoutes > 0 || removed.humanQuestionPackages > 0 || removed.coordinatorQuestionState > 0;
26710
- if (removedAny) {
26711
- await this.deps.saveState(state);
26712
- }
26713
- return {
26714
- candidates: candidates.length,
26715
- purgedCoordinators,
26716
- removed
26717
- };
26718
- });
26719
- if (this.deps.logger) {
26720
- this.deps.logger.info("orchestration.reset_gc.completed", "reset coordinator gc completed", {
26721
- trigger: input.trigger,
26722
- cutoffDays: input.cutoffDays,
26723
- candidates: result.candidates,
26724
- purgedCoordinators: result.purgedCoordinators,
26725
- deletedCounts: result.removed
26726
- });
26727
- }
26728
- return result;
26729
- } catch (error2) {
26730
- const logger2 = this.deps.logger;
26731
- if (logger2) {
26732
- const message = error2 instanceof Error ? error2.message : String(error2);
26733
- logger2.error("orchestration.reset_gc.failed", "reset coordinator gc failed", {
26734
- trigger: input.trigger,
26735
- cutoffDays: input.cutoffDays,
26736
- message
26737
- });
26738
- }
26739
- throw error2;
26740
- }
26741
- }
26742
27224
  async listPendingCoordinatorResults(coordinatorSession) {
26743
27225
  const state = await this.deps.loadState();
26744
27226
  if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
26745
27227
  return [];
26746
27228
  }
26747
- return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && this.canInjectTaskIntoCoordinator(state, task) && (task.injectionPending === true || task.coordinatorInjectedAt === undefined)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
27229
+ return Object.values(state.orchestration.tasks).filter((task) => sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && this.canInjectTaskIntoCoordinator(state, task) && (task.injectionPending === true || task.coordinatorInjectedAt === undefined)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
26748
27230
  }
26749
27231
  async listPendingCoordinatorBlockers(coordinatorSession) {
26750
27232
  const state = await this.deps.loadState();
26751
27233
  if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
26752
27234
  return [];
26753
27235
  }
26754
- const coordinatorState = state.orchestration.coordinatorQuestionState[coordinatorSession];
27236
+ const coordinatorState = state.orchestration.coordinatorQuestionState[stableCoordinatorSession(coordinatorSession)];
26755
27237
  const hiddenQueuedQuestionKeys = coordinatorState?.activePackageId ? new Set((coordinatorState.queuedQuestions ?? []).map((entry) => `${entry.taskId}:${entry.questionId}`)) : null;
26756
- return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.status === "blocked" && task.openQuestion?.status === "open" && !hiddenQueuedQuestionKeys?.has(`${task.taskId}:${task.openQuestion.questionId}`)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
27238
+ return Object.values(state.orchestration.tasks).filter((task) => sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && task.status === "blocked" && task.openQuestion?.status === "open" && !hiddenQueuedQuestionKeys?.has(`${task.taskId}:${task.openQuestion.questionId}`)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
26757
27239
  }
26758
27240
  async listContestedCoordinatorResults(coordinatorSession) {
26759
27241
  const state = await this.deps.loadState();
26760
27242
  if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
26761
27243
  return [];
26762
27244
  }
26763
- return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.reviewPending !== undefined).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
27245
+ return Object.values(state.orchestration.tasks).filter((task) => sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && task.reviewPending !== undefined).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
26764
27246
  }
26765
27247
  async listPendingCoordinatorGroups(coordinatorSession) {
26766
27248
  const state = await this.deps.loadState();
@@ -26769,7 +27251,7 @@ class OrchestrationService {
26769
27251
  }
26770
27252
  const groups = this.ensureGroups(state);
26771
27253
  const tasks = Object.values(state.orchestration.tasks);
26772
- return Object.values(groups).filter((group) => group.coordinatorSession === coordinatorSession).filter((group) => {
27254
+ return Object.values(groups).filter((group) => sameCoordinatorSession(group.coordinatorSession, coordinatorSession)).filter((group) => {
26773
27255
  const groupTasks = tasks.filter((task) => task.groupId === group.groupId);
26774
27256
  return this.canInjectGroupIntoCoordinator(state, group.groupId, groupTasks);
26775
27257
  }).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((group) => ({ ...group }));
@@ -26984,7 +27466,7 @@ class OrchestrationService {
26984
27466
  if (input.sourceHandle !== undefined && task.sourceHandle !== input.sourceHandle) {
26985
27467
  throw new Error(`task "${input.taskId}" belongs to source "${task.sourceHandle}", not "${input.sourceHandle}"`);
26986
27468
  }
26987
- if (input.coordinatorSession !== undefined && task.coordinatorSession !== input.coordinatorSession) {
27469
+ if (input.coordinatorSession !== undefined && !sameCoordinatorSession(task.coordinatorSession, input.coordinatorSession)) {
26988
27470
  throw new Error(`task "${input.taskId}" belongs to coordinator "${task.coordinatorSession}", not "${input.coordinatorSession}"`);
26989
27471
  }
26990
27472
  if (this.isTerminalStatus(task.status)) {
@@ -27411,11 +27893,11 @@ class OrchestrationService {
27411
27893
  ...binding.cwd ? { cwd: binding.cwd } : {}
27412
27894
  };
27413
27895
  }
27414
- const coordinatorSession = Object.values(state.sessions).find((session3) => session3.transport_session === sourceHandle);
27896
+ const coordinatorSession = Object.values(state.sessions).find((session3) => sameCoordinatorSession(session3.transport_session, sourceHandle));
27415
27897
  if (coordinatorSession) {
27416
27898
  return {
27417
27899
  sourceKind: "coordinator",
27418
- coordinatorSession: sourceHandle,
27900
+ coordinatorSession: stableCoordinatorSession(sourceHandle),
27419
27901
  workspace: coordinatorSession.workspace
27420
27902
  };
27421
27903
  }
@@ -27453,7 +27935,7 @@ class OrchestrationService {
27453
27935
  if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
27454
27936
  throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
27455
27937
  }
27456
- const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running" || task.status === "queued"));
27938
+ const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running" || task.status === "queued"));
27457
27939
  if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
27458
27940
  throw new Error("agent-requested delegation quota exceeded for this coordinator");
27459
27941
  }
@@ -27557,13 +28039,13 @@ class OrchestrationService {
27557
28039
  if (!filter) {
27558
28040
  return true;
27559
28041
  }
27560
- return (filter.sourceHandle === undefined || task.sourceHandle === filter.sourceHandle) && (filter.coordinatorSession === undefined || task.coordinatorSession === filter.coordinatorSession) && (filter.workspace === undefined || task.workspace === filter.workspace) && (filter.targetAgent === undefined || task.targetAgent === filter.targetAgent) && (filter.role === undefined || task.role === filter.role) && (filter.status === undefined || task.status === filter.status);
28042
+ return (filter.sourceHandle === undefined || task.sourceHandle === filter.sourceHandle) && (filter.coordinatorSession === undefined || sameCoordinatorSession(task.coordinatorSession, filter.coordinatorSession)) && (filter.workspace === undefined || task.workspace === filter.workspace) && (filter.targetAgent === undefined || task.targetAgent === filter.targetAgent) && (filter.role === undefined || task.role === filter.role) && (filter.status === undefined || task.status === filter.status);
27561
28043
  }
27562
28044
  isTerminalStatus(status) {
27563
28045
  return status === "completed" || status === "failed" || status === "cancelled";
27564
28046
  }
27565
28047
  assertCoordinatorOwnership(task, coordinatorSession) {
27566
- if (task.coordinatorSession !== coordinatorSession) {
28048
+ if (!sameCoordinatorSession(task.coordinatorSession, coordinatorSession)) {
27567
28049
  throw new Error(`task "${task.taskId}" belongs to coordinator "${task.coordinatorSession}", not "${coordinatorSession}"`);
27568
28050
  }
27569
28051
  }
@@ -27576,7 +28058,7 @@ class OrchestrationService {
27576
28058
  if (!group) {
27577
28059
  throw new Error(`group "${groupId}" does not exist`);
27578
28060
  }
27579
- if (group.coordinatorSession !== coordinatorSession) {
28061
+ if (!sameCoordinatorSession(group.coordinatorSession, coordinatorSession)) {
27580
28062
  throw new Error(`group "${groupId}" belongs to coordinator "${group.coordinatorSession}", not "${coordinatorSession}"`);
27581
28063
  }
27582
28064
  }
@@ -27587,13 +28069,14 @@ class OrchestrationService {
27587
28069
  return state.orchestration.humanQuestionPackages;
27588
28070
  }
27589
28071
  ensureCoordinatorQuestionState(state, coordinatorSession) {
28072
+ const key = stableCoordinatorSession(coordinatorSession);
27590
28073
  if (!("coordinatorQuestionState" in state.orchestration) || !state.orchestration.coordinatorQuestionState) {
27591
28074
  state.orchestration.coordinatorQuestionState = {};
27592
28075
  }
27593
- state.orchestration.coordinatorQuestionState[coordinatorSession] ??= {
28076
+ state.orchestration.coordinatorQuestionState[key] ??= {
27594
28077
  queuedQuestions: []
27595
28078
  };
27596
- return state.orchestration.coordinatorQuestionState[coordinatorSession];
28079
+ return state.orchestration.coordinatorQuestionState[key];
27597
28080
  }
27598
28081
  ensureCoordinatorRoutes(state) {
27599
28082
  if (!("coordinatorRoutes" in state.orchestration) || !state.orchestration.coordinatorRoutes) {
@@ -27774,7 +28257,7 @@ class OrchestrationService {
27774
28257
  const referencedGroupIds = new Set(Object.values(state.orchestration.tasks).map((task) => task.groupId).filter((groupId) => typeof groupId === "string"));
27775
28258
  let removedAny = false;
27776
28259
  for (const [groupId, group] of Object.entries(groups)) {
27777
- if (group.coordinatorSession !== coordinatorSession) {
28260
+ if (!sameCoordinatorSession(group.coordinatorSession, coordinatorSession)) {
27778
28261
  continue;
27779
28262
  }
27780
28263
  if (!referencedGroupIds.has(groupId)) {
@@ -27785,125 +28268,30 @@ class OrchestrationService {
27785
28268
  return removedAny;
27786
28269
  }
27787
28270
  removeCoordinatorMetadataIfUnused(state, coordinatorSession) {
27788
- const hasCoordinatorTasks = Object.values(state.orchestration.tasks).some((task) => task.coordinatorSession === coordinatorSession);
27789
- const hasCoordinatorBindings = Object.values(state.orchestration.workerBindings).some((binding) => binding.coordinatorSession === coordinatorSession);
28271
+ const key = stableCoordinatorSession(coordinatorSession);
28272
+ const hasCoordinatorTasks = Object.values(state.orchestration.tasks).some((task) => sameCoordinatorSession(task.coordinatorSession, coordinatorSession));
28273
+ const hasCoordinatorBindings = Object.values(state.orchestration.workerBindings).some((binding) => sameCoordinatorSession(binding.coordinatorSession, coordinatorSession));
27790
28274
  if (hasCoordinatorTasks || hasCoordinatorBindings) {
27791
28275
  return false;
27792
28276
  }
27793
28277
  let removedAny = false;
27794
28278
  const packages = this.ensureHumanQuestionPackages(state);
27795
28279
  for (const [packageId, packageRecord] of Object.entries(packages)) {
27796
- if (packageRecord.coordinatorSession === coordinatorSession) {
28280
+ if (sameCoordinatorSession(packageRecord.coordinatorSession, coordinatorSession)) {
27797
28281
  delete packages[packageId];
27798
28282
  removedAny = true;
27799
28283
  }
27800
28284
  }
27801
- if (state.orchestration.coordinatorQuestionState?.[coordinatorSession] !== undefined) {
27802
- delete state.orchestration.coordinatorQuestionState[coordinatorSession];
28285
+ if (state.orchestration.coordinatorQuestionState?.[key] !== undefined) {
28286
+ delete state.orchestration.coordinatorQuestionState[key];
27803
28287
  removedAny = true;
27804
28288
  }
27805
- if (state.orchestration.coordinatorRoutes?.[coordinatorSession] !== undefined) {
27806
- delete state.orchestration.coordinatorRoutes[coordinatorSession];
28289
+ if (state.orchestration.coordinatorRoutes?.[key] !== undefined) {
28290
+ delete state.orchestration.coordinatorRoutes[key];
27807
28291
  removedAny = true;
27808
28292
  }
27809
28293
  return removedAny;
27810
28294
  }
27811
- isResetCoordinatorSession(coordinatorSession) {
27812
- return coordinatorSession.includes(":reset-");
27813
- }
27814
- collectResetCoordinatorCandidates(state) {
27815
- const candidates = new Set;
27816
- for (const coordinatorSession of Object.keys(state.orchestration.coordinatorRoutes ?? {})) {
27817
- if (this.isResetCoordinatorSession(coordinatorSession)) {
27818
- candidates.add(coordinatorSession);
27819
- }
27820
- }
27821
- for (const task of Object.values(state.orchestration.tasks ?? {})) {
27822
- if (this.isResetCoordinatorSession(task.coordinatorSession)) {
27823
- candidates.add(task.coordinatorSession);
27824
- }
27825
- }
27826
- return [...candidates];
27827
- }
27828
- parseDateMs(value) {
27829
- if (!value) {
27830
- return null;
27831
- }
27832
- const ms = new Date(value).getTime();
27833
- return Number.isFinite(ms) ? ms : null;
27834
- }
27835
- resolveResetCoordinatorActivityAtMs(state, coordinatorSession) {
27836
- const routeUpdatedAt = state.orchestration.coordinatorRoutes?.[coordinatorSession]?.updatedAt;
27837
- const routeMs = this.parseDateMs(routeUpdatedAt);
27838
- let tasksMs = null;
27839
- for (const task of Object.values(state.orchestration.tasks ?? {})) {
27840
- if (task.coordinatorSession !== coordinatorSession) {
27841
- continue;
27842
- }
27843
- const candidate = this.parseDateMs(task.updatedAt);
27844
- if (candidate === null) {
27845
- continue;
27846
- }
27847
- tasksMs = tasksMs === null ? candidate : Math.max(tasksMs, candidate);
27848
- }
27849
- if (routeMs === null && tasksMs === null) {
27850
- return null;
27851
- }
27852
- if (routeMs === null) {
27853
- return tasksMs;
27854
- }
27855
- if (tasksMs === null) {
27856
- return routeMs;
27857
- }
27858
- return Math.max(routeMs, tasksMs);
27859
- }
27860
- cascadeRemoveCoordinatorRecords(state, coordinatorSession) {
27861
- const removed = {
27862
- tasks: 0,
27863
- workerBindings: 0,
27864
- groups: 0,
27865
- coordinatorRoutes: 0,
27866
- humanQuestionPackages: 0,
27867
- coordinatorQuestionState: 0
27868
- };
27869
- const tasks = state.orchestration.tasks;
27870
- for (const [taskId, task] of Object.entries(tasks ?? {})) {
27871
- if (task.coordinatorSession === coordinatorSession) {
27872
- delete tasks[taskId];
27873
- removed.tasks += 1;
27874
- }
27875
- }
27876
- const workerBindings = state.orchestration.workerBindings;
27877
- for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
27878
- if (binding.coordinatorSession === coordinatorSession) {
27879
- delete workerBindings[workerSession];
27880
- removed.workerBindings += 1;
27881
- }
27882
- }
27883
- const groups = this.ensureGroups(state);
27884
- for (const [groupId, group] of Object.entries(groups ?? {})) {
27885
- if (group.coordinatorSession === coordinatorSession) {
27886
- delete groups[groupId];
27887
- removed.groups += 1;
27888
- }
27889
- }
27890
- if (state.orchestration.coordinatorRoutes?.[coordinatorSession] !== undefined) {
27891
- delete state.orchestration.coordinatorRoutes[coordinatorSession];
27892
- removed.coordinatorRoutes += 1;
27893
- }
27894
- const packages = this.ensureHumanQuestionPackages(state);
27895
- for (const [packageId, packageRecord] of Object.entries(packages ?? {})) {
27896
- if (packageRecord.coordinatorSession === coordinatorSession) {
27897
- delete packages[packageId];
27898
- removed.humanQuestionPackages += 1;
27899
- }
27900
- }
27901
- if (state.orchestration.coordinatorQuestionState?.[coordinatorSession] !== undefined) {
27902
- delete state.orchestration.coordinatorQuestionState[coordinatorSession];
27903
- removed.coordinatorQuestionState += 1;
27904
- }
27905
- return removed;
27906
- }
27907
28295
  bumpGroupUpdated(state, groupId, now) {
27908
28296
  if (!groupId) {
27909
28297
  return;
@@ -28077,7 +28465,7 @@ class OrchestrationService {
28077
28465
  }
28078
28466
  const validQueuedQuestions = coordinatorState.queuedQuestions.filter((entry) => {
28079
28467
  const task = state.orchestration.tasks[entry.taskId];
28080
- return task?.coordinatorSession === coordinatorSession && task.status === "blocked" && task.openQuestion?.status === "open" && task.openQuestion.questionId === entry.questionId;
28468
+ return task !== undefined && sameCoordinatorSession(task.coordinatorSession, coordinatorSession) && task.status === "blocked" && task.openQuestion?.status === "open" && task.openQuestion.questionId === entry.questionId;
28081
28469
  });
28082
28470
  if (validQueuedQuestions.length !== coordinatorState.queuedQuestions.length) {
28083
28471
  coordinatorState.queuedQuestions = validQueuedQuestions;
@@ -28455,18 +28843,39 @@ class ScheduledTaskScheduler {
28455
28843
  return;
28456
28844
  this.ticking = true;
28457
28845
  try {
28458
- 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
+ }
28459
28853
  for (const task of dueTasks) {
28460
28854
  try {
28461
28855
  await this.dispatchWithTimeout(task);
28462
- await this.service.markExecuted(task.id);
28463
28856
  } catch (error2) {
28464
28857
  const message = error2 instanceof Error ? error2.message : String(error2);
28465
28858
  await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
28466
28859
  taskId: task.id,
28467
28860
  message
28468
28861
  });
28469
- 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
+ });
28470
28879
  }
28471
28880
  }
28472
28881
  } finally {
@@ -28566,7 +28975,7 @@ async function createScheduledTaskFromRoute(input, deps) {
28566
28975
  if (coordinatorSession.length === 0) {
28567
28976
  throw new Error("coordinatorSession must be a non-empty string");
28568
28977
  }
28569
- const route = deps.state.orchestration.coordinatorRoutes[coordinatorSession];
28978
+ const route = deps.state.orchestration.coordinatorRoutes[stableCoordinatorSession(coordinatorSession)];
28570
28979
  if (!route) {
28571
28980
  throw new Error(`no chat route is recorded for coordinator session "${coordinatorSession}"`);
28572
28981
  }
@@ -28593,7 +29002,7 @@ async function createScheduledTaskFromRoute(input, deps) {
28593
29002
  if (!session3) {
28594
29003
  throw new Error(`session "${route.sessionAlias}" recorded for coordinator session "${coordinatorSession}" was not found`);
28595
29004
  }
28596
- if (session3.transportSession !== coordinatorSession) {
29005
+ if (!sameCoordinatorSession(session3.transportSession, coordinatorSession)) {
28597
29006
  throw new Error(`session "${route.sessionAlias}" is no longer attached to coordinator session "${coordinatorSession}"`);
28598
29007
  }
28599
29008
  const executeAt = parseRouteScheduledTime(input.timeText, deps.now?.() ?? new Date);
@@ -28646,12 +29055,12 @@ var init_scheduled_route_create = __esm(() => {
28646
29055
 
28647
29056
  // src/scheduled/scheduled-route-manage.ts
28648
29057
  async function listScheduledTasksFromRoute(input, deps) {
28649
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
28650
- return deps.scheduled.listPending();
29058
+ const route = resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
29059
+ return deps.scheduled.listPending(route.chatKey);
28651
29060
  }
28652
29061
  async function cancelScheduledTaskFromRoute(input, deps) {
28653
- resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
28654
- 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);
28655
29064
  return { id: normalizeId(input.id), cancelled };
28656
29065
  }
28657
29066
  function resolveOwnedCoordinatorRoute(coordinatorSession, state, label) {
@@ -28659,7 +29068,7 @@ function resolveOwnedCoordinatorRoute(coordinatorSession, state, label) {
28659
29068
  if (session3.length === 0) {
28660
29069
  throw new Error("coordinatorSession must be a non-empty string");
28661
29070
  }
28662
- const route = state.orchestration.coordinatorRoutes[session3];
29071
+ const route = state.orchestration.coordinatorRoutes[stableCoordinatorSession(session3)];
28663
29072
  if (!route) {
28664
29073
  throw new Error(`no chat route is recorded for coordinator session "${session3}"`);
28665
29074
  }
@@ -28696,25 +29105,37 @@ class SessionService {
28696
29105
  const seen = new Set;
28697
29106
  const resolved = [];
28698
29107
  for (const session3 of Object.values(this.state.sessions)) {
28699
- if (seen.has(session3.transport_session)) {
29108
+ let candidate;
29109
+ try {
29110
+ candidate = this.toResolvedSession(session3);
29111
+ } catch {
28700
29112
  continue;
28701
29113
  }
28702
- seen.add(session3.transport_session);
28703
- try {
28704
- resolved.push(this.toResolvedSession(session3));
28705
- } 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);
28706
29125
  }
28707
29126
  return resolved;
28708
29127
  }
28709
29128
  resolveSession(alias, agent3, workspace3, transportSession) {
28710
29129
  this.validateSession(alias, agent3, workspace3);
29130
+ const existing = this.state.sessions[alias];
29131
+ const sameAgentExisting = existing && existing.agent === agent3 ? existing : undefined;
28711
29132
  return this.toResolvedSession({
28712
29133
  alias,
28713
29134
  agent: agent3,
28714
29135
  workspace: workspace3,
28715
29136
  transport_session: transportSession,
28716
- transport_agent_command: this.state.sessions[alias]?.transport_agent_command,
28717
- 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(),
28718
29139
  last_used_at: new Date().toISOString()
28719
29140
  });
28720
29141
  }
@@ -28751,9 +29172,10 @@ class SessionService {
28751
29172
  return this.state.chat_contexts[chatKey]?.current_session;
28752
29173
  }
28753
29174
  async getPreferredSessionForTransport(transportSession) {
28754
- const matches = Object.values(this.state.sessions).filter((session3) => session3.transport_session === transportSession).sort((left, right) => right.last_used_at.localeCompare(left.last_used_at));
28755
- const expectedAlias = transportSession.split(":").at(-1);
28756
- const expectedWorkspace = transportSession.split(":")[0];
29175
+ const target = stableCoordinatorSession(transportSession);
29176
+ const matches = Object.values(this.state.sessions).filter((session3) => stableCoordinatorSession(session3.transport_session) === target).sort((left, right) => right.last_used_at.localeCompare(left.last_used_at));
29177
+ const expectedAlias = target.split(":").at(-1);
29178
+ const expectedWorkspace = target.split(":")[0];
28757
29179
  const preferred = matches.find((session3) => session3.alias === expectedAlias && session3.workspace === expectedWorkspace) ?? matches[0];
28758
29180
  return preferred ? this.toResolvedSession(preferred) : null;
28759
29181
  }
@@ -28785,10 +29207,13 @@ class SessionService {
28785
29207
  const previousCurrent = prevCtx?.current_session;
28786
29208
  const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
28787
29209
  session3.last_used_at = new Date().toISOString();
28788
- this.state.chat_contexts[chatKey] = {
28789
- current_session: internalAlias,
28790
- ...carriedPrevious ? { previous_session: carriedPrevious } : {}
28791
- };
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;
28792
29217
  await this.persist();
28793
29218
  return {
28794
29219
  alias: toDisplaySessionAlias(session3.alias),
@@ -28815,10 +29240,13 @@ class SessionService {
28815
29240
  }
28816
29241
  const currentInternal = ctx?.current_session;
28817
29242
  prevSession.last_used_at = new Date().toISOString();
28818
- this.state.chat_contexts[chatKey] = {
28819
- current_session: prevInternal,
28820
- ...currentInternal && currentInternal !== prevInternal ? { previous_session: currentInternal } : {}
28821
- };
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;
28822
29250
  await this.persist();
28823
29251
  return {
28824
29252
  alias: toDisplaySessionAlias(prevSession.alias),
@@ -28989,13 +29417,26 @@ class SessionService {
28989
29417
  const wasActive = Object.values(this.state.chat_contexts).some((ctx) => ctx.current_session === alias);
28990
29418
  delete this.state.sessions[alias];
28991
29419
  for (const [chatKey, ctx] of Object.entries(this.state.chat_contexts)) {
28992
- if (ctx.current_session === alias) {
28993
- delete this.state.chat_contexts[chatKey];
28994
- continue;
28995
- }
28996
29420
  if (ctx.previous_session === alias) {
28997
29421
  delete ctx.previous_session;
28998
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
+ }
28999
29440
  }
29000
29441
  await this.persist();
29001
29442
  return { wasActive };
@@ -29109,6 +29550,7 @@ class SessionService {
29109
29550
  throw new Error(`transport session "${transportSession}" conflicts with an external coordinator`);
29110
29551
  }
29111
29552
  const existingSession = this.state.sessions[alias];
29553
+ const sameAgentExisting = existingSession && existingSession.agent === agent3 ? existingSession : undefined;
29112
29554
  const now = new Date(this.now()).toISOString();
29113
29555
  const normalizedTransportAgentCommand = transportAgentCommand?.trim();
29114
29556
  const session3 = {
@@ -29121,9 +29563,9 @@ class SessionService {
29121
29563
  agent_session_title: native?.title ?? undefined,
29122
29564
  agent_session_updated_at: native?.updatedAt,
29123
29565
  attached_at: native ? now : undefined,
29124
- ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
29125
- mode_id: existingSession?.mode_id,
29126
- 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,
29127
29569
  created_at: existingSession?.created_at ?? now,
29128
29570
  last_used_at: now
29129
29571
  };
@@ -29305,7 +29747,6 @@ async function runConsole(paths, deps) {
29305
29747
  let runtime = null;
29306
29748
  let consumerLock;
29307
29749
  let heartbeatTimer = null;
29308
- let gcResetTimer = null;
29309
29750
  let consumerLockAcquired = false;
29310
29751
  let daemonRuntimeStarted = false;
29311
29752
  const shutdownController = new AbortController;
@@ -29319,12 +29760,6 @@ async function runConsole(paths, deps) {
29319
29760
  if (deps.afterBuild) {
29320
29761
  await deps.afterBuild(runtime);
29321
29762
  }
29322
- try {
29323
- await runtime.orchestration.service.purgeExpiredResetCoordinators({
29324
- cutoffDays: 7,
29325
- trigger: "startup"
29326
- });
29327
- } catch {}
29328
29763
  try {
29329
29764
  await runtime.orchestration.service.reconcileParallelSlots();
29330
29765
  } catch (reconcileError) {
@@ -29380,6 +29815,7 @@ async function runConsole(paths, deps) {
29380
29815
  throw error2;
29381
29816
  }
29382
29817
  }
29818
+ await runtime.reapStaleQueueOwners();
29383
29819
  if (deps.beforeReady) {
29384
29820
  await deps.beforeReady(runtime);
29385
29821
  }
@@ -29393,10 +29829,6 @@ async function runConsole(paths, deps) {
29393
29829
  heartbeatTimer = setIntervalFn(() => {
29394
29830
  deps.daemonRuntime?.heartbeat().catch(() => {});
29395
29831
  }, deps.heartbeatIntervalMs ?? 30000);
29396
- const runtimeForGc = runtime;
29397
- gcResetTimer = setIntervalFn(() => {
29398
- runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
29399
- }, 86400000);
29400
29832
  }
29401
29833
  const channelStartPromise = deps.channels.startAll({
29402
29834
  agent: runtime.agent,
@@ -29451,7 +29883,6 @@ async function runConsole(paths, deps) {
29451
29883
  signalHandler,
29452
29884
  clearIntervalFn,
29453
29885
  heartbeatTimer,
29454
- gcResetTimer,
29455
29886
  ...deps.daemonRuntime ? { daemonRuntime: deps.daemonRuntime } : {},
29456
29887
  runtime,
29457
29888
  consumerLock,
@@ -29477,9 +29908,6 @@ async function runCleanupSequence(input) {
29477
29908
  if (input.heartbeatTimer !== null) {
29478
29909
  input.clearIntervalFn(input.heartbeatTimer);
29479
29910
  }
29480
- if (input.gcResetTimer !== null) {
29481
- input.clearIntervalFn(input.gcResetTimer);
29482
- }
29483
29911
  if (input.daemonRuntime && input.runtime) {
29484
29912
  try {
29485
29913
  await input.runtime.orchestration.server.stop();
@@ -29583,15 +30011,15 @@ class AcpxBridgeClient {
29583
30011
  onEvent
29584
30012
  });
29585
30013
  try {
29586
- const didWrite = this.writeLine(encodeBridgeRequest({
30014
+ this.writeLine(encodeBridgeRequest({
29587
30015
  id,
29588
30016
  method,
29589
30017
  params
29590
- }));
29591
- if (didWrite === false) {
29592
- this.pending.delete(id);
29593
- reject(new Error("bridge write buffer is full"));
29594
- }
30018
+ }), (error2) => {
30019
+ if (error2 && this.pending.delete(id)) {
30020
+ reject(error2);
30021
+ }
30022
+ });
29595
30023
  } catch (error2) {
29596
30024
  this.pending.delete(id);
29597
30025
  reject(error2);
@@ -29671,6 +30099,17 @@ class AcpxBridgeClient {
29671
30099
  }
29672
30100
  }
29673
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
+ }
29674
30113
  function buildBridgeSpawnSpec(options) {
29675
30114
  if (options.execPath.endsWith("bun")) {
29676
30115
  return {
@@ -29693,15 +30132,17 @@ async function spawnAcpxBridgeClient(options = {}) {
29693
30132
  cwd: options.cwd ?? process.cwd(),
29694
30133
  env: {
29695
30134
  ...process.env,
29696
- XACPX_LANG: getLocale(),
29697
- XACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
29698
- XACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
29699
- XACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
29700
- ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { XACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
30135
+ ...buildBridgeSpawnEnv(options)
29701
30136
  },
29702
30137
  stdio: ["pipe", "pipe", "inherit"]
29703
30138
  });
29704
- 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", () => {});
29705
30146
  const output = createInterface({
29706
30147
  input: child.stdout,
29707
30148
  crlfDelay: Infinity
@@ -29727,7 +30168,6 @@ async function spawnAcpxBridgeClient(options = {}) {
29727
30168
  await terminateProcessTree(child.pid ?? 0, { detachedProcessGroup: false });
29728
30169
  }
29729
30170
  };
29730
- await client.waitUntilReady();
29731
30171
  return client;
29732
30172
  }
29733
30173
  function awaitable(executor) {
@@ -30569,19 +31009,19 @@ var init_streaming_prompt = __esm(() => {
30569
31009
 
30570
31010
  // src/transport/acpx-cli/node-pty-helper.ts
30571
31011
  import { chmod as chmodFs } from "node:fs/promises";
30572
- import { dirname as dirname13, join as join16 } from "node:path";
31012
+ import { dirname as dirname11, join as join16 } from "node:path";
30573
31013
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
30574
31014
  if (platform === "win32") {
30575
31015
  return null;
30576
31016
  }
30577
- return join16(dirname13(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
31017
+ return join16(dirname11(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
30578
31018
  }
30579
- async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
31019
+ async function ensureNodePtyHelperExecutable(helperPath, chmod5 = chmodFs) {
30580
31020
  if (!helperPath) {
30581
31021
  return;
30582
31022
  }
30583
31023
  try {
30584
- await chmod3(helperPath, 493);
31024
+ await chmod5(helperPath, 493);
30585
31025
  } catch (error2) {
30586
31026
  if (error2.code === "ENOENT") {
30587
31027
  return;
@@ -30594,7 +31034,7 @@ var init_node_pty_helper = () => {};
30594
31034
  // src/transport/acpx-queue-owner-launcher.ts
30595
31035
  import { createHash as createHash3 } from "node:crypto";
30596
31036
  import { spawn as spawn8 } from "node:child_process";
30597
- import { readFile as readFile12, unlink } from "node:fs/promises";
31037
+ import { readFile as readFile13, unlink } from "node:fs/promises";
30598
31038
  import { homedir as homedir8 } from "node:os";
30599
31039
  import { join as join17 } from "node:path";
30600
31040
  function buildXacpxMcpServerSpec(input) {
@@ -30757,7 +31197,7 @@ async function terminateAcpxQueueOwner(sessionId) {
30757
31197
  const lockPath = queueLockFilePath(sessionId);
30758
31198
  let owner;
30759
31199
  try {
30760
- owner = JSON.parse(await readFile12(lockPath, "utf8"));
31200
+ owner = JSON.parse(await readFile13(lockPath, "utf8"));
30761
31201
  } catch {
30762
31202
  return;
30763
31203
  }
@@ -31416,10 +31856,11 @@ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
31416
31856
  const timeoutMs = deps.timeoutMs ?? 5000;
31417
31857
  const seen = new Set;
31418
31858
  const unique = targets.filter((target) => {
31419
- if (seen.has(target.transportSession)) {
31859
+ const key = JSON.stringify([target.agent, target.agentCommand ?? null, target.cwd, target.transportSession]);
31860
+ if (seen.has(key)) {
31420
31861
  return false;
31421
31862
  }
31422
- seen.add(target.transportSession);
31863
+ seen.add(key);
31423
31864
  return true;
31424
31865
  });
31425
31866
  let terminated = 0;
@@ -31503,6 +31944,17 @@ var init_queue_owner_reaper = __esm(() => {
31503
31944
  });
31504
31945
 
31505
31946
  // src/transport/collect-reap-targets.ts
31947
+ function collectReapTargets(sessions, orchestration3, config4) {
31948
+ return [
31949
+ ...sessions.listAllResolvedSessions().map((session3) => ({
31950
+ agent: session3.agent,
31951
+ ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
31952
+ cwd: session3.cwd,
31953
+ transportSession: session3.transportSession
31954
+ })),
31955
+ ...workerBindingReapTargets(orchestration3, config4)
31956
+ ];
31957
+ }
31506
31958
  function workerBindingReapTargets(orchestration3, config4) {
31507
31959
  const targets = [];
31508
31960
  for (const [workerSession, binding] of Object.entries(orchestration3.workerBindings)) {
@@ -31560,10 +32012,21 @@ class MessageChannelRegistry {
31560
32012
  throw new Error("all channels failed to start");
31561
32013
  }
31562
32014
  }
31563
- stopAll() {
32015
+ async stopAll() {
32016
+ let firstError;
31564
32017
  for (const channel of this.channels.values()) {
31565
- 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
+ }
31566
32027
  }
32028
+ if (firstError !== undefined)
32029
+ throw firstError;
31567
32030
  }
31568
32031
  getByChatKey(chatKey) {
31569
32032
  return this.channels.get(getChannelIdFromChatKey(chatKey)) ?? null;
@@ -31811,7 +32274,7 @@ __export(exports_main, {
31811
32274
  });
31812
32275
  import { randomUUID as randomUUID3 } from "node:crypto";
31813
32276
  import { homedir as homedir9 } from "node:os";
31814
- import { dirname as dirname14, join as join18 } from "node:path";
32277
+ import { dirname as dirname12, join as join18 } from "node:path";
31815
32278
  import { fileURLToPath as fileURLToPath5 } from "node:url";
31816
32279
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
31817
32280
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -31883,6 +32346,35 @@ async function buildApp(paths, deps = {}) {
31883
32346
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config4.transport.command });
31884
32347
  const stateStore = new StateStore(paths.statePath);
31885
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
+ }
31886
32378
  const stateMutex = new AsyncMutex;
31887
32379
  const debouncedStateStore = new DebouncedStateStore({
31888
32380
  delegate: stateStore,
@@ -31902,7 +32394,9 @@ async function buildApp(paths, deps = {}) {
31902
32394
  bridgeEntryPath: resolveBridgeEntryPath(),
31903
32395
  permissionMode: config4.transport.permissionMode,
31904
32396
  nonInteractivePermissions: config4.transport.nonInteractivePermissions,
31905
- ...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 } : {}
31906
32400
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config4.transport, command: acpxCommand });
31907
32401
  const quota = new QuotaManager({
31908
32402
  onInbound: (chatKey) => {
@@ -32224,7 +32718,7 @@ async function buildApp(paths, deps = {}) {
32224
32718
  }
32225
32719
  },
32226
32720
  findReusableWorkerSession: async ({ coordinatorSession, workspace: workspace3, cwd, targetAgent, role }) => {
32227
- const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.ephemeral !== true && current.coordinatorSession === coordinatorSession && current.workspace === workspace3 && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
32721
+ const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.ephemeral !== true && sameCoordinatorSession(current.coordinatorSession, coordinatorSession) && current.workspace === workspace3 && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
32228
32722
  return binding?.[0] ?? null;
32229
32723
  },
32230
32724
  logger: logger2
@@ -32255,6 +32749,9 @@ async function buildApp(paths, deps = {}) {
32255
32749
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration3, config4, logger2, deps.channel ?? null);
32256
32750
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
32257
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
+ },
32258
32755
  createScheduledTaskFromRoute: async (input) => await createScheduledTaskFromRoute(input, {
32259
32756
  state,
32260
32757
  config: config4,
@@ -32282,6 +32779,33 @@ async function buildApp(paths, deps = {}) {
32282
32779
  }),
32283
32780
  logger: logger2
32284
32781
  });
32782
+ const reapWarmQueueOwners = async (phase) => {
32783
+ try {
32784
+ const targets = collectReapTargets(sessions, state.orchestration, config4);
32785
+ if (targets.length === 0) {
32786
+ return;
32787
+ }
32788
+ const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
32789
+ onError: (target, error2) => {
32790
+ logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner", {
32791
+ phase,
32792
+ transport_session: target.transportSession,
32793
+ error: error2 instanceof Error ? error2.message : String(error2)
32794
+ }).catch(() => {});
32795
+ }
32796
+ });
32797
+ await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners", {
32798
+ phase,
32799
+ terminated,
32800
+ attempted
32801
+ }).catch(() => {});
32802
+ } catch (err) {
32803
+ await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed", {
32804
+ phase,
32805
+ error: err instanceof Error ? err.message : String(err)
32806
+ }).catch(() => {});
32807
+ }
32808
+ };
32285
32809
  return {
32286
32810
  agent: agent3,
32287
32811
  router: router3,
@@ -32302,41 +32826,14 @@ async function buildApp(paths, deps = {}) {
32302
32826
  service: scheduledService,
32303
32827
  scheduler: scheduledScheduler
32304
32828
  },
32829
+ reapStaleQueueOwners: () => reapWarmQueueOwners("startup"),
32305
32830
  dispose: async () => {
32306
32831
  scheduledScheduler.stop();
32307
32832
  if (progressHeartbeatInterval !== undefined) {
32308
32833
  clearInterval(progressHeartbeatInterval);
32309
32834
  }
32310
32835
  await Promise.allSettled([...pendingWorkerDispatches]);
32311
- try {
32312
- const targets = [
32313
- ...sessions.listAllResolvedSessions().map((session3) => ({
32314
- agent: session3.agent,
32315
- ...session3.agentCommand ? { agentCommand: session3.agentCommand } : {},
32316
- cwd: session3.cwd,
32317
- transportSession: session3.transportSession
32318
- })),
32319
- ...workerBindingReapTargets(state.orchestration, config4)
32320
- ];
32321
- if (targets.length > 0) {
32322
- const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
32323
- onError: (target, error2) => {
32324
- logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner on shutdown", {
32325
- transport_session: target.transportSession,
32326
- error: error2 instanceof Error ? error2.message : String(error2)
32327
- }).catch(() => {});
32328
- }
32329
- });
32330
- await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners on shutdown", {
32331
- terminated,
32332
- attempted
32333
- }).catch(() => {});
32334
- }
32335
- } catch (err) {
32336
- await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed during shutdown", {
32337
- error: err instanceof Error ? err.message : String(err)
32338
- }).catch(() => {});
32339
- }
32836
+ await reapWarmQueueOwners("shutdown");
32340
32837
  await debouncedStateStore.dispose();
32341
32838
  if ("dispose" in transport && typeof transport.dispose === "function") {
32342
32839
  await transport.dispose();
@@ -32392,7 +32889,7 @@ async function main() {
32392
32889
  }
32393
32890
  }
32394
32891
  async function prepareChannelMedia(configPath, config4) {
32395
- const runtimeDir = join18(dirname14(configPath), "runtime");
32892
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32396
32893
  const mediaRootDir = join18(runtimeDir, "media");
32397
32894
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
32398
32895
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -32407,7 +32904,7 @@ function resolveRuntimePaths() {
32407
32904
  throw new Error("Unable to resolve the current user home directory");
32408
32905
  }
32409
32906
  const configPath = coreEnv("CONFIG") ?? join18(coreHomeDir(home), "config.json");
32410
- const runtimeDir = join18(dirname14(configPath), "runtime");
32907
+ const runtimeDir = join18(dirname12(configPath), "runtime");
32411
32908
  return {
32412
32909
  configPath,
32413
32910
  statePath: coreEnv("STATE") ?? join18(coreHomeDir(home), "state.json"),
@@ -32422,12 +32919,12 @@ function resolveBridgeEntryPath() {
32422
32919
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
32423
32920
  }
32424
32921
  function resolveAppLogPath(configPath) {
32425
- const rootDir = dirname14(configPath);
32922
+ const rootDir = dirname12(configPath);
32426
32923
  const runtimeDir = join18(rootDir, "runtime");
32427
32924
  return join18(runtimeDir, "app.log");
32428
32925
  }
32429
32926
  function resolvePerfLogPath(configPath) {
32430
- const rootDir = dirname14(configPath);
32927
+ const rootDir = dirname12(configPath);
32431
32928
  const runtimeDir = join18(rootDir, "runtime");
32432
32929
  return join18(runtimeDir, "perf.log");
32433
32930
  }
@@ -32823,7 +33320,7 @@ var init_orchestration_health = __esm(() => {
32823
33320
  // src/doctor/checks/runtime-check.ts
32824
33321
  import { constants } from "node:fs";
32825
33322
  import { access as access4, stat as stat3 } from "node:fs/promises";
32826
- import { dirname as dirname15 } from "node:path";
33323
+ import { dirname as dirname13 } from "node:path";
32827
33324
  import { homedir as homedir11 } from "node:os";
32828
33325
  async function checkRuntime(options = {}) {
32829
33326
  const home = options.home ?? process.env.HOME ?? homedir11();
@@ -32924,7 +33421,7 @@ async function checkFileCreatable(label, path15, probe, platform) {
32924
33421
  detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
32925
33422
  };
32926
33423
  }
32927
- const parentCheck = await checkCreatableAncestorDirectory(dirname15(path15), probe, platform);
33424
+ const parentCheck = await checkCreatableAncestorDirectory(dirname13(path15), probe, platform);
32928
33425
  if (!parentCheck.ok) {
32929
33426
  return {
32930
33427
  ok: false,
@@ -32960,7 +33457,7 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
32960
33457
  blockingPath: path15
32961
33458
  };
32962
33459
  }
32963
- const parent = dirname15(path15);
33460
+ const parent = dirname13(path15);
32964
33461
  if (parent === path15) {
32965
33462
  return {
32966
33463
  ok: false,
@@ -33485,11 +33982,13 @@ async function defaultCheckOrchestrationHealth(deps) {
33485
33982
  }
33486
33983
  try {
33487
33984
  const store = new StateStore(deps.runtimePaths.statePath);
33488
- return await checkOrchestrationHealth({
33489
- loadState: () => store.load(),
33985
+ const inspection = await store.inspect();
33986
+ const result = await checkOrchestrationHealth({
33987
+ loadState: async () => inspection.state,
33490
33988
  now: () => new Date,
33491
33989
  heartbeatThresholdSeconds: config4.orchestration.progressHeartbeatSeconds
33492
33990
  });
33991
+ return applyStateInspectionReport(result, inspection.report, deps.runtimePaths.statePath);
33493
33992
  } catch (error2) {
33494
33993
  return {
33495
33994
  id: "orchestration",
@@ -33500,6 +33999,27 @@ async function defaultCheckOrchestrationHealth(deps) {
33500
33999
  };
33501
34000
  }
33502
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
+ }
33503
34023
  function formatError9(error2) {
33504
34024
  return error2 instanceof Error ? error2.message : String(error2);
33505
34025
  }
@@ -33542,7 +34062,7 @@ var init_doctor2 = __esm(async () => {
33542
34062
  init_core_home();
33543
34063
  import { randomUUID as randomUUID4 } from "node:crypto";
33544
34064
  import { homedir as homedir13 } from "node:os";
33545
- import { dirname as dirname16, join as join20, sep } from "node:path";
34065
+ import { dirname as dirname14, join as join20, sep } from "node:path";
33546
34066
  import { fileURLToPath as fileURLToPath7 } from "node:url";
33547
34067
 
33548
34068
  // src/runtime/migrate-core-home.ts
@@ -33626,8 +34146,8 @@ init_daemon_files();
33626
34146
 
33627
34147
  // src/daemon/daemon-runtime.ts
33628
34148
  init_daemon_status();
33629
- import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
33630
- import { dirname as dirname5 } from "node:path";
34149
+ init_private_runtime_dir();
34150
+ import { rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
33631
34151
 
33632
34152
  class DaemonRuntime {
33633
34153
  paths;
@@ -33653,8 +34173,8 @@ class DaemonRuntime {
33653
34173
  stdout_log: this.paths.stdoutLog,
33654
34174
  stderr_log: this.paths.stderrLog
33655
34175
  };
33656
- await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
33657
- await writeFile3(this.paths.pidFile, `${this.options.pid}
34176
+ await ensurePrivateRuntimeDir(this.paths.runtimeDir);
34177
+ await writeFile2(this.paths.pidFile, `${this.options.pid}
33658
34178
  `);
33659
34179
  await this.statusStore.save(this.currentStatus);
33660
34180
  }
@@ -46059,6 +46579,9 @@ function requireHome(env) {
46059
46579
  return home;
46060
46580
  }
46061
46581
 
46582
+ // src/mcp/xacpx-mcp-server.ts
46583
+ init_endpoint_probe();
46584
+
46062
46585
  // src/mcp/xacpx-mcp-tools.ts
46063
46586
  init_task_watch_timeouts();
46064
46587
  init_quota_errors();
@@ -46326,7 +46849,7 @@ function buildXacpxMcpToolRegistry(input) {
46326
46849
  });
46327
46850
  tools.push({
46328
46851
  name: "scheduled_list",
46329
- 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.",
46330
46853
  inputSchema: exports_external.object({}).strict(),
46331
46854
  handler: async () => await asToolResult(async () => {
46332
46855
  const tasks = await transport.scheduledList({ coordinatorSession });
@@ -46344,7 +46867,7 @@ function buildXacpxMcpToolRegistry(input) {
46344
46867
  });
46345
46868
  tools.push({
46346
46869
  name: "scheduled_cancel",
46347
- 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.",
46348
46871
  inputSchema: exports_external.object({
46349
46872
  id: exports_external.string().min(1).describe("The scheduled task id, e.g. 'k8f2' (a leading # is allowed).")
46350
46873
  }).strict(),
@@ -46574,7 +47097,7 @@ function formatToolError(error2) {
46574
47097
  init_orchestration_ipc();
46575
47098
  init_task_watch_timeouts();
46576
47099
  import { randomUUID } from "node:crypto";
46577
- import { createConnection } from "node:net";
47100
+ import { createConnection as createConnection2 } from "node:net";
46578
47101
 
46579
47102
  class OrchestrationClient {
46580
47103
  endpoint;
@@ -46642,7 +47165,7 @@ class OrchestrationClient {
46642
47165
  async request(method, params, timeoutMs = this.timeoutMs) {
46643
47166
  const id = this.createId();
46644
47167
  return await new Promise((resolve, reject) => {
46645
- const socket = createConnection(this.endpoint.path);
47168
+ const socket = createConnection2(this.endpoint.path);
46646
47169
  let buffer = "";
46647
47170
  let settled = false;
46648
47171
  let timer;
@@ -47285,7 +47808,9 @@ function installMcpStdioShutdownHooks(options) {
47285
47808
  const setIntervalFn = options.setIntervalFn ?? ((callback, ms) => setInterval(callback, ms));
47286
47809
  const clearIntervalFn = options.clearIntervalFn ?? ((handle) => clearInterval(handle));
47287
47810
  const parentPid = options.parentPid ?? process.ppid;
47288
- const parentCheckIntervalMs = options.parentCheckIntervalMs ?? parseParentCheckIntervalMs(coreEnv("MCP_PARENT_CHECK_INTERVAL_MS"));
47811
+ const parentCheckIntervalMs = options.parentCheckIntervalMs ?? parseIntervalMs(coreEnv("MCP_PARENT_CHECK_INTERVAL_MS"), 5000);
47812
+ const endpointCheckIntervalMs = options.endpointCheckIntervalMs ?? parseIntervalMs(coreEnv("MCP_ENDPOINT_CHECK_INTERVAL_MS"), 1e4);
47813
+ const endpointFailureThreshold = options.endpointFailureThreshold ?? 3;
47289
47814
  let disposed = false;
47290
47815
  let triggered = false;
47291
47816
  const triggerShutdown = (reason, context) => {
@@ -47318,6 +47843,31 @@ function installMcpStdioShutdownHooks(options) {
47318
47843
  }, parentCheckIntervalMs);
47319
47844
  parentTimer.unref?.();
47320
47845
  }
47846
+ let endpointTimer;
47847
+ const probeEndpoint = options.probeEndpoint;
47848
+ if (probeEndpoint && endpointCheckIntervalMs > 0 && endpointFailureThreshold > 0) {
47849
+ let consecutiveDead = 0;
47850
+ let probing = false;
47851
+ const runEndpointCheck = async () => {
47852
+ if (disposed || triggered || probing)
47853
+ return;
47854
+ probing = true;
47855
+ try {
47856
+ if (await probeEndpoint()) {
47857
+ consecutiveDead = 0;
47858
+ return;
47859
+ }
47860
+ consecutiveDead += 1;
47861
+ if (consecutiveDead >= endpointFailureThreshold) {
47862
+ triggerShutdown("daemon_endpoint_dead", { consecutiveFailures: consecutiveDead });
47863
+ }
47864
+ } catch {} finally {
47865
+ probing = false;
47866
+ }
47867
+ };
47868
+ endpointTimer = setIntervalFn(runEndpointCheck, endpointCheckIntervalMs);
47869
+ endpointTimer.unref?.();
47870
+ }
47321
47871
  return () => {
47322
47872
  if (disposed)
47323
47873
  return;
@@ -47332,13 +47882,16 @@ function installMcpStdioShutdownHooks(options) {
47332
47882
  if (parentTimer) {
47333
47883
  clearIntervalFn(parentTimer);
47334
47884
  }
47885
+ if (endpointTimer) {
47886
+ clearIntervalFn(endpointTimer);
47887
+ }
47335
47888
  };
47336
47889
  }
47337
- function parseParentCheckIntervalMs(raw) {
47890
+ function parseIntervalMs(raw, fallback) {
47338
47891
  if (raw === undefined || raw.trim().length === 0)
47339
- return 5000;
47892
+ return fallback;
47340
47893
  const parsed = Number(raw);
47341
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 5000;
47894
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
47342
47895
  }
47343
47896
  function errorContext(error2) {
47344
47897
  const record3 = error2;
@@ -47357,7 +47910,8 @@ function defaultIsProcessRunning3(pid) {
47357
47910
  }
47358
47911
  }
47359
47912
  async function runXacpxMcpServer(options) {
47360
- const transport = options.transport ?? createOrchestrationTransport(options.endpoint ?? resolveDefaultOrchestrationEndpoint(process.env, process.platform));
47913
+ const endpoint = options.endpoint ?? resolveDefaultOrchestrationEndpoint(process.env, process.platform);
47914
+ const transport = options.transport ?? createOrchestrationTransport(endpoint);
47361
47915
  const server = createXacpxMcpServer({
47362
47916
  transport,
47363
47917
  ...options.coordinatorSession ? { coordinatorSession: options.coordinatorSession } : {},
@@ -47390,6 +47944,7 @@ async function runXacpxMcpServer(options) {
47390
47944
  stdin,
47391
47945
  stdout,
47392
47946
  shutdown,
47947
+ probeEndpoint: () => canConnectToEndpoint(endpoint.path, 4000),
47393
47948
  onDiagnostic: options.onDiagnostic
47394
47949
  });
47395
47950
  await server.connect(stdio);
@@ -47422,7 +47977,6 @@ function sanitizeMcpClientName(input) {
47422
47977
  fallback: "mcp-host"
47423
47978
  });
47424
47979
  }
47425
-
47426
47980
  // src/mcp/parse-string-flag.ts
47427
47981
  function parseStringFlag(args, env, options) {
47428
47982
  let fromFlag = null;
@@ -47523,7 +48077,10 @@ async function maybeRunFirstUseOnboarding(input) {
47523
48077
  const agentExisted = Boolean(input.config.agents[agentName]);
47524
48078
  input.config.workspaces[workspaceName] = { cwd };
47525
48079
  input.config.agents[agentName] = template;
47526
- await input.saveConfig(input.config);
48080
+ await input.saveFirstRunConfig({
48081
+ workspace: { name: workspaceName, cwd },
48082
+ agent: { name: agentName, config: template }
48083
+ });
47527
48084
  const alias = `${workspaceName}:${agentName}`;
47528
48085
  input.deps.print(t().misc.onboardingCreatedWorkspace(workspaceName, alias));
47529
48086
  return {
@@ -47546,8 +48103,8 @@ function resolveTemplateChoice(answer, names) {
47546
48103
  // src/cli-update.ts
47547
48104
  init_plugin_home();
47548
48105
  import { spawn as spawn4 } from "node:child_process";
47549
- import { readFile as readFile8 } from "node:fs/promises";
47550
- 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";
47551
48108
  import { fileURLToPath as fileURLToPath3 } from "node:url";
47552
48109
 
47553
48110
  // src/plugins/package-manager.ts
@@ -47555,9 +48112,14 @@ init_plugin_home();
47555
48112
  import { spawn as spawn3 } from "node:child_process";
47556
48113
  import { rm as rm4 } from "node:fs/promises";
47557
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
+ }
47558
48119
  async function defaultRunCommand(command, args, options) {
47559
48120
  await new Promise((resolve, reject) => {
47560
- 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 });
47561
48123
  child.on("error", reject);
47562
48124
  child.on("exit", (code) => {
47563
48125
  if (code === 0)
@@ -47569,7 +48131,8 @@ async function defaultRunCommand(command, args, options) {
47569
48131
  }
47570
48132
  async function silentRun(command, args, options) {
47571
48133
  await new Promise((resolve, reject) => {
47572
- 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 });
47573
48136
  child.on("error", reject);
47574
48137
  child.on("exit", (code) => {
47575
48138
  if (code === 0)
@@ -47658,7 +48221,6 @@ async function handleUpdateCli(args, deps) {
47658
48221
  kind: "plugin",
47659
48222
  name: plugin.name,
47660
48223
  currentVersion: plugin.version,
47661
- pinned: Boolean(plugin.version),
47662
48224
  latestVersion: await latestOf(plugin.name)
47663
48225
  });
47664
48226
  }
@@ -47667,12 +48229,12 @@ async function handleUpdateCli(args, deps) {
47667
48229
  const target = targets[index];
47668
48230
  deps.print(`${index + 1}. ${formatTarget(target)}`);
47669
48231
  }
47670
- const unavailable = targets.filter((target) => !target.latestVersion || target.kind === "plugin" && !target.pinned);
48232
+ const unavailable = targets.filter((target) => !target.latestVersion);
47671
48233
  if (all && unavailable.length > 0) {
47672
48234
  deps.print(t().cliUpdate.unavailableAborted(unavailable.map((target) => target.name).join(", ")));
47673
48235
  return 1;
47674
48236
  }
47675
- const candidates = targets.filter((target) => target.latestVersion && (target.kind !== "plugin" || target.pinned) && (target.successorPackage ? true : target.currentVersion !== target.latestVersion));
48237
+ const candidates = targets.filter((target) => target.latestVersion && (target.successorPackage ? true : target.currentVersion !== target.latestVersion));
47676
48238
  const selected = await selectTargets(targets, candidates, { all, explicitTarget: explicitTargets[0], deps });
47677
48239
  if (!selected.ok) {
47678
48240
  deps.print(selected.message);
@@ -47746,7 +48308,7 @@ async function handleUpdateCli(args, deps) {
47746
48308
  }
47747
48309
  if (selected.targets.some((target) => target.kind === "plugin")) {
47748
48310
  config4.plugins = updatedPlugins;
47749
- await deps.saveConfig(config4);
48311
+ await deps.savePlugins(updatedPlugins);
47750
48312
  }
47751
48313
  return 0;
47752
48314
  }
@@ -47765,8 +48327,6 @@ async function selectTargets(targets, candidates, input) {
47765
48327
  return { ok: false, message: t().cliUpdate.targetNotFound(input.explicitTarget), exitCode: 1 };
47766
48328
  if (!target.latestVersion)
47767
48329
  return { ok: false, message: t().cliUpdate.targetVersionUnknown(target.name), exitCode: 1 };
47768
- if (target.kind === "plugin" && !target.pinned)
47769
- return { ok: false, message: t().cliUpdate.targetNotPinned(target.name), exitCode: 1 };
47770
48330
  if (!target.successorPackage && target.currentVersion === target.latestVersion)
47771
48331
  return { ok: true, targets: [] };
47772
48332
  return { ok: true, targets: [target] };
@@ -47790,8 +48350,6 @@ async function selectTargets(targets, candidates, input) {
47790
48350
  const target = targets[index - 1];
47791
48351
  if (!target.latestVersion)
47792
48352
  return { ok: false, message: t().cliUpdate.targetVersionUnknown(target.name), exitCode: 1 };
47793
- if (target.kind === "plugin" && !target.pinned)
47794
- return { ok: false, message: t().cliUpdate.targetNotPinned(target.name), exitCode: 1 };
47795
48353
  if (!target.successorPackage && target.currentVersion === target.latestVersion)
47796
48354
  continue;
47797
48355
  if (!selected.includes(target))
@@ -47860,9 +48418,10 @@ function compareSemver2(a, b) {
47860
48418
  return 0;
47861
48419
  return left.prerelease ? -1 : 1;
47862
48420
  }
48421
+ var spawnUsesShell = () => process.platform === "win32";
47863
48422
  async function runCapture(command, args) {
47864
48423
  return await new Promise((resolve, reject) => {
47865
- const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
48424
+ const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: spawnUsesShell() });
47866
48425
  let stdout2 = "";
47867
48426
  let stderr = "";
47868
48427
  child.stdout.setEncoding("utf8");
@@ -47879,7 +48438,7 @@ async function runCapture(command, args) {
47879
48438
  }
47880
48439
  async function runInherit(command, args) {
47881
48440
  await new Promise((resolve, reject) => {
47882
- const child = spawn4(command, args, { stdio: "inherit" });
48441
+ const child = spawn4(command, args, { stdio: "inherit", shell: spawnUsesShell() });
47883
48442
  child.on("error", reject);
47884
48443
  child.on("exit", (code) => {
47885
48444
  if (code === 0)
@@ -47891,10 +48450,10 @@ async function runInherit(command, args) {
47891
48450
  }
47892
48451
  async function readPackageName() {
47893
48452
  try {
47894
- const here = dirname10(fileURLToPath3(import.meta.url));
48453
+ const here = dirname8(fileURLToPath3(import.meta.url));
47895
48454
  for (const candidate of [join11(here, "..", "package.json"), join11(here, "..", "..", "package.json")]) {
47896
48455
  try {
47897
- const parsed = JSON.parse(await readFile8(candidate, "utf8"));
48456
+ const parsed = JSON.parse(await readFile9(candidate, "utf8"));
47898
48457
  if (typeof parsed.name === "string" && parsed.name.trim())
47899
48458
  return parsed.name.trim();
47900
48459
  } catch {}
@@ -48136,7 +48695,7 @@ async function addChannel(type, rawArgs, deps) {
48136
48695
  return 1;
48137
48696
  }
48138
48697
  config4.channels = [...config4.channels ?? [], candidate];
48139
- await deps.saveConfig(config4);
48698
+ await deps.saveChannels(config4.channels);
48140
48699
  deps.print(t().channelCli.channelAdded(type));
48141
48700
  for (const line of provider.renderSummary(candidate))
48142
48701
  deps.print(line);
@@ -48237,7 +48796,7 @@ async function removeChannel(type, rawArgs, deps) {
48237
48796
  return 1;
48238
48797
  }
48239
48798
  config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
48240
- await deps.saveConfig(config4);
48799
+ await deps.saveChannels(config4.channels);
48241
48800
  deps.print(t().channelCli.channelRemoved(channel.id));
48242
48801
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48243
48802
  }
@@ -48259,7 +48818,7 @@ async function setChannelEnabled(type, enabled, rawArgs, deps) {
48259
48818
  return 1;
48260
48819
  }
48261
48820
  channel.enabled = enabled;
48262
- await deps.saveConfig(config4);
48821
+ await deps.saveChannels(config4.channels);
48263
48822
  deps.print(t().channelCli.channelEnabledToggled(channel.id, enabled));
48264
48823
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48265
48824
  }
@@ -48281,7 +48840,7 @@ async function setChannelReplyMode(type, mode, rawArgs, deps) {
48281
48840
  return 1;
48282
48841
  }
48283
48842
  channel.replyMode = mode;
48284
- await deps.saveConfig(config4);
48843
+ await deps.saveChannels(config4.channels);
48285
48844
  deps.print(t().channelCli.channelReplyModeSet(channel.id, mode));
48286
48845
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48287
48846
  }
@@ -48427,7 +48986,7 @@ async function addChannelAccount(type, accountId, rawArgs, deps) {
48427
48986
  deps.print(validation.map((issue2) => issue2.message).join(";"));
48428
48987
  return 1;
48429
48988
  }
48430
- await deps.saveConfig(config4);
48989
+ await deps.saveChannels(config4.channels);
48431
48990
  deps.print(t().channelCli.channelAccountAdded(type, accountId));
48432
48991
  if (reEnabledChannel)
48433
48992
  deps.print(t().channelCli.channelReEnabled(type));
@@ -48467,7 +49026,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48467
49026
  return 1;
48468
49027
  }
48469
49028
  config4.channels = config4.channels.filter((channel) => channel.id !== existing.id);
48470
- await deps.saveConfig(config4);
49029
+ await deps.saveChannels(config4.channels);
48471
49030
  deps.print(t().channelCli.channelAccountRemovedWithChannel(type, accountId));
48472
49031
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48473
49032
  }
@@ -48484,7 +49043,7 @@ async function removeChannelAccount(type, accountId, rawArgs, deps) {
48484
49043
  deps.print(t().channelCli.channelAccountDefaultSwitched(options.defaultAccount));
48485
49044
  }
48486
49045
  existing.options = options;
48487
- await deps.saveConfig(config4);
49046
+ await deps.saveChannels(config4.channels);
48488
49047
  deps.print(t().channelCli.channelAccountRemoved(type, accountId));
48489
49048
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48490
49049
  }
@@ -48538,7 +49097,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48538
49097
  return 1;
48539
49098
  }
48540
49099
  existing.options = options;
48541
- await deps.saveConfig(config4);
49100
+ await deps.saveChannels(config4.channels);
48542
49101
  deps.print(t().channelCli.channelAccountEnabledToggled(type, accountId, enabled));
48543
49102
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
48544
49103
  }
@@ -48546,7 +49105,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
48546
49105
  // src/plugins/plugin-cli.ts
48547
49106
  init_core_home();
48548
49107
  init_plugin_home();
48549
- import { readFile as readFile10 } from "node:fs/promises";
49108
+ import { readFile as readFile11 } from "node:fs/promises";
48550
49109
  import { isAbsolute, join as join13, resolve } from "node:path";
48551
49110
  init_plugin_loader();
48552
49111
  init_validate_plugin();
@@ -48556,14 +49115,15 @@ init_channel_scope();
48556
49115
  init_plugin_loader();
48557
49116
  init_validate_plugin();
48558
49117
  init_known_plugins();
48559
- import { readFile as readFile9 } from "node:fs/promises";
49118
+ init_plugin_renames();
49119
+ import { readFile as readFile10 } from "node:fs/promises";
48560
49120
  import { join as join12 } from "node:path";
48561
49121
  function suggestedPluginPackageForChannel(type) {
48562
49122
  return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
48563
49123
  }
48564
49124
  async function readDependencyEntries(pluginHome) {
48565
49125
  try {
48566
- const raw = await readFile9(join12(pluginHome, "package.json"), "utf8");
49126
+ const raw = await readFile10(join12(pluginHome, "package.json"), "utf8");
48567
49127
  const parsed = JSON.parse(raw);
48568
49128
  const out = {};
48569
49129
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -48587,8 +49147,8 @@ async function inspectPlugins(input) {
48587
49147
  }
48588
49148
  const importPlugin = input.importPlugin ?? importPluginFromHome;
48589
49149
  const allConfigured = input.config.plugins;
48590
- const filterByName = input.pluginName ?? null;
48591
- 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)) {
48592
49152
  return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
48593
49153
  }
48594
49154
  const pushIfRelevant = (issue2) => {
@@ -48635,6 +49195,8 @@ async function inspectPlugins(input) {
48635
49195
  }
48636
49196
  const builtInChannelTypes = new Set(listKnownChannelIds());
48637
49197
  for (const channel of input.config.channels) {
49198
+ if (channel.enabled === false)
49199
+ continue;
48638
49200
  if (builtInChannelTypes.has(channel.type))
48639
49201
  continue;
48640
49202
  const provider = channelProviders.get(channel.type);
@@ -48662,12 +49224,31 @@ async function inspectPlugins(input) {
48662
49224
  init_known_plugins();
48663
49225
  init_plugin_renames();
48664
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
+ }
48665
49246
  function looksLikePath(spec) {
48666
49247
  return spec === "." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith(".\\") || spec.startsWith("..\\") || spec.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(spec) || isAbsolute(spec);
48667
49248
  }
48668
49249
  async function readDependencyEntries2(pluginHome) {
48669
49250
  try {
48670
- const raw = await readFile10(join13(pluginHome, "package.json"), "utf8");
49251
+ const raw = await readFile11(join13(pluginHome, "package.json"), "utf8");
48671
49252
  const parsed = JSON.parse(raw);
48672
49253
  const out = {};
48673
49254
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -48693,7 +49274,7 @@ async function resolveLocalPluginName(installSpec, pluginHome, namesBeforeInstal
48693
49274
  return name;
48694
49275
  }
48695
49276
  try {
48696
- const raw = await readFile10(join13(installSpec, "package.json"), "utf8");
49277
+ const raw = await readFile11(join13(installSpec, "package.json"), "utf8");
48697
49278
  const parsed = JSON.parse(raw);
48698
49279
  if (typeof parsed.name === "string" && parsed.name.trim())
48699
49280
  return parsed.name.trim();
@@ -48836,6 +49417,11 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48836
49417
  deps.print(t().pluginCli.unrecognizedArgs(flags.rest.join(" ")));
48837
49418
  return 1;
48838
49419
  }
49420
+ const invalidSpec = invalidSpecMessage([packageSpec, flags.version], deps.platform ?? process.platform);
49421
+ if (invalidSpec) {
49422
+ deps.print(invalidSpec);
49423
+ return 1;
49424
+ }
48839
49425
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48840
49426
  await ensurePluginHome(pluginHome);
48841
49427
  const installSpec = looksLikePath(packageSpec) && !isAbsolute(packageSpec) ? resolve(process.cwd(), packageSpec) : packageSpec;
@@ -48873,7 +49459,7 @@ async function addPlugin(packageSpec, rawArgs, deps) {
48873
49459
  } else {
48874
49460
  config4.plugins = [...config4.plugins, next];
48875
49461
  }
48876
- await deps.saveConfig(config4);
49462
+ await deps.savePlugins(config4.plugins);
48877
49463
  deps.print(t().pluginCli.pluginInstalled(recordedName));
48878
49464
  if (summary.channels.length > 0) {
48879
49465
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
@@ -48899,7 +49485,7 @@ async function removePlugin(packageName, rawArgs, deps) {
48899
49485
  }
48900
49486
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
48901
49487
  const validate = deps.validateInstalledPlugin ?? ((name) => validateInstalledPluginDefault(name, pluginHome));
48902
- const guard = await dependencyGuard(packageName, config4, validate);
49488
+ const guard = await dependencyGuard(existing.name, config4, validate);
48903
49489
  if (!guard.allow) {
48904
49490
  if (guard.reason)
48905
49491
  deps.print(guard.reason);
@@ -48909,14 +49495,14 @@ async function removePlugin(packageName, rawArgs, deps) {
48909
49495
  await removePluginPackage({ packageName: name, pluginHome });
48910
49496
  });
48911
49497
  try {
48912
- await remove(packageName);
49498
+ await remove(existing.name);
48913
49499
  } catch (error2) {
48914
- deps.print(t().pluginCli.pluginUninstallFailed(packageName, describeError(error2)));
49500
+ deps.print(t().pluginCli.pluginUninstallFailed(existing.name, describeError(error2)));
48915
49501
  return 1;
48916
49502
  }
48917
- config4.plugins = config4.plugins.filter((entry) => entry.name !== packageName);
48918
- await deps.saveConfig(config4);
48919
- 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));
48920
49506
  return await maybeRestartAfterMutation2(flags.restart, deps);
48921
49507
  }
48922
49508
  async function updatePlugins(args, deps) {
@@ -48936,6 +49522,11 @@ async function updatePlugins(args, deps) {
48936
49522
  deps.print("--all cannot be combined with --version");
48937
49523
  return 1;
48938
49524
  }
49525
+ const invalidSpec = invalidSpecMessage([flags.version], deps.platform ?? process.platform);
49526
+ if (invalidSpec) {
49527
+ deps.print(invalidSpec);
49528
+ return 1;
49529
+ }
48939
49530
  const config4 = await deps.loadConfig();
48940
49531
  ensurePluginsArray(config4);
48941
49532
  const pluginHome = deps.pluginHome ?? resolvePluginHome();
@@ -48992,7 +49583,7 @@ async function updatePlugins(args, deps) {
48992
49583
  deps.print(t().pluginCli.providesChannels(summary.channels.join(", ")));
48993
49584
  }
48994
49585
  }
48995
- await deps.saveConfig(config4);
49586
+ await deps.savePlugins(config4.plugins);
48996
49587
  return await maybeRestartAfterMutation2(flags.restart, deps);
48997
49588
  }
48998
49589
  async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
@@ -49023,7 +49614,7 @@ async function setPluginEnabled(packageName, enabled, rawArgs, deps) {
49023
49614
  }
49024
49615
  }
49025
49616
  existing.enabled = enabled;
49026
- await deps.saveConfig(config4);
49617
+ await deps.savePlugins(config4.plugins);
49027
49618
  deps.print(t().pluginCli.pluginEnabledToggled(packageName, enabled));
49028
49619
  return await maybeRestartAfterMutation2(flags.restart, deps);
49029
49620
  }
@@ -49205,7 +49796,7 @@ init_i18n();
49205
49796
  init_bootstrap();
49206
49797
  async function prepareMcpCoordinatorStartup(input) {
49207
49798
  const coordinatorSession = input.coordinatorSession.trim();
49208
- const existingSession = Object.values(input.state.sessions).find((session3) => session3.transport_session === coordinatorSession);
49799
+ const existingSession = Object.values(input.state.sessions).find((session3) => stableCoordinatorSession(session3.transport_session) === stableCoordinatorSession(coordinatorSession));
49209
49800
  const workspace3 = input.workspace?.trim();
49210
49801
  if (workspace3) {
49211
49802
  if (existingSession) {
@@ -49251,10 +49842,10 @@ function createMcpStdioIdentityResolver(input) {
49251
49842
  const workspace3 = input.workspace?.trim() || null;
49252
49843
  const sourceHandle = input.sourceHandle?.trim() || null;
49253
49844
  const resolvedWorkspace = workspace3;
49254
- const resolvedCoordinatorSession = parsedCoordinatorSession ?? inferExternalCoordinatorSession({
49845
+ const resolvedCoordinatorSession = stableCoordinatorSession(parsedCoordinatorSession ?? inferExternalCoordinatorSession({
49255
49846
  clientName: context.clientName,
49256
49847
  ...resolvedWorkspace ? { workspace: resolvedWorkspace } : { instanceId }
49257
- });
49848
+ }));
49258
49849
  const startup = await prepareMcpCoordinatorStartup({
49259
49850
  coordinatorSession: resolvedCoordinatorSession,
49260
49851
  ...resolvedWorkspace ? { workspace: resolvedWorkspace } : {},
@@ -49541,7 +50132,9 @@ async function defaultUpdate(args, input) {
49541
50132
  const store = await createCliConfigStore();
49542
50133
  const deps = {
49543
50134
  loadConfig: async () => await store.load(),
49544
- saveConfig: async (config4) => await store.save(config4),
50135
+ savePlugins: async (plugins) => {
50136
+ await store.replacePlugins(plugins);
50137
+ },
49545
50138
  readCurrentVersion: readVersion,
49546
50139
  print: input.print,
49547
50140
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -49550,6 +50143,27 @@ async function defaultUpdate(args, input) {
49550
50143
  };
49551
50144
  return await handleUpdateCli(args, deps);
49552
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
+ }
49553
50167
  async function runOnboardingBeforeStart(input) {
49554
50168
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49555
50169
  await ensureConfigExists(runtimePaths.configPath);
@@ -49557,10 +50171,14 @@ async function runOnboardingBeforeStart(input) {
49557
50171
  const stateStore = new StateStore(runtimePaths.statePath);
49558
50172
  const config4 = await configStore.load();
49559
50173
  const state = await stateStore.load();
50174
+ warnStateLoadReport(stateStore);
49560
50175
  const result = await maybeRunFirstUseOnboarding({
49561
50176
  config: config4,
49562
50177
  state,
49563
- 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
+ },
49564
50182
  deps: {
49565
50183
  print: input.print,
49566
50184
  cwd: input.cwd,
@@ -49771,7 +50389,7 @@ async function handleLaterCli(args, deps) {
49771
50389
  }
49772
50390
  async function laterList(print) {
49773
50391
  const scheduled = await createCliScheduledTaskService();
49774
- print(renderLaterList(scheduled.listPending(), (alias) => toDisplaySessionAlias(alias)));
50392
+ print(renderLaterList(scheduled.listPendingAllChats(), (alias) => toDisplaySessionAlias(alias)));
49775
50393
  return 0;
49776
50394
  }
49777
50395
  async function laterCancel(rawId, print) {
@@ -49781,7 +50399,7 @@ async function laterCancel(rawId, print) {
49781
50399
  return 1;
49782
50400
  }
49783
50401
  const scheduled = await createCliScheduledTaskService();
49784
- const ok = await scheduled.cancelPending(id);
50402
+ const ok = await scheduled.cancelPendingAnyChat(id);
49785
50403
  if (!ok) {
49786
50404
  print(t().cli.laterNotFound(id));
49787
50405
  print(t().cli.laterNotFoundHint);
@@ -49794,6 +50412,7 @@ async function createCliScheduledTaskService() {
49794
50412
  const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
49795
50413
  const stateStore = new StateStore(runtimePaths.statePath);
49796
50414
  const state = await stateStore.load();
50415
+ warnStateLoadReport(stateStore);
49797
50416
  return new ScheduledTaskService(state, stateStore);
49798
50417
  }
49799
50418
  function resolveConfigPathForCurrentEnv() {
@@ -49896,14 +50515,12 @@ async function createFirstRunSession(runtime, plan) {
49896
50515
  }
49897
50516
  async function rollbackFirstRunConfig(runtime, plan) {
49898
50517
  try {
49899
- const config4 = await runtime.configStore.load();
49900
- if (!plan.rollback.workspaceExisted && config4.workspaces[plan.workspace]) {
49901
- delete config4.workspaces[plan.workspace];
50518
+ if (!plan.rollback.workspaceExisted) {
50519
+ await runtime.configStore.removeWorkspace(plan.workspace);
49902
50520
  }
49903
- if (!plan.rollback.agentExisted && config4.agents[plan.agent]) {
49904
- delete config4.agents[plan.agent];
50521
+ if (!plan.rollback.agentExisted) {
50522
+ await runtime.configStore.removeAgent(plan.agent);
49905
50523
  }
49906
- await runtime.configStore.save(config4);
49907
50524
  } catch (error2) {
49908
50525
  await runtime.logger.error("onboarding.rollback_failed", "failed to roll back first-run config", {
49909
50526
  alias: plan.alias,
@@ -49935,7 +50552,9 @@ async function defaultMcpStdio(args, deps = {}) {
49935
50552
  await ensureConfigExists(runtimePaths.configPath);
49936
50553
  const config4 = await loadConfig(runtimePaths.configPath);
49937
50554
  availableAgents = Object.keys(config4.agents);
49938
- 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)));
49939
50558
  const resolveIdentity = createMcpStdioIdentityResolver({
49940
50559
  parsedCoordinatorSession,
49941
50560
  sourceHandle,
@@ -49999,7 +50618,9 @@ async function createChannelCliDeps(input) {
49999
50618
  const controller = input.controller ?? createDefaultController();
50000
50619
  const base = {
50001
50620
  loadConfig: async () => await store.load(),
50002
- saveConfig: async (config4) => await store.save(config4),
50621
+ saveChannels: async (channels) => {
50622
+ await store.replaceChannels(channels);
50623
+ },
50003
50624
  print: input.print,
50004
50625
  stderr: input.stderr ?? ((text) => process.stderr.write(text)),
50005
50626
  isInteractive: input.isInteractive ?? defaultIsInteractive,
@@ -50022,7 +50643,9 @@ async function createPluginCliDeps(input) {
50022
50643
  const controller = input.controller ?? createDefaultController();
50023
50644
  const base = {
50024
50645
  loadConfig: async () => await store.load(),
50025
- saveConfig: async (config4) => await store.save(config4),
50646
+ savePlugins: async (plugins) => {
50647
+ await store.replacePlugins(plugins);
50648
+ },
50026
50649
  print: input.print,
50027
50650
  isInteractive: input.isInteractive ?? defaultIsInteractive,
50028
50651
  promptText: input.promptText ?? defaultPromptText,
@@ -50164,7 +50787,7 @@ function safeDaemonLogPaths() {
50164
50787
  const configPath = resolveConfigPathForCurrentEnv();
50165
50788
  const paths = resolveDaemonPathsForCurrentConfig();
50166
50789
  return {
50167
- appLog: join20(dirname16(configPath), "runtime", "app.log"),
50790
+ appLog: join20(dirname14(configPath), "runtime", "app.log"),
50168
50791
  stderrLog: paths.stderrLog
50169
50792
  };
50170
50793
  } catch {