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
@@ -166,6 +166,12 @@ function saveBuffer(buffer) {
166
166
  return { attachmentRef, sizeBytes: buffer.length };
167
167
  }
168
168
 
169
+ /** Store `buffer` (a file a bot or agent hands on, session-runtime/
170
+ * handoff-attachments.mjs) under its content address: `{ attachmentRef, sizeBytes }`. */
171
+ export function storeAttachmentBuffer(buffer) {
172
+ return saveBuffer(buffer);
173
+ }
174
+
169
175
  export function isAttachmentReference(value) {
170
176
  return Boolean(value && typeof value === 'object' && ATTACHMENT_REF_RE.test(String(value.attachmentRef || '')));
171
177
  }
@@ -267,7 +267,7 @@ export function effortJudgeInfo(dir = effortJudgeModelDir()) {
267
267
  /* not installed */
268
268
  }
269
269
  // The installed model names its backbone in calibration.json; older packs predate the field.
270
- let backbone = 'mmBERT-small';
270
+ let backbone = 'EmbeddingGemma 2 (18 layers)';
271
271
  try {
272
272
  backbone = JSON.parse(readFileSync(join(dir, 'calibration.json'), 'utf8')).backbone || backbone;
273
273
  } catch {
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release": "effort-judge-v6",
3
+ "release": "effort-judge-v7",
4
4
  "files": {
5
5
  "model.onnx": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v6/model.onnx",
7
- "sha256": "5abed811aaa44690f8d6292472aa8d930abae49f73a99265bb7fe874044df395",
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v7/model.onnx",
7
+ "sha256": "d01ceb1685e123335d2b70809e7b53fcc07ffb110b9412b408bc6b09a5394288",
8
8
  "size": 153062716
9
9
  },
10
10
  "tokenizer.json": {
11
- "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v6/tokenizer.json",
11
+ "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v7/tokenizer.json",
12
12
  "sha256": "22c064aaa468855d6211fe5c2ec8fdec595c97c55eb2ffd3b9fdf71a93b16292",
13
13
  "size": 32170665
14
14
  },
15
15
  "calibration.json": {
16
- "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v6/calibration.json",
17
- "sha256": "cc52c28b19fa891cca8913c69871235d5f863bd224d9336ada7d0f8cfb0765f6",
18
- "size": 157
16
+ "url": "https://github.com/tribgames/mixdog/releases/download/effort-judge-v7/calibration.json",
17
+ "sha256": "01fe4cc99972bcca15e6ce8384cabf83c5705117e33da9a87541dc0f3d60a035",
18
+ "size": 223
19
19
  }
20
20
  }
21
21
  }
@@ -0,0 +1,152 @@
1
+ // An agent definition's kind (isContactAgentId reads it), decided read-only —
2
+ // the one rule for the runtime and the desktop (which reads `kind` from the
3
+ // runtime's catalog rows). Nothing is ever stamped on disk.
4
+ //
5
+ // An explicit frontmatter `kind` wins (`bot` from the bot dialog and Mixy's
6
+ // setup tool, `agent` from the Workflows page and other chats). v1.3.0 wrote
7
+ // no marker for bots, and v1.2.6 agents have none either, so an unmarked
8
+ // user-made agent is a bot only on evidence that only a v1.3.0 bot creation
9
+ // produced:
10
+ // T1 its home thread holds the creation greeting (agentGreetingKickoff,
11
+ // sent only to bots a dialog or the host greeted on creation; agents
12
+ // older than 1.3 were recorded without one);
13
+ // T2 its agent.json has an `avatar` (the bot dialog always sent one) AND
14
+ // its AGENT.md body is the bot dialog's default (agentBodyFromBrief).
15
+ // Anything else is a workflow agent, as in v1.2.6.
16
+ import { existsSync, readFileSync, statSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { agentHomeSessionId } from '../agent/orchestrator/session/agent-home.mjs';
19
+ import { readMarkdownDocument } from './markdown-frontmatter.mjs';
20
+ import { BOT_AGENT_KIND } from './agent-route-config.mjs';
21
+ import { botStoreActive, isStoreBot } from './agent-store.mjs';
22
+
23
+ const RUNTIME_MARKER = '[mixdog-runtime]';
24
+ /** The creation greeting's opening (shared/agent-greeting.ts, v1.3.0 on). */
25
+ const CREATION_GREETING = /(^|\n)\[mixdog-runtime\] The user just created you as /;
26
+ /** The closing lines of the bot dialog's default AGENT.md (agentBodyFromBrief). */
27
+ const DIALOG_BODY_TAIL = [
28
+ /^- Help the user( with this role)? in a natural, friendly conversation, in the language they write in\.$/,
29
+ /^- Ask a short question when a request is unclear, and say plainly when something is outside what you do\.$/,
30
+ /^- Keep replies as short or as detailed as the moment needs\.$/,
31
+ ];
32
+
33
+ function messageText(content) {
34
+ if (typeof content === 'string') return content;
35
+ if (!Array.isArray(content)) return '';
36
+ return content
37
+ .map((part) => (typeof part === 'string' ? part : typeof part?.text === 'string' ? part.text : ''))
38
+ .join('\n');
39
+ }
40
+
41
+ const NO_HOME = Object.freeze({ exists: false, greeted: false, chatted: false });
42
+ // Per session file, keyed by its size and mtime: a home thread is read once
43
+ // until it changes.
44
+ const homeEvidenceCache = new Map();
45
+
46
+ /** What `agentId`'s home thread (sessions/sess_home_<id>.json) holds:
47
+ * whether it exists, whether it has the creation greeting, and whether the
48
+ * user and the agent talked (any user message that is not a runtime turn,
49
+ * or any assistant message). A missing or unreadable file holds nothing. */
50
+ export function homeThreadEvidence(dataDir, agentId) {
51
+ const sessionId = agentHomeSessionId(agentId);
52
+ if (!dataDir || !sessionId) return NO_HOME;
53
+ const file = join(dataDir, 'sessions', `${sessionId}.json`);
54
+ let stat;
55
+ try {
56
+ stat = statSync(file);
57
+ } catch {
58
+ return NO_HOME;
59
+ }
60
+ const cached = homeEvidenceCache.get(file);
61
+ if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.value;
62
+ let messages = [];
63
+ try {
64
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
65
+ if (Array.isArray(doc?.messages)) messages = doc.messages;
66
+ } catch {
67
+ // unreadable: it proves nothing
68
+ }
69
+ let greeted = false;
70
+ let chatted = false;
71
+ for (const message of messages) {
72
+ if (message?.role === 'assistant') chatted = true;
73
+ if (message?.role !== 'user') continue;
74
+ const text = messageText(message.content);
75
+ if (CREATION_GREETING.test(text)) greeted = true;
76
+ else if (text.trim() && !text.trimStart().startsWith(RUNTIME_MARKER)) chatted = true;
77
+ }
78
+ const value = Object.freeze({ exists: true, greeted, chatted });
79
+ homeEvidenceCache.set(file, { size: stat.size, mtimeMs: stat.mtimeMs, value });
80
+ return value;
81
+ }
82
+
83
+ function isDialogDefaultBody(body) {
84
+ const lines = String(body || '')
85
+ .trim()
86
+ .split(/\r?\n/);
87
+ const tail = lines.slice(-DIALOG_BODY_TAIL.length);
88
+ return tail.length === DIALOG_BODY_TAIL.length && DIALOG_BODY_TAIL.every((line, index) => line.test(tail[index]));
89
+ }
90
+
91
+ /** The definition's kind: the explicit frontmatter kind (lower-cased); else,
92
+ * for an unmarked user-made agent (`dataDir`/`id` given, not a shipped role),
93
+ * `bot` on T1 or T2 evidence; else null (a workflow agent). `manifest` and
94
+ * `body` are the user copy's agent.json and AGENT.md body. */
95
+ export function agentDefinitionKind(definition) {
96
+ const { dataDir, id } = definition;
97
+ // Once bots have their own store the folder decides: bots/ holds bots,
98
+ // agents/ workflow agents (an explicit marker still read as written).
99
+ if (dataDir && id && isStoreBot(dataDir, id)) return BOT_AGENT_KIND;
100
+ if (dataDir && botStoreActive(dataDir)) return explicitKind(definition.frontmatterKind);
101
+ return evidenceKind(definition);
102
+ }
103
+
104
+ const explicitKind = (frontmatterKind) =>
105
+ (typeof frontmatterKind === 'string' ? frontmatterKind.trim().toLowerCase() : '') || null;
106
+
107
+ /** The pre-store rule alone (the split's decision input): the explicit kind,
108
+ * else `bot` on T1/T2 evidence for an unmarked user-made agent, else null. */
109
+ export function evidenceKind({ frontmatterKind, dataDir, shippedRoot, id, manifest, body }) {
110
+ const explicit = explicitKind(frontmatterKind);
111
+ if (explicit) return explicit;
112
+ if (!dataDir || !id || id === 'lead') return null;
113
+ if (shippedRoot && existsSync(join(shippedRoot, 'agents', id, 'AGENT.md'))) return null;
114
+ if (manifest?.avatar && isDialogDefaultBody(body)) return BOT_AGENT_KIND;
115
+ return homeThreadEvidence(dataDir, id).greeted ? BOT_AGENT_KIND : null;
116
+ }
117
+
118
+ /** agentDefinitionKind from the user copy on disk, for readers that have no
119
+ * loaded definition at hand: a store bot (bots/<id>/) is a bot; any other id
120
+ * is read from agents/<id>/, else from the shipped role. */
121
+ export function readAgentDefinitionKind({ dataDir, shippedRoot, id }) {
122
+ if (isStoreBot(dataDir, id)) return BOT_AGENT_KIND;
123
+ const dir = join(dataDir, 'agents', id);
124
+ let doc;
125
+ try {
126
+ doc = readMarkdownDocument(readFileSync(join(dir, 'AGENT.md'), 'utf8'));
127
+ } catch {
128
+ // No user copy: the shipped role's own frontmatter decides.
129
+ try {
130
+ return agentDefinitionKind({
131
+ frontmatterKind: readMarkdownDocument(readFileSync(join(shippedRoot, 'agents', id, 'AGENT.md'), 'utf8'))
132
+ .frontmatter.kind,
133
+ });
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ let manifest = {};
139
+ try {
140
+ manifest = JSON.parse(readFileSync(join(dir, 'agent.json'), 'utf8')) || {};
141
+ } catch {
142
+ manifest = {};
143
+ }
144
+ return agentDefinitionKind({
145
+ frontmatterKind: doc.frontmatter.kind,
146
+ dataDir,
147
+ shippedRoot,
148
+ id,
149
+ manifest,
150
+ body: doc.body,
151
+ });
152
+ }
@@ -1,3 +1,4 @@
1
1
  // Types for the desktop's one import of agent-route-config.mjs.
2
2
  export const WORKFLOW_AGENT_KIND: 'agent';
3
+ export const BOT_AGENT_KIND: 'bot';
3
4
  export function isContactAgentId(value: unknown, kind?: unknown): boolean;
@@ -20,16 +20,20 @@ export const DEFAULT_DISABLED_AGENT_ROSTER = Object.freeze([...DEFAULT_DISABLED_
20
20
  * an agent: a workflow delegation role, never a contact (봇). */
21
21
  export const WORKFLOW_AGENT_KIND = 'agent';
22
22
 
23
- /** A contact agent (a 봇): one the user made — not Mixy ('lead'), not a
24
- * shipped role (the starter sub-agents Worker, Reviewer, … and the
25
- * Maintainer), and not marked `kind: agent` (made on the Workflows page).
26
- * `kind` is the definition's frontmatter `kind`; an unmarked custom agent
27
- * stays a contact. The one rule for the runtime (delegation, agent catalogs)
28
- * and the desktop (contacts, agent lists). */
23
+ /** The AGENT.md frontmatter `kind` every bot creation path writes: the bot
24
+ * dialog, and the setup tool when Mixy (agent mode) creates an agent. */
25
+ export const BOT_AGENT_KIND = 'bot';
26
+
27
+ /** A contact agent (a 봇): one explicitly marked `kind: bot` at creation —
28
+ * never Mixy ('lead') nor a shipped role (the starter sub-agents Worker,
29
+ * Reviewer, … and the Maintainer). `kind` is the definition's frontmatter
30
+ * `kind`; an unmarked custom agent (every agent made before bots existed)
31
+ * stays a workflow agent. The one rule for the runtime (delegation, agent
32
+ * catalogs) and the desktop (contacts, agent lists). */
29
33
  export function isContactAgentId(value, kind) {
30
34
  const id = typeof value === 'string' ? value.trim().toLowerCase() : '';
31
- const marked = typeof kind === 'string' && kind.trim().toLowerCase() === WORKFLOW_AGENT_KIND;
32
- return Boolean(id) && id !== 'lead' && !DEFAULT_DISABLED_AGENT_ROSTER.includes(id) && !marked;
35
+ const bot = typeof kind === 'string' && kind.trim().toLowerCase() === BOT_AGENT_KIND;
36
+ return Boolean(id) && id !== 'lead' && !DEFAULT_DISABLED_AGENT_ROSTER.includes(id) && bot;
33
37
  }
34
38
 
35
39
  function record(value) {
@@ -128,9 +132,19 @@ export function withAgentDisabled(config, agentId, disabled) {
128
132
  // left under retired slots (agents.maintenance, workflowRoutes.*,
129
133
  // maintenance.memory, generated workflow-* presets) are scrubbed by
130
134
  // canonicalizeAgentRouteStorage, never folded in.
135
+ // A bot's route lives with the bot (agent-store.mjs registers the reader in
136
+ // the runtime): the resolver answers a route, null (a bot without one), or
137
+ // undefined (no bot — the config decides).
138
+ let botRouteResolver = null;
139
+ export function setBotRouteResolver(resolver) {
140
+ botRouteResolver = typeof resolver === 'function' ? resolver : null;
141
+ }
142
+
131
143
  export function configuredAgentRouteCandidates(config, agentId) {
132
144
  const id = String(agentId || '').trim();
133
145
  if (!id) return [];
146
+ const bot = botRouteResolver?.(id);
147
+ if (bot !== undefined) return bot ? [bot] : [];
134
148
  const route = record(config?.agents)[id];
135
149
  return route ? [route] : [];
136
150
  }
@@ -0,0 +1,132 @@
1
+ // Where a user-made definition lives once bots have their own store (1.3.1):
2
+ // bots under <dataDir>/bots/<id>/ (with their own model route in agent.json),
3
+ // workflow agents under <dataDir>/agents/<id>/. The store is active once the
4
+ // one-time split finished (bots/migration.json, bot-split-migration.mjs);
5
+ // before that — or if it never could run — every reader sees agents/ as
6
+ // before. The ids the split copied are bots for good: their agents/ originals
7
+ // stay on disk for rollback but no catalog reads them again.
8
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { setBotRouteResolver } from './agent-route-config.mjs';
11
+
12
+ export const BOTS_DIR = 'bots';
13
+ export const BOT_MIGRATION_MANIFEST = 'migration.json';
14
+ export const BOT_MIGRATION_VERSION = 1;
15
+ /** Mixy's bot id once it has its own folder. */
16
+ export const MIXY_BOT_ID = 'mixy';
17
+
18
+ function readJson(file) {
19
+ try {
20
+ const value = JSON.parse(readFileSync(file, 'utf8'));
21
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ // Small stat-keyed caches: the store is read on route and catalog paths.
28
+ const fileCache = new Map();
29
+ function cachedJson(file) {
30
+ let stat;
31
+ try {
32
+ stat = statSync(file);
33
+ } catch {
34
+ fileCache.delete(file);
35
+ return null;
36
+ }
37
+ const hit = fileCache.get(file);
38
+ if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) return hit.value;
39
+ const value = readJson(file);
40
+ fileCache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, value });
41
+ return value;
42
+ }
43
+
44
+ /** The finished split's record, or null before it ran. */
45
+ export function readBotMigration(dataDir) {
46
+ if (!dataDir) return null;
47
+ const manifest = cachedJson(join(dataDir, BOTS_DIR, BOT_MIGRATION_MANIFEST));
48
+ return manifest?.version === BOT_MIGRATION_VERSION ? manifest : null;
49
+ }
50
+
51
+ /** Whether `dataDir` has its bot store (the split finished). */
52
+ export function botStoreActive(dataDir) {
53
+ return readBotMigration(dataDir) !== null;
54
+ }
55
+
56
+ /** The agents/ ids the split copied to bots/: the agent catalog skips them. */
57
+ export function migratedBotIds(dataDir) {
58
+ return new Set(readBotMigration(dataDir)?.bots || []);
59
+ }
60
+
61
+ export const botDir = (dataDir, id) => join(dataDir, BOTS_DIR, id);
62
+
63
+ /** Whether `id` is a store bot: the store is active and the bot has its
64
+ * folder, or the split copied it (an id it moved is never an agent again).
65
+ * Mixy's own folder is no ordinary bot. */
66
+ export function isStoreBot(dataDir, id) {
67
+ if (!dataDir || !id || id === MIXY_BOT_ID || id.startsWith('.')) return false;
68
+ const manifest = readBotMigration(dataDir);
69
+ if (!manifest) return false;
70
+ return existsSync(botDir(dataDir, id)) || (manifest.bots || []).includes(id);
71
+ }
72
+
73
+ /** The folder of a user-made definition: bots/<id> for a store bot, else agents/<id>. */
74
+ export function userDefinitionDir(dataDir, id) {
75
+ return isStoreBot(dataDir, id) ? botDir(dataDir, id) : join(dataDir, 'agents', id);
76
+ }
77
+
78
+ const isCompleteRoute = (route) =>
79
+ Boolean(
80
+ route && typeof route === 'object' && String(route.provider || '').trim() && String(route.model || '').trim()
81
+ );
82
+
83
+ /** A store bot's own route (agent.json `route`): the route, null when it has
84
+ * none, undefined when `id` is no store bot. */
85
+ export function readBotRoute(dataDir, id) {
86
+ if (!isStoreBot(dataDir, id)) return undefined;
87
+ const route = cachedJson(join(botDir(dataDir, id), 'agent.json'))?.route;
88
+ return isCompleteRoute(route) ? route : null;
89
+ }
90
+
91
+ /** Stores (or with null removes) a store bot's route in its agent.json; every
92
+ * other key is kept. */
93
+ export function writeBotRoute(dataDir, id, route) {
94
+ const dir = botDir(dataDir, id);
95
+ const file = join(dir, 'agent.json');
96
+ const manifest = readJson(file) || {};
97
+ if (route) manifest.route = route;
98
+ else delete manifest.route;
99
+ mkdirSync(dir, { recursive: true });
100
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
101
+ fileCache.delete(file);
102
+ }
103
+
104
+ // --- Mixy: its own bot (bots/mixy), apart from the Lead ---
105
+ // Mixy's identity (name, face, persona, 일하는 방식, connectors, `created`)
106
+ // lives in bots/mixy once the store is active, in agents/lead before. Its
107
+ // model route is always its own (bots/mixy/agent.json `route`), never the
108
+ // Lead's: changing one never changes the other. Its home thread keeps the id
109
+ // sess_home_lead, and its runtime role id stays `lead`.
110
+
111
+ /** The folder Mixy's identity is read from and written to. */
112
+ export function mixyIdentityDir(dataDir) {
113
+ return botStoreActive(dataDir) ? botDir(dataDir, MIXY_BOT_ID) : join(dataDir, 'agents', 'lead');
114
+ }
115
+
116
+ /** Mixy's own route, or null when it has none (its thread keeps its own). */
117
+ export function readMixyRoute(dataDir) {
118
+ if (!dataDir) return null;
119
+ const route = cachedJson(join(botDir(dataDir, MIXY_BOT_ID), 'agent.json'))?.route;
120
+ return isCompleteRoute(route) ? route : null;
121
+ }
122
+
123
+ /** Stores (or with null removes) Mixy's own route; nothing of the Lead's. */
124
+ export function writeMixyRoute(dataDir, route) {
125
+ writeBotRoute(dataDir, MIXY_BOT_ID, route);
126
+ }
127
+
128
+ /** Every config route read (configuredAgentRouteCandidates) asks the bot
129
+ * store first: a bot's route lives with the bot, never in the config. */
130
+ export function registerBotRouteStore(getDataDir) {
131
+ setBotRouteResolver((id) => readBotRoute(getDataDir(), id));
132
+ }