mixdog 0.9.43 → 0.9.45
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/ansi-color-capability-test.mjs +90 -0
- package/scripts/generate-runtime-manifest.mjs +39 -22
- package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
- package/scripts/internal-comms-smoke.mjs +24 -7
- package/scripts/maintenance-default-routes-test.mjs +164 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
- package/scripts/routing-corpus.mjs +1 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
- package/scripts/tui-transcript-jitter-harness.mjs +58 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/rules/lead/lead-brief.md +5 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -14
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
- package/src/runtime/memory/data/runtime-manifest.json +11 -11
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
- package/src/runtime/shared/update-checker.mjs +40 -10
- package/src/standalone/agent-tool.mjs +65 -50
- package/src/standalone/explore-tool.mjs +17 -16
- package/src/tui/App.jsx +3 -6
- package/src/tui/app/provider-setup-picker.mjs +1 -0
- package/src/tui/app/use-transcript-window.mjs +5 -1
- package/src/tui/components/StatusLine.jsx +7 -6
- package/src/tui/dist/index.mjs +225 -90
- package/src/tui/engine/turn.mjs +1 -1
- package/src/tui/index.jsx +3 -2
- package/src/tui/markdown/streaming-markdown.mjs +35 -9
- package/src/tui/statusline-ansi-bridge.mjs +17 -7
- package/src/ui/ansi.mjs +85 -5
- package/src/ui/statusline-format.mjs +7 -7
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
`Task:`. Never infer exactness from a task name, file count, or perceived
|
|
9
9
|
difficulty.
|
|
10
10
|
- All other fields are optional task-specific deltas: `Anchors:`
|
|
11
|
-
`Allow/Forbid:` `Deliver
|
|
11
|
+
`Allow/Forbid:` `Deliver:`. Omit any that add no information.
|
|
12
12
|
- Anchors: `file:line` + one-line conclusion; never log/code bodies. Specify
|
|
13
13
|
outcome, not method unless required. `Deliver:` gives shape/size, never a long
|
|
14
|
-
handoff.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
handoff. The original request and official spec/test acceptance criteria beat
|
|
15
|
+
their brief summary.
|
|
16
|
+
- Each role independently constructs a role-appropriate, lossless `Task:` from
|
|
17
|
+
the original request and official spec/test acceptance criteria.
|
|
17
18
|
- Full brief only for fresh spawn/`respawned: true`; live follow-ups are delta.
|
|
18
19
|
Dead-tag send is cold: re-supply anchors.
|
|
19
20
|
- Never `send` mid-run; batch one follow-up after completion; interrupt only to
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { loadConfig } from '../config.mjs';
|
|
25
25
|
import { resolveRuntimeSpec } from '../config.mjs';
|
|
26
26
|
import { getHiddenAgent, resolveAgentSessionPermission } from '../internal-agents.mjs';
|
|
27
|
+
import { isKnownProvider } from '../../../../standalone/provider-admin.mjs';
|
|
27
28
|
import { prepareAgentSession } from './session-builder.mjs';
|
|
28
29
|
import {
|
|
29
30
|
askSession,
|
|
@@ -163,9 +164,30 @@ export function resolveHiddenRoleSchemaAllowedTools(hidden) {
|
|
|
163
164
|
* against config.presets for backward compatibility.
|
|
164
165
|
* - null — unresolved.
|
|
165
166
|
*
|
|
166
|
-
*
|
|
167
|
-
* agents
|
|
167
|
+
* Explore and memory hidden roles mirror public spawning precedence:
|
|
168
|
+
* `agents.<role>` (including legacy `agents.maintenance`) → workflow route →
|
|
169
|
+
* maintenance route → Main. The cycle1/2/3 agents share the memory knob via
|
|
170
|
+
* their `maintKey: 'memory'` override. Scheduler and webhook are unchanged.
|
|
168
171
|
*/
|
|
172
|
+
const DEFAULT_AGENT_ROUTE_PROVIDER = 'anthropic-oauth';
|
|
173
|
+
|
|
174
|
+
function normalizeMaintenanceCandidate(candidate, config) {
|
|
175
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return candidate || null;
|
|
176
|
+
const configuredProvider = String(config?.defaultProvider || '').trim();
|
|
177
|
+
const fallbackProvider = isKnownProvider(configuredProvider)
|
|
178
|
+
? configuredProvider
|
|
179
|
+
: DEFAULT_AGENT_ROUTE_PROVIDER;
|
|
180
|
+
const provider = String(candidate.provider || fallbackProvider).trim();
|
|
181
|
+
const model = String(candidate.model || '').trim();
|
|
182
|
+
if (!provider || !model) return null;
|
|
183
|
+
return {
|
|
184
|
+
provider,
|
|
185
|
+
model,
|
|
186
|
+
effort: String(candidate.effort || '').trim() || undefined,
|
|
187
|
+
fast: candidate.fast === true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfgIn = null }) {
|
|
170
192
|
if (preset) return preset;
|
|
171
193
|
if (optsPreset) return optsPreset;
|
|
@@ -175,7 +197,22 @@ export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfg
|
|
|
175
197
|
try {
|
|
176
198
|
const config = cfgIn || loadConfig({ secrets: false });
|
|
177
199
|
const maint = config?.maintenance || {};
|
|
178
|
-
|
|
200
|
+
const key = hidden.maintKey || hidden.slot;
|
|
201
|
+
const role = key === 'explore' ? 'explore' : (key === 'memory' ? 'maintainer' : '');
|
|
202
|
+
const workflowSlot = key === 'explore' ? 'explorer' : (key === 'memory' ? 'memory' : '');
|
|
203
|
+
if (!role) return maint[key] ?? null;
|
|
204
|
+
const candidates = [
|
|
205
|
+
role ? config?.agents?.[role] : null,
|
|
206
|
+
key === 'memory' ? config?.agents?.maintenance : null,
|
|
207
|
+
workflowSlot ? config?.workflowRoutes?.[workflowSlot] : null,
|
|
208
|
+
maint[key],
|
|
209
|
+
role ? config?.default : null,
|
|
210
|
+
];
|
|
211
|
+
for (const candidate of candidates) {
|
|
212
|
+
const route = normalizeMaintenanceCandidate(candidate, config);
|
|
213
|
+
if (route) return route;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
179
216
|
} catch { return null; }
|
|
180
217
|
}
|
|
181
218
|
return null;
|
|
@@ -7,6 +7,20 @@ import {
|
|
|
7
7
|
hasGrokOAuthCredentials,
|
|
8
8
|
} from './providers/oauth-credential-probes.mjs';
|
|
9
9
|
|
|
10
|
+
// Keep config loading free of standalone provider-admin and its provider
|
|
11
|
+
// runtimes. These identifiers mirror that module's static catalog, assembled
|
|
12
|
+
// solely from the lightweight config/provider constants already imported here.
|
|
13
|
+
const CONFIG_PROVIDER_IDS = new Set([
|
|
14
|
+
...Object.keys(AGENT_PROVIDER_ENV),
|
|
15
|
+
...Object.keys(OPENAI_COMPAT_PRESETS),
|
|
16
|
+
'openai-oauth',
|
|
17
|
+
'anthropic-oauth',
|
|
18
|
+
'grok-oauth',
|
|
19
|
+
]);
|
|
20
|
+
function isConfiguredProviderId(provider) {
|
|
21
|
+
return CONFIG_PROVIDER_IDS.has(String(provider || '').trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
// Thin wrapper around resolvePluginData so callers in this orchestrator tree
|
|
11
25
|
// can import a single helper without reaching into shared/.
|
|
12
26
|
export function getPluginData() {
|
|
@@ -18,15 +32,12 @@ export function getPluginData() {
|
|
|
18
32
|
// Canonical maintenance defaults. Single source of truth — imported by
|
|
19
33
|
// llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
|
|
20
34
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// memory
|
|
27
|
-
// preset knob — the cycle agents stay separate (cycle1/2/3-agent, distinct
|
|
28
|
-
// slots and invokedBy) but resolve their model from `maint.memory` via the
|
|
29
|
-
// `maintKey` override on their hidden-role entries.
|
|
35
|
+
// Explore and Maintainer start without a route so they dynamically inherit the
|
|
36
|
+
// Main route. Explicit `maintenance.explore` / `maintenance.memory` choices
|
|
37
|
+
// remain supported. The memory cycles (chunker / re-scorer / core reviewer)
|
|
38
|
+
// share the optional `memory` preset knob — the cycle agents stay separate
|
|
39
|
+
// (cycle1/2/3-agent, distinct slots and invokedBy) but resolve their model from
|
|
40
|
+
// `maint.memory` via the `maintKey` override on their hidden-role entries.
|
|
30
41
|
// scheduler/webhook still let a per-entry config.json model win first (the
|
|
31
42
|
// caller passes it explicitly via opts.preset); the haiku default below only
|
|
32
43
|
// applies when an entry omits its own model.
|
|
@@ -129,8 +140,6 @@ const _HAIKU_ROUTE = Object.freeze({
|
|
|
129
140
|
model: resolveAnthropicFamilyModel('haiku'),
|
|
130
141
|
});
|
|
131
142
|
export const DEFAULT_MAINTENANCE = Object.freeze({
|
|
132
|
-
explore: { ..._HAIKU_ROUTE },
|
|
133
|
-
memory: { ..._HAIKU_ROUTE },
|
|
134
143
|
scheduler: { ..._HAIKU_ROUTE },
|
|
135
144
|
webhook: { ..._HAIKU_ROUTE },
|
|
136
145
|
});
|
|
@@ -201,12 +210,18 @@ function normalizeSearchRoute(route) {
|
|
|
201
210
|
// is resolved against the config.presets array (the legacy lookup) and rewritten
|
|
202
211
|
// to a route. Unresolvable strings are dropped so the DEFAULT_MAINTENANCE route
|
|
203
212
|
// fills the slot. `presets` is the normalized preset array for legacy lookup.
|
|
204
|
-
function migrateMaintenanceRoutes(rawMaint, presets) {
|
|
213
|
+
function migrateMaintenanceRoutes(rawMaint, presets, defaultProvider) {
|
|
205
214
|
const out = {};
|
|
206
215
|
const list = Array.isArray(presets) ? presets : [];
|
|
216
|
+
const configuredProvider = String(defaultProvider || '').trim();
|
|
217
|
+
const fallbackProvider = isConfiguredProviderId(configuredProvider)
|
|
218
|
+
? configuredProvider
|
|
219
|
+
: 'anthropic-oauth';
|
|
207
220
|
for (const [slot, value] of Object.entries(rawMaint || {})) {
|
|
208
221
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
209
|
-
const provider = normalizeAgentProviderId(
|
|
222
|
+
const provider = normalizeAgentProviderId(
|
|
223
|
+
value.provider || ((slot === 'explore' || slot === 'memory') ? fallbackProvider : ''),
|
|
224
|
+
);
|
|
210
225
|
const model = String(value.model || '').trim();
|
|
211
226
|
if (provider && model) {
|
|
212
227
|
const route = { provider, model };
|
|
@@ -437,7 +452,7 @@ export function loadConfig(options = {}) {
|
|
|
437
452
|
delete workflowRoutes.search;
|
|
438
453
|
// Migrate legacy preset-name maintenance slots to direct routes,
|
|
439
454
|
// then overlay onto the route-shaped defaults.
|
|
440
|
-
const migratedMaint = migrateMaintenanceRoutes(rawMaint, normalizedPresets);
|
|
455
|
+
const migratedMaint = migrateMaintenanceRoutes(rawMaint, normalizedPresets, raw.defaultProvider);
|
|
441
456
|
return {
|
|
442
457
|
providers: mergedProviders,
|
|
443
458
|
mcpServers,
|
|
@@ -22,7 +22,7 @@ import { randomBytes, createHash } from 'crypto';
|
|
|
22
22
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
23
23
|
import { join } from 'path';
|
|
24
24
|
import { getPluginData } from '../config.mjs';
|
|
25
|
-
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
25
|
+
import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
|
|
26
26
|
import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
|
|
27
27
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
28
28
|
import { makeModelCache } from './model-cache.mjs';
|
|
@@ -180,6 +180,10 @@ function getOwnTokenPath() {
|
|
|
180
180
|
return join(dir, 'grok-oauth.json');
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function getRefreshLockPath() {
|
|
184
|
+
return `${getOwnTokenPath()}.refresh.lock`;
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
// expires_at may arrive as a unix number or an ISO-8601 string. Normalize both
|
|
184
188
|
// to epoch milliseconds; 0 means unknown.
|
|
185
189
|
function _normalizeExpiresAt(value) {
|
|
@@ -369,32 +373,53 @@ async function _postRefresh(tokens) {
|
|
|
369
373
|
}
|
|
370
374
|
}
|
|
371
375
|
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// (now-stale) refresh_token; on invalid_grant, re-read once and retry with a
|
|
376
|
-
// peer-rotated refresh_token before propagating.
|
|
376
|
+
// Mixdog processes share one grok-oauth.json and xAI rotates refresh tokens
|
|
377
|
+
// single-use. Hold a cross-process lease across the re-read, exchange, and
|
|
378
|
+
// atomic save so only one process can spend a generation.
|
|
377
379
|
async function refreshTokens(tokens, { force = false } = {}) {
|
|
378
380
|
if (!tokens?.refresh_token) {
|
|
379
381
|
throw new Error('[grok-oauth] refresh token not available — open /providers in mixdog to sign in again');
|
|
380
382
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
383
|
+
|
|
384
|
+
return withFileLock(getRefreshLockPath(), async () => {
|
|
385
|
+
const validAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
386
|
+
const disk = _loadOwnTokens();
|
|
387
|
+
// A waiter that entered with the prior generation adopts the winner's
|
|
388
|
+
// persisted, valid token instead of rotating it again. xAI may rotate
|
|
389
|
+
// only the refresh token while reissuing the same access token.
|
|
390
|
+
const diskGenerationChanged = disk?.access_token
|
|
391
|
+
&& (disk.access_token !== tokens.access_token
|
|
392
|
+
|| disk.refresh_token !== tokens.refresh_token);
|
|
393
|
+
if (diskGenerationChanged
|
|
394
|
+
&& (!disk.expires_at || disk.expires_at >= validAfter)) {
|
|
395
|
+
return disk;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const current = disk || tokens;
|
|
399
|
+
try {
|
|
400
|
+
return await _postRefresh(current);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err?.isInvalidGrant) {
|
|
403
|
+
// A writer that does not participate in this lease may still
|
|
404
|
+
// have won the rotation while the request was in flight.
|
|
405
|
+
// Adopt its valid generation; only exchange it when it cannot
|
|
406
|
+
// satisfy this caller.
|
|
407
|
+
const rotated = _loadOwnTokens();
|
|
408
|
+
if (rotated?.refresh_token && rotated.refresh_token !== current.refresh_token) {
|
|
409
|
+
if (rotated.access_token
|
|
410
|
+
&& (!rotated.expires_at || rotated.expires_at >= validAfter)) {
|
|
411
|
+
return rotated;
|
|
412
|
+
}
|
|
413
|
+
return await _postRefresh(rotated);
|
|
414
|
+
}
|
|
394
415
|
}
|
|
416
|
+
throw err;
|
|
395
417
|
}
|
|
396
|
-
|
|
397
|
-
|
|
418
|
+
}, {
|
|
419
|
+
timeoutMs: 120_000,
|
|
420
|
+
staleMs: 120_000,
|
|
421
|
+
secret: true,
|
|
422
|
+
});
|
|
398
423
|
}
|
|
399
424
|
|
|
400
425
|
// --- Model catalog cache (24h disk TTL) ---
|
|
@@ -325,7 +325,11 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
325
325
|
}
|
|
326
326
|
if (!state.sawResponseCreated) {
|
|
327
327
|
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
328
|
-
|
|
328
|
+
// An abnormal close before response.created has not produced any response
|
|
329
|
+
// bytes to the caller. It is therefore safe to reconnect and replay under
|
|
330
|
+
// the normal ws_1006 bounded retry policy (text/tool emission was denied
|
|
331
|
+
// above before reaching this gate).
|
|
332
|
+
if (closeCode !== 1006 && closeCode !== 1011 && closeCode !== 1012) return null
|
|
329
333
|
}
|
|
330
334
|
if (state.userAbort) return null
|
|
331
335
|
|
|
@@ -985,10 +985,12 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
985
985
|
// the execution policy on Windows PowerShell; pwsh on macOS/Linux
|
|
986
986
|
// accepts the parameter as a no-op, so it is safe unconditionally.
|
|
987
987
|
" $argList = @('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $innerPath)",
|
|
988
|
-
//
|
|
989
|
-
//
|
|
988
|
+
// Do not set WindowStyle: Hidden creates a separate transient conhost.
|
|
989
|
+
// On Windows, explicitly reuse the wrapper's CREATE_NO_WINDOW console.
|
|
990
|
+
// NoNewWindow is unavailable on non-Windows pwsh, so only add it for
|
|
991
|
+
// Windows (where $IsWindows is absent/null in Windows PowerShell 5.1).
|
|
990
992
|
' $spArgs = @{ FilePath = $exe; ArgumentList = $argList; RedirectStandardOutput = $stdoutPath; RedirectStandardError = $stderrPath; PassThru = $true }',
|
|
991
|
-
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'
|
|
993
|
+
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'NoNewWindow\'] = $true }',
|
|
992
994
|
' $p = Start-Process @spArgs',
|
|
993
995
|
' if ($timeoutMs -gt 0 -and -not $p.WaitForExit($timeoutMs)) {',
|
|
994
996
|
// Kill the whole process TREE, not just the direct child: Start-Process
|
|
@@ -1039,8 +1041,8 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
1039
1041
|
// No `-WindowStyle Hidden` CLI switch: windowsHide:true on the spawn below
|
|
1040
1042
|
// already gives CREATE_NO_WINDOW, and the visible command-line token trips
|
|
1041
1043
|
// Defender's hidden-PowerShell dropper signature (PowhidSubExec). The
|
|
1042
|
-
// in-wrapper Start-Process
|
|
1043
|
-
//
|
|
1044
|
+
// in-wrapper Start-Process uses Windows-only NoNewWindow to reuse this
|
|
1045
|
+
// hidden console rather than creating a transient conhost window.
|
|
1044
1046
|
// `-ExecutionPolicy` only applies to Windows PowerShell; build per-platform.
|
|
1045
1047
|
const isWin = process.platform === 'win32';
|
|
1046
1048
|
const wrapperArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-07-13T06:08:53.718Z",
|
|
4
4
|
"release_tag": "runtime-v0.4.0",
|
|
5
5
|
"pg": {
|
|
6
6
|
"major": 16,
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
"assets": {
|
|
13
13
|
"darwin-arm64": {
|
|
14
14
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-darwin-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "c11f4b3046f0eeecf88790d2fff88cee1834231f96c11ec315a7a97180ea1413",
|
|
16
|
+
"size": 23397436
|
|
17
17
|
},
|
|
18
18
|
"darwin-x64": {
|
|
19
19
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-darwin-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
20
|
-
"sha256": "
|
|
21
|
-
"size":
|
|
20
|
+
"sha256": "ef93b837b1e3ea0b30591f2d31de10f9a628db88ad00183d271d539fe3cb0adb",
|
|
21
|
+
"size": 23687427
|
|
22
22
|
},
|
|
23
23
|
"linux-arm64": {
|
|
24
24
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
|
-
"sha256": "
|
|
26
|
-
"size":
|
|
25
|
+
"sha256": "288d566fd38dd80ce2dcfa68083a76b6a9400fbd8020453cf72197d3cc161315",
|
|
26
|
+
"size": 22884871
|
|
27
27
|
},
|
|
28
28
|
"linux-x64": {
|
|
29
29
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
30
|
-
"sha256": "
|
|
31
|
-
"size":
|
|
30
|
+
"sha256": "31b40d32d51afb85963e33557c3140e378b21c7cc54cec794049276a2160afd3",
|
|
31
|
+
"size": 23282291
|
|
32
32
|
},
|
|
33
33
|
"win32-x64": {
|
|
34
34
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-win32-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
35
|
-
"sha256": "
|
|
36
|
-
"size":
|
|
35
|
+
"sha256": "5d3d9d6e3ae4a93bb0bde615365bdda3ab5ff7206c0d1a066ed51b316932a8f8",
|
|
36
|
+
"size": 44037023
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -37,7 +37,6 @@ const BUNDLED_MANIFEST_PATH = fileURLToPath(new URL('../data/runtime-manifest.js
|
|
|
37
37
|
|
|
38
38
|
// GitHub raw URL fallback — used only when no cached or bundled manifest exists.
|
|
39
39
|
const MANIFEST_URL = 'https://raw.githubusercontent.com/tribgames/mixdog/main/src/runtime/memory/data/runtime-manifest.json'
|
|
40
|
-
const LEGACY_RUNTIME_RELEASE_REPOSITORY = 'trib-plugin/mixdog'
|
|
41
40
|
|
|
42
41
|
// ---------------------------------------------------------------------------
|
|
43
42
|
// Platform key
|
|
@@ -183,7 +182,7 @@ function runtimeAssetUrlCandidates(url) {
|
|
|
183
182
|
.split(/[\s,;]+/u)
|
|
184
183
|
.map((repo) => repo.trim())
|
|
185
184
|
.filter(Boolean)
|
|
186
|
-
for (const repo of
|
|
185
|
+
for (const repo of overrides) {
|
|
187
186
|
if (repo && repo !== releaseRepo) add(value.replace(`/github.com/${releaseRepo}/`, `/github.com/${repo}/`))
|
|
188
187
|
}
|
|
189
188
|
return out
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* the child exits, reporting the resolved version on success.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { dirname, join } from 'node:path';
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { basename, delimiter, dirname, join } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
20
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
@@ -199,19 +199,49 @@ export async function checkLatestVersion({ force = false, dataDir } = {}) {
|
|
|
199
199
|
* `-WindowStyle Hidden` style flags that antivirus heuristics flag).
|
|
200
200
|
*/
|
|
201
201
|
export function npmCliJsPath() {
|
|
202
|
-
const execDir = dirname(process.execPath);
|
|
203
|
-
const candidates = [
|
|
204
|
-
// Windows: npm ships beside node.exe.
|
|
205
|
-
join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
206
|
-
// Unix layout: <prefix>/bin/node → <prefix>/lib/node_modules/npm.
|
|
207
|
-
join(execDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
208
|
-
];
|
|
209
202
|
// When launched via npm itself, npm_execpath is authoritative.
|
|
210
203
|
const envPath = process.env.npm_execpath;
|
|
211
|
-
|
|
204
|
+
const candidates = envPath && /npm-cli\.js$/i.test(envPath) ? [envPath] : [];
|
|
205
|
+
const execDirs = [dirname(process.execPath)];
|
|
206
|
+
try {
|
|
207
|
+
const realExecDir = dirname(realpathSync(process.execPath));
|
|
208
|
+
if (!execDirs.includes(realExecDir)) execDirs.push(realExecDir);
|
|
209
|
+
} catch { /* retain the raw executable path */ }
|
|
210
|
+
|
|
211
|
+
for (const execDir of execDirs) {
|
|
212
|
+
// Windows: npm ships beside node.exe.
|
|
213
|
+
candidates.push(join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
214
|
+
// Unix layout: <prefix>/bin/node → <prefix>/lib/node_modules/npm.
|
|
215
|
+
candidates.push(join(execDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
216
|
+
// Homebrew: <prefix>/bin/node → <prefix>/libexec/lib/node_modules/npm.
|
|
217
|
+
candidates.push(join(execDir, '..', 'libexec', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
218
|
+
}
|
|
212
219
|
for (const candidate of candidates) {
|
|
213
220
|
try { if (existsSync(candidate)) return candidate; } catch { /* keep looking */ }
|
|
214
221
|
}
|
|
222
|
+
|
|
223
|
+
// Last resort: npm shims on PATH commonly resolve into npm's package bin dir.
|
|
224
|
+
const pathDirs = (process.env.PATH || process.env.Path || '').split(delimiter);
|
|
225
|
+
for (const pathDir of pathDirs) {
|
|
226
|
+
if (!pathDir) continue;
|
|
227
|
+
const npmPaths = process.platform === 'win32'
|
|
228
|
+
? [join(pathDir, 'npm'), join(pathDir, 'npm.cmd')]
|
|
229
|
+
: [join(pathDir, 'npm')];
|
|
230
|
+
for (const npmPath of npmPaths) {
|
|
231
|
+
try {
|
|
232
|
+
if (!existsSync(npmPath)) continue;
|
|
233
|
+
const resolvedNpm = realpathSync(npmPath);
|
|
234
|
+
const npmBinDir = dirname(resolvedNpm);
|
|
235
|
+
if (
|
|
236
|
+
basename(npmBinDir) !== 'bin'
|
|
237
|
+
|| basename(dirname(npmBinDir)) !== 'npm'
|
|
238
|
+
|| basename(dirname(dirname(npmBinDir))) !== 'node_modules'
|
|
239
|
+
) continue;
|
|
240
|
+
const cliJs = join(npmBinDir, 'npm-cli.js');
|
|
241
|
+
if (existsSync(cliJs)) return cliJs;
|
|
242
|
+
} catch { /* keep looking */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
215
245
|
return null;
|
|
216
246
|
}
|
|
217
247
|
|
|
@@ -92,6 +92,70 @@ const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIME
|
|
|
92
92
|
const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 0);
|
|
93
93
|
const TAG_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
94
94
|
const MAX_TAG_TOMBSTONES = 500;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the route for a public agent spawn. Explore and Maintainer inherit
|
|
98
|
+
* Main only after their explicit agent, workflow, and maintenance routes.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveAgentSpawnPreset(config, args = {}) {
|
|
101
|
+
if (args.provider && args.model) {
|
|
102
|
+
return {
|
|
103
|
+
presetName: args.preset || '__direct__',
|
|
104
|
+
preset: {
|
|
105
|
+
id: '__direct__',
|
|
106
|
+
name: '__DIRECT__',
|
|
107
|
+
type: 'agent',
|
|
108
|
+
provider: clean(args.provider),
|
|
109
|
+
model: clean(args.model),
|
|
110
|
+
effort: clean(args.effort) || undefined,
|
|
111
|
+
fast: args.fast === true,
|
|
112
|
+
tools: 'full',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const agentName = normalizeAgentName(args.agent);
|
|
118
|
+
const configuredDefault = clean(config?.defaultProvider);
|
|
119
|
+
const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
|
|
120
|
+
? configuredDefault
|
|
121
|
+
: DEFAULT_PROVIDER;
|
|
122
|
+
const workflowSlot = agentName === 'explore' ? 'explorer'
|
|
123
|
+
: (agentName === 'maintainer' ? 'memory' : '');
|
|
124
|
+
const maintenanceSlot = agentName === 'explore' ? 'explore'
|
|
125
|
+
: (agentName === 'maintainer' ? 'memory' : '');
|
|
126
|
+
const agentRoute = !clean(args.preset)
|
|
127
|
+
? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
|
|
128
|
+
|| (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null)
|
|
129
|
+
|| normalizeAgentRoute(config?.workflowRoutes?.[workflowSlot], fallbackProvider)
|
|
130
|
+
|| normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
|
|
131
|
+
: null;
|
|
132
|
+
if (agentRoute) {
|
|
133
|
+
return {
|
|
134
|
+
presetName: agentPresetName(agentName),
|
|
135
|
+
preset: {
|
|
136
|
+
id: `agent-${agentName}`,
|
|
137
|
+
name: agentPresetName(agentName),
|
|
138
|
+
type: 'agent',
|
|
139
|
+
provider: agentRoute.provider,
|
|
140
|
+
model: agentRoute.model,
|
|
141
|
+
effort: agentRoute.effort,
|
|
142
|
+
fast: agentRoute.fast === true,
|
|
143
|
+
tools: 'full',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mainPreset = !clean(args.preset) && (agentName === 'explore' || agentName === 'maintainer')
|
|
149
|
+
? findPreset(config, config?.default)
|
|
150
|
+
: null;
|
|
151
|
+
if (mainPreset) return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
|
|
152
|
+
|
|
153
|
+
const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
|
|
154
|
+
if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
|
|
155
|
+
const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
|
|
156
|
+
if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
|
|
157
|
+
return { presetName, preset };
|
|
158
|
+
}
|
|
95
159
|
let lastSpawnStartAt = 0;
|
|
96
160
|
async function waitForSpawnStagger() {
|
|
97
161
|
if (SPAWN_STAGGER_MS <= 0) return;
|
|
@@ -741,55 +805,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
741
805
|
// chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
|
|
742
806
|
const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
|
|
743
807
|
|
|
744
|
-
function resolvePreset(config, args) {
|
|
745
|
-
if (args.provider && args.model) {
|
|
746
|
-
return {
|
|
747
|
-
presetName: args.preset || '__direct__',
|
|
748
|
-
preset: {
|
|
749
|
-
id: '__direct__',
|
|
750
|
-
name: '__DIRECT__',
|
|
751
|
-
type: 'agent',
|
|
752
|
-
provider: clean(args.provider),
|
|
753
|
-
model: clean(args.model),
|
|
754
|
-
effort: clean(args.effort) || undefined,
|
|
755
|
-
fast: args.fast === true,
|
|
756
|
-
tools: 'full',
|
|
757
|
-
},
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
const agentName = normalizeAgentName(args.agent);
|
|
762
|
-
const configuredDefault = clean(config?.defaultProvider);
|
|
763
|
-
const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
|
|
764
|
-
? configuredDefault
|
|
765
|
-
: DEFAULT_PROVIDER;
|
|
766
|
-
const agentRoute = !clean(args.preset)
|
|
767
|
-
? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
|
|
768
|
-
|| (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null))
|
|
769
|
-
: null;
|
|
770
|
-
if (agentRoute) {
|
|
771
|
-
return {
|
|
772
|
-
presetName: agentPresetName(agentName),
|
|
773
|
-
preset: {
|
|
774
|
-
id: `agent-${agentName}`,
|
|
775
|
-
name: agentPresetName(agentName),
|
|
776
|
-
type: 'agent',
|
|
777
|
-
provider: agentRoute.provider,
|
|
778
|
-
model: agentRoute.model,
|
|
779
|
-
effort: agentRoute.effort,
|
|
780
|
-
fast: agentRoute.fast === true,
|
|
781
|
-
tools: 'full',
|
|
782
|
-
},
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
|
|
787
|
-
if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
|
|
788
|
-
const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
|
|
789
|
-
if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
|
|
790
|
-
return { presetName, preset };
|
|
791
|
-
}
|
|
792
|
-
|
|
793
808
|
function list({ scanSessions = false, context = {} } = {}) {
|
|
794
809
|
refreshTagsFromSessions({ scanSessions, context });
|
|
795
810
|
const now = Date.now();
|
|
@@ -1175,7 +1190,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1175
1190
|
if (!agent) throw new Error('agent spawn: agent is required');
|
|
1176
1191
|
const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
|
|
1177
1192
|
const agentPerm = normalizeAgentPermission(agentPermission) || null;
|
|
1178
|
-
const { presetName, preset } =
|
|
1193
|
+
const { presetName, preset } = resolveAgentSpawnPreset(config, args);
|
|
1179
1194
|
await ensureProvider(config, preset.provider);
|
|
1180
1195
|
if (prepState?.timedOut) {
|
|
1181
1196
|
throw new Error('agent spawn prep timed out before session bind');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
4
|
+
import { makeAgentDispatch, resolveMaintenanceRoute } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
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';
|
|
@@ -195,10 +195,13 @@ function exploreResultCacheEnabled() {
|
|
|
195
195
|
&& !/^(?:0|false|off|no)$/i.test(String(process.env.MIXDOG_EXPLORE_RESULT_CACHE || '1'));
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
function exploreResultCacheKey({ cwd,
|
|
198
|
+
export function exploreResultCacheKey({ cwd, route, query }) {
|
|
199
199
|
return JSON.stringify({
|
|
200
200
|
cwd: String(cwd || ''),
|
|
201
|
-
|
|
201
|
+
provider: clean(route?.provider),
|
|
202
|
+
model: clean(route?.model),
|
|
203
|
+
effort: clean(route?.effort),
|
|
204
|
+
fast: route?.fast === true,
|
|
202
205
|
query: String(query || ''),
|
|
203
206
|
});
|
|
204
207
|
}
|
|
@@ -211,13 +214,14 @@ function findConfigPreset(config, presetName) {
|
|
|
211
214
|
|| null;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
export function resolveExploreRoute(config) {
|
|
218
|
+
const routeOrName = resolveMaintenanceRoute({ agent: 'explorer', config });
|
|
219
|
+
if (routeOrName && typeof routeOrName === 'object') return routeOrName;
|
|
220
|
+
return findConfigPreset(config, routeOrName);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function ensureExploreProviderReady(config, route) {
|
|
224
|
+
const provider = clean(route?.provider);
|
|
221
225
|
if (!provider) return;
|
|
222
226
|
const providers = { ...(config?.providers || {}) };
|
|
223
227
|
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
@@ -411,11 +415,8 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
411
415
|
scheduleExploreCodeGraphPrewarm(resolvedCwd);
|
|
412
416
|
scheduleExploreFindPrewarm(resolvedCwd);
|
|
413
417
|
const config = loadConfig();
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const presetName = (route && typeof route === 'object')
|
|
417
|
-
? `${clean(route.provider)}/${clean(route.model)}`
|
|
418
|
-
: (route || '');
|
|
418
|
+
const route = resolveExploreRoute(config);
|
|
419
|
+
await ensureExploreProviderReady(config, route);
|
|
419
420
|
// Turn budget is enforced BOTH ways: the prompt contract (rules/agent/
|
|
420
421
|
// 30-explorer.md, "hard max 3 tool turns, expected 1") steers the model,
|
|
421
422
|
// and maxLoopIterations mechanically backstops it — live traces showed
|
|
@@ -439,7 +440,7 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
439
440
|
// query resolves without delay regardless of index.
|
|
440
441
|
const stagger = working.length > 1 ? EXPLORE_FANOUT_STAGGER_MS : 0;
|
|
441
442
|
const settled = await Promise.allSettled(working.map((q, i) => {
|
|
442
|
-
const key = exploreResultCacheKey({ cwd: resolvedCwd,
|
|
443
|
+
const key = exploreResultCacheKey({ cwd: resolvedCwd, route, query: q });
|
|
443
444
|
return runExploreCached(key, () => {
|
|
444
445
|
const delay = (stagger > 0 && i > 0) ? new Promise((r) => setTimeout(r, stagger)) : null;
|
|
445
446
|
return delay
|