mixdog 0.9.52 → 0.9.53

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.
Files changed (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -14888,6 +14888,79 @@ function writeFileAtomicSync(filePath, data, opts = {}) {
14888
14888
  function writeJsonAtomicSync(filePath, value, opts = {}) {
14889
14889
  return writeFileAtomicSync(filePath, JSON.stringify(value, null, opts.compact ? 0 : 2) + "\n", opts);
14890
14890
  }
14891
+ async function writeFileAtomicAsync(filePath, data, opts = {}) {
14892
+ const run = async () => {
14893
+ const dir = dirname4(filePath);
14894
+ mkdirSync3(dir, { recursive: true });
14895
+ const tmp = join4(dir, `.${basename4(filePath)}.${randomBytes2(12).toString("hex")}.tmp`);
14896
+ try {
14897
+ const writeOpts = { encoding: opts.encoding || "utf8", flag: "wx", mode: opts.mode !== void 0 ? opts.mode : 384 };
14898
+ writeFileSync3(tmp, data, writeOpts);
14899
+ if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp);
14900
+ if (opts.fsync !== false) {
14901
+ let fd = null;
14902
+ try {
14903
+ fd = openSync(tmp, "r");
14904
+ fsyncSync(fd);
14905
+ } catch (err) {
14906
+ if (!["EPERM", "ENOTSUP", "EINVAL"].includes(err?.code)) throw err;
14907
+ } finally {
14908
+ try {
14909
+ if (fd !== null) closeSync(fd);
14910
+ } catch {
14911
+ }
14912
+ }
14913
+ }
14914
+ if (opts.createOnly === true) {
14915
+ try {
14916
+ linkSync(tmp, filePath);
14917
+ } catch (err) {
14918
+ try {
14919
+ unlinkSync2(tmp);
14920
+ } catch {
14921
+ }
14922
+ if (err?.code === "EEXIST") return false;
14923
+ throw err;
14924
+ }
14925
+ try {
14926
+ unlinkSync2(tmp);
14927
+ } catch {
14928
+ }
14929
+ } else {
14930
+ renameWithRetrySync(tmp, filePath, opts);
14931
+ }
14932
+ if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(filePath);
14933
+ if (opts.fsyncDir === true) {
14934
+ let dfd = null;
14935
+ try {
14936
+ dfd = openSync(dir, "r");
14937
+ fsyncSync(dfd);
14938
+ } catch (err) {
14939
+ if (!["EPERM", "ENOTSUP", "EINVAL", "EACCES"].includes(err?.code)) throw err;
14940
+ } finally {
14941
+ try {
14942
+ if (dfd !== null) closeSync(dfd);
14943
+ } catch {
14944
+ }
14945
+ }
14946
+ }
14947
+ return true;
14948
+ } catch (err) {
14949
+ try {
14950
+ if (existsSync4(tmp)) unlinkSync2(tmp);
14951
+ } catch {
14952
+ }
14953
+ throw err;
14954
+ }
14955
+ };
14956
+ if (opts.lock === true) {
14957
+ return withFileLock(`${filePath}.lock`, run, opts);
14958
+ }
14959
+ return run();
14960
+ }
14961
+ function writeJsonAtomicAsync(filePath, value, opts = {}) {
14962
+ return writeFileAtomicAsync(filePath, JSON.stringify(value, null, opts.compact ? 0 : 2) + "\n", opts);
14963
+ }
14891
14964
  async function updateJsonAtomic(filePath, mutator, opts = {}) {
14892
14965
  const { lock: _lock, ...writeOpts } = opts;
14893
14966
  return withFileLock(`${filePath}.lock`, () => {
@@ -14915,6 +14988,13 @@ import {
14915
14988
  statSync as statSync3,
14916
14989
  writeFileSync as writeFileSync4
14917
14990
  } from "fs";
14991
+ import {
14992
+ copyFile as copyFileP,
14993
+ mkdir as mkdirP,
14994
+ readdir as readdirP,
14995
+ rm as rmP,
14996
+ stat as statP
14997
+ } from "fs/promises";
14918
14998
  import { dirname as dirname5, join as join5, resolve as resolve3 } from "path";
14919
14999
  import { homedir as homedir3 } from "os";
14920
15000
  import { createHash } from "crypto";
@@ -14977,44 +15057,44 @@ function loadLatestMixdogConfigFromBackup(_dataDir) {
14977
15057
  }
14978
15058
  return null;
14979
15059
  }
14980
- function copyTree(src, dst, copied) {
14981
- const st = statSync3(src);
15060
+ function markUserDataInitialized(dataDir) {
15061
+ try {
15062
+ mkdirSync4(getBackupRoot(), { recursive: true });
15063
+ writeFileSync4(initMarkerPath(dataDir), JSON.stringify({
15064
+ dataDir,
15065
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15066
+ }, null, 2) + "\n", "utf8");
15067
+ } catch {
15068
+ }
15069
+ }
15070
+ async function copyTreeAsync(src, dst, copied) {
15071
+ const st = await statP(src);
14982
15072
  if (st.isDirectory()) {
14983
- for (const name of readdirSync(src)) {
14984
- copyTree(join5(src, name), join5(dst, name), copied);
15073
+ for (const name of await readdirP(src)) {
15074
+ await copyTreeAsync(join5(src, name), join5(dst, name), copied);
14985
15075
  }
14986
15076
  return;
14987
15077
  }
14988
15078
  if (!st.isFile()) return;
14989
- mkdirSync4(dirname5(dst), { recursive: true });
14990
- copyFileSync(src, dst);
15079
+ await mkdirP(dirname5(dst), { recursive: true });
15080
+ await copyFileP(src, dst);
14991
15081
  copied.push(dst);
14992
15082
  }
14993
- function pruneBackups(keep = 40) {
15083
+ async function pruneBackupsAsync(keep = 40) {
14994
15084
  let entries = [];
14995
15085
  try {
14996
- entries = readdirSync(getBackupRoot(), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().reverse();
15086
+ entries = (await readdirP(getBackupRoot(), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().reverse();
14997
15087
  } catch {
14998
15088
  return;
14999
15089
  }
15000
15090
  for (const name of entries.slice(keep)) {
15001
15091
  try {
15002
- rmSync(join5(getBackupRoot(), name), { recursive: true, force: true });
15092
+ await rmP(join5(getBackupRoot(), name), { recursive: true, force: true });
15003
15093
  } catch {
15004
15094
  }
15005
15095
  }
15006
15096
  }
15007
- function markUserDataInitialized(dataDir) {
15008
- try {
15009
- mkdirSync4(getBackupRoot(), { recursive: true });
15010
- writeFileSync4(initMarkerPath(dataDir), JSON.stringify({
15011
- dataDir,
15012
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15013
- }, null, 2) + "\n", "utf8");
15014
- } catch {
15015
- }
15016
- }
15017
- function backupUserData(dataDir, reason = "snapshot") {
15097
+ async function backupUserDataAsync(dataDir, reason = "snapshot") {
15018
15098
  if (process.env.MIXDOG_SKIP_USER_DATA_BACKUP === "1" || process.env.MIXDOG_SKIP_USER_DATA_BACKUP === "true") {
15019
15099
  return { dir: null, copied: [] };
15020
15100
  }
@@ -15023,15 +15103,15 @@ function backupUserData(dataDir, reason = "snapshot") {
15023
15103
  const copied = [];
15024
15104
  for (const rel of USER_DATA_FILES) {
15025
15105
  const src = join5(dataDir, rel);
15026
- if (existsSync5(src)) copyTree(src, join5(backupDir, rel), copied);
15106
+ if (existsSync5(src)) await copyTreeAsync(src, join5(backupDir, rel), copied);
15027
15107
  }
15028
15108
  for (const rel of USER_DATA_DIRS) {
15029
15109
  const src = join5(dataDir, rel);
15030
- if (existsSync5(src)) copyTree(src, join5(backupDir, rel), copied);
15110
+ if (existsSync5(src)) await copyTreeAsync(src, join5(backupDir, rel), copied);
15031
15111
  }
15032
15112
  if (copied.length > 0) {
15033
15113
  markUserDataInitialized(dataDir);
15034
- pruneBackups();
15114
+ await pruneBackupsAsync();
15035
15115
  if (process.env.MIXDOG_SETUP_QUIET !== "1") {
15036
15116
  process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}
15037
15117
  `);
@@ -15059,7 +15139,13 @@ var AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
15059
15139
 
15060
15140
  // src/runtime/shared/config.mjs
15061
15141
  var _require = createRequire3(import.meta.url);
15062
- var { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret, hasSecret: _hasSecret } = _require("../../lib/keychain-cjs.cjs");
15142
+ var {
15143
+ getSecret: _getSecret,
15144
+ setSecret: _setSecret,
15145
+ deleteSecret: _deleteSecret,
15146
+ hasSecret: _hasSecret,
15147
+ invalidateSecretCache: _invalidateSecretCache
15148
+ } = _require("../../lib/keychain-cjs.cjs");
15063
15149
  var DATA_DIR = resolvePluginData();
15064
15150
  var CONFIG_PATH = join6(DATA_DIR, "mixdog-config.json");
15065
15151
  var GENERATED_KEY = "_generated";
@@ -15152,27 +15238,6 @@ function readJsonFile(path2) {
15152
15238
  return null;
15153
15239
  }
15154
15240
  }
15155
- function writeJsonFile(path2, data) {
15156
- mkdirSync5(dirname6(path2), { recursive: true, mode: 448 });
15157
- if (path2 === CONFIG_PATH) {
15158
- try {
15159
- backupUserData(DATA_DIR, "pre-config-write");
15160
- } catch {
15161
- }
15162
- }
15163
- writeJsonAtomicSync(path2, data, { lock: false, fsyncDir: true, mode: 384, secret: true });
15164
- if (path2 === CONFIG_PATH) {
15165
- invalidateConfigReadCache();
15166
- try {
15167
- markUserDataInitialized(DATA_DIR);
15168
- } catch {
15169
- }
15170
- try {
15171
- backupUserData(DATA_DIR, "post-config-write");
15172
- } catch {
15173
- }
15174
- }
15175
- }
15176
15241
  function readAll() {
15177
15242
  const parsed = readJsonFile(CONFIG_PATH);
15178
15243
  if (parsed != null) return parsed;
@@ -15231,22 +15296,58 @@ function readAllForRmW() {
15231
15296
  return restoreAllForRmWOrThrow(parseErr.message);
15232
15297
  }
15233
15298
  }
15234
- function writeAll(data) {
15235
- writeJsonFile(CONFIG_PATH, data);
15236
- }
15237
- function withConfigLock(fn) {
15238
- return withFileLockSync(`${CONFIG_PATH}.lock`, fn, { secret: true });
15239
- }
15240
15299
  function readSection(section) {
15241
15300
  return stripGeneratedMarker(readAll()[section] ?? null) ?? {};
15242
15301
  }
15243
- function updateSection(section, updater) {
15244
- withConfigLock(() => {
15302
+ async function writeJsonFileAsync(path2, data) {
15303
+ mkdirSync5(dirname6(path2), { recursive: true, mode: 448 });
15304
+ if (path2 === CONFIG_PATH) {
15305
+ try {
15306
+ await backupUserDataAsync(DATA_DIR, "pre-config-write");
15307
+ } catch {
15308
+ }
15309
+ }
15310
+ await writeJsonAtomicAsync(path2, data, { lock: false, fsyncDir: true, mode: 384, secret: true });
15311
+ if (path2 === CONFIG_PATH) {
15312
+ invalidateConfigReadCache();
15313
+ try {
15314
+ markUserDataInitialized(DATA_DIR);
15315
+ } catch {
15316
+ }
15317
+ try {
15318
+ await backupUserDataAsync(DATA_DIR, "post-config-write");
15319
+ } catch {
15320
+ }
15321
+ }
15322
+ }
15323
+ async function writeAllAsync(data) {
15324
+ await writeJsonFileAsync(CONFIG_PATH, data);
15325
+ }
15326
+ function withConfigLockAsync(fn) {
15327
+ return withFileLock(`${CONFIG_PATH}.lock`, fn, { secret: true });
15328
+ }
15329
+ var _pendingConfigWrites = /* @__PURE__ */ new Set();
15330
+ function trackConfigWrite(work) {
15331
+ let promise;
15332
+ try {
15333
+ promise = Promise.resolve(work());
15334
+ } catch (error) {
15335
+ promise = Promise.reject(error);
15336
+ }
15337
+ _pendingConfigWrites.add(promise);
15338
+ const remove = () => {
15339
+ _pendingConfigWrites.delete(promise);
15340
+ };
15341
+ promise.then(remove, remove);
15342
+ return promise;
15343
+ }
15344
+ function updateSectionAsync(section, updater) {
15345
+ return trackConfigWrite(() => withConfigLockAsync(async () => {
15245
15346
  const all = readAllForRmW();
15246
15347
  const current = stripGeneratedMarker(all[section] || {});
15247
15348
  all[section] = stripGeneratedMarker(typeof updater === "function" ? updater(current) : updater);
15248
- writeAll(all);
15249
- });
15349
+ await writeAllAsync(all);
15350
+ }));
15250
15351
  }
15251
15352
  var CAPABILITY_DEFAULTS = Object.freeze({ homeAccess: false });
15252
15353
  var SECRET_ACCOUNTS = Object.freeze({
@@ -15348,7 +15449,7 @@ async function toggleVoice({ pushNotice, setProgressHint } = {}) {
15348
15449
  const dataDir = resolvePluginData();
15349
15450
  if (isVoiceEnabled()) {
15350
15451
  try {
15351
- updateSection("voice", (current) => ({ ...current, enabled: false }));
15452
+ await updateSectionAsync("voice", (current) => ({ ...current, enabled: false }));
15352
15453
  } catch (err) {
15353
15454
  pushNotice?.(`Voice OFF failed: ${err?.message || err}`, "error");
15354
15455
  return { ok: false, error: err?.message || String(err) };
@@ -15363,7 +15464,7 @@ async function toggleVoice({ pushNotice, setProgressHint } = {}) {
15363
15464
  _voiceInstallBusy = true;
15364
15465
  try {
15365
15466
  await ensureVoiceRuntimeReady({ dataDir, pushNotice, setProgressHint });
15366
- updateSection("voice", (current) => ({ ...current, enabled: true }));
15467
+ await updateSectionAsync("voice", (current) => ({ ...current, enabled: true }));
15367
15468
  setProgressHint?.("");
15368
15469
  pushNotice?.("Voice ON \u2014 channel voice messages will be transcribed", "info");
15369
15470
  return true;
@@ -20860,19 +20961,21 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20860
20961
  }
20861
20962
  if (channelPrompt.kind === "webhook-domain") {
20862
20963
  if (!commandText) return false;
20863
- store.setWebhookConfig?.({ ngrokDomain: commandText });
20864
- resumeAfterChannelPrompt(channelPrompt);
20964
+ Promise.resolve(store.setWebhookConfig?.({ ngrokDomain: commandText })).then(() => resumeAfterChannelPrompt(channelPrompt)).catch((e) => {
20965
+ store.pushNotice(`webhook config update failed: ${e?.message || e}`, "error");
20966
+ });
20865
20967
  return true;
20866
20968
  }
20867
20969
  const parts = commandText.split("|").map((part) => part.trim());
20868
20970
  if (channelPrompt.kind === "channel-add") {
20869
20971
  const isPipe = parts.length > 1;
20870
20972
  const channelId = isPipe ? parts[1] : parts[0];
20871
- store.setChannel({
20973
+ Promise.resolve(store.setChannel({
20872
20974
  channelId,
20873
20975
  backend: channelPrompt.backend
20976
+ })).then(() => resumeAfterChannelPrompt(channelPrompt)).catch((e) => {
20977
+ store.pushNotice(`channel save failed: ${e?.message || e}`, "error");
20874
20978
  });
20875
- resumeAfterChannelPrompt(channelPrompt);
20876
20979
  return true;
20877
20980
  }
20878
20981
  if (channelPrompt.kind === "schedule-add") {
@@ -25982,13 +26085,13 @@ function createEngineApiB(bag) {
25982
26085
  pushNotice("webhook/ngrok authtoken forgotten", "info");
25983
26086
  return result;
25984
26087
  },
25985
- setChannel: (entry) => {
25986
- const result = runtime.setChannel(entry);
26088
+ setChannel: async (entry) => {
26089
+ const result = await runtime.setChannel(entry);
25987
26090
  pushNotice("channel saved", "info");
25988
26091
  return result;
25989
26092
  },
25990
- setWebhookConfig: (patch) => {
25991
- const result = runtime.setWebhookConfig(patch);
26093
+ setWebhookConfig: async (patch) => {
26094
+ const result = await runtime.setWebhookConfig(patch);
25992
26095
  pushNotice("webhook config updated", "info");
25993
26096
  return result;
25994
26097
  },
@@ -26076,6 +26179,39 @@ function createEngineApiB(bag) {
26076
26179
  listSessions: (options) => {
26077
26180
  return runtime.listSessions(options);
26078
26181
  },
26182
+ switchContext: async (options) => {
26183
+ if (getState().commandBusy) return false;
26184
+ set({ commandBusy: true });
26185
+ clearToastTimers();
26186
+ resetAllStreamingMarkdownStablePrefixes();
26187
+ const rollbackSnapshot = snapshotTuiBeforeSessionReset();
26188
+ resetTuiForPendingSessionReset();
26189
+ try {
26190
+ await runtime.switchContext(options);
26191
+ clearUiActivityBeforeContextSync();
26192
+ flags.pendingSessionReset = false;
26193
+ resetStatsAndSyncContext();
26194
+ set({
26195
+ items: replaceItems([]),
26196
+ toasts: [],
26197
+ queued: [],
26198
+ thinking: null,
26199
+ spinner: null,
26200
+ lastTurn: null,
26201
+ sessionId: null,
26202
+ cwd: runtime.cwd,
26203
+ ...routeState(),
26204
+ stats: { ...getState().stats }
26205
+ });
26206
+ return true;
26207
+ } catch (error) {
26208
+ restoreTuiAfterFailedSessionReset(rollbackSnapshot);
26209
+ throw error;
26210
+ } finally {
26211
+ flags.pendingSessionReset = false;
26212
+ set({ commandBusy: false });
26213
+ }
26214
+ },
26079
26215
  newSession: async () => {
26080
26216
  if (getState().commandBusy) return false;
26081
26217
  set({ commandBusy: true });
@@ -27972,6 +28108,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
27972
28108
  name: "mixdog-tui",
27973
28109
  signals: ["SIGINT", "SIGTERM", "SIGHUP"],
27974
28110
  timeoutMs: EXIT_WAIT_TIMEOUT_MS + 1e3,
28111
+ restoreTerminal,
27975
28112
  beforeCleanup: restoreTerminal,
27976
28113
  cleanup: disposeStoreOnce,
27977
28114
  afterCleanup: (reason) => {
@@ -280,13 +280,13 @@ export function createEngineApiB(bag) {
280
280
  pushNotice('webhook/ngrok authtoken forgotten', 'info');
281
281
  return result;
282
282
  },
283
- setChannel: (entry) => {
284
- const result = runtime.setChannel(entry);
283
+ setChannel: async (entry) => {
284
+ const result = await runtime.setChannel(entry);
285
285
  pushNotice('channel saved', 'info');
286
286
  return result;
287
287
  },
288
- setWebhookConfig: (patch) => {
289
- const result = runtime.setWebhookConfig(patch);
288
+ setWebhookConfig: async (patch) => {
289
+ const result = await runtime.setWebhookConfig(patch);
290
290
  pushNotice('webhook config updated', 'info');
291
291
  return result;
292
292
  },
@@ -376,6 +376,39 @@ export function createEngineApiB(bag) {
376
376
  listSessions: (options) => {
377
377
  return runtime.listSessions(options);
378
378
  },
379
+ switchContext: async (options) => {
380
+ if (getState().commandBusy) return false;
381
+ set({ commandBusy: true });
382
+ clearToastTimers();
383
+ resetAllStreamingMarkdownStablePrefixes();
384
+ const rollbackSnapshot = snapshotTuiBeforeSessionReset();
385
+ resetTuiForPendingSessionReset();
386
+ try {
387
+ await runtime.switchContext(options);
388
+ clearUiActivityBeforeContextSync();
389
+ flags.pendingSessionReset = false;
390
+ resetStatsAndSyncContext();
391
+ set({
392
+ items: replaceItems([]),
393
+ toasts: [],
394
+ queued: [],
395
+ thinking: null,
396
+ spinner: null,
397
+ lastTurn: null,
398
+ sessionId: null,
399
+ cwd: runtime.cwd,
400
+ ...routeState(),
401
+ stats: { ...getState().stats },
402
+ });
403
+ return true;
404
+ } catch (error) {
405
+ restoreTuiAfterFailedSessionReset(rollbackSnapshot);
406
+ throw error;
407
+ } finally {
408
+ flags.pendingSessionReset = false;
409
+ set({ commandBusy: false });
410
+ }
411
+ },
379
412
  newSession: async () => {
380
413
  if (getState().commandBusy) return false;
381
414
  set({ commandBusy: true });
package/src/tui/index.jsx CHANGED
@@ -530,6 +530,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
530
530
  name: 'mixdog-tui',
531
531
  signals: ['SIGINT', 'SIGTERM', 'SIGHUP'],
532
532
  timeoutMs: EXIT_WAIT_TIMEOUT_MS + 1000,
533
+ restoreTerminal,
533
534
  beforeCleanup: restoreTerminal,
534
535
  cleanup: disposeStoreOnce,
535
536
  afterCleanup: (reason) => {
@@ -18,7 +18,7 @@
18
18
  * loaded lazily via dynamic import — it should not load into memory unless a
19
19
  * user actually asks for voice.
20
20
  */
21
- import { readSection, updateSection } from '../../runtime/shared/config.mjs';
21
+ import { readSection, updateSectionAsync } from '../../runtime/shared/config.mjs';
22
22
  import { resolvePluginData } from '../../runtime/shared/plugin-paths.mjs';
23
23
 
24
24
  let _voiceRuntimeFetcherPromise = null;
@@ -148,12 +148,12 @@ export async function ensureVoiceRuntimeReady({ dataDir = resolvePluginData(), p
148
148
  export async function toggleVoice({ pushNotice, setProgressHint } = {}) {
149
149
  const dataDir = resolvePluginData();
150
150
  if (isVoiceEnabled()) {
151
- // updateSection is a synchronous file write (readAllForRmW + atomic write
152
- // under a file lock) and can throw — a locked/corrupt config, a
151
+ // The async RMW preserves the shared config lock/ACL protocol without
152
+ // blocking the TUI event loop. It can still throw — a locked/corrupt config,
153
153
  // permissions error, etc. Guard it the same way the ON path is guarded and
154
154
  // report failure via pushNotice instead of silently pretending success.
155
155
  try {
156
- updateSection('voice', (current) => ({ ...current, enabled: false }));
156
+ await updateSectionAsync('voice', (current) => ({ ...current, enabled: false }));
157
157
  } catch (err) {
158
158
  pushNotice?.(`Voice OFF failed: ${err?.message || err}`, 'error');
159
159
  return { ok: false, error: err?.message || String(err) };
@@ -168,7 +168,7 @@ export async function toggleVoice({ pushNotice, setProgressHint } = {}) {
168
168
  _voiceInstallBusy = true;
169
169
  try {
170
170
  await ensureVoiceRuntimeReady({ dataDir, pushNotice, setProgressHint });
171
- updateSection('voice', (current) => ({ ...current, enabled: true }));
171
+ await updateSectionAsync('voice', (current) => ({ ...current, enabled: true }));
172
172
  setProgressHint?.('');
173
173
  pushNotice?.('Voice ON — channel voice messages will be transcribed', 'info');
174
174
  return true;
@@ -1 +0,0 @@
1
- export default {connectToDevTools(){}};