mixdog 0.7.16 → 0.7.18

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 (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/UNINSTALL.md +7 -4
  4. package/bin/statusline-lib.mjs +29 -6
  5. package/bin/statusline-route.mjs +273 -0
  6. package/bin/statusline.mjs +31 -8
  7. package/commands/model.md +61 -0
  8. package/hooks/session-start.cjs +15 -1
  9. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  10. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  11. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  12. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  13. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  14. package/package.json +1 -1
  15. package/rules/lead/01-general.md +3 -0
  16. package/scripts/gateway-model.mjs +596 -0
  17. package/scripts/lib/gateway-inventory.mjs +178 -0
  18. package/scripts/lib/gateway-settings.mjs +78 -0
  19. package/scripts/run-mcp.mjs +26 -1
  20. package/scripts/statusline-launcher-smoke.mjs +155 -2
  21. package/scripts/uninstall.mjs +44 -7
  22. package/server-main.mjs +252 -11
  23. package/setup/setup.html +1 -1
  24. package/src/agent/index.mjs +96 -5
  25. package/src/agent/orchestrator/bridge-trace.mjs +18 -0
  26. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +4 -1
  27. package/src/agent/orchestrator/providers/anthropic.mjs +6 -2
  28. package/src/agent/orchestrator/session/loop.mjs +145 -4
  29. package/src/agent/orchestrator/session/trim.mjs +1 -1
  30. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +62 -6
  31. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  32. package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
  33. package/src/agent/tool-defs.mjs +10 -3
  34. package/src/channels/lib/runtime-paths.mjs +39 -4
  35. package/src/gateway/claude-current.mjs +255 -0
  36. package/src/gateway/oauth-usage.mjs +598 -0
  37. package/src/gateway/route-meta.mjs +629 -0
  38. package/src/gateway/server.mjs +713 -0
  39. package/src/shared/llm/cost.mjs +1 -1
  40. package/src/shared/seed.mjs +25 -0
  41. package/tools.json +21 -3
@@ -8,7 +8,7 @@ import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
8
8
  import { collectSkillsCached, loadSkillContent } from '../context/collect.mjs';
9
9
  import { traceBridgeLoop, traceBridgeTool, traceBridgeTrim, estimateProviderPayloadBytes, messagePrefixHash } from '../bridge-trace.mjs';
10
10
  import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal } from './manager.mjs';
11
- import { trimMessages, estimateRequestReserveTokens } from './trim.mjs';
11
+ import { trimMessages, estimateMessagesTokens, estimateRequestReserveTokens } from './trim.mjs';
12
12
  import { isContextOverflowError } from '../providers/retry-classifier.mjs';
13
13
  import { classifyBashFileLookupCommand, stripSoftWarns } from '../tool-loop-guard.mjs';
14
14
  import { maybeOffloadToolResult } from './tool-result-offload.mjs';
@@ -145,7 +145,7 @@ const _require = createRequire(import.meta.url);
145
145
  const _hooksLib = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../hooks/lib/permission-evaluator.cjs');
146
146
  const { evaluatePermission: _evaluatePermission } = _require(_hooksLib);
147
147
  const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
148
- const SAFETY_TRIM_PERCENT = 0.90;
148
+ const SAFETY_TRIM_PERCENT = 1.00;
149
149
  // Stricter budget used for the one-shot retry after a provider rejects a send
150
150
  // with a context-window-exceeded error. 0.60×contextWindow forces older
151
151
  // non-system / non-latest turns to drop hard so the retry fits even when the
@@ -154,6 +154,47 @@ const SAFETY_TRIM_PERCENT = 0.90;
154
154
  // retry block around provider.send below.
155
155
  const OVERFLOW_RETRY_TRIM_PERCENT = 0.60;
156
156
 
157
+ function estimateMessagesTokensSafe(messages) {
158
+ try { return estimateMessagesTokens(messages); }
159
+ catch { return null; }
160
+ }
161
+
162
+ class BridgeContextOverflowError extends Error {
163
+ constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
164
+ const target = [provider, model].filter(Boolean).join('/') || 'target model';
165
+ const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
166
+ super(
167
+ `bridge context overflow (${target}, stage=${stage || 'trim'}): ` +
168
+ `latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
169
+ `reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
170
+ `messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
171
+ );
172
+ this.name = 'BridgeContextOverflowError';
173
+ this.code = 'BRIDGE_CONTEXT_OVERFLOW';
174
+ this.sessionId = sessionId || null;
175
+ this.provider = provider || null;
176
+ this.model = model || null;
177
+ this.contextWindow = contextWindow ?? null;
178
+ this.budgetTokens = budgetTokens ?? null;
179
+ this.reserveTokens = reserveTokens ?? null;
180
+ this.messageTokensEst = messageTokensEst ?? null;
181
+ if (cause) this.cause = cause;
182
+ }
183
+ }
184
+
185
+ function bridgeContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
186
+ return new BridgeContextOverflowError({
187
+ stage,
188
+ sessionId,
189
+ provider: sessionRef?.provider || null,
190
+ model: sessionRef?.model || model || null,
191
+ contextWindow: sessionRef?.contextWindow ?? null,
192
+ budgetTokens,
193
+ reserveTokens,
194
+ messageTokensEst,
195
+ }, cause);
196
+ }
197
+
157
198
  // Cache-hit results always inline the cached body. The earlier size-gated
158
199
  // `[cache-hit-ref]` branch confused bridge workers whose context did not
159
200
  // contain the referenced prior tool_result, triggering shell-cat detours.
@@ -719,7 +760,41 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
719
760
  const beforeCount = messages.length;
720
761
  let beforeBytes = null;
721
762
  try { beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { beforeBytes = null; }
722
- const trimmed = trimMessages(messages, safetyBudget, { reserveTokens });
763
+ const messageTokensEst = estimateMessagesTokensSafe(messages);
764
+ let trimmed;
765
+ try {
766
+ trimmed = trimMessages(messages, safetyBudget, { reserveTokens });
767
+ } catch (trimErr) {
768
+ traceBridgeTrim({
769
+ sessionId,
770
+ iteration: iterations + 1,
771
+ stage: 'pre_send',
772
+ prune_count: 0,
773
+ trim_changed: false,
774
+ input_prefix_hash: messagePrefixHash(messages),
775
+ before_count: beforeCount,
776
+ after_count: messages.length,
777
+ before_bytes: beforeBytes,
778
+ after_bytes: beforeBytes,
779
+ context_window: sessionRef.contextWindow,
780
+ budget_tokens: safetyBudget,
781
+ reserve_tokens: reserveTokens,
782
+ message_tokens_est: messageTokensEst,
783
+ provider: sessionRef.provider,
784
+ model: sessionRef.model || model,
785
+ error: trimErr && trimErr.message ? trimErr.message : String(trimErr),
786
+ error_code: 'BRIDGE_CONTEXT_OVERFLOW',
787
+ });
788
+ throw bridgeContextOverflowError({
789
+ stage: 'pre_send',
790
+ sessionId,
791
+ sessionRef,
792
+ model,
793
+ budgetTokens: safetyBudget,
794
+ reserveTokens,
795
+ messageTokensEst,
796
+ }, trimErr);
797
+ }
723
798
  const trimChanged = messagesArrayChanged(messages, trimmed);
724
799
  const pruneCount = Math.max(beforeCount - trimmed.length, 0);
725
800
  if (trimChanged) {
@@ -742,6 +817,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
742
817
  traceBridgeTrim({
743
818
  sessionId,
744
819
  iteration: iterations + 1,
820
+ stage: 'pre_send',
745
821
  prune_count: pruneCount,
746
822
  trim_changed: trimChanged,
747
823
  input_prefix_hash: messagePrefixHash(messages),
@@ -749,6 +825,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
749
825
  after_count: messages.length,
750
826
  before_bytes: beforeBytes,
751
827
  after_bytes: afterBytes,
828
+ context_window: sessionRef.contextWindow,
829
+ budget_tokens: safetyBudget,
830
+ reserve_tokens: reserveTokens,
831
+ message_tokens_est: messageTokensEst,
832
+ provider: sessionRef.provider,
833
+ model: sessionRef.model || model,
752
834
  });
753
835
  }
754
836
  const nextIteration = iterations + 1;
@@ -875,9 +957,68 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
875
957
  }
876
958
  const overflowBudget = Math.floor(sessionRef.contextWindow * OVERFLOW_RETRY_TRIM_PERCENT);
877
959
  const overflowReserve = estimateRequestReserveTokens(sendTools.length ? sendTools : tools);
878
- const retrimmed = trimMessages(messages, overflowBudget, { reserveTokens: overflowReserve });
960
+ const beforeCount = messages.length;
961
+ let beforeBytes = null;
962
+ try { beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { beforeBytes = null; }
963
+ const messageTokensEst = estimateMessagesTokensSafe(messages);
964
+ let retrimmed;
965
+ try {
966
+ retrimmed = trimMessages(messages, overflowBudget, { reserveTokens: overflowReserve });
967
+ } catch (trimErr) {
968
+ traceBridgeTrim({
969
+ sessionId,
970
+ iteration: nextIteration,
971
+ stage: 'overflow_retry',
972
+ prune_count: 0,
973
+ trim_changed: false,
974
+ input_prefix_hash: messagePrefixHash(messages),
975
+ before_count: beforeCount,
976
+ after_count: messages.length,
977
+ before_bytes: beforeBytes,
978
+ after_bytes: beforeBytes,
979
+ context_window: sessionRef.contextWindow,
980
+ budget_tokens: overflowBudget,
981
+ reserve_tokens: overflowReserve,
982
+ message_tokens_est: messageTokensEst,
983
+ provider: sessionRef.provider,
984
+ model: sessionRef.model || model,
985
+ error: trimErr && trimErr.message ? trimErr.message : String(trimErr),
986
+ error_code: 'BRIDGE_CONTEXT_OVERFLOW',
987
+ });
988
+ throw bridgeContextOverflowError({
989
+ stage: 'overflow_retry',
990
+ sessionId,
991
+ sessionRef,
992
+ model,
993
+ budgetTokens: overflowBudget,
994
+ reserveTokens: overflowReserve,
995
+ messageTokensEst,
996
+ }, trimErr);
997
+ }
998
+ const trimChanged = messagesArrayChanged(messages, retrimmed);
999
+ const pruneCount = Math.max(beforeCount - retrimmed.length, 0);
879
1000
  messages.length = 0;
880
1001
  messages.push(...retrimmed);
1002
+ let afterBytes = null;
1003
+ try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
1004
+ traceBridgeTrim({
1005
+ sessionId,
1006
+ iteration: nextIteration,
1007
+ stage: 'overflow_retry',
1008
+ prune_count: pruneCount,
1009
+ trim_changed: trimChanged,
1010
+ input_prefix_hash: messagePrefixHash(messages),
1011
+ before_count: beforeCount,
1012
+ after_count: messages.length,
1013
+ before_bytes: beforeBytes,
1014
+ after_bytes: afterBytes,
1015
+ context_window: sessionRef.contextWindow,
1016
+ budget_tokens: overflowBudget,
1017
+ reserve_tokens: overflowReserve,
1018
+ message_tokens_est: messageTokensEst,
1019
+ provider: sessionRef.provider,
1020
+ model: sessionRef.model || model,
1021
+ });
881
1022
  // The transcript prefix changed; the server-side conversation anchor
882
1023
  // (previous_response_id / WS continuation) is now invalid. Drop
883
1024
  // providerState so the retry starts a fresh chain instead of
@@ -18,7 +18,7 @@ function messageEstimateText(m) {
18
18
  function estimateMessageTokens(m) {
19
19
  return estimateTokens(messageEstimateText(m)) + 4;
20
20
  }
21
- function estimateMessagesTokens(messages) {
21
+ export function estimateMessagesTokens(messages) {
22
22
  return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
23
23
  }
24
24
 
@@ -67,6 +67,35 @@ function isNonEmptyString(v) {
67
67
  return typeof v === 'string' && v.length > 0;
68
68
  }
69
69
 
70
+ function hasMeaningfulWriteSingle(a) {
71
+ return (isNonEmptyString(a?.path) || isNonEmptyString(a?.file_path))
72
+ && hasOwn(a, 'content')
73
+ && typeof a.content === 'string';
74
+ }
75
+
76
+ function isInertWriteEntry(w) {
77
+ return w && typeof w === 'object'
78
+ && !isNonEmptyString(w.path)
79
+ && !isNonEmptyString(w.file_path)
80
+ && (!hasOwn(w, 'content') || w.content === '');
81
+ }
82
+
83
+ function hasMeaningfulEditSingle(a) {
84
+ return hasOwn(a, 'old_string')
85
+ && hasOwn(a, 'new_string')
86
+ && isNonEmptyString(a.old_string)
87
+ && typeof a.new_string === 'string';
88
+ }
89
+
90
+ function isInertEditEntry(e) {
91
+ return e && typeof e === 'object'
92
+ && !isNonEmptyString(e.path)
93
+ && !isNonEmptyString(e.file_path)
94
+ && (!hasOwn(e, 'old_string') || e.old_string === '')
95
+ && (!hasOwn(e, 'new_string') || e.new_string === '')
96
+ && (!hasOwn(e, 'replace_all') || e.replace_all === false || e.replace_all === null || e.replace_all === undefined);
97
+ }
98
+
70
99
  function isStringOrStringArray(v) {
71
100
  if (typeof v === 'string') return true;
72
101
  if (!Array.isArray(v) || v.length === 0) return false;
@@ -216,8 +245,11 @@ function guardEdit(a) {
216
245
  if (op === 'rename') {
217
246
  return guardRename(a);
218
247
  }
219
- const hasSingle = hasOwn(a, 'old_string') && hasOwn(a, 'new_string');
220
- const hasBatch = hasOwn(a, 'edits') && Array.isArray(a.edits) && a.edits.length > 0;
248
+ if (Array.isArray(a.edits)) {
249
+ a.edits = a.edits.filter((e) => !isInertEditEntry(e));
250
+ }
251
+ const hasSingle = hasMeaningfulEditSingle(a);
252
+ const hasBatch = Array.isArray(a.edits) && a.edits.length > 0;
221
253
  if (hasSingle && hasBatch) {
222
254
  return 'Error: edit requires either (old_string + new_string) OR edits[], not both';
223
255
  }
@@ -231,16 +263,39 @@ function guardEdit(a) {
231
263
  if (typeof a.old_string !== 'string') {
232
264
  return `Error: edit arg "old_string" must be a string (got ${describeType(a.old_string)})`;
233
265
  }
266
+ if (a.old_string.length === 0) {
267
+ return 'Error: edit arg "old_string" must be non-empty';
268
+ }
234
269
  if (typeof a.new_string !== 'string') {
235
270
  return `Error: edit arg "new_string" must be a string (got ${describeType(a.new_string)})`;
236
271
  }
237
272
  }
273
+ if (hasBatch) {
274
+ for (let i = 0; i < a.edits.length; i++) {
275
+ const e = a.edits[i];
276
+ if (!e || typeof e !== 'object') {
277
+ return `Error: edit edits[${i}] must be an object with old_string + new_string`;
278
+ }
279
+ if (typeof e.old_string !== 'string') {
280
+ return `Error: edit edits[${i}].old_string must be a string (got ${describeType(e.old_string)})`;
281
+ }
282
+ if (e.old_string.length === 0) {
283
+ return `Error: edit edits[${i}].old_string must be non-empty`;
284
+ }
285
+ if (typeof e.new_string !== 'string') {
286
+ return `Error: edit edits[${i}].new_string must be a string (got ${describeType(e.new_string)})`;
287
+ }
288
+ }
289
+ }
238
290
  return null;
239
291
  }
240
292
 
241
293
  function guardWrite(a) {
242
- const hasSingle = (hasOwn(a, 'path') || hasOwn(a, 'file_path')) && hasOwn(a, 'content');
243
- const hasBatch = hasOwn(a, 'writes') && Array.isArray(a.writes) && a.writes.length > 0;
294
+ if (Array.isArray(a.writes)) {
295
+ a.writes = a.writes.filter((w) => !isInertWriteEntry(w));
296
+ }
297
+ const hasSingle = hasMeaningfulWriteSingle(a);
298
+ const hasBatch = Array.isArray(a.writes) && a.writes.length > 0;
244
299
  if (hasSingle && hasBatch) {
245
300
  return 'Error: write requires either (path + content) OR writes[], not both';
246
301
  }
@@ -265,8 +320,9 @@ function guardWrite(a) {
265
320
  if (!w || typeof w !== 'object') {
266
321
  return `Error: write writes[${i}] must be an object with path + content`;
267
322
  }
268
- if (typeof w.path !== 'string' || !w.path) {
269
- return `Error: write writes[${i}].path must be a non-empty string (got ${describeType(w.path)})`;
323
+ const p = hasOwn(w, 'path') ? w.path : w.file_path;
324
+ if (typeof p !== 'string' || !p) {
325
+ return `Error: write writes[${i}].path must be a non-empty string (got ${describeType(p)})`;
270
326
  }
271
327
  if (typeof w.content !== 'string') {
272
328
  return `Error: write writes[${i}].content must be a string (got ${describeType(w.content)})`;
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.16",
2
+ "version": "0.7.18",
3
3
  "_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-darwin-arm64",
7
- "sha256": "53f45f8373000fb55ccb49f9b03af0275ca2a663f924b68ee8ded4535e14445a"
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-graph-darwin-arm64",
7
+ "sha256": "34653a7cdf457295c4835fa9819627e9690c323b6012e6f571259478406bbe04"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-darwin-x64",
11
- "sha256": "347dc5e1b39dfc33ae07e01319b085c1f1ee775a70be1ac09f7b0d2954b36591"
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-graph-darwin-x64",
11
+ "sha256": "1c0648ed8a1b9fd2d8bc44d85b91262cb11e0773d2280d6ec351ee1ff1c579f0"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-linux-arm64",
15
- "sha256": "7648be2c4bc89bef837b4843639a9ab38c382ba103518ad52d4439587521542f"
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-graph-linux-arm64",
15
+ "sha256": "71c9d69125b97e93466e5a5613c00db2d797c1f25cc4237e776225574c5f6970"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-linux-x64",
19
- "sha256": "6e5a1b548c69c3a51170eb11c769985be7ca7029ede2597b19304c8dc106eded"
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-graph-linux-x64",
19
+ "sha256": "4800a7ac77b777ddb5862f252cdc00b55230455921e0c58416d7a77b120604a7"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-win32-x64.exe",
23
- "sha256": "6e53cdf523871f818942678d95e2235105b939ceeafc56a3b83b8190eeeba026"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-graph-win32-x64.exe",
23
+ "sha256": "1d0d62f5b78965c7d5b05ebc7cf7bdef1a3e14cc2600aa205ca762e56c446934"
24
24
  }
25
25
  }
26
26
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.16",
2
+ "version": "0.7.18",
3
3
  "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-darwin-arm64",
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-patch-darwin-arm64",
7
7
  "sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-darwin-x64",
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-patch-darwin-x64",
11
11
  "sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-linux-arm64",
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-patch-linux-arm64",
15
15
  "sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-linux-x64",
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-patch-linux-x64",
19
19
  "sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-win32-x64.exe",
23
- "sha256": "81fbabab9aa53f5fa57b1691b5ab24cbc1cf6ac1c2a36482c336411abe452d14"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.18/mixdog-patch-win32-x64.exe",
23
+ "sha256": "cf2f28f8b410a5b0b6deb500fbb332be7cba11adee4340c3433212327b14412c"
24
24
  }
25
25
  }
26
26
  }
@@ -13,6 +13,13 @@ export const TOOL_DEFS = [
13
13
  description: 'List models.',
14
14
  inputSchema: { type: 'object', properties: {} },
15
15
  },
16
+ {
17
+ name: 'gateway_select_model',
18
+ title: 'Select Gateway Model',
19
+ annotations: { title: 'Select Gateway Model', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
20
+ description: 'Interactively choose how the mixdog gateway routes Claude Code\'s main model. The first choice follows Claude Code\'s own current /model setting (including effort/fast where available); the rest list enabled providers + models. On selection, writes the gateway target/mode (+ enables the gateway module) — live on the next gateway request, no restart. If the client does not support elicitation, it falls back to listing the models and tells you to run /mixdog:model. Opt-in: only runs when you invoke it.',
21
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
22
+ },
16
23
  {
17
24
  name: 'explore',
18
25
  title: 'Explore',
@@ -70,7 +77,7 @@ export const TOOL_DEFS = [
70
77
  title: 'Bridge to External Model',
71
78
  annotations: { title: 'Bridge to External Model', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, bridgeHidden: true },
72
79
  description: 'Unified worker session control. type selects the action (default "spawn"). '
73
- + 'spawn: dispatch a detached worker — requires role + message (alias: prompt); optional tag names the session for later send/close (auto "<role><n>" if omitted). '
80
+ + 'spawn: dispatch a detached worker — requires role + prompt; optional tag names the session for later send/close (auto "<role><n>" if omitted). '
74
81
  + 'send: resume a worker by tag (or raw sess_ id) — requires tag + message. To CONTINUE the same task on the same files/context — fix rounds, re-reviews, iterative refinement, or any follow-up — PREFER reusing the original tag over a new spawn: it preserves the worker\'s transcript + file context + change history, so it is faster, cheaper (the prompt cache stays warm), and more accurate than re-discovering from scratch. The return\'s respawned flag = false when the live session continued (context kept), true when a closed/cold tag was respawned fresh (context reset). '
75
82
  + 'close: stop a worker — requires tag. '
76
83
  + 'list: enumerate active worker sessions with stage/staleness — no args (optional role/status filter).',
@@ -80,8 +87,8 @@ export const TOOL_DEFS = [
80
87
  type: { type: 'string', enum: ['spawn', 'send', 'close', 'list'], description: 'Action discriminator. Default "spawn".' },
81
88
  role: { type: 'string', description: 'Worker role (from user-workflow). Required for spawn; optional filter for list.' },
82
89
  tag: { type: 'string', description: 'Stable session handle. spawn: optional name; send/close: required target (tag or raw sess_ id).' },
83
- message: { type: 'string', description: 'Brief for spawn / follow-up for send. `prompt` is accepted as an alias on spawn. A spawn brief must state: goal+why, mode (write-code vs research-only), anchor (file:line/symbol/command), and done (output shape + stop condition).' },
84
- prompt: { type: 'string', description: 'Alias of message for spawn (backward compat).' },
90
+ message: { type: 'string', description: 'Follow-up text for send. For spawn, use prompt. A spawn brief must state: goal+why, mode (write-code vs research-only), anchor (file:line/symbol/command), and done (output shape + stop condition).' },
91
+ prompt: { type: 'string', description: 'Brief for spawn. Prefer this over message; must contain non-whitespace content unless file/ref is provided.' },
85
92
  status: { type: 'string', description: 'list: optional status filter (e.g. running, idle, error).' },
86
93
  provider: { type: 'string', description: 'spawn: override the worker provider.' },
87
94
  model: { type: 'string', description: 'spawn: override the worker model.' },
@@ -193,6 +193,9 @@ function writeActiveInstance(state) {
193
193
  writeJsonFile(ACTIVE_INSTANCE_FILE, state);
194
194
  }
195
195
  function buildActiveInstanceState(instanceId, meta) {
196
+ const gatewayMeta = Object.fromEntries(
197
+ Object.entries(meta || {}).filter(([k]) => k.startsWith('gateway_'))
198
+ );
196
199
  return {
197
200
  instanceId,
198
201
  ...buildRuntimeIdentity(),
@@ -202,10 +205,11 @@ function buildActiveInstanceState(instanceId, meta) {
202
205
  turnEndFile: getTurnEndPath(instanceId),
203
206
  statusFile: getStatusPath(instanceId),
204
207
  ...meta?.channelId ? { channelId: meta.channelId } : {},
205
- ...meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {},
206
- ...meta?.httpPort ? { httpPort: meta.httpPort } : {},
207
- ...meta?.memory_port ? { memory_port: meta.memory_port } : {},
208
- ...typeof meta?.backendReady === "boolean" ? { backendReady: meta.backendReady } : {}
208
+ ...meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {},
209
+ ...meta?.httpPort ? { httpPort: meta.httpPort } : {},
210
+ ...meta?.memory_port ? { memory_port: meta.memory_port } : {},
211
+ ...gatewayMeta,
212
+ ...typeof meta?.backendReady === "boolean" ? { backendReady: meta.backendReady } : {}
209
213
  };
210
214
  }
211
215
  function refreshActiveInstance(instanceId, meta) {
@@ -229,6 +233,9 @@ function refreshActiveInstance(instanceId, meta) {
229
233
  && prevServerPid === identity.server_pid
230
234
  && Number.isFinite(prevServerStartedAt)
231
235
  ) ? prevServerStartedAt : Date.now();
236
+ const gatewayMeta = Object.fromEntries(
237
+ Object.entries(meta || {}).filter(([k]) => k.startsWith('gateway_'))
238
+ );
232
239
  const next = {
233
240
  ...(prev?.instanceId === instanceId ? prevRest : buildActiveInstanceState(instanceId)),
234
241
  ...identity,
@@ -238,6 +245,7 @@ function refreshActiveInstance(instanceId, meta) {
238
245
  ...meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {},
239
246
  ...meta?.httpPort ? { httpPort: meta.httpPort } : {},
240
247
  ...meta?.memory_port ? { memory_port: meta.memory_port } : {},
248
+ ...gatewayMeta,
241
249
  ...typeof meta?.backendReady === "boolean" ? { backendReady: meta.backendReady } : {},
242
250
  };
243
251
  if (typeof meta?.transcriptPath === "string" && meta.transcriptPath) {
@@ -283,6 +291,33 @@ function refreshActiveInstance(instanceId, meta) {
283
291
  preservedExtra.memory_server_pid = prevMemoryServerPid;
284
292
  }
285
293
  }
294
+ // gateway_port follows the same preservation rule as memory_port: the
295
+ // gateway child advertises itself independently, and active-owner
296
+ // heartbeats must not erase that discovery record while its owning
297
+ // server-main process is still alive.
298
+ const prevGatewayServerPid = parsePositivePid(prevForPreserve?.gateway_server_pid);
299
+ const prevGatewayOwnerAlive = (() => {
300
+ if (prevGatewayServerPid === null) return false;
301
+ try {
302
+ process.kill(prevGatewayServerPid, 0);
303
+ return true;
304
+ } catch (e) {
305
+ if (e && e.code === "ESRCH") return false;
306
+ return true;
307
+ }
308
+ })();
309
+ const sameGatewayAdvertiser =
310
+ prevGatewayServerPid !== null &&
311
+ identity.server_pid !== null &&
312
+ prevGatewayServerPid === identity.server_pid;
313
+ if (sameGatewayAdvertiser || prevGatewayOwnerAlive) {
314
+ if (prevForPreserve && Object.prototype.hasOwnProperty.call(prevForPreserve, 'gateway_port')) {
315
+ for (const [key, value] of Object.entries(prevForPreserve)) {
316
+ if (key.startsWith('gateway_')) preservedExtra[key] = value;
317
+ }
318
+ preservedExtra.gateway_server_pid = prevGatewayServerPid;
319
+ }
320
+ }
286
321
  return { ...preservedExtra, ...next };
287
322
  }, { compact: true, fsyncDir: true });
288
323
  }