mixdog 0.9.19 → 0.9.21

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -34
  3. package/package.json +3 -2
  4. package/scripts/build-tui.mjs +6 -0
  5. package/scripts/hook-bus-test.mjs +8 -0
  6. package/scripts/log-writer-guard-smoke.mjs +131 -0
  7. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  8. package/scripts/recall-bench-cases.json +0 -1
  9. package/scripts/recall-quality-cases.json +1 -2
  10. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  11. package/scripts/tool-smoke.mjs +150 -45
  12. package/src/defaults/skills/setup/SKILL.md +327 -0
  13. package/src/help.mjs +2 -5
  14. package/src/lib/mixdog-debug.cjs +13 -0
  15. package/src/mixdog-session-runtime.mjs +7 -3328
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  17. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  19. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  20. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  23. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  24. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  26. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  27. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  28. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  29. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  30. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  31. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  32. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  34. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  35. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  36. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  37. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  42. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  44. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  45. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  46. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  47. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  49. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  52. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  53. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  55. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  56. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  57. package/src/runtime/channels/index.mjs +6 -2183
  58. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  59. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  60. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  61. package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
  62. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  63. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  64. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  65. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  66. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  67. package/src/runtime/channels/lib/worker-main.mjs +777 -0
  68. package/src/runtime/memory/index.mjs +163 -1725
  69. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  70. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  71. package/src/runtime/memory/lib/http-router.mjs +811 -0
  72. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  73. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  74. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  75. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  76. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  77. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  78. package/src/runtime/memory/lib/memory.mjs +39 -0
  79. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  80. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  81. package/src/runtime/shared/atomic-file.mjs +138 -80
  82. package/src/runtime/shared/child-guardian.mjs +61 -3
  83. package/src/session-runtime/boot-profile.mjs +36 -0
  84. package/src/session-runtime/channel-config-api.mjs +70 -0
  85. package/src/session-runtime/context-status.mjs +181 -0
  86. package/src/session-runtime/env.mjs +17 -0
  87. package/src/session-runtime/lifecycle-api.mjs +242 -0
  88. package/src/session-runtime/model-route-api.mjs +198 -0
  89. package/src/session-runtime/provider-auth-api.mjs +135 -0
  90. package/src/session-runtime/resource-api.mjs +282 -0
  91. package/src/session-runtime/runtime-core.mjs +2104 -0
  92. package/src/session-runtime/session-turn-api.mjs +274 -0
  93. package/src/session-runtime/tool-catalog.mjs +18 -264
  94. package/src/session-runtime/tool-defs.mjs +2 -2
  95. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  96. package/src/standalone/agent-tool.mjs +2 -2
  97. package/src/standalone/channel-worker.mjs +67 -7
  98. package/src/standalone/folder-dialog.mjs +4 -1
  99. package/src/standalone/memory-runtime-proxy.mjs +154 -17
  100. package/src/standalone/seeds.mjs +28 -1
  101. package/src/tui/App.jsx +40 -28
  102. package/src/tui/app/core-memory-picker.mjs +1 -1
  103. package/src/tui/app/doctor.mjs +175 -0
  104. package/src/tui/app/slash-commands.mjs +1 -0
  105. package/src/tui/app/slash-dispatch.mjs +9 -0
  106. package/src/tui/app/use-mouse-input.mjs +6 -0
  107. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  108. package/src/tui/dist/index.mjs +2851 -2162
  109. package/src/tui/engine/context-state.mjs +145 -0
  110. package/src/tui/engine/prompt-history.mjs +27 -0
  111. package/src/tui/engine/session-api-ext.mjs +478 -0
  112. package/src/tui/engine/session-api.mjs +564 -0
  113. package/src/tui/engine/session-flow.mjs +485 -0
  114. package/src/tui/engine/turn.mjs +1078 -0
  115. package/src/tui/engine.mjs +68 -2620
  116. package/src/tui/index.jsx +7 -0
  117. package/vendor/ink/build/ink.js +16 -1
  118. package/vendor/ink/build/output.js +30 -4
  119. package/vendor/ink/build/render.js +5 -0
  120. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,268 @@
1
+ // manager/prefetch-bridge.mjs
2
+ // Explicit-prefetch bridge extracted verbatim from manager.mjs. Runs Lead-
3
+ // supplied files[]/callers[]/references[] prefetch (outside the agent loop),
4
+ // with a pass-through permission guard for API compatibility.
5
+ import { createRequire } from 'module';
6
+ import { fileURLToPath } from 'url';
7
+ import { isAgentOwner } from '../../agent-owner.mjs';
8
+ import { executeInternalTool } from '../../internal-tools.mjs';
9
+ import { classifyResultKind } from '../result-classification.mjs';
10
+ import { tryPrefetchCached, setPrefetchCached } from '../read-dedup.mjs';
11
+ import { _executeCodeGraphToolLazy } from './runtime-loaders.mjs';
12
+
13
+ // ── Prefetch permission guard ─────────────────────────────────────────────────
14
+ // Runs the shared permission evaluator for tool calls that originate in the
15
+ // prefetch path (outside the agent loop). Permission enforcement is disabled
16
+ // (the evaluator always returns allow), so this is effectively a pass-through
17
+ // kept for API compatibility. Returns an error string if blocked, or null.
18
+ const _permEvalForPrefetch = (() => {
19
+ const _req = createRequire(import.meta.url);
20
+ try {
21
+ const { dirname: _pdir, resolve: _pres } = _req('path');
22
+ const _hooksLib = _pres(_pdir(fileURLToPath(import.meta.url)), '../../../../../hooks/lib/permission-evaluator.cjs');
23
+ return _req(_hooksLib).evaluatePermission;
24
+ } catch { return null; }
25
+ })();
26
+ function _guardedPrefetchTool(toolName, toolArgs, session) {
27
+ if (!_permEvalForPrefetch) return null;
28
+ // When no explicit mode is attached to the session, run the evaluator
29
+ // under 'default'. The evaluator now always allows, so this never blocks.
30
+ const permissionMode = session?.permissionMode || 'default';
31
+ const projectDir = session?.cwd || undefined;
32
+ const userCwd = session?.cwd || undefined;
33
+ const MCP_PFX = 'mcp__plugin_mixdog_mixdog__';
34
+ const fullName = toolName.startsWith(MCP_PFX) || toolName.startsWith('mcp__') ? toolName : `${MCP_PFX}${toolName}`;
35
+ try {
36
+ const { decision, reason } = _permEvalForPrefetch({ toolName: fullName, toolInput: toolArgs || {}, permissionMode, projectDir, userCwd });
37
+ if (decision === 'deny' || decision === 'ask') {
38
+ return `Error: prefetch tool "${toolName}" blocked (decision=${decision}): ${reason}`;
39
+ }
40
+ } catch (e) {
41
+ process.stderr.write(`[prefetch-guard] evaluator error: ${e?.message}\n`);
42
+ }
43
+ return null;
44
+ }
45
+
46
+ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
47
+ if (!explicitPrefetch || typeof explicitPrefetch !== 'object') return null;
48
+ if (!isAgentOwner(session)) return null;
49
+ const parts = [];
50
+ const failed = [];
51
+ const totalEntries = [];
52
+ // files[] — string entries use the default head excerpt; object entries
53
+ // {path, n?, full?} let the caller widen the window or pull the full file
54
+ // so worker doesn't have to re-read deep ranges of an already-prefetched
55
+ // file (a recurring iter burner observed in baseline session telemetry).
56
+ const _rawFilesIn = Array.isArray(explicitPrefetch.files) ? explicitPrefetch.files : [];
57
+ const _readOptsByFile = new Map();
58
+ const files = [];
59
+ const _seenFiles = new Set();
60
+ const _addPrefetchFile = (file, opts = null) => {
61
+ if (typeof file !== 'string' || !file) return;
62
+ if (!_seenFiles.has(file)) {
63
+ _seenFiles.add(file);
64
+ files.push(file);
65
+ }
66
+ if (!opts || Object.keys(opts).length === 0) return;
67
+ const prev = _readOptsByFile.get(file) || {};
68
+ const merged = { ...prev };
69
+ if (opts.mode === 'full') {
70
+ merged.mode = 'full';
71
+ delete merged.n;
72
+ } else if (merged.mode !== 'full' && Number.isFinite(opts.n) && opts.n > 0) {
73
+ merged.n = Math.max(Number(merged.n) || 0, opts.n);
74
+ }
75
+ if (Object.keys(merged).length > 0) _readOptsByFile.set(file, merged);
76
+ };
77
+ for (const entry of _rawFilesIn) {
78
+ if (typeof entry === 'string' && entry) {
79
+ _addPrefetchFile(entry);
80
+ } else if (entry && typeof entry === 'object' && typeof entry.path === 'string' && entry.path) {
81
+ const opts = {};
82
+ if (entry.full === true) opts.mode = 'full';
83
+ else if (Number.isFinite(entry.n) && entry.n > 0) opts.n = entry.n;
84
+ _addPrefetchFile(entry.path, opts);
85
+ }
86
+ }
87
+ if (files.length > 0) {
88
+ const _pfGuard = _guardedPrefetchTool('read', { path: files }, session);
89
+ if (_pfGuard) {
90
+ process.stderr.write(`[agent-prefetch] files read blocked: ${_pfGuard}\n`);
91
+ failed.push(...files);
92
+ totalEntries.push(...files);
93
+ } else {
94
+ totalEntries.push(...files);
95
+ // R20: per-file prefetch cache (cross-dispatch, process-local).
96
+ // Try each file from cache first; batch misses into one disk read.
97
+ const { resolve: _pfResolve, isAbsolute: _pfIsAbs, normalize: _pfNorm } = await import('path');
98
+ const _pfCwd = session.cwd || null;
99
+ function _pfAbsPath(f) {
100
+ const abs = _pfIsAbs(f) ? f : _pfResolve(_pfCwd || process.cwd(), f);
101
+ return _pfNorm(abs);
102
+ }
103
+ const fileHits = []; // { file, abs, content } — satisfied from cache
104
+ const fileMisses = []; // { file, abs } — need disk read
105
+ for (const f of files) {
106
+ const abs = _pfAbsPath(f);
107
+ // Skip the cross-dispatch cache when the caller asked for a
108
+ // non-default window (custom n or full-file). Cache key is the
109
+ // path alone, so a default-window cache hit would silently feed
110
+ // the wrong slice back to the next caller.
111
+ const hit = _readOptsByFile.has(f) ? null : tryPrefetchCached(abs);
112
+ if (hit) {
113
+ fileHits.push({ file: f, abs, content: hit.content });
114
+ } else {
115
+ fileMisses.push({ file: f, abs });
116
+ }
117
+ }
118
+ // Disk read for misses (single batch call).
119
+ const missFiles = fileMisses.map(m => m.file);
120
+ const missResults = {}; // file → content string
121
+ if (missFiles.length > 0) {
122
+ // Read each miss file individually so we can cache per-file.
123
+ // The files list is small (typically 2-5), so N awaits is fine.
124
+ await Promise.all(missFiles.map(async (f) => {
125
+ const opts = _readOptsByFile.get(f) || {};
126
+ const readArgs = { path: f };
127
+ if (opts.mode === 'full') {
128
+ readArgs.mode = 'full';
129
+ } else {
130
+ readArgs.mode = 'head';
131
+ readArgs.n = Number.isFinite(opts.n) ? opts.n : 120;
132
+ }
133
+ const out = await executeInternalTool('read', readArgs).catch((e) => {
134
+ process.stderr.write(`[agent-prefetch] file read failed (${f}): ${e && e.message || e}\n`);
135
+ return null;
136
+ });
137
+ if (out !== null) {
138
+ missResults[f] = String(out);
139
+ }
140
+ }));
141
+ // Cache successful miss results.
142
+ for (const { file, abs } of fileMisses) {
143
+ const content = missResults[file];
144
+ if (content && classifyResultKind(content) !== 'error') {
145
+ // Only cache default-window reads; custom-window results
146
+ // would poison the shared cross-dispatch cache.
147
+ if (!_readOptsByFile.has(file)) setPrefetchCached(abs, content);
148
+ } else if (content === undefined || classifyResultKind(content) === 'error') {
149
+ failed.push(file);
150
+ }
151
+ }
152
+ }
153
+ // Assemble combined output preserving original file order.
154
+ const readParts = [];
155
+ const hitByFile = new Map(fileHits.map((h) => [h.file, h]));
156
+ for (const f of files) {
157
+ const hitEntry = hitByFile.get(f);
158
+ if (hitEntry) {
159
+ readParts.push(hitEntry.content);
160
+ continue;
161
+ }
162
+ const content = missResults[f];
163
+ if (content && classifyResultKind(content) !== 'error') {
164
+ readParts.push(content);
165
+ }
166
+ // else: already pushed to failed above
167
+ }
168
+ if (readParts.length > 0) {
169
+ parts.push(`### prefetch files\nread ${readParts.length}\n\n${readParts.join('\n\n')}`);
170
+ }
171
+ // Log hit/miss counters so dispatch telemetry shows prefetch effectiveness.
172
+ if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
173
+ process.stderr.write(
174
+ `[prefetch] files=${files.length} cached=${fileHits.length} miss=${fileMisses.length} failed=${failed.length}\n`
175
+ );
176
+ }
177
+ // Attach stats to session so post-hoc analyzers (inspect-session.mjs)
178
+ // can see prefetch effectiveness without parsing stderr logs.
179
+ if (session && typeof session === 'object') {
180
+ if (!session.prefetchStats) session.prefetchStats = { files: 0, cached: 0, miss: 0, failed: 0 };
181
+ session.prefetchStats.files += files.length;
182
+ session.prefetchStats.cached += fileHits.length;
183
+ session.prefetchStats.miss += fileMisses.length;
184
+ session.prefetchStats.failed += failed.length;
185
+ }
186
+ }
187
+ }
188
+ // callers[]
189
+ const callers = Array.isArray(explicitPrefetch.callers) ? explicitPrefetch.callers.filter(c => c && typeof c.symbol === 'string') : [];
190
+ {
191
+ const callerTasks = callers.map(({ symbol, file }) => {
192
+ const cgArgs = { mode: 'callers', symbol };
193
+ if (file) cgArgs.file = file;
194
+ if (session?.cwd) cgArgs.cwd = session.cwd;
195
+ totalEntries.push(symbol);
196
+ const blocked = _guardedPrefetchTool('code_graph', cgArgs, session);
197
+ if (blocked) {
198
+ process.stderr.write(`[agent-prefetch] callers(${symbol}) blocked: ${blocked}\n`);
199
+ return Promise.resolve({ symbol, out: null, blocked: true });
200
+ }
201
+ return _executeCodeGraphToolLazy('code_graph', cgArgs, session?.cwd)
202
+ .then(out => ({ symbol, out }))
203
+ .catch(e => {
204
+ process.stderr.write(`[agent-prefetch] callers(${symbol}) failed: ${e && e.message || e}\n`);
205
+ return { symbol, out: null };
206
+ });
207
+ });
208
+ const callerResults = await Promise.allSettled(callerTasks);
209
+ for (const r of callerResults) {
210
+ const { symbol, out, blocked } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
211
+ if (blocked) { failed.push(symbol); continue; }
212
+ if (out && classifyResultKind(String(out)) !== 'error') {
213
+ parts.push(`### prefetch callers ${symbol}\n${out}`);
214
+ } else {
215
+ failed.push(symbol);
216
+ }
217
+ }
218
+ }
219
+ // references[]
220
+ const references = Array.isArray(explicitPrefetch.references) ? explicitPrefetch.references.filter(r => r && typeof r.symbol === 'string') : [];
221
+ {
222
+ const refTasks = references.map(({ symbol, file }) => {
223
+ const cgArgs = { mode: 'references', symbol };
224
+ if (file) cgArgs.file = file;
225
+ if (session?.cwd) cgArgs.cwd = session.cwd;
226
+ totalEntries.push(symbol);
227
+ const blocked = _guardedPrefetchTool('code_graph', cgArgs, session);
228
+ if (blocked) {
229
+ process.stderr.write(`[agent-prefetch] references(${symbol}) blocked: ${blocked}\n`);
230
+ return Promise.resolve({ symbol, out: null, blocked: true });
231
+ }
232
+ return _executeCodeGraphToolLazy('code_graph', cgArgs, session?.cwd)
233
+ .then(out => ({ symbol, out }))
234
+ .catch(e => {
235
+ process.stderr.write(`[agent-prefetch] references(${symbol}) failed: ${e && e.message || e}\n`);
236
+ return { symbol, out: null };
237
+ });
238
+ });
239
+ const refResults = await Promise.allSettled(refTasks);
240
+ for (const r of refResults) {
241
+ const { symbol, out, blocked } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
242
+ if (blocked) { failed.push(symbol); continue; }
243
+ if (out && classifyResultKind(String(out)) !== 'error') {
244
+ parts.push(`### prefetch references ${symbol}\n${out}`);
245
+ } else {
246
+ failed.push(symbol);
247
+ }
248
+ }
249
+ }
250
+ if (session && typeof session === 'object' && (callers.length > 0 || references.length > 0)) {
251
+ if (!session.prefetchStats) session.prefetchStats = { files: 0, cached: 0, miss: 0, failed: 0, callers: 0, references: 0 };
252
+ session.prefetchStats.callers = (session.prefetchStats.callers || 0) + callers.length;
253
+ session.prefetchStats.references = (session.prefetchStats.references || 0) + references.length;
254
+ }
255
+ if (parts.length === 0) {
256
+ // All entries failed but Lead presence must still be signalled — emit
257
+ // warn-only so the gate logic can distinguish "prefetch was requested"
258
+ // from "no prefetch at all".
259
+ if (totalEntries.length > 0 && failed.length > 0) {
260
+ return `<prefetch-warn>${failed.length} of ${totalEntries.length} prefetch entries failed: ${[...new Set(failed)].join(', ')}</prefetch-warn>`;
261
+ }
262
+ return null;
263
+ }
264
+ const warnLine = failed.length > 0
265
+ ? `<prefetch-warn>${failed.length} of ${totalEntries.length} prefetch entries failed: ${[...new Set(failed)].join(', ')}</prefetch-warn>\n`
266
+ : '';
267
+ return `${warnLine}<prefetch>\n${parts.join('\n\n')}\n</prefetch>`;
268
+ }
@@ -0,0 +1,22 @@
1
+ // manager/provider-cache-key.mjs
2
+ // Provider-scoped unified cache key extracted verbatim from manager.mjs. Goal:
3
+ // all orchestrator-internal dispatches (agent/maintenance/mcp/scheduler/webhook)
4
+ // targeting the same provider land in a single server-side cache shard, so the
5
+ // shared prefix (tools + system + pool system prompt) is reused regardless of
6
+ // role. Per-role / per-session differentiation lives after the system prefix
7
+ // (BP3 sessionMarker system block / later messages), which is naturally
8
+ // separated by provider-side content hashing.
9
+ const PROVIDER_ALIAS = {
10
+ 'openai-oauth': 'codex', // ChatGPT subscription (OpenAI OAuth backend)
11
+ 'anthropic-oauth': 'claude', // Claude Max subscription
12
+ 'openai': 'openai',
13
+ 'anthropic': 'anthropic',
14
+ 'gemini': 'gemini',
15
+ 'deepseek': 'deepseek',
16
+ 'xai': 'xai',
17
+ };
18
+ export function providerCacheKey(provider, override) {
19
+ if (override) return String(override);
20
+ if (!provider) return 'mixdog-default';
21
+ return `mixdog-${PROVIDER_ALIAS[provider] || provider}`;
22
+ }
@@ -0,0 +1,26 @@
1
+ // manager/runtime-loaders.mjs
2
+ // Lazy runtime import bridges extracted from manager.mjs. Dynamic import()
3
+ // keeps the heavy code_graph tool / agent loop / bash-session runtimes out of
4
+ // the session-creation path and avoids a circular import through loop.mjs.
5
+ let _codeGraphRuntimePromise = null;
6
+ let _agentLoopPromise = null;
7
+ let _bashSessionRuntimePromise = null;
8
+ export async function _executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
9
+ _codeGraphRuntimePromise ??= import('../../tools/code-graph.mjs');
10
+ const mod = await _codeGraphRuntimePromise;
11
+ if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
12
+ return mod.executeCodeGraphTool(name, args, cwd, signal, options);
13
+ }
14
+ export async function _getAgentLoop() {
15
+ _agentLoopPromise ??= import('../loop.mjs');
16
+ const mod = await _agentLoopPromise;
17
+ if (typeof mod.agentLoop !== 'function') throw new Error('agent loop runtime is not available');
18
+ return mod.agentLoop;
19
+ }
20
+ export function _closeBashSessionLazy(sessionId, reason) {
21
+ if (!sessionId) return;
22
+ _bashSessionRuntimePromise ??= import('../../tools/bash-session.mjs');
23
+ _bashSessionRuntimePromise
24
+ .then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
25
+ .catch(() => {});
26
+ }
@@ -0,0 +1,124 @@
1
+ // manager/session-close.mjs
2
+ // Session teardown extracted verbatim from manager.mjs. closeSession plants a
3
+ // disk tombstone (or bumps generation when tombstone=false), aborts the
4
+ // in-flight controller, tears down runtime/bash/dedup/offload/pending state,
5
+ // and defers the runtime-map clear. abortSessionTurn cancels the current turn
6
+ // without tombstoning.
7
+ import { loadSession, markSessionClosed, bumpSessionGeneration } from '../store.mjs';
8
+ import { clearReadDedupSession } from '../read-dedup.mjs';
9
+ import { clearOffloadSession } from '../tool-result-offload.mjs';
10
+ import { SessionClosedError } from './session-errors.mjs';
11
+ import { _dropPendingMessageState } from './pending-messages.mjs';
12
+ import { _stopToolActivityHeartbeat, _getRuntimeEntry, _clearSessionRuntime } from './runtime-liveness.mjs';
13
+ import { _closeBashSessionLazy } from './runtime-loaders.mjs';
14
+
15
+ /**
16
+ * Close a session. Plants a `closed=true` tombstone on disk with a bumped
17
+ * generation (so any racing saveSession() drops its write), aborts the
18
+ * in-flight controller if one exists, and clears the in-memory runtime entry.
19
+ *
20
+ * IMPORTANT: we deliberately do NOT unlink the session file here. The tombstone
21
+ * on disk is the authoritative signal that blocks resurrection — a late
22
+ * saveSession() re-reads disk via _shouldDrop() and will find the tombstone.
23
+ * If we delete the file, a late save sees no file, decides nothing to drop,
24
+ * and recreates the session in its pre-close state.
25
+ *
26
+ * Long-term cleanup: `sweepTombstones()` below unlinks tombstones older than
27
+ * TOMBSTONE_MAX_AGE_MS (24h — vastly longer than any realistic in-flight race).
28
+ */
29
+ export function closeSession(id, reason = 'manual', opts = {}) {
30
+ // tombstone=false: detach runtime resources (heartbeat, bash shells,
31
+ // controller abort, runtime-map clear) WITHOUT planting the disk
32
+ // tombstone. Used for non-empty sessions on /resume-away, /new, and
33
+ // TUI exit — previously every one of those paths unconditionally
34
+ // tombstoned the outgoing session, which made it vanish from the
35
+ // Resume list immediately and get hard-deleted by sweepTombstones()
36
+ // after 24h even though it had real conversation content worth
37
+ // resuming. Only truly-empty scratch sessions should still tombstone.
38
+ const tombstone = opts.tombstone !== false;
39
+ if (!id) return false;
40
+ _stopToolActivityHeartbeat(id);
41
+ // Prefer in-memory runtime session — allBashSessionIds may not be persisted
42
+ // yet for shells opened in the current turn (BL-bash-disk-sync).
43
+ const inMemory = _getRuntimeEntry(id)?.session;
44
+ const persisted = inMemory || loadSession(id);
45
+ const bashSessionId = persisted?.implicitBashSessionId || null;
46
+ // Collect all persistent bash shells created during this session.
47
+ const allBashIds = Array.isArray(persisted?.allBashSessionIds)
48
+ ? persisted.allBashSessionIds.filter(Boolean)
49
+ : (bashSessionId ? [bashSessionId] : []);
50
+ // Deduplicate: allBashIds already covers implicitBashSessionId, but guard
51
+ // against old session records that only have implicitBashSessionId.
52
+ if (bashSessionId && !allBashIds.includes(bashSessionId)) allBashIds.push(bashSessionId);
53
+ // 1. Tombstone first — this wins the race against saveSession().
54
+ // Skipped when tombstone=false: no closed:true marker is planted, so
55
+ // the session file stays intact and resumeSession() will accept it.
56
+ // We still bump the on-disk generation via bumpSessionGeneration() —
57
+ // that alone is what protects the session from a late save race: any
58
+ // saveSession() still in flight from this detached turn (e.g. the
59
+ // cancel-cleanup save below) carries the OLD generation as its
60
+ // expectedGeneration, so _shouldDrop()'s ownership-counter rule drops
61
+ // it once disk generation moves past that. Without this bump the late
62
+ // write could silently overwrite the session after the user resumes
63
+ // it back (BL: burned-session late-save clobber).
64
+ const newGen = tombstone ? markSessionClosed(id, reason) : bumpSessionGeneration(id, reason);
65
+ // 2. Mark runtime as closed so post-await validation in askSession fires.
66
+ const entry = _getRuntimeEntry(id);
67
+ if (entry) {
68
+ entry.closed = true;
69
+ entry.closedReason = reason;
70
+ if (typeof newGen === 'number') entry.generation = newGen;
71
+ entry.stage = 'cancelling';
72
+ entry.updatedAt = Date.now();
73
+ // 3. Abort the in-flight controller. Providers that honour the signal
74
+ // unwind immediately; providers that don't will still be caught by
75
+ // the generation check after their await eventually returns.
76
+ try { entry.controller?.abort(new SessionClosedError(id, `closeSession (reason=${reason})`, reason)); } catch { /* ignore */ }
77
+ }
78
+ // Diagnostic: one-line stderr so operators can distinguish the four close
79
+ // pathways (request-abort / manual / idle-sweep / runner-crash). iterCount
80
+ // is not currently tracked on runtime state; askStartedAt is — derive
81
+ // duration from it when present.
82
+ try {
83
+ const askStartedAt = entry?.askStartedAt;
84
+ const durationMs = (typeof askStartedAt === 'number') ? (Date.now() - askStartedAt) : null;
85
+ const parts = [`session=${id}`, `reason=${reason}`, `tombstone=${tombstone}`];
86
+ if (durationMs != null) parts.push(`duration=${durationMs}ms`);
87
+ if (process.env.MIXDOG_DEBUG_SESSION_LOG) process.stderr.write(`[agent-close] ${parts.join(' ')}\n`);
88
+ } catch { /* best-effort */ }
89
+ for (const bsid of allBashIds) {
90
+ try { _closeBashSessionLazy(bsid, `agent-close:${id}`); } catch { /* ignore */ }
91
+ }
92
+ // Drop session-scoped read dedup cache so the Map doesn't accumulate
93
+ // entries across mcp-server lifetime.
94
+ try { clearReadDedupSession(id); } catch { /* ignore */ }
95
+ // Drop offload sidecars + module-level counter for this session so a
96
+ // long-running mcp-server doesn't leak disk (tool-results/<id>/*.txt)
97
+ // or Map entries across session lifetime. Fire-and-forget — close path
98
+ // should not await disk IO; errors are swallowed inside.
99
+ try { clearOffloadSession(id); } catch { /* ignore */ }
100
+ // Drop the in-memory pending-message queue and any buffered-persist entry
101
+ // for this session — otherwise both Maps accumulate one entry per closed
102
+ // session for the life of the mcp-server.
103
+ _dropPendingMessageState(id, { clearPersisted: tombstone });
104
+ // 4. Defer runtime map clear to next tick so any settling askSession can
105
+ // observe `closed=true` / bumped generation before we yank the entry.
106
+ // Disk tombstone remains — that's what blocks resurrection.
107
+ setImmediate(() => {
108
+ _clearSessionRuntime(id);
109
+ });
110
+ return true;
111
+ }
112
+ export function abortSessionTurn(id, reason = 'turn-abort') {
113
+ if (!id) return false;
114
+ _stopToolActivityHeartbeat(id);
115
+ const entry = _getRuntimeEntry(id);
116
+ if (!entry || entry.closed) return false;
117
+ entry.stage = 'cancelling';
118
+ entry.closedReason = reason;
119
+ entry.updatedAt = Date.now();
120
+ try {
121
+ entry.controller?.abort(new SessionClosedError(id, `abortSessionTurn (reason=${reason})`, reason));
122
+ } catch { /* ignore */ }
123
+ return true;
124
+ }