mixdog 0.9.36 → 0.9.37
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/package.json +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +36 -6
- package/scripts/explore-prompt-policy-test.mjs +27 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +15 -7
- package/src/rules/lead/01-general.md +4 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +13 -2
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +467 -151
- package/src/tui/engine/agent-job-feed.mjs +26 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +78 -28
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +232 -156
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Many independent singletons self-register process-level drains (exit,
|
|
2
|
+
// beforeExit, SIGTERM, …). Standalone scripts that import runtime modules
|
|
3
|
+
// directly bypass src/app.mjs, so raise the cap from a shared helper too.
|
|
4
|
+
export function ensureProcessListenerHeadroom(min = 64) {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof process.getMaxListeners !== 'function' || typeof process.setMaxListeners !== 'function') return;
|
|
7
|
+
const current = process.getMaxListeners();
|
|
8
|
+
if (current === 0 || current >= min) return;
|
|
9
|
+
process.setMaxListeners(min);
|
|
10
|
+
} catch { /* ignore */ }
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -234,9 +234,12 @@ export function createCwdPlugins({
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
async function loadCoreMemoryContext() {
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
237
|
+
// User-curated core memory injects into new sessions by default.
|
|
238
|
+
// Explicit opt-out (MIXDOG_BOOT_CORE_MEMORY=0/false/no/off) skips the
|
|
239
|
+
// memory/PG startup cost; recall and memory tools still initialize the
|
|
240
|
+
// memory service on first use.
|
|
241
|
+
const bootFlag = String(process.env.MIXDOG_BOOT_CORE_MEMORY ?? '').trim().toLowerCase();
|
|
242
|
+
if (bootFlag === '0' || bootFlag === 'false' || bootFlag === 'no' || bootFlag === 'off') {
|
|
240
243
|
bootProfile('core-memory:skipped');
|
|
241
244
|
return '';
|
|
242
245
|
}
|
|
@@ -17,6 +17,21 @@ import {
|
|
|
17
17
|
SEARCH_DEFAULT_MODEL,
|
|
18
18
|
} from './workflow.mjs';
|
|
19
19
|
import { writeStatuslineRoute } from './statusline-route.mjs';
|
|
20
|
+
import { SUMMARY_PREFIX } from '../runtime/agent/orchestrator/session/compact.mjs';
|
|
21
|
+
import {
|
|
22
|
+
hasUserConversationMessage,
|
|
23
|
+
} from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
24
|
+
|
|
25
|
+
function isSummaryAnchorMessage(message) {
|
|
26
|
+
return message?.role === 'user'
|
|
27
|
+
&& typeof message.content === 'string'
|
|
28
|
+
&& message.content.startsWith(SUMMARY_PREFIX);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasRouteHistoryMessage(messages) {
|
|
32
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
33
|
+
return hasUserConversationMessage(list) || list.some(isSummaryAnchorMessage);
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
// Model/route/search-route selection + mutation surface. Extracted verbatim from
|
|
22
37
|
// the runtime API object; stateless helpers are imported directly and the
|
|
@@ -33,6 +48,8 @@ export function createModelRouteApi(deps) {
|
|
|
33
48
|
persistLeadRoute, refreshRouteEffort,
|
|
34
49
|
refreshStatuslineUsageSnapshot, scheduleStatuslineUsageRefresh,
|
|
35
50
|
invalidateContextStatusCache, invalidateProviderCaches,
|
|
51
|
+
createCurrentSession, invalidatePreSessionToolSurface,
|
|
52
|
+
pushTranscriptRebind,
|
|
36
53
|
collectSearchProviderModels,
|
|
37
54
|
} = deps;
|
|
38
55
|
return {
|
|
@@ -125,10 +142,41 @@ export function createModelRouteApi(deps) {
|
|
|
125
142
|
await refreshRouteEffort(modelMeta);
|
|
126
143
|
refreshStatuslineUsageSnapshot(getRoute());
|
|
127
144
|
scheduleStatuslineUsageRefresh();
|
|
128
|
-
|
|
145
|
+
const session = getSession();
|
|
146
|
+
// Model/provider changes are next-session-only for a session the user
|
|
147
|
+
// has already talked in or compacted (provider-keyed prompt cache). But
|
|
148
|
+
// an EMPTY current session — no committed route history and no in-flight
|
|
149
|
+
// first-turn prompt — has no cache to protect, so /model before the first
|
|
150
|
+
// chat takes effect live: route + statusline update immediately.
|
|
151
|
+
const currentSessionEmpty = !!session
|
|
152
|
+
&& !hasRouteHistoryMessage(session.messages)
|
|
153
|
+
&& !hasRouteHistoryMessage(session.liveTurnMessages);
|
|
154
|
+
const applyLive = applyToCurrentSession || currentSessionEmpty;
|
|
155
|
+
if (!applyLive) {
|
|
156
|
+
return getRoute();
|
|
157
|
+
}
|
|
158
|
+
if (currentSessionEmpty && session?.id && typeof createCurrentSession === 'function') {
|
|
159
|
+
// If the boot create is still finishing SessionStart/deferred-surface
|
|
160
|
+
// work, drain that promise first. Otherwise createCurrentSession()
|
|
161
|
+
// would return the old in-flight promise after we tombstone/null the
|
|
162
|
+
// session, racing the intended rebuild for the new provider.
|
|
163
|
+
await createCurrentSession('model-switch-empty-drain');
|
|
164
|
+
const emptySession = getSession();
|
|
165
|
+
if (!emptySession?.id
|
|
166
|
+
|| hasRouteHistoryMessage(emptySession.messages)
|
|
167
|
+
|| hasRouteHistoryMessage(emptySession.liveTurnMessages)) {
|
|
168
|
+
invalidateContextStatusCache();
|
|
169
|
+
return getRoute();
|
|
170
|
+
}
|
|
171
|
+
statusRoutes?.clearGatewaySessionRoute?.(emptySession.id);
|
|
172
|
+
mgr.closeSession?.(emptySession.id, 'cli-model-switch-empty', { tombstone: true });
|
|
173
|
+
setSession(null);
|
|
174
|
+
invalidatePreSessionToolSurface?.();
|
|
175
|
+
await createCurrentSession('model-switch-empty');
|
|
176
|
+
pushTranscriptRebind?.();
|
|
177
|
+
invalidateContextStatusCache();
|
|
129
178
|
return getRoute();
|
|
130
179
|
}
|
|
131
|
-
const session = getSession();
|
|
132
180
|
if (session) {
|
|
133
181
|
const route = getRoute();
|
|
134
182
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
@@ -31,7 +31,14 @@ export function createProviderAuthApi({
|
|
|
31
31
|
}) {
|
|
32
32
|
function refreshProviderCatalogsSoon() {
|
|
33
33
|
if (typeof refreshProviderCatalogs !== 'function') return;
|
|
34
|
-
try {
|
|
34
|
+
try {
|
|
35
|
+
void Promise.resolve(refreshProviderCatalogs())
|
|
36
|
+
.then(() => {
|
|
37
|
+
invalidateProviderCaches();
|
|
38
|
+
warmProviderModelCache();
|
|
39
|
+
})
|
|
40
|
+
.catch(() => {});
|
|
41
|
+
} catch { /* best-effort */ }
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
return {
|
|
@@ -80,6 +80,24 @@ export function createResourceApi(deps) {
|
|
|
80
80
|
}
|
|
81
81
|
return chain.running;
|
|
82
82
|
}
|
|
83
|
+
function configuredProfileIdentityLine() {
|
|
84
|
+
try {
|
|
85
|
+
const config = getConfig();
|
|
86
|
+
const stored = config?.profile ?? config?.agent?.profile;
|
|
87
|
+
const profile = cfgMod.normalizeProfileConfig(stored);
|
|
88
|
+
const title = clean(profile?.title);
|
|
89
|
+
if (!title) return '';
|
|
90
|
+
return `[profile] Current configured user name/identity: ${title}. This profile value is authoritative; ignore stale memory rows that say the user's identity is unknown.`;
|
|
91
|
+
} catch {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isIdentityRecallQuery(query) {
|
|
96
|
+
const q = clean(query).toLowerCase().replace(/\s+/g, '');
|
|
97
|
+
if (!q) return false;
|
|
98
|
+
return /(?:\uB0B4\uAC00|\uB098\uB294|\uB098|\uC0AC\uC6A9\uC790|\uC720\uC800|user|my|me).*(?:\uB204\uAD6C|\uB204\uAD70|\uC815\uCCB4|\uC774\uB984|name|identity)|(?:whoami|whoami\?|whoami?)|who(?:am)?i|whoami/.test(q)
|
|
99
|
+
|| /^(?:\uB098\uB204\uAD6C\uB0D0|\uB098\uB294\uB204\uAD6C\uB0D0|\uB0B4\uAC00\uB204\uAD6C\uB0D0|\uB0B4\uC774\uB984\uBB50|\uB0B4\uC774\uB984\uBB50\uC57C|whoami)$/i.test(q);
|
|
100
|
+
}
|
|
83
101
|
return {
|
|
84
102
|
mcpStatus() {
|
|
85
103
|
return mcpStatus();
|
|
@@ -300,6 +318,10 @@ export function createResourceApi(deps) {
|
|
|
300
318
|
const session = getSession();
|
|
301
319
|
const currentCwd = getCurrentCwd();
|
|
302
320
|
const baseQuery = query || args?.query || '';
|
|
321
|
+
if (isIdentityRecallQuery(baseQuery)) {
|
|
322
|
+
const profileLine = configuredProfileIdentityLine();
|
|
323
|
+
if (profileLine) return profileLine;
|
|
324
|
+
}
|
|
303
325
|
if (args?.currentSession !== false && session?.id) {
|
|
304
326
|
const currentText = currentSessionRecallRows(session, baseQuery, { limit: args?.limit });
|
|
305
327
|
if (!isEmptyRecallText(currentText)) return currentText;
|
|
@@ -713,7 +713,7 @@ export async function createMixdogSessionRuntime({
|
|
|
713
713
|
// available / installs on quit" nag while the background stage runs
|
|
714
714
|
// silently. The wording lives in the notice surface (notification-plan):
|
|
715
715
|
// this emit only carries meta.version. TUI maps meta.kind 'update-notice'
|
|
716
|
-
// to a
|
|
716
|
+
// to a transient notice, never a model-visible message; tone 'info' =
|
|
717
717
|
// non-urgent, applies on the next launch.
|
|
718
718
|
const announceReady = () => {
|
|
719
719
|
emitRuntimeNotification('update ready', { kind: 'update-notice', version: ver, tone: 'info' });
|
|
@@ -1228,7 +1228,15 @@ export async function createMixdogSessionRuntime({
|
|
|
1228
1228
|
if (!startupProviderCatalogRefreshStarted && !closeRequested) {
|
|
1229
1229
|
startupProviderCatalogRefreshStarted = true;
|
|
1230
1230
|
try {
|
|
1231
|
-
reg.refreshProviderCatalogsOnStartup()
|
|
1231
|
+
void Promise.resolve(reg.refreshProviderCatalogsOnStartup())
|
|
1232
|
+
.then(() => {
|
|
1233
|
+
invalidateProviderCaches();
|
|
1234
|
+
warmProviderModelCache();
|
|
1235
|
+
bootProfile('provider-catalogs:refresh-ready');
|
|
1236
|
+
})
|
|
1237
|
+
.catch((error) => {
|
|
1238
|
+
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1239
|
+
});
|
|
1232
1240
|
bootProfile('provider-catalogs:refresh-started');
|
|
1233
1241
|
} catch (error) {
|
|
1234
1242
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
@@ -2102,6 +2110,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2102
2110
|
scheduleStatuslineUsageRefresh,
|
|
2103
2111
|
invalidateContextStatusCache,
|
|
2104
2112
|
invalidateProviderCaches,
|
|
2113
|
+
createCurrentSession,
|
|
2114
|
+
invalidatePreSessionToolSurface,
|
|
2115
|
+
pushTranscriptRebind,
|
|
2105
2116
|
collectSearchProviderModels,
|
|
2106
2117
|
});
|
|
2107
2118
|
const workflowAgentsApi = createWorkflowAgentsApi({
|
|
@@ -122,7 +122,12 @@ export function createSettingsApi({
|
|
|
122
122
|
// language catalog entry and the full language list for the picker UI.
|
|
123
123
|
getProfile() {
|
|
124
124
|
const config = getConfig();
|
|
125
|
-
|
|
125
|
+
// In-memory config is flat: `config.profile` is what the save path
|
|
126
|
+
// (buildAgentSaveBuilder) persists into the on-disk `agent.profile`
|
|
127
|
+
// slot. Fall back to a nested `agent.profile` only for any stray
|
|
128
|
+
// nested snapshot.
|
|
129
|
+
const stored = config?.profile ?? config?.agent?.profile;
|
|
130
|
+
const profile = cfgMod.normalizeProfileConfig(stored);
|
|
126
131
|
return {
|
|
127
132
|
...profile,
|
|
128
133
|
languageEntry: cfgMod.profileLanguageEntry(profile.language),
|
|
@@ -152,7 +157,7 @@ export function createSettingsApi({
|
|
|
152
157
|
},
|
|
153
158
|
setProfile(input = {}) {
|
|
154
159
|
const config = getConfig();
|
|
155
|
-
const current = cfgMod.normalizeProfileConfig(config
|
|
160
|
+
const current = cfgMod.normalizeProfileConfig(config?.profile ?? config?.agent?.profile);
|
|
156
161
|
const next = { ...current };
|
|
157
162
|
if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
|
|
158
163
|
next.title = input.title ?? input.name ?? '';
|
|
@@ -161,6 +166,10 @@ export function createSettingsApi({
|
|
|
161
166
|
next.language = input.language ?? input.lang ?? 'system';
|
|
162
167
|
}
|
|
163
168
|
const normalized = cfgMod.normalizeProfileConfig(next);
|
|
169
|
+
// Persist flat: buildAgentSaveBuilder (config.mjs saveConfig) reads
|
|
170
|
+
// `config.profile` and writes it into the on-disk `agent.profile`
|
|
171
|
+
// section, which the prompt builder (readAgentConfig) reads. Writing a
|
|
172
|
+
// nested `agent.profile` here would be dropped by the save path.
|
|
164
173
|
saveConfigAndAdopt({ ...config, profile: normalized });
|
|
165
174
|
return this.getProfile();
|
|
166
175
|
},
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
|
|
15
15
|
import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
16
16
|
import { normalizeAgentPermission } from '../runtime/shared/markdown-frontmatter.mjs';
|
|
17
|
+
import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
|
|
17
18
|
import { prepareAgentSession } from '../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
18
19
|
import {
|
|
19
20
|
abortAgentProgressWatchdog,
|
|
@@ -63,6 +64,8 @@ import { createNotify } from './agent-tool/notify.mjs';
|
|
|
63
64
|
// identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
|
|
64
65
|
export { AGENT_TOOL };
|
|
65
66
|
|
|
67
|
+
ensureProcessListenerHeadroom(64);
|
|
68
|
+
|
|
66
69
|
const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
67
70
|
|
|
68
71
|
// Grace window during which a terminated/idle worker row is kept around so the
|
|
@@ -98,15 +101,18 @@ const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIME
|
|
|
98
101
|
|
|
99
102
|
// Global spawn-start stagger: unlimited-N parallel fan-out otherwise fires all
|
|
100
103
|
// first provider calls in the same instant, racing the server-side prompt-
|
|
101
|
-
// cache write/propagation window.
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
104
|
+
// cache write/propagation window. Default 0 (off): mirrors the explore fan-out
|
|
105
|
+
// finding — the first spawn's prompt-cache write only lands after its iter1
|
|
106
|
+
// completes (~seconds), so a sub-second stagger yields ~no cross-spawn cache
|
|
107
|
+
// reads while charging every later spawn the full delay, i.e. pure fan-out
|
|
108
|
+
// latency for negligible hit-rate gain. Kept as a knob for tuning: set
|
|
109
|
+
// MIXDOG_SPAWN_STAGGER_MS>0 to re-enable. When >0 it chains (not a fixed lane
|
|
110
|
+
// count) so it scales to any N: each new spawn's start is pushed to at least
|
|
111
|
+
// STAGGER_MS after the previous spawn's start; sequential/non-overlapping
|
|
112
|
+
// spawns pay zero added latency. Applied inside the deferred job body (see
|
|
113
|
+
// startDeferredSpawnJob) so the agent tool call itself still returns task_id
|
|
114
|
+
// immediately.
|
|
115
|
+
const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 0);
|
|
110
116
|
let lastSpawnStartAt = 0;
|
|
111
117
|
async function waitForSpawnStagger() {
|
|
112
118
|
if (SPAWN_STAGGER_MS <= 0) return;
|
|
@@ -5,6 +5,9 @@ import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/a
|
|
|
5
5
|
import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
6
6
|
import { initProviders } from '../runtime/agent/orchestrator/providers/registry.mjs';
|
|
7
7
|
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
8
|
+
import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
|
|
9
|
+
|
|
10
|
+
ensureProcessListenerHeadroom(64);
|
|
8
11
|
|
|
9
12
|
// Ported from the original mixdog tool-defs.mjs `explore` entry.
|
|
10
13
|
// `aiWrapped` is dropped: in the standalone build there is no aiWrapped
|
|
@@ -89,7 +92,7 @@ function escapeXml(str) {
|
|
|
89
92
|
// reminder. The full no-verdict contract lives at system level
|
|
90
93
|
// (rules/agent/30-explorer.md).
|
|
91
94
|
export function buildExplorerPrompt(query) {
|
|
92
|
-
return `<query>${escapeXml(query)}</query>\nReminder: RULE ZERO is a binary gate after every tool result: >=1 path:line matching a SPECIFIC query token (product/library/domain name) -> answer NOW with those exact coordinates; zero -> one more batch if budget remains. A generic-word-only match (schema, handler, config, resolver, index, error...) while the query's specific tokens match nowhere counts as ZERO, not a hit. There is no third branch: "hits exist but I want better ones" IS branch one — answer now. A code_graph symbol hit (find_symbol/symbol_search returning path:line) IS an anchor — emit it directly, never re-locate it with grep. Credibility is mechanical (specific-token match), never judged: "is this the real implementation / final handler / just a wrapper" are caller questions, FORBIDDEN here; mark weak anchors ? and answer anyway. Flow/how/trigger queries: first matching definition/entry anchors ARE the complete answer — never trace the chain, one anchor per concept. You locate WHERE, never WHY. Scope is ALWAYS the session working directory: omit path or pass only a path seen in an earlier result; inventing a directory (/workspace/..., another repo's layout) is a defect — on zero hits change TOKENS, never guess paths. HARD max 3 tool turns; counter line (turn 1/3, turn 2/3, turn 3/3) on every tool message; expected shape is turn 1 -> answer, turns 2-3 are miss-recovery only (previous turn had ZERO matching lines), with changed tokens; after turn 3/3 you MUST answer best-so-far. Each turn = ONE maximal batch: a single grep whose pattern[] packs ALL token variants PLUS code_graph/find/glob in the SAME message; a single-tool turn is a wasted turn. Before emitting EXPLORATION_FAILED re-scan ALL earlier results: any line matching a specific query token -> answer with that anchor (? if weak) — a weak anchor beats a false miss; EXPLORATION_FAILED only when all 3 turns found zero token-matching lines. Output only anchor lines formatted as path:line — symbol/name — short reason, max 3 lines, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
|
|
95
|
+
return `<query>${escapeXml(query)}</query>\nReminder: RULE ZERO is a binary gate after every tool result: >=1 path:line matching a SPECIFIC query token (product/library/domain name) -> STOP and answer NOW with those exact coordinates, this IS your final turn; zero -> one more batch if budget remains. Turns 2-3 exist SOLELY as zero-hit recovery (the previous turn matched ZERO specific tokens); spending a turn to confirm, refine, or upgrade an anchor you already hold is a defect. A generic-word-only match (schema, handler, config, resolver, index, error...) while the query's specific tokens match nowhere counts as ZERO, not a hit. There is no third branch: "hits exist but I want better ones" IS branch one — answer now. A code_graph symbol hit (find_symbol/symbol_search returning path:line) IS an anchor — emit it directly, never re-locate it with grep. Credibility is mechanical (specific-token match), never judged: "is this the real implementation / final handler / just a wrapper" are caller questions, FORBIDDEN here; mark weak anchors ? and answer anyway. Flow/how/trigger queries: first matching definition/entry anchors ARE the complete answer — never trace the chain, one anchor per concept. You locate WHERE, never WHY. Scope is ALWAYS the session working directory: omit path or pass only a path seen in an earlier result; inventing a directory (/workspace/..., another repo's layout) is a defect — on zero hits change TOKENS, never guess paths. HARD max 3 tool turns; counter line (turn 1/3, turn 2/3, turn 3/3) on every tool message; expected shape is turn 1 -> answer, turns 2-3 are miss-recovery only (previous turn had ZERO matching lines), with changed tokens; after turn 3/3 you MUST answer best-so-far. Each turn = ONE maximal batch: a single grep whose pattern[] packs ALL token variants PLUS code_graph/find/glob in the SAME message; a single-tool turn is a wasted turn. Before emitting EXPLORATION_FAILED re-scan ALL earlier results: any line matching a specific query token -> answer with that anchor (? if weak) — a weak anchor beats a false miss; EXPLORATION_FAILED only when all 3 turns found zero token-matching lines. Output only anchor lines formatted as path:line — symbol/name — short reason, max 3 lines, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
export function normalizeExploreQueries(rawQuery) {
|
package/src/tui/App.jsx
CHANGED
|
@@ -1494,7 +1494,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1494
1494
|
const n = Number(value || 0);
|
|
1495
1495
|
const d = Number(total || 0);
|
|
1496
1496
|
if (!d) return 'N/A';
|
|
1497
|
-
|
|
1497
|
+
const p = Math.max(0, Math.min(100, (n / d) * 100));
|
|
1498
|
+
return `${p > 0 && p < 1 ? p.toFixed(1) : Math.floor(p)}%`;
|
|
1498
1499
|
};
|
|
1499
1500
|
const fmt = (value) => {
|
|
1500
1501
|
const n = Number(value || 0);
|
|
@@ -2153,12 +2154,12 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2153
2154
|
}
|
|
2154
2155
|
}
|
|
2155
2156
|
if (!commandText) return false;
|
|
2156
|
-
if (state.commandBusy) {
|
|
2157
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
2158
|
-
return false;
|
|
2159
|
-
}
|
|
2160
2157
|
|
|
2161
2158
|
if (commandText.startsWith('/')) {
|
|
2159
|
+
if (state.commandBusy) {
|
|
2160
|
+
store.pushNotice('wait for the current command to finish', 'warn');
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2162
2163
|
const [cmd, ...rest] = commandText.slice(1).split(/\s+/);
|
|
2163
2164
|
const accepted = runSlashCommand(cmd, rest.join(' ').trim());
|
|
2164
2165
|
if (accepted !== false) clearPastedImagesSnapshot();
|
|
@@ -537,6 +537,7 @@ function computeTranscriptItemVariantKey(item) {
|
|
|
537
537
|
const count = Number(item.count ?? 0);
|
|
538
538
|
const completed = item.completedCount === undefined ? 'u' : Number(item.completedCount);
|
|
539
539
|
const errors = item.errorCount === undefined ? 'u' : Number(item.errorCount);
|
|
540
|
+
const callErrors = item.callErrorCount === undefined ? 'u' : Number(item.callErrorCount);
|
|
540
541
|
const isError = item.isError ? 1 : 0;
|
|
541
542
|
const normalizedName = String(normalizeToolName(item.name) || '').toLowerCase();
|
|
542
543
|
const aggregate = item.aggregate ? 1 : 0;
|
|
@@ -547,7 +548,7 @@ function computeTranscriptItemVariantKey(item) {
|
|
|
547
548
|
const bgPrompt = textShapeFingerprint(bgArgs.prompt);
|
|
548
549
|
const bgMessage = textShapeFingerprint(bgArgs.message);
|
|
549
550
|
const bgError = textShapeFingerprint(bgArgs.error);
|
|
550
|
-
return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
|
|
551
|
+
return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:ce${callErrors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
|
|
551
552
|
}
|
|
552
553
|
return `x${expanded}:s${textShapeFingerprint(item.text ?? item.result ?? '')}`;
|
|
553
554
|
}
|
|
@@ -568,13 +569,43 @@ export const transcriptMeasuredRowsCache = new WeakMap();
|
|
|
568
569
|
// (its final settled height is then captured by the normal WeakMap path).
|
|
569
570
|
export const streamingMeasuredRowsById = new Map();
|
|
570
571
|
|
|
572
|
+
// High-water clamp for the STREAMING row ESTIMATE, keyed by stream item id.
|
|
573
|
+
// measureStreamingMarkdownRenderedRows (measure-rendered-rows.mjs) adds a +1
|
|
574
|
+
// gap row only while childCount===2 (stablePrefix box + unstableSuffix box).
|
|
575
|
+
// As the stable/unstable split moves per token — and when the suffix is
|
|
576
|
+
// momentarily whitespace-only, stablePrefix empties — childCount flips 1↔2
|
|
577
|
+
// frame-to-frame, so the estimate oscillates ±1. Streaming text only ever
|
|
578
|
+
// GROWS, so any per-frame DECREASE is spurious. Hold the max estimate seen this
|
|
579
|
+
// streaming run so streamingEstimateRows is NON-DECREASING, killing the -1 dip
|
|
580
|
+
// that shifts the transcript when a newline settles. Entry stores columns/
|
|
581
|
+
// toolExpanded so a real layout-basis change resets the water line (row count
|
|
582
|
+
// legitimately changes with width). Lifecycle mirrors streamingMeasuredRowsById
|
|
583
|
+
// exactly: pruned by pruneStreamingMeasuredRowsById and cleared at the same
|
|
584
|
+
// settle / invalidate delete sites in estimateTranscriptItemRowsCached.
|
|
585
|
+
const streamingEstimateHighWaterById = new Map();
|
|
586
|
+
|
|
587
|
+
/** True when either streaming id-keyed store holds entries, so the mount-prune
|
|
588
|
+
* call site fires even when only the estimate high-water map is populated (an
|
|
589
|
+
* item that never got a Yoga measurement but reached an estimate high-water,
|
|
590
|
+
* then was bulk-replaced, must still be pruned). */
|
|
591
|
+
export function hasStreamingRowStateToPrune() {
|
|
592
|
+
return streamingMeasuredRowsById.size > 0 || streamingEstimateHighWaterById.size > 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
571
595
|
/** Drop streamingMeasuredRowsById entries for ids no longer mounted, so the
|
|
572
596
|
* store does not grow unbounded over a long session (mirrors the id→item /
|
|
573
597
|
* id→callback map pruning already done for the mounted set). */
|
|
574
598
|
export function pruneStreamingMeasuredRowsById(liveIds) {
|
|
575
|
-
if (!liveIds
|
|
576
|
-
|
|
577
|
-
|
|
599
|
+
if (!liveIds) return;
|
|
600
|
+
if (streamingMeasuredRowsById.size > 0) {
|
|
601
|
+
for (const id of streamingMeasuredRowsById.keys()) {
|
|
602
|
+
if (!liveIds.has(id)) streamingMeasuredRowsById.delete(id);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (streamingEstimateHighWaterById.size > 0) {
|
|
606
|
+
for (const id of streamingEstimateHighWaterById.keys()) {
|
|
607
|
+
if (!liveIds.has(id)) streamingEstimateHighWaterById.delete(id);
|
|
608
|
+
}
|
|
578
609
|
}
|
|
579
610
|
}
|
|
580
611
|
|
|
@@ -627,7 +658,21 @@ export function streamingEstimateRows(item, columns, toolOutputExpanded) {
|
|
|
627
658
|
const trimmedText = assistantTextForStreamingRowEstimate(item.text);
|
|
628
659
|
const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
|
|
629
660
|
const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
|
|
630
|
-
|
|
661
|
+
const quantized = Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
|
|
662
|
+
// High-water clamp: streaming text only grows, so never report fewer rows than
|
|
663
|
+
// this id already reached this run — absorbs the childCount 1↔2 gap flip that
|
|
664
|
+
// otherwise dips the estimate ±1 frame-to-frame. A columns/expanded change
|
|
665
|
+
// resets the water line (new width → legitimately new row count).
|
|
666
|
+
const id = item?.id;
|
|
667
|
+
if (id == null) return quantized;
|
|
668
|
+
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
669
|
+
const prev = streamingEstimateHighWaterById.get(id);
|
|
670
|
+
if (!prev || prev.columns !== columns || prev.toolExpanded !== toolExpanded) {
|
|
671
|
+
streamingEstimateHighWaterById.set(id, { rows: quantized, columns, toolExpanded });
|
|
672
|
+
return quantized;
|
|
673
|
+
}
|
|
674
|
+
if (quantized > prev.rows) prev.rows = quantized;
|
|
675
|
+
return prev.rows;
|
|
631
676
|
}
|
|
632
677
|
|
|
633
678
|
// ── Same-frame streaming-tail growth compensation (scrolled-up only) ────────
|
|
@@ -705,15 +750,20 @@ export function estimateTranscriptItemRowsCached(item, columns, toolOutputExpand
|
|
|
705
750
|
// estimate frame.
|
|
706
751
|
return idEntry.rows;
|
|
707
752
|
}
|
|
708
|
-
if (idEntry)
|
|
753
|
+
if (idEntry) {
|
|
754
|
+
streamingMeasuredRowsById.delete(item.id);
|
|
755
|
+
streamingEstimateHighWaterById.delete(item.id);
|
|
756
|
+
}
|
|
709
757
|
// No confirmed measurement yet for this id (item just started streaming):
|
|
710
758
|
// nothing to defer against, so the first frame falls back to the estimate.
|
|
711
759
|
return streamingEstimateRows(item, columns, toolOutputExpanded);
|
|
712
760
|
}
|
|
713
|
-
if (item.kind === 'assistant'
|
|
714
|
-
// Item settled (no longer streaming): the id-keyed floor
|
|
715
|
-
// relevant
|
|
716
|
-
|
|
761
|
+
if (item.kind === 'assistant') {
|
|
762
|
+
// Item settled (no longer streaming): the id-keyed floor and the estimate
|
|
763
|
+
// high-water are no longer relevant — the normal WeakMap-measured path now
|
|
764
|
+
// owns its height. Clear both so a later id reuse / this run cannot leak.
|
|
765
|
+
if (streamingMeasuredRowsById.has(item.id)) streamingMeasuredRowsById.delete(item.id);
|
|
766
|
+
if (streamingEstimateHighWaterById.has(item.id)) streamingEstimateHighWaterById.delete(item.id);
|
|
717
767
|
}
|
|
718
768
|
const variantKey = transcriptItemVariantKey(item);
|
|
719
769
|
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
transcriptMeasuredRowsCache,
|
|
16
16
|
streamingMeasuredRowsById,
|
|
17
17
|
pruneStreamingMeasuredRowsById,
|
|
18
|
+
hasStreamingRowStateToPrune,
|
|
18
19
|
transcriptItemVariantKey,
|
|
19
20
|
buildTranscriptRowIndexIncremental,
|
|
20
21
|
transcriptStructureSignature,
|
|
@@ -632,7 +633,7 @@ export function useTranscriptWindow({
|
|
|
632
633
|
if (!els.has(key)) refCache.delete(key);
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
|
-
if (
|
|
636
|
+
if (hasStreamingRowStateToPrune()) {
|
|
636
637
|
pruneStreamingMeasuredRowsById(new Set(liveItems.keys()));
|
|
637
638
|
}
|
|
638
639
|
});
|
|
@@ -48,7 +48,10 @@ function percent(value, total) {
|
|
|
48
48
|
function percentLabel(value, total) {
|
|
49
49
|
const pct = percent(value, total);
|
|
50
50
|
if (pct === null) return 'N/A';
|
|
51
|
-
|
|
51
|
+
// Match the footer/statusline display: sub-1% keeps one decimal, otherwise
|
|
52
|
+
// show the floored percentage so /context and the statusline do not disagree
|
|
53
|
+
// by 1% around half-percent boundaries.
|
|
54
|
+
return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.floor(pct)}%`;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
function usageColor(pct) {
|
|
@@ -73,7 +73,6 @@ export function displayToolName(name, args) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const TOOL_BLINK_MS = 500;
|
|
76
|
-
const TOOL_BLINK_LIMIT_MS = 3000;
|
|
77
76
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
78
77
|
// One shared-tick cadence covers both the 500ms blink and per-second elapsed;
|
|
79
78
|
// finer than either boundary so both stay crisp off a single timer.
|
|
@@ -87,7 +86,7 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
87
86
|
// dropping the pad just normalizes the spacing.
|
|
88
87
|
return formatToolActionHeader(name, args, { pending, count });
|
|
89
88
|
}
|
|
90
|
-
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
|
|
89
|
+
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
|
|
91
90
|
const rowWidth = Math.max(1, Number(columns || 80));
|
|
92
91
|
const groupCount = Math.max(1, Number(count || 1));
|
|
93
92
|
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
|
|
@@ -116,13 +115,11 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
116
115
|
// land — no empty band.
|
|
117
116
|
const hasVisibleProgress = doneCount > 0 || Boolean(String(rt || '').trim());
|
|
118
117
|
const pendingDisplayReady = !pending || !startedAtMs || pendingDelayElapsed || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS || hasVisibleProgress || deferredDisplayReady;
|
|
119
|
-
// Derived blink (was two per-card setIntervals + a setTimeout):
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// is identical to the old interval without owning a timer.
|
|
118
|
+
// Derived blink (was two per-card setIntervals + a setTimeout): while pending,
|
|
119
|
+
// the dot keeps blinking until the tool resolves. Phase comes from Date.now()
|
|
120
|
+
// so the cadence is identical to the old interval without owning a timer.
|
|
123
121
|
const blinkActive = pending && pendingDisplayReady;
|
|
124
|
-
const
|
|
125
|
-
const blinkOn = !blinkActive || blinkExpired
|
|
122
|
+
const blinkOn = !blinkActive
|
|
126
123
|
? true
|
|
127
124
|
: Math.floor(nowMs / TOOL_BLINK_MS) % 2 === 0;
|
|
128
125
|
// Keep the action verb in its active form until the engine explicitly seals
|
|
@@ -135,6 +132,12 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
135
132
|
const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : (completedAtMs || nowMs)) - startedAtMs) : 0;
|
|
136
133
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
137
134
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
135
|
+
// Real tool-call failures only (backend isError / error toolKind). Drives the
|
|
136
|
+
// ● dot color; command/result failures (shell exit, failed status) are counted
|
|
137
|
+
// in `failedCount`/L2 detail but never in `callFailedCount`, so they never
|
|
138
|
+
// paint the dot red. Fall back to 0 (never `isError`) when the engine did not
|
|
139
|
+
// supply a call-error count so a result failure can't leak into the dot.
|
|
140
|
+
const callFailedCount = clampFailureCount(callErrorCount, groupCount, false);
|
|
138
141
|
const displayGroupCount = groupCount;
|
|
139
142
|
const displayCategories = normalizeCountMap(categories || {});
|
|
140
143
|
// In the DONE state, count only successful calls: error-terminated calls are
|
|
@@ -209,8 +212,8 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
209
212
|
const aggregateTerminalStatus = pending
|
|
210
213
|
? 'running'
|
|
211
214
|
: (resultTerminalStatus(rt) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
212
|
-
const dotColor = toolStatusColor({ pending, groupCount,
|
|
213
|
-
const dotText = pending && !
|
|
215
|
+
const dotColor = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus: aggregateTerminalStatus });
|
|
216
|
+
const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
|
|
214
217
|
const gutter = 2;
|
|
215
218
|
const showHeaderExpandHint = hasRawResult;
|
|
216
219
|
const hintLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
@@ -423,7 +426,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
423
426
|
const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
|
|
424
427
|
visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
|
|
425
428
|
}
|
|
426
|
-
const finalStatusColor = toolStatusColor({ pending, groupCount,
|
|
429
|
+
const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus });
|
|
427
430
|
const dotColor = finalStatusColor;
|
|
428
431
|
// Agent surface cards use directional markers: `←` for requests going OUT
|
|
429
432
|
// (spawn/send/etc.) and `→` for the response coming back IN. Background
|
|
@@ -441,7 +444,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
441
444
|
// `●` turn marker is a true 1-cell glyph and keeps the padding-only gutter.
|
|
442
445
|
const isDirectionalMarker = isAgentResponse || isAgentSurfaceCard;
|
|
443
446
|
const markerText = isDirectionalMarker ? `${markerGlyph} ` : markerGlyph;
|
|
444
|
-
const dotText = pending && !
|
|
447
|
+
const dotText = pending && !blinkOn ? ' ' : markerText;
|
|
445
448
|
let labelText;
|
|
446
449
|
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs);
|
|
447
450
|
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|
|
@@ -77,7 +77,7 @@ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutp
|
|
|
77
77
|
node = <ToolHookDenialCard item={item} columns={columns} />;
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
|
-
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} doneCategories={item.doneCategories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady}
|
|
80
|
+
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} callErrorCount={item.callErrorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} doneCategories={item.doneCategories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady} />;
|
|
81
81
|
break;
|
|
82
82
|
}
|
|
83
83
|
case 'notice':
|
|
@@ -390,16 +390,16 @@ export function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
390
390
|
// partial failure -> mixdogOrange || warning (some, not all, of the group failed)
|
|
391
391
|
// all failed -> theme.error
|
|
392
392
|
// cancelled -> theme.warning
|
|
393
|
-
//
|
|
394
|
-
// (
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
export function toolStatusColor({ pending, groupCount,
|
|
393
|
+
// The RED/orange failure color is driven ONLY by real tool-call errors
|
|
394
|
+
// (`callFailedCount` — backend isError / error toolKind), NOT by command/result
|
|
395
|
+
// failures like a shell non-zero exit or a `[status: failed]` result. Those
|
|
396
|
+
// keep the card's L2 detail showing "Failed" but leave the dot on the success
|
|
397
|
+
// color. `terminalStatus` is still consulted so a cancelled card stays warning.
|
|
398
|
+
export function toolStatusColor({ pending, groupCount, callFailedCount = 0, terminalStatus = '' }) {
|
|
399
399
|
if (pending) return theme.text;
|
|
400
400
|
const status = normalizeTerminalStatus(terminalStatus);
|
|
401
401
|
if (status === 'cancelled') return theme.warning;
|
|
402
|
-
if (
|
|
403
|
-
if (groupCount > 1 &&
|
|
402
|
+
if (callFailedCount <= 0) return theme.success;
|
|
403
|
+
if (groupCount > 1 && callFailedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
404
404
|
return theme.error;
|
|
405
405
|
}
|