mixdog 0.9.18 → 0.9.20

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +5 -3
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/output-style-smoke.mjs +2 -2
  9. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  10. package/scripts/recall-bench-cases.json +10 -0
  11. package/scripts/recall-bench.mjs +91 -2
  12. package/scripts/recall-quality-cases.json +1 -2
  13. package/scripts/recall-usecase-cases.json +1 -1
  14. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  15. package/scripts/smoke-runtime-negative.ps1 +106 -106
  16. package/scripts/tool-efficiency-diag.mjs +5 -2
  17. package/scripts/tool-smoke.mjs +251 -72
  18. package/src/agents/debugger/AGENT.md +3 -3
  19. package/src/agents/heavy-worker/AGENT.md +7 -10
  20. package/src/agents/maintainer/AGENT.md +1 -2
  21. package/src/agents/reviewer/AGENT.md +1 -2
  22. package/src/agents/worker/AGENT.md +5 -8
  23. package/src/defaults/agents.json +3 -0
  24. package/src/help.mjs +2 -5
  25. package/src/lib/mixdog-debug.cjs +13 -0
  26. package/src/mixdog-session-runtime.mjs +7 -3311
  27. package/src/rules/agent/00-core.md +4 -3
  28. package/src/rules/agent/30-explorer.md +53 -22
  29. package/src/rules/lead/02-channels.md +3 -3
  30. package/src/rules/lead/lead-tool.md +3 -2
  31. package/src/rules/shared/01-tool.md +24 -29
  32. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  39. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  42. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  43. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  45. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  48. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  49. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  50. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  51. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  52. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  53. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  54. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  55. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  57. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  58. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  59. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  60. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  61. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  63. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  64. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  66. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  68. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  80. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  83. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  84. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  85. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  86. package/src/runtime/channels/backends/discord.mjs +43 -1
  87. package/src/runtime/channels/index.mjs +6 -2096
  88. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  89. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  90. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  91. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  92. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  93. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  94. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  95. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  96. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  97. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  98. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  99. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  100. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  101. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  102. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  103. package/src/runtime/memory/index.mjs +73 -1725
  104. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  105. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  106. package/src/runtime/memory/lib/http-router.mjs +772 -0
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  108. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  109. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  111. package/src/runtime/memory/lib/query-handlers.mjs +38 -6
  112. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  113. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  114. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  115. package/src/runtime/shared/atomic-file.mjs +10 -4
  116. package/src/runtime/shared/background-tasks.mjs +4 -2
  117. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  118. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  119. package/src/runtime/shared/tool-surface.mjs +30 -1
  120. package/src/session-runtime/boot-profile.mjs +36 -0
  121. package/src/session-runtime/channel-config-api.mjs +70 -0
  122. package/src/session-runtime/config-lifecycle.mjs +1 -1
  123. package/src/session-runtime/context-status.mjs +181 -0
  124. package/src/session-runtime/cwd-plugins.mjs +46 -3
  125. package/src/session-runtime/env.mjs +17 -0
  126. package/src/session-runtime/lifecycle-api.mjs +242 -0
  127. package/src/session-runtime/mcp-glue.mjs +24 -3
  128. package/src/session-runtime/model-route-api.mjs +198 -0
  129. package/src/session-runtime/output-styles.mjs +44 -10
  130. package/src/session-runtime/provider-auth-api.mjs +135 -0
  131. package/src/session-runtime/resource-api.mjs +282 -0
  132. package/src/session-runtime/runtime-core.mjs +2046 -0
  133. package/src/session-runtime/session-turn-api.mjs +274 -0
  134. package/src/session-runtime/tool-catalog.mjs +18 -264
  135. package/src/session-runtime/tool-defs.mjs +2 -2
  136. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  137. package/src/session-runtime/workflow.mjs +16 -1
  138. package/src/standalone/agent-tool.mjs +2 -2
  139. package/src/standalone/channel-worker.mjs +78 -5
  140. package/src/standalone/explore-tool.mjs +1 -1
  141. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  142. package/src/tui/App.jsx +88 -97
  143. package/src/tui/app/channel-pickers.mjs +45 -0
  144. package/src/tui/app/core-memory-picker.mjs +1 -1
  145. package/src/tui/app/slash-commands.mjs +0 -1
  146. package/src/tui/app/slash-dispatch.mjs +0 -16
  147. package/src/tui/app/transcript-window.mjs +44 -1
  148. package/src/tui/app/use-mouse-input.mjs +15 -2
  149. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  150. package/src/tui/app/use-transcript-scroll.mjs +82 -4
  151. package/src/tui/app/use-transcript-window.mjs +65 -5
  152. package/src/tui/components/PromptInput.jsx +33 -64
  153. package/src/tui/components/ToolExecution.jsx +2 -2
  154. package/src/tui/dist/index.mjs +7908 -7558
  155. package/src/tui/engine/context-state.mjs +145 -0
  156. package/src/tui/engine/prompt-history.mjs +27 -0
  157. package/src/tui/engine/session-api-ext.mjs +478 -0
  158. package/src/tui/engine/session-api.mjs +545 -0
  159. package/src/tui/engine/session-flow.mjs +485 -0
  160. package/src/tui/engine/turn.mjs +1078 -0
  161. package/src/tui/engine.mjs +69 -2582
  162. package/src/tui/index.jsx +7 -0
  163. package/src/tui/lib/voice-setup.mjs +166 -0
  164. package/src/tui/paste-attachments.mjs +12 -5
  165. package/src/tui/prompt-history-store.mjs +125 -12
  166. package/vendor/ink/build/ink.js +16 -1
  167. package/vendor/ink/build/output.js +30 -4
  168. package/vendor/ink/build/render.js +5 -0
  169. package/scripts/bench/cache-probe-tasks.json +0 -8
  170. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  171. package/scripts/bench/lead-review-tasks.json +0 -20
  172. package/scripts/bench/r4-mixed-tasks.json +0 -20
  173. package/scripts/bench/r5-orchestrated-task.json +0 -7
  174. package/scripts/bench/review-tasks.json +0 -20
  175. package/scripts/bench/round-codex.json +0 -114
  176. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  177. package/scripts/bench/round-mixdog-lead.json +0 -269
  178. package/scripts/bench/round-mixdog.json +0 -126
  179. package/scripts/bench/round-r10-bigsample.json +0 -679
  180. package/scripts/bench/round-r11-codexalign.json +0 -257
  181. package/scripts/bench/round-r13-clientmeta.json +0 -464
  182. package/scripts/bench/round-r14-betafeatures.json +0 -466
  183. package/scripts/bench/round-r15-fulldefault.json +0 -462
  184. package/scripts/bench/round-r16-sessionid.json +0 -466
  185. package/scripts/bench/round-r17-wirebytes.json +0 -456
  186. package/scripts/bench/round-r18-prewarm.json +0 -468
  187. package/scripts/bench/round-r19-clean.json +0 -472
  188. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  189. package/scripts/bench/round-r21-delta-retry.json +0 -473
  190. package/scripts/bench/round-r22-full-probe.json +0 -693
  191. package/scripts/bench/round-r23-itemprobe.json +0 -701
  192. package/scripts/bench/round-r24-shapefix.json +0 -677
  193. package/scripts/bench/round-r25-serial.json +0 -464
  194. package/scripts/bench/round-r26-parallel3.json +0 -671
  195. package/scripts/bench/round-r27-parallel10.json +0 -894
  196. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  197. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  198. package/scripts/bench/round-r30-instid.json +0 -253
  199. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  200. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  201. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  202. package/scripts/bench/round-r34-orchestrated.json +0 -120
  203. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  204. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  205. package/scripts/bench/round-r4-codex.json +0 -114
  206. package/scripts/bench/round-r4-mixed.json +0 -225
  207. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  208. package/scripts/bench/round-r6-codex.json +0 -114
  209. package/scripts/bench/round-r6-solo.json +0 -257
  210. package/scripts/bench/round-r7-full.json +0 -254
  211. package/scripts/bench/round-r8-fulldefault.json +0 -255
  212. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  213. package/src/tui/lib/voice-recorder.mjs +0 -469
  214. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,181 @@
1
+ import {
2
+ estimateRequestReserveTokens,
3
+ estimateToolSchemaTokens,
4
+ estimateTranscriptContextUsage,
5
+ resolveCompactBufferTokens,
6
+ resolveCompactTriggerTokens,
7
+ summarizeContextMessages,
8
+ } from '../runtime/agent/orchestrator/session/context-utils.mjs';
9
+ import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
10
+
11
+ // Live /context gauge computation + its self-owned memoization cache. Extracted
12
+ // verbatim from the runtime API object; the runtime injects live getters for
13
+ // the mutable session/route/cwd/mode locals. The cache (key + value) is owned
14
+ // here now, so invalidateContextStatusCache() is returned for the runtime to
15
+ // call from the same places it used to clear the inline locals.
16
+ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMode }) {
17
+ let contextStatusCacheKey = null;
18
+ let contextStatusCacheValue = null;
19
+
20
+ function contextStatusCacheKeyFor({ messages, tools }) {
21
+ const session = getSession();
22
+ const route = getRoute();
23
+ const compaction = session?.compaction || {};
24
+ const lastMessage = messages[messages.length - 1] || null;
25
+ return {
26
+ session,
27
+ sessionId: session?.id || null,
28
+ provider: route.provider,
29
+ model: route.model,
30
+ cwd: getCurrentCwd(),
31
+ mode: getMode(),
32
+ messages,
33
+ messageCount: messages.length,
34
+ lastMessage,
35
+ lastMessageRole: lastMessage?.role || null,
36
+ lastMessageContent: lastMessage?.content || null,
37
+ tools,
38
+ toolCount: tools.length,
39
+ contextWindow: session?.contextWindow || null,
40
+ rawContextWindow: session?.rawContextWindow || null,
41
+ effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
42
+ autoCompactTokenLimit: Number(session?.autoCompactTokenLimit || 0),
43
+ lastContextTokens: Number(session?.lastContextTokens || 0),
44
+ lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
45
+ lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
46
+ lastInputTokens: Number(session?.lastInputTokens || 0),
47
+ lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
48
+ lastOutputTokens: Number(session?.lastOutputTokens || 0),
49
+ lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
50
+ lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
51
+ totalInputTokens: Number(session?.totalInputTokens || 0),
52
+ totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
53
+ totalOutputTokens: Number(session?.totalOutputTokens || 0),
54
+ totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
55
+ totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
56
+ compactBoundaryTokens: Number(session?.compactBoundaryTokens || 0),
57
+ compactionBoundaryTokens: Number(compaction.boundaryTokens || 0),
58
+ compactionTriggerTokens: Number(compaction.triggerTokens || 0),
59
+ compactionLastChangedAt: Number(compaction.lastChangedAt || 0),
60
+ compactionLastCompactAt: Number(compaction.lastCompactAt || 0),
61
+ };
62
+ }
63
+
64
+ function sameContextStatusCacheKey(a, b) {
65
+ if (!a || !b) return false;
66
+ for (const key of Object.keys(a)) {
67
+ if (!Object.is(a[key], b[key])) return false;
68
+ }
69
+ return true;
70
+ }
71
+
72
+ function invalidateContextStatusCache() {
73
+ contextStatusCacheKey = null;
74
+ contextStatusCacheValue = null;
75
+ }
76
+
77
+ function contextStatus() {
78
+ const session = getSession();
79
+ const route = getRoute();
80
+ // Prefer the in-flight working transcript while a turn is running so the
81
+ // context gauge reflects LIVE growth (user turn + tool calls/results) as
82
+ // it accumulates, instead of freezing at the pre-turn committed snapshot.
83
+ // askSession() sets session.liveTurnMessages for the turn duration and
84
+ // clears it on commit/cancel/error, after which we fall back to the
85
+ // authoritative committed transcript.
86
+ const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
87
+ const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
88
+ const tools = Array.isArray(session?.tools) ? session.tools : [];
89
+ const cacheKey = contextStatusCacheKeyFor({ messages, tools });
90
+ if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
91
+ return contextStatusCacheValue;
92
+ }
93
+
94
+ const messageSummary = summarizeContextMessages(messages);
95
+ const toolSchemaTokens = estimateToolSchemaTokens(tools);
96
+ const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
97
+ const requestReserveTokens = estimateRequestReserveTokens(tools);
98
+ const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
99
+ const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
100
+ const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
101
+ const lastContextTokens = Number(session?.lastContextTokens || 0);
102
+ const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
103
+ messageCount: messageSummary.count,
104
+ });
105
+ const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
106
+ const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
107
+ const lastUsageStale = !!lastContextTokens && (
108
+ session?.lastContextTokensStaleAfterCompact === true
109
+ || (compactAt > 0 && usageAt > 0 && usageAt <= compactAt)
110
+ || (compactAt > 0 && usageAt <= 0)
111
+ );
112
+ const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
113
+ const displayWindow = compactBoundaryTokens || effectiveWindow;
114
+ // The transcript estimate is the single source of truth for the displayed
115
+ // context footprint. Provider-reported input_tokens (lastContextTokens)
116
+ // swing non-monotonically and are not window-bounded on some providers
117
+ // (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
118
+ // metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
119
+ // the gauge numerator.
120
+ const usedTokens = estimatedContextTokens;
121
+ const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
122
+ // Use the same shared compact-policy math as manager/loop. Do not trust
123
+ // persisted trigger telemetry as an independent policy input: it is an
124
+ // output snapshot and was the source of repeated /context false positives.
125
+ const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
126
+ const compactBufferTokens = compactBoundaryTokens
127
+ ? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
128
+ : resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
129
+ const value = {
130
+ sessionId: session?.id || null,
131
+ provider: route.provider,
132
+ model: route.model,
133
+ cwd: getCurrentCwd(),
134
+ toolMode: getMode(),
135
+ contextWindow: displayWindow || effectiveWindow || null,
136
+ effectiveContextWindow: effectiveWindow || null,
137
+ rawContextWindow: rawWindow || null,
138
+ effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
139
+ usedTokens,
140
+ usedSource: 'estimated',
141
+ currentEstimatedTokens: estimatedContextTokens,
142
+ lastApiRequestTokens: lastContextTokens || 0,
143
+ lastApiRequestStale: lastUsageStale,
144
+ freeTokens,
145
+ compaction: {
146
+ ...(session?.compaction || {}),
147
+ boundaryTokens: compactBoundaryTokens || null,
148
+ triggerTokens: compactTriggerTokens || null,
149
+ bufferTokens: compactBufferTokens || null,
150
+ currentEstimatedTokens: estimatedContextTokens,
151
+ lastApiRequestTokens: lastContextTokens || 0,
152
+ lastApiRequestStale: lastUsageStale,
153
+ },
154
+ messages: messageSummary,
155
+ request: {
156
+ toolSchemaTokens,
157
+ toolSchemaBreakdown,
158
+ requestOverheadTokens,
159
+ reserveTokens: requestReserveTokens,
160
+ },
161
+ usage: {
162
+ lastInputTokens: Number(session?.lastInputTokens || 0),
163
+ lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
164
+ lastOutputTokens: Number(session?.lastOutputTokens || 0),
165
+ lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
166
+ lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
167
+ lastContextTokens,
168
+ totalInputTokens: Number(session?.totalInputTokens || 0),
169
+ totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
170
+ totalOutputTokens: Number(session?.totalOutputTokens || 0),
171
+ totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
172
+ totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
173
+ },
174
+ };
175
+ contextStatusCacheKey = cacheKey;
176
+ contextStatusCacheValue = value;
177
+ return value;
178
+ }
179
+
180
+ return { contextStatus, invalidateContextStatusCache };
181
+ }
@@ -45,6 +45,48 @@ export function createCwdPlugins({
45
45
  cfgMod,
46
46
  STANDALONE_DATA_DIR,
47
47
  }) {
48
+ // Per-plugin-root caches for pluginsStatus(): manifest + MCP discovery keyed
49
+ // by root path + manifest mtime (invalidated on manifest edit); recursive
50
+ // skill-file count keyed by root with a ~5s TTL fallback (the walk has no
51
+ // single mtime to key on).
52
+ const pluginRootCache = new Map();
53
+ const skillCountCache = new Map();
54
+ const mcpDiscoveryCache = new Map();
55
+ function manifestMtimeKey(root) {
56
+ let key = '';
57
+ for (const rel of ['.codex-plugin/plugin.json', 'plugin.json']) {
58
+ try { key += `${statSync(resolve(root, rel)).mtimeMs}:`; } catch { key += '0:'; }
59
+ }
60
+ return key;
61
+ }
62
+ function cachedPluginData(root) {
63
+ const key = manifestMtimeKey(root);
64
+ const hit = pluginRootCache.get(root);
65
+ if (hit && hit.key === key) return hit;
66
+ const entry = { key, manifest: pluginManifest(root) };
67
+ pluginRootCache.set(root, entry);
68
+ return entry;
69
+ }
70
+ // MCP discovery probes candidate script files (.mcp.json, scripts/run-mcp.mjs,
71
+ // ...) whose add/remove is NOT reflected in the manifest mtime, so key it on a
72
+ // short TTL instead (~5s, like the skill-file count).
73
+ function cachedMcpDiscovery(root) {
74
+ const now = Date.now();
75
+ const hit = mcpDiscoveryCache.get(root);
76
+ if (hit && (now - hit.at) < 5000) return hit.mcp;
77
+ const mcp = discoverPluginMcp(root);
78
+ mcpDiscoveryCache.set(root, { at: now, mcp });
79
+ return mcp;
80
+ }
81
+ function cachedSkillCount(root) {
82
+ const now = Date.now();
83
+ const hit = skillCountCache.get(root);
84
+ if (hit && (now - hit.at) < 5000) return hit.count;
85
+ const count = countSkillFiles(root);
86
+ skillCountCache.set(root, { at: now, count });
87
+ return count;
88
+ }
89
+
48
90
  function resolveCwdPath(value) {
49
91
  const raw = clean(value);
50
92
  if (!raw) throw new Error('cwd: path is required for action=set');
@@ -125,7 +167,8 @@ export function createCwdPlugins({
125
167
  const addRegisteredPlugin = (entry) => {
126
168
  const root = clean(entry.root);
127
169
  if (!root || !existsSync(root)) return;
128
- const manifest = pluginManifest(root);
170
+ const cached = cachedPluginData(root);
171
+ const manifest = cached.manifest;
129
172
  const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
130
173
  const plugin = {
131
174
  id: clean(entry.id) || name,
@@ -141,9 +184,9 @@ export function createCwdPlugins({
141
184
  root,
142
185
  installedAt: entry.installedAt || null,
143
186
  updatedAt: entry.updatedAt || null,
144
- skillCount: countSkillFiles(root),
187
+ skillCount: cachedSkillCount(root),
145
188
  ...(() => {
146
- const { mcpScript, mcpInline } = discoverPluginMcp(root);
189
+ const { mcpScript, mcpInline } = cachedMcpDiscovery(root);
147
190
  return { mcpScript, mcpInline };
148
191
  })(),
149
192
  };
@@ -0,0 +1,17 @@
1
+ // Environment-variable coercion helpers shared by the session runtime.
2
+ // Extracted verbatim from mixdog-session-runtime.mjs during the facade split.
3
+ export function envFlag(name) {
4
+ return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
5
+ }
6
+
7
+ export function envPresent(name) {
8
+ return process.env[name] !== undefined && process.env[name] !== '';
9
+ }
10
+
11
+ export function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
12
+ const raw = process.env[name];
13
+ if (raw === undefined || raw === '') return fallback;
14
+ const n = Number(raw);
15
+ if (!Number.isFinite(n)) return fallback;
16
+ return Math.min(max, Math.max(min, Math.floor(n)));
17
+ }
@@ -0,0 +1,242 @@
1
+ import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
2
+ import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
3
+ import { isAgentOwner } from '../runtime/agent/orchestrator/agent-owner.mjs';
4
+ import { writeStatuslineRoute } from './statusline-route.mjs';
5
+ import {
6
+ sessionMessageText,
7
+ isSessionPreviewNoise,
8
+ cleanSessionPreview,
9
+ clean,
10
+ hasOwn,
11
+ } from './session-text.mjs';
12
+ import { toolSpecForMode, deferredSurfaceModeForLead } from './effort.mjs';
13
+
14
+ // Session lifecycle surface: teardown (close/abort), resume/new, and the
15
+ // resumable-session listing. Extracted verbatim from the runtime API object;
16
+ // stateless helpers are imported directly and the runtime injects live
17
+ // getters/setters for the mutable session/route/cwd locals plus the closure
18
+ // callbacks and long-lived handles (managers, timers, channel/agent/mcp).
19
+ export function createLifecycleApi(deps) {
20
+ const {
21
+ getSession, setSession, getRoute, setRoute, getConfig, getMode, getCurrentCwd,
22
+ setCloseRequested, getMemoryModPromise, setMemoryModPromise,
23
+ setSessionNeedsCwdRefresh,
24
+ hooks, hookCommonPayload, mgr, statusRoutes, channels, agentTool, mcpClient,
25
+ warmupTimers, prewarmTimers,
26
+ flushConfigSave, flushBackendSave, flushOutputStyleSave,
27
+ withTeardownDeadline, closePatchRuntimeIfLoaded,
28
+ createCurrentSession, refreshRouteEffort,
29
+ invalidateContextStatusCache, invalidatePreSessionToolSurface,
30
+ applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
31
+ } = deps;
32
+ return {
33
+ async close(reason = 'cli-exit', options = {}) {
34
+ const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
35
+ setCloseRequested(true);
36
+ // SessionEnd: bridge teardown to the standard hook bus. reason mapped to
37
+ // standard values ('clear'/'exit' where applicable, else 'other'). Short
38
+ // await guard so a slow hook cannot wedge teardown; best-effort.
39
+ try {
40
+ const rl = String(reason || '').toLowerCase();
41
+ const endReason = /clear/.test(rl) ? 'clear'
42
+ : /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
43
+ : 'other';
44
+ const session = getSession();
45
+ if (session?.id) {
46
+ await withTeardownDeadline(
47
+ Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
48
+ 300,
49
+ undefined,
50
+ );
51
+ }
52
+ } catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
53
+ // Persist any change that is still sitting in the debounce window so a
54
+ // toggle made right before exit is not lost. Synchronous + best-effort:
55
+ // teardown must continue even if the final write fails.
56
+ try { flushConfigSave(); } catch {}
57
+ try { flushBackendSave(); } catch {}
58
+ try { flushOutputStyleSave(); } catch {}
59
+ if (prewarmTimers.channelStartTimer) {
60
+ clearTimeout(prewarmTimers.channelStartTimer);
61
+ prewarmTimers.channelStartTimer = null;
62
+ }
63
+ for (const timerKey of [
64
+ 'providerSetupWarmupTimer',
65
+ 'providerWarmupTimer',
66
+ 'providerModelWarmupTimer',
67
+ 'modelCatalogWarmupTimer',
68
+ ]) {
69
+ if (warmupTimers[timerKey]) {
70
+ clearTimeout(warmupTimers[timerKey]);
71
+ warmupTimers[timerKey] = null;
72
+ }
73
+ }
74
+ if (prewarmTimers.codeGraphPrewarmTimer) {
75
+ clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
76
+ prewarmTimers.codeGraphPrewarmTimer = null;
77
+ }
78
+ for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
79
+ if (warmupTimers[timerKey]) {
80
+ clearTimeout(warmupTimers[timerKey]);
81
+ warmupTimers[timerKey] = null;
82
+ }
83
+ }
84
+ try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
85
+ const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
86
+ try { agentTool.closeAll(reason); } catch {}
87
+ let mcpStop = null;
88
+ try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
89
+ const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
90
+ ? import('../runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
91
+ .then((mod) => mod?.drainOpenaiWsPool?.(reason))
92
+ .catch(() => {})
93
+ : null;
94
+ const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
95
+ const memoryModPromise = getMemoryModPromise();
96
+ const memoryStop = memoryModPromise
97
+ ? memoryModPromise
98
+ .then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
99
+ .catch(() => {})
100
+ .finally(() => {
101
+ setMemoryModPromise(null);
102
+ })
103
+ : null;
104
+ let ok = false;
105
+ const session = getSession();
106
+ if (session?.id) {
107
+ statusRoutes?.clearGatewaySessionRoute?.(session.id);
108
+ // Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
109
+ // always tombstoned the current session, so a session you were
110
+ // mid-conversation in vanished from the Resume list the instant you
111
+ // quit and was hard-deleted by the 24h tombstone sweep. Only
112
+ // tombstone truly-empty scratch sessions; non-empty sessions must
113
+ // survive exit resumable.
114
+ // liveTurnMessages holds the in-flight user prompt until turn
115
+ // commit — an active first-turn ask has its user message there,
116
+ // not yet in session.messages, so it must also be checked or a
117
+ // first-turn exit could still burn a real session.
118
+ const tombstone = !hasUserConversationMessage(session.messages)
119
+ && !hasUserConversationMessage(session.liveTurnMessages);
120
+ ok = mgr.closeSession(session.id, reason, { tombstone });
121
+ setSession(null);
122
+ }
123
+ const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
124
+ ? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
125
+ .then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
126
+ .catch(() => {})
127
+ : null;
128
+ const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
129
+ ? import('../runtime/agent/orchestrator/tools/bash-session.mjs')
130
+ .then((mod) => mod?.shutdownBashSessions?.(reason))
131
+ .catch(() => {})
132
+ : null;
133
+ if (detach) {
134
+ try { await withTeardownDeadline(channelStop, 300, false); } catch {}
135
+ try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
136
+ try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
137
+ try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
138
+ for (const stop of [mcpStop, openaiWsStop, patchStop]) {
139
+ Promise.resolve(stop).catch(() => {});
140
+ }
141
+ return ok;
142
+ }
143
+ await Promise.allSettled([
144
+ withTeardownDeadline(channelStop, 5500, false),
145
+ withTeardownDeadline(mcpStop, 1500, false),
146
+ withTeardownDeadline(openaiWsStop, 1500, false),
147
+ withTeardownDeadline(patchStop, 1500, false),
148
+ withTeardownDeadline(memoryStop, 5500, false),
149
+ withTeardownDeadline(shellJobsStop, 1500, false),
150
+ withTeardownDeadline(bashSessionsStop, 1500, false),
151
+ ]);
152
+ return ok;
153
+ },
154
+ abort(reason = 'cli-abort') {
155
+ const session = getSession();
156
+ if (!session?.id) return false;
157
+ return mgr.abortSessionTurn(session.id, reason);
158
+ },
159
+ listSessions() {
160
+ return mgr.listSessions({}).map(s => {
161
+ const owner = clean(s.owner || 'user').toLowerCase();
162
+ if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
163
+ const sourceType = clean(s.sourceType || '').toLowerCase();
164
+ const sourceName = clean(s.sourceName || '').toLowerCase();
165
+ const agent = clean(s.agent || '').toLowerCase();
166
+ const leadish = agent === 'lead'
167
+ || sourceType === 'lead'
168
+ || (sourceType === 'cli')
169
+ || (!sourceType && !sourceName && !isAgentOwner(owner));
170
+ if (!leadish) return null;
171
+ let preview = cleanSessionPreview(s.preview || '');
172
+ let messageCount = Math.max(0, Number(s.messageCount) || 0);
173
+ if (!preview && Array.isArray(s.messages)) {
174
+ const msgs = s.messages || [];
175
+ const userPreviews = msgs
176
+ .filter(m => m && m.role === 'user')
177
+ .map(m => cleanSessionPreview(sessionMessageText(m.content)))
178
+ .filter(text => !isSessionPreviewNoise(text));
179
+ preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
180
+ messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
181
+ }
182
+ if (!preview && messageCount === 0) return null;
183
+ return {
184
+ id: s.id,
185
+ updatedAt: s.updatedAt,
186
+ cwd: s.cwd || '',
187
+ model: s.model,
188
+ provider: s.provider,
189
+ messageCount,
190
+ preview,
191
+ };
192
+ }).filter(Boolean);
193
+ },
194
+ async newSession() {
195
+ const session = getSession();
196
+ if (session?.id) {
197
+ const tombstone = !hasUserConversationMessage(session.messages)
198
+ && !hasUserConversationMessage(session.liveTurnMessages);
199
+ mgr.closeSession(session.id, 'cli-new', { tombstone });
200
+ setSession(null);
201
+ }
202
+ invalidateContextStatusCache();
203
+ await createCurrentSession();
204
+ return getSession().id;
205
+ },
206
+ async resume(id) {
207
+ const prev = getSession();
208
+ const previousId = prev?.id || null;
209
+ const previousMessages = prev?.messages || null;
210
+ const previousLive = prev?.liveTurnMessages || null;
211
+ const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()));
212
+ if (!resumed) return null;
213
+ if (previousId && previousId !== resumed.id) {
214
+ statusRoutes?.clearGatewaySessionRoute?.(previousId);
215
+ const tombstone = !hasUserConversationMessage(previousMessages)
216
+ && !hasUserConversationMessage(previousLive);
217
+ mgr.closeSession(previousId, 'cli-resume', { tombstone });
218
+ }
219
+ setSession(resumed);
220
+ applyResolvedCwd(resumed.cwd || getCurrentCwd(), { markRefresh: false });
221
+ const route = getRoute();
222
+ const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
223
+ setRoute(resolveRoute(getConfig(), { provider: resumed.provider, model: resumed.model, effort: resumeEffort }));
224
+ await refreshRouteEffort();
225
+ const session = getSession();
226
+ session.effort = getRoute().effectiveEffort || null;
227
+ session.cwd = getCurrentCwd();
228
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(getMode()), standaloneTools, { provider: getRoute().provider });
229
+ invalidatePreSessionToolSurface();
230
+ invalidateContextStatusCache();
231
+ setSessionNeedsCwdRefresh(false);
232
+ writeStatuslineRoute(statusRoutes, session, getRoute());
233
+ return {
234
+ id: resumed.id,
235
+ messages: resumed.messages || [],
236
+ cwd: getCurrentCwd(),
237
+ provider: resumed.provider,
238
+ model: resumed.model,
239
+ };
240
+ },
241
+ };
242
+ }
@@ -4,9 +4,30 @@
4
4
  // `state` object so the facade's teardown/reconnect paths still observe it.
5
5
  // Method behavior is byte-for-byte identical; only grouping changes.
6
6
  import { resolve } from 'node:path';
7
+ import { statSync } from 'node:fs';
7
8
  import { clean } from './session-text.mjs';
8
9
  import { readProjectMcpServers } from './plugin-mcp.mjs';
9
10
 
11
+ // Cache project-local `.mcp.json` reads by path + mtime so repeated mcpStatus()
12
+ // calls skip existsSync+readFileSync+JSON.parse when the file is unchanged.
13
+ // Invalidated automatically on any mtime change (or create/delete via mtime=0).
14
+ const projectMcpCache = new Map();
15
+ const PROJECT_MCP_CACHE_MAX = 32;
16
+ function cachedProjectMcpServers(cwd) {
17
+ const path = resolve(cwd || '.', '.mcp.json');
18
+ let mtimeMs = 0;
19
+ try { mtimeMs = statSync(path).mtimeMs; } catch { mtimeMs = 0; }
20
+ const hit = projectMcpCache.get(path);
21
+ if (hit && hit.mtimeMs === mtimeMs) return hit.value;
22
+ const value = readProjectMcpServers(cwd);
23
+ // Bound the cache: Map preserves insertion order, so drop the oldest entry.
24
+ if (!projectMcpCache.has(path) && projectMcpCache.size >= PROJECT_MCP_CACHE_MAX) {
25
+ projectMcpCache.delete(projectMcpCache.keys().next().value);
26
+ }
27
+ projectMcpCache.set(path, { mtimeMs, value });
28
+ return value;
29
+ }
30
+
10
31
  export function createMcpGlue({
11
32
  mcpClient,
12
33
  getConfig,
@@ -23,15 +44,15 @@ export function createMcpGlue({
23
44
  }
24
45
 
25
46
  // Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
26
- // On name collision the project-local `.mcp.json` entry WINS (Claude Code
27
- // precedence: project > user config). `sources[name]` records each server's
47
+ // On name collision the project-local `.mcp.json` entry WINS
48
+ // (precedence: project > user config). `sources[name]` records each server's
28
49
  // origin ('config' | 'project') for status reporting.
29
50
  function resolveEffectiveMcpServers() {
30
51
  const config = getConfig();
31
52
  const configured = config?.mcpServers && typeof config.mcpServers === 'object'
32
53
  ? config.mcpServers
33
54
  : {};
34
- const project = readProjectMcpServers(getCurrentCwd());
55
+ const project = cachedProjectMcpServers(getCurrentCwd());
35
56
  const servers = { ...configured, ...project };
36
57
  const sources = {};
37
58
  for (const name of Object.keys(configured)) sources[name] = 'config';