mixdog 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/NOTICE.md +52 -9
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -1
  4. package/src/defaults/skills/setup/references/actions.md +4 -1
  5. package/src/defaults/skills/setup-guide/SKILL.md +10 -7
  6. package/src/output-styles/common.md +13 -8
  7. package/src/rules/agent-mode/AGENT_MODE.md +1 -1
  8. package/src/rules/agent-mode/MIXY.md +11 -4
  9. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
  10. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
  11. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  12. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  13. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  14. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  15. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  16. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  17. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  18. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  19. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  20. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
  21. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
  22. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
  23. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
  24. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
  25. package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
  26. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
  27. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  28. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  29. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
  30. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  31. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  32. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  33. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
  34. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  36. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  38. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
  39. package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
  40. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
  41. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  42. package/src/runtime/attachments/store.mjs +6 -0
  43. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  44. package/src/runtime/effort-judge/model-manifest.json +7 -7
  45. package/src/runtime/shared/agent-kind.mjs +152 -0
  46. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  47. package/src/runtime/shared/agent-route-config.mjs +22 -8
  48. package/src/runtime/shared/agent-store.mjs +132 -0
  49. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  50. package/src/runtime/shared/config-patch.mjs +20 -1
  51. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  52. package/src/runtime/shared/connector-catalog.mjs +2 -4
  53. package/src/runtime/shared/connector-triggers.mjs +2 -0
  54. package/src/runtime/shared/err-text.mjs +3 -1
  55. package/src/runtime/shared/error-presentation.mjs +91 -6
  56. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  57. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  58. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  59. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  60. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  61. package/src/runtime/shared/plain-preview.mjs +22 -17
  62. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  63. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  64. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  65. package/src/runtime/shared/user-data-guard.mjs +5 -2
  66. package/src/session-runtime/agent-home-delivery.mjs +423 -56
  67. package/src/session-runtime/agent-room-delivery.mjs +45 -4
  68. package/src/session-runtime/agent-task-sessions.mjs +299 -39
  69. package/src/session-runtime/boot/apis.mjs +13 -0
  70. package/src/session-runtime/boot/providers.mjs +14 -4
  71. package/src/session-runtime/config-lifecycle.mjs +43 -2
  72. package/src/session-runtime/handoff-attachments.mjs +593 -0
  73. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
  74. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  75. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  76. package/src/session-runtime/model-route/set-route.mjs +21 -1
  77. package/src/session-runtime/native-web-search.mjs +2 -2
  78. package/src/session-runtime/new-session-config.mjs +6 -1
  79. package/src/session-runtime/route-resolution.mjs +5 -11
  80. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  81. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  82. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  83. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  84. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  85. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  86. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  87. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
  88. package/src/session-runtime/tool-policy-refresh.mjs +13 -2
  89. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
  90. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
  91. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  92. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  93. package/src/standalone/daemon-session-control.mjs +13 -5
  94. package/src/standalone/daemon.mjs +16 -4
  95. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  96. package/src/standalone/session-service/agent-tree.mjs +4 -3
  97. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  98. package/src/tui/dist/index.mjs +70 -9
  99. package/src/tui/session/notice-surface.mjs +3 -4
  100. package/src/tui/session/session-api/routes.mjs +14 -3
  101. package/src/tui/session/transcript-intake.mjs +2 -0
  102. package/src/tui/session/turn-live-call.mjs +11 -3
  103. package/src/tui/session/turn.mjs +13 -3
@@ -10,7 +10,13 @@ export function createRouteTuning(deps, persist) {
10
10
  const { getConfig, getRoute, setRouteState, resolveRoute, lookupModelMeta, refreshRouteEffort } = deps;
11
11
 
12
12
  /** Save the live route's modelSettings, re-resolve through the lead preset
13
- * when one exists, refresh effort and tune the current session. */
13
+ * when one exists, refresh effort and tune the current session.
14
+ *
15
+ * This is not a Main-model write and needs no compare-and-set: it persists
16
+ * the LIVE route (read after every await), and only when that route's model
17
+ * is already the Main model (persistAdoptedModelSettings: tunesNonMainModel),
18
+ * so the lead preset's model is rewritten with itself and never changes. A
19
+ * live route without a model normalizes to nothing and writes no preset. */
14
20
  async function commitRouteTuning(fastCapable, modelMeta) {
15
21
  persist.saveRouteModelSettings(getRoute(), fastCapable);
16
22
  const leadRoute = persist.persistAdoptedModelSettings(getRoute());
@@ -195,9 +195,23 @@ async function adoptRoute(deps, persist, next, options, lease) {
195
195
  scheduleStatuslineUsageRefresh,
196
196
  } = deps;
197
197
  const { selectedRoute, fastCapable, modelMeta } = await resolveSelectedRoute(deps, next);
198
+ // The Lead route a conditional fill wrote, until the file confirms it.
199
+ let filled = null;
198
200
  // Provider readiness may outlast the session the selection was made for.
199
201
  const adopted = lease.run(
200
202
  () => {
203
+ // Compare-and-set: a route that already has a model (picked while this
204
+ // selection was resolving) is never replaced. Checked synchronously with
205
+ // the adopt below, so nothing can set a route in between. A Lead route
206
+ // another process persisted may not have reached this runtime yet (the
207
+ // change notification is debounced): the config is re-read from disk
208
+ // right here, which adopts it and fills the empty route, and that route
209
+ // is the winner this selection answers with.
210
+ if (options?.onlyIfEmpty === true) {
211
+ if (clean(getRoute()?.model)) return false;
212
+ deps.reloadFullConfig?.();
213
+ if (clean(getRoute()?.model)) return false;
214
+ }
201
215
  if (options?.sessionOnly === true) {
202
216
  // An agent child's route belongs to that session alone: the shared
203
217
  // modelSettings bucket (saved effort/Fast per model) and the main
@@ -210,7 +224,9 @@ async function adoptRoute(deps, persist, next, options, lease) {
210
224
  // — the session surface's model pick, an heir — keeps it.
211
225
  const leadRoute = persist.persistAdoptedModelSettings(selectedRoute, {
212
226
  keepMainModel: options?.keepMainModel !== false,
227
+ onlyIfAbsent: options?.onlyIfEmpty === true,
213
228
  });
229
+ filled = leadRoute;
214
230
  setRouteState(resolveRoute(getConfig(), leadRoute ? { model: workflowPresetId('lead') } : selectedRoute));
215
231
  }
216
232
  return true;
@@ -218,9 +234,13 @@ async function adoptRoute(deps, persist, next, options, lease) {
218
234
  () => false
219
235
  );
220
236
  if (!adopted) return false;
237
+ // A conditional fill reaches the file only while no Lead route is stored
238
+ // there (judged under the config file lock). When another process wrote one
239
+ // after the re-read above, the file's route replaces the fill in the runtime.
240
+ const replaced = filled && (await deps.confirmLeadFill?.(filled)) === true;
221
241
  // The refresh completes against the route just adopted and only while the
222
242
  // selection still owns it.
223
- await refreshRouteEffort(modelMeta, getRoute(), null, lease);
243
+ await refreshRouteEffort(replaced ? null : modelMeta, getRoute(), null, lease);
224
244
  return lease.run(
225
245
  () => {
226
246
  refreshStatuslineUsageSnapshot(getRoute());
@@ -14,7 +14,7 @@ import {
14
14
  } from './native-web-search/tool-shape.mjs';
15
15
  import { createWebSearchRouteCandidates } from './native-web-search/routes.mjs';
16
16
  import { reportNativeWebSearchUsage } from './native-web-search-usage.mjs';
17
- import { canFallbackRoute } from '../runtime/shared/model-route-fallback.mjs';
17
+ import { canFallbackCapacityRoute } from '../runtime/shared/model-route-fallback.mjs';
18
18
 
19
19
  export function createNativeWebSearch(deps) {
20
20
  const {
@@ -103,7 +103,7 @@ export function createNativeWebSearch(deps) {
103
103
  try {
104
104
  return await sendCandidate(candidate, toolType, webSearchArgs, signal);
105
105
  } catch (err) {
106
- if (!canFallbackRoute(err, { signal })) throw err;
106
+ if (!canFallbackCapacityRoute(err, { signal })) throw err;
107
107
  errors.push(
108
108
  `${candidate.provider}${candidate.model ? `/${candidate.model}` : ''}/${toolType}: ${err?.message || String(err)}`
109
109
  );
@@ -1,4 +1,5 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
+ import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
2
3
  import { flushPendingSessionConfigWrites } from './config-lifecycle.mjs';
3
4
 
4
5
  // Run only after the lifecycle has established that it needs a NEW session.
@@ -27,7 +28,11 @@ export function createNewSessionConfig({
27
28
  if (!initialConfig || typeof initialConfig !== 'object') {
28
29
  reloadFullConfig();
29
30
  if (!firstSession || !initialRouteExplicit) {
30
- rt.route = resolveRoute(rt.config, {});
31
+ const configured = resolveRoute(rt.config, {});
32
+ // With no Lead model configured the model picked for this runtime's
33
+ // still-empty conversation (a session-only choice never writes the
34
+ // Lead route) stands; the reload must not drop it before the first turn.
35
+ if (clean(configured?.model) || !clean(rt.route?.model)) rt.route = configured;
31
36
  }
32
37
  }
33
38
  invalidateOutputStyleStatusCache();
@@ -1,5 +1,5 @@
1
- // Route resolution against the live runtime record: pick a model for a
2
- // model-less first turn and refresh the route's effort / Fast / context
1
+ // Route resolution against the live runtime record: refuse a model-less first
2
+ // turn (no auto-pick) and refresh the route's effort / Fast / context
3
3
  // fields from provider metadata.
4
4
  import { ensureProviderEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
5
5
  import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
@@ -22,17 +22,11 @@ export function createRouteResolution({ rt, collectProviderModels, ensureProvide
22
22
  if (routeHasModel()) return rt.route;
23
23
  const models = await runAbortable(signal, () => collectProviderModels());
24
24
  throwIfAborted(signal);
25
- const picked = models[0] || null;
26
- if (!picked) {
25
+ if (!models?.length) {
27
26
  throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
28
27
  }
29
- rt.route = {
30
- ...rt.route,
31
- provider: picked.provider,
32
- model: picked.id,
33
- preset: null,
34
- };
35
- return rt.route;
28
+ // Never auto-pick: the user chooses the model.
29
+ throw new Error('No model selected. Choose a model with /model first.');
36
30
  }
37
31
 
38
32
  async function refreshRouteEffort(modelMetaOverride = null, expectedRoute = null, signal = null, lease = null) {
@@ -12,6 +12,8 @@ import {
12
12
  import { PRESET_ALIASES } from './tool-def.mjs';
13
13
  import { positiveInt } from '../../../runtime/shared/numbers.mjs';
14
14
  import { clean } from '../../../runtime/shared/clean.mjs';
15
+ import { readAgentDefinitionKind } from '../../../runtime/shared/agent-kind.mjs';
16
+ import { isStoreBot, userDefinitionDir } from '../../../runtime/shared/agent-store.mjs';
15
17
  import { normalizeContextTokens } from '../../../runtime/shared/llm/default-context-window.mjs';
16
18
 
17
19
  export { positiveInt };
@@ -300,13 +302,18 @@ const _frontmatterPermCache = new Map(); // key -> { value, atMs }
300
302
  const FRONTMATTER_PERM_CACHE_TTL_MS = envTimeoutMs('MIXDOG_AGENT_FRONTMATTER_TTL_MS', 5_000);
301
303
 
302
304
  function agentDeleted(cleanAgent, dataDir) {
303
- return Boolean(dataDir) && existsSync(join(dataDir, 'agents', cleanAgent, '.deleted'));
305
+ return Boolean(dataDir) && existsSync(join(userDefinitionDir(dataDir, cleanAgent), '.deleted'));
304
306
  }
305
307
 
306
308
  /** Where an agent's definition may live, in precedence order: the data dir
307
309
  * (user/custom agents) before the shipped source root. */
308
310
  function agentDefinitionFiles(cleanAgent, dataDir, standaloneSourceRoot) {
309
311
  const candidates = [];
312
+ if (dataDir && isStoreBot(dataDir, cleanAgent)) {
313
+ // A bot of the store: only its own folder (never the agents/ original).
314
+ candidates.push(join(userDefinitionDir(dataDir, cleanAgent), 'AGENT.md'));
315
+ return candidates;
316
+ }
310
317
  if (dataDir) {
311
318
  candidates.push(join(dataDir, 'agents', cleanAgent, 'AGENT.md'));
312
319
  candidates.push(join(dataDir, 'agents', `${cleanAgent}.md`));
@@ -353,13 +360,25 @@ export function readAgentFrontmatterPermission(agent, dataDir, standaloneSourceR
353
360
  }
354
361
 
355
362
  const _frontmatterKindCache = new Map(); // key -> { value, atMs }
363
+ const _frontmatterKindValueCache = new Map(); // key -> { value, atMs }
356
364
 
357
- /** The agent definition's frontmatter `kind` (isContactAgentId reads it), or
358
- * null when it sets none. */
365
+ /** The agent definition's kind (isContactAgentId reads it), by the one
366
+ * read-only rule (agent-kind.mjs), cached per agent for the frontmatter TTL. */
359
367
  export function readAgentFrontmatterKind(agent, dataDir, standaloneSourceRoot) {
360
- return cachedFrontmatterValue(_frontmatterKindCache, agent, dataDir, standaloneSourceRoot, (fm) =>
361
- typeof fm.kind === 'string' && fm.kind.trim() ? fm.kind.trim() : null
368
+ const id = clean(agent);
369
+ if (!id || agentDeleted(id, dataDir)) return null;
370
+ const cacheKey = `${dataDir}\u0000${id}`;
371
+ const cached = _frontmatterKindCache.get(cacheKey);
372
+ if (cached && Date.now() - cached.atMs < FRONTMATTER_PERM_CACHE_TTL_MS) return cached.value;
373
+ // An explicit kind in the definition in effect (any layout) wins; only an
374
+ // unmarked user copy goes to the evidence rule.
375
+ const explicit = cachedFrontmatterValue(_frontmatterKindValueCache, id, dataDir, standaloneSourceRoot, (fm) =>
376
+ typeof fm.kind === 'string' && fm.kind.trim() ? fm.kind.trim().toLowerCase() : null
362
377
  );
378
+ const value =
379
+ explicit || (dataDir ? readAgentDefinitionKind({ dataDir, shippedRoot: standaloneSourceRoot, id }) : null);
380
+ _frontmatterKindCache.set(cacheKey, { value, atMs: Date.now() });
381
+ return value;
363
382
  }
364
383
 
365
384
  /** The agent definition's `firstResponseTimeoutMs` (a non-negative integer;
@@ -16,6 +16,7 @@ import {
16
16
  terminalResultHook,
17
17
  } from './job-task-reconcile.mjs';
18
18
  import { createTurnReviewCollector } from './turn-review.mjs';
19
+ import { handoffContent } from '../../handoff-attachments.mjs';
19
20
  import { settledWorkerStatus } from './worker-index/row-liveness.mjs';
20
21
 
21
22
  // `send` to an existing worker: resolve the live session, run one follow-up
@@ -69,8 +70,10 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
69
70
  return hooks;
70
71
  }
71
72
 
72
- function runTurn({ session, sessionId, prompt, args }, turnHooks) {
73
+ function runTurn({ session, sessionId, prompt: text, args }, turnHooks) {
73
74
  const cwd = session.cwd || defaultCwd;
75
+ // The caller's forwarded files ride along as attachment parts.
76
+ const prompt = handoffContent(text, args.attachmentParts);
74
77
  if (canUseSessionSurface(session)) {
75
78
  return sessionSurface.runTurn({ session, prompt, context: args.context || null, cwd, ...turnHooks });
76
79
  }
@@ -194,7 +197,11 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
194
197
  const { session, sessionId, prompt } = prepared;
195
198
  const tag = registry.tagForSession(sessionId);
196
199
  if (canUseSessionSurface(session) && views.isSessionBusy(sessionId)) {
197
- const queued = await sessionSurface.enqueueTurn({ session, prompt, context: prepared.args.context || null });
200
+ const queued = await sessionSurface.enqueueTurn({
201
+ session,
202
+ prompt: handoffContent(prompt, prepared.args.attachmentParts),
203
+ context: prepared.args.context || null,
204
+ });
198
205
  if (queued) {
199
206
  return renderResult({ queued: true, ...extras, tag, sessionId, agent: session.agent || null, ...queued });
200
207
  }
@@ -204,7 +211,8 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
204
211
  views.isSessionBusy(sessionId) &&
205
212
  typeof mgr.enqueuePendingMessage === 'function'
206
213
  ) {
207
- const queueDepth = mgr.enqueuePendingMessage(sessionId, prompt);
214
+ // The queued follow-up keeps the forwarded files, as the canonical queue does.
215
+ const queueDepth = mgr.enqueuePendingMessage(sessionId, handoffContent(prompt, prepared.args.attachmentParts));
208
216
  return renderResult({ queued: true, ...extras, tag, sessionId, agent: session.agent || null, queueDepth });
209
217
  }
210
218
  const job = spawnFlow.startJob(
@@ -25,6 +25,7 @@ import { settledWorkerStatus } from '../worker-index/row-liveness.mjs';
25
25
  import { saveSessionAsync } from '../../../../runtime/agent/orchestrator/session/store.mjs';
26
26
  import { presetDescriptor } from './spawn-prep.mjs';
27
27
  import { workerStageForStatus } from '../worker-rows.mjs';
28
+ import { handoffContent } from '../../../handoff-attachments.mjs';
28
29
 
29
30
  // Mid-turn worker persistence cadence. The watchdog sweep runs every second;
30
31
  // a transcript save is far more expensive than a bookkeeping stamp, so the
@@ -117,7 +118,9 @@ export function createSpawnRunner({
117
118
  };
118
119
 
119
120
  async function runTurn(prepared, turnHooks) {
120
- const { args, session, prompt, workerCwd } = prepared;
121
+ const { args, session, workerCwd } = prepared;
122
+ // The caller's forwarded files ride along as attachment parts.
123
+ const prompt = handoffContent(prepared.prompt, args.attachmentParts);
121
124
  if (typeof sessionSurface?.runTurn === 'function' && sessionSurface.canRun?.(session) !== false) {
122
125
  return sessionSurface.runTurn({ session, prompt, context: args.context || null, cwd: workerCwd, ...turnHooks });
123
126
  }
@@ -27,11 +27,10 @@ export function resolveAgentSpawnPreset(config, args = {}) {
27
27
  }
28
28
 
29
29
  const agentName = normalizeAgentName(args.agent);
30
- const agentRoute = !clean(args.preset)
31
- ? configuredAgentRouteCandidates(config, agentName)
32
- .map((candidate) => normalizeAgentRoute(candidate))
33
- .find(Boolean) || null
34
- : null;
30
+ // The agent's stored route: config.agents, or a store bot's own folder.
31
+ const candidates = !clean(args.preset) ? configuredAgentRouteCandidates(config, agentName) : [];
32
+ const agentRoute = candidates.map((candidate) => normalizeAgentRoute(candidate)).find(Boolean) || null;
33
+ const storedFallbacks = candidates.find((candidate) => normalizeAgentRoute(candidate))?.fallbacks;
35
34
  if (agentRoute) {
36
35
  return {
37
36
  presetName: agentPresetName(agentName),
@@ -41,9 +40,7 @@ export function resolveAgentSpawnPreset(config, args = {}) {
41
40
  type: 'agent',
42
41
  provider: agentRoute.provider,
43
42
  model: agentRoute.model,
44
- ...(config.agents?.[agentName]?.fallbacks?.length
45
- ? { fallbacks: config.agents[agentName].fallbacks.map(normalizeAgentRoute) }
46
- : {}),
43
+ ...(storedFallbacks?.length ? { fallbacks: storedFallbacks.map(normalizeAgentRoute) } : {}),
47
44
  effort: agentRoute.effort,
48
45
  fast: agentRoute.fast === true,
49
46
  ...(agentRoute.autoEffort === false ? { autoEffort: false } : {}),
@@ -53,6 +53,13 @@ export const AGENT_TOOL = {
53
53
  file: { type: 'string', description: 'Path to task text; alternative to prompt/message for spawn/send.' },
54
54
  cwd: { type: 'string', description: 'Project directory for a new or respawned agent.' },
55
55
  context: { type: 'string', description: 'Extra context for the spawn/send turn.' },
56
+ attachments: {
57
+ type: 'array',
58
+ items: { type: 'string' },
59
+ maxItems: 8,
60
+ description:
61
+ "spawn/send: files to attach (images, PDF/Office, text): a conversation attachment's file name or [attachment:id], or a path inside your folders. Produced files come back as paths; forward them by path.",
62
+ },
56
63
  model: {
57
64
  type: 'string',
58
65
  description:
@@ -377,6 +377,17 @@ const SETUP_STATUS_READERS = {
377
377
  // schema, the per-action field list and the route/object-argument minimums.
378
378
  function validateSetupInput(args) {
379
379
  const action = requireEnum(args?.action, SETUP_ACTIONS, 'action');
380
+ // Old callers still ask to connect GitHub: it is the signed-in GitHub CLI, not a catalog connector.
381
+ if (
382
+ action === 'connect_connector' &&
383
+ String(args.connector ?? '')
384
+ .trim()
385
+ .toLowerCase() === 'github'
386
+ ) {
387
+ throw new Error(
388
+ 'GitHub uses the signed-in GitHub CLI; no connector needed. Use the github tool or Settings → Git.'
389
+ );
390
+ }
380
391
  const validationError = schemaValueError({ ...args, action }, SETUP_TOOL_DEFS[0].inputSchema, 'setup');
381
392
  if (validationError) throw new Error(`[tool-input-validation] ${validationError}`);
382
393
  const fields = SETUP_ACTION_FIELDS[action].split(' ').filter(Boolean);
@@ -647,7 +658,7 @@ const SETUP_ACTION_HANDLERS = {
647
658
  const entry = connectorById(requireText(args.connector, 'connector'));
648
659
  if (!entry || entry.ownKey) throw new Error(`connector must be one of ${CONNECTOR_IDS.join(', ')}`);
649
660
  const server = (rt.mcpStatus?.()?.servers || []).find((row) => row.connector === entry.id) || null;
650
- const connected = entry.backend.kind === 'builtin' ? null : server?.connected === true;
661
+ const connected = server?.connected === true;
651
662
  return {
652
663
  connector: entry.id,
653
664
  name: entry.name,
@@ -1,7 +1,9 @@
1
1
  import { SETUP_EXTENDED_PROPERTIES, SETUP_DESKTOP_ACTIONS } from './settings-contract.mjs';
2
2
  import { MEDIA_KINDS } from '../../runtime/media/tool-defs.mjs';
3
3
  import { OPENAI_COMPAT_PRESETS } from '../../runtime/agent/orchestrator/providers/openai-compat-presets.mjs';
4
- import { agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
4
+ import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
5
+ import { AGENT_MODE, resolveSessionMode } from '../../runtime/agent/orchestrator/session/agent-mode.mjs';
6
+ import { BOT_AGENT_KIND, WORKFLOW_AGENT_KIND } from '../../runtime/shared/agent-route-config.mjs';
5
7
  import { assertAutomationAccess, scheduleCallerScope } from '../services/schedule-access.mjs';
6
8
 
7
9
  const own = (value, key) => Object.hasOwn(value || {}, key);
@@ -164,13 +166,20 @@ async function readDefinition(rt, kind, name) {
164
166
  };
165
167
  }
166
168
 
167
- async function saveDefinition(rt, args) {
169
+ // A created agent's explicit marker: the one given, else a bot when Mixy's
170
+ // agent-mode chat creates it (Mixy creates bots), else a workflow agent.
171
+ function createdAgentKind(input, caller) {
172
+ if (own(input, 'kind')) return input.kind;
173
+ const { mode, agentId } = resolveSessionMode({ session: caller });
174
+ return mode === AGENT_MODE && agentId === MIXY_AGENT_ID ? BOT_AGENT_KIND : WORKFLOW_AGENT_KIND;
175
+ }
176
+
177
+ async function saveDefinition(rt, args, context = {}) {
168
178
  const kind = args.definitionKind;
169
179
  const input = args.definition;
170
- const allowed =
171
- kind === 'skill'
172
- ? ['originalName', 'name', 'description', 'body', 'whenToUse', 'toolDependencies']
173
- : ['id', 'name', 'description', 'body'];
180
+ let allowed = ['id', 'name', 'description', 'body'];
181
+ if (kind === 'skill') allowed = ['originalName', 'name', 'description', 'body', 'whenToUse', 'toolDependencies'];
182
+ else if (kind === 'agent') allowed = [...allowed, 'kind'];
174
183
  const unexpected = Object.keys(input).find((key) => !allowed.includes(key));
175
184
  if (unexpected) throw new Error(`${unexpected} is not a ${kind} definition field`);
176
185
  const creating = args.action === 'create_definition';
@@ -200,7 +209,10 @@ async function saveDefinition(rt, args) {
200
209
  const agents = await rt.listAgents();
201
210
  if (agents.some((agent) => agent.id === input.id)) throw new Error(`agent "${input.id}" already exists`);
202
211
  }
203
- return rt.saveAgentDefinition(creating ? next : { ...next, id: current.id });
212
+ if (creating) return rt.saveAgentDefinition({ ...next, kind: createdAgentKind(input, context.caller) });
213
+ // The stored kind is kept as it is; an edit naming one is refused by the editor.
214
+ const { kind: _storedKind, ...edit } = { ...pick(current, definitionKeys), ...input };
215
+ return rt.saveAgentDefinition({ ...edit, ...(own(input, 'kind') ? { kind: input.kind } : {}), id: current.id });
204
216
  }
205
217
  return creating ? rt.addSkill(next) : rt.saveSkill({ ...next, originalName: current.originalName });
206
218
  }
@@ -157,6 +157,12 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
157
157
  name: text,
158
158
  description: text,
159
159
  body: text,
160
+ kind: {
161
+ type: 'string',
162
+ enum: ['bot', 'agent'],
163
+ description:
164
+ 'Agent create only: "bot" (a contact the user chats with) or "agent" (a workflow sub-agent). Omitted: a bot when created from the representative bot\'s chat, else a workflow agent. Fixed after creation.',
165
+ },
160
166
  whenToUse: text,
161
167
  toolDependencies: {
162
168
  type: ['array', 'null'],
@@ -343,7 +343,17 @@ export function createToolPolicyRefresh({
343
343
  to: session.workflow.name,
344
344
  at: Date.now(),
345
345
  };
346
- recordTranscriptBoundary(session, 'workflowSwitches', workflowSwitch, (entry) => entry?.at === workflowSwitch.at);
346
+ // Two switches can land in the same millisecond (A→B→A): the same
347
+ // switch is a repeat only when its direction matches too.
348
+ recordTranscriptBoundary(
349
+ session,
350
+ 'workflowSwitches',
351
+ workflowSwitch,
352
+ (entry) =>
353
+ entry?.at === workflowSwitch.at &&
354
+ entry?.fromId === workflowSwitch.fromId &&
355
+ entry?.toId === workflowSwitch.toId
356
+ );
347
357
  }
348
358
  return { appliedToCurrentSession: true, workflow: session.workflow, workflowSwitch };
349
359
  }
@@ -369,7 +379,8 @@ export function createToolPolicyRefresh({
369
379
  const identity = session?.promptIdentity;
370
380
  const agent = identity?.agent;
371
381
  if (!session?.id || !agent || isAgentOwner(session)) return false;
372
- const revision = agentProfileRevision(agent);
382
+ // Only Mixy's own (agent-mode) thread embeds Mixy's profile.
383
+ const revision = agentProfileRevision(agent, { mixy: sessionModeOf(session) === AGENT_MODE });
373
384
  const config = getConfig();
374
385
  const workflowId = session.workflow?.id;
375
386
  const workflowConfig = workflowId
@@ -10,6 +10,8 @@ import { throwIfAborted } from '../../../runtime/shared/abort-race.mjs';
10
10
  // UserPromptSubmit hook.
11
11
  export function createTurnPreparation({
12
12
  getSession,
13
+ setSession,
14
+ mgr,
13
15
  getCurrentCwd,
14
16
  awaitRoutePreparation,
15
17
  createCurrentSession,
@@ -105,6 +107,15 @@ export function createTurnPreparation({
105
107
  }
106
108
  throwIfAborted(turn.signal);
107
109
  assertSessionAdmission?.();
110
+ // askSession runs the turn on the session the store serves for this id.
111
+ // While this runtime idled, the store's idle sweep may have released the
112
+ // snapshot it holds, and the store then serves a fresh parse of the file:
113
+ // preparing the turn on the held copy would leave its one-shot work (the
114
+ // first-turn MCP fold, its flag) undone on the turn's session, which then
115
+ // rewrites the prompt it already sent on the next turn. Prepare the turn
116
+ // on the session it runs on (as completeTurn adopts it afterwards).
117
+ const stored = mgr.getSession(getSession().id);
118
+ if (stored && stored !== getSession()) setSession(stored);
108
119
  // Turn admission: waits while this session's workspace transitions or
109
120
  // another session changes the checkout this session works in, then holds
110
121
  // the turn registered (released by settleTurn) so no checkout changes