mixdog 0.9.86 → 0.9.87

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 (38) hide show
  1. package/package.json +2 -2
  2. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
  3. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
  4. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  5. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  6. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  7. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
  8. package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
  9. package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
  10. package/src/runtime/memory/tool-defs.mjs +19 -3
  11. package/src/session-runtime/lifecycle-api.mjs +15 -0
  12. package/src/session-runtime/provider-auth-api.mjs +8 -0
  13. package/src/session-runtime/provider-usage.mjs +30 -5
  14. package/src/session-runtime/runtime-core.mjs +267 -481
  15. package/src/session-runtime/session-lifecycle.mjs +378 -0
  16. package/src/standalone/agent-shard/shard-pool.mjs +6 -5
  17. package/src/standalone/agent-tool/job-views.mjs +329 -0
  18. package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
  19. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  20. package/src/standalone/agent-tool.mjs +100 -1151
  21. package/src/standalone/usage-dashboard.mjs +23 -1
  22. package/src/tui/App.jsx +362 -2576
  23. package/src/tui/app/app-view.jsx +504 -0
  24. package/src/tui/app/create-app-pickers.mjs +313 -0
  25. package/src/tui/app/prompt-submit.mjs +501 -0
  26. package/src/tui/app/shell-layout.mjs +563 -0
  27. package/src/tui/app/usage-context-panels.mjs +288 -0
  28. package/src/tui/app/use-copy-selection.mjs +56 -0
  29. package/src/tui/app/use-global-key-input.mjs +151 -0
  30. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  31. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  32. package/src/tui/app/use-prompt-hint.mjs +58 -0
  33. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  34. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  35. package/src/tui/app/use-transcript-activity.mjs +131 -0
  36. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  37. package/src/tui/dist/index.mjs +9802 -9024
  38. package/src/tui/engine/session-api-ext.mjs +23 -0
@@ -0,0 +1,378 @@
1
+ // Session lifecycle, extracted from runtime-core.mjs: first-turn route
2
+ // resolution, route-effort refresh, and createCurrentSession (provider
3
+ // session construction with MCP wiring and reset handling). Shared mutable
4
+ // runtime state flows through the rt bag.
5
+ import { ensureProviderEnabled, normalizeCompactionConfig } from './config-helpers.mjs';
6
+ import { clean, hasOwn } from './session-text.mjs';
7
+ import { coerceEffortFor, deferredSurfaceModeForLead, effortItemsFor, toolSpecForMode } from './effort.mjs';
8
+ import { fastCapableFor } from './model-capabilities.mjs';
9
+ import { bootProfile } from './boot-profile.mjs';
10
+ import { STANDALONE_DATA_DIR } from './runtime-paths.mjs';
11
+ import { LEAD_DISALLOWED_TOOLS } from './tool-defs.mjs';
12
+ import { attachSessionHooks } from './session-hooks.mjs';
13
+ import { applyDeferredToolSurface } from './tool-catalog.mjs';
14
+ import { writeStatuslineRoute } from './statusline-route.mjs';
15
+ import { createWarmupSchedulers } from './warmup-schedulers.mjs';
16
+ import { warmCatalogsInBackground } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
17
+ import { envFlag } from './env.mjs';
18
+ import { createPrewarmSchedulers } from './prewarm.mjs';
19
+ import { hasActiveAutomation } from '../standalone/channel-admin.mjs';
20
+ import { createRemoteTranscript } from './remote-transcript.mjs';
21
+ export function createSessionLifecycle({
22
+ rt,
23
+ collectProviderModels,
24
+ ensureProvidersReady,
25
+ lookupModelMeta,
26
+ mgr,
27
+ loadCoreMemoryContext,
28
+ awaitKeychainPrewarm,
29
+ ensureConfigForRouteProvider,
30
+ reg,
31
+ cfgMod,
32
+ activeWorkflowContext,
33
+ memoryEnabled,
34
+ hooks,
35
+ hookCommonPayload,
36
+ mcpClient,
37
+ modelStandaloneTools,
38
+ applyPreSessionToolSelection,
39
+ statusRoutes,
40
+ warmupTimers,
41
+ providerModelCaches,
42
+ reloadFullConfig,
43
+ refreshStatuslineUsageSnapshot,
44
+ warmProviderModelCache,
45
+ cachedProviderSetup,
46
+ providerWarmupDelayMs,
47
+ providerSetupWarmupDelayMs,
48
+ providerModelWarmupDelayMs,
49
+ modelCatalogWarmupDelayMs,
50
+ statuslineUsageWarmupDelayMs,
51
+ statuslineUsageRefreshDelayMs,
52
+ backgroundBusyRetryMs,
53
+ providerWarmupEnabled,
54
+ modelPrefetchEnabled,
55
+ modelCatalogWarmupEnabled,
56
+ prewarmTimers,
57
+ channelsEnabled,
58
+ getCodeGraphModule,
59
+ channels,
60
+ codeGraphPrewarmDelayMs,
61
+ channelStartDelayMs,
62
+ sessionPrewarmDelayMs,
63
+ codeGraphPrewarmEnabled,
64
+ sessionPrewarmEnabled,
65
+ prewarmState,
66
+ }) {
67
+ async function resolveMissingRouteModelForFirstTurn() {
68
+ if (routeHasModel()) return rt.route;
69
+ const models = await collectProviderModels();
70
+ const picked = models[0] || null;
71
+ if (!picked) {
72
+ throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
73
+ }
74
+ rt.route = {
75
+ ...rt.route,
76
+ provider: picked.provider,
77
+ model: picked.id,
78
+ preset: null,
79
+ };
80
+ return rt.route;
81
+ }
82
+
83
+ async function refreshRouteEffort(modelMetaOverride = null, expectedRoute = null) {
84
+ const targetRoute = expectedRoute || rt.route;
85
+ await ensureProvidersReady(ensureProviderEnabled(rt.config, targetRoute.provider));
86
+ const modelMeta = modelMetaOverride || await lookupModelMeta(targetRoute.provider, targetRoute.model);
87
+ // A rapid second resume/model change can replace the route while provider
88
+ // metadata is loading. Never let the older completion overwrite it.
89
+ if (expectedRoute && rt.route !== expectedRoute) return null;
90
+ const requested = hasOwn(targetRoute, 'effort')
91
+ ? targetRoute.effort
92
+ : (targetRoute.preset?.effort || null);
93
+ const effectiveEffort = coerceEffortFor(targetRoute.provider, modelMeta, requested);
94
+ const fastCapable = fastCapableFor(targetRoute.provider, modelMeta);
95
+ // Carry the catalog display name onto the route so the statusline shows a
96
+ // human label (e.g. "Claude Fable 5") for preset-less direct models instead
97
+ // of the raw id. `name` is only trusted when it differs from the raw model
98
+ // id (some providers echo the id as `name`), so it can't clobber a better
99
+ // already-resolved label. Falls back to existing route.modelDisplay, then unset.
100
+ const metaName = clean(modelMeta?.name);
101
+ const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
102
+ || (metaName && metaName !== clean(targetRoute.model) ? metaName : '')
103
+ || clean(targetRoute.modelDisplay);
104
+ rt.route = {
105
+ ...targetRoute,
106
+ fast: fastCapable ? targetRoute.fast === true : false,
107
+ fastCapable,
108
+ effectiveEffort,
109
+ effortOptions: effortItemsFor(rt.route.provider, modelMeta, effectiveEffort),
110
+ ...(modelDisplay ? { modelDisplay } : {}),
111
+ };
112
+ return rt.route;
113
+ }
114
+
115
+ function routeHasModel() {
116
+ return !!clean(rt.route?.model);
117
+ }
118
+
119
+ function requireModelRoute() {
120
+ if (routeHasModel()) return;
121
+ throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
122
+ }
123
+
124
+ async function recreateCurrentSessionIfReady() {
125
+ if (!routeHasModel()) {
126
+ rt.session = null;
127
+ return null;
128
+ }
129
+ return await createCurrentSession();
130
+ }
131
+
132
+ async function createCurrentSession(reason = 'demand') {
133
+ if (rt.sessionCreatePromise) return await rt.sessionCreatePromise;
134
+ if (rt.session?.id && !rt.sessionNeedsCwdRefresh) {
135
+ const liveSession = mgr.getSession(rt.session.id);
136
+ if (liveSession && liveSession.closed !== true && liveSession.status !== 'closed') {
137
+ rt.session = liveSession;
138
+ return rt.session;
139
+ }
140
+ rt.session = null;
141
+ }
142
+
143
+ const startedAt = performance.now();
144
+ bootProfile('session:create:start', { mode: rt.mode, reason });
145
+ const promise = (async () => {
146
+ // Demand-only: this starts only after the user submits (unless an
147
+ // explicitly enabled prewarm caller asks for a session). Core-memory
148
+ // startup does not depend on keychain/provider readiness, so overlap the
149
+ // two cold paths instead of paying their bounded waits serially.
150
+ const coreMemoryContextPromise = loadCoreMemoryContext();
151
+ await awaitKeychainPrewarm();
152
+ ensureConfigForRouteProvider();
153
+ await resolveMissingRouteModelForFirstTurn();
154
+ requireModelRoute();
155
+ bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
156
+ // Route effort waits on provider readiness while the already-started
157
+ // memory load continues independently.
158
+ const [, coreMemoryContext] = await Promise.all([
159
+ refreshRouteEffort(),
160
+ coreMemoryContextPromise,
161
+ ]);
162
+ bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
163
+ const providerImpl = reg.getProvider(rt.route.provider);
164
+ if (!providerImpl) {
165
+ throw new Error(`Provider "${rt.route.provider}" is not configured.`);
166
+ }
167
+ bootProfile('session:create:provider-ready', { ms: (performance.now() - startedAt).toFixed(1) });
168
+ if (rt.closeRequested) throw new Error('runtime is closing');
169
+ const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
170
+ // Load the active WORKFLOW.md pack once for both summary + context block.
171
+ const { summary: workflow, context: workflowContext } = activeWorkflowContext(rt.config, dataDir);
172
+ const sessionOpts = {
173
+ provider: rt.route.provider,
174
+ model: rt.route.model,
175
+ preset: rt.route.preset || undefined,
176
+ tools: toolSpecForMode(rt.mode),
177
+ owner: 'cli',
178
+ agent: 'lead',
179
+ lane: 'cli',
180
+ sourceType: 'lead',
181
+ sourceName: 'main',
182
+ clientHostPid: process.pid,
183
+ disallowedTools: LEAD_DISALLOWED_TOOLS,
184
+ cwd: rt.currentCwd,
185
+ ...(rt.desktopSession && typeof rt.desktopSession === 'object' ? { desktopSession: rt.desktopSession } : {}),
186
+ coreMemoryContext,
187
+ workflow,
188
+ workflowContext,
189
+ fast: rt.route.fast === true,
190
+ compaction: rt.config.compaction && typeof rt.config.compaction === 'object'
191
+ ? normalizeCompactionConfig(rt.config.compaction, { memoryEnabled: memoryEnabled() })
192
+ : undefined,
193
+ };
194
+ if (hasOwn(rt.route, 'effort') || rt.route.effectiveEffort) {
195
+ sessionOpts.effort = rt.route.effectiveEffort || null;
196
+ }
197
+ rt.session = mgr.createSession(sessionOpts);
198
+ rt.sessionNeedsCwdRefresh = false;
199
+ attachSessionHooks(rt.session, { hooks, hookCommonPayload, getCwd: () => rt.currentCwd });
200
+ // Every-create MCP fold (NO blocking): seed the INITIAL provider-visible
201
+ // surface (and native BP1 manifest) from MCP servers connected at create
202
+ // time. There is no await — a boot connect still mid-handshake is caught on
203
+ // the first user turn by refreshInitialDeferredMcpSurface (session-turn-api),
204
+ // which re-folds the live registry into the first-turn surface before the
205
+ // prompt renders. This fold keeps recreate paths (cwd change with MCP
206
+ // already connected) seeding their manifest instead of re-announcing late.
207
+ let connectedMcpTools = [];
208
+ try { connectedMcpTools = mcpClient.getMcpTools?.() || []; }
209
+ catch { connectedMcpTools = []; }
210
+ applyDeferredToolSurface(
211
+ rt.session,
212
+ deferredSurfaceModeForLead(rt.mode),
213
+ connectedMcpTools.length ? [...modelStandaloneTools(), ...connectedMcpTools] : modelStandaloneTools(),
214
+ { provider: rt.route.provider },
215
+ );
216
+ // Session-local one-shot: mark this FRESH session eligible for the
217
+ // first-turn deferred-surface refresh (session-turn-api). A resumed
218
+ // session (prior transcript) is NEVER marked, so its already-baked BP1 is
219
+ // never rebuilt or re-announced — the gate is per-session, not the
220
+ // process-wide firstTurnCompleted.
221
+ rt.session.deferredInitialRefreshPending = !/resume/i.test(String(reason || ''));
222
+ applyPreSessionToolSelection();
223
+ writeStatuslineRoute(statusRoutes, rt.session, rt.route);
224
+ hooks.emit('session:create', { sessionId: rt.session.id, provider: rt.route.provider, model: rt.route.model, toolMode: rt.mode, cwd: rt.currentCwd });
225
+ // SessionStart: bridge to the standard project hook bus. Best-effort;
226
+ // a hook error must never break session creation. additionalContext is
227
+ // injected before the first user turn as a system-reminder context pair.
228
+ try {
229
+ const startSource = /resume/i.test(String(reason || ''))
230
+ ? 'resume'
231
+ : (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
232
+ const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: rt.session.id, source: startSource, model: rt.route.model }));
233
+ const startContext = Array.isArray(startDispatch?.additionalContext)
234
+ ? startDispatch.additionalContext.join('\n\n')
235
+ : String(startDispatch?.additionalContext || '');
236
+ if (startContext.trim()) {
237
+ rt.session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
238
+ rt.session.messages.push({ role: 'assistant', content: '.' });
239
+ rt.session.updatedAt = Date.now();
240
+ }
241
+ } catch { /* best-effort: never break session create */ }
242
+ bootProfile('session:create:ready', {
243
+ ms: (performance.now() - startedAt).toFixed(1),
244
+ reason,
245
+ tools: Array.isArray(rt.session.tools) ? rt.session.tools.length : 0,
246
+ catalog: Array.isArray(rt.session.deferredToolCatalog) ? rt.session.deferredToolCatalog.length : 0,
247
+ });
248
+ // A rebind push may have been deferred (e.g. 'acquired' landed before this
249
+ // session existed). The writer is now bindable — flush it exactly once.
250
+ remoteTranscript.flushPendingTranscriptRebind();
251
+ return rt.session;
252
+ })();
253
+
254
+ rt.sessionCreatePromise = promise;
255
+ try {
256
+ return await promise;
257
+ } finally {
258
+ if (rt.sessionCreatePromise === promise) rt.sessionCreatePromise = null;
259
+ }
260
+ }
261
+
262
+ const {
263
+ scheduleProviderWarmup,
264
+ scheduleProviderSetupWarmup,
265
+ scheduleProviderModelWarmup,
266
+ scheduleModelCatalogWarmup,
267
+ scheduleStatuslineUsageWarmup,
268
+ scheduleStatuslineUsageRefresh,
269
+ } = createWarmupSchedulers({
270
+ timers: warmupTimers,
271
+ bootProfile,
272
+ getRoute: () => rt.route,
273
+ getConfig: () => rt.config,
274
+ isCloseRequested: () => rt.closeRequested,
275
+ getActiveTurnCount: () => rt.activeTurnCount,
276
+ getSessionCreatePromise: () => rt.sessionCreatePromise,
277
+ getProviderModelsCache: () => providerModelCaches.providerModelsCache,
278
+ getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
279
+ reloadFullConfig,
280
+ ensureConfigForRouteProvider,
281
+ awaitKeychainPrewarm,
282
+ ensureProvidersReady,
283
+ ensureProviderEnabled,
284
+ refreshStatuslineUsageSnapshot,
285
+ warmProviderModelCache,
286
+ cachedProviderSetup,
287
+ warmCatalogsInBackground,
288
+ isFirstTurnCompleted: () => rt.firstTurnCompleted,
289
+ isCatalogRefreshPending: () => rt.startupProviderCatalogRefreshPending,
290
+ envFlag,
291
+ delays: {
292
+ providerWarmupDelayMs,
293
+ providerSetupWarmupDelayMs,
294
+ providerModelWarmupDelayMs,
295
+ modelCatalogWarmupDelayMs,
296
+ statuslineUsageWarmupDelayMs,
297
+ statuslineUsageRefreshDelayMs,
298
+ backgroundBusyRetryMs,
299
+ },
300
+ flags: {
301
+ providerWarmupEnabled,
302
+ modelPrefetchEnabled,
303
+ modelCatalogWarmupEnabled,
304
+ },
305
+ });
306
+ rt.scheduleProviderModelWarmupRef = scheduleProviderModelWarmup;
307
+ rt.scheduleProviderSetupWarmupRef = scheduleProviderSetupWarmup;
308
+
309
+ const {
310
+ scheduleCodeGraphPrewarm,
311
+ scheduleLeadSessionPrewarm,
312
+ scheduleToolRuntimeWarmup,
313
+ invokeChannelStart,
314
+ scheduleChannelStart,
315
+ } = createPrewarmSchedulers({
316
+ timers: prewarmTimers,
317
+ bootProfile,
318
+ getCurrentCwd: () => rt.currentCwd,
319
+ isCloseRequested: () => rt.closeRequested,
320
+ getActiveTurnCount: () => rt.activeTurnCount,
321
+ getSessionCreatePromise: () => rt.sessionCreatePromise,
322
+ getSession: () => rt.session,
323
+ isRemoteEnabled: () => rt.remoteEnabled,
324
+ channelsEnabled,
325
+ hasActiveAutomation,
326
+ getCodeGraphModule,
327
+ createCurrentSession,
328
+ channels,
329
+ envFlag,
330
+ delays: {
331
+ codeGraphPrewarmDelayMs,
332
+ channelStartDelayMs,
333
+ sessionPrewarmDelayMs,
334
+ backgroundBusyRetryMs,
335
+ },
336
+ flags: {
337
+ codeGraphPrewarmEnabled,
338
+ sessionPrewarmEnabled,
339
+ },
340
+ state: prewarmState,
341
+ });
342
+
343
+ // Remote transcript binding + worker rebind pushes live in
344
+ // remote-transcript.mjs; the facade injects the mutable session/cwd/remote
345
+ // state it needs.
346
+ const remoteTranscript = createRemoteTranscript({
347
+ getSession: () => rt.session,
348
+ getCwd: () => rt.currentCwd,
349
+ isRemoteEnabled: () => rt.remoteEnabled,
350
+ getRemoteSessionId: () => rt.remoteSessionId,
351
+ setRemoteSessionId: (next) => { rt.remoteSessionId = next; },
352
+ isCloseRequested: () => rt.closeRequested,
353
+ channelsEnabled,
354
+ channels,
355
+ bootProfile,
356
+ });
357
+
358
+ return {
359
+ resolveMissingRouteModelForFirstTurn,
360
+ scheduleProviderWarmup,
361
+ scheduleProviderSetupWarmup,
362
+ scheduleProviderModelWarmup,
363
+ scheduleModelCatalogWarmup,
364
+ scheduleStatuslineUsageWarmup,
365
+ scheduleStatuslineUsageRefresh,
366
+ scheduleCodeGraphPrewarm,
367
+ scheduleLeadSessionPrewarm,
368
+ scheduleToolRuntimeWarmup,
369
+ invokeChannelStart,
370
+ scheduleChannelStart,
371
+ refreshRouteEffort,
372
+ routeHasModel,
373
+ requireModelRoute,
374
+ recreateCurrentSessionIfReady,
375
+ createCurrentSession,
376
+ remoteTranscript,
377
+ };
378
+ }
@@ -30,11 +30,12 @@ const IDLE_REAP_SWEEP_MS = 60_000;
30
30
  export function agentShardEnabled() {
31
31
  const value = process.env.MIXDOG_AGENT_SHARD;
32
32
  if (value === '1') return true;
33
- if (value === '0') return false;
34
- // P3 default: ON for lead-like hosts. Worker-mode children (channels
35
- // worker, shard child itself) never recursively shard their own spawns
36
- // unless explicitly opted in.
37
- return process.env.MIXDOG_WORKER_MODE !== '1';
33
+ // P2 gate: sharding stays opt-in (MIXDOG_AGENT_SHARD=1). The P3
34
+ // default-ON flip (lead-like hosts shard unless MIXDOG_WORKER_MODE=1)
35
+ // is deferred until the remaining P2 items land — a default-ON pool
36
+ // routes every spawn through shard children, which breaks hosts that
37
+ // inject a custom session manager (fake/test mgr, embedded runtimes).
38
+ return false;
38
39
  }
39
40
 
40
41
  function agentShardPoolSize() {