mixdog 0.9.34 → 0.9.35
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.
- package/README.md +6 -0
- package/package.json +4 -2
- package/scripts/_devtools-stub.mjs +1 -0
- package/scripts/_jitter-fuzz.jsx +42 -0
- package/scripts/_jitter-fuzz.mjs +44410 -0
- package/scripts/_jitter-fuzz2.jsx +30 -0
- package/scripts/_jitter-fuzz2.mjs +44400 -0
- package/scripts/_jitter-probe.jsx +35 -0
- package/scripts/_jitter-probe.mjs +44397 -0
- package/scripts/_jp2.jsx +16 -0
- package/scripts/_jp2.mjs +45614 -0
- package/scripts/agent-live-arg-guard-smoke.mjs +20 -0
- package/scripts/agent-live-path-suffix-smoke.mjs +34 -0
- package/scripts/agent-live-toolcall-args-smoke.mjs +45 -0
- package/scripts/apply-patch-edit-smoke.mjs +71 -0
- package/scripts/async-notify-settlement-test.mjs +99 -0
- package/scripts/forwarder-rebind-tail-test.mjs +67 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/pending-completion-drop-test.mjs +46 -27
- package/scripts/provider-toolcall-test.mjs +1 -0
- package/scripts/recall-bench-cases.json +1 -0
- package/scripts/session-bench.mjs +44 -0
- package/scripts/shell-hardening-test.mjs +209 -0
- package/scripts/ship-mode-test.mjs +98 -0
- package/scripts/steering-drain-buckets-test.mjs +59 -0
- package/scripts/tool-smoke.mjs +46 -10
- package/scripts/worker-notify-rejection-test.mjs +51 -0
- package/src/lib/mixdog-debug.cjs +69 -0
- package/src/rules/agent/00-common.md +4 -5
- package/src/rules/agent/00-core.md +13 -20
- package/src/rules/agent/20-skip-protocol.md +3 -8
- package/src/rules/agent/30-explorer.md +31 -54
- package/src/rules/lead/01-general.md +3 -6
- package/src/rules/lead/lead-brief.md +10 -16
- package/src/rules/lead/lead-tool.md +3 -5
- package/src/rules/shared/01-tool.md +7 -9
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +19 -23
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -13
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +21 -7
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +17 -9
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +127 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +105 -28
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +7 -3
- package/src/runtime/memory/lib/memory-embed.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +25 -10
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/background-tasks.mjs +21 -1
- package/src/runtime/shared/tool-execution-contract.mjs +29 -16
- package/src/session-runtime/model-route-api.mjs +3 -3
- package/src/session-runtime/provider-auth-api.mjs +13 -0
- package/src/session-runtime/runtime-core.mjs +40 -6
- package/src/session-runtime/tool-catalog.mjs +3 -2
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/notify.mjs +14 -1
- package/src/tui/dist/index.mjs +63 -25
- package/src/tui/engine/session-flow.mjs +26 -13
- package/src/tui/engine/turn.mjs +7 -8
- package/src/tui/engine.mjs +4 -2
- package/src/vendor/statusline/bin/statusline-route.mjs +60 -4
- package/src/vendor/statusline/src/gateway/route-meta.mjs +37 -8
- package/src/workflows/default/WORKFLOW.md +26 -32
- package/src/workflows/solo/WORKFLOW.md +12 -13
|
@@ -477,13 +477,14 @@ export function createQueryHandlers({
|
|
|
477
477
|
const applyFreshness = !isCalendarPeriod
|
|
478
478
|
|
|
479
479
|
// period='last': no time window and no session exclusion — 'last' is a
|
|
480
|
-
//
|
|
480
|
+
// recent-session browse; with a query, filter those recent sessions by
|
|
481
|
+
// topic instead of falling through to the unbounded semantic search path.
|
|
481
482
|
// No boot-timestamp cap (the old cap hid every session that ran while a
|
|
482
483
|
// long-lived daemon stayed up), no gap-bounded burst, no current-session
|
|
483
484
|
// filter: limit/offset page through history and the grouped renderer
|
|
484
485
|
// separates sessions. temporal stays unbounded (mode marker only).
|
|
485
486
|
|
|
486
|
-
if (query) {
|
|
487
|
+
if (query && temporal?.mode !== 'last') {
|
|
487
488
|
const _t0 = Date.now()
|
|
488
489
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
489
490
|
let queryVector = null
|
|
@@ -673,6 +674,20 @@ export function createQueryHandlers({
|
|
|
673
674
|
if (temporal?.mode === 'last') {
|
|
674
675
|
const sessionCount = Number.isFinite(requestedLimit) ? limit : 5
|
|
675
676
|
const PER_SESSION_ROW_CAP = 10
|
|
677
|
+
const PER_SESSION_SEARCH_CAP = 50
|
|
678
|
+
const queryTerms = sessionRecallTerms(query)
|
|
679
|
+
const matchesQueryTerms = (row) => {
|
|
680
|
+
if (queryTerms.length === 0) return true
|
|
681
|
+
const rowText = `${row?.content ?? ''} ${row?.element ?? ''} ${row?.summary ?? ''}`.toLowerCase()
|
|
682
|
+
if (queryTerms.some((term) => rowText.includes(term))) return true
|
|
683
|
+
if (Array.isArray(row?.members)) {
|
|
684
|
+
return row.members.some((member) => {
|
|
685
|
+
const memberText = `${member?.content ?? ''} ${member?.element ?? ''} ${member?.summary ?? ''}`.toLowerCase()
|
|
686
|
+
return queryTerms.some((term) => memberText.includes(term))
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
return false
|
|
690
|
+
}
|
|
676
691
|
const excludeStatuses = includeArchived ? [] : ['archived']
|
|
677
692
|
const VALID_LAST_CATS = new Set(['rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue'])
|
|
678
693
|
const catList = category == null
|
|
@@ -684,7 +699,7 @@ export function createQueryHandlers({
|
|
|
684
699
|
// the fill draws from — roots (status-filtered) + unchunked raw
|
|
685
700
|
// leaves, member rows excluded — so a session ranked here can never
|
|
686
701
|
// fill empty (e.g. an archived-root session whose only surviving rows
|
|
687
|
-
// are members). projectScope + excludeStatuses +
|
|
702
|
+
// are members). projectScope + excludeStatuses + promoted
|
|
688
703
|
// exclusion all match the fill filters below.
|
|
689
704
|
const selWhere = [
|
|
690
705
|
'session_id IS NOT NULL',
|
|
@@ -702,10 +717,6 @@ export function createQueryHandlers({
|
|
|
702
717
|
const ph = excludeStatuses.map((s) => { selParams.push(s); return `$${selParams.length}` }).join(',')
|
|
703
718
|
selWhere.push(`(status IS NULL OR status NOT IN (${ph}))`)
|
|
704
719
|
}
|
|
705
|
-
if (catList.length > 0) {
|
|
706
|
-
const ph = catList.map((c) => { selParams.push(c); return `$${selParams.length}` }).join(',')
|
|
707
|
-
selWhere.push(`category IN (${ph})`)
|
|
708
|
-
}
|
|
709
720
|
for (const c of buildPromotedExclusionClauses()) selWhere.push(c)
|
|
710
721
|
selParams.push(sessionCount, offset)
|
|
711
722
|
// Deterministic tie-breaker: equal MAX(ts) across sessions would let the
|
|
@@ -724,14 +735,15 @@ export function createQueryHandlers({
|
|
|
724
735
|
for (const s of sessRows) {
|
|
725
736
|
const sid = String(s?.session_id || '').trim()
|
|
726
737
|
if (!sid) continue
|
|
727
|
-
const
|
|
738
|
+
const perSessionFetchCap = queryTerms.length > 0 ? PER_SESSION_SEARCH_CAP : PER_SESSION_ROW_CAP
|
|
739
|
+
const sf = { limit: perSessionFetchCap, session_id: sid, projectScope, sort: 'date' }
|
|
728
740
|
if (includeMembers) sf.includeMembers = true
|
|
729
741
|
if (excludeStatuses.length > 0) sf.excludeStatuses = excludeStatuses
|
|
730
742
|
if (catList.length > 0) sf.category = catList
|
|
731
743
|
const sRows = await retrieveEntries(db, sf)
|
|
732
744
|
let merged = sRows
|
|
733
745
|
if (includeRaw) {
|
|
734
|
-
const rawRows = await readRawRowsInWindow(db, null, Date.now(),
|
|
746
|
+
const rawRows = await readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: queryTerms })
|
|
735
747
|
const seenIds = new Set(sRows.map((r) => Number(r.id)))
|
|
736
748
|
for (const r of sRows) if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
|
|
737
749
|
// readRawRowsInWindow carries no category filter, so a category-
|
|
@@ -754,9 +766,12 @@ export function createQueryHandlers({
|
|
|
754
766
|
if (newRaw.length > 0) {
|
|
755
767
|
merged = [...sRows, ...newRaw]
|
|
756
768
|
merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
|
|
757
|
-
merged = merged.slice(0, PER_SESSION_ROW_CAP)
|
|
758
769
|
}
|
|
759
770
|
}
|
|
771
|
+
if (queryTerms.length > 0) {
|
|
772
|
+
merged = merged.filter(matchesQueryTerms)
|
|
773
|
+
}
|
|
774
|
+
merged = merged.slice(0, PER_SESSION_ROW_CAP)
|
|
760
775
|
for (const r of merged) allRows.push(r)
|
|
761
776
|
}
|
|
762
777
|
const _currentSessionHint = String(args?.currentSessionId || '').trim()
|
|
@@ -30,13 +30,13 @@ export const TOOL_DEFS = [
|
|
|
30
30
|
name: 'recall',
|
|
31
31
|
title: 'Recall',
|
|
32
32
|
annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
33
|
-
description: 'Retrieve stored memory/session history. Call when a task ties to prior work — resumes, continuations, or references to earlier decisions/messages. Do not call as a reflexive pre-step, to verify a just-made decision, or before storing memory. Query is topic/semantic search, not regex. Patterns: period:"last"
|
|
33
|
+
description: 'Retrieve stored memory/session history. Call when a task ties to prior work — resumes, continuations, or references to earlier decisions/messages. Do not call as a reflexive pre-step, to verify a just-made decision, or before storing memory. Query is topic/semantic search, not regex. Patterns: period:"last" browses recent sessions; +query narrows topic; sessionId without query for current session; period:"3h"/"30m"/"24h" for recent; YYYY-MM-DD, date ranges, or HH:MM~HH:MM for time windows; id for exact follow-up.',
|
|
34
34
|
inputSchema: {
|
|
35
35
|
type: 'object',
|
|
36
36
|
properties: {
|
|
37
37
|
query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Search text, or array for independent fan-out queries.' },
|
|
38
38
|
id: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 1 }], description: 'Exact #id(s) from recall. Do not invent ids.' },
|
|
39
|
-
period: { type: 'string', description: "last (recent sessions
|
|
39
|
+
period: { type: 'string', description: "last (recent sessions; +query topic-filter; limit=session count [default 5], offset=session paging), Nm/Nh/Nd (rolling), today/yesterday/this_week/last_week, all, YYYY-MM-DD, YYYY-MM-DD~YYYY-MM-DD, HH:MM~HH:MM (today), or 'YYYY-MM-DD HH:MM~HH:MM'." },
|
|
40
40
|
limit: { type: 'number', description: 'Max entries.' },
|
|
41
41
|
offset: { type: 'number', description: 'Skip entries.' },
|
|
42
42
|
sort: { type: 'string', enum: ['importance', 'date'], description: 'importance or date.' },
|
|
@@ -63,7 +63,7 @@ export const TOOL_DEFS = [
|
|
|
63
63
|
type: 'object',
|
|
64
64
|
properties: {
|
|
65
65
|
query: { type: 'string', description: 'Search text.' },
|
|
66
|
-
period: { type: 'string', description: "last, Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date range, HH:MM~HH:MM (today), or 'YYYY-MM-DD HH:MM~HH:MM'." },
|
|
66
|
+
period: { type: 'string', description: "last (recent sessions; +query topic-filter), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date range, HH:MM~HH:MM (today), or 'YYYY-MM-DD HH:MM~HH:MM'." },
|
|
67
67
|
sort: { type: 'string', enum: ['date', 'importance'], description: 'date or importance.' },
|
|
68
68
|
category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
|
|
69
69
|
limit: { type: 'number', default: 30, description: 'Max entries.' },
|
|
@@ -358,6 +358,17 @@ export function notifyTaskCompletion(task, instruction) {
|
|
|
358
358
|
context: task.notifyContext,
|
|
359
359
|
enqueueFallback: _enqueueFallback || undefined,
|
|
360
360
|
logPrefix: `background-${task.surface}`,
|
|
361
|
+
// Async notifyFn: `sent` is reported optimistically while the delivery
|
|
362
|
+
// promise is still in flight, so mark delivered now but let settlement
|
|
363
|
+
// confirm it. If the notifyFn rejects/declines AND the fallback rescue also
|
|
364
|
+
// fails, un-mark so a later reconcile can retry rather than leaving the
|
|
365
|
+
// completion silently unsent. A successful/rescued async delivery keeps the
|
|
366
|
+
// marks, preserving exact-once.
|
|
367
|
+
onSettled: (delivered) => {
|
|
368
|
+
if (delivered) return;
|
|
369
|
+
task.notified = false;
|
|
370
|
+
task.notifiedWithBody = false;
|
|
371
|
+
},
|
|
361
372
|
});
|
|
362
373
|
if (sent) {
|
|
363
374
|
task.notified = true;
|
|
@@ -382,7 +393,16 @@ export function reconcileBackgroundTask(taskId, {
|
|
|
382
393
|
} = {}) {
|
|
383
394
|
const task = getBackgroundTask(taskId);
|
|
384
395
|
if (!task) return null;
|
|
385
|
-
if (TERMINAL_STATUSES.has(task.status))
|
|
396
|
+
if (TERMINAL_STATUSES.has(task.status)) {
|
|
397
|
+
// Already terminal, but a prior async completion notify may have un-marked
|
|
398
|
+
// itself after a reject/decline whose fallback rescue also missed
|
|
399
|
+
// (notifyTaskCompletion's onSettled clears notified/notifiedWithBody). The
|
|
400
|
+
// task never leaves its terminal status, so without retrying here the
|
|
401
|
+
// body-carrying completion stays silently unsent. Re-fire the notification;
|
|
402
|
+
// it is idempotent once notifiedWithBody is set, preserving exact-once.
|
|
403
|
+
if (task.notifiedWithBody !== true) notifyTaskCompletion(task, instruction);
|
|
404
|
+
return task;
|
|
405
|
+
}
|
|
386
406
|
return completeBackgroundTask(task.taskId, {
|
|
387
407
|
status,
|
|
388
408
|
result,
|
|
@@ -253,6 +253,7 @@ export function notifyToolCompletion({
|
|
|
253
253
|
context,
|
|
254
254
|
enqueueFallback,
|
|
255
255
|
logPrefix = 'tool-execution',
|
|
256
|
+
onSettled,
|
|
256
257
|
} = {}) {
|
|
257
258
|
const ctx = normalizeToolNotifyContext(context);
|
|
258
259
|
const message = String(text || '');
|
|
@@ -275,22 +276,34 @@ export function notifyToolCompletion({
|
|
|
275
276
|
try {
|
|
276
277
|
const notifyResult = ctx.notifyFn(message, meta);
|
|
277
278
|
if (notifyResult !== false) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
const isThenable = notifyResult && typeof notifyResult.then === 'function';
|
|
280
|
+
if (isThenable) {
|
|
281
|
+
// A Promise notifyFn has NOT delivered yet — settlement decides the
|
|
282
|
+
// real outcome. Return `true` synchronously so the caller does not
|
|
283
|
+
// double-deliver through the sync fallback, but signal the FINAL
|
|
284
|
+
// delivered state via onSettled so the caller only *marks* the
|
|
285
|
+
// completion delivered after settlement. On a reject or explicit
|
|
286
|
+
// false/0 resolve, rescue via enqueueFallback; onSettled then reports
|
|
287
|
+
// whether that rescue (or the notifyFn itself) actually delivered, so
|
|
288
|
+
// a caller can un-mark and retry when nothing landed. The truthy
|
|
289
|
+
// resolve path never enqueues, preserving exact-once delivery.
|
|
290
|
+
Promise.resolve(notifyResult).then((settled) => {
|
|
291
|
+
if (settled === false || settled === 0) {
|
|
292
|
+
const rescued = tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id);
|
|
293
|
+
if (typeof onSettled === 'function') onSettled(rescued);
|
|
294
|
+
} else if (typeof onSettled === 'function') {
|
|
295
|
+
onSettled(true);
|
|
296
|
+
}
|
|
297
|
+
}).catch((err) => {
|
|
298
|
+
try {
|
|
299
|
+
process.stderr.write(`[${logPrefix}] async completion notify failed: id=${id || 'unknown'} err=${err?.message || err}\n`);
|
|
300
|
+
} catch {}
|
|
301
|
+
const rescued = tryEnqueueFallback(ctx, message, meta, enqueueFallback, logPrefix, id);
|
|
302
|
+
if (typeof onSettled === 'function') onSettled(rescued);
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
// Synchronous non-false result → confirmed delivered now.
|
|
294
307
|
return true;
|
|
295
308
|
}
|
|
296
309
|
} catch (err) {
|
|
@@ -29,7 +29,7 @@ export function createModelRouteApi(deps) {
|
|
|
29
29
|
getConfigHasSecrets, getSearchRouteState, setSearchRouteState,
|
|
30
30
|
cfgMod, reg, mgr, statusRoutes,
|
|
31
31
|
resolveRoute, searchCapableFor, lookupModelMeta,
|
|
32
|
-
adoptConfig, saveConfigAndAdopt, ensureFullConfig,
|
|
32
|
+
adoptConfig, saveConfigAndAdopt, ensureFullConfig, ensureProvidersReady,
|
|
33
33
|
persistLeadRoute, refreshRouteEffort,
|
|
34
34
|
refreshStatuslineUsageSnapshot, scheduleStatuslineUsageRefresh,
|
|
35
35
|
invalidateContextStatusCache, invalidateProviderCaches,
|
|
@@ -70,7 +70,7 @@ export function createModelRouteApi(deps) {
|
|
|
70
70
|
throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
|
|
71
71
|
}
|
|
72
72
|
ensureFullConfig();
|
|
73
|
-
await
|
|
73
|
+
await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
|
|
74
74
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
75
75
|
if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
|
|
76
76
|
throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
|
|
@@ -107,7 +107,7 @@ export function createModelRouteApi(deps) {
|
|
|
107
107
|
requested.provider = getRoute().provider;
|
|
108
108
|
}
|
|
109
109
|
let selectedRoute = resolveRoute(getConfig(), requested);
|
|
110
|
-
await
|
|
110
|
+
await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
|
|
111
111
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
112
112
|
if (!providerExplicitlyRequested
|
|
113
113
|
&& !selectedRoute.preset
|
|
@@ -24,10 +24,16 @@ export function createProviderAuthApi({
|
|
|
24
24
|
reloadFullConfig,
|
|
25
25
|
invalidateProviderCaches,
|
|
26
26
|
warmProviderModelCache,
|
|
27
|
+
refreshProviderCatalogs,
|
|
27
28
|
cachedProviderSetup,
|
|
28
29
|
getUsageDashboard,
|
|
29
30
|
collectProviderModels,
|
|
30
31
|
}) {
|
|
32
|
+
function refreshProviderCatalogsSoon() {
|
|
33
|
+
if (typeof refreshProviderCatalogs !== 'function') return;
|
|
34
|
+
try { void Promise.resolve(refreshProviderCatalogs()).catch(() => {}); } catch { /* best-effort */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
return {
|
|
32
38
|
listProviders() {
|
|
33
39
|
return renderProviderStatus(displayConfig());
|
|
@@ -44,6 +50,7 @@ export function createProviderAuthApi({
|
|
|
44
50
|
: await loginOAuthProvider(cfgMod, providerId);
|
|
45
51
|
reloadFullConfig();
|
|
46
52
|
invalidateProviderCaches();
|
|
53
|
+
refreshProviderCatalogsSoon();
|
|
47
54
|
warmProviderModelCache();
|
|
48
55
|
return result;
|
|
49
56
|
},
|
|
@@ -51,6 +58,7 @@ export function createProviderAuthApi({
|
|
|
51
58
|
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
52
59
|
reloadFullConfig();
|
|
53
60
|
invalidateProviderCaches();
|
|
61
|
+
refreshProviderCatalogsSoon();
|
|
54
62
|
warmProviderModelCache();
|
|
55
63
|
return result;
|
|
56
64
|
},
|
|
@@ -63,6 +71,7 @@ export function createProviderAuthApi({
|
|
|
63
71
|
reloadFullConfig();
|
|
64
72
|
if (completed) {
|
|
65
73
|
invalidateProviderCaches();
|
|
74
|
+
refreshProviderCatalogsSoon();
|
|
66
75
|
warmProviderModelCache();
|
|
67
76
|
}
|
|
68
77
|
return completed;
|
|
@@ -71,6 +80,7 @@ export function createProviderAuthApi({
|
|
|
71
80
|
const completed = await result.completeCode(code);
|
|
72
81
|
reloadFullConfig();
|
|
73
82
|
invalidateProviderCaches();
|
|
83
|
+
refreshProviderCatalogsSoon();
|
|
74
84
|
warmProviderModelCache();
|
|
75
85
|
return completed;
|
|
76
86
|
},
|
|
@@ -80,6 +90,7 @@ export function createProviderAuthApi({
|
|
|
80
90
|
const result = saveProviderApiKey(cfgMod, providerId, secret);
|
|
81
91
|
reloadFullConfig();
|
|
82
92
|
invalidateProviderCaches();
|
|
93
|
+
refreshProviderCatalogsSoon();
|
|
83
94
|
warmProviderModelCache();
|
|
84
95
|
return result;
|
|
85
96
|
},
|
|
@@ -105,6 +116,7 @@ export function createProviderAuthApi({
|
|
|
105
116
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
106
117
|
reloadFullConfig();
|
|
107
118
|
invalidateProviderCaches();
|
|
119
|
+
refreshProviderCatalogsSoon();
|
|
108
120
|
warmProviderModelCache();
|
|
109
121
|
return result;
|
|
110
122
|
},
|
|
@@ -112,6 +124,7 @@ export function createProviderAuthApi({
|
|
|
112
124
|
const result = forgetProviderAuth(cfgMod, providerId);
|
|
113
125
|
reloadFullConfig();
|
|
114
126
|
invalidateProviderCaches();
|
|
127
|
+
refreshProviderCatalogsSoon();
|
|
115
128
|
warmProviderModelCache();
|
|
116
129
|
return result;
|
|
117
130
|
},
|
|
@@ -581,7 +581,8 @@ export async function createMixdogSessionRuntime({
|
|
|
581
581
|
providerSetupQuickCache: { setup: null, at: 0 },
|
|
582
582
|
providerSetupPromise: null,
|
|
583
583
|
};
|
|
584
|
-
|
|
584
|
+
const providerInitPromises = new Map();
|
|
585
|
+
let startupProviderCatalogRefreshStarted = false;
|
|
585
586
|
let lastProjectMcpKey = null;
|
|
586
587
|
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
587
588
|
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
@@ -1150,7 +1151,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1150
1151
|
providerUsageCaches.providerSetupCache = { setup: null, at: 0 };
|
|
1151
1152
|
providerUsageCaches.providerSetupQuickCache = { setup: null, at: 0 };
|
|
1152
1153
|
providerUsageCaches.providerSetupPromise = null;
|
|
1153
|
-
|
|
1154
|
+
providerInitPromises.clear();
|
|
1154
1155
|
modelMetaByRoute.clear();
|
|
1155
1156
|
}
|
|
1156
1157
|
|
|
@@ -1197,13 +1198,43 @@ export async function createMixdogSessionRuntime({
|
|
|
1197
1198
|
STANDALONE_DATA_DIR,
|
|
1198
1199
|
});
|
|
1199
1200
|
|
|
1201
|
+
function providerInitCacheKey(value) {
|
|
1202
|
+
function sortKeysDeep(v) {
|
|
1203
|
+
if (Array.isArray(v)) return v.map(sortKeysDeep);
|
|
1204
|
+
if (v && typeof v === 'object') {
|
|
1205
|
+
const out = {};
|
|
1206
|
+
for (const key of Object.keys(v).sort()) out[key] = sortKeysDeep(v[key]);
|
|
1207
|
+
return out;
|
|
1208
|
+
}
|
|
1209
|
+
return v;
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
1213
|
+
} catch {
|
|
1214
|
+
return `uncacheable:${Date.now()}:${Math.random()}`;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1200
1218
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1219
|
+
const initKey = providerInitCacheKey(providerConfig);
|
|
1220
|
+
const existing = providerInitPromises.get(initKey);
|
|
1221
|
+
if (existing) return await existing;
|
|
1222
|
+
const providerInitPromise = reg.initProviders(providerConfig)
|
|
1203
1223
|
.finally(() => {
|
|
1204
|
-
providerInitPromise
|
|
1224
|
+
if (providerInitPromises.get(initKey) === providerInitPromise) providerInitPromises.delete(initKey);
|
|
1205
1225
|
});
|
|
1206
|
-
|
|
1226
|
+
providerInitPromises.set(initKey, providerInitPromise);
|
|
1227
|
+
const result = await providerInitPromise;
|
|
1228
|
+
if (!startupProviderCatalogRefreshStarted && !closeRequested) {
|
|
1229
|
+
startupProviderCatalogRefreshStarted = true;
|
|
1230
|
+
try {
|
|
1231
|
+
reg.refreshProviderCatalogsOnStartup();
|
|
1232
|
+
bootProfile('provider-catalogs:refresh-started');
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return result;
|
|
1207
1238
|
}
|
|
1208
1239
|
|
|
1209
1240
|
const {
|
|
@@ -1982,6 +2013,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1982
2013
|
reloadFullConfig,
|
|
1983
2014
|
invalidateProviderCaches,
|
|
1984
2015
|
warmProviderModelCache,
|
|
2016
|
+
refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
|
|
1985
2017
|
cachedProviderSetup,
|
|
1986
2018
|
getUsageDashboard,
|
|
1987
2019
|
collectProviderModels,
|
|
@@ -2063,6 +2095,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2063
2095
|
adoptConfig,
|
|
2064
2096
|
saveConfigAndAdopt,
|
|
2065
2097
|
ensureFullConfig,
|
|
2098
|
+
ensureProvidersReady,
|
|
2066
2099
|
persistLeadRoute,
|
|
2067
2100
|
refreshRouteEffort,
|
|
2068
2101
|
refreshStatuslineUsageSnapshot,
|
|
@@ -2086,6 +2119,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2086
2119
|
lookupModelMeta,
|
|
2087
2120
|
adoptConfig,
|
|
2088
2121
|
saveConfigAndAdopt,
|
|
2122
|
+
ensureProvidersReady,
|
|
2089
2123
|
displayConfig,
|
|
2090
2124
|
agentRouteFromConfig,
|
|
2091
2125
|
loadAgentDefinition,
|
|
@@ -757,8 +757,9 @@ function pendingAndFailedMcpServers(mcpStatus) {
|
|
|
757
757
|
// tools PLUS pending/failed MCP servers. No listing, no ranking, no substring
|
|
758
758
|
// filter. `options.mcpStatus` is the runtime getter for per-server status.
|
|
759
759
|
export function renderToolSearch(args = {}, session, mode = 'full', options = {}) {
|
|
760
|
-
const
|
|
761
|
-
|
|
760
|
+
const unionCatalog = deferredCatalogUnion(session);
|
|
761
|
+
const catalog = unionCatalog.length
|
|
762
|
+
? unionCatalog
|
|
762
763
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
763
764
|
const requestedNames = parseLoadToolNames(args);
|
|
764
765
|
const { pending: pendingMcpServers, failed: failedMcpServers } = pendingAndFailedMcpServers(options?.mcpStatus);
|
|
@@ -25,7 +25,7 @@ export function createWorkflowAgentsApi(deps) {
|
|
|
25
25
|
const {
|
|
26
26
|
getConfig, getRoute, setRouteState, getSession, setSession, getConfigHasSecrets,
|
|
27
27
|
cfgMod, reg, mgr, STANDALONE_DATA_DIR,
|
|
28
|
-
resolveRoute, lookupModelMeta, adoptConfig, saveConfigAndAdopt, displayConfig,
|
|
28
|
+
resolveRoute, lookupModelMeta, adoptConfig, saveConfigAndAdopt, displayConfig, ensureProvidersReady,
|
|
29
29
|
agentRouteFromConfig, loadAgentDefinition, activeWorkflowId, listWorkflowPacks,
|
|
30
30
|
loadWorkflowPack, workflowSummary,
|
|
31
31
|
getOutputStyleStatusCached, seedOutputStyleStatusCache, scheduleOutputStyleSave,
|
|
@@ -204,7 +204,7 @@ export function createWorkflowAgentsApi(deps) {
|
|
|
204
204
|
const id = normalizeAgentId(agentId);
|
|
205
205
|
if (!id) throw new Error(`unknown agent "${agentId}"`);
|
|
206
206
|
let selectedRoute = resolveRoute(getConfig(), { ...(next || {}) });
|
|
207
|
-
await
|
|
207
|
+
await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
|
|
208
208
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
209
209
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
210
210
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
@@ -37,7 +37,20 @@ export function createNotify(mgr) {
|
|
|
37
37
|
try {
|
|
38
38
|
const result = upstream(text, meta);
|
|
39
39
|
ownerDelivered = result !== false;
|
|
40
|
-
if (ownerDelivered
|
|
40
|
+
if (ownerDelivered && result && typeof result.then === 'function') {
|
|
41
|
+
// Async notifyFn: `ownerDelivered` is optimistic while the delivery
|
|
42
|
+
// promise is in flight — settlement decides the real outcome. On a
|
|
43
|
+
// reject or explicit false/0 resolve, rescue via the owner-session
|
|
44
|
+
// enqueue fallback so the completion is not silently swallowed. A
|
|
45
|
+
// truthy resolve never enqueues, preserving exact-once delivery.
|
|
46
|
+
Promise.resolve(result).then((settled) => {
|
|
47
|
+
if ((settled === false || settled === 0) && ownerSessionId) {
|
|
48
|
+
enqueueCompletionMessage(ownerSessionId, text, meta);
|
|
49
|
+
}
|
|
50
|
+
}).catch(() => {
|
|
51
|
+
if (ownerSessionId) enqueueCompletionMessage(ownerSessionId, text, meta);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
41
54
|
} catch {
|
|
42
55
|
ownerDelivered = false;
|
|
43
56
|
}
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -50,6 +50,39 @@ var require_mixdog_debug = __commonJS({
|
|
|
50
50
|
function isMixdogDebugEnabled() {
|
|
51
51
|
return isTruthyEnv(process.env.MIXDOG_DEBUG) || isTruthyEnv(process.env.MIXDOG_DEBUG_SESSION_START);
|
|
52
52
|
}
|
|
53
|
+
var _cachedFromSource = null;
|
|
54
|
+
function _detectFromSourceCheckout() {
|
|
55
|
+
if (_cachedFromSource !== null) return _cachedFromSource;
|
|
56
|
+
try {
|
|
57
|
+
const moduleDir = module && module.filename ? path2.dirname(module.filename) : process.cwd();
|
|
58
|
+
const roots = [
|
|
59
|
+
path2.resolve(moduleDir, "..", ".."),
|
|
60
|
+
process.cwd()
|
|
61
|
+
];
|
|
62
|
+
_cachedFromSource = roots.some((root) => fs.existsSync(path2.join(root, ".git")));
|
|
63
|
+
} catch {
|
|
64
|
+
_cachedFromSource = false;
|
|
65
|
+
}
|
|
66
|
+
return _cachedFromSource;
|
|
67
|
+
}
|
|
68
|
+
function resolveMixdogMode() {
|
|
69
|
+
const raw = String(process.env.MIXDOG_MODE || "").trim().toLowerCase();
|
|
70
|
+
if (raw === "dev" || raw === "development" || raw === "debug") return "dev";
|
|
71
|
+
if (raw === "ship" || raw === "shipping" || raw === "prod" || raw === "production") return "ship";
|
|
72
|
+
if (isTruthyEnv(process.env.MIXDOG_SHIP)) return "ship";
|
|
73
|
+
if (isMixdogDebugEnabled()) return "dev";
|
|
74
|
+
return _detectFromSourceCheckout() ? "dev" : "ship";
|
|
75
|
+
}
|
|
76
|
+
function isShippingMode() {
|
|
77
|
+
return resolveMixdogMode() === "ship";
|
|
78
|
+
}
|
|
79
|
+
function isDevMode() {
|
|
80
|
+
return resolveMixdogMode() === "dev";
|
|
81
|
+
}
|
|
82
|
+
function isDiagnosticIOEnabled() {
|
|
83
|
+
if (isTruthyEnv(process.env.MIXDOG_DIAGNOSTICS)) return true;
|
|
84
|
+
return !isShippingMode();
|
|
85
|
+
}
|
|
53
86
|
var CANONICAL_PLUGIN_LOG_NAMES = /* @__PURE__ */ new Set([
|
|
54
87
|
"boot.log",
|
|
55
88
|
"crash.log",
|
|
@@ -143,6 +176,10 @@ var require_mixdog_debug = __commonJS({
|
|
|
143
176
|
}
|
|
144
177
|
module.exports = {
|
|
145
178
|
isMixdogDebugEnabled,
|
|
179
|
+
resolveMixdogMode,
|
|
180
|
+
isShippingMode,
|
|
181
|
+
isDevMode,
|
|
182
|
+
isDiagnosticIOEnabled,
|
|
146
183
|
pruneStalePluginDataLogSiblings,
|
|
147
184
|
appendSessionStartCriticalLog,
|
|
148
185
|
DEFAULT_STALE_LOG_SIBLING_MAX,
|
|
@@ -23702,18 +23739,24 @@ function createSessionFlow(bag) {
|
|
|
23702
23739
|
return true;
|
|
23703
23740
|
}
|
|
23704
23741
|
function drainPendingSteering() {
|
|
23705
|
-
const
|
|
23706
|
-
|
|
23707
|
-
|
|
23708
|
-
const
|
|
23709
|
-
if (
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
23716
|
-
|
|
23742
|
+
const predicate = (entry) => !isSlashQueuedEntry(entry);
|
|
23743
|
+
const out = [];
|
|
23744
|
+
for (; ; ) {
|
|
23745
|
+
const batch = dequeueQueueBatch("later", { predicate });
|
|
23746
|
+
if (batch.length === 0) break;
|
|
23747
|
+
for (const entry of batch) {
|
|
23748
|
+
const content = entry.content;
|
|
23749
|
+
const value = typeof content === "string" ? content.trim() : { text: String(entry.text || "").trim(), content };
|
|
23750
|
+
if (typeof value === "string") {
|
|
23751
|
+
if (value.length > 0) out.push(value);
|
|
23752
|
+
} else if (Array.isArray(value.content)) {
|
|
23753
|
+
if (value.content.length > 0) out.push(value);
|
|
23754
|
+
} else if (String(value.content ?? "").trim().length > 0) {
|
|
23755
|
+
out.push(value);
|
|
23756
|
+
}
|
|
23757
|
+
}
|
|
23758
|
+
commitSteeringQueueEntries(batch);
|
|
23759
|
+
}
|
|
23717
23760
|
return out;
|
|
23718
23761
|
}
|
|
23719
23762
|
async function restoreLeadSteeringFromDisk() {
|
|
@@ -24055,18 +24098,13 @@ function createRunTurn(bag) {
|
|
|
24055
24098
|
const deferredEntries = [];
|
|
24056
24099
|
const flushDeferredUpTo = (entry) => {
|
|
24057
24100
|
if (!entry) return;
|
|
24058
|
-
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
}
|
|
24066
|
-
try {
|
|
24067
|
-
e.push();
|
|
24068
|
-
} catch {
|
|
24069
|
-
}
|
|
24101
|
+
const specs = collectDeferredUpTo(entry);
|
|
24102
|
+
if (!specs.length) return;
|
|
24103
|
+
flags.pushingFromDeferredEntry = true;
|
|
24104
|
+
try {
|
|
24105
|
+
appendItemsBatch(specs);
|
|
24106
|
+
} finally {
|
|
24107
|
+
flags.pushingFromDeferredEntry = false;
|
|
24070
24108
|
}
|
|
24071
24109
|
};
|
|
24072
24110
|
flags.flushDeferredBeforeImmediatePush = () => {
|
|
@@ -25988,7 +26026,7 @@ var TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
|
25988
26026
|
})();
|
|
25989
26027
|
var LEAD_TURN_TIMEOUT_MS = (() => {
|
|
25990
26028
|
const value = Number(process.env.MIXDOG_LEAD_TURN_TIMEOUT_MS);
|
|
25991
|
-
return Number.isFinite(value) && value > 0 ? Math.max(1e4, Math.round(value)) :
|
|
26029
|
+
return Number.isFinite(value) && value > 0 ? Math.max(1e4, Math.round(value)) : 5 * 6e4;
|
|
25992
26030
|
})();
|
|
25993
26031
|
var TUI_DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_DEBUG || ""));
|
|
25994
26032
|
var tuiDebug = (msg) => {
|
|
@@ -210,20 +210,33 @@ export function createSessionFlow(bag) {
|
|
|
210
210
|
// through the normal command processor after the turn, not be sent as plain
|
|
211
211
|
// text. Consumed entries are spliced out of `pending` here, so the post-turn
|
|
212
212
|
// drain() loop will not re-execute them.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
//
|
|
214
|
+
// dequeueQueueBatch drains ONE priority/mode bucket per call, capped at a
|
|
215
|
+
// max priority. A concurrent user steering prompt (`next`) and a task
|
|
216
|
+
// notification (`later`) sit in different buckets, so a single `next`-capped
|
|
217
|
+
// dequeue would leave the notification pending and the post-turn drain()
|
|
218
|
+
// loop would spawn an unintended follow-up turn/reply. Loop up to `later`
|
|
219
|
+
// so EVERY non-slash bucket is consumed by the current turn.
|
|
220
|
+
const predicate = (entry) => !isSlashQueuedEntry(entry);
|
|
221
|
+
const out = [];
|
|
222
|
+
for (;;) {
|
|
223
|
+
const batch = dequeueQueueBatch('later', { predicate });
|
|
224
|
+
if (batch.length === 0) break;
|
|
225
|
+
for (const entry of batch) {
|
|
217
226
|
const content = entry.content;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (Array.isArray(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
const value = typeof content === 'string'
|
|
228
|
+
? content.trim()
|
|
229
|
+
: { text: String(entry.text || '').trim(), content };
|
|
230
|
+
if (typeof value === 'string') {
|
|
231
|
+
if (value.length > 0) out.push(value);
|
|
232
|
+
} else if (Array.isArray(value.content)) {
|
|
233
|
+
if (value.content.length > 0) out.push(value);
|
|
234
|
+
} else if (String(value.content ?? '').trim().length > 0) {
|
|
235
|
+
out.push(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
commitSteeringQueueEntries(batch);
|
|
239
|
+
}
|
|
227
240
|
return out;
|
|
228
241
|
}
|
|
229
242
|
|